Skip to content

Commit

Permalink
feature(multi-bucket): add multi-bucket support to storage components (
Browse files Browse the repository at this point in the history
…#5562)

* initial commit to add 'bucket' property to storage components

* chore: use StorageBucket type in StorageImagePathProps

* remove duplicate StorageBucket type declaration

* chore: update aws-amplify version to include multi-bucket support

* docs: include references to new 'bucket' prop and its usage

* more explicitly clarifying that  can be a string in docs example

* chore: changing reference of storage manager to file uploader

* chore: updating yarn.lock

* chore: undoing unnecessary linting changes

* chore: moving yarn.lock from main branch parity

* chore: updating yarn.lock to main

* chore: add missing references to 'bucket'

* chore: adding tests and new example app

* chore: add end of file line

* chore: add changeset

* chore: setting more obviously fake bucket name as example

* chore: adding link for setting up multi-bucket configuration to docs

* chore: removing unnecessary type definitions

* chore: removing unnecessary type from Storage Image props

* chore: adding bucket as omitted prop to gen1 props

* fix(tests): updating test data to fit expected behavior

* chore: adjusting prop order, import consolidation, and added description

* chore: add FileUploader example app and e2e test

---------

Co-authored-by: Caleb Pollman <[email protected]>
  • Loading branch information
jordanvn and calebpollman authored Feb 7, 2025
1 parent cd508ee commit 2e107f9
Show file tree
Hide file tree
Showing 24 changed files with 330 additions and 103 deletions.
5 changes: 5 additions & 0 deletions .changeset/loud-walls-do.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@aws-amplify/ui-react-storage': minor
---

Support for multiple buckets added to storage image and file uploader
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { FileUploader } from '@aws-amplify/ui-react-storage';

export const App = () => {
return (
<FileUploader
acceptedFileTypes={['image/*']}
bucket={{
bucketName: 'my-bucket-xxxxxxxx',
region: 'us-west-2',
}}
path="public/"
maxFileCount={1}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { FileUploader } from '@aws-amplify/ui-react-storage';

export const App = () => {
return (
<FileUploader
acceptedFileTypes={['image/*']}
bucket="my-bucket"
path="public/"
maxFileCount={1}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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: '',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,26 @@ You can limit what users upload with these 3 props:
</ExampleCode>
</Example>

## 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:

<Example>
<FileTypesExample />
<ExampleCode>
```jsx file=./examples/BucketFriendly.tsx
```
</ExampleCode>
</Example>

Alternatively, you can specify the bucket using the name and region it is assigned within S3:

<Example>
<ExampleCode>
```jsx file=./examples/BucketExact.tsx
```
</ExampleCode>
</Example>

## Pausable / Resumable Uploads

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import amplifyOutputs from '@environments/storage/gen2/amplify_outputs';
export default amplifyOutputs;
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React from 'react';
import { Amplify } from 'aws-amplify';
import { FileUploader } from '@aws-amplify/ui-react-storage';
import '@aws-amplify/ui-react/styles.css';
import amplifyOutputs from './amplify_outputs';
import { Radio, RadioGroupField } from '@aws-amplify/ui-react';
Amplify.configure(amplifyOutputs);

export function FileUploaderExample() {
const [bucket, setBucket] = React.useState('Bucket 1');
return (
<div>
<RadioGroupField
defaultValue="Bucket 1"
legend="Bucket"
name="Bucket to upload to"
onChange={(e) => setBucket(e.target.value)}
>
<Radio value="Bucket 1">Bucket 1</Radio>
<Radio value="Bucket 2">Bucket 2</Radio>
</RadioGroupField>
{bucket === 'Bucket 1' ? (
<div>
<FileUploader
acceptedFileTypes={['*']}
bucket="StorageEndToEnd"
displayText={{ dropFilesText: 'Drop files into Bucket 1' }}
path="public/"
maxFileCount={1}
showThumbnails
/>
</div>
) : (
<div>
<FileUploader
acceptedFileTypes={['*']}
bucket="StorageEndToEndSecondary"
displayText={{ dropFilesText: 'Drop files into Bucket 2' }}
path="public/"
maxFileCount={1}
showThumbnails
/>
</div>
)}
</div>
);
}
export default FileUploaderExample;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import amplifyOutputs from '@environments/storage/gen2/amplify_outputs';
export default amplifyOutputs;
Original file line number Diff line number Diff line change
@@ -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 (
<>
<StorageImage
alt="public cat 1"
bucket="StorageEndToEnd"
path="public/public-e2e.jpeg"
onLoad={() => setIsFirstImgLoaded(true)}
/>
{isFirstImgLoaded ? (
<Text>The first public image is loaded.</Text>
) : (
<Loader testId="Loader1" />
)}
<StorageImage
alt="public cat 2"
bucket="StorageEndToEndSecondary"
path={() => 'public/public-e2e.jpeg'}
onLoad={() => setIsSecondImgLoaded(true)}
/>
{isSecondImgLoaded ? (
<Text>The second public image is loaded.</Text>
) : (
<Loader testId="Loader2" />
)}
</>
);
}
export default StorageImageExample;
21 changes: 20 additions & 1 deletion packages/e2e/cypress/integration/common/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,14 @@ Given("I'm running the docs page", () => {
cy.visit('/');
});

Given('I intercept requests to host including {string}', (host: string) => {
cy.intercept({ url: '**' }, (req) => {
if (req.headers.host?.includes(host)) {
req.alias = host;
}
});
});

Given(
'I intercept {string} with fixture {string}',
(json: string, fixture: string) => {
Expand Down Expand Up @@ -115,6 +123,15 @@ Then(
}
);

Then(
'I confirm the {string} request was made to host containing {string}',
(request: string, hostValue: string) => {
cy.wait(`@${request}`).then((interception) => {
expect(interception.request.headers.host).to.include(hostValue);
});
}
);

Given('I spy request {string}', (json: string) => {
let routeMatcher;

Expand Down Expand Up @@ -345,7 +362,9 @@ When('I click the {string} checkbox', (label: string) => {
});

When('I click the {string} radio button', (label: string) => {
cy.findByLabelText(new RegExp(`^${escapeRegExp(label)}`, 'i')).click();
cy.findByLabelText(new RegExp(`^${escapeRegExp(label)}`, 'i')).click({
force: true,
});
});

When('I reload the page', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Feature: Upload a file to multiple S3 buckets with public access level settings

Background:
Given I'm running the example "ui/components/storage/file-uploader/multi-bucket"

@react
Scenario: I upload a file to each bucket
Given I intercept requests to host including "s3"
Then I see "Drop files into Bucket 1"
Then I select a file with file name "test.jpg"
Then I see "test.jpg"
Then I see "Uploaded"
Then I confirm the "s3" request was made to host containing "storageendtoendbucket"
Then I see "1 file uploaded"
Then I click the "Bucket 2" radio button
Then I see "Drop files into Bucket 2"
Then I select a file with file name "test.jpg"
Then I see "test.jpg"
Then I see "Uploaded"
Then I confirm the "s3" request was made to host containing "storageendtoendsecondary"
Original file line number Diff line number Diff line change
@@ -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."
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const FileUploaderBase = React.forwardRef(function FileUploader(
acceptedFileTypes = [],
accessLevel,
autoUpload = true,
bucket,
components,
defaultFiles,
displayText: overrideDisplayText,
Expand Down Expand Up @@ -142,6 +143,7 @@ const FileUploaderBase = React.forwardRef(function FileUploader(

useUploadFiles({
accessLevel,
bucket,
files,
isResumable,
maxFileCount,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,40 @@ describe('FileUploader', () => {
});
});

it('passes a supplied bucket name to the options object', async () => {
const onUploadSuccess = jest.fn();
render(
<FileUploader
bucket="my-bucket"
path="my-path"
maxFileCount={100}
onUploadSuccess={onUploadSuccess}
/>
);
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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@ import * as React from 'react';
import { TransferProgressEvent } from 'aws-amplify/storage';
import { isFunction } from '@aws-amplify/ui';

import { PathCallback, uploadFile } from '../../utils';
import { getInput } from '../../utils';
import { FileStatus } from '../../types';
import { FileUploaderProps } from '../../types';
import { getInput, PathCallback, uploadFile } from '../../utils';
import { FileStatus, FileUploaderProps, StorageBucket } from '../../types';
import { UseFileUploader } from '../useFileUploader';

export interface UseUploadFilesProps
Expand All @@ -25,11 +23,13 @@ export interface UseUploadFilesProps
'setUploadingFile' | 'setUploadProgress' | 'setUploadSuccess' | 'files'
> {
accessLevel?: FileUploaderProps['accessLevel'];
bucket?: StorageBucket;
path?: string | PathCallback;
}

export function useUploadFiles({
accessLevel,
bucket,
files,
isResumable,
maxFileCount,
Expand Down Expand Up @@ -68,6 +68,7 @@ export function useUploadFiles({
if (file) {
const input = getInput({
accessLevel,
bucket,
file,
key,
onProgress,
Expand Down Expand Up @@ -105,6 +106,7 @@ export function useUploadFiles({
}, [
files,
accessLevel,
bucket,
isResumable,
setUploadProgress,
setUploadingFile,
Expand Down
14 changes: 13 additions & 1 deletion packages/react-storage/src/components/FileUploader/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -129,10 +138,12 @@ export interface FileUploaderProps {
path?: string;

useAccelerateEndpoint?: boolean;

bucket?: never;
}

export interface FileUploaderPathProps
extends Omit<FileUploaderProps, 'accessLevel' | 'path'> {
extends Omit<FileUploaderProps, 'accessLevel' | 'bucket' | 'path'> {
/**
* S3 bucket key, allows either a `string` or a `PathCallback`:
* - `string`: `path` is prefixed to the file `key` for each file
Expand All @@ -141,5 +152,6 @@ export interface FileUploaderPathProps
*/
path: string | PathCallback;
accessLevel?: never;
bucket?: StorageBucket;
useAccelerateEndpoint?: boolean;
}
Loading

0 comments on commit 2e107f9

Please sign in to comment.