+
+ 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..0b9528203e
--- /dev/null
+++ b/client/src/features/project/components/projectMigration/ProjectMigrationForm.tsx
@@ -0,0 +1,134 @@
+/*!
+ * 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, useContext, useEffect } from "react";
+import {
+ Control,
+ Controller,
+ FieldErrors,
+ FieldNamesMarkedBoolean,
+ UseFormSetValue,
+ UseFormWatch,
+} 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";
+import ProjectVisibilityFormField from "../../../projectsV2/fields/ProjectVisibilityFormField";
+import SlugPreviewFormField from "../../../projectsV2/fields/SlugPreviewFormField";
+import { ProjectMigrationForm } from "./ProjectMigration.types";
+
+import styles from "../../../projectsV2/fields/RenkuV1FormFields.module.scss";
+
+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");
+ const { params } = useContext(AppContext);
+
+ 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 = `${params?.BASE_URL ?? ""}/v2/projects/${
+ currentNamespace ?? ""
+ }/`;
+ const location = useLocation();
+ const isRenkuV1 = isRenkuLegacy(location.pathname);
+ const formId = "project-migration-form";
+ return (
+ <>
+
+
+ (
+
+ )}
+ rules={{ required: true }}
+ />
+
Please provide a name
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+}
diff --git a/client/src/features/project/hook/useGetSessionLauncherData.ts b/client/src/features/project/hook/useGetSessionLauncherData.ts
new file mode 100644
index 0000000000..fde63ade88
--- /dev/null
+++ b/client/src/features/project/hook/useGetSessionLauncherData.ts
@@ -0,0 +1,193 @@
+/*!
+ * 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 { skipToken } from "@reduxjs/toolkit/query";
+import { useEffect, useMemo } from "react";
+import useAppSelector from "../../../utils/customHooks/useAppSelector.hook";
+import useLegacySelector from "../../../utils/customHooks/useLegacySelector.hook";
+import { useGetResourcePoolsQuery } from "../../dataServices/computeResources.api";
+import useDefaultBranchOption from "../../session/hooks/options/useDefaultBranchOption.hook";
+import useDefaultCommitOption from "../../session/hooks/options/useDefaultCommitOption.hook";
+import {
+ ProjectMetadataParams,
+ useGetConfigQuery,
+ useProjectMetadataMutation,
+} from "../projectCoreApi";
+import projectGitLabApi, {
+ useGetAllRepositoryBranchesQuery,
+ useGetAllRepositoryCommitsQuery,
+} from "../projectGitLab.api";
+import { useCoreSupport } from "../useProjectCoreSupport";
+
+export function useGetSessionLauncherData() {
+ 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 isSupported = coreSupportComputed && backendAvailable;
+
+ const { data: projectConfig } = useGetConfigQuery(
+ backendAvailable && coreSupportComputed && currentBranch && commit
+ ? {
+ apiVersion,
+ metadataVersion,
+ projectRepositoryUrl,
+ branch: currentBranch,
+ commit,
+ }
+ : skipToken
+ );
+
+ const [projectMetadata, projectMetadataStatus] = useProjectMetadataMutation();
+
+ const { data: resourcePools, isFetching: resourcePoolsIsFetching } =
+ useGetResourcePoolsQuery(
+ projectConfig
+ ? {
+ cpuRequest: projectConfig.config.sessions?.legacyConfig?.cpuRequest,
+ gpuRequest: projectConfig.config.sessions?.legacyConfig?.gpuRequest,
+ memoryRequest:
+ projectConfig.config.sessions?.legacyConfig?.memoryRequest,
+ storageRequest: projectConfig.config.sessions?.storage,
+ }
+ : skipToken
+ );
+
+ useEffect(() => {
+ if (
+ projectRepositoryUrl &&
+ commit &&
+ backendAvailable &&
+ coreSupportComputed
+ ) {
+ const params: ProjectMetadataParams = {
+ projectRepositoryUrl: `${projectRepositoryUrl}`,
+ commitSha: commit,
+ isDelayed: false,
+ metadataVersion,
+ apiVersion,
+ };
+ projectMetadata(params);
+ }
+ }, [
+ projectRepositoryUrl,
+ commit,
+ backendAvailable,
+ coreSupportComputed,
+ apiVersion,
+ metadataVersion,
+ projectMetadata,
+ ]);
+
+ const templateName: string | undefined = useMemo(() => {
+ if (
+ !projectMetadataStatus?.error &&
+ !projectConfig?.config?.sessions?.dockerImage
+ ) {
+ const templateInfo =
+ projectMetadataStatus.data?.result?.template_info ?? "";
+ const templateParts = templateInfo.split(": ");
+ return templateParts.pop() || undefined;
+ }
+ return undefined;
+ }, [projectMetadataStatus, projectConfig]);
+
+ useDefaultBranchOption({ branches, defaultBranch });
+ useDefaultCommitOption({ commits });
+
+ const tag = useMemo(() => commit.slice(0, 7), [commit]);
+
+ const {
+ currentData: registry,
+ isFetching: renkuRegistryIsFetching,
+ error: renkuRegistryError,
+ } = projectGitLabApi.useGetRenkuRegistryQuery(
+ gitLabProjectId
+ ? {
+ projectId: `${gitLabProjectId}`,
+ }
+ : skipToken
+ );
+
+ const {
+ currentData: registryTag,
+ isFetching: registryTagIsFetching,
+ error: renkuRegistryTagError,
+ } = projectGitLabApi.useGetRegistryTagQuery(
+ gitLabProjectId && registry && tag
+ ? {
+ projectId: gitLabProjectId,
+ registryId: registry.id,
+ tag,
+ }
+ : skipToken
+ );
+
+ return {
+ registry,
+ registryTag,
+ isFetchingData:
+ renkuRegistryIsFetching ||
+ registryTagIsFetching ||
+ backendAvailable === undefined ||
+ coreSupportComputed === undefined ||
+ resourcePoolsIsFetching,
+ error: renkuRegistryError || renkuRegistryTagError,
+ projectConfig,
+ commits,
+ branch: defaultBranch,
+ templateName,
+ resourcePools,
+ isProjectSupported: isSupported,
+ };
+}
diff --git a/client/src/features/project/projectCoreApi.ts b/client/src/features/project/projectCoreApi.ts
index 73bdea9447..2ca13c71ed 100644
--- a/client/src/features/project/projectCoreApi.ts
+++ b/client/src/features/project/projectCoreApi.ts
@@ -76,6 +76,12 @@ interface UpdateConfigParams extends Omit {
};
}
+export interface ProjectMetadataParams extends CoreVersionUrl {
+ commitSha: string;
+ projectRepositoryUrl: string;
+ isDelayed: boolean;
+}
+
interface UpdateConfigResponse {
branch: string;
update: {
@@ -83,6 +89,16 @@ interface UpdateConfigResponse {
};
}
+interface ProjectMetadataResponse {
+ result?: {
+ name: string;
+ template_info: string;
+ id: string;
+ agent: string;
+ created: string;
+ };
+}
+
interface UpdateConfigRawResponse {
result?: {
config?: { [key: string]: string | null };
@@ -99,7 +115,7 @@ function urlWithQueryParams(url: string, queryParams: any) {
export const projectCoreApi = createApi({
reducerPath: "projectCore",
baseQuery: fetchBaseQuery({ baseUrl: "/ui-server/api/renku" }),
- tagTypes: ["project", "project-status", "ProjectConfig"],
+ tagTypes: ["project", "project-status", "ProjectConfig", "ProjectMetadata"],
keepUnusedDataFor: 10,
endpoints: (builder) => ({
getDatasetFiles: builder.query({
@@ -310,6 +326,39 @@ export const projectCoreApi = createApi({
{ type: "ProjectConfig", id: arg.projectRepositoryUrl },
],
}),
+ projectMetadata: builder.mutation<
+ ProjectMetadataResponse,
+ ProjectMetadataParams
+ >({
+ query: ({
+ projectRepositoryUrl,
+ commitSha,
+ isDelayed,
+ metadataVersion,
+ apiVersion,
+ }) => {
+ const body = {
+ git_url: projectRepositoryUrl,
+ commit_sha: commitSha,
+ is_delayed: isDelayed,
+ };
+ return {
+ url: versionedPathForEndpoint({
+ endpoint: "project.show",
+ metadataVersion,
+ apiVersion,
+ }),
+ method: "POST",
+ body,
+ validateStatus: (response, body) =>
+ response.status >= 200 && response.status < 300 && !body.error,
+ };
+ },
+ transformErrorResponse: (error) => transformRenkuCoreErrorResponse(error),
+ invalidatesTags: (_result, _error, arg) => [
+ { type: "ProjectMetadata", id: arg.projectRepositoryUrl },
+ ],
+ }),
}),
});
@@ -419,4 +468,5 @@ export const {
useStartMigrationMutation,
useGetConfigQuery,
useUpdateConfigMutation,
+ useProjectMetadataMutation,
} = projectCoreApi;
diff --git a/client/src/features/projectsV2/api/projectV2.api.ts b/client/src/features/projectsV2/api/projectV2.api.ts
index 9d79665a8b..5f1ac43905 100644
--- a/client/src/features/projectsV2/api/projectV2.api.ts
+++ b/client/src/features/projectsV2/api/projectV2.api.ts
@@ -43,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
@@ -71,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
@@ -219,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 = {
@@ -239,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 = {
@@ -393,6 +437,28 @@ export type ProjectPatch = {
is_template?: IsTemplate;
secrets_mount_directory?: SecretsMountDirectoryPatch;
};
+export type SessionName = string;
+export type ContainerImage = string;
+export type DefaultUrl = string;
+export type ResourceClassId = number | null;
+export type DiskStorage = number;
+export type MigrationSessionLauncherPost = {
+ name: SessionName;
+ container_image: ContainerImage;
+ default_url?: DefaultUrl & any;
+ 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 = {
@@ -468,9 +534,12 @@ export const {
useGetProjectsByProjectIdQuery,
usePatchProjectsByProjectIdMutation,
useDeleteProjectsByProjectIdMutation,
+ useGetRenkuV1ProjectsByV1IdMigrationsQuery,
+ usePostRenkuV1ProjectsByV1IdMigrationsMutation,
useGetNamespacesByNamespaceProjectsAndSlugQuery,
useGetProjectsByProjectIdCopiesQuery,
usePostProjectsByProjectIdCopiesMutation,
+ useGetProjectsByProjectIdMigrationInfoQuery,
useGetProjectsByProjectIdMembersQuery,
usePatchProjectsByProjectIdMembersMutation,
useDeleteProjectsByProjectIdMembersAndMemberIdMutation,
diff --git a/client/src/features/projectsV2/api/projectV2.openapi.json b/client/src/features/projectsV2/api/projectV2.openapi.json
index e7470bd48f..f09e67a3fe 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,133 @@
}
}
},
+ "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"
+ },
+ "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 +1533,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 8ea71d5d62..63d5de9803 100644
--- a/client/src/features/projectsV2/fields/ProjectVisibilityFormField.tsx
+++ b/client/src/features/projectsV2/fields/ProjectVisibilityFormField.tsx
@@ -22,14 +22,19 @@ 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";
+import styles from "./RenkuV1FormFields.module.scss";
export default function ProjectVisibilityFormField({
control,
formId,
name,
}: GenericProjectFormFieldProps) {
+ const location = useLocation();
+ const isRenkuV1 = isRenkuLegacy(location.pathname);
return (