diff --git a/packages/react-storage/src/components/StorageBrowser/configuration/concurrencyContext.tsx b/packages/react-storage/src/components/StorageBrowser/configuration/concurrencyContext.tsx new file mode 100644 index 0000000000..95f5ec9cd4 --- /dev/null +++ b/packages/react-storage/src/components/StorageBrowser/configuration/concurrencyContext.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { createContextUtilities } from '@aws-amplify/ui-react-core'; +import { DEFAULT_ACTION_CONCURRENCY } from '../useAction/constants'; + +export interface ConcurrencyConfig { + concurrency: number; +} + +const ERROR_MESSAGE = + '`useConcurrencyConfig` must be called from within a `ConcurrencyConfigProvider`.'; + +export const { useConcurrencyConfig, ConcurrencyConfigContext } = + createContextUtilities({ + contextName: 'ConcurrencyConfig', + errorMessage: ERROR_MESSAGE, + }); + +export interface ConcurrencyConfigProviderProps { + children?: React.ReactNode; + concurrency?: number; +} + +export function ConcurrencyConfigProvider({ + children, + concurrency, +}: ConcurrencyConfigProviderProps): React.JSX.Element { + const value = React.useMemo( + () => ({ concurrency: concurrency ?? DEFAULT_ACTION_CONCURRENCY }), + [concurrency] + ); + + return ( + + {children} + + ); +} diff --git a/packages/react-storage/src/components/StorageBrowser/configuration/index.ts b/packages/react-storage/src/components/StorageBrowser/configuration/index.ts index 0eab98ad55..9288b4398f 100644 --- a/packages/react-storage/src/components/StorageBrowser/configuration/index.ts +++ b/packages/react-storage/src/components/StorageBrowser/configuration/index.ts @@ -6,3 +6,8 @@ export { PaginationConfigProvider, } from './paginationContext'; export type { PaginationConfig } from './paginationContext'; +export { + useConcurrencyConfig, + ConcurrencyConfigProvider, +} from './concurrencyContext'; +export type { ConcurrencyConfig } from './concurrencyContext'; diff --git a/packages/react-storage/src/components/StorageBrowser/createStorageBrowser/createProvider.tsx b/packages/react-storage/src/components/StorageBrowser/createStorageBrowser/createProvider.tsx index 4f378d32b7..5703c96397 100644 --- a/packages/react-storage/src/components/StorageBrowser/createStorageBrowser/createProvider.tsx +++ b/packages/react-storage/src/components/StorageBrowser/createStorageBrowser/createProvider.tsx @@ -11,6 +11,7 @@ import { import { createConfigurationProvider, PaginationConfigProvider, + ConcurrencyConfigProvider, } from '../configuration'; import { DisplayTextProvider } from '../displayText'; import { defaultValidateFile, FileItemsProvider } from '../fileItems'; @@ -77,7 +78,7 @@ export default function createProvider< ...components, }; - const { validateFile = defaultValidateFile } = options ?? {}; + const { validateFile = defaultValidateFile, concurrency } = options ?? {}; /** * Provides state, configuration and action values that are shared between @@ -94,25 +95,27 @@ export default function createProvider< - - - - - - - - - filePreview={filePreview} - > - {children} - - - - - - - - + + + + + + + + + + filePreview={filePreview} + > + {children} + + + + + + + + + diff --git a/packages/react-storage/src/components/StorageBrowser/createStorageBrowser/types.ts b/packages/react-storage/src/components/StorageBrowser/createStorageBrowser/types.ts index 010a8b0610..c836e9bfed 100644 --- a/packages/react-storage/src/components/StorageBrowser/createStorageBrowser/types.ts +++ b/packages/react-storage/src/components/StorageBrowser/createStorageBrowser/types.ts @@ -121,6 +121,21 @@ export interface StorageBrowserActions { } export interface StorageBrowserOptions { + /** + * @description Number of concurrent tasks to process for batch actions (upload, download, copy, delete) + * @default 4 + * @example + * ```tsx + * const { StorageBrowser } = createStorageBrowser({ + * config: managedAuthAdapter, + * options: { + * concurrency: 4, + * } + * }); + * ``` + */ + concurrency?: number; + /** * @description Overrides default file validation called when selecting files to be uploaded * @param {File} file — The file to validate diff --git a/packages/react-storage/src/components/StorageBrowser/useAction/__tests__/useHandler.spec.ts b/packages/react-storage/src/components/StorageBrowser/useAction/__tests__/useHandler.spec.ts index 4612d70ac8..4a35e5848e 100644 --- a/packages/react-storage/src/components/StorageBrowser/useAction/__tests__/useHandler.spec.ts +++ b/packages/react-storage/src/components/StorageBrowser/useAction/__tests__/useHandler.spec.ts @@ -1,6 +1,6 @@ import { renderHook } from '@testing-library/react'; -import { useGetActionInput } from '../../configuration'; +import { useGetActionInput, useConcurrencyConfig } from '../../configuration'; import { useProcessTasks } from '../../tasks'; import { useHandler } from '../useHandler'; @@ -10,6 +10,7 @@ jest.mock('../../configuration'); jest.mock('../../tasks'); const useProcessTasksMock = jest.mocked(useProcessTasks); +const useConcurrencyConfigMock = jest.mocked(useConcurrencyConfig); const config = { accountId: '123456789012', @@ -23,6 +24,10 @@ const useGetActionInputMock = jest .mocked(useGetActionInput) .mockReturnValue(getConfig); +useConcurrencyConfigMock.mockReturnValue({ + concurrency: DEFAULT_ACTION_CONCURRENCY, +}); + const handler = jest.fn(); const reset = jest.fn(); @@ -156,4 +161,27 @@ describe('useHandler', () => { onTaskSuccess: input.onTaskSuccess, }); }); + + it('uses global concurrency config from useConcurrencyConfig', () => { + const customConcurrency = 8; + useConcurrencyConfigMock.mockReturnValueOnce({ + concurrency: customConcurrency, + }); + + useProcessTasksMock.mockReturnValueOnce([ + mockBatchState, + mockUseProcessDispatch, + ]); + + const { result } = renderHook(() => useHandler(handler, { items: [] })); + + const [, dispatch] = result.current; + dispatch(); + + expect(useConcurrencyConfigMock).toHaveBeenCalledTimes(1); + expect(mockUseProcessDispatch).toHaveBeenCalledWith({ + config, + options: { concurrency: customConcurrency }, + }); + }); }); diff --git a/packages/react-storage/src/components/StorageBrowser/useAction/useHandler.ts b/packages/react-storage/src/components/StorageBrowser/useAction/useHandler.ts index 8492d880be..92595f3ff4 100644 --- a/packages/react-storage/src/components/StorageBrowser/useAction/useHandler.ts +++ b/packages/react-storage/src/components/StorageBrowser/useAction/useHandler.ts @@ -1,7 +1,6 @@ import React from 'react'; -import { useGetActionInput } from '../configuration'; -import { DEFAULT_ACTION_CONCURRENCY } from './constants'; +import { useGetActionInput, useConcurrencyConfig } from '../configuration'; import type { ActionHandler } from '../actions'; import type { Task } from '../tasks'; import { useProcessTasks } from '../tasks'; @@ -48,6 +47,7 @@ export function useHandler< ): HandleTasksState | HandleTaskState { const [state, handleProcessing] = useProcessTasks(handler, options); const getConfig = useGetActionInput(); + const { concurrency } = useConcurrencyConfig(); const { reset, isProcessing, tasks, ...rest } = state; @@ -64,10 +64,14 @@ export function useHandler< ...(hasData ? { data: input.data, all: [input.data] } : // if no `data` provided, provide `concurrency` to `options` - { options: { concurrency: DEFAULT_ACTION_CONCURRENCY } }), + { + options: { + concurrency, + }, + }), }); }, - [getConfig, handleProcessing, reset] + [getConfig, handleProcessing, reset, concurrency] ); if (isOptionsWithItems(options)) { diff --git a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadView/__tests__/useUploadView.spec.ts b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadView/__tests__/useUploadView.spec.ts index 08d024cb59..501f062388 100644 --- a/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadView/__tests__/useUploadView.spec.ts +++ b/packages/react-storage/src/components/StorageBrowser/views/LocationActionView/UploadView/__tests__/useUploadView.spec.ts @@ -14,6 +14,7 @@ jest.mock('../../../../configuration', () => ({ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion ...(jest.requireActual('../../../../configuration') as object), usePaginationConfig: () => ({ pageSize: 10 }), + useConcurrencyConfig: () => ({ concurrency: 4 }), })); const rootLocation: LocationData = {