diff --git a/dev-env/docker-compose-dev.yml b/dev-env/docker-compose-dev.yml index d42f96bf8..cf12b1cfb 100644 --- a/dev-env/docker-compose-dev.yml +++ b/dev-env/docker-compose-dev.yml @@ -72,6 +72,9 @@ services: -Ddataverse.files.s3.connection-pool-size=2048 -Ddataverse.files.s3.custom-endpoint-region=us-east-1 -Ddataverse.files.s3.custom-endpoint-url=https://s3.us-east-1.amazonaws.com + -Ddataverse.files.file1.type=file + -Ddataverse.files.file1.label=FileSystem + -Ddataverse.files.file1.storage-location=/dv/files/file1 # We publish the port on the host machine instead of just exposing it within the network, so that the browser can access the URLs of images generated by Dataverse (http://localhost:8080...). # This is necessary because the dev_nginx proxy is placed on top of the Dataverse service, making those URLs unreachable unless this port is exposed. # This workaround is only necessary and intended for the local dev environment and will not be used in the remote environment, where we use a production DNS. diff --git a/package-lock.json b/package-lock.json index c5b242488..30240671b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@dnd-kit/sortable": "8.0.0", "@dnd-kit/utilities": "3.2.2", "@faker-js/faker": "7.6.0", - "@iqss/dataverse-client-javascript": "2.1.0-alpha.4", + "@iqss/dataverse-client-javascript": "2.2.0-alpha.4", "@iqss/dataverse-design-system": "*", "@istanbuljs/nyc-config-typescript": "1.0.2", "@tanstack/react-table": "8.9.2", @@ -3285,9 +3285,9 @@ } }, "node_modules/@iqss/dataverse-client-javascript": { - "version": "2.1.0-alpha.4", - "resolved": "https://npm.pkg.github.com/download/@IQSS/dataverse-client-javascript/2.1.0-alpha.4/0a0dc68d4d99581d7ec017e58dbce3407f99f5d9", - "integrity": "sha512-UwHnFSYuvhxpc/JG2cFr5+bwERZXqGfNBTMSUQwtJaj6vfHO7anJAAxAx8g/AB8b4JtR9rljnYjAbsGJXicfBw==", + "version": "2.2.0-alpha.4", + "resolved": "https://npm.pkg.github.com/download/@IQSS/dataverse-client-javascript/2.2.0-alpha.4/d2c88452850b51c1fdd0024943f5c30209de30aa", + "integrity": "sha512-2ClUmlJib/7Gv1hieePvQ7h+9M1NFePo6bfG2fTCKLbEAogh9ByA5R58aefdq4X3QhN6sfc3O8FzFI/fP3NdoA==", "license": "MIT", "dependencies": { "@types/node": "^18.15.11", diff --git a/package.json b/package.json index 6d54dca10..7e58b5693 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "@dnd-kit/sortable": "8.0.0", "@dnd-kit/utilities": "3.2.2", "@faker-js/faker": "7.6.0", - "@iqss/dataverse-client-javascript": "2.1.0-alpha.4", + "@iqss/dataverse-client-javascript": "2.2.0-alpha.4", "@iqss/dataverse-design-system": "*", "@istanbuljs/nyc-config-typescript": "1.0.2", "@tanstack/react-table": "8.9.2", diff --git a/public/locales/en/collection.json b/public/locales/en/collection.json index 6690be4a1..c654adc0e 100644 --- a/public/locales/en/collection.json +++ b/public/locales/en/collection.json @@ -49,6 +49,7 @@ "helpText": "Share this collection on your favorite social media networks." }, "editedAlert": "You have successfully updated your collection!", + "storageDriverUpdateFailed": "The collection was created, but the storage driver could not be updated.", "editCollection": { "edit": "Edit", "generalInfo": "General Information", diff --git a/public/locales/es/collection.json b/public/locales/es/collection.json index adaa711ba..f5b299cfb 100644 --- a/public/locales/es/collection.json +++ b/public/locales/es/collection.json @@ -49,6 +49,7 @@ "helpText": "Comparte esta colección en tus redes sociales favoritas." }, "editedAlert": "¡Has actualizado tu colección correctamente!", + "storageDriverUpdateFailed": "La colección se creó, pero no se pudo actualizar el driver de almacenamiento.", "editCollection": { "edit": "Editar", "generalInfo": "Información general", diff --git a/src/collection/domain/models/AllowedStorageDrivers.ts b/src/collection/domain/models/AllowedStorageDrivers.ts new file mode 100644 index 000000000..8d0dc7eae --- /dev/null +++ b/src/collection/domain/models/AllowedStorageDrivers.ts @@ -0,0 +1 @@ +export type AllowedStorageDrivers = Record diff --git a/src/collection/domain/models/StorageDriver.ts b/src/collection/domain/models/StorageDriver.ts new file mode 100644 index 000000000..308ba635f --- /dev/null +++ b/src/collection/domain/models/StorageDriver.ts @@ -0,0 +1,8 @@ +export interface StorageDriver { + name: string + type?: string + label?: string + directUpload: boolean + directDownload: boolean + uploadOutOfBand: boolean +} diff --git a/src/collection/domain/repositories/CollectionRepository.ts b/src/collection/domain/repositories/CollectionRepository.ts index fbd456e8c..b0bafc293 100644 --- a/src/collection/domain/repositories/CollectionRepository.ts +++ b/src/collection/domain/repositories/CollectionRepository.ts @@ -13,6 +13,8 @@ import { PublicationStatus } from '@/shared/core/domain/models/PublicationStatus import { LinkingObjectType } from '../useCases/getCollectionsForLinking' import { CollectionSummary } from '../models/CollectionSummary' import { CollectionLinks } from '../models/CollectionLinks' +import { AllowedStorageDrivers } from '../models/AllowedStorageDrivers' +import { StorageDriver } from '../models/StorageDriver' export interface CollectionRepository { getById: (id?: string) => Promise @@ -58,4 +60,10 @@ export interface CollectionRepository { linkingCollectionIdOrAlias: number | string ): Promise getLinks(collectionIdOrAlias: number | string): Promise + getAllowedStorageDrivers(collectionIdOrAlias: number | string): Promise + getStorageDriver( + collectionIdOrAlias: number | string, + getEffective?: boolean + ): Promise + setStorageDriver(collectionIdOrAlias: number | string, driverLabel: string): Promise } diff --git a/src/collection/domain/useCases/getCollectionAllowedStorageDrivers.ts b/src/collection/domain/useCases/getCollectionAllowedStorageDrivers.ts new file mode 100644 index 000000000..783938760 --- /dev/null +++ b/src/collection/domain/useCases/getCollectionAllowedStorageDrivers.ts @@ -0,0 +1,9 @@ +import { AllowedStorageDrivers } from '../models/AllowedStorageDrivers' +import { CollectionRepository } from '../repositories/CollectionRepository' + +export async function getCollectionAllowedStorageDrivers( + collectionRepository: CollectionRepository, + collectionIdOrAlias: number | string +): Promise { + return collectionRepository.getAllowedStorageDrivers(collectionIdOrAlias) +} diff --git a/src/collection/domain/useCases/getCollectionStorageDriver.ts b/src/collection/domain/useCases/getCollectionStorageDriver.ts new file mode 100644 index 000000000..8a11af6c1 --- /dev/null +++ b/src/collection/domain/useCases/getCollectionStorageDriver.ts @@ -0,0 +1,10 @@ +import { StorageDriver } from '../models/StorageDriver' +import { CollectionRepository } from '../repositories/CollectionRepository' + +export async function getCollectionStorageDriver( + collectionRepository: CollectionRepository, + collectionIdOrAlias: number | string, + getEffective?: boolean +): Promise { + return collectionRepository.getStorageDriver(collectionIdOrAlias, getEffective) +} diff --git a/src/collection/domain/useCases/setCollectionDriver.ts b/src/collection/domain/useCases/setCollectionDriver.ts new file mode 100644 index 000000000..7e6d515dd --- /dev/null +++ b/src/collection/domain/useCases/setCollectionDriver.ts @@ -0,0 +1,9 @@ +import { CollectionRepository } from '../repositories/CollectionRepository' + +export async function setCollectionDriver( + collectionRepository: CollectionRepository, + collectionIdOrAlias: number | string, + driverLabel: string +): Promise { + return collectionRepository.setStorageDriver(collectionIdOrAlias, driverLabel) +} diff --git a/src/collection/infrastructure/repositories/CollectionJSDataverseRepository.ts b/src/collection/infrastructure/repositories/CollectionJSDataverseRepository.ts index 01b3cc6a5..5dc3f4c12 100644 --- a/src/collection/infrastructure/repositories/CollectionJSDataverseRepository.ts +++ b/src/collection/infrastructure/repositories/CollectionJSDataverseRepository.ts @@ -16,7 +16,10 @@ import { deleteCollectionFeaturedItem, getCollectionsForLinking, linkCollection, - getCollectionLinks + getCollectionLinks, + getAllowedCollectionStorageDrivers, + getCollectionStorageDriver, + setCollectionStorageDriver } from '@iqss/dataverse-client-javascript' import { JSCollectionMapper } from '../mappers/JSCollectionMapper' import { CollectionDTO } from '../../domain/useCases/DTOs/CollectionDTO' @@ -34,6 +37,8 @@ import { PublicationStatus } from '@/shared/core/domain/models/PublicationStatus import { CollectionSummary } from '@/collection/domain/models/CollectionSummary' import { LinkingObjectType } from '@/collection/domain/useCases/getCollectionsForLinking' import { CollectionLinks } from '@/collection/domain/models/CollectionLinks' +import { AllowedStorageDrivers } from '@/collection/domain/models/AllowedStorageDrivers' +import { StorageDriver } from '@/collection/domain/models/StorageDriver' export class CollectionJSDataverseRepository implements CollectionRepository { getById(id?: string): Promise { @@ -174,4 +179,19 @@ export class CollectionJSDataverseRepository implements CollectionRepository { getLinks(collectionIdOrAlias: number | string): Promise { return getCollectionLinks.execute(collectionIdOrAlias) } + + getAllowedStorageDrivers(collectionIdOrAlias: number | string): Promise { + return getAllowedCollectionStorageDrivers.execute(collectionIdOrAlias) + } + + getStorageDriver( + collectionIdOrAlias: number | string, + getEffective?: boolean + ): Promise { + return getCollectionStorageDriver.execute(collectionIdOrAlias, getEffective) + } + + setStorageDriver(collectionIdOrAlias: number | string, driverLabel: string): Promise { + return setCollectionStorageDriver.execute(collectionIdOrAlias, driverLabel) + } } diff --git a/src/sections/shared/form/EditCreateCollectionForm/EditCreateCollectionForm.tsx b/src/sections/shared/form/EditCreateCollectionForm/EditCreateCollectionForm.tsx index 2f5537d70..8a2dac73c 100644 --- a/src/sections/shared/form/EditCreateCollectionForm/EditCreateCollectionForm.tsx +++ b/src/sections/shared/form/EditCreateCollectionForm/EditCreateCollectionForm.tsx @@ -23,6 +23,8 @@ import { CollectionHelper } from '@/sections/collection/CollectionHelper' import { MetadataFieldsHelper } from '../DatasetMetadataForm/MetadataFieldsHelper' import { MetadataBlockInfo } from '@/metadata-block-info/domain/models/MetadataBlockInfo' import { useCollectionRepositories } from '@/shared/contexts/repositories/RepositoriesProvider' +import { useGetCollectionAllowedStorageDrivers } from '@/shared/hooks/useGetCollectionAllowedStorageDrivers' +import { useGetCollectionStorageDriver } from '@/shared/hooks/useGetCollectionStorageDriver' export const METADATA_BLOCKS_NAMES_GROUPER = 'metadataBlockNames' export const USE_FIELDS_FROM_PARENT = 'useFieldsFromParent' @@ -66,6 +68,8 @@ export const EditCreateCollectionForm = ({ const onEditMode = mode === 'edit' const isEditingRootCollection = onEditMode && CollectionHelper.isRootCollection(collection.hierarchy) + const canSelectStorageDriver = user.superuser + const storageDriverCollectionId = onEditMode ? collection.id : parentCollection.id const { metadataBlocksInfo, @@ -102,6 +106,27 @@ export const EditCreateCollectionForm = ({ metadataBlockInfoRepository }) + const { + allowedStorageDrivers, + isLoading: isLoadingAllowedStorageDrivers, + error: allowedStorageDriversError + } = useGetCollectionAllowedStorageDrivers({ + collectionIdOrAlias: storageDriverCollectionId, + collectionRepository, + enabled: canSelectStorageDriver + }) + + const { + storageDriver, + isLoading: isLoadingStorageDriver, + error: storageDriverError + } = useGetCollectionStorageDriver({ + collectionIdOrAlias: storageDriverCollectionId, + collectionRepository, + enabled: canSelectStorageDriver, + getEffective: true + }) + const baseInputLevels: FormattedCollectionInputLevels = useDeepCompareMemo(() => { return CollectionFormHelper.defineBaseInputLevels(allMetadataBlocksInfoNormalized) }, [allMetadataBlocksInfoNormalized]) @@ -149,13 +174,17 @@ export const EditCreateCollectionForm = ({ isLoadingMetadataBlocksInfo || isLoadingAllMetadataBlocksInfo || isLoadingCollectionFacets || - isLoadingFacetableMetadataFields + isLoadingFacetableMetadataFields || + isLoadingAllowedStorageDrivers || + isLoadingStorageDriver const dataLoadingErrors = [ metadataBlockInfoError, allMetadataBlocksInfoError, collectionFacetsError, - facetableMetadataFieldsError + facetableMetadataFieldsError, + allowedStorageDriversError, + storageDriverError ] useEffect(() => { @@ -204,6 +233,14 @@ export const EditCreateCollectionForm = ({ mode === 'edit' ? collection.isFacetRoot : undefined ) + const currentStorageDriverLabel = storageDriver?.label ?? storageDriver?.name + const defaultStorageDriver = + (currentStorageDriverLabel && allowedStorageDrivers[currentStorageDriverLabel] !== undefined + ? currentStorageDriverLabel + : undefined) ?? + Object.keys(allowedStorageDrivers)[0] ?? + '' + const formDefaultValues: CollectionFormData = { hostCollection: isEditingRootCollection ? null @@ -213,7 +250,7 @@ export const EditCreateCollectionForm = ({ type: onEditMode ? collection.type : '', contacts: defaultContacts, affiliation: onEditMode ? collection.affiliation ?? '' : user?.affiliation ?? '', - storage: 'S3', + storage: defaultStorageDriver, description: onEditMode ? collection.description ?? '' : '', [USE_FIELDS_FROM_PARENT]: useFieldsFromParentDefault, [METADATA_BLOCKS_NAMES_GROUPER]: defaultBlocksNames, @@ -231,6 +268,8 @@ export const EditCreateCollectionForm = ({ allFacetableMetadataFields={facetableMetadataFields} defaultCollectionFacets={defaultCollectionFacets} isEditingRootCollection={isEditingRootCollection} + canSelectStorageDriver={canSelectStorageDriver} + allowedStorageDrivers={allowedStorageDrivers} /> ) } diff --git a/src/sections/shared/form/EditCreateCollectionForm/collection-form/CollectionForm.tsx b/src/sections/shared/form/EditCreateCollectionForm/collection-form/CollectionForm.tsx index 0cdd082cb..dab8bcaad 100644 --- a/src/sections/shared/form/EditCreateCollectionForm/collection-form/CollectionForm.tsx +++ b/src/sections/shared/form/EditCreateCollectionForm/collection-form/CollectionForm.tsx @@ -17,6 +17,7 @@ import { EditCreateCollectionFormMode } from '../EditCreateCollectionForm' import { RouteWithParams } from '@/sections/Route.enum' import { useCollectionRepositories } from '@/shared/contexts/repositories/RepositoriesProvider' import styles from './CollectionForm.module.scss' +import { AllowedStorageDrivers } from '@/collection/domain/models/AllowedStorageDrivers' export interface CollectionFormProps { mode: EditCreateCollectionFormMode @@ -26,6 +27,8 @@ export interface CollectionFormProps { allFacetableMetadataFields: MetadataField[] defaultCollectionFacets: CollectionFormFacet[] isEditingRootCollection: boolean + canSelectStorageDriver: boolean + allowedStorageDrivers: AllowedStorageDrivers } export const CollectionForm = ({ @@ -35,7 +38,9 @@ export const CollectionForm = ({ allMetadataBlocksInfo, allFacetableMetadataFields, defaultCollectionFacets, - isEditingRootCollection + isEditingRootCollection, + canSelectStorageDriver, + allowedStorageDrivers }: CollectionFormProps) => { const { collectionRepository } = useCollectionRepositories() const formContainerRef = useRef(null) @@ -53,7 +58,8 @@ export const CollectionForm = ({ collectionIdOrParentCollectionId, collectionRepository, onSubmittedCollectionError, - form.formState.dirtyFields + form.formState.dirtyFields, + canSelectStorageDriver ) function onSubmittedCollectionError() { @@ -92,7 +98,11 @@ export const CollectionForm = ({ onSubmit={form.handleSubmit(submitForm)} noValidate={true} data-testid="collection-form"> - + diff --git a/src/sections/shared/form/EditCreateCollectionForm/collection-form/top-fields-section/StorageField.tsx b/src/sections/shared/form/EditCreateCollectionForm/collection-form/top-fields-section/StorageField.tsx new file mode 100644 index 000000000..349109115 --- /dev/null +++ b/src/sections/shared/form/EditCreateCollectionForm/collection-form/top-fields-section/StorageField.tsx @@ -0,0 +1,46 @@ +import { Controller, useFormContext } from 'react-hook-form' +import { useTranslation } from 'react-i18next' +import { Col, Form } from '@iqss/dataverse-design-system' +import { AllowedStorageDrivers } from '@/collection/domain/models/AllowedStorageDrivers' + +interface StorageFieldProps { + allowedStorageDrivers: AllowedStorageDrivers +} + +export const StorageField = ({ allowedStorageDrivers }: StorageFieldProps) => { + const { t } = useTranslation('shared', { keyPrefix: 'collectionForm' }) + const { control } = useFormContext() + const storageDriverOptions = Object.entries(allowedStorageDrivers) + + if (storageDriverOptions.length === 0) { + return null + } + + return ( + + + {t('fields.storage.label')} + + ( + + + {storageDriverOptions.map(([driverLabel, displayName]) => ( + + ))} + + {error?.message} + + )} + /> + + ) +} diff --git a/src/sections/shared/form/EditCreateCollectionForm/collection-form/top-fields-section/TopFieldsSection.tsx b/src/sections/shared/form/EditCreateCollectionForm/collection-form/top-fields-section/TopFieldsSection.tsx index 2985417d8..1d019451e 100644 --- a/src/sections/shared/form/EditCreateCollectionForm/collection-form/top-fields-section/TopFieldsSection.tsx +++ b/src/sections/shared/form/EditCreateCollectionForm/collection-form/top-fields-section/TopFieldsSection.tsx @@ -6,12 +6,20 @@ import { collectionTypeOptions } from '@/collection/domain/useCases/DTOs/Collect import { ContactsField } from './ContactsField' import { IdentifierField } from './IdentifierField' import { DescriptionField } from './DescriptionField' +import { StorageField } from './StorageField' +import { AllowedStorageDrivers } from '@/collection/domain/models/AllowedStorageDrivers' interface TopFieldsSectionProps { isEditingRootCollection: boolean + canSelectStorageDriver: boolean + allowedStorageDrivers: AllowedStorageDrivers } -export const TopFieldsSection = ({ isEditingRootCollection }: TopFieldsSectionProps) => { +export const TopFieldsSection = ({ + isEditingRootCollection, + canSelectStorageDriver, + allowedStorageDrivers +}: TopFieldsSectionProps) => { const { t } = useTranslation('shared', { keyPrefix: 'collectionForm' }) const { control } = useFormContext() @@ -152,34 +160,7 @@ export const TopFieldsSection = ({ isEditingRootCollection }: TopFieldsSectionPr - {/* 👇 To be defined, at the moment the SPA only supports file uploading through direct upload (S3), so we are disabling the storage selector */} - {/* - - {t('fields.storage.label')} - - ( - - - - {Object.values(collectionStorageOptions).map((type) => ( - - ))} - - {error?.message} - - )} - /> - */} + {canSelectStorageDriver && } {/* Category (type) & Email (contacts) & Description */} diff --git a/src/sections/shared/form/EditCreateCollectionForm/collection-form/useSubmitCollection.ts b/src/sections/shared/form/EditCreateCollectionForm/collection-form/useSubmitCollection.ts index 6adb44af9..2253f7441 100644 --- a/src/sections/shared/form/EditCreateCollectionForm/collection-form/useSubmitCollection.ts +++ b/src/sections/shared/form/EditCreateCollectionForm/collection-form/useSubmitCollection.ts @@ -19,6 +19,7 @@ import { import { CollectionDTO } from '@/collection/domain/useCases/DTOs/CollectionDTO' import { createCollection } from '@/collection/domain/useCases/createCollection' import { editCollection } from '@/collection/domain/useCases/editCollection' +import { setCollectionDriver } from '@/collection/domain/useCases/setCollectionDriver' import { RouteWithParams } from '@/sections/Route.enum' import { JSDataverseWriteErrorHandler } from '@/shared/helpers/JSDataverseWriteErrorHandler' import { CollectionFormHelper } from '../CollectionFormHelper' @@ -51,7 +52,8 @@ export function useSubmitCollection( collectionIdOrParentCollectionId: string, collectionRepository: CollectionRepository, onSubmitErrorCallback: () => void, - dirtyFields: CollectionFormDirtyFields + dirtyFields: CollectionFormDirtyFields, + canSelectStorageDriver: boolean ): UseSubmitCollectionReturnType { const navigate = useNavigate() const { t } = useTranslation('collection') @@ -101,13 +103,32 @@ export function useSubmitCollection( inheritFacetsFromParent: useFacetsFromParentChecked } + const shouldSetStorageDriver = + canSelectStorageDriver && Boolean(formData.storage) && Boolean(dirtyFields.storage) + + const setSelectedStorageDriver = (collectionIdOrAlias: number | string): Promise => { + if (!shouldSetStorageDriver) { + return Promise.resolve() + } + + return setCollectionDriver(collectionRepository, collectionIdOrAlias, formData.storage).then( + () => undefined + ) + } + if (mode === 'create') { createCollection( collectionRepository, newOrUpdatedCollection, collectionIdOrParentCollectionId ) - .then(() => { + .then(async (newCollectionIdentifier) => { + try { + await setSelectedStorageDriver(newCollectionIdentifier) + } catch { + toast.error(t('storageDriverUpdateFailed')) + } + setSubmitError(null) setSubmissionStatus(SubmissionStatus.SubmitComplete) needsUpdateStore.setNeedsUpdate(true) @@ -126,6 +147,7 @@ export function useSubmitCollection( }) } else { editCollection(collectionRepository, newOrUpdatedCollection, collectionIdOrParentCollectionId) + .then(() => setSelectedStorageDriver(collectionIdOrParentCollectionId)) .then(() => { setSubmitError(null) setSubmissionStatus(SubmissionStatus.SubmitComplete) diff --git a/src/sections/shared/form/EditCreateCollectionForm/types.ts b/src/sections/shared/form/EditCreateCollectionForm/types.ts index 9f4c04e9b..c07b40389 100644 --- a/src/sections/shared/form/EditCreateCollectionForm/types.ts +++ b/src/sections/shared/form/EditCreateCollectionForm/types.ts @@ -1,5 +1,4 @@ import { type FieldNamesMarkedBoolean } from 'react-hook-form' -import { CollectionStorage } from '@/collection/domain/useCases/DTOs/CollectionDTO' import { MetadataBlockInfo, MetadataField @@ -17,7 +16,7 @@ export type CollectionFormData = { name: string affiliation: string alias: string - storage: CollectionStorage + storage: string type: CollectionType | '' description: string contacts: { value: string }[] diff --git a/src/shared/hooks/useGetCollectionAllowedStorageDrivers.ts b/src/shared/hooks/useGetCollectionAllowedStorageDrivers.ts new file mode 100644 index 000000000..de7121413 --- /dev/null +++ b/src/shared/hooks/useGetCollectionAllowedStorageDrivers.ts @@ -0,0 +1,64 @@ +import { useEffect, useState } from 'react' +import { AllowedStorageDrivers } from '@/collection/domain/models/AllowedStorageDrivers' +import { CollectionRepository } from '@/collection/domain/repositories/CollectionRepository' +import { getCollectionAllowedStorageDrivers } from '@/collection/domain/useCases/getCollectionAllowedStorageDrivers' + +interface Props { + collectionIdOrAlias: number | string + collectionRepository: CollectionRepository + enabled?: boolean +} + +interface UseGetCollectionAllowedStorageDriversReturn { + allowedStorageDrivers: AllowedStorageDrivers + error: string | null + isLoading: boolean +} + +export const useGetCollectionAllowedStorageDrivers = ({ + collectionIdOrAlias, + collectionRepository, + enabled = true +}: Props): UseGetCollectionAllowedStorageDriversReturn => { + const [allowedStorageDrivers, setAllowedStorageDrivers] = useState({}) + const [isLoading, setIsLoading] = useState(enabled) + const [error, setError] = useState(null) + + useEffect(() => { + if (!enabled) { + setAllowedStorageDrivers({}) + setIsLoading(false) + setError(null) + return + } + + const handleGetCollectionAllowedStorageDrivers = async () => { + setIsLoading(true) + try { + const allowedStorageDrivers = await getCollectionAllowedStorageDrivers( + collectionRepository, + collectionIdOrAlias + ) + + setAllowedStorageDrivers(allowedStorageDrivers) + setError(null) + } catch (err) { + const errorMessage = + err instanceof Error && err.message + ? err.message + : 'Something went wrong getting the allowed storage drivers for this collection. Try again later.' + setError(errorMessage) + } finally { + setIsLoading(false) + } + } + + void handleGetCollectionAllowedStorageDrivers() + }, [collectionIdOrAlias, collectionRepository, enabled]) + + return { + allowedStorageDrivers, + error, + isLoading + } +} diff --git a/src/shared/hooks/useGetCollectionStorageDriver.ts b/src/shared/hooks/useGetCollectionStorageDriver.ts new file mode 100644 index 000000000..e9762aa48 --- /dev/null +++ b/src/shared/hooks/useGetCollectionStorageDriver.ts @@ -0,0 +1,67 @@ +import { useEffect, useState } from 'react' +import { StorageDriver } from '@/collection/domain/models/StorageDriver' +import { CollectionRepository } from '@/collection/domain/repositories/CollectionRepository' +import { getCollectionStorageDriver } from '@/collection/domain/useCases/getCollectionStorageDriver' + +interface Props { + collectionIdOrAlias: number | string + collectionRepository: CollectionRepository + enabled?: boolean + getEffective?: boolean +} + +interface UseGetCollectionStorageDriverReturn { + storageDriver: StorageDriver | null + error: string | null + isLoading: boolean +} + +export const useGetCollectionStorageDriver = ({ + collectionIdOrAlias, + collectionRepository, + enabled = true, + getEffective +}: Props): UseGetCollectionStorageDriverReturn => { + const [storageDriver, setStorageDriver] = useState(null) + const [isLoading, setIsLoading] = useState(enabled) + const [error, setError] = useState(null) + + useEffect(() => { + if (!enabled) { + setStorageDriver(null) + setIsLoading(false) + setError(null) + return + } + + const handleGetCollectionStorageDriver = async () => { + setIsLoading(true) + try { + const storageDriver = await getCollectionStorageDriver( + collectionRepository, + collectionIdOrAlias, + getEffective + ) + + setStorageDriver(storageDriver) + setError(null) + } catch (err) { + const errorMessage = + err instanceof Error && err.message + ? err.message + : 'Something went wrong getting the storage driver for this collection. Try again later.' + setError(errorMessage) + } finally { + setIsLoading(false) + } + } + + void handleGetCollectionStorageDriver() + }, [collectionIdOrAlias, collectionRepository, enabled, getEffective]) + + return { + storageDriver, + error, + isLoading + } +} diff --git a/src/stories/collection/CollectionMockRepository.ts b/src/stories/collection/CollectionMockRepository.ts index 5406ff8ed..99d2459e1 100644 --- a/src/stories/collection/CollectionMockRepository.ts +++ b/src/stories/collection/CollectionMockRepository.ts @@ -19,6 +19,8 @@ import { CollectionSummary } from '@/collection/domain/models/CollectionSummary' import { LinkingObjectType } from '@/collection/domain/useCases/getCollectionsForLinking' import { CollectionSummaryMother } from '@tests/component/collection/domain/models/CollectionSummaryMother' import { CollectionLinks } from '@/collection/domain/models/CollectionLinks' +import { AllowedStorageDrivers } from '@/collection/domain/models/AllowedStorageDrivers' +import { StorageDriver } from '@/collection/domain/models/StorageDriver' export class CollectionMockRepository implements CollectionRepository { getById(_id?: string): Promise { @@ -249,4 +251,38 @@ export class CollectionMockRepository implements CollectionRepository { }, FakerHelper.loadingTimout()) }) } + + getAllowedStorageDrivers(_collectionIdOrAlias: number | string): Promise { + return new Promise((resolve) => { + setTimeout(() => { + resolve({ S3: 'S3' }) + }, FakerHelper.loadingTimout()) + }) + } + + getStorageDriver( + _collectionIdOrAlias: number | string, + _getEffective?: boolean + ): Promise { + return new Promise((resolve) => { + setTimeout(() => { + resolve({ + name: 's3', + type: 's3', + label: 'S3', + directUpload: true, + directDownload: true, + uploadOutOfBand: false + }) + }, FakerHelper.loadingTimout()) + }) + } + + setStorageDriver(_collectionIdOrAlias: number | string, _driverLabel: string): Promise { + return new Promise((resolve) => { + setTimeout(() => { + resolve('Storage driver updated.') + }, FakerHelper.loadingTimout()) + }) + } } diff --git a/src/stories/create-collection/CreateCollection.stories.tsx b/src/stories/create-collection/CreateCollection.stories.tsx index 2628eeb39..86a841a86 100644 --- a/src/stories/create-collection/CreateCollection.stories.tsx +++ b/src/stories/create-collection/CreateCollection.stories.tsx @@ -11,7 +11,9 @@ import { FakerHelper } from '../../../tests/component/shared/FakerHelper' import { MetadataBlockInfoMockRepository } from '../shared-mock-repositories/metadata-block-info/MetadataBlockInfoMockRepository' import { MetadataBlockInfoMockLoadingRepository } from '../shared-mock-repositories/metadata-block-info/MetadataBlockInfoMockLoadingRepository' import { MetadataBlockInfoMockErrorRepository } from '../shared-mock-repositories/metadata-block-info/MetadataBlockInfoMockErrorRepository' -import { WithRepositories } from '../WithRepositories' +import { RepositoriesStoryProvider, WithRepositories } from '../WithRepositories' +import { SessionContext } from '@/sections/session/SessionContext' +import { UserMother } from '@tests/component/users/domain/models/UserMother' import { ROOT_COLLECTION_ALIAS } from '@tests/e2e-integration/shared/collection/ROOT_COLLECTION_ALIAS' @@ -36,6 +38,45 @@ export const Default: Story = { /> ) } + +const collectionRepositoryWithStorageDrivers = new CollectionMockRepository() +collectionRepositoryWithStorageDrivers.getAllowedStorageDrivers = () => { + return Promise.resolve({ + s3: 's3 (Default)', + file1: 'FileSystem' + }) +} +collectionRepositoryWithStorageDrivers.getStorageDriver = () => { + return Promise.resolve({ + name: 's3', + type: 's3', + label: 's3', + directUpload: true, + directDownload: true, + uploadOutOfBand: false + }) +} + +export const SuperUserWithStorageDriver: Story = { + render: () => ( + {}, + isLoadingUser: false, + sessionError: null, + refetchUserSession: () => Promise.resolve() + }}> + + + + + ) +} + export const Loading: Story = { decorators: [WithRepositories({ collectionRepository: new CollectionLoadingMockRepository() })], render: () => ( diff --git a/tests/component/sections/create-collection/CreateCollection.spec.tsx b/tests/component/sections/create-collection/CreateCollection.spec.tsx index c6a7cd033..6925431a8 100644 --- a/tests/component/sections/create-collection/CreateCollection.spec.tsx +++ b/tests/component/sections/create-collection/CreateCollection.spec.tsx @@ -54,7 +54,7 @@ describe('CreateCollection', () => { return Cypress.Promise.delay(DELAYED_TIME).then(() => collection) }) - cy.customMount( + cy.mountAuthenticated( { }) it('should render the correct breadcrumbs', () => { - cy.customMount( + cy.mountAuthenticated( { it('should show page not found when owner collection does not exist', () => { collectionRepository.getById = cy.stub().resolves(null) - cy.customMount( + cy.mountAuthenticated( { return Cypress.Promise.delay(DELAYED_TIME).then(() => collection) }) - cy.customMount( + cy.mountAuthenticated( { }) it('should render the correct breadcrumbs', () => { - cy.customMount( + cy.mountAuthenticated( { it('should show page not found when collection does not exist', () => { collectionRepository.getById = cy.stub().resolves(null) - cy.customMount( + cy.mountAuthenticated( { collectionRepository.create = cy.stub().resolves(1) collectionRepository.edit = cy.stub().resolves({}) collectionRepository.getFacets = cy.stub().resolves(collectionFacets) + collectionRepository.getAllowedStorageDrivers = cy.stub().resolves(allowedStorageDrivers) + collectionRepository.getStorageDriver = cy.stub().resolves(s3StorageDriver) + collectionRepository.setStorageDriver = cy.stub().resolves('Storage driver updated.') userRepository.getAuthenticated = cy.stub().resolves(testUser) metadataBlockInfoRepository.getByCollectionId = cy.stub().resolves(colllectionMetadataBlocks) metadataBlockInfoRepository.getAll = cy.stub().resolves(allMetadataBlocksMock) @@ -156,6 +174,39 @@ describe('EditCreateCollectionForm', () => { cy.findByLabelText(/^Email/i).should('have.value', testUser.email) }) + it('does not show the storage field when the current user is not a superuser', () => { + cy.mountAuthenticated( + + ) + + cy.findByTestId('collection-form').should('exist') + cy.get('body').should('not.contain', 'Storage') + }) + + it('shows the allowed storage drivers when the current user is a superuser', () => { + cy.mountAuthenticated( + , + undefined, + { superuser: true } + ) + + cy.findByLabelText(/^Storage/i).should('have.value', 's3') + cy.findByRole('option', { name: 's3 (Default)' }).should('exist') + cy.findByRole('option', { name: 'Swift' }).should('exist') + }) + it('submit button should be disabled when form has not been touched', () => { cy.customMount( { cy.get('@setNeedsUpdate').should('have.been.calledWith', true) }) + it('sets the selected storage driver after creating a collection as a superuser', () => { + collectionRepository.setStorageDriver = cy.stub().as('setStorageDriver').resolves() + + cy.mountAuthenticated( + , + undefined, + { superuser: true } + ) + + cy.findByRole('button', { name: 'Apply suggestion' }).click() + cy.findByLabelText(/^Category/i).select(1) + cy.findByLabelText(/^Storage/i).select('swift') + + cy.findByRole('button', { name: 'Create Collection' }).click() + + cy.get('@setStorageDriver').should('have.been.calledWith', 1, 'swift') + }) + + it('does not fail collection creation when setting the selected storage driver fails', () => { + cy.spy(needsUpdateStore, 'setNeedsUpdate').as('setNeedsUpdate') + collectionRepository.setStorageDriver = cy + .stub() + .as('setStorageDriver') + .rejects(new Error('Error setting storage driver')) + + cy.mountAuthenticated( + , + undefined, + { superuser: true } + ) + + cy.findByRole('button', { name: 'Apply suggestion' }).click() + cy.findByLabelText(/^Category/i).select(1) + cy.findByLabelText(/^Storage/i).select('swift') + + cy.findByRole('button', { name: 'Create Collection' }).click() + + cy.get('@setStorageDriver').should('have.been.calledWith', 1, 'swift') + cy.findByText('Error').should('not.exist') + cy.findByText('Success!').should('exist') + cy.findByText( + 'The collection was created, but the storage driver could not be updated.' + ).should('exist') + cy.get('@setNeedsUpdate').should('have.been.calledWith', true) + }) + it('submits a valid form and fails', () => { collectionRepository.create = cy.stub().rejects(new Error('Error creating collection')) @@ -1153,6 +1262,32 @@ describe('EditCreateCollectionForm', () => { cy.findByText('Success!').should('exist') }) + it('sets the selected storage driver after editing a collection as a superuser', () => { + collectionRepository.setStorageDriver = cy.stub().as('setStorageDriver').resolves() + + cy.mountAuthenticated( + , + undefined, + { superuser: true } + ) + + cy.findByLabelText(/^Storage/i).select('swift') + cy.findByRole('button', { name: 'Save Changes' }).click() + + cy.get('@setStorageDriver').should( + 'have.been.calledWith', + COLLECTION_BEING_EDITED_ID, + 'swift' + ) + }) + it('submits a valid form and fails', () => { collectionRepository.edit = cy.stub().rejects(new Error('Error editing collection'))