diff --git a/.changeset/loud-walls-do.md b/.changeset/loud-walls-do.md
new file mode 100644
index 00000000000..d165c85a04f
--- /dev/null
+++ b/.changeset/loud-walls-do.md
@@ -0,0 +1,5 @@
+---
+'@aws-amplify/ui-react-storage': minor
+---
+
+Support for multiple buckets added to storage image and file uploader
diff --git a/docs/src/pages/[platform]/connected-components/storage/fileuploader/examples/BucketExact.tsx b/docs/src/pages/[platform]/connected-components/storage/fileuploader/examples/BucketExact.tsx
new file mode 100644
index 00000000000..ee307913098
--- /dev/null
+++ b/docs/src/pages/[platform]/connected-components/storage/fileuploader/examples/BucketExact.tsx
@@ -0,0 +1,15 @@
+import { FileUploader } from '@aws-amplify/ui-react-storage';
+
+export const App = () => {
+ return (
+
+ );
+};
diff --git a/docs/src/pages/[platform]/connected-components/storage/fileuploader/examples/BucketFriendly.tsx b/docs/src/pages/[platform]/connected-components/storage/fileuploader/examples/BucketFriendly.tsx
new file mode 100644
index 00000000000..92403a00301
--- /dev/null
+++ b/docs/src/pages/[platform]/connected-components/storage/fileuploader/examples/BucketFriendly.tsx
@@ -0,0 +1,12 @@
+import { FileUploader } from '@aws-amplify/ui-react-storage';
+
+export const App = () => {
+ return (
+
+ );
+};
diff --git a/docs/src/pages/[platform]/connected-components/storage/fileuploader/props.ts b/docs/src/pages/[platform]/connected-components/storage/fileuploader/props.ts
index 7ff4ae22a7f..2fbc79b67c7 100644
--- a/docs/src/pages/[platform]/connected-components/storage/fileuploader/props.ts
+++ b/docs/src/pages/[platform]/connected-components/storage/fileuploader/props.ts
@@ -49,6 +49,12 @@ export const FILE_UPLOADER = [
'Determines if the upload will automatically start after a file is selected. The default value is `true`',
type: 'boolean',
},
+ {
+ name: 'bucket?',
+ description:
+ 'The S3 bucket which be will accessed. Allows either a string containing the user-assigned "friendly name" or an object containing a combination of the backend-assigned name on S3 and the S3 region.',
+ type: 'string | { bucketName: string, region: string }',
+ },
{
name: `maxFileCount`,
description: '',
diff --git a/docs/src/pages/[platform]/connected-components/storage/fileuploader/react.mdx b/docs/src/pages/[platform]/connected-components/storage/fileuploader/react.mdx
index 1d25a6d9700..49abab61979 100644
--- a/docs/src/pages/[platform]/connected-components/storage/fileuploader/react.mdx
+++ b/docs/src/pages/[platform]/connected-components/storage/fileuploader/react.mdx
@@ -168,6 +168,26 @@ You can limit what users upload with these 3 props:
+## Setting a Bucket
+
+If you have [configured your Amplify project to use multiple S3 buckets](https://docs.amplify.aws/react/build-a-backend/storage/set-up-storage/#configure-additional-storage-buckets), you can use the `bucket` prop to choose which of the buckets the component will use:
+
+
+
+
+ ```jsx file=./examples/BucketFriendly.tsx
+ ```
+
+
+
+Alternatively, you can specify the bucket using the name and region it is assigned within S3:
+
+
+
+ ```jsx file=./examples/BucketExact.tsx
+ ```
+
+
## Pausable / Resumable Uploads
diff --git a/docs/src/pages/[platform]/connected-components/storage/storageimage/props.ts b/docs/src/pages/[platform]/connected-components/storage/storageimage/props.ts
index 86a705020b7..3583fcac317 100644
--- a/docs/src/pages/[platform]/connected-components/storage/storageimage/props.ts
+++ b/docs/src/pages/[platform]/connected-components/storage/storageimage/props.ts
@@ -10,6 +10,12 @@ export const STORAGE_IMAGE = [
'The path to the image in Storage, representing a full S3 object key. See https://docs.amplify.aws/react/build-a-backend/storage/download-files/',
type: 'string | ((input: { identityId?: string }) => string);',
},
+ {
+ name: 'bucket?',
+ description:
+ 'The S3 bucket which be will accessed. Allows either a string containing the user-assigned "friendly name" or an object containing a combination of the backend-assigned name on S3 and the S3 region.',
+ type: 'string | { bucketName: string, region: string }',
+ },
{
name: 'imgKey',
description:
diff --git a/examples/next/pages/ui/components/storage/storage-image/multi-bucket/amplify_outputs.js b/examples/next/pages/ui/components/storage/storage-image/multi-bucket/amplify_outputs.js
new file mode 100644
index 00000000000..aa7b2dc0d65
--- /dev/null
+++ b/examples/next/pages/ui/components/storage/storage-image/multi-bucket/amplify_outputs.js
@@ -0,0 +1,2 @@
+import amplifyOutputs from '@environments/storage/gen2/amplify_outputs';
+export default amplifyOutputs;
diff --git a/examples/next/pages/ui/components/storage/storage-image/multi-bucket/index.page.tsx b/examples/next/pages/ui/components/storage/storage-image/multi-bucket/index.page.tsx
new file mode 100644
index 00000000000..14dff8fd98a
--- /dev/null
+++ b/examples/next/pages/ui/components/storage/storage-image/multi-bucket/index.page.tsx
@@ -0,0 +1,42 @@
+import * as React from 'react';
+
+import { Amplify } from 'aws-amplify';
+import { Text, Loader } from '@aws-amplify/ui-react';
+import { StorageImage } from '@aws-amplify/ui-react-storage';
+import '@aws-amplify/ui-react/styles.css';
+import amplifyOutputs from './amplify_outputs';
+
+Amplify.configure(amplifyOutputs);
+
+export function StorageImageExample() {
+ const [isFirstImgLoaded, setIsFirstImgLoaded] = React.useState(false);
+ const [isSecondImgLoaded, setIsSecondImgLoaded] = React.useState(false);
+
+ return (
+ <>
+ setIsFirstImgLoaded(true)}
+ />
+ {isFirstImgLoaded ? (
+ The first public image is loaded.
+ ) : (
+
+ )}
+ 'public/public-e2e.jpeg'}
+ onLoad={() => setIsSecondImgLoaded(true)}
+ />
+ {isSecondImgLoaded ? (
+ The second public image is loaded.
+ ) : (
+
+ )}
+ >
+ );
+}
+export default StorageImageExample;
diff --git a/packages/e2e/features/ui/components/storage/storage-image/load-public-images-from-two-s3-buckets.feature b/packages/e2e/features/ui/components/storage/storage-image/load-public-images-from-two-s3-buckets.feature
new file mode 100644
index 00000000000..5cf35c622f0
--- /dev/null
+++ b/packages/e2e/features/ui/components/storage/storage-image/load-public-images-from-two-s3-buckets.feature
@@ -0,0 +1,13 @@
+Feature: Load two images, each from a different S3 bucket with public access level settings
+
+ Background:
+ Given I'm running the example "ui/components/storage/storage-image/multi-bucket"
+
+ @react
+ Scenario: I successfully load two images from two buckets
+ Then I see "Loader1" element
+ Then I see "Loader2" element
+ Then I see the "public cat 1" image
+ Then I see the "public cat 2" image
+ Then I see "The first public image is loaded."
+ Then I see "The second public image is loaded."
diff --git a/packages/react-storage/src/components/FileUploader/FileUploader.tsx b/packages/react-storage/src/components/FileUploader/FileUploader.tsx
index 755e551891a..5487a35726d 100644
--- a/packages/react-storage/src/components/FileUploader/FileUploader.tsx
+++ b/packages/react-storage/src/components/FileUploader/FileUploader.tsx
@@ -45,6 +45,7 @@ const FileUploaderBase = React.forwardRef(function FileUploader(
acceptedFileTypes = [],
accessLevel,
autoUpload = true,
+ bucket,
components,
defaultFiles,
displayText: overrideDisplayText,
@@ -142,6 +143,7 @@ const FileUploaderBase = React.forwardRef(function FileUploader(
useUploadFiles({
accessLevel,
+ bucket,
files,
isResumable,
maxFileCount,
diff --git a/packages/react-storage/src/components/FileUploader/__tests__/FileUploader.test.tsx b/packages/react-storage/src/components/FileUploader/__tests__/FileUploader.test.tsx
index 9a63d2b5603..53a54e9fad8 100644
--- a/packages/react-storage/src/components/FileUploader/__tests__/FileUploader.test.tsx
+++ b/packages/react-storage/src/components/FileUploader/__tests__/FileUploader.test.tsx
@@ -246,6 +246,40 @@ describe('FileUploader', () => {
});
});
+ it('passes a supplied bucket name to the options object', async () => {
+ const onUploadSuccess = jest.fn();
+ render(
+
+ );
+ const hiddenInput = document.querySelector(
+ 'input[type="file"]'
+ ) as HTMLInputElement;
+
+ expect(hiddenInput).toBeInTheDocument();
+ const file = new File(['file content'], 'file.txt', { type: 'text/plain' });
+ fireEvent.change(hiddenInput, {
+ target: { files: [file] },
+ });
+
+ // Wait for the file to be uploaded
+ await waitFor(() => {
+ expect(uploadDataSpy).toHaveBeenCalledWith({
+ data: file,
+ options: {
+ bucket: 'my-bucket',
+ contentType: 'text/plain',
+ onProgress: expect.any(Function),
+ },
+ path: 'my-pathfile.txt',
+ });
+ });
+ });
+
it('calls onUploadStart callback when file starts uploading', async () => {
const onUploadStart = jest.fn();
render(
diff --git a/packages/react-storage/src/components/FileUploader/hooks/useUploadFiles/useUploadFiles.ts b/packages/react-storage/src/components/FileUploader/hooks/useUploadFiles/useUploadFiles.ts
index 26ad1ba119c..80a6d4b0b9f 100644
--- a/packages/react-storage/src/components/FileUploader/hooks/useUploadFiles/useUploadFiles.ts
+++ b/packages/react-storage/src/components/FileUploader/hooks/useUploadFiles/useUploadFiles.ts
@@ -5,6 +5,7 @@ import { isFunction } from '@aws-amplify/ui';
import { PathCallback, uploadFile } from '../../utils';
import { getInput } from '../../utils';
+import { StorageBucket } from '../../types';
import { FileStatus } from '../../types';
import { FileUploaderProps } from '../../types';
import { UseFileUploader } from '../useFileUploader';
@@ -25,11 +26,13 @@ export interface UseUploadFilesProps
'setUploadingFile' | 'setUploadProgress' | 'setUploadSuccess' | 'files'
> {
accessLevel?: FileUploaderProps['accessLevel'];
+ bucket?: StorageBucket;
path?: string | PathCallback;
}
export function useUploadFiles({
accessLevel,
+ bucket,
files,
isResumable,
maxFileCount,
@@ -68,6 +71,7 @@ export function useUploadFiles({
if (file) {
const input = getInput({
accessLevel,
+ bucket,
file,
key,
onProgress,
@@ -105,6 +109,7 @@ export function useUploadFiles({
}, [
files,
accessLevel,
+ bucket,
isResumable,
setUploadProgress,
setUploadingFile,
diff --git a/packages/react-storage/src/components/FileUploader/types.ts b/packages/react-storage/src/components/FileUploader/types.ts
index eaf9790a936..981ac3ae2d5 100644
--- a/packages/react-storage/src/components/FileUploader/types.ts
+++ b/packages/react-storage/src/components/FileUploader/types.ts
@@ -10,6 +10,15 @@ import {
} from './ui';
import { FileUploaderDisplayText, PathCallback, UploadTask } from './utils';
+export interface BucketInfo {
+ bucketName: string;
+ region: string;
+}
+
+// accepts either a 'friendly name' that the user has assigned
+// or an object containing the region as well as the name generated by the backend
+export type StorageBucket = string | BucketInfo;
+
export enum FileStatus {
ADDED = 'added',
QUEUED = 'queued',
@@ -71,6 +80,7 @@ export interface FileUploaderProps {
/**
* Component overrides
*/
+ bucket?: never;
components?: {
Container?: React.ComponentType;
DropZone?: React.ComponentType;
@@ -132,7 +142,7 @@ export interface FileUploaderProps {
}
export interface FileUploaderPathProps
- extends Omit {
+ extends Omit {
/**
* S3 bucket key, allows either a `string` or a `PathCallback`:
* - `string`: `path` is prefixed to the file `key` for each file
@@ -141,5 +151,6 @@ export interface FileUploaderPathProps
*/
path: string | PathCallback;
accessLevel?: never;
+ bucket?: StorageBucket;
useAccelerateEndpoint?: boolean;
}
diff --git a/packages/react-storage/src/components/FileUploader/utils/__tests__/getInput.spec.ts b/packages/react-storage/src/components/FileUploader/utils/__tests__/getInput.spec.ts
index bf9d4eacb09..2414b785e28 100644
--- a/packages/react-storage/src/components/FileUploader/utils/__tests__/getInput.spec.ts
+++ b/packages/react-storage/src/components/FileUploader/utils/__tests__/getInput.spec.ts
@@ -60,6 +60,7 @@ describe('getInput', () => {
const expected: UploadDataWithPathInput = {
data: file,
options: {
+ bucket: undefined,
contentType: file.type,
useAccelerateEndpoint: undefined,
onProgress,
@@ -78,6 +79,7 @@ describe('getInput', () => {
const expected: UploadDataWithPathInput = {
data: file,
options: {
+ bucket: undefined,
contentType: file.type,
useAccelerateEndpoint: undefined,
onProgress,
@@ -97,6 +99,7 @@ describe('getInput', () => {
data: file,
options: {
accessLevel,
+ bucket: undefined,
contentType: file.type,
useAccelerateEndpoint: undefined,
onProgress,
@@ -116,6 +119,7 @@ describe('getInput', () => {
data: file,
options: {
accessLevel,
+ bucket: undefined,
contentType: file.type,
useAccelerateEndpoint: undefined,
onProgress,
@@ -134,6 +138,7 @@ describe('getInput', () => {
const expected: UploadDataWithPathInput = {
data: file,
options: {
+ bucket: undefined,
contentType: file.type,
useAccelerateEndpoint: undefined,
onProgress,
@@ -218,6 +223,7 @@ describe('getInput', () => {
const expected: UploadDataWithPathInput = {
data,
options: {
+ bucket: undefined,
contentType: 'binary/octet-stream',
useAccelerateEndpoint: undefined,
onProgress,
@@ -237,6 +243,7 @@ describe('getInput', () => {
const expected: UploadDataWithPathInput = {
data,
options: {
+ bucket: undefined,
contentType: 'binary/octet-stream',
useAccelerateEndpoint: true,
onProgress,
diff --git a/packages/react-storage/src/components/FileUploader/utils/getInput.ts b/packages/react-storage/src/components/FileUploader/utils/getInput.ts
index 4aa0e2910e7..9440a58a51c 100644
--- a/packages/react-storage/src/components/FileUploader/utils/getInput.ts
+++ b/packages/react-storage/src/components/FileUploader/utils/getInput.ts
@@ -3,12 +3,13 @@ import { UploadDataWithPathInput, UploadDataInput } from 'aws-amplify/storage';
import { isString, isFunction } from '@aws-amplify/ui';
-import { ProcessFile, StorageAccessLevel } from '../types';
+import { ProcessFile, StorageAccessLevel, StorageBucket } from '../types';
import { resolveFile } from './resolveFile';
import { PathCallback, PathInput } from './uploadFile';
export interface GetInputParams {
accessLevel: StorageAccessLevel | undefined;
+ bucket?: StorageBucket;
file: File;
key: string;
onProgress: NonNullable['onProgress'];
@@ -19,6 +20,7 @@ export interface GetInputParams {
export const getInput = ({
accessLevel,
+ bucket,
file,
key,
onProgress,
@@ -41,7 +43,13 @@ export const getInput = ({
const contentType = file.type || 'binary/octet-stream';
// IMPORTANT: always pass `...rest` here for backwards compatibility
- const options = { contentType, onProgress, useAccelerateEndpoint, ...rest };
+ const options = {
+ bucket,
+ contentType,
+ onProgress,
+ useAccelerateEndpoint,
+ ...rest,
+ };
let inputResult: PathInput | UploadDataInput;
if (hasKeyInput) {
diff --git a/packages/react-storage/src/components/StorageImage/StorageImage.tsx b/packages/react-storage/src/components/StorageImage/StorageImage.tsx
index 56bfd573146..63ff2cd8d09 100644
--- a/packages/react-storage/src/components/StorageImage/StorageImage.tsx
+++ b/packages/react-storage/src/components/StorageImage/StorageImage.tsx
@@ -44,6 +44,7 @@ const getDeprecationMessage = ({
export const StorageImage = ({
accessLevel,
+ bucket,
className,
fallbackSrc,
identityId,
@@ -82,11 +83,20 @@ export const StorageImage = ({
onError,
options: {
accessLevel,
+ bucket,
targetIdentityId: identityId,
validateObjectExistence,
},
}),
- [accessLevel, imgKey, identityId, onError, path, validateObjectExistence]
+ [
+ accessLevel,
+ bucket,
+ imgKey,
+ identityId,
+ onError,
+ path,
+ validateObjectExistence,
+ ]
);
const { url } = useGetUrl(input);
diff --git a/packages/react-storage/src/components/StorageImage/__tests__/StorageImage.test.tsx b/packages/react-storage/src/components/StorageImage/__tests__/StorageImage.test.tsx
index b97808ef9c6..31a579c4c9e 100644
--- a/packages/react-storage/src/components/StorageImage/__tests__/StorageImage.test.tsx
+++ b/packages/react-storage/src/components/StorageImage/__tests__/StorageImage.test.tsx
@@ -170,6 +170,20 @@ describe('StorageImage', () => {
);
});
+ it('should pass bucket to getUrl when supplied', () => {
+ getUrlSpy.mockResolvedValue({
+ url: new URL(imgURL),
+ expiresAt: new Date(),
+ });
+ render();
+
+ expect(getUrlSpy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ options: expect.objectContaining({ bucket: 'my-bucket' }),
+ })
+ );
+ });
+
describe('with `imgKey`', () => {
it('should get the presigned URL and pass it to image src attribute', async () => {
getUrlSpy.mockResolvedValue({
diff --git a/packages/react-storage/src/components/StorageImage/types.ts b/packages/react-storage/src/components/StorageImage/types.ts
index ab9bcbef367..a02a808339f 100644
--- a/packages/react-storage/src/components/StorageImage/types.ts
+++ b/packages/react-storage/src/components/StorageImage/types.ts
@@ -1,4 +1,5 @@
import { ImageProps } from '@aws-amplify/ui-react';
+import { StorageBucket } from '../FileUploader/types';
type StorageAccessLevel = 'guest' | 'protected' | 'private';
@@ -16,6 +17,7 @@ export interface StorageImageProps extends Omit {
* `accessLevel` will be replaced with `path` in a future major version of Amplify UI. See https://ui.docs.amplify.aws/react/connected-components/storage/storageimage#props
*/
accessLevel: StorageAccessLevel;
+ bucket?: never;
/**
* @deprecated
* `identityId` will be replaced with `path` in a future major version of Amplify UI. See https://ui.docs.amplify.aws/react/connected-components/storage/storageimage#props
@@ -37,6 +39,7 @@ export interface StorageImageProps extends Omit {
type OmittedPropKey =
| 'accessLevel'
+ | 'bucket'
| 'imgKey'
| 'identityId'
| 'onStorageGetError'
@@ -47,6 +50,7 @@ export interface StorageImagePathProps
path: string | ((input: { identityId?: string }) => string);
imgKey?: never;
accessLevel?: never;
+ bucket?: StorageBucket;
identityId?: never;
onStorageGetError?: never;
}
diff --git a/packages/react-storage/src/components/StorageManager/StorageManager.tsx b/packages/react-storage/src/components/StorageManager/StorageManager.tsx
index aacbc785759..811cf15c23d 100644
--- a/packages/react-storage/src/components/StorageManager/StorageManager.tsx
+++ b/packages/react-storage/src/components/StorageManager/StorageManager.tsx
@@ -46,6 +46,7 @@ const StorageManagerBase = React.forwardRef(function StorageManager(
acceptedFileTypes = [],
accessLevel,
autoUpload = true,
+ bucket,
components,
defaultFiles,
displayText: overrideDisplayText,
@@ -149,6 +150,7 @@ const StorageManagerBase = React.forwardRef(function StorageManager(
useUploadFiles({
accessLevel,
+ bucket,
files,
isResumable,
maxFileCount,
diff --git a/packages/react-storage/src/components/StorageManager/types.ts b/packages/react-storage/src/components/StorageManager/types.ts
index b89ba652f01..0306d7e40f7 100644
--- a/packages/react-storage/src/components/StorageManager/types.ts
+++ b/packages/react-storage/src/components/StorageManager/types.ts
@@ -1,20 +1,14 @@
-import * as React from 'react';
+import { FileStatus, FileUploaderProps } from '../FileUploader/types';
+import { PathCallback, UploadTask } from '../FileUploader/utils';
-import { FileStatus, StorageAccessLevel } from '../FileUploader/types';
-import {
- FileUploaderDisplayText as StorageManagerDisplayText,
- PathCallback,
- UploadTask,
-} from '../FileUploader/utils';
+interface BucketInfo {
+ bucketName: string;
+ region: string;
+}
-import {
- ContainerProps,
- DropZoneProps,
- FileListHeaderProps,
- FileListFooterProps,
- FileListProps,
- FilePickerProps,
-} from './ui';
+// accepts either a 'friendly name' that the user has assigned
+// or an object containing the region as well as the name generated by the backend
+export type StorageBucket = string | BucketInfo;
export interface StorageFile {
id: string;
@@ -47,93 +41,17 @@ export interface StorageManagerHandle {
clearFiles: () => void;
}
-export interface StorageManagerProps {
- /**
- * List of accepted File types, values of `['*']` or undefined allow any files
- * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept
- */
- acceptedFileTypes?: string[];
- /**
- * Access level for file uploads
- * @see https://docs.amplify.aws/lib/storage/configureaccess/q/platform/js/
- */
- accessLevel: StorageAccessLevel;
-
- /**
- * Determines if the upload will automatically start after a file is selected, default value: true
- */
- autoUpload?: boolean;
- /**
- * Component overrides
- */
- components?: {
- Container?: React.ComponentType;
- DropZone?: React.ComponentType;
- FileList?: React.ComponentType;
- FilePicker?: React.ComponentType;
- FileListHeader?: React.ComponentType;
- FileListFooter?: React.ComponentType;
- };
- /**
- * List of default files already uploaded
- */
- defaultFiles?: DefaultFile[];
- /**
- * Overrides default display text
- */
- displayText?: StorageManagerDisplayText;
- /**
- * Determines if upload can be paused / resumed
- */
- isResumable?: boolean;
- /**
- * Maximum total files to upload in each batch
- */
- maxFileCount: number;
- /**
- * Maximum file size in bytes
- */
- maxFileSize?: number;
- /**
- * When a file is removed
- */
- onFileRemove?: (file: { key: string }) => void;
- /**
- * Monitor upload errors
- */
- onUploadError?: (error: string, file: { key: string }) => void;
- /**
- * Monitor upload success
- */
- onUploadSuccess?: (event: { key?: string }) => void;
- /**
- * When a file begins uploading
- */
- onUploadStart?: (event: { key?: string }) => void;
- /**
- * Process file before upload
- */
- processFile?: ProcessFile;
- /**
- * Determines if thumbnails show for image files
- */
- showThumbnails?: boolean;
- /**
- * Provided value is prefixed to the file `key` for each file
- */
- path?: string;
-
- useAccelerateEndpoint?: boolean;
-}
+export interface StorageManagerProps extends FileUploaderProps {}
export interface StorageManagerPathProps
- extends Omit {
+ extends Omit {
/**
* S3 bucket key, allows either a `string` or a `PathCallback`:
* - `string`: `path` is prefixed to the file `key` for each file
* - `PathCallback`: callback provided an input containing the current `identityId`,
* resolved value is prefixed to the file `key` for each file
*/
+ bucket?: StorageBucket;
path: string | PathCallback;
accessLevel?: never;
useAccelerateEndpoint?: boolean;