diff --git a/client/src/components/navbar/NavBarItems.tsx b/client/src/components/navbar/NavBarItems.tsx index 7dc3b8336d..4e0bf67678 100644 --- a/client/src/components/navbar/NavBarItems.tsx +++ b/client/src/components/navbar/NavBarItems.tsx @@ -281,10 +281,14 @@ export function RenkuToolbarNotifications({ } interface RenkuToolbarItemUserProps { + isV2?: boolean; params: AppParams; } -export function RenkuToolbarItemUser({ params }: RenkuToolbarItemUserProps) { +export function RenkuToolbarItemUser({ + isV2, + params, +}: RenkuToolbarItemUserProps) { const user = useLegacySelector((state) => state.stateModel.user); const { renku10Enabled } = useAppSelector(({ featureFlags }) => featureFlags); @@ -305,6 +309,8 @@ export function RenkuToolbarItemUser({ params }: RenkuToolbarItemUserProps) { ); } + const userSecretsUrl = isV2 ? ABSOLUTE_ROUTES.v2.secrets : "/secrets"; + return ( - + User Secrets @@ -351,9 +357,6 @@ export function RenkuToolbarItemUser({ params }: RenkuToolbarItemUserProps) { > Renku 2.0 Settings - - Renku 2.0 Secrets (temp) - )} diff --git a/client/src/features/dataConnectorsV2/components/DataConnectorView.tsx b/client/src/features/dataConnectorsV2/components/DataConnectorView.tsx index f1182366e3..d106e54d31 100644 --- a/client/src/features/dataConnectorsV2/components/DataConnectorView.tsx +++ b/client/src/features/dataConnectorsV2/components/DataConnectorView.tsx @@ -17,16 +17,16 @@ */ import cx from "classnames"; import { useMemo, useRef } from "react"; -import { Link, generatePath } from "react-router-dom-v5-compat"; -import { Offcanvas, OffcanvasBody, UncontrolledTooltip } from "reactstrap"; import { - InfoCircleFill, Folder, Gear, + InfoCircleFill, Key, Lock, PersonBadge, } from "react-bootstrap-icons"; +import { Link, generatePath } from "react-router-dom-v5-compat"; +import { Offcanvas, OffcanvasBody, UncontrolledTooltip } from "reactstrap"; import { Clipboard } from "../../../components/clipboard/Clipboard"; import { Loader } from "../../../components/Loader"; @@ -38,15 +38,14 @@ import { CredentialMoreInfo } from "../../project/components/cloudStorage/CloudS import { CLOUD_STORAGE_SENSITIVE_FIELD_TOKEN } from "../../project/components/cloudStorage/projectCloudStorage.constants"; import { getCredentialFieldDefinitions } from "../../project/utils/projectCloudStorage.utils"; import { useGetNamespacesByNamespaceSlugQuery } from "../../projectsV2/api/projectV2.enhanced-api"; +import { storageSecretNameToFieldName } from "../../secretsV2/secrets.utils"; +import UserAvatar from "../../usersV2/show/UserAvatar"; import type { DataConnectorRead, DataConnectorToProjectLink, } from "../api/data-connectors.api"; import { useGetDataConnectorsByDataConnectorIdSecretsQuery } from "../api/data-connectors.enhanced-api"; -import { storageSecretNameToFieldName } from "../../secrets/secrets.utils"; - -import UserAvatar from "../../usersV2/show/UserAvatar"; import DataConnectorActions from "./DataConnectorActions"; import useDataConnectorProjects from "./useDataConnectorProjects.hook"; diff --git a/client/src/features/rootV2/NavbarV2.tsx b/client/src/features/rootV2/NavbarV2.tsx index 63c71c36b9..e75448070e 100644 --- a/client/src/features/rootV2/NavbarV2.tsx +++ b/client/src/features/rootV2/NavbarV2.tsx @@ -226,7 +226,7 @@ export default function NavbarV2() { - + diff --git a/client/src/features/secrets/GeneralSecretNew.tsx b/client/src/features/secrets/GeneralSecretNew.tsx index d4f2a9354a..d3a4bccecf 100644 --- a/client/src/features/secrets/GeneralSecretNew.tsx +++ b/client/src/features/secrets/GeneralSecretNew.tsx @@ -19,12 +19,10 @@ import cx from "classnames"; import { useCallback, useEffect, useState } from "react"; import { PlusLg, XLg } from "react-bootstrap-icons"; -import { Controller, useForm } from "react-hook-form"; +import { useForm } from "react-hook-form"; import { Button, Form, - Input, - Label, Modal, ModalBody, ModalFooter, @@ -33,9 +31,10 @@ import { import { RtkOrNotebooksError } from "../../components/errors/RtkErrorAlert"; import { Loader } from "../../components/Loader"; +import FilenameField from "../secretsV2/fields/FilenameField"; +import NameField from "../secretsV2/fields/NameField"; +import SecretValueField from "../secretsV2/fields/SecretValueField"; import { usePostUserSecretMutation, usersApi } from "../usersV2/api/users.api"; -import { AddSecretForm } from "./secrets.types"; -import { SECRETS_VALUE_LENGTH_LIMIT } from "./secrets.utils"; export default function GeneralSecretNew() { // Set up the modal @@ -53,8 +52,8 @@ export default function GeneralSecretNew() { } = useForm({ defaultValues: { name: "", + filename: "", value: "", - kind: "general", }, }); @@ -62,8 +61,15 @@ export default function GeneralSecretNew() { const [getSecrets, secrets] = usersApi.useLazyGetUserSecretsQuery(); const [addSecretMutation, result] = usePostUserSecretMutation(); const onSubmit = useCallback( - (newSecret: AddSecretForm) => { - addSecretMutation({ secretPost: newSecret }); + (data: AddSecretForm) => { + const filename = data.filename.trim(); + addSecretMutation({ + secretPost: { + name: data.name, + value: data.value, + ...(filename ? { default_filename: filename } : {}), + }, + }); }, [addSecretMutation] ); @@ -97,74 +103,9 @@ export default function GeneralSecretNew() { data-cy="secrets-new-form" onSubmit={handleSubmit(onSubmit)} > -
- - ( - - )} - rules={{ - required: "Please provide a name.", - validate: (value) => - secrets.data?.map((s) => s.name).includes(value) - ? "This name is already used by another secret." - : value && value.startsWith(".") - ? "Name cannot start with a dot." - : !/^[a-zA-Z0-9_.-]+$/.test(value) - ? "Only letters, numbers, dots (.), underscores (_), and dashes (-)." - : undefined, - }} - /> - {errors.name && ( -
{errors.name.message}
- )} -
- -
- - ( - - )} - rules={{ - required: "Please provide a value.", - validate: (value) => - value.length > SECRETS_VALUE_LENGTH_LIMIT - ? `Value cannot exceed ${SECRETS_VALUE_LENGTH_LIMIT} characters.` - : undefined, - }} - /> - {errors.value && ( -
{errors.value.message}
- )} -
+ + + ); @@ -201,7 +142,11 @@ export default function GeneralSecretNew() { onClick={handleSubmit(onSubmit)} type="submit" > - + {result.isLoading ? ( + + ) : ( + + )} Add @@ -219,3 +164,9 @@ export default function GeneralSecretNew() { ); } + +interface AddSecretForm { + name: string; + filename: string; + value: string; +} diff --git a/client/src/features/secrets/SecretDelete.tsx b/client/src/features/secrets/SecretDelete.tsx.old similarity index 100% rename from client/src/features/secrets/SecretDelete.tsx rename to client/src/features/secrets/SecretDelete.tsx.old diff --git a/client/src/features/secrets/SecretEdit.tsx b/client/src/features/secrets/SecretEdit.tsx.old similarity index 98% rename from client/src/features/secrets/SecretEdit.tsx rename to client/src/features/secrets/SecretEdit.tsx.old index 1e88c68521..3829788d53 100644 --- a/client/src/features/secrets/SecretEdit.tsx +++ b/client/src/features/secrets/SecretEdit.tsx.old @@ -34,7 +34,7 @@ import { import { RtkOrNotebooksError } from "../../components/errors/RtkErrorAlert"; import { usePatchUserSecretMutation } from "../usersV2/api/users.api"; import { EditSecretForm, SecretDetails } from "./secrets.types"; -import { SECRETS_VALUE_LENGTH_LIMIT } from "./secrets.utils"; +import { SECRETS_VALUE_LENGTH_LIMIT } from "./secrets.constants"; interface SecretsEditProps { secret: SecretDetails; diff --git a/client/src/features/secrets/Secrets.tsx b/client/src/features/secrets/Secrets.tsx index b54436e662..8a6aa90dae 100644 --- a/client/src/features/secrets/Secrets.tsx +++ b/client/src/features/secrets/Secrets.tsx @@ -28,8 +28,7 @@ import WipBadge from "../projectsV2/shared/WipBadge"; import GeneralSecretNew from "./GeneralSecretNew"; import SecretsList from "./SecretsList"; -// import StorageSecretsList from "./StorageSecretsList"; -import { SECRETS_DOCS_URL } from "./secrets.utils"; +import { SECRETS_DOCS_URL } from "./secrets.constants"; function GeneralSecretSection() { return ( @@ -52,36 +51,13 @@ function GeneralSecretSection() { - + ); } -// function StorageSecretSection() { -// return ( -// <> -// -// -//
-//

Storage Secrets

-//
-//

-// Credentials used to access data connectors can be persisted as -// storage secrets. -//

-// -//
-// -// -// -// -// -// -// ); -// } - function SecretsPageInfo() { return ( <> @@ -130,7 +106,6 @@ export default function Secrets() { {user.logged && } - {/* {user.logged && } */} ); } diff --git a/client/src/features/secrets/SecretsList.tsx b/client/src/features/secrets/SecretsList.tsx index ba7e6c0a20..7ea5874a41 100644 --- a/client/src/features/secrets/SecretsList.tsx +++ b/client/src/features/secrets/SecretsList.tsx @@ -22,15 +22,12 @@ import { Col, Container, Row } from "reactstrap"; import { RtkOrNotebooksError } from "../../components/errors/RtkErrorAlert"; import { Loader } from "../../components/Loader"; import { useGetUserSecretsQuery } from "../usersV2/api/users.api"; -import type { SecretKind } from "./secrets.types"; import SecretsListItem from "./SecretsListItem"; -interface SecretsListParams { - kind: SecretKind; -} - -export default function SecretsList({ kind }: SecretsListParams) { - const secrets = useGetUserSecretsQuery({ userSecretsParams: { kind } }); +export default function SecretsList() { + const secrets = useGetUserSecretsQuery({ + userSecretsParams: { kind: "general" }, + }); if (secrets.isLoading) return ; @@ -39,17 +36,14 @@ export default function SecretsList({ kind }: SecretsListParams) { if (secrets.data?.length === 0) return null; - const secretsList = secrets.data?.map((secret) => { - return ( - - - - ); - }); return ( - {secretsList} + {secrets.data?.map((secret) => ( + + + + ))} ); diff --git a/client/src/features/secrets/SecretsListItem.tsx b/client/src/features/secrets/SecretsListItem.tsx index 6dbed0fc3e..a7144486ff 100644 --- a/client/src/features/secrets/SecretsListItem.tsx +++ b/client/src/features/secrets/SecretsListItem.tsx @@ -20,21 +20,14 @@ import cx from "classnames"; import { Card, CardBody } from "reactstrap"; import { TimeCaption } from "../../components/TimeCaption"; -import SecretEdit from "./SecretEdit"; -import SecretDelete from "./SecretDelete"; -import { SecretDetails, SecretKind } from "./secrets.types"; -import { storageSecretNameToFieldName } from "./secrets.utils"; +import SecretItemActions from "../secretsV2/SecretItemActions"; +import type { SecretWithId } from "../usersV2/api/users.api"; interface SecretsListItemProps { - kind: SecretKind; - secret: SecretDetails; + secret: SecretWithId; } -export default function SecretsListItem({ - kind, - secret, -}: SecretsListItemProps) { - const secretDisplayName = - kind === "storage" ? storageSecretNameToFieldName(secret) : secret.name; + +export default function SecretsListItem({ secret }: SecretsListItemProps) { return ( - {secretDisplayName} + {secret.name} Edited{" "} -
- - -
+ + +
+ Filename: {secret.default_filename}
diff --git a/client/src/features/secrets/StorageSecretsList.tsx b/client/src/features/secrets/StorageSecretsList.tsx deleted file mode 100644 index ace7e5bcd1..0000000000 --- a/client/src/features/secrets/StorageSecretsList.tsx +++ /dev/null @@ -1,80 +0,0 @@ -/*! - * Copyright 2024 - Swiss Data Science Center (SDSC) - * A partnership between École Polytechnique Fédérale de Lausanne (EPFL) and - * Eidgenössische Technische Hochschule Zürich (ETHZ). - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License - */ - -import cx from "classnames"; -import { Col, Container, Row } from "reactstrap"; - -import { RtkOrNotebooksError } from "../../components/errors/RtkErrorAlert"; -import { Loader } from "../../components/Loader"; -import { useGetUserSecretsQuery } from "../usersV2/api/users.api"; -import type { SecretDetails } from "./secrets.types"; -import { storageSecretNameToStorageId } from "./secrets.utils"; -import SecretsListItem from "./SecretsListItem"; - -interface SecretGroupProps { - group: string; - secrets: SecretDetails[]; -} -function SecretGroup({ group, secrets }: SecretGroupProps) { - return ( - <> - - -
{group}
- -
- - {secrets.map((secret) => ( - - - - ))} - - - ); -} - -export default function StorageSecretsList() { - const secrets = useGetUserSecretsQuery({ - userSecretsParams: { kind: "storage" }, - }); - - if (secrets.isLoading) return ; - - if (secrets.isError) - return ; - - if (secrets.data == null || secrets.data?.length === 0) return null; - const secretsGroups = secrets.data.reduce((acc, secret) => { - const group = storageSecretNameToStorageId(secret); - if (group in acc) { - acc[group].push(secret); - } else { - acc[group] = [secret]; - } - return acc; - }, {} as Record); - - return ( - - {Object.entries(secretsGroups).map(([group, secrets]) => ( - - ))} - - ); -} diff --git a/client/src/features/secrets/secrets.utils.ts b/client/src/features/secrets/secrets.constants.ts similarity index 73% rename from client/src/features/secrets/secrets.utils.ts rename to client/src/features/secrets/secrets.constants.ts index 8f241b1b81..27248dd5e7 100644 --- a/client/src/features/secrets/secrets.utils.ts +++ b/client/src/features/secrets/secrets.constants.ts @@ -17,18 +17,7 @@ */ import { Docs } from "../../utils/constants/Docs"; -import type { SecretDetails } from "./secrets.types"; export const SECRETS_DOCS_URL = Docs.rtdTopicGuide("secrets/secrets.html"); export const SECRETS_VALUE_LENGTH_LIMIT = 5_000; - -type Secret = Pick; - -export function storageSecretNameToFieldName(secret: Secret) { - return secret.name.split("-").slice(1).join("-") || secret.name; -} - -export function storageSecretNameToStorageId(secret: Secret) { - return secret.name.split("-")[0]; -} diff --git a/client/src/features/secrets/secrets.types.ts b/client/src/features/secrets/secrets.types.ts.old similarity index 100% rename from client/src/features/secrets/secrets.types.ts rename to client/src/features/secrets/secrets.types.ts.old diff --git a/client/src/features/secretsV2/fields/FilenameField.tsx b/client/src/features/secretsV2/fields/FilenameField.tsx new file mode 100644 index 0000000000..346aa0a8cc --- /dev/null +++ b/client/src/features/secretsV2/fields/FilenameField.tsx @@ -0,0 +1,70 @@ +/*! + * 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, type FieldValues } from "react-hook-form"; +import { Input, Label } from "reactstrap"; +import { UserSecretFormFieldProps } from "./fields.types"; + +type FilenameFieldProps = UserSecretFormFieldProps; + +export default function FilenameField({ + control, + errors, + formId, + name, + rules, +}: FilenameFieldProps) { + const fieldIdSuffix = `user-secret-${name}`; + const fieldId = formId ? `${formId}-${fieldIdSuffix}` : fieldIdSuffix; + + return ( +
+ + ( + + )} + rules={{ + pattern: { + value: /^[a-zA-Z0-9_\-.]+$/, + message: + 'A valid filename must consist of alphanumeric characters, "-", "_" or "."', + }, + ...rules, + }} + /> +
+ {errors[name]?.message ? ( + <>{errors[name]?.message} + ) : ( + <>Invalid filename + )} +
+
+ ); +} diff --git a/client/src/features/secretsV2/fields/SecretValueField.tsx b/client/src/features/secretsV2/fields/SecretValueField.tsx index 9d2f00ecc8..c34a60816c 100644 --- a/client/src/features/secretsV2/fields/SecretValueField.tsx +++ b/client/src/features/secretsV2/fields/SecretValueField.tsx @@ -20,8 +20,8 @@ import cx from "classnames"; import { Controller, type FieldValues } from "react-hook-form"; import { FormText, Label } from "reactstrap"; +import { SECRETS_VALUE_LENGTH_LIMIT } from "../../secrets/secrets.constants"; import type { UserSecretFormFieldProps } from "./fields.types"; -import { SECRETS_VALUE_LENGTH_LIMIT } from "../../secrets/secrets.utils"; type SecretValueFieldProps = UserSecretFormFieldProps; diff --git a/client/src/features/secretsV2/secrets.utils.ts b/client/src/features/secretsV2/secrets.utils.ts new file mode 100644 index 0000000000..96e5792d9d --- /dev/null +++ b/client/src/features/secretsV2/secrets.utils.ts @@ -0,0 +1,25 @@ +/*! + * 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 type { SecretWithId } from "../usersV2/api/users.api"; + +type Secret = Pick; + +export function storageSecretNameToFieldName(secret: Secret) { + return secret.name.split("-").slice(1).join("-") || secret.name; +} diff --git a/client/src/features/session/components/options/SessionUserSecrets.tsx b/client/src/features/session/components/options/SessionUserSecrets.tsx index 986cda157c..11a7ba5f89 100644 --- a/client/src/features/session/components/options/SessionUserSecrets.tsx +++ b/client/src/features/session/components/options/SessionUserSecrets.tsx @@ -218,7 +218,7 @@ function SecretsCheckboxList({ className={cx("form-check-label", "my-auto")} for={`secrets-session-${secret.name}`} > - {secret.name} + {secret.name} - filename: {secret.default_filename} ))} diff --git a/client/src/features/session/startSessionOptions.types.ts b/client/src/features/session/startSessionOptions.types.ts index f1655b4805..283c137c7b 100644 --- a/client/src/features/session/startSessionOptions.types.ts +++ b/client/src/features/session/startSessionOptions.types.ts @@ -65,6 +65,7 @@ export interface SessionEnvironmentVariable { } export interface SessionSecrets { - name: string; id: string; + name: string; + default_filename: string; } diff --git a/client/src/features/sessionsV2/DataConnectorSecretsModal.tsx b/client/src/features/sessionsV2/DataConnectorSecretsModal.tsx index 89ae5cc311..100c0763de 100644 --- a/client/src/features/sessionsV2/DataConnectorSecretsModal.tsx +++ b/client/src/features/sessionsV2/DataConnectorSecretsModal.tsx @@ -46,12 +46,12 @@ import { import { Loader } from "../../components/Loader"; import type { RCloneOption } from "../dataConnectorsV2/api/data-connectors.api"; -import { DataConnectorConfiguration } from "../dataConnectorsV2/components/useDataConnectorConfiguration.hook"; import { validationParametersFromDataConnectorConfiguration } from "../dataConnectorsV2/components/dataConnector.utils"; +import { DataConnectorConfiguration } from "../dataConnectorsV2/components/useDataConnectorConfiguration.hook"; import { useTestCloudStorageConnectionMutation } from "../project/components/cloudStorage/projectCloudStorage.api"; import { CLOUD_STORAGE_SAVED_SECRET_DISPLAY_VALUE } from "../project/components/cloudStorage/projectCloudStorage.constants"; import type { CloudStorageDetailsOptions } from "../project/components/cloudStorage/projectCloudStorage.types"; -import { storageSecretNameToFieldName } from "../secrets/secrets.utils"; +import { storageSecretNameToFieldName } from "../secretsV2/secrets.utils"; const CONTEXT_STRINGS = { session: { diff --git a/client/src/features/sessionsV2/SessionStartPage.tsx b/client/src/features/sessionsV2/SessionStartPage.tsx index 4caafe96ab..82e161d572 100644 --- a/client/src/features/sessionsV2/SessionStartPage.tsx +++ b/client/src/features/sessionsV2/SessionStartPage.tsx @@ -54,7 +54,7 @@ import { } from "../project/utils/projectCloudStorage.utils"; import type { Project } from "../projectsV2/api/projectV2.api"; import { useGetNamespacesByNamespaceProjectsAndSlugQuery } from "../projectsV2/api/projectV2.enhanced-api"; -import { storageSecretNameToFieldName } from "../secrets/secrets.utils"; +import { storageSecretNameToFieldName } from "../secretsV2/secrets.utils"; import DataConnectorSecretsModal from "./DataConnectorSecretsModal"; import SessionSecretsModal from "./SessionSecretsModal"; import { SelectResourceClassModal } from "./components/SessionModals/SelectResourceClass";