From a9d377867f59f4044299ef919ccbf7d8111d861f Mon Sep 17 00:00:00 2001 From: Andrea Cordoba Date: Mon, 10 Feb 2025 21:51:30 +0100 Subject: [PATCH 01/23] feat: add migration project modal --- .../components/ProjectEntityHeader.tsx | 24 +- .../components/ProjectEntityMigration.tsx | 490 ++++++++++++++++++ .../project/hook/useGetDockerImage.ts | 77 +++ .../features/projects/projectMigration.api.ts | 21 + .../features/projectsV2/api/projectV2.api.ts | 15 + .../projectsV2/api/projectV2.enhanced-api.ts | 3 + .../fields/ProjectVisibilityFormField.tsx | 16 +- .../options/SessionProjectDockerImage.tsx | 8 +- client/src/styles/bootstrap_ext/_button.scss | 6 + client/src/utils/helpers/EnhancedState.ts | 3 + 10 files changed, 652 insertions(+), 11 deletions(-) create mode 100644 client/src/features/project/components/ProjectEntityMigration.tsx create mode 100644 client/src/features/project/hook/useGetDockerImage.ts create mode 100644 client/src/features/projects/projectMigration.api.ts diff --git a/client/src/features/project/components/ProjectEntityHeader.tsx b/client/src/features/project/components/ProjectEntityHeader.tsx index aa94848d44..9bf01b8e6f 100644 --- a/client/src/features/project/components/ProjectEntityHeader.tsx +++ b/client/src/features/project/components/ProjectEntityHeader.tsx @@ -27,6 +27,7 @@ import { useProjectMetadataQuery, } from "../projectKg.api"; import { ProjectStatusIcon } from "./migrations/ProjectStatusIcon"; +import { ProjectEntityMigration } from "./ProjectEntityMigration"; type ProjectEntityHeaderProps = EntityHeaderProps & { defaultBranch: string; @@ -73,13 +74,20 @@ export function ProjectEntityHeader(props: ProjectEntityHeaderProps) { ); return ( - + <> + + + ); } diff --git a/client/src/features/project/components/ProjectEntityMigration.tsx b/client/src/features/project/components/ProjectEntityMigration.tsx new file mode 100644 index 0000000000..d5df4b6d35 --- /dev/null +++ b/client/src/features/project/components/ProjectEntityMigration.tsx @@ -0,0 +1,490 @@ +/*! + * Copyright 2025 - 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 { DateTime } from "luxon"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { XLg } from "react-bootstrap-icons"; +import { Controller, useForm } from "react-hook-form"; +import { Link } from "react-router-dom"; +import { + Button, + Form, + Input, + Label, + Modal, + ModalBody, + ModalFooter, + ModalHeader, +} from "reactstrap"; +import { + InfoAlert, + SuccessAlert, + WarnAlert, +} from "../../../components/Alert.jsx"; +import { RtkErrorAlert } from "../../../components/errors/RtkErrorAlert"; +import KeywordsInput from "../../../components/form-field/KeywordsInput"; +import { Loader } from "../../../components/Loader"; +import useLegacySelector from "../../../utils/customHooks/useLegacySelector.hook"; +import { useGetMigrationQuery } from "../../projects/projectMigration.api"; +import { + Description, + KeywordsList, + LegacySlug, + ProjectName, + RepositoriesList, + Repository, + Slug, + usePostProjectMigrationsMutation, + Visibility, +} from "../../projectsV2/api/projectV2.api"; +import ProjectNamespaceFormField from "../../projectsV2/fields/ProjectNamespaceFormField"; +import ProjectVisibilityFormField from "../../projectsV2/fields/ProjectVisibilityFormField"; +import { GitLabRegistryTag } from "../GitLab.types.ts"; +import { useGetDockerImage } from "../hook/useGetDockerImage"; +import { ProjectConfig } from "../project.types.ts"; + +interface ProjectMigrationForm { + name: ProjectName; + namespace: Slug; + slug: LegacySlug; + visibility: Visibility; + description?: Description; + keywords?: KeywordsList; + repositories?: Repository; + containerImage?: string; + launcherName?: string; + defaultUrl?: string; +} + +interface ProjectEntityMigrationProps { + projectId: number; + description?: { isLoading?: boolean; unavailable?: string; value: string }; + tagList: string[]; +} +export function ProjectEntityMigration({ + projectId, + description, + tagList, +}: ProjectEntityMigrationProps) { + const [isOpen, setIsOpen] = useState(false); + + const { + data: projectMigration, + isFetching: isFetchingMigrations, + isLoading: isLoadingMigrations, + refetch: refetchMigrations, + } = useGetMigrationQuery(projectId); + + const { registryTag, registryTagIsFetching, projectConfig } = + useGetDockerImage(); + + const linkToProject = useMemo(() => { + return projectMigration + ? `/v2/projects/${projectMigration.namespace}/${projectMigration.slug}` + : ""; + }, [projectMigration]); + + const projectMetadata: unknown = useLegacySelector( + (state) => state.stateModel.project.metadata + ); + + useEffect(() => { + if (!isOpen) { + refetchMigrations(); + } + }, [isOpen, refetchMigrations]); + + const toggle = useCallback(() => { + setIsOpen((open) => !open); + }, []); + + if (isFetchingMigrations || isLoadingMigrations) return ; + + if (projectMigration) + return ( + +

This project has been migrated to a newer version of Renku.

+ + Go to the 2.0 version of the project + +
+ ); + + return ( + <> + +

+ This version of Renku is deprecated. Please migrate your project to + Renku 2.0 +

+ +
+ + + ); +} +interface ProjectMetadata { + accessLevel: number; + defaultBranch: string; + externalUrl: string; + httpUrl: string; + id: string; + namespace: string; + path: string; + pathWithNamespace: string; + visibility: string; + description: string; + title: string; + tagList: string[]; +} + +function MigrationModal({ + isOpen, + toggle, + projectMetadata, + description, + tagList, + registryTag, + registryTagIsFetching, + projectConfig, +}: { + isOpen: boolean; + toggle: () => void; + projectMetadata: ProjectMetadata; + description?: string; + tagList: string[]; + registryTag?: GitLabRegistryTag; + registryTagIsFetching: boolean; + projectConfig?: ProjectConfig; +}) { + const [migrateProject, result] = usePostProjectMigrationsMutation(); + + const { + control, + formState: { errors }, + handleSubmit, + register, + setValue, + } = useForm({ + defaultValues: { + description: "", + name: projectMetadata.title, + namespace: "", + slug: projectMetadata.path, + visibility: + projectMetadata.visibility === "public" ? "public" : "private", + keywords: tagList ?? [], + repositories: projectMetadata.httpUrl ?? "", + }, + }); + + useEffect(() => { + if (description !== undefined) { + setValue("description", description); + } + }, [description, setValue]); + + useEffect(() => { + if (tagList !== undefined) { + setValue("keywords", tagList); + } + }, [tagList, setValue]); + + useEffect(() => { + if (registryTag !== undefined) { + const nowFormatted = DateTime.now().toFormat("yyyy-MM-dd HH:mm:ss"); + setValue("containerImage", registryTag.location); + setValue("launcherName", `${registryTag.location} ${nowFormatted}`); + } + }, [registryTag, setValue]); + + useEffect(() => { + if (projectConfig !== undefined) { + setValue("defaultUrl", projectConfig?.config?.sessions?.defaultUrl ?? ""); + } + }, [projectConfig, setValue]); + + const onSubmit = useCallback( + (data: ProjectMigrationForm) => { + const dataMigration = { + ...data, + repositories: [data.repositories] as RepositoriesList, + }; + migrateProject({ + projectPost: dataMigration, + v1Id: parseInt(projectMetadata.id), + }); + }, + [migrateProject, projectMetadata.id] + ); + + const [areKeywordsDirty, setKeywordsDirty] = useState(false); + + const linkToProject = useMemo(() => { + return result?.data + ? `/v2/projects/${result.data.namespace}/${result.data.slug}` + : ""; + }, [result.data]); + + const form = !result.data && ( + <> +
+ + ( + + )} + rules={{ required: true }} + /> +
Please provide a name
+
+
+ +
+
+ +
+
+ + ( + + )} + rules={{ required: true }} + /> +
Please provide a slug
+
+
+ + ( + + )} + rules={{ required: false }} + /> +
Please provide a description
+
+
+ !areKeywordsDirty, + })} + /> +
+
+ + ( + + )} + rules={{ required: true }} + /> +
Please provide a repository
+
+
+ +
+
+ + ( + + )} + rules={{ required: true }} + /> +
Please provide a container image
+
+
+ + ( + + )} + rules={{ required: true }} + /> +
Please provide a launcher name
+
+
+ + ( + + )} + rules={{ required: false }} + /> +
Please provide a default url
+
+ + ); + + const successResult = result?.data && ( + <> + +

This project has been migrated successfully migrated to Renku 2.0

+ + Go to the 2.0 version of the project + +
+ + ); + + return ( + +
+ Migrate project to Renku 2.0 + + {result.error && } + {form} + {successResult} + + + {!result.data && ( + <> + + + + )} + {result.data && ( + + )} + +
+
+ ); +} diff --git a/client/src/features/project/hook/useGetDockerImage.ts b/client/src/features/project/hook/useGetDockerImage.ts new file mode 100644 index 0000000000..dba37954b5 --- /dev/null +++ b/client/src/features/project/hook/useGetDockerImage.ts @@ -0,0 +1,77 @@ +import { skipToken } from "@reduxjs/toolkit/query"; +import useAppSelector from "../../../utils/customHooks/useAppSelector.hook"; +import useLegacySelector from "../../../utils/customHooks/useLegacySelector.hook"; +import { useDockerImageStatusStateMachine } from "../../session/components/options/SessionProjectDockerImage"; +import useDefaultBranchOption from "../../session/hooks/options/useDefaultBranchOption.hook"; +import useDefaultCommitOption from "../../session/hooks/options/useDefaultCommitOption.hook"; +import { useGetConfigQuery } from "../projectCoreApi"; +import { + useGetAllRepositoryBranchesQuery, + useGetAllRepositoryCommitsQuery, +} from "../projectGitLab.api"; +import { useCoreSupport } from "../useProjectCoreSupport"; + +export function useGetDockerImage() { + const defaultBranch = useLegacySelector( + (state) => state.stateModel.project.metadata.defaultBranch + ); + const gitLabProjectId = useLegacySelector( + (state) => state.stateModel.project.metadata.id ?? null + ); + const projectRepositoryUrl = useLegacySelector( + (state) => state.stateModel.project.metadata.externalUrl + ); + const { branch: currentBranch, commit } = useAppSelector( + ({ startSessionOptions }) => startSessionOptions + ); + + const { data: branches } = useGetAllRepositoryBranchesQuery( + gitLabProjectId + ? { + projectId: `${gitLabProjectId}`, + } + : skipToken + ); + const { data: commits } = useGetAllRepositoryCommitsQuery( + gitLabProjectId && currentBranch + ? { + branch: currentBranch, + projectId: `${gitLabProjectId}`, + } + : skipToken + ); + const { coreSupport } = useCoreSupport({ + gitUrl: projectRepositoryUrl ?? undefined, + branch: defaultBranch ?? undefined, + }); + const { + apiVersion, + backendAvailable, + computed: coreSupportComputed, + metadataVersion, + } = coreSupport; + + const { data: projectConfig } = useGetConfigQuery( + backendAvailable && coreSupportComputed && currentBranch && commit + ? { + apiVersion, + metadataVersion, + projectRepositoryUrl, + branch: currentBranch, + commit, + } + : skipToken + ); + + useDefaultBranchOption({ branches, defaultBranch }); + useDefaultCommitOption({ commits }); + const { registry, registryTag, registryTagIsFetching } = + useDockerImageStatusStateMachine(); + return { + registry, + registryTag, + registryTagIsFetching, + projectConfig, + commits, + }; +} diff --git a/client/src/features/projects/projectMigration.api.ts b/client/src/features/projects/projectMigration.api.ts new file mode 100644 index 0000000000..eb89b967dc --- /dev/null +++ b/client/src/features/projects/projectMigration.api.ts @@ -0,0 +1,21 @@ +import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/dist/query/react"; +import { Project } from "../projectsV2/api/projectV2.api"; + +export const projectMigrationApi = createApi({ + reducerPath: "renku_v1_projects", + baseQuery: fetchBaseQuery({ baseUrl: "/ui-server/api" }), + endpoints: (builder) => ({ + getMigration: builder.query({ + query: (projectId) => { + return { + url: `/data/renku_v1_projects/${projectId}/migrations`, + method: "GET", + }; + }, + }), + }), + refetchOnMountOrArgChange: 3, + keepUnusedDataFor: 0, +}); + +export const { useGetMigrationQuery } = projectMigrationApi; diff --git a/client/src/features/projectsV2/api/projectV2.api.ts b/client/src/features/projectsV2/api/projectV2.api.ts index 9d79665a8b..c5e5402d43 100644 --- a/client/src/features/projectsV2/api/projectV2.api.ts +++ b/client/src/features/projectsV2/api/projectV2.api.ts @@ -14,6 +14,16 @@ const injectedRtkApi = api.injectEndpoints({ body: queryArg.projectPost, }), }), + postProjectMigrations: build.mutation< + PostProjectsApiResponse, + PostProjectsMigrationsApiArg + >({ + query: (queryArg) => ({ + url: `/renku_v1_projects/${queryArg.v1Id}/migrations`, + method: "POST", + body: queryArg.projectPost, + }), + }), getProjectsByProjectId: build.query< GetProjectsByProjectIdApiResponse, GetProjectsByProjectIdApiArg @@ -200,6 +210,10 @@ export type PostProjectsApiResponse = export type PostProjectsApiArg = { projectPost: ProjectPost; }; +export type PostProjectsMigrationsApiArg = { + projectPost: ProjectPost; + v1Id: number; +}; export type GetProjectsByProjectIdApiResponse = /** status 200 The project */ Project; export type GetProjectsByProjectIdApiArg = { @@ -484,4 +498,5 @@ export const { useGetSessionSecretSlotsBySlotIdQuery, usePatchSessionSecretSlotsBySlotIdMutation, useDeleteSessionSecretSlotsBySlotIdMutation, + usePostProjectMigrationsMutation, } = injectedRtkApi; diff --git a/client/src/features/projectsV2/api/projectV2.enhanced-api.ts b/client/src/features/projectsV2/api/projectV2.enhanced-api.ts index 56440923f2..eb748c2718 100644 --- a/client/src/features/projectsV2/api/projectV2.enhanced-api.ts +++ b/client/src/features/projectsV2/api/projectV2.enhanced-api.ts @@ -269,6 +269,9 @@ const enhancedApi = injectedApi.enhanceEndpoints({ postProjects: { invalidatesTags: ["Project"], }, + postProjectMigrations: { + invalidatesTags: ["Project"], + }, getProjectsByProjectIdSessionSecretSlots: { providesTags: (result, _, { projectId }) => result diff --git a/client/src/features/projectsV2/fields/ProjectVisibilityFormField.tsx b/client/src/features/projectsV2/fields/ProjectVisibilityFormField.tsx index 8ea71d5d62..c70d624903 100644 --- a/client/src/features/projectsV2/fields/ProjectVisibilityFormField.tsx +++ b/client/src/features/projectsV2/fields/ProjectVisibilityFormField.tsx @@ -22,7 +22,9 @@ import type { FieldValues } from "react-hook-form"; import { Controller } from "react-hook-form"; import { Globe, Lock } from "react-bootstrap-icons"; +import { useLocation } from "react-router-dom"; import { ButtonGroup, Input, Label } from "reactstrap"; +import { isRenkuLegacy } from "../../../utils/helpers/HelperFunctionsV2"; import type { GenericProjectFormFieldProps } from "./formField.types"; export default function ProjectVisibilityFormField({ @@ -30,6 +32,8 @@ export default function ProjectVisibilityFormField({ formId, name, }: GenericProjectFormFieldProps) { + const location = useLocation(); + const isRenkuV1 = isRenkuLegacy(location.pathname); return (
- {registryTagIsFetching && } - - {details} +
@@ -402,7 +375,7 @@ function MigrationModal({ - - - - ); -} -interface ProjectMetadata { - accessLevel: number; - defaultBranch: string; - externalUrl: string; - httpUrl: string; - id: string; - namespace: string; - path: string; - pathWithNamespace: string; - visibility: string; - description: string; - title: string; - tagList: string[]; -} - -interface ProjectMigrationForm { - name: ProjectName; - namespace: Slug; - slug: LegacySlug; - visibility: Visibility; -} - -function MigrationModal({ - isOpen, - toggle, - projectMetadata, - description, - tagList, -}: { - isOpen: boolean; - toggle: () => void; - projectMetadata: ProjectMetadata; - description?: string; - tagList: string[]; -}) { - const [showDetails, setShowDetails] = useState(false); - const [migrateProject, result] = usePostProjectMigrationsMutation(); - const { - registryTag, - isFetchingData, - projectConfig, - branch, - commits, - templateName, - resourcePools, - isProjectSupported, - } = useGetSessionLauncherData(); - const { - control, - formState: { dirtyFields, errors }, - handleSubmit, - watch, - setValue, - } = useForm({ - defaultValues: { - name: projectMetadata.title, - namespace: "", - slug: projectMetadata.path, - visibility: - projectMetadata.visibility === "public" ? "public" : "private", - }, - }); - - const onToggleShowDetails = useCallback(() => { - setShowDetails((isOpen) => !isOpen); - }, []); - - const currentName = watch("name"); - const currentNamespace = watch("namespace"); - const currentSlug = watch("slug"); - useEffect(() => { - setValue("slug", slugFromTitle(currentName, true, true), { - shouldValidate: true, - }); - }, [currentName, setValue]); - const resetUrl = useCallback(() => { - setValue("slug", slugFromTitle(currentName, true, true), { - shouldValidate: true, - }); - }, [setValue, currentName]); - const url = `renkulab.io/p/${currentNamespace ?? ""}/`; - const containerImage = useMemo(() => { - return ( - projectConfig?.config?.sessions?.dockerImage ?? registryTag?.location - ); - }, [projectConfig, registryTag]); - - const defaultSessionClass = useMemo( - () => - resourcePools?.flatMap((pool) => pool.classes).find((c) => c.default) ?? - resourcePools?.find(() => true)?.classes[0] ?? - undefined, - [resourcePools] - ); - - const resourceClass = useMemo(() => { - return ( - resourcePools - ?.flatMap((pool) => pool.classes) - .find((c) => c.id == defaultSessionClass?.id && c.matching) ?? - resourcePools - ?.filter((pool) => pool.default) - .flatMap((pool) => pool.classes) - .find((c) => c.matching) ?? - resourcePools - ?.flatMap((pool) => pool.classes) - .find((c) => c.id == defaultSessionClass?.id) ?? - undefined - ); - }, [resourcePools, defaultSessionClass]); - - const onSubmit = useCallback( - (data: ProjectMigrationForm) => { - if (!containerImage) return; - const nowFormatted = DateTime.now().toFormat("yyyy-MM-dd HH:mm:ss"); - const commandFormatted = safeParseJSONStringArray(MIGRATION_COMMAND); - const argsFormatted = safeParseJSONStringArray(MIGRATION_ARGS); - - const dataMigration = { - project: { - name: data.name, - namespace: data.namespace, - slug: data.slug, - visibility: data.visibility, - description: description, - keywords: tagList, - repositories: [projectMetadata.httpUrl ?? ""] as RepositoriesList, - }, - sessionLauncher: { - containerImage, - name: `${templateName ?? data.name} ${nowFormatted}`, - defaultUrl: projectConfig?.config?.sessions?.defaultUrl ?? "", - working_directory: MIGRATION_WORKING_DIRECTORY, - mount_directory: MIGRATION_MOUNT_DIRECTORY, - port: MIGRATION_PORT, - command: commandFormatted.data, - args: argsFormatted.data, - resourceClassId: resourceClass?.id, - }, - }; - migrateProject({ - projectMigrationPost: dataMigration, - v1Id: parseInt(projectMetadata.id), - }); - }, - [ - migrateProject, - projectMetadata.id, - projectMetadata.httpUrl, - tagList, - description, - projectConfig, - containerImage, - templateName, - resourceClass, - ] - ); - - const linkToProject = useMemo(() => { - return result?.data - ? `/v2/projects/${result.data.namespace}/${result.data.slug}` - : ""; - }, [result.data]); - - const isPinnedImage = !!projectConfig?.config?.sessions?.dockerImage; - - const form = !result.data && ( - <> -
- - ( - - )} - rules={{ required: true }} - /> -
Please provide a name
-
-
- -
-
- -
-
- -
-
-
- - - -
- {!isPinnedImage && containerImage && isProjectSupported && ( -
-
- {" "} - The image for this project is used to create a session launcher and - will not update as you make additional commits. -
-
- )} - - ); - - const successResult = result?.data && ( - <> - -

This project has been migrated successfully migrated to Renku 2.0

- - Go to the 2.0 version of the project - -
- - ); - - return ( - -
- Migrate project to Renku 2.0 - - {result.error && } - {form} - {successResult} - {!containerImage && !isFetchingData && ( - - Container image not available, it is building or not exist - - )} - {!isProjectSupported && !isFetchingData && ( - - Sessions might not work. Please update the project to migrate it - to Renku 2.0. - - )} - - - {!result.data && ( - <> - - - - )} - {result.data && ( - - )} - -
-
- ); -} - -interface DetailsMigrationProps { - pinnedImage?: boolean; - commits?: GitLabRepositoryCommit[]; - containerImage?: string; - branch?: string; - fetchingSessionInfo?: boolean; - keywords?: string; - description?: string; - codeRepository: string; - resourceClass?: ResourceClass; -} -export function DetailsMigration({ - pinnedImage, - commits, - containerImage, - branch, - fetchingSessionInfo, - keywords, - description, - codeRepository, - resourceClass, -}: DetailsMigrationProps) { - const commitMessage = useMemo(() => { - return commits ? commits[0].message : ""; - }, [commits]); - const shortIdCommit = useMemo(() => { - return commits ? commits[0].short_id : undefined; - }, [commits]); - - const resourceClassData = resourceClass - ? { - gpu: resourceClass.gpu, - cpu: resourceClass.cpu, - storage: resourceClass.max_storage, - memory: resourceClass.memory, - } - : undefined; - - const detailsSession = ( -
- {pinnedImage ? ( - <> -
- {" "} - The pinned image for this project will be used to create a session - launcher.{" "} -
-
- - Container image: {containerImage} -
-
- - Resource class:{" "} - {resourceClass ? ( - <> - resourceClass.name{" "} - - - ) : ( - Not found resource class - )} -
- - ) : ( - <> -
- The latest image for this project will be used to create a session - launcher. -
-
- - Container image: {containerImage} -
-
- - Branch: {branch} -
-
- - Commit: {shortIdCommit} -{" "} - {commitMessage} -
-
- - Resource class:{" "} - {resourceClass ? ( - <> - {resourceClass?.name} |{" "} - - - ) : ( - Not found resource class - )} -
- - )} -
- ); - - return ( -
- - {detailsSession} -
- Code repository: {codeRepository} -
-
- Keywords:{" "} - {keywords ? ( - keywords - ) : ( - Not found keywords - )} -
-
- Description:{" "} - {description ? ( - description - ) : ( - Not found description - )} -
-
- ); -} diff --git a/client/src/features/project/components/projectMigration/ProjectEntityMigration.tsx b/client/src/features/project/components/projectMigration/ProjectEntityMigration.tsx new file mode 100644 index 0000000000..2c7bbcf392 --- /dev/null +++ b/client/src/features/project/components/projectMigration/ProjectEntityMigration.tsx @@ -0,0 +1,387 @@ +/*! + * Copyright 2025 - 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 { DateTime } from "luxon"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { XLg } from "react-bootstrap-icons"; +import { useForm } from "react-hook-form"; +import { Link } from "react-router-dom-v5-compat"; +import { + Button, + Form, + Modal, + ModalBody, + ModalFooter, + ModalHeader, +} from "reactstrap"; +import { + ErrorAlert, + InfoAlert, + SuccessAlert, + WarnAlert, +} from "../../../../components/Alert"; +import { RtkErrorAlert } from "../../../../components/errors/RtkErrorAlert"; +import { Loader } from "../../../../components/Loader"; +import { Links } from "../../../../utils/constants/Docs.js"; +import useLegacySelector from "../../../../utils/customHooks/useLegacySelector.hook"; +import { useGetMigrationQuery } from "../../../projects/projectMigration.api"; +import { + RepositoriesList, + usePostProjectMigrationsMutation, +} from "../../../projectsV2/api/projectV2.api"; +import { safeParseJSONStringArray } from "../../../sessionsV2/session.utils"; +import { useGetSessionLauncherData } from "../../hook/useGetSessionLauncherData"; +import { + MIGRATION_ARGS, + MIGRATION_COMMAND, + MIGRATION_MOUNT_DIRECTORY, + MIGRATION_PORT, + MIGRATION_WORKING_DIRECTORY, +} from "../../ProjectMigration.constants"; +import { getProjectV2Path } from "../../utils/projectMigration.utils"; +import { + ProjectMetadata, + ProjectMigrationForm, +} from "./ProjectMigration.types"; +import { ProjectMigrationFormInputs } from "./ProjectMigrationForm"; +import { + DetailsMigration, + DetailsNotIncludedInMigration, +} from "./ProjectMigrationDetails"; + +interface ProjectEntityMigrationProps { + projectId: number; + description?: { isLoading?: boolean; unavailable?: string; value: string }; + tagList: string[]; +} +export function ProjectEntityMigration({ + projectId, + description, + tagList, +}: ProjectEntityMigrationProps) { + const [isOpenModal, setIsOpenModal] = useState(false); + + const { + data: projectMigration, + isFetching: isFetchingMigrations, + isLoading: isLoadingMigrations, + refetch: refetchMigrations, + } = useGetMigrationQuery(projectId); + + const linkToProject = useMemo(() => { + return projectMigration + ? getProjectV2Path(projectMigration.namespace, projectMigration.slug) + : ""; + }, [projectMigration]); + + const projectMetadata: unknown = useLegacySelector( + (state) => state.stateModel.project.metadata + ); + + useEffect(() => { + if (!isOpenModal) { + refetchMigrations(); + } + }, [isOpenModal, refetchMigrations]); + + const toggle = useCallback(() => { + setIsOpenModal((open) => !open); + }, []); + + if (isFetchingMigrations || isLoadingMigrations) return ; + + if (projectMigration) + return ( + +

+ This project has been migrated to a newer version of Renku, Renku 2.0 +

+
+ + Go to the 2.0 version of the project + + + Learn more + +
+
+ ); + + return ( + <> + +

+ This version of Renku will be deprecated in the future. Please migrate + your project to Renku 2.0. +

+
+ + + Learn more + +
+
+ + + ); +} + +function MigrationModal({ + isOpen, + toggle, + projectMetadata, + description, + tagList, +}: { + isOpen: boolean; + toggle: () => void; + projectMetadata: ProjectMetadata; + description?: string; + tagList: string[]; +}) { + const { + control, + formState: { dirtyFields, errors }, + handleSubmit, + watch, + setValue, + } = useForm({ + defaultValues: { + name: projectMetadata.title, + namespace: "", + slug: projectMetadata.path, + visibility: + projectMetadata.visibility === "public" ? "public" : "private", + }, + }); + const [migrateProject, result] = usePostProjectMigrationsMutation(); + const { + registryTag, + isFetchingData, + projectConfig, + branch, + commits, + templateName, + resourcePools, + isProjectSupported, + } = useGetSessionLauncherData(); + + const isPinnedImage = !!projectConfig?.config?.sessions?.dockerImage; + + const containerImage = useMemo(() => { + return ( + projectConfig?.config?.sessions?.dockerImage ?? registryTag?.location + ); + }, [projectConfig, registryTag]); + + const defaultSessionClass = useMemo( + () => + resourcePools?.flatMap((pool) => pool.classes).find((c) => c.default) ?? + resourcePools?.find(() => true)?.classes[0] ?? + undefined, + [resourcePools] + ); + + const resourceClass = useMemo(() => { + return ( + resourcePools + ?.flatMap((pool) => pool.classes) + .find((c) => c.id == defaultSessionClass?.id && c.matching) ?? + resourcePools + ?.filter((pool) => pool.default) + .flatMap((pool) => pool.classes) + .find((c) => c.matching) ?? + resourcePools + ?.flatMap((pool) => pool.classes) + .find((c) => c.id == defaultSessionClass?.id) ?? + undefined + ); + }, [resourcePools, defaultSessionClass]); + + const linkToProject = useMemo(() => { + return result?.data + ? getProjectV2Path(result.data.namespace, result.data.slug) + : ""; + }, [result.data]); + + const onSubmit = useCallback( + (data: ProjectMigrationForm) => { + if (!containerImage) return; + const nowFormatted = DateTime.now().toFormat("yyyy-MM-dd HH:mm:ss"); + const commandFormatted = safeParseJSONStringArray(MIGRATION_COMMAND); + const argsFormatted = safeParseJSONStringArray(MIGRATION_ARGS); + + const dataMigration = { + project: { + name: data.name, + namespace: data.namespace, + slug: data.slug, + visibility: data.visibility, + description: description, + keywords: tagList, + repositories: [projectMetadata.httpUrl ?? ""] as RepositoriesList, + }, + sessionLauncher: { + containerImage, + name: `${templateName ?? data.name} ${nowFormatted}`, + defaultUrl: projectConfig?.config?.sessions?.defaultUrl ?? "", + working_directory: MIGRATION_WORKING_DIRECTORY, + mount_directory: MIGRATION_MOUNT_DIRECTORY, + port: MIGRATION_PORT, + command: commandFormatted.data, + args: argsFormatted.data, + resourceClassId: resourceClass?.id, + }, + }; + migrateProject({ + projectMigrationPost: dataMigration, + v1Id: parseInt(projectMetadata.id), + }); + }, + [ + migrateProject, + projectMetadata.id, + projectMetadata.httpUrl, + tagList, + description, + projectConfig, + containerImage, + templateName, + resourceClass, + ] + ); + + return ( + +
+ Migrate project to Renku 2.0 + + {result.error && } + {!result.data && ( + + )} + {isFetchingData && ( +
+ Loading session data... +
+ )} + {!result.data && !isFetchingData && ( + <> + + + + )} + {result?.data && ( + +

This project has been successfully migrated to Renku 2.0

+ + Go to the 2.0 version of the project + +
+ )} + {!containerImage && !isFetchingData && ( + + Container image not available, it is building or not exist + + )} + {!isProjectSupported && !isFetchingData && ( + + Sessions might not work. Please update the project to migrate it + to Renku 2.0. + + )} +
+ + {!result.data && ( + <> + + + + )} + {result.data && ( + + )} + +
+
+ ); +} diff --git a/client/src/features/project/components/projectMigration/ProjectMigration.types.ts b/client/src/features/project/components/projectMigration/ProjectMigration.types.ts new file mode 100644 index 0000000000..17de7a8368 --- /dev/null +++ b/client/src/features/project/components/projectMigration/ProjectMigration.types.ts @@ -0,0 +1,46 @@ +/*! + * Copyright 2025 - 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 { + LegacySlug, + ProjectName, + Slug, + Visibility, +} from "../../../projectsV2/api/projectV2.api.ts"; + +export interface ProjectMigrationForm { + name: ProjectName; + namespace: Slug; + slug: LegacySlug; + visibility: Visibility; +} + +export interface ProjectMetadata { + accessLevel: number; + defaultBranch: string; + externalUrl: string; + httpUrl: string; + id: string; + namespace: string; + path: string; + pathWithNamespace: string; + visibility: string; + description: string; + title: string; + tagList: string[]; +} diff --git a/client/src/features/project/components/projectMigration/ProjectMigrationDetails.tsx b/client/src/features/project/components/projectMigration/ProjectMigrationDetails.tsx new file mode 100644 index 0000000000..69f3284d58 --- /dev/null +++ b/client/src/features/project/components/projectMigration/ProjectMigrationDetails.tsx @@ -0,0 +1,276 @@ +/*! + * Copyright 2025 - 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, useMemo, useState } from "react"; +import { + BarChartSteps, + Bookmarks, + CardImage, + Check2Circle, + Database, + FileCode, + FileEarmarkExcel, + FileText, + FileX, + People, + PlayCircle, +} from "react-bootstrap-icons"; +import { Collapse } from "reactstrap"; +import { InfoAlert } from "../../../../components/Alert.jsx"; +import ChevronFlippedIcon from "../../../../components/icons/ChevronFlippedIcon"; +import { ResourceClass } from "../../../dataServices/dataServices.types"; +import { SessionRowResourceRequests } from "../../../session/components/SessionsList"; +import { GitLabRepositoryCommit } from "../../GitLab.types"; + +interface DetailsMigrationProps { + isPinnedImage?: boolean; + commits?: GitLabRepositoryCommit[]; + containerImage?: string; + branch?: string; + keywords?: string; + description?: string; + codeRepository: string; + resourceClass?: ResourceClass; + isProjectSupported: boolean; +} +export function DetailsMigration({ + isPinnedImage, + commits, + containerImage, + branch, + keywords, + description, + codeRepository, + resourceClass, + isProjectSupported, +}: DetailsMigrationProps) { + const [showDetails, setShowDetails] = useState(false); + const onToggleShowDetails = useCallback(() => { + setShowDetails((isOpen) => !isOpen); + }, []); + + const commitMessage = useMemo(() => { + return commits ? commits[0].message : ""; + }, [commits]); + const shortIdCommit = useMemo(() => { + return commits ? commits[0].short_id : undefined; + }, [commits]); + + const resourceClassData = resourceClass + ? { + gpu: resourceClass.gpu, + cpu: resourceClass.cpu, + storage: resourceClass.max_storage, + memory: resourceClass.memory, + } + : undefined; + + const containerImageInfo = ( +
+ - Container image: {containerImage} +
+ ); + const resourceClassInfo = ( +
+ - Resource class:{" "} + {resourceClass ? ( + <> + {resourceClass?.name} |{" "} + + + ) : ( + Resource class not found + )} +
+ ); + + const detailsSession = ( +
+ {isPinnedImage ? ( + <> +
+ The pinned image for this project will be used to create a session + launcher. +
+ {containerImageInfo} + {resourceClassInfo} + + ) : ( + <> +
+ The latest image for this project will be used to create a session + launcher. +
+ {containerImageInfo} +
+ - Branch: {branch} +
+
+ - Commit: {shortIdCommit} -{" "} + {commitMessage} +
+ {resourceClassInfo} + + )} +
+ ); + + const containerImageInfoAlert = ( +
+ + The image for this project is used to create a session launcher and will + not update as you make additional commits. + +
+ ); + + return ( + <> + {!isPinnedImage && + containerImage && + isProjectSupported && + !showDetails && + containerImageInfoAlert} + +
+ +
+
+ + + Code repository: + {" "} + {codeRepository} +
+
+ + Datasets & + Data in Git LFS:{" "} + + Will continue to be available via the git lfs command line +
+
+ + Session launcher{" "} + +
+ {detailsSession} + {!isPinnedImage && + containerImage && + isProjectSupported && + showDetails && + containerImageInfoAlert} +
+ + Workflows:{" "} + + Will continue to be available via the git lfs command line +
+
+ + Description: + {" "} + {description ? ( + description + ) : ( + Description not found + )} +
+
+ + Keywords: + {" "} + {keywords ? ( + keywords + ) : ( + Keywords not found + )} +
+
+
+
+ + ); +} + +export function DetailsNotIncludedInMigration() { + const [showDetails, setShowDetails] = useState(true); + const onToggleShowDetails = useCallback(() => { + setShowDetails((isOpen) => !isOpen); + }, []); + + return ( +
+ +
+ +
+
+ + Members: + {" "} + Members will not be migrated. Please add members directly to the + Renku 2.0 project. +
+
+ + Cloud storage: + {" "} + We're sorry, cloud storage migration isn't available at + the moment. Please reconfigure your cloud storage as a Renku 2.0 + Data Connector. +
+
+ + Project image: + {" "} + We're sorry, project image migration isn't available at + the moment. +
+
+
+
+
+ ); +} diff --git a/client/src/features/project/components/projectMigration/ProjectMigrationForm.tsx b/client/src/features/project/components/projectMigration/ProjectMigrationForm.tsx new file mode 100644 index 0000000000..e9db64385c --- /dev/null +++ b/client/src/features/project/components/projectMigration/ProjectMigrationForm.tsx @@ -0,0 +1,119 @@ +/*! + * Copyright 2025 - 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, useEffect } from "react"; +import { + Control, + Controller, + FieldErrors, + FieldNamesMarkedBoolean, + UseFormSetValue, + UseFormWatch, +} from "react-hook-form"; +import { Input, Label } from "reactstrap"; +import { slugFromTitle } from "../../../../utils/helpers/HelperFunctions.js"; +import ProjectNamespaceFormField from "../../../projectsV2/fields/ProjectNamespaceFormField"; +import ProjectVisibilityFormField from "../../../projectsV2/fields/ProjectVisibilityFormField"; +import SlugPreviewFormField from "../../../projectsV2/fields/SlugPreviewFormField"; +import { PROJECT_V2_PATH } from "../../utils/projectMigration.utils"; +import { ProjectMigrationForm } from "./ProjectMigration.types"; + +interface ProjectMigrationFormInputsProps { + control: Control; + errors: FieldErrors; + watch: UseFormWatch; + setValue: UseFormSetValue; + dirtyFields: Partial>>; +} +export function ProjectMigrationFormInputs({ + control, + dirtyFields, + errors, + watch, + setValue, +}: ProjectMigrationFormInputsProps) { + const currentName = watch("name"); + const currentNamespace = watch("namespace"); + const currentSlug = watch("slug"); + useEffect(() => { + setValue("slug", slugFromTitle(currentName, true, true), { + shouldValidate: true, + }); + }, [currentName, setValue]); + const resetUrl = useCallback(() => { + setValue("slug", slugFromTitle(currentName, true, true), { + shouldValidate: true, + }); + }, [setValue, currentName]); + const url = `${PROJECT_V2_PATH}${currentNamespace ?? ""}/`; + + return ( + <> +
+ + ( + + )} + rules={{ required: true }} + /> +
Please provide a name
+
+
+ +
+
+ +
+
+ +
+ + ); +} diff --git a/client/src/features/project/utils/projectMigration.utils.ts b/client/src/features/project/utils/projectMigration.utils.ts new file mode 100644 index 0000000000..796e22c422 --- /dev/null +++ b/client/src/features/project/utils/projectMigration.utils.ts @@ -0,0 +1,22 @@ +/*! + * Copyright 2025 - 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. + */ + +export const PROJECT_V2_PATH = "/v2/projects/"; +export function getProjectV2Path(namespace: string, slug: string) { + return `${PROJECT_V2_PATH}${namespace}/${slug}`; +} diff --git a/client/src/utils/constants/Docs.js b/client/src/utils/constants/Docs.js index 480938e0e7..971ac34f6c 100644 --- a/client/src/utils/constants/Docs.js +++ b/client/src/utils/constants/Docs.js @@ -47,6 +47,8 @@ const Links = { ETHZ: "https://ethz.ch/en.html", RENKU_BLOG: "https://blog.renkulab.io", RENKU_2_LEARN_MORE: "https://blog.renkulab.io/early-access", + RENKU_2_MIGRATION_INFO: + "https://blog.renkulab.io/migration-renku-v1-to-renku-v2", }; const GitlabLinks = { diff --git a/client/src/utils/helpers/HelperFunctionsV2.ts b/client/src/utils/helpers/HelperFunctionsV2.ts new file mode 100644 index 0000000000..e7883174ce --- /dev/null +++ b/client/src/utils/helpers/HelperFunctionsV2.ts @@ -0,0 +1,26 @@ +/*! + * Copyright 2025 - 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. + */ + +export function isRenkuLegacy(pathname: string | undefined) { + return ( + typeof pathname === "string" && + (!pathname.startsWith("/v2") || + pathname.startsWith("/projects") || + pathname.startsWith("/datasets")) + ); +} From 783c162db131f2473b5235a416f7f98389c1d7b9 Mon Sep 17 00:00:00 2001 From: Andrea Cordoba Date: Tue, 4 Mar 2025 11:57:35 +0100 Subject: [PATCH 14/23] fix icon datasets in migration description and styles when keywords or description are not found --- .../projectMigration/ProjectMigrationDetails.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/client/src/features/project/components/projectMigration/ProjectMigrationDetails.tsx b/client/src/features/project/components/projectMigration/ProjectMigrationDetails.tsx index 69f3284d58..63f6bf2b53 100644 --- a/client/src/features/project/components/projectMigration/ProjectMigrationDetails.tsx +++ b/client/src/features/project/components/projectMigration/ProjectMigrationDetails.tsx @@ -24,7 +24,7 @@ import { Check2Circle, Database, FileCode, - FileEarmarkExcel, + FileEarmarkRuled, FileText, FileX, People, @@ -172,7 +172,7 @@ export function DetailsMigration({
- Datasets & + Datasets & Data in Git LFS:{" "} Will continue to be available via the git lfs command line @@ -201,7 +201,9 @@ export function DetailsMigration({ {description ? ( description ) : ( - Description not found + + Description not found + )}
@@ -211,7 +213,7 @@ export function DetailsMigration({ {keywords ? ( keywords ) : ( - Keywords not found + Keywords not found )}
From 552551d514cf3711af8aa04cce34bebedf168aae Mon Sep 17 00:00:00 2001 From: Andrea Cordoba Date: Tue, 4 Mar 2025 13:42:29 +0100 Subject: [PATCH 15/23] update copy and banner links, update icon sizes --- .../ProjectEntityMigration.tsx | 12 ++++-------- .../ProjectMigrationDetails.tsx | 19 ++++++++++++++----- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/client/src/features/project/components/projectMigration/ProjectEntityMigration.tsx b/client/src/features/project/components/projectMigration/ProjectEntityMigration.tsx index 2c7bbcf392..54f7385934 100644 --- a/client/src/features/project/components/projectMigration/ProjectEntityMigration.tsx +++ b/client/src/features/project/components/projectMigration/ProjectEntityMigration.tsx @@ -117,7 +117,7 @@ export function ProjectEntityMigration({ Go to the 2.0 version of the project -

- This version of Renku will be deprecated in the future. Please migrate - your project to Renku 2.0. -

+

This project can be migrated to Renku 2.0

- Sessions might not work. Please update the project to migrate it - to Renku 2.0. + Please update this project before migrating it to Renku 2.0. )} diff --git a/client/src/features/project/components/projectMigration/ProjectMigrationDetails.tsx b/client/src/features/project/components/projectMigration/ProjectMigrationDetails.tsx index 63f6bf2b53..cb6dc62262 100644 --- a/client/src/features/project/components/projectMigration/ProjectMigrationDetails.tsx +++ b/client/src/features/project/components/projectMigration/ProjectMigrationDetails.tsx @@ -30,9 +30,11 @@ import { People, PlayCircle, } from "react-bootstrap-icons"; +import { Link } from "react-router-dom-v5-compat"; import { Collapse } from "reactstrap"; import { InfoAlert } from "../../../../components/Alert.jsx"; import ChevronFlippedIcon from "../../../../components/icons/ChevronFlippedIcon"; +import { Links } from "../../../../utils/constants/Docs.js"; import { ResourceClass } from "../../../dataServices/dataServices.types"; import { SessionRowResourceRequests } from "../../../session/components/SessionsList"; import { GitLabRepositoryCommit } from "../../GitLab.types"; @@ -133,8 +135,15 @@ export function DetailsMigration({ const containerImageInfoAlert = (
- The image for this project is used to create a session launcher and will - not update as you make additional commits. + This session image will not update as you make additional commits.{" "} + + Learn more +
); @@ -156,8 +165,8 @@ export function DetailsMigration({ )} onClick={onToggleShowDetails} > - What will be migrated{" "} - + What will be + migrated
@@ -241,7 +250,7 @@ export function DetailsNotIncludedInMigration() { )} onClick={onToggleShowDetails} > - What will NOT be migrated{" "} + What will NOT be migrated{" "}
From 561ef8639de8cb29947614265e6a6032477a6137 Mon Sep 17 00:00:00 2001 From: Andrea Cordoba Date: Tue, 4 Mar 2025 15:48:06 +0100 Subject: [PATCH 16/23] update link migration --- client/src/utils/constants/Docs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/utils/constants/Docs.js b/client/src/utils/constants/Docs.js index 971ac34f6c..1b115d9f8b 100644 --- a/client/src/utils/constants/Docs.js +++ b/client/src/utils/constants/Docs.js @@ -48,7 +48,7 @@ const Links = { RENKU_BLOG: "https://blog.renkulab.io", RENKU_2_LEARN_MORE: "https://blog.renkulab.io/early-access", RENKU_2_MIGRATION_INFO: - "https://blog.renkulab.io/migration-renku-v1-to-renku-v2", + "https://renku.notion.site/How-to-migrate-a-Renku-1-0-project-to-Renku-2-0-1ac0df2efafc80a88e58e2b3db035110", }; const GitlabLinks = { From a2319546f70f28607ed683f718ae146f2cfc61a4 Mon Sep 17 00:00:00 2001 From: Andrea Cordoba Date: Wed, 5 Mar 2025 22:43:17 +0100 Subject: [PATCH 17/23] apply code suggestions --- .../ProjectEntityMigration.tsx | 28 +- .../projectMigration/ProjectMigrationForm.tsx | 15 +- .../project/utils/projectMigration.utils.ts | 22 -- .../features/projects/projectMigration.api.ts | 21 -- .../features/projectsV2/api/projectV2.api.ts | 130 +++++--- .../projectsV2/api/projectV2.enhanced-api.ts | 3 - .../projectsV2/api/projectV2.openapi.json | 312 ++++++++++++++++++ .../fields/ProjectVisibilityFormField.tsx | 11 +- .../fields/RenkuV1FormFields.module.scss | 18 + .../projectsV2/fields/SlugFormField.tsx | 9 +- client/src/styles/bootstrap_ext/_button.scss | 14 - client/src/styles/components/_renku_form.scss | 2 +- client/src/utils/helpers/EnhancedState.ts | 3 - 13 files changed, 461 insertions(+), 127 deletions(-) delete mode 100644 client/src/features/project/utils/projectMigration.utils.ts delete mode 100644 client/src/features/projects/projectMigration.api.ts create mode 100644 client/src/features/projectsV2/fields/RenkuV1FormFields.module.scss diff --git a/client/src/features/project/components/projectMigration/ProjectEntityMigration.tsx b/client/src/features/project/components/projectMigration/ProjectEntityMigration.tsx index 54f7385934..896b828c35 100644 --- a/client/src/features/project/components/projectMigration/ProjectEntityMigration.tsx +++ b/client/src/features/project/components/projectMigration/ProjectEntityMigration.tsx @@ -21,7 +21,7 @@ import { DateTime } from "luxon"; import { useCallback, useEffect, useMemo, useState } from "react"; import { XLg } from "react-bootstrap-icons"; import { useForm } from "react-hook-form"; -import { Link } from "react-router-dom-v5-compat"; +import { generatePath, Link } from "react-router-dom-v5-compat"; import { Button, Form, @@ -38,12 +38,14 @@ import { } from "../../../../components/Alert"; import { RtkErrorAlert } from "../../../../components/errors/RtkErrorAlert"; import { Loader } from "../../../../components/Loader"; +import { ABSOLUTE_ROUTES } from "../../../../routing/routes.constants.ts"; import { Links } from "../../../../utils/constants/Docs.js"; import useLegacySelector from "../../../../utils/customHooks/useLegacySelector.hook"; -import { useGetMigrationQuery } from "../../../projects/projectMigration.api"; import { + PostRenkuV1ProjectsByV1IdMigrationsApiArg, RepositoriesList, - usePostProjectMigrationsMutation, + useGetRenkuV1ProjectsByV1IdMigrationsQuery, + usePostRenkuV1ProjectsByV1IdMigrationsMutation, } from "../../../projectsV2/api/projectV2.api"; import { safeParseJSONStringArray } from "../../../sessionsV2/session.utils"; import { useGetSessionLauncherData } from "../../hook/useGetSessionLauncherData"; @@ -54,16 +56,15 @@ import { MIGRATION_PORT, MIGRATION_WORKING_DIRECTORY, } from "../../ProjectMigration.constants"; -import { getProjectV2Path } from "../../utils/projectMigration.utils"; import { ProjectMetadata, ProjectMigrationForm, } from "./ProjectMigration.types"; -import { ProjectMigrationFormInputs } from "./ProjectMigrationForm"; import { DetailsMigration, DetailsNotIncludedInMigration, } from "./ProjectMigrationDetails"; +import { ProjectMigrationFormInputs } from "./ProjectMigrationForm"; interface ProjectEntityMigrationProps { projectId: number; @@ -82,11 +83,14 @@ export function ProjectEntityMigration({ isFetching: isFetchingMigrations, isLoading: isLoadingMigrations, refetch: refetchMigrations, - } = useGetMigrationQuery(projectId); + } = useGetRenkuV1ProjectsByV1IdMigrationsQuery({ v1Id: projectId }); const linkToProject = useMemo(() => { return projectMigration - ? getProjectV2Path(projectMigration.namespace, projectMigration.slug) + ? generatePath(ABSOLUTE_ROUTES.v2.projects.show.root, { + namespace: projectMigration.namespace, + slug: projectMigration.slug, + }) : ""; }, [projectMigration]); @@ -185,7 +189,8 @@ function MigrationModal({ projectMetadata.visibility === "public" ? "public" : "private", }, }); - const [migrateProject, result] = usePostProjectMigrationsMutation(); + const [migrateProject, result] = + usePostRenkuV1ProjectsByV1IdMigrationsMutation(); const { registryTag, isFetchingData, @@ -231,7 +236,10 @@ function MigrationModal({ const linkToProject = useMemo(() => { return result?.data - ? getProjectV2Path(result.data.namespace, result.data.slug) + ? generatePath(ABSOLUTE_ROUTES.v2.projects.show.root, { + namespace: result.data.namespace, + slug: result.data.slug, + }) : ""; }, [result.data]); @@ -267,7 +275,7 @@ function MigrationModal({ migrateProject({ projectMigrationPost: dataMigration, v1Id: parseInt(projectMetadata.id), - }); + } as PostRenkuV1ProjectsByV1IdMigrationsApiArg); }, [ migrateProject, diff --git a/client/src/features/project/components/projectMigration/ProjectMigrationForm.tsx b/client/src/features/project/components/projectMigration/ProjectMigrationForm.tsx index e9db64385c..a10820ccdf 100644 --- a/client/src/features/project/components/projectMigration/ProjectMigrationForm.tsx +++ b/client/src/features/project/components/projectMigration/ProjectMigrationForm.tsx @@ -26,14 +26,17 @@ import { UseFormSetValue, UseFormWatch, } from "react-hook-form"; +import { useLocation } from "react-router-dom"; import { Input, Label } from "reactstrap"; import { slugFromTitle } from "../../../../utils/helpers/HelperFunctions.js"; +import { isRenkuLegacy } from "../../../../utils/helpers/HelperFunctionsV2"; import ProjectNamespaceFormField from "../../../projectsV2/fields/ProjectNamespaceFormField"; import ProjectVisibilityFormField from "../../../projectsV2/fields/ProjectVisibilityFormField"; import SlugPreviewFormField from "../../../projectsV2/fields/SlugPreviewFormField"; -import { PROJECT_V2_PATH } from "../../utils/projectMigration.utils"; import { ProjectMigrationForm } from "./ProjectMigration.types"; +import styles from "../../../projectsV2/fields/RenkuV1FormFields.module.scss"; + interface ProjectMigrationFormInputsProps { control: Control; errors: FieldErrors; @@ -61,7 +64,9 @@ export function ProjectMigrationFormInputs({ shouldValidate: true, }); }, [setValue, currentName]); - const url = `${PROJECT_V2_PATH}${currentNamespace ?? ""}/`; + const url = `renkulab.io/v2/projects/${currentNamespace ?? ""}/`; + const location = useLocation(); + const isRenkuV1 = isRenkuLegacy(location.pathname); return ( <> @@ -74,7 +79,11 @@ export function ProjectMigrationFormInputs({ name="name" render={({ field }) => ( ({ - getMigration: builder.query({ - query: (projectId) => { - return { - url: `/data/renku_v1_projects/${projectId}/migrations`, - method: "GET", - }; - }, - }), - }), - refetchOnMountOrArgChange: 3, - keepUnusedDataFor: 0, -}); - -export const { useGetMigrationQuery } = projectMigrationApi; diff --git a/client/src/features/projectsV2/api/projectV2.api.ts b/client/src/features/projectsV2/api/projectV2.api.ts index be4796f0c3..82855b7227 100644 --- a/client/src/features/projectsV2/api/projectV2.api.ts +++ b/client/src/features/projectsV2/api/projectV2.api.ts @@ -14,34 +14,6 @@ const injectedRtkApi = api.injectEndpoints({ body: queryArg.projectPost, }), }), - postProjectMigrations: build.mutation< - PostProjectsApiResponse, - PostProjectsMigrationsApiArg - >({ - query: (queryArg) => ({ - url: `/renku_v1_projects/${queryArg.v1Id}/migrations`, - method: "POST", - body: { - project: queryArg.projectMigrationPost.project, - session_launcher: { - container_image: - queryArg.projectMigrationPost.sessionLauncher.containerImage, - default_url: - queryArg.projectMigrationPost.sessionLauncher.defaultUrl, - name: queryArg.projectMigrationPost.sessionLauncher.name, - working_directory: - queryArg.projectMigrationPost.sessionLauncher.workingDirectory, - mount_directory: - queryArg.projectMigrationPost.sessionLauncher.mountDirectory, - port: queryArg.projectMigrationPost.sessionLauncher.port, - command: queryArg.projectMigrationPost.sessionLauncher.command, - args: queryArg.projectMigrationPost.sessionLauncher.args, - resource_class_id: - queryArg.projectMigrationPost.sessionLauncher.resourceClassId, - }, - }, - }), - }), getProjectsByProjectId: build.query< GetProjectsByProjectIdApiResponse, GetProjectsByProjectIdApiArg @@ -71,6 +43,24 @@ const injectedRtkApi = api.injectEndpoints({ method: "DELETE", }), }), + getRenkuV1ProjectsByV1IdMigrations: build.query< + GetRenkuV1ProjectsByV1IdMigrationsApiResponse, + GetRenkuV1ProjectsByV1IdMigrationsApiArg + >({ + query: (queryArg) => ({ + url: `/renku_v1_projects/${queryArg.v1Id}/migrations`, + }), + }), + postRenkuV1ProjectsByV1IdMigrations: build.mutation< + PostRenkuV1ProjectsByV1IdMigrationsApiResponse, + PostRenkuV1ProjectsByV1IdMigrationsApiArg + >({ + query: (queryArg) => ({ + url: `/renku_v1_projects/${queryArg.v1Id}/migrations`, + method: "POST", + body: queryArg.projectMigrationPost, + }), + }), getNamespacesByNamespaceProjectsAndSlug: build.query< GetNamespacesByNamespaceProjectsAndSlugApiResponse, GetNamespacesByNamespaceProjectsAndSlugApiArg @@ -99,6 +89,14 @@ const injectedRtkApi = api.injectEndpoints({ body: queryArg.projectPost, }), }), + getProjectsByProjectIdMigrationInfo: build.query< + GetProjectsByProjectIdMigrationInfoApiResponse, + GetProjectsByProjectIdMigrationInfoApiArg + >({ + query: (queryArg) => ({ + url: `/projects/${queryArg.projectId}/migration_info`, + }), + }), getProjectsByProjectIdMembers: build.query< GetProjectsByProjectIdMembersApiResponse, GetProjectsByProjectIdMembersApiArg @@ -228,24 +226,6 @@ export type PostProjectsApiResponse = export type PostProjectsApiArg = { projectPost: ProjectPost; }; -export type PostProjectsMigrationsApiArg = { - projectMigrationPost: { - project: ProjectPost; - sessionLauncher: { - containerImage: string; - defaultUrl?: string; - name?: string; - command?: string[] | null; - args?: string[] | null; - port?: number; - workingDirectory?: string; - mountDirectory?: string; - resourceClassId?: number; - diskStorage?: number; - }; - }; - v1Id: number; -}; export type GetProjectsByProjectIdApiResponse = /** status 200 The project */ Project; export type GetProjectsByProjectIdApiArg = { @@ -265,6 +245,19 @@ export type DeleteProjectsByProjectIdApiResponse = export type DeleteProjectsByProjectIdApiArg = { projectId: Ulid; }; +export type GetRenkuV1ProjectsByV1IdMigrationsApiResponse = + /** status 200 Project exists in v2 and has been migrated */ Project; +export type GetRenkuV1ProjectsByV1IdMigrationsApiArg = { + /** The ID of the project in Renku v1 */ + v1Id: number; +}; +export type PostRenkuV1ProjectsByV1IdMigrationsApiResponse = + /** status 201 The project was created */ Project; +export type PostRenkuV1ProjectsByV1IdMigrationsApiArg = { + /** The ID of the project in Renku v1 */ + v1Id: number; + projectMigrationPost: ProjectMigrationPost; +}; export type GetNamespacesByNamespaceProjectsAndSlugApiResponse = /** status 200 The project */ Project; export type GetNamespacesByNamespaceProjectsAndSlugApiArg = { @@ -285,6 +278,11 @@ export type PostProjectsByProjectIdCopiesApiArg = { projectId: Ulid; projectPost: ProjectPost; }; +export type GetProjectsByProjectIdMigrationInfoApiResponse = + /** status 200 Project exists in v2 and is a migrated project from v1 */ ProjectMigrationInfo; +export type GetProjectsByProjectIdMigrationInfoApiArg = { + projectId: Ulid; +}; export type GetProjectsByProjectIdMembersApiResponse = /** status 200 The project's members */ ProjectMemberListResponse; export type GetProjectsByProjectIdMembersApiArg = { @@ -439,6 +437,42 @@ export type ProjectPatch = { is_template?: IsTemplate; secrets_mount_directory?: SecretsMountDirectoryPatch; }; +export type SessionName = string; +export type ContainerImage = string; +export type DefaultUrl = string; +export type EnvironmentUid = number; +export type EnvironmentGid = number; +export type EnvironmentWorkingDirectory = string; +export type EnvironmentMountDirectory = string; +export type EnvironmentPort = number; +export type EnvironmentCommand = string[]; +export type EnvironmentArgs = string[]; +export type ResourceClassId = number | null; +export type DiskStorage = number; +export type MigrationSessionLauncherPost = { + name: SessionName; + container_image: ContainerImage; + default_url?: DefaultUrl & any; + uid?: EnvironmentUid & any; + gid?: EnvironmentGid & any; + working_directory?: EnvironmentWorkingDirectory; + mount_directory?: EnvironmentMountDirectory; + port?: EnvironmentPort & any; + command?: EnvironmentCommand; + args?: EnvironmentArgs; + resource_class_id?: ResourceClassId; + disk_storage?: DiskStorage; +}; +export type ProjectMigrationPost = { + project: ProjectPost; + session_launcher?: MigrationSessionLauncherPost; +}; +export type ProjectMigrationInfo = { + project_id: Ulid; + /** The id of the project in v1 */ + v1_id: number; + launcher_id?: Ulid; +}; export type UserFirstLastName = string; export type Role = "viewer" | "editor" | "owner"; export type ProjectMemberResponse = { @@ -514,9 +548,12 @@ export const { useGetProjectsByProjectIdQuery, usePatchProjectsByProjectIdMutation, useDeleteProjectsByProjectIdMutation, + useGetRenkuV1ProjectsByV1IdMigrationsQuery, + usePostRenkuV1ProjectsByV1IdMigrationsMutation, useGetNamespacesByNamespaceProjectsAndSlugQuery, useGetProjectsByProjectIdCopiesQuery, usePostProjectsByProjectIdCopiesMutation, + useGetProjectsByProjectIdMigrationInfoQuery, useGetProjectsByProjectIdMembersQuery, usePatchProjectsByProjectIdMembersMutation, useDeleteProjectsByProjectIdMembersAndMemberIdMutation, @@ -530,5 +567,4 @@ export const { useGetSessionSecretSlotsBySlotIdQuery, usePatchSessionSecretSlotsBySlotIdMutation, useDeleteSessionSecretSlotsBySlotIdMutation, - usePostProjectMigrationsMutation, } = injectedRtkApi; diff --git a/client/src/features/projectsV2/api/projectV2.enhanced-api.ts b/client/src/features/projectsV2/api/projectV2.enhanced-api.ts index eb748c2718..56440923f2 100644 --- a/client/src/features/projectsV2/api/projectV2.enhanced-api.ts +++ b/client/src/features/projectsV2/api/projectV2.enhanced-api.ts @@ -269,9 +269,6 @@ const enhancedApi = injectedApi.enhanceEndpoints({ postProjects: { invalidatesTags: ["Project"], }, - postProjectMigrations: { - invalidatesTags: ["Project"], - }, getProjectsByProjectIdSessionSecretSlots: { providesTags: (result, _, { projectId }) => result diff --git a/client/src/features/projectsV2/api/projectV2.openapi.json b/client/src/features/projectsV2/api/projectV2.openapi.json index e7470bd48f..8c885407d3 100644 --- a/client/src/features/projectsV2/api/projectV2.openapi.json +++ b/client/src/features/projectsV2/api/projectV2.openapi.json @@ -234,6 +234,88 @@ "tags": ["projects"] } }, + "/renku_v1_projects/{v1_id}/migrations": { + "get": { + "summary": "Check if a v1 project has been migrated to v2", + "parameters": [ + { + "in": "path", + "name": "v1_id", + "required": true, + "description": "The ID of the project in Renku v1", + "schema": { + "type": "integer" + } + } + ], + "responses": { + "200": { + "description": "Project exists in v2 and has been migrated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Project" + } + } + } + }, + "404": { + "description": "No corresponding project found in v2", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "default": { + "$ref": "#/components/responses/Error" + } + }, + "tags": ["projects"] + }, + "post": { + "summary": "Create a new project migrated from Renku v1", + "parameters": [ + { + "in": "path", + "name": "v1_id", + "required": true, + "description": "The ID of the project in Renku v1", + "schema": { + "type": "integer" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectMigrationPost" + } + } + } + }, + "responses": { + "201": { + "description": "The project was created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Project" + } + } + } + }, + "default": { + "$ref": "#/components/responses/Error" + } + }, + "tags": ["projects"] + } + }, "/namespaces/{namespace}/projects/{slug}": { "get": { "summary": "Get a project by namespace and project slug", @@ -401,6 +483,47 @@ "tags": ["projects"] } }, + "/projects/{project_id}/migration_info": { + "get": { + "summary": "Check if a v2 project is a project migrated from v1", + "parameters": [ + { + "in": "path", + "name": "project_id", + "required": true, + "schema": { + "$ref": "#/components/schemas/Ulid" + } + } + ], + "responses": { + "200": { + "description": "Project exists in v2 and is a migrated project from v1", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProjectMigrationInfo" + } + } + } + }, + "404": { + "description": "No corresponding project migrated from v1", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "default": { + "$ref": "#/components/responses/Error" + } + }, + "tags": ["projects"] + } + }, "/projects/{project_id}/members": { "get": { "summary": "Get all members of a project", @@ -1024,6 +1147,178 @@ } } }, + "ProjectMigrationPost": { + "description": "Project v1 data to be migrated in Renku", + "type": "object", + "additionalProperties": false, + "properties": { + "project": { + "$ref": "#/components/schemas/ProjectPost" + }, + "session_launcher": { + "$ref": "#/components/schemas/MigrationSessionLauncherPost" + } + }, + "required": ["project"] + }, + "MigrationSessionLauncherPost": { + "description": "Data required to create a session launcher for a project migrated", + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "$ref": "#/components/schemas/SessionName" + }, + "container_image": { + "$ref": "#/components/schemas/ContainerImage" + }, + "default_url": { + "allOf": [ + { + "$ref": "#/components/schemas/DefaultUrl" + }, + { + "default": "/lab" + } + ], + "default": "/lab" + }, + "uid": { + "allOf": [ + { + "$ref": "#/components/schemas/EnvironmentUid" + }, + { + "default": 1000 + } + ], + "default": 1000 + }, + "gid": { + "allOf": [ + { + "$ref": "#/components/schemas/EnvironmentGid" + }, + { + "default": 1000 + } + ], + "default": 1000 + }, + "working_directory": { + "$ref": "#/components/schemas/EnvironmentWorkingDirectory" + }, + "mount_directory": { + "$ref": "#/components/schemas/EnvironmentMountDirectory" + }, + "port": { + "allOf": [ + { + "$ref": "#/components/schemas/EnvironmentPort" + }, + { + "default": 8080 + } + ], + "default": 8080 + }, + "command": { + "$ref": "#/components/schemas/EnvironmentCommand" + }, + "args": { + "$ref": "#/components/schemas/EnvironmentArgs" + }, + "resource_class_id": { + "$ref": "#/components/schemas/ResourceClassId" + }, + "disk_storage": { + "$ref": "#/components/schemas/DiskStorage" + } + }, + "required": ["name", "container_image"] + }, + "SessionName": { + "description": "Renku session name", + "type": "string", + "minLength": 1, + "maxLength": 99, + "example": "My Renku Session :)" + }, + "ContainerImage": { + "description": "A container image", + "type": "string", + "maxLength": 500, + "pattern": "^[a-z0-9]+((\\.|_|__|-+)[a-z0-9]+)*(\\/[a-z0-9]+((\\.|_|__|-+)[a-z0-9]+)*)*(:[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}|@sha256:[a-fA-F0-9]{64}){0,1}$", + "example": "renku/renkulab-py:3.10-0.18.1" + }, + "DefaultUrl": { + "description": "The default path to open in a session", + "type": "string", + "maxLength": 200, + "example": "/lab" + }, + "EnvironmentUid": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": true, + "maximum": 65535, + "description": "The user ID used to run the session" + }, + "EnvironmentGid": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": true, + "maximum": 65535, + "description": "The group ID used to run the session" + }, + "EnvironmentWorkingDirectory": { + "type": "string", + "description": "The location where the session will start, if left unset it will default to the session image working directory.", + "minLength": 1, + "example": "/home/jovyan/work" + }, + "EnvironmentMountDirectory": { + "type": "string", + "description": "The location where the persistent storage for the session will be mounted, usually it should be identical to or a parent of the working directory, if left unset will default to the working directory.", + "minLength": 1, + "example": "/home/jovyan/work" + }, + "EnvironmentCommand": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The command that will be run i.e. will overwrite the image Dockerfile ENTRYPOINT, equivalent to command in Kubernetes", + "minLength": 1 + }, + "EnvironmentArgs": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The arguments that will follow the command, i.e. will overwrite the image Dockerfile CMD, equivalent to args in Kubernetes", + "minLength": 1 + }, + "EnvironmentPort": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": true, + "exclusiveMaximum": true, + "maximum": 65400, + "description": "The TCP port (on any container in the session) where user requests will be routed to from the ingress" + }, + "ResourceClassId": { + "description": "The identifier of a resource class", + "type": "integer", + "default": null, + "nullable": true + }, + "DiskStorage": { + "description": "The size of disk storage for the session, in gigabytes", + "type": "integer", + "minimum": 1, + "example": 8 + }, "Ulid": { "description": "ULID identifier", "type": "string", @@ -1283,6 +1578,23 @@ } ] }, + "ProjectMigrationInfo": { + "description": "Information if a project is a migrated project", + "type": "object", + "properties": { + "project_id": { + "$ref": "#/components/schemas/Ulid" + }, + "v1_id": { + "description": "The id of the project in v1", + "type": "integer" + }, + "launcher_id": { + "$ref": "#/components/schemas/Ulid" + } + }, + "required": ["v1_id", "project_id"] + }, "ProjectPermissions": { "description": "The set of permissions on a project", "type": "object", diff --git a/client/src/features/projectsV2/fields/ProjectVisibilityFormField.tsx b/client/src/features/projectsV2/fields/ProjectVisibilityFormField.tsx index c70d624903..63d5de9803 100644 --- a/client/src/features/projectsV2/fields/ProjectVisibilityFormField.tsx +++ b/client/src/features/projectsV2/fields/ProjectVisibilityFormField.tsx @@ -27,6 +27,7 @@ import { ButtonGroup, Input, Label } from "reactstrap"; import { isRenkuLegacy } from "../../../utils/helpers/HelperFunctionsV2"; import type { GenericProjectFormFieldProps } from "./formField.types"; +import styles from "./RenkuV1FormFields.module.scss"; export default function ProjectVisibilityFormField({ control, formId, @@ -49,7 +50,10 @@ export default function ProjectVisibilityFormField({ ({ ({ compact, @@ -33,6 +36,8 @@ export default function SlugFormField({ name, url, }: SlugFormFieldProps) { + const location = useLocation(); + const isRenkuV1 = isRenkuLegacy(location.pathname); const content = ( ({ className={cx( "form-control", errors.slug && "is-invalid", - compact && "p-1" + compact && "p-1", + isRenkuV1 && styles.RenkuV1inputGroup, + isRenkuV1 && styles.RenkuV1input )} data-cy={`${entityName}-slug-input`} id={`${entityName}-slug`} diff --git a/client/src/styles/bootstrap_ext/_button.scss b/client/src/styles/bootstrap_ext/_button.scss index 40518ed95f..3e5afe4651 100644 --- a/client/src/styles/bootstrap_ext/_button.scss +++ b/client/src/styles/bootstrap_ext/_button.scss @@ -513,17 +513,3 @@ $borderRadius: 1000px; transform: scale(1); } } - -.btn-group > .btn-check:checked + .btn { - background-color: #{$rk-green} !important; - color: white !important; - border-color: #{$rk-green} !important; -} - -.input-group - > :not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not( - .valid-feedback - ):not(.invalid-tooltip):not(.invalid-feedback) { - border-top-left-radius: 0; - border-bottom-left-radius: 0; -} diff --git a/client/src/styles/components/_renku_form.scss b/client/src/styles/components/_renku_form.scss index b6a75d7094..51a7774bf1 100644 --- a/client/src/styles/components/_renku_form.scss +++ b/client/src/styles/components/_renku_form.scss @@ -31,7 +31,7 @@ } .form-control:focus { - border: 1px solid var(--bs-rk-green); + border: 1px solid var(--bs-rk-border-input-focus); box-sizing: border-box; box-shadow: 0 0 6px var(--bs-rk-shadow-focus); } diff --git a/client/src/utils/helpers/EnhancedState.ts b/client/src/utils/helpers/EnhancedState.ts index 57d0c797f1..deaf457f8a 100644 --- a/client/src/utils/helpers/EnhancedState.ts +++ b/client/src/utils/helpers/EnhancedState.ts @@ -47,7 +47,6 @@ import { datasetFormSlice } from "../../features/project/dataset"; import { projectCoreApi } from "../../features/project/projectCoreApi"; import projectGitLabApi from "../../features/project/projectGitLab.api"; import { projectKgApi } from "../../features/project/projectKg.api"; -import { projectMigrationApi } from "../../features/projects/projectMigration.api.ts"; import { projectsApi } from "../../features/projects/projects.api"; import { projectV2Api } from "../../features/projectsV2/api/projectV2.enhanced-api"; import { recentUserActivityApi } from "../../features/recentUserActivity/RecentUserActivityApi"; @@ -102,7 +101,6 @@ export const createStore = ( [projectGitLabApi.reducerPath]: projectGitLabApi.reducer, [projectKgApi.reducerPath]: projectKgApi.reducer, [projectsApi.reducerPath]: projectsApi.reducer, - [projectMigrationApi.reducerPath]: projectMigrationApi.reducer, [projectV2Api.reducerPath]: projectV2Api.reducer, [recentUserActivityApi.reducerPath]: recentUserActivityApi.reducer, [repositoriesApi.reducerPath]: repositoriesApi.reducer, @@ -141,7 +139,6 @@ export const createStore = ( .concat(projectGitLabApi.middleware) .concat(projectKgApi.middleware) .concat(projectsApi.middleware) - .concat(projectMigrationApi.middleware) .concat(projectV2Api.middleware) .concat(recentUserActivityApi.middleware) .concat(repositoriesApi.middleware) From 34def1a1aa01132bf35a2669219a3e7abab438b9 Mon Sep 17 00:00:00 2001 From: Andrea Cordoba Date: Thu, 6 Mar 2025 10:55:29 +0100 Subject: [PATCH 18/23] fix migrate project request and solve rebase conflict --- .../projectMigration/ProjectEntityMigration.tsx | 8 ++++---- .../projectMigration/ProjectMigrationDetails.tsx | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/client/src/features/project/components/projectMigration/ProjectEntityMigration.tsx b/client/src/features/project/components/projectMigration/ProjectEntityMigration.tsx index 896b828c35..1e81cfa4b7 100644 --- a/client/src/features/project/components/projectMigration/ProjectEntityMigration.tsx +++ b/client/src/features/project/components/projectMigration/ProjectEntityMigration.tsx @@ -260,16 +260,16 @@ function MigrationModal({ keywords: tagList, repositories: [projectMetadata.httpUrl ?? ""] as RepositoriesList, }, - sessionLauncher: { - containerImage, + session_launcher: { + container_image: containerImage, name: `${templateName ?? data.name} ${nowFormatted}`, - defaultUrl: projectConfig?.config?.sessions?.defaultUrl ?? "", + default_url: projectConfig?.config?.sessions?.defaultUrl ?? "", working_directory: MIGRATION_WORKING_DIRECTORY, mount_directory: MIGRATION_MOUNT_DIRECTORY, port: MIGRATION_PORT, command: commandFormatted.data, args: argsFormatted.data, - resourceClassId: resourceClass?.id, + resource_class_id: resourceClass?.id, }, }; migrateProject({ diff --git a/client/src/features/project/components/projectMigration/ProjectMigrationDetails.tsx b/client/src/features/project/components/projectMigration/ProjectMigrationDetails.tsx index cb6dc62262..6314fe5fa4 100644 --- a/client/src/features/project/components/projectMigration/ProjectMigrationDetails.tsx +++ b/client/src/features/project/components/projectMigration/ProjectMigrationDetails.tsx @@ -88,11 +88,11 @@ export function DetailsMigration({ ); const resourceClassInfo = ( -
- - Resource class:{" "} +
+ - Resource class: {resourceClass ? ( <> - {resourceClass?.name} |{" "} + {resourceClass?.name} | ) : ( From f6748bc5e43684d066279f823a90fd5beafd5a24 Mon Sep 17 00:00:00 2001 From: Andrea Cordoba Date: Wed, 12 Mar 2025 09:00:38 +0100 Subject: [PATCH 19/23] add code suggestion: - add access to migrate projects to OWNERS and ADMINS - use ExternalLinks component for Learn more buttons and links - remove unnecessary typing - fix copy in Error Alert - use params to get url project URl --- .../components/ProjectEntityHeader.tsx | 2 +- .../ProjectEntityMigration.tsx | 48 ++++++++++--------- .../ProjectMigration.types.ts | 2 +- .../ProjectMigrationDetails.tsx | 15 +++--- .../projectMigration/ProjectMigrationForm.tsx | 13 +++-- .../project/hook/useGetSessionLauncherData.ts | 2 +- 6 files changed, 44 insertions(+), 38 deletions(-) diff --git a/client/src/features/project/components/ProjectEntityHeader.tsx b/client/src/features/project/components/ProjectEntityHeader.tsx index 883794fbb3..04976529a9 100644 --- a/client/src/features/project/components/ProjectEntityHeader.tsx +++ b/client/src/features/project/components/ProjectEntityHeader.tsx @@ -84,7 +84,7 @@ export function ProjectEntityHeader(props: ProjectEntityHeaderProps) { return ( <> - {accessLevel === ACCESS_LEVELS.OWNER && visibility === "public" && ( + {accessLevel >= ACCESS_LEVELS.OWNER && visibility === "public" && ( ( + const projectMetadata = useLegacySelector( (state) => state.stateModel.project.metadata ); @@ -120,14 +123,13 @@ export function ProjectEntityMigration({ Go to the 2.0 version of the project - - Learn more - + url={Links.RENKU_2_LEARN_MORE} + />
); @@ -140,14 +142,13 @@ export function ProjectEntityMigration({ - - Learn more - + url={Links.RENKU_2_MIGRATION_INFO} + />
); @@ -246,7 +247,7 @@ function MigrationModal({ const onSubmit = useCallback( (data: ProjectMigrationForm) => { if (!containerImage) return; - const nowFormatted = DateTime.now().toFormat("yyyy-MM-dd HH:mm:ss"); + const nowFormatted = toHumanDateTime({ datetime: DateTime.now() }); const commandFormatted = safeParseJSONStringArray(MIGRATION_COMMAND); const argsFormatted = safeParseJSONStringArray(MIGRATION_ARGS); @@ -267,15 +268,15 @@ function MigrationModal({ working_directory: MIGRATION_WORKING_DIRECTORY, mount_directory: MIGRATION_MOUNT_DIRECTORY, port: MIGRATION_PORT, - command: commandFormatted.data, - args: argsFormatted.data, + command: commandFormatted.data as EnvironmentCommand, + args: argsFormatted.data as EnvironmentArgs, resource_class_id: resourceClass?.id, }, }; migrateProject({ projectMigrationPost: dataMigration, v1Id: parseInt(projectMetadata.id), - } as PostRenkuV1ProjectsByV1IdMigrationsApiArg); + }); }, [ migrateProject, @@ -346,7 +347,8 @@ function MigrationModal({ )} {!containerImage && !isFetchingData && ( - Container image not available, it is building or not exist + Container image not available, it does not exist or is currently + building. )} {!isProjectSupported && !isFetchingData && ( diff --git a/client/src/features/project/components/projectMigration/ProjectMigration.types.ts b/client/src/features/project/components/projectMigration/ProjectMigration.types.ts index 17de7a8368..45928b4ff9 100644 --- a/client/src/features/project/components/projectMigration/ProjectMigration.types.ts +++ b/client/src/features/project/components/projectMigration/ProjectMigration.types.ts @@ -21,7 +21,7 @@ import { ProjectName, Slug, Visibility, -} from "../../../projectsV2/api/projectV2.api.ts"; +} from "../../../projectsV2/api/projectV2.api"; export interface ProjectMigrationForm { name: ProjectName; diff --git a/client/src/features/project/components/projectMigration/ProjectMigrationDetails.tsx b/client/src/features/project/components/projectMigration/ProjectMigrationDetails.tsx index 6314fe5fa4..97b85d1025 100644 --- a/client/src/features/project/components/projectMigration/ProjectMigrationDetails.tsx +++ b/client/src/features/project/components/projectMigration/ProjectMigrationDetails.tsx @@ -30,9 +30,9 @@ import { People, PlayCircle, } from "react-bootstrap-icons"; -import { Link } from "react-router-dom-v5-compat"; import { Collapse } from "reactstrap"; import { InfoAlert } from "../../../../components/Alert.jsx"; +import { ExternalLink } from "../../../../components/ExternalLinks.tsx"; import ChevronFlippedIcon from "../../../../components/icons/ChevronFlippedIcon"; import { Links } from "../../../../utils/constants/Docs.js"; import { ResourceClass } from "../../../dataServices/dataServices.types"; @@ -136,14 +136,13 @@ export function DetailsMigration({
This session image will not update as you make additional commits.{" "} - - Learn more - + url={Links.RENKU_2_MIGRATION_INFO} + />
); diff --git a/client/src/features/project/components/projectMigration/ProjectMigrationForm.tsx b/client/src/features/project/components/projectMigration/ProjectMigrationForm.tsx index a10820ccdf..2fdfcb80f4 100644 --- a/client/src/features/project/components/projectMigration/ProjectMigrationForm.tsx +++ b/client/src/features/project/components/projectMigration/ProjectMigrationForm.tsx @@ -17,7 +17,7 @@ */ import cx from "classnames"; -import { useCallback, useEffect } from "react"; +import { useCallback, useContext, useEffect } from "react"; import { Control, Controller, @@ -28,6 +28,7 @@ import { } from "react-hook-form"; import { useLocation } from "react-router-dom"; import { Input, Label } from "reactstrap"; +import AppContext from "../../../../utils/context/appContext"; import { slugFromTitle } from "../../../../utils/helpers/HelperFunctions.js"; import { isRenkuLegacy } from "../../../../utils/helpers/HelperFunctionsV2"; import ProjectNamespaceFormField from "../../../projectsV2/fields/ProjectNamespaceFormField"; @@ -54,6 +55,8 @@ export function ProjectMigrationFormInputs({ const currentName = watch("name"); const currentNamespace = watch("namespace"); const currentSlug = watch("slug"); + const { params } = useContext(AppContext); + useEffect(() => { setValue("slug", slugFromTitle(currentName, true, true), { shouldValidate: true, @@ -64,7 +67,9 @@ export function ProjectMigrationFormInputs({ shouldValidate: true, }); }, [setValue, currentName]); - const url = `renkulab.io/v2/projects/${currentNamespace ?? ""}/`; + const url = `${params?.BASE_URL ?? ""}/v2/projects/${ + currentNamespace ?? "" + }/`; const location = useLocation(); const isRenkuV1 = isRenkuLegacy(location.pathname); @@ -77,12 +82,12 @@ export function ProjectMigrationFormInputs({ ( + render={({ field, fieldState: { error } }) => ( Date: Wed, 12 Mar 2025 10:26:26 +0100 Subject: [PATCH 20/23] fix learn more button color --- .../components/projectMigration/ProjectEntityMigration.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/features/project/components/projectMigration/ProjectEntityMigration.tsx b/client/src/features/project/components/projectMigration/ProjectEntityMigration.tsx index 1df414989e..1912121d47 100644 --- a/client/src/features/project/components/projectMigration/ProjectEntityMigration.tsx +++ b/client/src/features/project/components/projectMigration/ProjectEntityMigration.tsx @@ -127,7 +127,7 @@ export function ProjectEntityMigration({ role="button" showLinkIcon={true} title="Learn more" - className={cx("btn", "btn-outline-info")} + color="outline-info" url={Links.RENKU_2_LEARN_MORE} /> @@ -146,7 +146,7 @@ export function ProjectEntityMigration({ role="button" showLinkIcon={true} title="Learn more" - className={cx("btn", "btn-outline-warning")} + color="outline-warning" url={Links.RENKU_2_MIGRATION_INFO} /> From d0f1b724e7bd511590afadb8748ae25333326dbc Mon Sep 17 00:00:00 2001 From: Andrea Cordoba Date: Wed, 12 Mar 2025 11:30:35 +0100 Subject: [PATCH 21/23] fix copy workflows --- .../components/projectMigration/ProjectMigrationDetails.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/src/features/project/components/projectMigration/ProjectMigrationDetails.tsx b/client/src/features/project/components/projectMigration/ProjectMigrationDetails.tsx index 97b85d1025..7ec7789641 100644 --- a/client/src/features/project/components/projectMigration/ProjectMigrationDetails.tsx +++ b/client/src/features/project/components/projectMigration/ProjectMigrationDetails.tsx @@ -200,7 +200,8 @@ export function DetailsMigration({ Workflows:{" "} - Will continue to be available via the git lfs command line + You may continue to use Renku workflows in your session via the + CLI.
From 80cf38bb2064389f77f55ac9aedda1f388b32b50 Mon Sep 17 00:00:00 2001 From: Andrea Cordoba Date: Wed, 12 Mar 2025 13:00:33 +0100 Subject: [PATCH 22/23] remove session launcher values for migrating a v1 project, now handled in the backend --- .../project/ProjectMigration.constants.ts | 24 ---------- .../ProjectEntityMigration.tsx | 18 -------- .../features/projectsV2/api/projectV2.api.ts | 14 ------ .../projectsV2/api/projectV2.openapi.json | 45 ------------------- 4 files changed, 101 deletions(-) delete mode 100644 client/src/features/project/ProjectMigration.constants.ts diff --git a/client/src/features/project/ProjectMigration.constants.ts b/client/src/features/project/ProjectMigration.constants.ts deleted file mode 100644 index 454170a500..0000000000 --- a/client/src/features/project/ProjectMigration.constants.ts +++ /dev/null @@ -1,24 +0,0 @@ -/*! - * Copyright 2015 - 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. - */ - -export const MIGRATION_PORT = 8888; -export const MIGRATION_WORKING_DIRECTORY = "/home/jovyan/work"; //eslint-disable-line -export const MIGRATION_MOUNT_DIRECTORY = "/home/jovyan/work"; //eslint-disable-line -export const MIGRATION_ARGS = - '["jupyter server --ServerApp.ip=$RENKU_SESSION_IP --ServerApp.port=$RENKU_SESSION_PORT --ServerApp.allow_origin=* --ServerApp.base_url=$RENKU_BASE_URL_PATH --ServerApp.root_dir=$RENKU_WORKING_DIR --ServerApp.allow_remote_access=True --ContentsManager.allow_hidden=True --ServerApp.token=\\"\\" --ServerApp.password=\\"\\" "]'; -export const MIGRATION_COMMAND = '["sh","-c"]'; diff --git a/client/src/features/project/components/projectMigration/ProjectEntityMigration.tsx b/client/src/features/project/components/projectMigration/ProjectEntityMigration.tsx index 1912121d47..ee5837f316 100644 --- a/client/src/features/project/components/projectMigration/ProjectEntityMigration.tsx +++ b/client/src/features/project/components/projectMigration/ProjectEntityMigration.tsx @@ -44,21 +44,11 @@ import { Links } from "../../../../utils/constants/Docs.js"; import useLegacySelector from "../../../../utils/customHooks/useLegacySelector.hook"; import { toHumanDateTime } from "../../../../utils/helpers/DateTimeUtils"; import { - EnvironmentArgs, - EnvironmentCommand, RepositoriesList, useGetRenkuV1ProjectsByV1IdMigrationsQuery, usePostRenkuV1ProjectsByV1IdMigrationsMutation, } from "../../../projectsV2/api/projectV2.api"; -import { safeParseJSONStringArray } from "../../../sessionsV2/session.utils"; import { useGetSessionLauncherData } from "../../hook/useGetSessionLauncherData"; -import { - MIGRATION_ARGS, - MIGRATION_COMMAND, - MIGRATION_MOUNT_DIRECTORY, - MIGRATION_PORT, - MIGRATION_WORKING_DIRECTORY, -} from "../../ProjectMigration.constants"; import { ProjectMetadata, ProjectMigrationForm, @@ -248,9 +238,6 @@ function MigrationModal({ (data: ProjectMigrationForm) => { if (!containerImage) return; const nowFormatted = toHumanDateTime({ datetime: DateTime.now() }); - const commandFormatted = safeParseJSONStringArray(MIGRATION_COMMAND); - const argsFormatted = safeParseJSONStringArray(MIGRATION_ARGS); - const dataMigration = { project: { name: data.name, @@ -265,11 +252,6 @@ function MigrationModal({ container_image: containerImage, name: `${templateName ?? data.name} ${nowFormatted}`, default_url: projectConfig?.config?.sessions?.defaultUrl ?? "", - working_directory: MIGRATION_WORKING_DIRECTORY, - mount_directory: MIGRATION_MOUNT_DIRECTORY, - port: MIGRATION_PORT, - command: commandFormatted.data as EnvironmentCommand, - args: argsFormatted.data as EnvironmentArgs, resource_class_id: resourceClass?.id, }, }; diff --git a/client/src/features/projectsV2/api/projectV2.api.ts b/client/src/features/projectsV2/api/projectV2.api.ts index 82855b7227..5f1ac43905 100644 --- a/client/src/features/projectsV2/api/projectV2.api.ts +++ b/client/src/features/projectsV2/api/projectV2.api.ts @@ -440,26 +440,12 @@ export type ProjectPatch = { export type SessionName = string; export type ContainerImage = string; export type DefaultUrl = string; -export type EnvironmentUid = number; -export type EnvironmentGid = number; -export type EnvironmentWorkingDirectory = string; -export type EnvironmentMountDirectory = string; -export type EnvironmentPort = number; -export type EnvironmentCommand = string[]; -export type EnvironmentArgs = string[]; export type ResourceClassId = number | null; export type DiskStorage = number; export type MigrationSessionLauncherPost = { name: SessionName; container_image: ContainerImage; default_url?: DefaultUrl & any; - uid?: EnvironmentUid & any; - gid?: EnvironmentGid & any; - working_directory?: EnvironmentWorkingDirectory; - mount_directory?: EnvironmentMountDirectory; - port?: EnvironmentPort & any; - command?: EnvironmentCommand; - args?: EnvironmentArgs; resource_class_id?: ResourceClassId; disk_storage?: DiskStorage; }; diff --git a/client/src/features/projectsV2/api/projectV2.openapi.json b/client/src/features/projectsV2/api/projectV2.openapi.json index 8c885407d3..f09e67a3fe 100644 --- a/client/src/features/projectsV2/api/projectV2.openapi.json +++ b/client/src/features/projectsV2/api/projectV2.openapi.json @@ -1183,51 +1183,6 @@ ], "default": "/lab" }, - "uid": { - "allOf": [ - { - "$ref": "#/components/schemas/EnvironmentUid" - }, - { - "default": 1000 - } - ], - "default": 1000 - }, - "gid": { - "allOf": [ - { - "$ref": "#/components/schemas/EnvironmentGid" - }, - { - "default": 1000 - } - ], - "default": 1000 - }, - "working_directory": { - "$ref": "#/components/schemas/EnvironmentWorkingDirectory" - }, - "mount_directory": { - "$ref": "#/components/schemas/EnvironmentMountDirectory" - }, - "port": { - "allOf": [ - { - "$ref": "#/components/schemas/EnvironmentPort" - }, - { - "default": 8080 - } - ], - "default": 8080 - }, - "command": { - "$ref": "#/components/schemas/EnvironmentCommand" - }, - "args": { - "$ref": "#/components/schemas/EnvironmentArgs" - }, "resource_class_id": { "$ref": "#/components/schemas/ResourceClassId" }, From 7441a0969ab2b0a4e049706b7e65c4fa2686fded Mon Sep 17 00:00:00 2001 From: Andrea Cordoba Date: Mon, 17 Mar 2025 13:56:37 +0100 Subject: [PATCH 23/23] fix: include formId in ProjectVisibilityFormField used in Project migration form --- .../components/projectMigration/ProjectMigrationForm.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/src/features/project/components/projectMigration/ProjectMigrationForm.tsx b/client/src/features/project/components/projectMigration/ProjectMigrationForm.tsx index 2fdfcb80f4..0b9528203e 100644 --- a/client/src/features/project/components/projectMigration/ProjectMigrationForm.tsx +++ b/client/src/features/project/components/projectMigration/ProjectMigrationForm.tsx @@ -72,7 +72,7 @@ export function ProjectMigrationFormInputs({ }/`; const location = useLocation(); const isRenkuV1 = isRenkuLegacy(location.pathname); - + const formId = "project-migration-form"; return ( <>
@@ -126,6 +126,7 @@ export function ProjectMigrationFormInputs({ name="visibility" control={control} errors={errors} + formId={formId} />