diff --git a/client/src/features/ProjectPageV2/ProjectPageContent/SessionSecrets/AddSessionSecretButton.tsx b/client/src/features/ProjectPageV2/ProjectPageContent/SessionSecrets/AddSessionSecretButton.tsx index 78f7b6d71..1fb3e71a3 100644 --- a/client/src/features/ProjectPageV2/ProjectPageContent/SessionSecrets/AddSessionSecretButton.tsx +++ b/client/src/features/ProjectPageV2/ProjectPageContent/SessionSecrets/AddSessionSecretButton.tsx @@ -65,7 +65,8 @@ interface AddSessionSecretModalProps { function AddSessionSecretModal({ isOpen, toggle }: AddSessionSecretModalProps) { const { project } = useProject(); - const { id: projectId } = project; + const { id: projectId, secrets_mount_directory: secretsMountDirectory } = + project; const [postSessionSecretSlot, result] = usePostSessionSecretSlotsMutation(); @@ -132,7 +133,12 @@ function AddSessionSecretModal({ isOpen, toggle }: AddSessionSecretModalProps) { errors={errors} name="description" /> - + + + + ); +} + +interface UpdateSecretsMountDirectoryModalProps { + isOpen: boolean; + toggle: () => void; +} + +function UpdateSecretsMountDirectoryModal({ + isOpen, + toggle, +}: UpdateSecretsMountDirectoryModalProps) { + const { project } = useProject(); + const { id: projectId } = project; + + const [patchProject, result] = usePatchProjectsByProjectIdMutation(); + + const { + control, + formState: { errors, isDirty }, + handleSubmit, + reset, + } = useForm({ + defaultValues: { + secretsMountDirectory: project.secrets_mount_directory, + }, + }); + + const submitHandler = useCallback( + (data: UpdateSecretsMountDirectoryFrom) => { + patchProject({ + "If-Match": project.etag ?? "", + projectId, + projectPatch: { + secrets_mount_directory: data.secretsMountDirectory, + }, + }); + }, + [patchProject, project.etag, projectId] + ); + const onSubmit = useMemo( + () => handleSubmit(submitHandler), + [handleSubmit, submitHandler] + ); + + const onReset = useCallback(() => { + patchProject({ + "If-Match": project.etag ?? "", + projectId, + projectPatch: { + secrets_mount_directory: "", + }, + }); + }, [patchProject, project.etag, projectId]); + + useEffect(() => { + reset({ + secretsMountDirectory: project.secrets_mount_directory, + }); + }, [project, reset]); + + useEffect(() => { + if (!isOpen) { + reset(); + result.reset(); + } + }, [isOpen, reset, result]); + + useEffect(() => { + if (result.isSuccess) { + toggle(); + } + }, [result.isSuccess, toggle]); + + return ( + +
+ Update secrets mount location + +

+ Change the location where secrets will be mounted in sessions. Note + that the change will only apply to new sessions. +

+ + {result.error && ( + + )} + + +
+ + + + + +
+
+ ); +} + +interface UpdateSecretsMountDirectoryFrom { + secretsMountDirectory: string; +} diff --git a/client/src/features/ProjectPageV2/ProjectPageContent/SessionSecrets/fields/FilenameField.tsx b/client/src/features/ProjectPageV2/ProjectPageContent/SessionSecrets/fields/FilenameField.tsx index 8b4b71e5a..e58ee1d8b 100644 --- a/client/src/features/ProjectPageV2/ProjectPageContent/SessionSecrets/fields/FilenameField.tsx +++ b/client/src/features/ProjectPageV2/ProjectPageContent/SessionSecrets/fields/FilenameField.tsx @@ -17,21 +17,30 @@ */ import cx from "classnames"; -import { Controller, type FieldValues } from "react-hook-form"; +import { Controller, useWatch, type FieldValues } from "react-hook-form"; import { FormText, Input, Label } from "reactstrap"; import type { SessionSecretFormFieldProps } from "./fields.types"; -type FilenameFieldProps = SessionSecretFormFieldProps; +interface FilenameFieldProps + extends SessionSecretFormFieldProps { + secretsMountDirectory: string; +} export default function FilenameField({ control, errors, name, + secretsMountDirectory, }: FilenameFieldProps) { const fieldId = `session-secret-${name}`; const fieldHelpId = `${fieldId}-help`; + const watch = useWatch({ control, name }); + const fullPath = watch + ? `${secretsMountDirectory}/${watch}` + : `${secretsMountDirectory}/`; + return (
@@ -66,8 +75,14 @@ export default function FilenameField({ )}
- This is the filename which will be used when mounting the secret inside - sessions. +

+ This is the filename which will be used when mounting the secret + inside sessions. +

+

+ The secret will be populated at:{" "} + {fullPath}. +

); diff --git a/client/src/features/ProjectPageV2/ProjectPageContent/SessionSecrets/sessionSecrets.constants.ts b/client/src/features/ProjectPageV2/ProjectPageContent/SessionSecrets/sessionSecrets.constants.ts new file mode 100644 index 000000000..9f90dc089 --- /dev/null +++ b/client/src/features/ProjectPageV2/ProjectPageContent/SessionSecrets/sessionSecrets.constants.ts @@ -0,0 +1,19 @@ +/*! + * Copyright 2024 - Swiss Data Science Center (SDSC) + * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and + * Eidgenössische Technische Hochschule Zürich (ETHZ). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const SESSION_SECRETS_CARD_ID = "project-settings-session-secrets"; diff --git a/client/src/features/projectsV2/api/projectV2.api.ts b/client/src/features/projectsV2/api/projectV2.api.ts index 00c99f683..9d79665a8 100644 --- a/client/src/features/projectsV2/api/projectV2.api.ts +++ b/client/src/features/projectsV2/api/projectV2.api.ts @@ -328,6 +328,7 @@ export type Keyword = string; export type KeywordsList = Keyword[]; export type ProjectDocumentation = string; export type IsTemplate = boolean; +export type SecretsMountDirectory = string; export type Project = { id: Ulid; name: ProjectName; @@ -344,6 +345,7 @@ export type Project = { documentation?: ProjectDocumentation; template_id?: Ulid; is_template?: IsTemplate; + secrets_mount_directory: SecretsMountDirectory; }; export type ProjectsList = Project[]; export type ErrorResponse = { @@ -374,8 +376,10 @@ export type ProjectPost = { description?: Description; keywords?: KeywordsList; documentation?: ProjectDocumentation; + secrets_mount_directory?: SecretsMountDirectory; }; export type WithDocumentation = boolean; +export type SecretsMountDirectoryPatch = string; export type ProjectPatch = { name?: ProjectName; namespace?: Slug; @@ -387,6 +391,7 @@ export type ProjectPatch = { /** template_id is set when copying a project from a template project and it cannot be modified. This field can be either null or an empty string; a null value won't change it while an empty string value will delete it, meaning that the project is unlinked from its template */ template_id?: string; is_template?: IsTemplate; + secrets_mount_directory?: SecretsMountDirectoryPatch; }; export type UserFirstLastName = string; export type Role = "viewer" | "editor" | "owner"; diff --git a/client/src/features/projectsV2/api/projectV2.openapi.json b/client/src/features/projectsV2/api/projectV2.openapi.json index 61937e8b9..fe8a71495 100644 --- a/client/src/features/projectsV2/api/projectV2.openapi.json +++ b/client/src/features/projectsV2/api/projectV2.openapi.json @@ -904,6 +904,9 @@ "is_template": { "$ref": "#/components/schemas/IsTemplate", "default": false + }, + "secrets_mount_directory": { + "$ref": "#/components/schemas/SecretsMountDirectory" } }, "required": [ @@ -913,7 +916,8 @@ "slug", "created_by", "creation_date", - "visibility" + "visibility", + "secrets_mount_directory" ], "example": { "id": "01AN4Z79ZS5XN0F25N3DB94T4R", @@ -933,7 +937,8 @@ } ], "keywords": ["keyword 1", "keyword 2"], - "template_id": "01JC3CB5426KC7P5STS5X3KSS8" + "template_id": "01JC3CB5426KC7P5STS5X3KSS8", + "secrets_mount_directory": "/secrets" } }, "ProjectPost": { @@ -964,6 +969,9 @@ }, "documentation": { "$ref": "#/components/schemas/ProjectDocumentation" + }, + "secrets_mount_directory": { + "$ref": "#/components/schemas/SecretsMountDirectory" } }, "required": ["name", "namespace"] @@ -1002,6 +1010,9 @@ }, "is_template": { "$ref": "#/components/schemas/IsTemplate" + }, + "secrets_mount_directory": { + "$ref": "#/components/schemas/SecretsMountDirectoryPatch" } } }, @@ -1105,6 +1116,17 @@ "description": "Shows if a project is a template or not", "type": "boolean" }, + "SecretsMountDirectory": { + "description": "The location where the secrets will be provided inside sessions, if left unset it will default to `/secrets`.", + "type": "string", + "minLength": 1, + "default": "/secrets", + "example": "/secrets" + }, + "SecretsMountDirectoryPatch": { + "type": "string", + "example": "/secrets" + }, "ProjectMemberListPatchRequest": { "description": "List of members and their access level to the project", "type": "array", diff --git a/client/src/features/projectsV2/fields/SecretsMountDirectoryField.tsx b/client/src/features/projectsV2/fields/SecretsMountDirectoryField.tsx new file mode 100644 index 000000000..1cec5045e --- /dev/null +++ b/client/src/features/projectsV2/fields/SecretsMountDirectoryField.tsx @@ -0,0 +1,79 @@ +/*! + * Copyright 2024 - Swiss Data Science Center (SDSC) + * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and + * Eidgenössische Technische Hochschule Zürich (ETHZ). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import cx from "classnames"; +import { Controller, useWatch, type FieldValues } from "react-hook-form"; +import { FormText, Input, Label } from "reactstrap"; + +import { GenericProjectFormFieldProps } from "./formField.types"; + +type SecretsMountDirectoryFieldProps = + GenericProjectFormFieldProps; + +export default function SecretsMountDirectoryField({ + control, + errors, + name, +}: SecretsMountDirectoryFieldProps) { + const fieldId = `project-${name}`; + const fieldHelpId = `${fieldId}-help`; + + const watch = useWatch({ control, name }); + + return ( +
+ + ( + + )} + rules={{ + required: "Please provide a location for the secrets", + }} + /> +
+ {errors[name]?.message ? ( + <>{errors[name]?.message} + ) : ( + <>Invalid location + )} +
+ +

+ This is the location which will be used when mounting secrets inside + sessions. +

+ {!watch.startsWith("/") && ( +

+ Note that this location will be relative to the "working + directory". +

+ )} +
+
+ ); +} diff --git a/client/src/features/secretsV2/GeneralSecretItem.tsx b/client/src/features/secretsV2/GeneralSecretItem.tsx index 159a7aff4..188cce427 100644 --- a/client/src/features/secretsV2/GeneralSecretItem.tsx +++ b/client/src/features/secretsV2/GeneralSecretItem.tsx @@ -17,14 +17,15 @@ */ import cx from "classnames"; -import { Col, ListGroupItem, Row } from "reactstrap"; - import { useMemo } from "react"; import { generatePath, Link } from "react-router-dom-v5-compat"; +import { Col, ListGroupItem, Row } from "reactstrap"; + import { RtkOrNotebooksError } from "../../components/errors/RtkErrorAlert"; import { Loader } from "../../components/Loader"; import { TimeCaption } from "../../components/TimeCaption"; import { ABSOLUTE_ROUTES } from "../../routing/routes.constants"; +import { SESSION_SECRETS_CARD_ID } from "../ProjectPageV2/ProjectPageContent/SessionSecrets/sessionSecrets.constants"; import type { Project, SessionSecretSlot, @@ -125,7 +126,7 @@ function GeneralSecretUsedInProject({ return null; } - const projectUrl = generatePath(ABSOLUTE_ROUTES.v2.projects.show.root, { + const projectUrl = generatePath(ABSOLUTE_ROUTES.v2.projects.show.settings, { namespace: project.namespace, slug: project.slug, }); @@ -133,7 +134,7 @@ function GeneralSecretUsedInProject({ return (
  • - + {project.name} {" - "} diff --git a/client/src/features/sessionsV2/SessionView/SessionView.tsx b/client/src/features/sessionsV2/SessionView/SessionView.tsx index 6c7b3aeda..67406a475 100644 --- a/client/src/features/sessionsV2/SessionView/SessionView.tsx +++ b/client/src/features/sessionsV2/SessionView/SessionView.tsx @@ -42,6 +42,7 @@ import { import { TimeCaption } from "../../../components/TimeCaption"; import { CommandCopy } from "../../../components/commandCopy/CommandCopy"; import { RepositoryItem } from "../../ProjectPageV2/ProjectPageContent/CodeRepositories/CodeRepositoryDisplay"; +import SessionViewSessionSecrets from "../../ProjectPageV2/ProjectPageContent/SessionSecrets/SessionViewSessionSecrets"; import useProjectPermissions from "../../ProjectPageV2/utils/useProjectPermissions.hook"; import { useGetDataConnectorsListByDataConnectorIdsQuery } from "../../dataConnectorsV2/api/data-connectors.enhanced-api"; import { @@ -466,7 +467,7 @@ export function SessionView({ {project?.repositories?.length} )}
    - {project?.repositories && project.repositories.length > 0 ? ( + {project.repositories && project.repositories.length > 0 ? ( {project.repositories.map((repositoryUrl, index) => ( )} + + diff --git a/tests/cypress/fixtures/projectV2/create-projectV2.json b/tests/cypress/fixtures/projectV2/create-projectV2.json index 6941d4c03..3ee31d90b 100644 --- a/tests/cypress/fixtures/projectV2/create-projectV2.json +++ b/tests/cypress/fixtures/projectV2/create-projectV2.json @@ -5,5 +5,6 @@ "created_by": { "id": "owner-KC-id" }, - "visibility": "public" + "visibility": "public", + "secrets_mount_directory": "/secrets" } diff --git a/tests/cypress/fixtures/projectV2/list-projectV2.json b/tests/cypress/fixtures/projectV2/list-projectV2.json index e2363f96c..a74acf86b 100644 --- a/tests/cypress/fixtures/projectV2/list-projectV2.json +++ b/tests/cypress/fixtures/projectV2/list-projectV2.json @@ -11,7 +11,8 @@ "https://domain.name/repo2.git" ], "visibility": "public", - "description": "Project 2 description" + "description": "Project 2 description", + "secrets_mount_directory": "/secrets" }, { "id": "01HF96BXZ3JF9DX88B7XB405S5", @@ -22,6 +23,7 @@ "created_by": { "id": "user1-uuid" }, "repositories": [], "visibility": "private", - "description": "Project 1 description" + "description": "Project 1 description", + "secrets_mount_directory": "/secrets" } ] diff --git a/tests/cypress/fixtures/projectV2/read-projectV2-empty.json b/tests/cypress/fixtures/projectV2/read-projectV2-empty.json index 5b511a453..f5bce6305 100644 --- a/tests/cypress/fixtures/projectV2/read-projectV2-empty.json +++ b/tests/cypress/fixtures/projectV2/read-projectV2-empty.json @@ -7,5 +7,6 @@ "created_by": "user1-uuid", "repositories": [], "visibility": "public", - "description": "Project 2 description" + "description": "Project 2 description", + "secrets_mount_directory": "/secrets" } diff --git a/tests/cypress/fixtures/projectV2/read-projectV2.json b/tests/cypress/fixtures/projectV2/read-projectV2.json index 01fdc4de6..03d34935d 100644 --- a/tests/cypress/fixtures/projectV2/read-projectV2.json +++ b/tests/cypress/fixtures/projectV2/read-projectV2.json @@ -10,5 +10,6 @@ "https://domain.name/repo2.git" ], "visibility": "public", - "description": "Project 2 description" + "description": "Project 2 description", + "secrets_mount_directory": "/secrets" } diff --git a/tests/cypress/fixtures/projectV2/update-projectV2-metadata.json b/tests/cypress/fixtures/projectV2/update-projectV2-metadata.json index b01568ab8..0092a5b0f 100644 --- a/tests/cypress/fixtures/projectV2/update-projectV2-metadata.json +++ b/tests/cypress/fixtures/projectV2/update-projectV2-metadata.json @@ -10,5 +10,6 @@ "https://domain.name/repo2.git" ], "visibility": "public", - "description": "new description" + "description": "new description", + "secrets_mount_directory": "/secrets" } diff --git a/tests/cypress/fixtures/projectV2/update-projectV2-one-repository.json b/tests/cypress/fixtures/projectV2/update-projectV2-one-repository.json index 8225a903b..7452c152f 100644 --- a/tests/cypress/fixtures/projectV2/update-projectV2-one-repository.json +++ b/tests/cypress/fixtures/projectV2/update-projectV2-one-repository.json @@ -7,5 +7,6 @@ "created_by": "user1-uuid", "repositories": ["https://gitlab.dev.renku.ch/url-repo.git"], "visibility": "public", - "description": "Project 2 description" + "description": "Project 2 description", + "secrets_mount_directory": "/secrets" } diff --git a/tests/cypress/fixtures/projectV2/update-projectV2-repositories.json b/tests/cypress/fixtures/projectV2/update-projectV2-repositories.json index 835e96c21..9e742d366 100644 --- a/tests/cypress/fixtures/projectV2/update-projectV2-repositories.json +++ b/tests/cypress/fixtures/projectV2/update-projectV2-repositories.json @@ -11,5 +11,6 @@ "https://domain.name/repo3.git" ], "visibility": "public", - "description": "Project 2 description" + "description": "Project 2 description", + "secrets_mount_directory": "/secrets" }