Home
import * as React from 'react';
import { StorageBrowser } from './StorageBrowser';
export default function Example() {
return <StorageBrowser />;
}
Overview
Storage Browser for Amazon S3 is an open source component that you can add to your web applications to provide your end users with a simple, graphical interface to work with data stored in Amazon S3. With Storage Browser for S3, you can provide authorized end users access to easily browse, download, upload, copy, and delete data in S3 directly from your own applications.
End users work with S3 locations within the Storage Browser interface. Locations are S3 buckets or prefixes that you authorize end users to access using Amazon S3 Access Grants or Identity and Access Management (IAM) policies, depending on your use case. When the Storage Browser component is first rendered, it will show the LocationsView which displays only the locations you have granted them access to. Once an end user has selected a location, they can browse the S3 bucket or prefix, and all the data contained further down the S3 resource path, but they cannot browse buckets or prefixes higher up the S3 resource path.
Quick start
The quickest way to get started using the Storage Browser for S3 is to use one of the sample projects at https://github.com/aws-samples/sample-amplify-storage-browser. There are 3 samples to get you started based on different authentication and bucket setups:
- Amplify Auth and Storage
- Amplify Auth with existing bucket
- IAM Identity Center with Microsoft Entra ID and S3 Access Grants
Step 1: Fork the sample repository
Go to https://github.com/aws-samples/sample-amplify-storage-browser/fork and unselect "Copy the main branch only". The sample repository is structured by branches, so each sample has its own branch.
Step 2: Deploy the project
Follow the steps on the README for the sample branch to deploy the application. You can use any hosting provider to host the frontend assets. If you are using one of the Amplify samples you can deploy the entire application (frontend and backend) by creating a new Amplify application:
https://console.aws.amazon.com/amplify/create/repo-branch
Then selecting the newly created Github repository and choosing the appropriate branch. When the build completes, visit the newly deployed branch by selecting "Visit deployed URL".
Setup and Authentication
The StorageBrowser
component is the first Amplify UI component to support different authentication methods other than Amplify Auth, which is based on Amazon Cognito and IAM policies.
In order to show S3 locations and their contents to end users, you first need to set up your preferred authentication and authorization methods. There are 3 ways you can set up authentication/authorization with the storage browser component:
- Amplify auth: If you are already using Amplify then this option lets you get started the fastest. It uses Amazon Cognito for authentication and IAM policies for authorization and access. And by using Amplify Gen 2, the access rules for users and groups can be customized. This option is ideal for use cases when you want to connect your customers and third-party partners to your data in S3.
- AWS IAM Identity Center and S3 Access Grants: We recommend this option if you want to grant access on a per-S3-prefix basis to both IAM principals and directly to users or groups from your corporate directory. With S3 Access Grants capabilities, applications can request data from Amazon S3 on behalf of the current authenticated user. This means your applications no longer need to first map the user to an IAM principal. And when you use S3 Access Grants with IAM Identity Center trusted identity propagation, each AWS CloudTrail data event for S3 references the end user identity that accessed your data. This option is ideal for use cases when you want to connect your entire workforce with your data in S3.
- Customer managed auth: We recommend this option if you have your own identity and authorization service for authenticating and authorizing users in your application. To use this option, you will need to provide the list of S3 locations to display to the user and a mechanism for fetching scoped AWS STS tokens for each location.
The StorageBrowser
component signs all requests made to Amazon S3 based on the temporary credentials generated by one of the authorization modes above. With Ampify Auth, this is handled automatically for you based on your backed resource definitions. With IAM Identity Center and S3 Access Grants, you are responsible for setting up IAM Identity Center and S3 Access Grants and configuring them to work with your application. With custom auth, you are responsible for configuring your application to vend STS tokens to the StorageBrowser component to authorize end users' requests during their session.
Bucket CORS
The StorageBrowser
component is a client-side component that makes authorized requests to S3 APIs from the browser. If you are not using Amplify to provision your S3 buckets, you will need to enable Cross-Origin Resource Sharing (CORS) policies on the buckets you want available in the StorageBrowser
component.
[
{
"ID": "S3CORSRuleId1",
"AllowedHeaders": [
"*"
],
"AllowedMethods": [
"GET",
"HEAD",
"PUT",
"POST",
"DELETE"
],
"AllowedOrigins": [
"*"
],
"ExposeHeaders": [
"last-modified",
"content-type",
"content-length",
"etag",
"x-amz-version-id",
"x-amz-request-id",
"x-amz-id-2",
"x-amz-cf-id",
"x-amz-storage-class",
"date",
"access-control-expose-headers"
],
"MaxAgeSeconds": 3000
}
]
Amplify Auth
Make sure you have an Amplify Gen2 project started, by following the getting started guides. Then create an S3 bucket with access rules by defining a storage resource and adding authorization rules:
export const storage = defineStorage({
name: 'myProjectFiles',
access: (allow) => ({
'public/*': [
allow.guest.to(['read']),
allow.authenticated.to(['read', 'write', 'delete']),
],
'protected/{entity_id}/*': [
allow.authenticated.to(['read']),
allow.entity('identity').to(['read', 'write', 'delete'])
],
'private/{entity_id}/*': [
allow.entity('identity').to(['read', 'write', 'delete'])
]
})
});
The access rules defined in defineStorage
are treated as locations, which is described in the Overview. Therefore, users will have access to all of the S3 resource paths you have authorized them to access and can start browsing the S3 buckets or prefixes specified by these paths.
Then in your React code, call Amplify.configure()
with your amplify_outputs.json
. If you have some access rules that require a logged in user, like allow.authenticated
, you can wrap your page in the <Authenticator>
component to easily add authentication flows to your app.
import {
createAmplifyAuthAdapter,
createStorageBrowser,
} from '@aws-amplify/ui-react-storage/browser';
import "@aws-amplify/ui-react-storage/styles.css";
import config from './amplify_outputs.json';
// note: `Amplify.configure` must be run before call to `createAmplifyAuthAdapter`
Amplify.configure(config);
export const { StorageBrowser } = createStorageBrowser({
config: createAmplifyAuthAdapter(),
});
IAM Identity Center and S3 Access Grants
To use this authorization method, you first have to configure an IAM Identity Center and set up permission grants for your users and groups in S3 Access Grants. Then, you connect your application to Identity Center and configure your application to exchange an identity token from your external Identity Provider with one from Identity Center. Afterwards, you configure your application to provide the Identity Center token to Storage Browser when a user opens the page in your application to access your data in S3. To learn more, visit the S3 documentation.
import {
createManagedAuthAdapter,
createStorageBrowser,
} from '@aws-amplify/ui-react-storage/browser';
import '@aws-amplify/ui-react-storage/styles.css';
export const { StorageBrowser } = createStorageBrowser({
config: createManagedAuthAdapter({
credentialsProvider: async (options?: { forceRefresh?: boolean }) => {
// return your credentials object
return {
credentials: {
accessKeyId: 'my-access-key-id',
secretAccessKey: 'my-secret-access-key',
sessionToken: 'my-session-token',
expiration: new Date(),
},
}
},
// AWS `region` and `accountId` of the S3 Access Grants Instance.
region: '',
accountId: '',
// call `onAuthStateChange` when end user auth state changes
// to clear sensitive data from the `StorageBrowser` state
registerAuthListener: (onAuthStateChange) => {},
})
});
Customer managed auth
To use your own identity and authorization service with Storage Browser, you will need to provide the temporary credentials used to sign the requests your end users are making to S3 through the StorageBrowser
component.
import { createStorageBrowser } from '@aws-amplify/ui-react-storage/browser';
import '@aws-amplify/ui-react-storage/styles.css';
export const { StorageBrowser } = createStorageBrowser({
config: {
// Default AWS `region` and `accountId` of the S3 buckets.
region: 'XXX',
accountId: 'XXXXXX',
listLocations: async (input = {}) => {
const { nextToken, pageSize } = input;
return {
items: [
{
bucket: '[bucket name]',
prefix: '', // empty path means bucket root
id: 'XXXXXXX', // unique identifier
type: 'BUCKET',
permissions: ['delete', 'get', 'list', 'write'],
},
{
bucket: '[bucket name]',
prefix: 'some/path',
id: 'XXXXXXX', // unique identifier
type: 'PREFIX',
permissions: ['delete', 'get', 'list', 'write'],
}
]
}
},
getLocationCredentials: async ({ scope, permission }) => {
// get credentials for specified scope and permission
return {
credentials: {
accessKeyId: '',
secretAccessKey: '',
sessionToken: '',
expiration: new Date(),
}
}
},
registerAuthListener: (onAuthChange) => {
// call `onAuthChange` to notify the `StorageBrowser` that an end user has signed out
}
},
})
You will also need to provide these config values to createStorageBrowser
:
interface Config {
accountId?: string;
customEndpoint?: string;
getLocationCredentials: GetLocationCredentials;
listLocations: ListLocations;
registerAuthListener: RegisterAuthListener;
region: string;
}
Customization
Theming
The StorageBrowser
component is built on Amplify UI components so if you already have an Amplify UI theme it will just work with the Storage Browser out of the box. The components used in the Storage Browser are: <Button>
, <Breadcrumbs>
, <Menu>
, <Pagination>
, <SearchField>
, <Checkbox>
, <TextField>
, and <Message>
.
Theme object
You can use the createTheme()
function to theme the StorageBrowser
component
Home
import * as React from 'react';
import {
ThemeStyle,
createTheme,
defineComponentTheme,
} from '@aws-amplify/ui-react/server';
import { View } from '@aws-amplify/ui-react';
const storageBrowserTheme = defineComponentTheme({
name: 'storage-browser',
theme: (tokens) => {
return {
_element: {
controls: {
flexDirection: 'row-reverse',
backgroundColor: tokens.colors.background.primary,
padding: tokens.space.small,
borderRadius: tokens.radii.small,
},
title: {
fontWeight: tokens.fontWeights.thin,
},
},
};
},
});
const theme = createTheme({
name: 'my-theme',
primaryColor: 'green',
components: [storageBrowserTheme],
});
export default function Example() {
return (
<View backgroundColor="background.tertiary" {...theme.containerProps()}>
<StorageBrowser />
<ThemeStyle theme={theme} />
</View>
);
}
CSS
One way to theme the Storage Browser component is to use plain CSS. Amplify UI components use CSS classnames and CSS variables, so you can just write CSS to style the Storage Browser.
Icons
You can use the <IconsProvider>
to customize the icons used in the StorageBrowser
component.
Home
import * as React from 'react';
import {
FcAlphabeticalSortingAz,
FcAlphabeticalSortingZa,
FcMinus,
FcNext,
FcPrevious,
FcRefresh,
FcSearch,
} from 'react-icons/fc';
import { IconsProvider } from '@aws-amplify/ui-react';
export default function Example() {
return (
<IconsProvider
icons={{
storageBrowser: {
refresh: <FcRefresh />,
'sort-indeterminate': <FcMinus />,
'sort-ascending': <FcAlphabeticalSortingAz />,
'sort-descending': <FcAlphabeticalSortingZa />,
},
searchField: {
search: <FcSearch />,
},
pagination: {
next: <FcNext />,
previous: <FcPrevious />,
},
}}
>
<StorageBrowser />
</IconsProvider>
);
}
Display Text
You can customize all of the text (except S3 data like keys and bucket names) used in Storage Browser by using the displayText
prop which is a nested object organized by view. You don't need to provide an entire object; the StorageBrowser
component will merge your input with the default strings. Some texts are plain strings and others are are functions that take some input, like a date, and return a string.
Select a location
import * as React from 'react';
import { StorageBrowser } from './StorageBrowser';
export default function Example() {
return (
<StorageBrowser
displayText={{
LocationsView: {
// Some display texts are a string
title: 'Select a location',
// Some are a function that return a string
getPermissionName: (permissions) => permissions.join('/'),
},
}}
/>
);
}
All of the display texts are listed below with their default values and types.
CopyView
Name | Description | Type |
---|---|---|
actionCancelLabel | Cancel |
|
actionExitLabel | Exit |
|
actionDestinationLabel | Copy destination |
|
statusDisplayCanceledLabel | Canceled |
|
statusDisplayCompletedLabel | Completed |
|
statusDisplayFailedLabel | Failed |
|
statusDisplayInProgressLabel | In progress |
|
statusDisplayTotalLabel | Total |
|
statusDisplayQueuedLabel | Not started |
|
tableColumnCancelHeader |
| |
tableColumnStatusHeader | Status |
|
tableColumnFolderHeader | Folder |
|
tableColumnNameHeader | Name |
|
tableColumnTypeHeader | Type |
|
tableColumnSizeHeader | Size |
|
title | Copy |
|
actionStartLabel | Copy |
|
getListFoldersResultsMessage |
| |
loadingIndicatorLabel | Loading |
|
overwriteWarningMessage | Copied files will overwrite existing files at selected destination. |
|
searchPlaceholder | Search for folders |
|
getActionCompleteMessage |
| |
searchSubmitLabel | Submit |
|
searchClearLabel | Clear search |
|
CreateFolderView
Name | Description | Type |
---|---|---|
actionCancelLabel | Cancel |
|
actionExitLabel | Exit |
|
actionDestinationLabel | Destination |
|
statusDisplayCanceledLabel | Canceled |
|
statusDisplayCompletedLabel | Completed |
|
statusDisplayFailedLabel | Failed |
|
statusDisplayInProgressLabel | In progress |
|
statusDisplayTotalLabel | Total |
|
statusDisplayQueuedLabel | Not started |
|
tableColumnCancelHeader |
| |
tableColumnStatusHeader | Status |
|
tableColumnFolderHeader | Folder |
|
tableColumnNameHeader | Name |
|
tableColumnTypeHeader | Type |
|
tableColumnSizeHeader | Size |
|
title | Create folder |
|
actionStartLabel | Create folder |
|
folderNameLabel | Folder name |
|
folderNamePlaceholder | Folder name cannot contain "/", nor end or start with "." |
|
getValidationMessage |
| |
getActionCompleteMessage |
|
DeleteView
Name | Description | Type |
---|---|---|
actionCancelLabel | Cancel |
|
actionExitLabel | Exit |
|
actionDestinationLabel | Destination |
|
statusDisplayCanceledLabel | Canceled |
|
statusDisplayCompletedLabel | Completed |
|
statusDisplayFailedLabel | Failed |
|
statusDisplayInProgressLabel | In progress |
|
statusDisplayTotalLabel | Total |
|
statusDisplayQueuedLabel | Not started |
|
tableColumnCancelHeader |
| |
tableColumnStatusHeader | Status |
|
tableColumnFolderHeader | Folder |
|
tableColumnNameHeader | Name |
|
tableColumnTypeHeader | Type |
|
tableColumnSizeHeader | Size |
|
title | Delete |
|
actionStartLabel | Delete |
|
getActionCompleteMessage |
|
LocationDetailView
Name | Description | Type |
---|---|---|
loadingIndicatorLabel | Loading |
|
searchSubmitLabel | Submit |
|
searchClearLabel | Clear search |
|
getDateDisplayValue |
| |
getListItemsResultMessage |
| |
searchSubfoldersToggleLabel | Include subfolders |
|
searchPlaceholder | Search current folder |
|
tableColumnLastModifiedHeader | Last modified |
|
tableColumnNameHeader | Name |
|
tableColumnSizeHeader | Size |
|
tableColumnTypeHeader | Type |
|
selectFileLabel | Select file |
|
selectAllFilesLabel | Select all files |
|
getActionListItemLabel |
| |
getTitle |
|
LocationsView
Name | Description | Type |
---|---|---|
loadingIndicatorLabel | Loading |
|
searchSubmitLabel | Submit |
|
searchClearLabel | Clear search |
|
getDateDisplayValue |
| |
title | Home |
|
searchPlaceholder | Filter folders and files |
|
getListLocationsResultMessage |
| |
getPermissionName |
| |
getDownloadLabel |
| |
tableColumnBucketHeader | Bucket |
|
tableColumnFolderHeader | Folder |
|
tableColumnPermissionsHeader | Permissions |
|
tableColumnActionsHeader | Actions |
|
UploadView
Name | Description | Type |
---|---|---|
actionCancelLabel | Cancel |
|
actionExitLabel | Exit |
|
actionDestinationLabel | Destination |
|
statusDisplayCanceledLabel | Canceled |
|
statusDisplayCompletedLabel | Completed |
|
statusDisplayFailedLabel | Failed |
|
statusDisplayInProgressLabel | In progress |
|
statusDisplayTotalLabel | Total |
|
statusDisplayQueuedLabel | Not started |
|
tableColumnCancelHeader |
| |
tableColumnStatusHeader | Status |
|
tableColumnFolderHeader | Folder |
|
tableColumnNameHeader | Name |
|
tableColumnTypeHeader | Type |
|
tableColumnSizeHeader | Size |
|
actionStartLabel | Upload |
|
addFilesLabel | Add files |
|
addFolderLabel | Add folder |
|
getActionCompleteMessage |
| |
getFilesValidationMessage |
| |
overwriteToggleLabel | Overwrite existing files |
|
statusDisplayOverwritePreventedLabel | Overwrite prevented |
|
tableColumnProgressHeader | Progress |
|
title | Upload |
|
Internationalization
You can also use the displayText
prop to also support different languages. You can use an open source library like i18next, react-intl, or make your own:
Home
import * as React from 'react';
import { ToggleButton, ToggleButtonGroup } from '@aws-amplify/ui-react';
const dictionary = {
en: null,
es: {
LocationsView: {
title: 'Inicio',
},
},
};
export default function Example() {
const [language, setLanguage] = React.useState('en');
return (
<>
<ToggleButtonGroup
value={language}
isExclusive
onChange={(value) => setLanguage(value)}
>
<ToggleButton value="en">En</ToggleButton>
<ToggleButton value="es">Es</ToggleButton>
</ToggleButtonGroup>
<StorageBrowser displayText={dictionary[language]} />
</>
);
}
Composition
If you wanted to rearrange some of the components in any of the views without wanting to rebuild the entire view yourself, you can use the composable components of the view(s) you wish to customize. You will first need to use the useView('[view name]')
hook and pass the return to the <StorageBrowser.[view name].Provider>
and then the children of the provider can be the composed sub-components.
Below you wll find examples of how to compose each view. See View reference for a list of components that can be composed in each view.
Locations
import * as React from 'react';
import { Button, Flex, Text } from '@aws-amplify/ui-react';
import { IconChevronRight } from '@aws-amplify/ui-react/internal';
import { StorageBrowser, useView } from './StorageBrowser';
import { ComposedCopyView } from './ComposedCopyView';
import { ComposedCreateFolderView } from './ComposedCreateFolderView';
import { ComposedDeleteView } from './ComposedDeleteView';
import { ComposedUploadView } from './ComposedUploadView';
function LocationsView() {
const state = useView('Locations');
return (
<Flex direction="column" padding="medium">
<Text fontWeight="bold">Locations</Text>
{state.pageItems.map((location) => {
return (
<Button
key={location.id}
justifyContent="flex-start"
onClick={() => {
state.onNavigate(location);
}}
>
<Text flex="1">
s3://{location.bucket}/{location.prefix}
</Text>
<Text as="span" color="font.tertiary" fontWeight="normal">
{location.permissions.includes('list') ? 'Read' : null}{' '}
{location.permissions.includes('write') ? 'Write' : null}
</Text>
<IconChevronRight color="font.tertiary" />
</Button>
);
})}
</Flex>
);
}
const { LocationActionView } = StorageBrowser;
function MyLocationActionView() {
const state = useView('LocationDetail');
const onExit = () => {
state.onActionSelect('');
};
switch (state.actionType) {
case 'copy':
return <ComposedCopyView onExit={onExit} />;
case 'createFolder':
return <ComposedCreateFolderView onExit={onExit} />;
case 'delete':
return <ComposedDeleteView onExit={onExit} />;
case 'upload':
return <ComposedUploadView onExit={onExit} />;
default:
return <LocationActionView onExit={onExit} />;
}
}
function MyStorageBrowser() {
const state = useView('LocationDetail');
const ref = React.useRef<HTMLDialogElement>(null);
React.useEffect(() => {
if (state.actionType) {
ref.current?.showModal();
} else {
ref.current?.close();
}
}, [state.actionType]);
if (!state.location.current) {
return <LocationsView />;
}
return (
<>
<StorageBrowser.LocationDetailView />
<dialog ref={ref}>
<MyLocationActionView />
</dialog>
</>
);
}
export default function Example() {
return (
<StorageBrowser.Provider>
<MyStorageBrowser />
</StorageBrowser.Provider>
);
}
import * as React from 'react';
import { Flex } from '@aws-amplify/ui-react';
import { StorageBrowser, useView } from './StorageBrowser';
function LocationsView() {
const state = useView('Locations');
return (
<StorageBrowser.LocationsView.Provider {...state}>
<Flex direction="row">
<StorageBrowser.LocationsView.Refresh />
<StorageBrowser.LocationsView.Search />
</Flex>
<StorageBrowser.LocationsView.LocationsTable />
<StorageBrowser.LocationsView.Pagination />
</StorageBrowser.LocationsView.Provider>
);
}
export default function Example() {
return (
<StorageBrowser.Provider>
<LocationsView />
</StorageBrowser.Provider>
);
}
import * as React from 'react';
import { Flex } from '@aws-amplify/ui-react';
import { StorageBrowser, useView } from './StorageBrowser';
function LocationDetailView() {
const state = useView('LocationDetail');
return (
<StorageBrowser.LocationDetailView.Provider {...state}>
<Flex direction="row">
<StorageBrowser.LocationDetailView.Refresh />
<StorageBrowser.LocationDetailView.Search />
</Flex>
<StorageBrowser.LocationDetailView.LocationItemsTable />
<StorageBrowser.LocationDetailView.Pagination />
</StorageBrowser.LocationDetailView.Provider>
);
}
export default function Example() {
return (
<StorageBrowser.Provider>
<LocationDetailView />
</StorageBrowser.Provider>
);
}
import * as React from 'react';
import { StorageBrowser, useView } from './StorageBrowser';
export function ComposedCreateFolderView({ onExit }: { onExit: () => void }) {
const state = useView('CreateFolder');
return (
<StorageBrowser.CreateFolderView.Provider {...state} onActionExit={onExit}>
<StorageBrowser.CreateFolderView.Exit />
<StorageBrowser.CreateFolderView.NameField />
<StorageBrowser.CreateFolderView.Start />
<StorageBrowser.CreateFolderView.Message />
</StorageBrowser.CreateFolderView.Provider>
);
}
import * as React from 'react';
import { Flex } from '@aws-amplify/ui-react';
import { StorageBrowser, useView } from './StorageBrowser';
export function ComposedUploadView({ onExit }: { onExit: () => void }) {
const state = useView('Upload');
return (
<StorageBrowser.UploadView.Provider {...state} onActionExit={onExit}>
<StorageBrowser.UploadView.Exit />
<StorageBrowser.UploadView.TasksTable />
<Flex direction="row" width="100%">
<StorageBrowser.UploadView.AddFiles />
<StorageBrowser.UploadView.AddFolder />
<StorageBrowser.UploadView.Start />
</Flex>
<StorageBrowser.UploadView.Message />
</StorageBrowser.UploadView.Provider>
);
}
import * as React from 'react';
import { StorageBrowser, useView } from './StorageBrowser';
export function ComposedCopyView({ onExit }: { onExit: () => void }) {
const state = useView('Copy');
return (
<StorageBrowser.CopyView.Provider {...state} onActionExit={onExit}>
<StorageBrowser.CopyView.Exit />
<StorageBrowser.CopyView.TasksTable />
<StorageBrowser.CopyView.Destination />
<StorageBrowser.CopyView.FoldersSearch />
<StorageBrowser.CopyView.FoldersTable />
<StorageBrowser.CopyView.Start />
<StorageBrowser.CopyView.Message />
</StorageBrowser.CopyView.Provider>
);
}
import * as React from 'react';
import { StorageBrowser, useView } from './StorageBrowser';
export function ComposedDeleteView({ onExit }: { onExit: () => void }) {
const state = useView('Delete');
return (
<StorageBrowser.DeleteView.Provider {...state} onActionExit={onExit}>
<StorageBrowser.DeleteView.Exit />
<StorageBrowser.DeleteView.TasksTable />
<StorageBrowser.DeleteView.Start />
<StorageBrowser.DeleteView.Message />
</StorageBrowser.DeleteView.Provider>
);
}
Custom UI
The createStorageBrowser
function returns a useView()
hook that lets you use the Storage Browser's internal state and event handlers to build your own UI on top the Storage Browser functionality.
The useView()
hook takes a single argument which is the name of the view you want to get the UI state and event handlers for. The available views are Locations
, LocationDetails
, Copy
, Upload
, Delete
, and CreateFolder
. The return value of the hook will have all the necessary internal state and event handlers required to build that view. In fact, the Storage Browser component itself uses the useView()
to manage its state, so you can build the UI exactly how we do!
Locations
import * as React from 'react';
import { StorageBrowser, useView } from './StorageBrowser';
import { CustomDeleteView } from './CustomDeleteView';
import { CustomCopyView } from './CustomCopyView';
import { CustomCreateFolderView } from './CustomCreateFolderView';
import { CustomUploadView } from './CustomUploadView';
import { CustomLocationsView } from './CustomLocationsView';
function MyLocationActionView() {
const state = useView('LocationDetail');
const onExit = () => {
state.onActionSelect('');
};
switch (state.actionType) {
case 'copy':
return <CustomCopyView onExit={onExit} />;
case 'createFolder':
return <CustomCreateFolderView onExit={onExit} />;
case 'delete':
return <CustomDeleteView onExit={onExit} />;
case 'upload':
return <CustomUploadView onExit={onExit} />;
default:
return null;
}
}
function MyStorageBrowser() {
const state = useView('LocationDetail');
if (!state.location.current) {
return <CustomLocationsView />;
}
if (state.actionType) {
return <MyLocationActionView />;
}
return <StorageBrowser.LocationDetailView />;
}
export default function Example() {
return (
<StorageBrowser.Provider>
<MyStorageBrowser />
</StorageBrowser.Provider>
);
}
import * as React from 'react';
import { Button, Flex, Text } from '@aws-amplify/ui-react';
import { FiChevronRight } from 'react-icons/fi';
import { StorageBrowser, useView } from './StorageBrowser';
export function CustomLocationsView() {
const state = useView('Locations');
return (
<Flex direction="column" padding="medium">
<Text fontWeight="bold">Locations</Text>
{state.pageItems.map((location) => {
return (
<Button
key={location.id}
justifyContent="flex-start"
onClick={() => {
state.onNavigate(location);
}}
>
<Text flex="1">
s3://{location.bucket}/{location.prefix}
</Text>
<Text as="span" color="font.tertiary" fontWeight="normal">
{location.permissions.includes('list') ? 'Read' : null}{' '}
{location.permissions.includes('write') ? 'Write' : null}
</Text>
<FiChevronRight color="font.tertiary" />
</Button>
);
})}
</Flex>
);
}
import * as React from 'react';
import { Button, Flex, Text } from '@aws-amplify/ui-react';
import { useView } from './StorageBrowser';
export function CustomCopyView({ onExit }: { onExit: () => void }) {
const state = useView('Copy');
return (
<Flex direction="column">
<Button variation="link" alignSelf="flex-start" onClick={onExit}>
Exit
</Button>
{state.tasks.map((task) => (
<Flex key={task.data.key} direction="row">
<Text>{task.data.key}</Text>
</Flex>
))}
<Button onClick={state.onActionStart}>
Copy {state.tasks.length} files
</Button>
</Flex>
);
}
import * as React from 'react';
import { Button, Flex, TextField } from '@aws-amplify/ui-react';
import { useView } from './StorageBrowser';
export function CustomCreateFolderView({ onExit }: { onExit: () => void }) {
const state = useView('CreateFolder');
return (
<Flex
as="form"
onSubmit={(e) => {
e.preventDefault();
try {
state.onActionStart();
} catch (error) {
console.log(error);
}
}}
direction="column"
>
<Button variation="link" alignSelf="flex-start" onClick={onExit}>
Exit
</Button>
<TextField
label=""
value={state.folderName}
onChange={(e) => {
state.onFolderNameChange(e.target.value);
}}
outerEndComponent={<Button type="submit">Start</Button>}
/>
</Flex>
);
}
import * as React from 'react';
import { Button, Flex, Text, View } from '@aws-amplify/ui-react';
import { useView } from './StorageBrowser';
export function CustomUploadView({ onExit }: { onExit: () => void }) {
const state = useView('Upload');
return (
<Flex direction="column">
<Button variation="link" alignSelf="flex-start" onClick={onExit}>
Exit
</Button>
<Button
onClick={() => {
state.onSelectFiles('FILE');
}}
>
Add files
</Button>
{state.tasks.map((task) => {
return (
<View key={task.data.key}>
<Text>{task.data.key}</Text>
<Text>{task.progress}</Text>
</View>
);
})}
</Flex>
);
}
import * as React from 'react';
import { Button, Flex, Text } from '@aws-amplify/ui-react';
import { StorageBrowser, useView } from './StorageBrowser';
export function CustomDeleteView({ onExit }: { onExit: () => void }) {
const state = useView('Delete');
return (
<Flex direction="column">
<Button variation="link" alignSelf="flex-start" onClick={onExit}>
Exit
</Button>
{state.tasks.map((task) => (
<Flex key={task.data.key} direction="row">
<Text>{task.data.key}</Text>
</Flex>
))}
<Button onClick={state.onActionStart}>
Delete {state.tasks.length} files
</Button>
</Flex>
);
}
Customize actions
StorageBrowser
uses actions to handle data operations with S3. The actions
config allows for modification and replacement of default actions, or adding custom actions. The actions
config accepts 2 categories of actions: default
and custom
.
Extend default actions
The following default
actions are invoked by the default UI views:
listLocationItems
: List data underbucket
orprefix
in LocationDetails view.copy
: Copy data to another location within abucket
orprefix
in Copy action view.createFolder
: Create a folder within abucket
orprefix
in CreateFolder action view.delete
: Delete a given object in Delete action view.download
: Download a given object in LocationDetails view using an anchor link.upload
: Upload a file in Upload action view.
defaultActionConfigs
allows access to the default implementation of these actions. Below is an example to instrument the default upload action to count the files being uploaded.
import { Amplify } from 'aws-amplify';
import {
createAmplifyAuthAdapter,
createStorageBrowser,
defaultHandlers,
defaultActionConfigs,
} from '@aws-amplify/ui-react-storage/browser';
import '@aws-amplify/ui-react-storage/styles.css';
import config from './aws-exports';
Amplify.configure(config);
const uploads = [];
const errors = [];
const fileUploadCounter = {
add: (value: { key: string }) => {
uploads.push(value);
console.log('uploads', uploads);
},
recordError: (error: Error) => {
errors.push(error);
console.log('errors', errors);
},
};
export const { StorageBrowser } = createStorageBrowser({
config: createAmplifyAuthAdapter(),
actions: {
default: {
upload: {
...defaultActionConfigs.upload,
handler: (input) => {
const output = defaultHandlers.upload(input);
output.result.then((result) => {
const { error, status, value } = result;
if (status === 'COMPLETE') {
fileUploadCounter.add(value);
} else if (
status === 'FAILED' ||
status === 'OVERWRITE_PREVENTED'
) {
fileUploadCounter.recordError(error);
}
});
return output;
},
},
},
},
});
Add Custom actions
custom
actions can be provided to add entirely new functionality to StorageBrowser
which can be used by custom UI views.
The createStorageBrowser
function returns a useAction
hook that allows invocation of the custom or default actions in a custom UI view. There are 2 ways to use useAction
:
-
If an action should be executed concurrently on a list of data, e.g. a list of selected files, invoke the hook by supplying an array in
items
:// The execution status of each task is available in `tasks` const [ { tasks }, handleAction ] = useAction('my-action-name', { items: ['input1', 'input2'] }); // call `handleAction()` to invoke concurrent execution against the provided `items` <button onClick={() => handleAction()}>Start Concurrent Tasks!<button>
-
To run an action against a single target invoke the hook with the action name only
// The execution status of the action is available in `task` const [{ task }, handleAction] = useAction('[my action name]'); // call `handleAction` to invoke the execution against the given input data <button onClick={() => handleAction(targetData)}>Start Single Task<button>
Below is an example of a custom action that launches a custom action to generate a temporary presigned URL to selected objects.
Home
import React from 'react';
import { createStorageBrowser } from '@aws-amplify/ui-react-storage/browser';
import { Button, Flex, Link, StepperField, Text } from '@aws-amplify/ui-react';
import { generateUrlHandler } from './generateUrlHandler';
const { StorageBrowser, useAction, useView } = createStorageBrowser({
actions: {
custom: {
generateUrl: {
actionListItem: {
icon: 'download',
label: 'Generate Download Links',
disable: (selected) => !selected?.length,
},
handler: generateUrlHandler,
viewName: 'GenerateUrlView',
},
},
},
});
const GenerateUrlView = () => {
const [duration, setDuration] = React.useState(60);
const { onActionExit, fileDataItems } = useView('LocationDetail');
const items = React.useMemo(
() =>
!fileDataItems
? []
: fileDataItems.map((item) => ({ ...item, duration })),
[fileDataItems, duration]
);
const [
// Execution status and result of each task. The status includes 'CANCELED', 'FAILED', 'COMPLETE', 'OVERWRITE_PREVENTED', 'QUEUED', 'PENDING'.
{ tasks },
// Start executing the action against the provided `items`.
handleGenerate,
] = useAction(
// Name of the action.
'generateUrl',
// List of action inputs.
{ items }
);
return (
<Flex direction="column">
<Button onClick={onActionExit}>Exit</Button>
<StepperField
label="Duration"
step={15}
value={duration}
min={15}
max={300}
onStepChange={setDuration}
/>
<Button onClick={() => handleGenerate()}>Start</Button>
{!tasks
? null
: tasks.map(({ data, status, value }) => {
return (
<Flex direction="row" key={data.fileKey}>
<Text>{data.fileKey}</Text>
{value?.link ? <Link href={value.link}>link</Link> : null}
<Text>{status}</Text>
</Flex>
);
})}
</Flex>
);
};
function MyStorageBrowser() {
const state = useView('LocationDetail');
if (!state.location.current) {
return <StorageBrowser.LocationsView />;
} else if (state.actionType === 'generateUrl') {
return <GenerateUrlView />;
} else if (state.actionType) {
return <StorageBrowser.LocationActionView />;
} else {
return <StorageBrowser.LocationDetailView />;
}
}
export default function Example() {
return (
<StorageBrowser.Provider>
<MyStorageBrowser />
</StorageBrowser.Provider>
);
}
import { S3, GetObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { ActionHandler } from '@aws-amplify/ui-react-storage/browser';
type GenerateLink = ActionHandler<
{ duration: number; fileKey: string },
{ link: string }
>;
export const generateUrlHandler: GenerateLink = ({ data, config, options }) => {
const handleGenerateUrl = async () => {
try {
const s3 = new S3({
region: config.region,
credentials: (await config.credentials()).credentials,
});
const command = new GetObjectCommand({
Bucket: config.bucket,
Key: data.key,
});
const url = await getSignedUrl(s3, command, {
expiresIn: data.duration * 60,
});
const result = {
status: 'COMPLETE' as const,
value: { link: url },
};
return result;
} catch (error) {
const message = 'Unable to generate link';
return {
status: 'FAILED' as const,
message,
error,
};
}
};
return { result: handleGenerateUrl() };
};
StorageBrowser
with a default location
Initializing The StorageBrowser
can be initialized with a default location using the defaultValue
prop.
import { useParams } from 'react-router-dom';
export default function App() {
const { value } = useParams<{ value: string }>();
return <StorageBrowser defaultValue={value ? JSON.parse(value) : null} />;
}
'use client';
import { useSearchParams } from 'next/navigation';
export default function Page() {
const params = useSearchParams();
const value = params.get('value');
return <StorageBrowser defaultValue={value ? JSON.parse(value) : null} />;
}
StorageBrowser
in controlled mode
Run The StorageBrowser
can be used as a controlled component by providing the value
and onValueChange
props.
import { useSearchParams } from 'react-router-dom';
import { StorageBrowserEventValue } from '@aws-amplify/ui-react-storage/browser';
export default function App() {
const [params, setParams] = useSearchParams({
value: JSON.stringify({}),
});
const value = params.get('value');
const handleValueChange = (nextValue: StorageBrowserEventValue) => {
setParams((prev) => {
prev.set('value', JSON.stringify(nextValue));
return prev;
});
};
return (
<StorageBrowser
onValueChange={handleValueChange}
value={value ? JSON.parse(value) : null}
/>
);
}
'use client';
import React from 'react';
import { useRouter, usePathname, useSearchParams } from 'next/navigation';
import { StorageBrowserEventValue } from '@aws-amplify/ui-react-storage/browser';
export default function Page() {
const router = useRouter();
const pathname = usePathname();
const params = useSearchParams();
const value = params.get('value');
const handleValueChange = React.useCallback(
(nextValue: StorageBrowserEventValue) => {
const nextParams = new URLSearchParams();
nextParams.set('value', JSON.stringify(nextValue));
router.push(`${pathname}?${nextParams.toString()}`);
},
[pathname, router]
);
return (
<StorageBrowser
onValueChange={handleValueChange}
value={value ? JSON.parse(value) : null}
/>
);
}
View reference
Below you will find the interfaces that the useView()
hook returns for a given view as well as the composable sub-components of the view.
Locations view
Locations view state
interface LocationsViewState {
hasNextPage: boolean;
hasError: boolean;
highestPageVisited: number;
isLoading: boolean;
message: string | undefined;
shouldShowEmptyMessage: boolean;
pageItems: LocationData[];
page: number;
searchQuery: string;
hasExhaustedSearch: boolean;
onDownload: (item: LocationData) => void;
onNavigate: (location: LocationData) => void;
onRefresh: () => void;
onPaginate: (page: number) => void;
onSearch: () => void;
onSearchQueryChange: (value: string) => void;
onSearchClear: () => void;
}
Locations view components
<StorageBrowser.LocationsView.Provider />
<StorageBrowser.LocationsView.LoadingIndicator />
<StorageBrowser.LocationsView.LocationsTable />
<StorageBrowser.LocationsView.Message />
<StorageBrowser.LocationsView.Pagination />
<StorageBrowser.LocationsView.Refresh />
<StorageBrowser.LocationsView.Search />
<StorageBrowser.LocationsView.Title />
LocationDetails view
LocationDetails view state
interface LocationDetailViewState {
actions: ActionsListItem[];
hasError: boolean;
hasNextPage: boolean;
hasDownloadError: boolean;
highestPageVisited: number;
isLoading: boolean;
isSearchingSubfolders: boolean;
location: LocationState;
areAllFilesSelected: boolean;
fileDataItems: FileDataItem[] | undefined;
hasFiles: boolean;
message: string | undefined;
downloadErrorMessage: string | undefined;
shouldShowEmptyMessage: boolean;
searchQuery: string;
hasExhaustedSearch: boolean;
pageItems: LocationItemData[];
page: number;
onActionSelect: (actionType: string) => void;
onDropFiles: (files: File[]) => void;
onRefresh: () => void;
onNavigate: (location: LocationData, path?: string) => void;
onNavigateHome: () => void;
onPaginate: (page: number) => void;
onDownload: (fileItem: FileDataItem) => void;
onSelect: (isSelected: boolean, fileItem: FileData) => void;
onSelectAll: () => void;
onSearch: () => void;
onSearchClear: () => void;
onSearchQueryChange: (value: string) => void;
onToggleSearchSubfolders: () => void;
}
LocationDetails view components
<StorageBrowser.LocationDetails.Provider />
<StorageBrowser.LocationDetails.ActionsList />
<StorageBrowser.LocationDetails.DropZone />
<StorageBrowser.LocationDetails.LoadingIndicator />
<StorageBrowser.LocationDetails.LocationItemsTable />
<StorageBrowser.LocationDetails.Message />
<StorageBrowser.LocationDetails.Navigation />
<StorageBrowser.LocationDetails.Pagination />
<StorageBrowser.LocationDetails.Refresh />
<StorageBrowser.LocationDetails.Search />
<StorageBrowser.LocationDetails.SearchSubfoldersToggle />
<StorageBrowser.LocationDetails.Title />
Upload view
Upload view state
interface UploadViewState {
isOverwritingEnabled: boolean;
onDropFiles: (files: File[]) => void;
onSelectFiles: (type: 'FILE' | 'FOLDER') => void;
onToggleOverwrite: () => void;
invalidFiles: FileItems | undefined;
isProcessing: boolean;
isProcessingComplete: boolean;
location: LocationState;
onActionCancel: () => void;
onActionExit: () => void;
onActionStart: () => void;
onTaskRemove?: (task: Task<UploadHandlerData>) => void;
statusCounts: StatusCounts;
tasks: Task<UploadHandlerData>[];
}
Upload view components
<StorageBrowser.UploadView.Provider />
<StorageBrowser.UploadView.AddFiles />
<StorageBrowser.UploadView.AddFolder />
<StorageBrowser.UploadView.Cancel />
<StorageBrowser.UploadView.DropZone />
<StorageBrowser.UploadView.Destination />
<StorageBrowser.UploadView.Exit />
<StorageBrowser.UploadView.Message />
<StorageBrowser.UploadView.OverwriteToggle />
<StorageBrowser.UploadView.Start />
<StorageBrowser.UploadView.Statuses />
<StorageBrowser.UploadView.TasksTable />
<StorageBrowser.UploadView.Title />
Copy view
Copy view state
interface CopyViewState {
invalidFiles: FileItems | undefined;
isProcessing: boolean;
isProcessingComplete: boolean;
location: LocationState;
onActionCancel: () => void;
onActionExit: () => void;
onActionStart: () => void;
onTaskRemove?: (task: Task<CopyHandlerData>) => void;
statusCounts: StatusCounts;
tasks: Task<CopyHandlerData>[];
}
Copy view components
<StorageBrowser.CopyView.Provider />
<StorageBrowser.CopyView.Cancel />
<StorageBrowser.CopyView.Destination />
<StorageBrowser.CopyView.Exit />
<StorageBrowser.CopyView.FoldersLoadingIndicator />
<StorageBrowser.CopyView.FoldersMessage />
<StorageBrowser.CopyView.FoldersPagination />
<StorageBrowser.CopyView.FoldersSearch />
<StorageBrowser.CopyView.FoldersTable />
<StorageBrowser.CopyView.Message />
<StorageBrowser.CopyView.Start />
<StorageBrowser.CopyView.Statuses />
<StorageBrowser.CopyView.TasksTable />
<StorageBrowser.CopyView.Title />
Delete view
Delete view state
interface DeleteViewState {
isProcessing: boolean;
isProcessingComplete: boolean;
location: LocationState;
onActionCancel: () => void;
onActionExit: () => void;
onActionStart: () => void;
onTaskRemove?: (task: Task<DeleteHandlerData>) => void;
statusCounts: StatusCounts;
tasks: Task<DeleteHandlerData>[];
}
Delete view components
<StorageBrowser.DeleteView.Provider />
<StorageBrowser.DeleteView.Cancel />
<StorageBrowser.DeleteView.Exit />
<StorageBrowser.DeleteView.Message />
<StorageBrowser.DeleteView.Start />
<StorageBrowser.DeleteView.Statuses />
<StorageBrowser.DeleteView.TasksTable />
<StorageBrowser.DeleteView.Title />
CreateFolder view
CreateFolder view state
interface CreateFolderViewState {
isProcessing: boolean;
isProcessingComplete: boolean;
location: LocationState;
onActionCancel: () => void;
onActionExit: () => void;
onActionStart: () => void;
onTaskRemove?: (task: Task<CreateFolderHandlerData>) => void;
statusCounts: StatusCounts;
tasks: Task<CreateFolderHandlerData>[];
}
CreateFolder view components
<StorageBrowser.CreateFolderView.Provider />
<StorageBrowser.CreateFolderView.Exit />
<StorageBrowser.CreateFolderView.NameField />
<StorageBrowser.CreateFolderView.Message />
<StorageBrowser.CreateFolderView.Start />
<StorageBrowser.CreateFolderView.Title />
Roadmap
We made our roadmap for Storage Browser public so that you could see what's in store and provide feedback to help drive the future direction of this product. Visit our roadmap and share your thoughts today.
These are the features we are currently evaluating.
- Thumbnail previews
- Full object previews
- User-controlled object tags
- Support for S3 Glacier Flexible Retrieval and S3 Glacier Deep Archive
- Support for S3 Access Points
- User controlled byte-range GETs
- Support for a CloudFront cache