Please provide credentials for the following cloud storage
- {missingCredentialsStorage.length > 1 && "s"}:{" "}
+ {missingCredentialsStorage.length > 1 && "s"} or disable{" "}
+ {missingCredentialsStorage.length > 1 ? "them" : "it"}:{" "}
{missingCredentialsStorage.map(({ name }) => name).join(", ")}
diff --git a/client/src/features/session/components/options/AutostartSessionOptions.tsx b/client/src/features/session/components/options/AutostartSessionOptions.tsx
index 30d510d068..a25ffc7eca 100644
--- a/client/src/features/session/components/options/AutostartSessionOptions.tsx
+++ b/client/src/features/session/components/options/AutostartSessionOptions.tsx
@@ -23,7 +23,7 @@ import useAppDispatch from "../../../../utils/customHooks/useAppDispatch.hook";
import useAppSelector from "../../../../utils/customHooks/useAppSelector.hook";
import useLegacySelector from "../../../../utils/customHooks/useLegacySelector.hook";
import { useGetResourcePoolsQuery } from "../../../dataServices/dataServices.api";
-import { useGetCloudStorageForProjectQuery } from "../../../project/projectCloudStorage.api";
+import { useGetCloudStorageForProjectQuery } from "../../../project/components/cloudStorage/projectCloudStorage.api";
import {
useGetAllRepositoryBranchesQuery,
useGetRepositoryCommitsQuery,
@@ -170,10 +170,7 @@ function useAutostartSessionOptions(): void {
skip:
!gitLabProjectId ||
!notebooksVersion ||
- !(
- notebooksVersion.cloudStorageEnabled.s3 ||
- notebooksVersion.cloudStorageEnabled.azureBlob
- ),
+ !notebooksVersion.cloudStorageEnabled,
}
);
diff --git a/client/src/features/session/components/options/SessionCloudStorageOption.tsx b/client/src/features/session/components/options/SessionCloudStorageOption.tsx
index c0d26c5898..9532287f43 100644
--- a/client/src/features/session/components/options/SessionCloudStorageOption.tsx
+++ b/client/src/features/session/components/options/SessionCloudStorageOption.tsx
@@ -17,43 +17,15 @@
*/
import cx from "classnames";
-import {
- ChangeEvent,
- useCallback,
- useEffect,
- useMemo,
- useRef,
- useState,
-} from "react";
-import {
- CloudFill,
- ExclamationTriangleFill,
- EyeFill,
- EyeSlashFill,
- InfoCircleFill,
- PencilSquare,
- PlusLg,
- TrashFill,
- XLg,
-} from "react-bootstrap-icons";
-import { Controller, useForm } from "react-hook-form";
+import { useCallback, useEffect, useRef, useState } from "react";
+import { EyeFill, EyeSlashFill, InfoCircleFill } from "react-bootstrap-icons";
import { Link } from "react-router-dom";
import {
Button,
- Card,
- CardBody,
- Col,
- Collapse,
Container,
- Form,
- FormText,
Input,
InputGroup,
Label,
- Modal,
- ModalBody,
- ModalFooter,
- ModalHeader,
PopoverBody,
Row,
UncontrolledPopover,
@@ -61,39 +33,23 @@ import {
} from "reactstrap";
import { ACCESS_LEVELS } from "../../../../api-client";
-import { ErrorAlert, InfoAlert } from "../../../../components/Alert";
-import { ExternalLink } from "../../../../components/ExternalLinks";
import { Loader } from "../../../../components/Loader";
-import { RtkErrorAlert } from "../../../../components/errors/RtkErrorAlert";
-import ChevronFlippedIcon from "../../../../components/icons/ChevronFlippedIcon";
+import { RtkOrNotebooksError } from "../../../../components/errors/RtkErrorAlert";
import LazyRenkuMarkdown from "../../../../components/markdown/LazyRenkuMarkdown";
import useAppDispatch from "../../../../utils/customHooks/useAppDispatch.hook";
import useAppSelector from "../../../../utils/customHooks/useAppSelector.hook";
import useLegacySelector from "../../../../utils/customHooks/useLegacySelector.hook";
import { Url } from "../../../../utils/helpers/url";
import { StateModelProject } from "../../../project/Project";
-import {
- useGetCloudStorageForProjectQuery,
- useValidateCloudStorageConfigurationMutation,
-} from "../../../project/projectCloudStorage.api";
-import {
- CLOUD_STORAGE_CONFIGURATION_PLACEHOLDER,
- CLOUD_STORAGE_READWRITE_ENABLED,
- CLOUD_STORAGE_SENSITIVE_FIELD_TOKEN,
-} from "../../../project/projectCloudStorage.constants";
-import {
- formatCloudStorageConfiguration,
- getProvidedSensitiveFields,
- parseCloudStorageConfiguration,
-} from "../../../project/utils/projectCloudStorage.utils";
import { useGetNotebooksVersionQuery } from "../../../versions/versions.api";
import { SessionCloudStorage } from "../../startSessionOptions.types";
import {
- addCloudStorageItem,
- removeCloudStorageItem,
setCloudStorage,
updateCloudStorageItem,
} from "../../startSessionOptionsSlice";
+import CloudStorageItem from "../../../project/components/cloudStorage/CloudStorageItem";
+import { useGetCloudStorageForProjectQuery } from "../../../project/components/cloudStorage/projectCloudStorage.api";
+import { CLOUD_STORAGE_SENSITIVE_FIELD_TOKEN } from "../../../project/components/cloudStorage/projectCloudStorage.constants";
export default function SessionCloudStorageOption() {
const { data: notebooksVersion, isLoading } = useGetNotebooksVersionQuery();
@@ -109,17 +65,16 @@ export default function SessionCloudStorageOption() {
);
}
- return notebooksVersion?.cloudStorageEnabled.s3 ||
- notebooksVersion?.cloudStorageEnabled.azureBlob ? (
+ return notebooksVersion?.cloudStorageEnabled ? (
) : null;
}
function SessionS3CloudStorageOption() {
- const { namespace, path } = useLegacySelector
(
- (state) => state.stateModel.project.metadata
- );
-
+ const { accessLevel, namespace, path } = useLegacySelector<
+ StateModelProject["metadata"]
+ >((state) => state.stateModel.project.metadata);
+ const devAccess = accessLevel >= ACCESS_LEVELS.DEVELOPER;
const settingsStorageUrl = Url.get(Url.pages.project.settings.storage, {
namespace,
path,
@@ -128,33 +83,29 @@ function SessionS3CloudStorageOption() {
return (
Cloud Storage
-
- Use data from sources like AWS S3, Google Cloud
- Storage, etc.
-
-
- It is recommended to configure cloud storage options from the{" "}
- Project's settings.
-
-
+ {devAccess && (
+
+ Cloud storage options can be adjusted from the{" "}
+ Project's settings.
+
+ )}
+
);
}
-function CloudStorageList() {
- const { accessLevel, id: projectId } = useLegacySelector<
- StateModelProject["metadata"]
- >((state) => state.stateModel.project.metadata);
-
- const devAccess = accessLevel >= ACCESS_LEVELS.DEVELOPER;
-
+interface CloudStorageListProps {
+ devAccess: boolean;
+}
+function CloudStorageSection({ devAccess }: CloudStorageListProps) {
+ const { id: projectId } = useLegacySelector(
+ (state) => state.stateModel.project.metadata
+ );
+ const dispatch = useAppDispatch();
const cloudStorageList = useAppSelector(
({ startSessionOptions }) => startSessionOptions.cloudStorage
);
- const dispatch = useAppDispatch();
-
- const { data: notebooksVersion } = useGetNotebooksVersionQuery();
const {
data: storageForProject,
error,
@@ -166,11 +117,6 @@ function CloudStorageList() {
{ skip: !devAccess }
);
- const support = useMemo(
- () => (notebooksVersion?.cloudStorageEnabled.s3 ? "s3" : "azure"),
- [notebooksVersion?.cloudStorageEnabled]
- );
-
// Populate session cloud storage from project's settings
useEffect(() => {
if (storageForProject == null) {
@@ -178,12 +124,7 @@ function CloudStorageList() {
}
const initialCloudStorage: SessionCloudStorage[] = storageForProject.map(
({ storage, sensitive_fields }) => ({
- active:
- (storage.storage_type === "s3" && support === "s3") ||
- (storage.storage_type === "azureblob" && support === "azure"),
- supported:
- (storage.storage_type === "s3" && support === "s3") ||
- (storage.storage_type === "azureblob" && support === "azure"),
+ active: true,
...(sensitive_fields
? {
sensitive_fields: sensitive_fields.map(({ name, ...rest }) => ({
@@ -197,263 +138,151 @@ function CloudStorageList() {
})
);
dispatch(setCloudStorage(initialCloudStorage));
- }, [dispatch, storageForProject, support]);
-
- if (isLoading) {
- return ;
- }
-
- return (
- <>
- {error && (
-
-
- Error: could not load this project's cloud storage settings.
-
-
- )}
- {cloudStorageList.length > 0 && (
-
-
- {cloudStorageList.map((storage, index) => (
-
- ))}
-
-
- )}
-
- >
- );
-}
+ }, [dispatch, storageForProject]);
-interface CloudStorageItemProps {
- index: number;
- storage: SessionCloudStorage;
-}
+ if (isLoading) return ;
-function CloudStorageItem({ index, storage }: CloudStorageItemProps) {
- const {
- active,
- configuration,
- name,
- sensitive_fields,
- supported,
- target_path,
- } = storage;
-
- const providedSensitiveFields = useMemo(
- () => getProvidedSensitiveFields(configuration),
- [configuration]
- );
- const requiredSensitiveFields = useMemo(
- () =>
- sensitive_fields?.filter(({ name }) =>
- providedSensitiveFields.includes(name)
- ),
- [providedSensitiveFields, sensitive_fields]
- );
-
- const [isOpen, setIsOpen] = useState(false);
- const toggle = useCallback(() => {
- setIsOpen((isOpen) => !isOpen);
- }, []);
+ if (error) {
+ return ;
+ }
- const dispatch = useAppDispatch();
+ if (!cloudStorageList || cloudStorageList.length === 0) {
+ return (
+
+ No cloud storage configured for this project
+ {devAccess
+ ? ""
+ : ". Only developers can add a new one; you can fork the project if you wish to attach a storage"}
+ .
+
+ );
+ }
- const onToggleActive = useCallback(() => {
- dispatch(
- updateCloudStorageItem({
- index,
- storage: { ...storage, active: !storage.active },
- })
+ const storageList = cloudStorageList.map((storage, index) => {
+ const { active, sensitive_fields, ...unsafeStorageProps } = storage;
+ const { configuration, ...otherStorageProps } = unsafeStorageProps;
+
+ const safeConfiguration = Object.fromEntries(
+ Object.entries(configuration).map(([key, value]) => [
+ key,
+ sensitive_fields && sensitive_fields.find((f) => f.name === key)
+ ? CLOUD_STORAGE_SENSITIVE_FIELD_TOKEN
+ : value,
+ ])
);
- }, [dispatch, index, storage]);
- const onChangeCredential = useCallback(
- (fieldIndex: number) => (event: ChangeEvent) => {
- if (sensitive_fields == null) {
- return;
- }
- const name = sensitive_fields[fieldIndex].name;
- const value = event.target.value;
- const newSensitiveFields = [...sensitive_fields];
- newSensitiveFields.splice(fieldIndex, 1, {
- ...sensitive_fields[fieldIndex],
- name,
- value,
- });
+ const storageDefinition = {
+ storage: { configuration: safeConfiguration, ...otherStorageProps },
+ sensitive_fields: storage.sensitive_fields,
+ };
+ const localId = `cloud-storage-${storage.name}`;
+
+ const onToggleActive = () =>
dispatch(
updateCloudStorageItem({
index,
- storage: {
- ...storage,
- sensitive_fields: newSensitiveFields,
- },
+ storage: { ...storage, active: !storage.active },
})
);
- },
- [dispatch, index, sensitive_fields, storage]
- );
- const onRemoveItem = useCallback(() => {
- dispatch(removeCloudStorageItem({ index: index }));
- }, [dispatch, index]);
- return (
-
-
-
-
-
-
- Mount in this session
-
-
-
- {name}
- {storage.storage_id == null && (
- <>
- {" "}
-
- (temporary)
-
- >
- )}
-
-
- Mount point:
- {active ? (
- {target_path}
- ) : (
- Not mounted
- )}
-
- {storage.storage_id == null && (
-
-
-
-
-
- )}
-
+ const changeCredential = (name: string, value: string) => {
+ if (sensitive_fields) {
+ const fieldIndex = sensitive_fields.findIndex((f) => f.name === name);
+ const newSensitiveFields = [...sensitive_fields];
+ newSensitiveFields.splice(fieldIndex, 1, {
+ ...sensitive_fields[fieldIndex],
+ name,
+ value,
+ });
+
+ dispatch(
+ updateCloudStorageItem({
+ index,
+ storage: {
+ ...storage,
+ configuration: {
+ ...storage.configuration,
+ [name]: value,
+ },
+ sensitive_fields: newSensitiveFields,
+ },
+ })
+ );
+ }
+ };
- {!supported && (
-
-
-
- This cloud storage configuration is currently not supported.
-
-
- )}
+ const sensitiveFields =
+ storage.sensitive_fields && storage.sensitive_fields.length
+ ? storage.sensitive_fields.filter((f) => {
+ if (Object.keys(storage.configuration).find((c) => c === f.name)) {
+ return true;
+ }
+ })
+ : null;
+
+ const requiredSensitiveFields =
+ sensitiveFields && sensitiveFields.length ? (
+
+
+ Please fill in the credentials required to use this cloud storage
+
+ {sensitiveFields.map((item, fieldIndex) => (
+
+ ))}
+
+ ) : null;
- {supported &&
- requiredSensitiveFields != null &&
- requiredSensitiveFields.length > 0 && (
-
- Credentials
-
- Please fill in the credentials required to use this cloud
- storage
-
- {requiredSensitiveFields.map((item, fieldIndex) => (
-
- ))}
-
- )}
+ return (
+
+
+
+
+ Mount the storage in this session
+
+
+ {requiredSensitiveFields}
+
+ );
+ });
-
-
- More details
-
-
-
-
-
-
-
-
-
-
-
-
+ return (
+
+ {storageList}
+
);
}
interface CredentialFieldProps {
active: boolean;
- fieldIndex: number;
- index: number;
item: {
name: string;
help: string;
value: string;
};
- onChangeCredential: (
- fieldIndex: number
- ) => (event: ChangeEvent) => void;
+ changeCredential: (name: string, value: string) => void;
}
function CredentialField({
active,
- fieldIndex,
- index,
item,
- onChangeCredential,
+ changeCredential,
}: CredentialFieldProps) {
const [showPassword, setShowPassword] = useState(false);
const onToggleVisibility = useCallback(() => {
@@ -466,7 +295,7 @@ function CredentialField({
return (
-
+
{item.name}
*
@@ -479,10 +308,10 @@ function CredentialField({
!item.value && active && "is-invalid"
)}
disabled={!active}
- id={`credentials-${index}-${item.name}`}
+ id={`credentials-${item.name}`}
type={showPassword ? "text" : "password"}
value={item.value}
- onChange={onChangeCredential(fieldIndex)}
+ onChange={(e) => changeCredential(item.name, e.target.value)}
/>
);
}
-
-function CloudStorageDetails({ index, storage }: CloudStorageItemProps) {
- const { namespace, path } = useLegacySelector(
- (state) => state.stateModel.project.metadata
- );
-
- const settingsStorageUrl = Url.get(Url.pages.project.settings.storage, {
- namespace,
- path,
- });
-
- const { configuration, name, readonly, source_path, target_path } = storage;
-
- const providedSensitiveFields = useMemo(
- () => getProvidedSensitiveFields(configuration),
- [configuration]
- );
- const requiredSensitiveFields = useMemo(
- () =>
- storage.sensitive_fields?.filter(({ name }) =>
- providedSensitiveFields.includes(name)
- ),
- [providedSensitiveFields, storage.sensitive_fields]
- );
- const configCredentials = (requiredSensitiveFields ?? []).reduce(
- (prev, { name, value }) => ({ ...prev, [name]: value }),
- {} as Record
- );
- const configWithCredentials = { ...configuration, ...configCredentials };
- const configContent = formatCloudStorageConfiguration({
- configuration: configWithCredentials,
- name,
- });
-
- const dispatch = useAppDispatch();
-
- const onChangeSourcePath = useCallback(
- (event: ChangeEvent) => {
- const value = event.target.value;
- dispatch(
- updateCloudStorageItem({
- index,
- storage: { ...storage, source_path: value },
- })
- );
- },
- [dispatch, index, storage]
- );
- const onChangeTargetPath = useCallback(
- (event: ChangeEvent) => {
- const value = event.target.value;
- dispatch(
- updateCloudStorageItem({
- index,
- storage: { ...storage, target_path: value },
- })
- );
- },
- [dispatch, index, storage]
- );
- const onChangeReadWriteMode = useCallback(() => {
- dispatch(
- updateCloudStorageItem({
- index,
- storage: { ...storage, readonly: !storage.readonly },
- })
- );
- }, [dispatch, index, storage]);
-
- const [tempConfigContent, setTempConfigContent] = useState(configContent);
- const onChangeConfiguration = useCallback(
- (event: ChangeEvent) => {
- setTempConfigContent(event.target.value);
- },
- []
- );
- const onUpdateConfiguration = useCallback(() => {
- const parsedConfiguration =
- parseCloudStorageConfiguration(tempConfigContent);
-
- const sensitiveFieldKeys =
- storage.sensitive_fields?.map(({ name }) => name) ?? [];
-
- const updatedExistingConfiguration = Object.keys(configuration)
- .flatMap((key) =>
- sensitiveFieldKeys.includes(key)
- ? [[key, CLOUD_STORAGE_SENSITIVE_FIELD_TOKEN] as const]
- : parsedConfiguration[key] != null
- ? [[key, parsedConfiguration[key]] as const]
- : []
- )
- .reduce(
- (obj, [key, value]) => ({ ...obj, [key]: value }),
- {} as Record
- );
- const updatedNewConfiguration = Object.entries(parsedConfiguration)
- .filter(([key]) => !Object.keys(updateCloudStorageItem).includes(key))
- .map(([key, value]) =>
- sensitiveFieldKeys.includes(key)
- ? ([key, CLOUD_STORAGE_SENSITIVE_FIELD_TOKEN] as const)
- : ([key, value] as const)
- )
- .reduce(
- (obj, [key, value]) => ({ ...obj, [key]: value }),
- {} as Record
- );
-
- const updatedSensitiveFields = storage.sensitive_fields?.map(
- ({ name, help }) =>
- parsedConfiguration[name] != null
- ? { name, help, value: parsedConfiguration[name] }
- : { name, help, value: "" }
- );
-
- dispatch(
- updateCloudStorageItem({
- index,
- storage: {
- ...storage,
- configuration: {
- ...updatedExistingConfiguration,
- ...updatedNewConfiguration,
- },
- sensitive_fields: updatedSensitiveFields,
- },
- })
- );
- }, [configuration, dispatch, index, storage, tempConfigContent]);
-
- useEffect(() => {
- setTempConfigContent(configContent);
- }, [configContent]);
-
- return (
-
-
- Changes made here will apply only for this session. Use the{" "}
- project's settings to
- permanently change cloud storage settings.
-
-
-
-
- Source Path
-
-
-
-
-
-
- Mount Point
-
-
-
-
- {!CLOUD_STORAGE_READWRITE_ENABLED ? null : (
-
-
Mode
-
-
-
- Read-only
-
-
-
-
-
- Read/Write
-
-
-
- )}
-
-
- Configuration
-
-
- You can paste here the output of{" "}
-
- rclone config show <name>
-
- .
-
-
-
-
-
- );
-}
-
-function AddTemporaryCloudStorageButton() {
- const [isOpen, setIsOpen] = useState(false);
- const toggle = useCallback(() => {
- setIsOpen((open) => !open);
- }, []);
-
- return (
- <>
-
-
- Add Temporary Cloud Storage
-
-
- >
- );
-}
-
-interface AddTemporaryCloudStorageModalProps {
- isOpen: boolean;
- toggle: () => void;
-}
-
-function AddTemporaryCloudStorageModal({
- isOpen,
- toggle,
-}: AddTemporaryCloudStorageModalProps) {
- const { namespace, path } = useLegacySelector(
- (state) => state.stateModel.project.metadata
- );
-
- const settingsStorageUrl = Url.get(Url.pages.project.settings.storage, {
- namespace,
- path,
- });
-
- const dispatch = useAppDispatch();
-
- const [validateCloudStorageConfiguration, result] =
- useValidateCloudStorageConfigurationMutation();
-
- const {
- control,
- formState: { errors },
- getValues,
- handleSubmit,
- reset,
- } = useForm({
- defaultValues: {
- configuration: "",
- name: "",
- readonly: true,
- source_path: "",
- },
- });
- const onSubmit = useCallback(
- (data: AddTemporaryCloudStorageForm) => {
- const configuration = parseCloudStorageConfiguration(data.configuration);
- validateCloudStorageConfiguration({ configuration });
- },
- [validateCloudStorageConfiguration]
- );
-
- useEffect(() => {
- if (result.isSuccess) {
- const data = getValues();
- const configuration = parseCloudStorageConfiguration(data.configuration);
- dispatch(
- addCloudStorageItem({
- active: true,
- configuration,
- name: data.name,
- private: false,
- readonly: data.readonly,
- source_path: data.source_path,
- storage_id: null,
- storage_type: "",
- supported: true,
- target_path: data.name,
- })
- );
- toggle();
- }
- }, [dispatch, getValues, result.isSuccess, toggle]);
-
- // Reset state when closed
- useEffect(() => {
- if (!isOpen) {
- reset();
- }
- }, [isOpen, reset]);
-
- return (
-
-
-
- Add Temporary Cloud Storage
-
-
-
-
-
-
-
- Close
-
-
- {result.isLoading ? (
-
- ) : (
-
- )}
- Add Temporary Cloud Storage
-
-
-
- );
-}
-
-interface AddTemporaryCloudStorageForm {
- configuration: string;
- name: string;
- readonly: boolean;
- source_path: string;
-}
-
-function S3ExplanationLink() {
- return (
-
- );
-}
diff --git a/client/src/features/session/hooks/options/useDefaultCloudStorageOption.hook.ts b/client/src/features/session/hooks/options/useDefaultCloudStorageOption.hook.ts
index 25c0143eac..65c1ff1d6a 100644
--- a/client/src/features/session/hooks/options/useDefaultCloudStorageOption.hook.ts
+++ b/client/src/features/session/hooks/options/useDefaultCloudStorageOption.hook.ts
@@ -16,14 +16,14 @@
* limitations under the License.
*/
-import { useEffect, useMemo } from "react";
+import { useEffect } from "react";
import useAppDispatch from "../../../../utils/customHooks/useAppDispatch.hook";
-import { CloudStorage } from "../../../project/projectCloudStorage.types";
import { getProvidedSensitiveFields } from "../../../project/utils/projectCloudStorage.utils";
import { NotebooksVersion } from "../../../versions/versions.types";
import { setError } from "../../startSession.slice";
import { SessionCloudStorage } from "../../startSessionOptions.types";
import { setCloudStorage } from "../../startSessionOptionsSlice";
+import { CloudStorage } from "../../../project/components/cloudStorage/projectCloudStorage.types";
interface UseDefaultCloudStorageOptionArgs {
notebooksVersion: NotebooksVersion | undefined;
@@ -36,10 +36,7 @@ export default function useDefaultCloudStorageOption({
}: UseDefaultCloudStorageOptionArgs): void {
const dispatch = useAppDispatch();
- const support = useMemo(
- () => (notebooksVersion?.cloudStorageEnabled.s3 ? "s3" : "azure"),
- [notebooksVersion?.cloudStorageEnabled]
- );
+ const supported = !!notebooksVersion?.cloudStorageEnabled;
// Populate session cloud storage from project's settings
useEffect(() => {
@@ -48,7 +45,7 @@ export default function useDefaultCloudStorageOption({
}
const initialCloudStorage: SessionCloudStorage[] = storageForProject.map(
- getInitialCloudStorageItem(support)
+ getInitialCloudStorageItem()
);
const missingCredentialsStorage = initialCloudStorage
@@ -67,19 +64,14 @@ export default function useDefaultCloudStorageOption({
}
dispatch(setCloudStorage(initialCloudStorage));
- }, [dispatch, storageForProject, support]);
+ }, [dispatch, storageForProject, supported]);
}
-function getInitialCloudStorageItem(
- support: "s3" | "azure"
-): (storageDefinition: CloudStorage) => SessionCloudStorage {
+function getInitialCloudStorageItem(): (
+ storageDefinition: CloudStorage
+) => SessionCloudStorage {
return ({ storage, sensitive_fields }) => ({
- active:
- (storage.storage_type === "s3" && support === "s3") ||
- (storage.storage_type === "azureblob" && support === "azure"),
- supported:
- (storage.storage_type === "s3" && support === "s3") ||
- (storage.storage_type === "azureblob" && support === "azure"),
+ active: true,
...(sensitive_fields
? {
sensitive_fields: sensitive_fields.map(({ name, ...rest }) => ({
diff --git a/client/src/features/session/sessions.api.utils.ts b/client/src/features/session/sessions.api.utils.ts
index 9ee3f6f113..74499adddd 100644
--- a/client/src/features/session/sessions.api.utils.ts
+++ b/client/src/features/session/sessions.api.utils.ts
@@ -22,56 +22,15 @@ import { SessionCloudStorage } from "./startSessionOptions.types";
export function convertCloudStorageForSessionApi(
cloudStorage: SessionCloudStorage
): CloudStorageDefinitionForSessionApi | null {
- const {
- configuration,
+ const { configuration, readonly, source_path, storage_type, target_path } =
+ cloudStorage;
+
+ return {
+ configuration: configuration.type
+ ? configuration
+ : { ...configuration, type: storage_type },
readonly,
- sensitive_fields,
source_path,
- storage_type,
target_path,
- } = cloudStorage;
-
- if (storage_type === "s3" || configuration["type"] === "s3") {
- const endpoint =
- configuration["endpoint"] && configuration["endpoint"].startsWith("http")
- ? configuration["endpoint"]
- : configuration["endpoint"]
- ? `https://${configuration["endpoint"]}`
- : configuration["region"]
- ? `https://s3.${configuration["region"]}.amazonaws.com`
- : "https://s3.amazonaws.com";
-
- return {
- configuration: {
- type: "s3",
- endpoint,
- access_key_id: sensitive_fields?.find(
- ({ name }) => name === "access_key_id"
- )?.value,
- secret_access_key: sensitive_fields?.find(
- ({ name }) => name === "secret_access_key"
- )?.value,
- },
- readonly,
- source_path,
- target_path,
- };
- }
-
- if (storage_type === "azureblob" || configuration["type"] === "azureblob") {
- return {
- configuration: {
- type: "azureblob",
- endpoint: configuration["account"] ?? "",
- secret_access_key:
- sensitive_fields?.find(({ name }) => name === "secret_access_key")
- ?.value ?? "",
- },
- readonly,
- source_path,
- target_path,
- };
- }
-
- return null;
+ };
}
diff --git a/client/src/features/session/sessions.types.ts b/client/src/features/session/sessions.types.ts
index 0b3f257f66..facffa9046 100644
--- a/client/src/features/session/sessions.types.ts
+++ b/client/src/features/session/sessions.types.ts
@@ -110,19 +110,17 @@ export interface PatchSessionParams {
}
export interface CloudStorageDefinitionForSessionApi {
- configuration:
- | {
- type: "s3";
- endpoint: string;
- access_key_id?: string;
- secret_access_key?: string;
- }
- | {
- type: "azureblob";
- endpoint: string;
- secret_access_key: string;
- };
+ configuration: Record;
readonly: boolean;
source_path: string;
target_path: string;
}
+
+export interface NotebooksErrorContent {
+ code: number;
+ message: string;
+}
+
+export interface NotebooksErrorResponse {
+ error: NotebooksErrorContent;
+}
diff --git a/client/src/features/session/startSessionOptions.types.ts b/client/src/features/session/startSessionOptions.types.ts
index 82408aeb58..dfe0c45b06 100644
--- a/client/src/features/session/startSessionOptions.types.ts
+++ b/client/src/features/session/startSessionOptions.types.ts
@@ -16,6 +16,8 @@
* limitations under the License.
*/
+import { CloudStorageConfiguration } from "../project/components/cloudStorage/projectCloudStorage.types";
+
export interface StartSessionOptions {
branch: string;
cloudStorage: SessionCloudStorage[];
@@ -30,18 +32,9 @@ export interface StartSessionOptions {
storage: number;
}
-export interface SessionCloudStorage {
+export interface SessionCloudStorage extends CloudStorageConfiguration {
active: boolean;
- configuration: Record;
- name: string;
- private: boolean;
- readonly: boolean;
- source_path: string;
sensitive_fields?: { name: string; help: string; value: string }[];
- storage_id: string | null;
- storage_type: string;
- supported: boolean;
- target_path: string;
}
// ? See: ./components/options/SessionProjectDockerImage.md
diff --git a/client/src/features/versions/versions.api.ts b/client/src/features/versions/versions.api.ts
index 80f3017716..51c204ca91 100644
--- a/client/src/features/versions/versions.api.ts
+++ b/client/src/features/versions/versions.api.ts
@@ -137,17 +137,15 @@ export const versionsApi = createApi({
if (response.versions?.length < 1)
throw new Error("Unexpected response");
const singleVersion = response.versions[0];
+
return {
name: response.name,
version: singleVersion?.version ?? "unavailable",
anonymousSessionsEnabled:
singleVersion?.data?.anonymousSessionsEnabled ?? false,
sshEnabled: singleVersion?.data?.sshEnabled ?? false,
- cloudStorageEnabled: {
- s3: singleVersion?.data?.cloudstorageEnabled?.s3 ?? false,
- azureBlob:
- singleVersion?.data?.cloudstorageEnabled?.azure_blob ?? false,
- },
+ cloudStorageEnabled:
+ singleVersion?.data?.cloudstorageEnabled ?? false,
};
},
transformErrorResponse: () => {
@@ -156,10 +154,7 @@ export const versionsApi = createApi({
version: "unavailable",
anonymousSessionsEnabled: false,
sshEnabled: false,
- cloudStorageEnabled: {
- s3: false,
- azureBlob: false,
- },
+ cloudStorageEnabled: false,
} as NotebooksVersion;
},
providesTags: (result) =>
diff --git a/client/src/features/versions/versions.types.ts b/client/src/features/versions/versions.types.ts
index 2dd5a57877..638905e918 100644
--- a/client/src/features/versions/versions.types.ts
+++ b/client/src/features/versions/versions.types.ts
@@ -68,10 +68,7 @@ export interface KgVersion {
interface NotebookComponent extends BaseVersion {
data: {
anonymousSessionsEnabled: boolean;
- cloudstorageEnabled: {
- s3: boolean;
- azure_blob: boolean;
- };
+ cloudstorageEnabled: boolean;
sshEnabled: boolean;
};
}
@@ -82,12 +79,9 @@ export interface NotebooksVersionResponse extends BaseVersionResponse {
}
export interface NotebooksVersion {
- name: string;
- version: string;
anonymousSessionsEnabled: boolean;
+ cloudStorageEnabled: boolean;
+ name: string;
sshEnabled: boolean;
- cloudStorageEnabled: {
- s3: boolean;
- azureBlob: boolean;
- };
+ version: string;
}
diff --git a/client/src/styles/bootstrap/_custom_bootstrap_variables.scss b/client/src/styles/bootstrap/_custom_bootstrap_variables.scss
index d28388de7e..769fbbd4a4 100644
--- a/client/src/styles/bootstrap/_custom_bootstrap_variables.scss
+++ b/client/src/styles/bootstrap/_custom_bootstrap_variables.scss
@@ -106,3 +106,6 @@ $popover-bg: $rk-white;
// List styles
$list-group-bg: $transparent;
+
+// Breadcumb styles
+$breadcrumb-divider: quote(">");
diff --git a/client/src/utils/helpers/EnhancedState.ts b/client/src/utils/helpers/EnhancedState.ts
index 012395f2a5..71b74e9e4d 100644
--- a/client/src/utils/helpers/EnhancedState.ts
+++ b/client/src/utils/helpers/EnhancedState.ts
@@ -38,7 +38,7 @@ import { inactiveKgProjectsApi } from "../../features/inactiveKgProjects/Inactiv
import { kgInactiveProjectsSlice } from "../../features/inactiveKgProjects/inactiveKgProjectsSlice";
import { kgSearchApi } from "../../features/kgSearch";
import { datasetFormSlice } from "../../features/project/dataset";
-import projectCloudStorageApi from "../../features/project/projectCloudStorage.api";
+import projectCloudStorageApi from "../../features/project/components/cloudStorage/projectCloudStorage.api";
import { projectCoreApi } from "../../features/project/projectCoreApi";
import projectGitLabApi from "../../features/project/projectGitLab.api";
import { projectKgApi } from "../../features/project/projectKg.api";
diff --git a/tests/cypress.config.ts b/tests/cypress.config.ts
index 6998bf740b..052612220b 100644
--- a/tests/cypress.config.ts
+++ b/tests/cypress.config.ts
@@ -5,9 +5,6 @@ export default defineConfig({
baseUrl: "http://localhost:3000",
specPattern: "cypress/e2e/**/*.{js,jsx,ts,tsx}",
},
- env: {
- CLOUD_STORAGE_READWRITE_ENABLED: false,
- },
retries: {
runMode: 2,
openMode: 0,
diff --git a/tests/cypress/e2e/newSession.spec.ts b/tests/cypress/e2e/newSession.spec.ts
index 8c06014e6e..c6ee7d36cd 100644
--- a/tests/cypress/e2e/newSession.spec.ts
+++ b/tests/cypress/e2e/newSession.spec.ts
@@ -172,45 +172,25 @@ describe("launch sessions", () => {
cy.visit("/projects/e2e/local-test-project/sessions/new");
cy.wait("@getSessionImage", { timeout: 10000 });
cy.get(".form-label").contains("Cloud Storage").should("be.visible");
- cy.get("button")
- .contains("Add Temporary Cloud Storage")
- .should("be.visible")
- .and("not.be.disabled");
- cy.get(".card").contains("Example storage").should("be.visible");
- cy.get(".card")
- .contains("Mount point")
- .should("be.visible")
- .siblings()
+ cy.getDataCy("cloud-storage-item")
+ .contains("example-storage")
+ .should("be.visible");
+ cy.getDataCy("cloud-storage-item")
.contains("mount/path")
.should("be.visible");
-
- cy.get(".card")
- .find("button")
- .contains("More details")
- .should("be.visible")
- .click();
-
- cy.get("label")
- .contains("Source Path")
- .should("be.visible")
- .siblings("input")
- .should("have.value", "bucket/source")
+ cy.getDataCy("cloud-storage-item")
+ .contains("s3/Other")
+ .should("be.visible");
+ cy.getDataCy("cloud-storage-item")
+ .contains("mount/path")
.should("be.visible");
- cy.get("label")
- .contains("Mount Point")
- .should("be.visible")
- .siblings("input")
- .should("have.value", "mount/path")
+ cy.getDataCy("cloud-storage-details-toggle").should("be.visible").click();
+ cy.getDataCy("cloud-storage-details-section")
+ .contains("https://s3.example.com")
.should("be.visible");
- if (Cypress.env("CLOUD_STORAGE_READWRITE_ENABLED")) {
- cy.contains("Read-only").siblings("input").should("be.checked");
- cy.get("label")
- .contains("Read/Write")
- .siblings("input")
- .should("not.be.checked");
- }
+ cy.get("#cloud-storage-example-storage-active").should("be.checked");
});
});
diff --git a/tests/cypress/e2e/projectSettings.spec.ts b/tests/cypress/e2e/projectSettings.spec.ts
index 417a1a67a5..5354013a72 100644
--- a/tests/cypress/e2e/projectSettings.spec.ts
+++ b/tests/cypress/e2e/projectSettings.spec.ts
@@ -275,87 +275,75 @@ describe("Cloud storage settings page", () => {
cy.wait("@getNotebooksVersions");
cy.wait("@getCloudStorage");
- cy.getDataCy("settings-container")
+ cy.getDataCy("cloud-storage-section")
.find(".card")
- .contains("Example storage")
+ .contains("example-storage")
.should("be.visible");
- cy.getDataCy("settings-container")
- .find(".card")
- .contains("bucket/source")
+ cy.getDataCy("cloud-storage-item")
+ .contains("s3/Other")
.should("be.visible");
- cy.getDataCy("settings-container")
- .find(".card")
+ cy.getDataCy("cloud-storage-item")
.contains("mount/path")
.should("be.visible");
});
it("can update an existing storage", () => {
- fixtures.versions().cloudStorage();
+ fixtures.versions().cloudStorageStar();
fixtures.patchCloudStorage();
cy.visit("/projects/e2e/local-test-project/settings/storage");
cy.wait("@getNotebooksVersions");
cy.wait("@getCloudStorage");
- cy.getDataCy("settings-container")
+ cy.getDataCy("cloud-storage-section")
.find(".card")
- .contains("Example storage")
+ .contains("example-storage")
.should("be.visible")
.click();
- cy.getDataCy("settings-container")
- .find(".card")
+ cy.getDataCy("cloud-storage-details-toggle").should("be.visible").click();
+ cy.getDataCy("cloud-storage-details-buttons")
.find("button")
.contains("Edit")
+ .click();
+
+ cy.getDataCy("cloud-storage-edit-header")
+ .contains("Edit Cloud Storage")
+ .should("be.visible");
+ cy.get("#sourcePath")
+ .should("have.value", "bucket/source")
+ .type("{selectAll}bucket/new-target");
+ cy.getDataCy("cloud-storage-edit-next-button")
.should("be.visible")
+ .contains("Next")
.click();
- cy.get("label").contains("Name").click();
- cy.get(":focused").type("{selectAll}My special storage");
-
- cy.get("label").contains("Source path").click();
- cy.get(":focused").type("/subfolder");
-
- cy.get("label").contains("Mount point").click();
- cy.get(":focused").type("{selectAll}/mnt/special");
-
- cy.get("label")
- .contains("Requires credentials")
- .click()
- .siblings("input")
- .should("not.be.checked");
-
- if (Cypress.env("CLOUD_STORAGE_READWRITE_ENABLED")) {
- cy.get("label")
- .contains("Read/Write")
- .click()
- .siblings("input")
- .should("be.checked");
- cy.get("label")
- .contains("Read-only")
- .siblings("input")
- .should("not.be.checked");
- }
-
- cy.get("button[type='submit']")
- .contains("Save changes")
+ cy.get("#name")
+ .should("have.value", "example-storage")
+ .type("{selectAll}another-storage");
+ cy.get("#mountPoint")
+ .should("have.value", "mount/path")
+ .type("{selectAll}another/path");
+
+ cy.getDataCy("cloud-storage-edit-update-button")
+ .should("be.visible")
+ .contains("Update");
+ cy.getDataCy("cloud-storage-edit-back-button")
.should("be.visible")
+ .contains("Back")
.click();
- cy.wait("@patchCloudStorage").then(({ request }) => {
- const { body } = request;
- const {
- name,
- private: isPrivate,
- readonly,
- source_path,
- target_path,
- } = body;
-
- expect(name).to.equal("My special storage");
- expect(isPrivate).to.be.false;
- expect(readonly).to.be.undefined;
- expect(source_path).to.equal("bucket/source/subfolder");
- expect(target_path).to.equal("/mnt/special");
- });
+ cy.get("#sourcePath").should("have.value", "bucket/new-target");
+ cy.getDataCy("cloud-storage-edit-rest-button").should("be.visible").click();
+ cy.get("#sourcePath").should("have.value", "bucket/source");
+ cy.getDataCy("cloud-storage-edit-next-button")
+ .should("be.visible")
+ .contains("Next")
+ .click();
+ cy.get("#mountPoint").should("have.value", "mount/path");
+
+ cy.getDataCy("cloud-storage-edit-update-button")
+ .should("be.visible")
+ .contains("Update")
+ .click();
});
it("can remove an existing storage", () => {
@@ -365,17 +353,15 @@ describe("Cloud storage settings page", () => {
cy.wait("@getNotebooksVersions");
cy.wait("@getCloudStorage");
- cy.getDataCy("settings-container")
+ cy.getDataCy("cloud-storage-section")
.find(".card")
- .contains("Example storage")
+ .contains("example-storage")
.should("be.visible")
.click();
-
- cy.getDataCy("settings-container")
- .find(".card")
+ cy.getDataCy("cloud-storage-details-toggle").should("be.visible").click();
+ cy.getDataCy("cloud-storage-details-buttons")
.find("button")
.contains("Delete")
- .should("be.visible")
.click();
cy.get(".modal").contains("Are you sure?").should("be.visible");
@@ -389,6 +375,7 @@ describe("Cloud storage settings page", () => {
});
it("can add new storage (simple)", () => {
+ fixtures.getStorageSchema();
fixtures
.versions({
notebooks: { fixture: "version-notebooks-s3.json" },
@@ -399,82 +386,47 @@ describe("Cloud storage settings page", () => {
cy.wait("@getNotebooksVersions");
cy.wait("@getCloudStorage");
- cy.getDataCy("settings-container")
+ cy.getDataCy("cloud-storage-section")
.find("button")
.contains("Add Cloud Storage")
.should("be.visible")
.click();
- cy.get(".modal").contains("Add Cloud Storage").should("be.visible");
-
- cy.get("label").contains("Name").click();
- cy.get(":focused").type("My new storage");
-
- cy.get(".modal")
- .contains("For AWS S3 buckets, supported URLs are of the form")
+ cy.getDataCy("cloud-storage-edit-header")
+ .contains("Add Cloud Storage")
.should("be.visible");
- cy.get("label").contains("Endpoint URL").click();
- cy.get(":focused").type("s3://data.s3.eu-central-2.amazonaws.com/folder");
-
- cy.get("label")
- .contains("Requires credentials")
- .siblings("input")
- .should("be.checked");
-
- if (Cypress.env("CLOUD_STORAGE_READWRITE_ENABLED")) {
- cy.get("label")
- .contains("Read/Write")
- .click()
- .siblings("input")
- .should("be.checked");
- cy.get("label")
- .contains("Read-only")
- .siblings("input")
- .should("not.be.checked");
- }
-
- cy.get("button[type='submit']")
- .contains("Add Storage")
+
+ cy.getDataCy("cloud-storage-edit-schema")
+ .contains("webdav")
.should("be.visible")
.click();
+ cy.getDataCy("cloud-storage-edit-next-button").should("be.visible").click();
- cy.wait("@postCloudStorage").then(({ request }) => {
- const { body } = request;
- const { name, private: isPrivate, readonly, target_path } = body;
-
- expect(name).to.equal("My new storage");
- expect(isPrivate).to.be.true;
- expect(readonly).to.equal(
- !Cypress.env("CLOUD_STORAGE_READWRITE_ENABLED")
- );
- expect(target_path).to.equal("My new storage");
- });
-
- cy.get(".modal").contains("Please select which credentials are required");
-
- cy.get("label")
- .contains("first")
- .click()
- .siblings("input")
- .should("be.checked");
+ cy.get("#sourcePath").should("have.value", "").type("bucket/my-source");
+ cy.getDataCy("cloud-storage-edit-next-button").should("be.visible").click();
- cy.get("label")
- .contains("second")
- .siblings("input")
- .should("not.be.checked");
+ cy.get("#name").should("have.value", "").type("fake-storage");
+ cy.get("#mountPoint").should("have.value", "external_storage/fake-storage");
- cy.get("button[type='submit']")
- .contains("Finish cloud storage setup")
+ cy.getDataCy("cloud-storage-edit-update-button")
.should("be.visible")
+ .contains("Add")
.click();
- cy.wait("@patchCloudStorage").then(({ request }) => {
+ cy.wait("@postCloudStorage").then(({ request }) => {
const { body } = request;
- const { configuration } = body;
+ const { name, readonly, target_path } = body;
- expect(configuration).to.haveOwnProperty("first");
- expect(configuration["first"]).to.equal("");
- expect(configuration).not.to.haveOwnProperty("second");
+ expect(name).to.equal("fake-storage");
+ expect(readonly).to.be.false;
+ expect(target_path).to.equal("external_storage/fake-storage");
});
+
+ cy.getDataCy("cloud-storage-edit-body").contains(
+ "storage fake-storage has been succesfully added"
+ );
+ cy.getDataCy("cloud-storage-edit-close-button")
+ .should("be.visible")
+ .click();
});
});
diff --git a/tests/cypress/fixtures/cloudStorage/cloud-storage.json b/tests/cypress/fixtures/cloudStorage/cloud-storage.json
index 9f842c725a..7403455ff3 100644
--- a/tests/cypress/fixtures/cloudStorage/cloud-storage.json
+++ b/tests/cypress/fixtures/cloudStorage/cloud-storage.json
@@ -2,10 +2,11 @@
{
"storage": {
"configuration": {
+ "type": "s3",
"provider": "Other",
"endpoint": "https://s3.example.com"
},
- "name": "Example storage",
+ "name": "example-storage",
"private": true,
"project_id": 1,
"readonly": true,
diff --git a/tests/cypress/fixtures/cloudStorage/new-cloud-storage.json b/tests/cypress/fixtures/cloudStorage/new-cloud-storage.json
index f22f3fb31f..298cc5227d 100644
--- a/tests/cypress/fixtures/cloudStorage/new-cloud-storage.json
+++ b/tests/cypress/fixtures/cloudStorage/new-cloud-storage.json
@@ -4,14 +4,14 @@
"provider": "AWS",
"region": "eu-central-2"
},
- "name": "My new storage",
+ "name": "fake-storage",
"private": true,
"project_id": 1,
"readonly": false,
- "source_path": "data/folder",
+ "source_path": "bucket/my-source",
"storage_id": 2,
"storage_type": "s3",
- "target_path": "My new storage"
+ "target_path": "external_storage/webdav"
},
"sensitive_fields": [
{ "help": "First credential", "name": "first" },
diff --git a/tests/cypress/fixtures/cloudStorage/storage-schema.json b/tests/cypress/fixtures/cloudStorage/storage-schema.json
new file mode 100644
index 0000000000..f80ccc8755
--- /dev/null
+++ b/tests/cypress/fixtures/cloudStorage/storage-schema.json
@@ -0,0 +1,234 @@
+[
+ {
+ "name": "webdav",
+ "description": "WebDAV",
+ "prefix": "webdav",
+ "options": [
+ {
+ "name": "url",
+ "help": "URL of http host to connect to.\n\nE.g. https://example.com.",
+ "provider": "",
+ "default": "",
+ "value": null,
+ "examples": null,
+ "short_opt": "",
+ "hide": 0,
+ "required": true,
+ "is_password": false,
+ "no_prefix": false,
+ "advanced": false,
+ "exclusive": false,
+ "sensitive": false,
+ "default_str": "",
+ "value_str": "",
+ "type": "string"
+ },
+ {
+ "name": "vendor",
+ "help": "Name of the WebDAV site/service/software you are using.",
+ "provider": "",
+ "default": "",
+ "value": null,
+ "examples": [
+ {
+ "value": "fastmail",
+ "help": "Fastmail Files",
+ "provider": ""
+ },
+ {
+ "value": "nextcloud",
+ "help": "Nextcloud",
+ "provider": ""
+ },
+ {
+ "value": "owncloud",
+ "help": "Owncloud",
+ "provider": ""
+ },
+ {
+ "value": "sharepoint",
+ "help": "Sharepoint Online, authenticated by Microsoft account",
+ "provider": ""
+ },
+ {
+ "value": "sharepoint-ntlm",
+ "help": "Sharepoint with NTLM authentication, usually self-hosted or on-premises",
+ "provider": ""
+ },
+ {
+ "value": "other",
+ "help": "Other site/service or software",
+ "provider": ""
+ }
+ ],
+ "short_opt": "",
+ "hide": 0,
+ "required": false,
+ "is_password": false,
+ "no_prefix": false,
+ "advanced": false,
+ "exclusive": false,
+ "sensitive": false,
+ "default_str": "",
+ "value_str": "",
+ "type": "string"
+ },
+ {
+ "name": "user",
+ "help": "User name.\n\nIn case NTLM authentication is used, the username should be in the format 'Domain\\User'.",
+ "provider": "",
+ "default": "",
+ "value": null,
+ "examples": null,
+ "short_opt": "",
+ "hide": 0,
+ "required": false,
+ "is_password": false,
+ "no_prefix": false,
+ "advanced": false,
+ "exclusive": false,
+ "sensitive": false,
+ "default_str": "",
+ "value_str": "",
+ "type": "string"
+ },
+ {
+ "name": "pass",
+ "help": "Password.",
+ "provider": "",
+ "default": "",
+ "value": null,
+ "examples": null,
+ "short_opt": "",
+ "hide": 0,
+ "required": false,
+ "is_password": true,
+ "no_prefix": false,
+ "advanced": false,
+ "exclusive": false,
+ "sensitive": true,
+ "default_str": "",
+ "value_str": "",
+ "type": "string"
+ },
+ {
+ "name": "bearer_token",
+ "help": "Bearer token instead of user/pass (e.g. a Macaroon).",
+ "provider": "",
+ "default": "",
+ "value": null,
+ "examples": null,
+ "short_opt": "",
+ "hide": 0,
+ "required": false,
+ "is_password": false,
+ "no_prefix": false,
+ "advanced": false,
+ "exclusive": false,
+ "sensitive": true,
+ "default_str": "",
+ "value_str": "",
+ "type": "string"
+ },
+ {
+ "name": "bearer_token_command",
+ "help": "Command to run to get a bearer token.",
+ "provider": "",
+ "default": "",
+ "value": null,
+ "examples": null,
+ "short_opt": "",
+ "hide": 0,
+ "required": false,
+ "is_password": false,
+ "no_prefix": false,
+ "advanced": true,
+ "exclusive": false,
+ "sensitive": false,
+ "default_str": "",
+ "value_str": "",
+ "type": "string"
+ },
+ {
+ "name": "encoding",
+ "help": "The encoding for the backend.\n\nSee the [encoding section in the overview](/overview/#encoding) for more info.\n\nDefault encoding is Slash,LtGt,DoubleQuote,Colon,Question,Asterisk,Pipe,Hash,Percent,BackSlash,Del,Ctl,LeftSpace,LeftTilde,RightSpace,RightPeriod,InvalidUtf8 for sharepoint-ntlm or identity otherwise.",
+ "provider": "",
+ "default": "",
+ "value": null,
+ "examples": null,
+ "short_opt": "",
+ "hide": 0,
+ "required": false,
+ "is_password": false,
+ "no_prefix": false,
+ "advanced": true,
+ "exclusive": false,
+ "sensitive": false,
+ "default_str": "",
+ "value_str": "",
+ "type": "string"
+ },
+ {
+ "name": "headers",
+ "help": "Set HTTP headers for all transactions.\n\nUse this to set additional HTTP headers for all transactions\n\nThe input format is comma separated list of key,value pairs. Standard\n[CSV encoding](https://godoc.org/encoding/csv) may be used.\n\nFor example, to set a Cookie use 'Cookie,name=value', or '\"Cookie\",\"name=value\"'.\n\nYou can set multiple headers, e.g. '\"Cookie\",\"name=value\",\"Authorization\",\"xxx\"'.\n",
+ "provider": "",
+ "default": [],
+ "value": null,
+ "examples": null,
+ "short_opt": "",
+ "hide": 0,
+ "required": false,
+ "is_password": false,
+ "no_prefix": false,
+ "advanced": true,
+ "exclusive": false,
+ "sensitive": false,
+ "default_str": "",
+ "value_str": "",
+ "type": "CommaSepList"
+ },
+ {
+ "name": "pacer_min_sleep",
+ "help": "Minimum time to sleep between API calls.",
+ "provider": "",
+ "default": 10000000,
+ "value": null,
+ "examples": null,
+ "short_opt": "",
+ "hide": 0,
+ "required": false,
+ "is_password": false,
+ "no_prefix": false,
+ "advanced": true,
+ "exclusive": false,
+ "sensitive": false,
+ "default_str": "10ms",
+ "value_str": "10ms",
+ "type": "Duration"
+ },
+ {
+ "name": "nextcloud_chunk_size",
+ "help": "Nextcloud upload chunk size.\n\nWe recommend configuring your NextCloud instance to increase the max chunk size to 1 GB for better upload performances.\nSee https://docs.nextcloud.com/server/latest/admin_manual/configuration_files/big_file_upload_configuration.html#adjust-chunk-size-on-nextcloud-side\n\nSet to 0 to disable chunked uploading.\n",
+ "provider": "",
+ "default": 10485760,
+ "value": null,
+ "examples": null,
+ "short_opt": "",
+ "hide": 0,
+ "required": false,
+ "is_password": false,
+ "no_prefix": false,
+ "advanced": true,
+ "exclusive": false,
+ "sensitive": false,
+ "default_str": "10Mi",
+ "value_str": "10Mi",
+ "type": "SizeSuffix"
+ }
+ ],
+ "command_help": null,
+ "aliases": null,
+ "hide": false,
+ "metadata_info": null
+ }
+]
diff --git a/tests/cypress/support/renkulab-fixtures/cloudStorage.ts b/tests/cypress/support/renkulab-fixtures/cloudStorage.ts
index 4f9b0d344d..f2a02fca9f 100644
--- a/tests/cypress/support/renkulab-fixtures/cloudStorage.ts
+++ b/tests/cypress/support/renkulab-fixtures/cloudStorage.ts
@@ -26,6 +26,20 @@ import { NameOnlyFixture, SimpleFixture } from "./fixtures.types";
export function CloudStorage(Parent: T) {
return class CloudStorageFixtures extends Parent {
cloudStorage(args?: SimpleFixture) {
+ const {
+ fixture = "cloudStorage/cloud-storage.json",
+ name = "getCloudStorage",
+ } = args ?? {};
+ const response = { fixture };
+ cy.intercept(
+ "GET",
+ "/ui-server/api/data/storage?project_id=*",
+ response
+ ).as(name);
+ return this;
+ }
+
+ cloudStorageStar(args?: SimpleFixture) {
const {
fixture = "cloudStorage/cloud-storage.json",
name = "getCloudStorage",
@@ -35,6 +49,16 @@ export function CloudStorage(Parent: T) {
return this;
}
+ cloudStorageSPecific(args?: SimpleFixture) {
+ const {
+ fixture = "cloudStorage/cloud-storage.json",
+ name = "getCloudStorage",
+ } = args ?? {};
+ const response = { fixture };
+ cy.intercept("GET", "/ui-server/api/data/storage", response).as(name);
+ return this;
+ }
+
postCloudStorage(args?: SimpleFixture) {
const {
fixture = "cloudStorage/new-cloud-storage.json",
@@ -52,6 +76,18 @@ export function CloudStorage(Parent: T) {
return this;
}
+ getStorageSchema(args?: NameOnlyFixture) {
+ const { name = "getStorageSchema" } = args ?? {};
+ const response = {
+ fixture: "cloudStorage/storage-schema.json",
+ statusCode: 200,
+ };
+ cy.intercept("GET", "/ui-server/api/data/storage_schema", response).as(
+ name
+ );
+ return this;
+ }
+
deleteCloudStorage(args?: NameOnlyFixture) {
const { name = "deleteCloudStorage" } = args ?? {};
const response = { statusCode: 204 };