From 803002059fb604c8b3d67b634919971ec29ed3a8 Mon Sep 17 00:00:00 2001 From: Danny Banks Date: Fri, 29 Nov 2024 14:15:45 -0800 Subject: [PATCH] StorageBrowser docs (#6198) Co-authored-by: AllanZhengYP Co-authored-by: Caleb Pollman --- .../__snapshots__/sitemap.test.ts.snap | 1 + docs/package.json | 1 + docs/src/data/links.tsx | 7 + .../storage-browser/examples/Composed.tsx | 103 +++ .../examples/ComposedCopyView.tsx | 18 + .../examples/ComposedCreateFolderView.tsx | 15 + .../examples/ComposedDeleteView.tsx | 15 + .../examples/ComposedLocationDetailView.tsx | 26 + .../examples/ComposedLocationsView.tsx | 26 + .../examples/ComposedUploadView.tsx | 20 + .../storage-browser/examples/Custom.tsx | 68 ++ .../examples/CustomCopyView.tsx | 23 + .../examples/CustomCreateFolderView.tsx | 34 + .../examples/CustomDeleteView.tsx | 23 + .../examples/CustomLocationsView.tsx | 34 + .../examples/CustomUploadView.tsx | 30 + .../storage-browser/examples/Default.tsx | 6 + .../storage-browser/examples/DisplayText.tsx | 17 + .../storage-browser/examples/Icons.tsx | 36 + .../examples/MockStorageBrowser.tsx | 10 + .../storage-browser/examples/Theming.tsx | 42 ++ .../examples/defaultActions.ts | 154 ++++ .../storage/storage-browser/examples/i18n.tsx | 30 + .../storage/storage-browser/examples/index.ts | 9 + .../storage-browser/examples/locations.ts | 25 + .../storage-browser/examples/mockConfig.ts | 22 + .../storage/storage-browser/index.page.mdx | 21 + .../storage/storage-browser/props.ts | 19 + .../storage/storage-browser/react.mdx | 694 ++++++++++++++++++ docs/src/styles/docs/base.scss | 7 + .../__snapshots__/exports.spec.ts.snap | 1 + .../StorageBrowser/displayText/index.ts | 2 +- .../src/components/StorageBrowser/index.ts | 1 + packages/ui/src/theme/components/index.ts | 4 +- 34 files changed, 1541 insertions(+), 3 deletions(-) create mode 100644 docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/Composed.tsx create mode 100644 docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/ComposedCopyView.tsx create mode 100644 docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/ComposedCreateFolderView.tsx create mode 100644 docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/ComposedDeleteView.tsx create mode 100644 docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/ComposedLocationDetailView.tsx create mode 100644 docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/ComposedLocationsView.tsx create mode 100644 docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/ComposedUploadView.tsx create mode 100644 docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/Custom.tsx create mode 100644 docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/CustomCopyView.tsx create mode 100644 docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/CustomCreateFolderView.tsx create mode 100644 docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/CustomDeleteView.tsx create mode 100644 docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/CustomLocationsView.tsx create mode 100644 docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/CustomUploadView.tsx create mode 100644 docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/Default.tsx create mode 100644 docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/DisplayText.tsx create mode 100644 docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/Icons.tsx create mode 100644 docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/MockStorageBrowser.tsx create mode 100644 docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/Theming.tsx create mode 100644 docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/defaultActions.ts create mode 100644 docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/i18n.tsx create mode 100644 docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/index.ts create mode 100644 docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/locations.ts create mode 100644 docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/mockConfig.ts create mode 100644 docs/src/pages/[platform]/connected-components/storage/storage-browser/index.page.mdx create mode 100644 docs/src/pages/[platform]/connected-components/storage/storage-browser/props.ts create mode 100644 docs/src/pages/[platform]/connected-components/storage/storage-browser/react.mdx diff --git a/docs/__tests__/__snapshots__/sitemap.test.ts.snap b/docs/__tests__/__snapshots__/sitemap.test.ts.snap index f27eb3b189e..83a7b256462 100644 --- a/docs/__tests__/__snapshots__/sitemap.test.ts.snap +++ b/docs/__tests__/__snapshots__/sitemap.test.ts.snap @@ -110,6 +110,7 @@ exports[`Sitemap Snapshot 1`] = ` /react/connected-components/liveness/troubleshooting, /react/connected-components/storage, /react/connected-components/storage/fileuploader, +/react/connected-components/storage/storage-browser, /react/connected-components/storage/storageimage, /react/getting-started/accessibility, /react/getting-started/figma, diff --git a/docs/package.json b/docs/package.json index 8985e9424b2..7f2f1f13b38 100644 --- a/docs/package.json +++ b/docs/package.json @@ -24,6 +24,7 @@ }, "dependencies": { "@aws-amplify/ui-react": "6.7.0", + "@aws-amplify/ui-react-storage": "3.4.1", "@docsearch/react": "3", "@mdx-js/loader": "^2.1.0", "@mdx-js/mdx": "^2.1.0", diff --git a/docs/src/data/links.tsx b/docs/src/data/links.tsx index 159598d87ac..ef68fafba0e 100644 --- a/docs/src/data/links.tsx +++ b/docs/src/data/links.tsx @@ -204,6 +204,13 @@ export const connectedComponents: ComponentNavItem[] = [ body: "Amplify UI Storage components allow you to store files in the cloud using Amplify's Storage category", platforms: ['react'], }, + { + href: '/connected-components/storage/storage-browser', + label: 'Storage Browser', + body: 'The StorageBrowser provides users a simple interface for interacting with data stored in Amazon S3.', + platforms: ['react'], + tertiary: true, + }, { href: '/connected-components/storage/storageimage', label: 'Storage Image', diff --git a/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/Composed.tsx b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/Composed.tsx new file mode 100644 index 00000000000..9aa9b614a38 --- /dev/null +++ b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/Composed.tsx @@ -0,0 +1,103 @@ +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 './MockStorageBrowser'; +import { ComposedCopyView } from './ComposedCopyView'; +import { ComposedCreateFolderView } from './ComposedCreateFolderView'; +import { ComposedDeleteView } from './ComposedDeleteView'; +import { ComposedUploadView } from './ComposedUploadView'; + +function LocationsView() { + const state = useView('Locations'); + + return ( + + Locations + {state.pageItems.map((location) => { + return ( + + ); + })} + + ); +} + +const { LocationActionView } = StorageBrowser; + +function MyLocationActionView({ + type, + onExit, +}: { + type?: string; + onExit: () => void; +}) { + let DialogContent = null; + if (!type) return DialogContent; + + switch (type) { + case 'copy': + return ; + case 'createFolder': + return ; + case 'delete': + return ; + case 'upload': + return ; + default: + return ; + } +} + +function MyStorageBrowser() { + const state = useView('LocationDetail'); + const [currentAction, setCurrentAction] = React.useState(); + const ref = React.useRef(null); + + if (!state.location.current) { + return ; + } + + return ( + <> + { + setCurrentAction(action); + ref.current?.showModal(); + }} + /> + + { + setCurrentAction(undefined); + ref.current?.close(); + }} + /> + + + ); +} + +export default function Example() { + return ( + + + + ); +} diff --git a/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/ComposedCopyView.tsx b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/ComposedCopyView.tsx new file mode 100644 index 00000000000..5564554c011 --- /dev/null +++ b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/ComposedCopyView.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import { StorageBrowser, useView } from './MockStorageBrowser'; + +export function ComposedCopyView({ onExit }: { onExit: () => void }) { + const state = useView('Copy'); + + return ( + + + + + + + + + + ); +} diff --git a/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/ComposedCreateFolderView.tsx b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/ComposedCreateFolderView.tsx new file mode 100644 index 00000000000..362eabf4087 --- /dev/null +++ b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/ComposedCreateFolderView.tsx @@ -0,0 +1,15 @@ +import * as React from 'react'; +import { StorageBrowser, useView } from './MockStorageBrowser'; + +export function ComposedCreateFolderView({ onExit }: { onExit: () => void }) { + const state = useView('CreateFolder'); + + return ( + + + + + + + ); +} diff --git a/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/ComposedDeleteView.tsx b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/ComposedDeleteView.tsx new file mode 100644 index 00000000000..a8184df575f --- /dev/null +++ b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/ComposedDeleteView.tsx @@ -0,0 +1,15 @@ +import * as React from 'react'; +import { StorageBrowser, useView } from './MockStorageBrowser'; + +export function ComposedDeleteView({ onExit }: { onExit: () => void }) { + const state = useView('Delete'); + + return ( + + + + + + + ); +} diff --git a/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/ComposedLocationDetailView.tsx b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/ComposedLocationDetailView.tsx new file mode 100644 index 00000000000..8f2c4efd7b4 --- /dev/null +++ b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/ComposedLocationDetailView.tsx @@ -0,0 +1,26 @@ +import * as React from 'react'; +import { Flex } from '@aws-amplify/ui-react'; +import { StorageBrowser, useView } from './MockStorageBrowser'; + +function LocationDetailView() { + const state = useView('LocationDetail'); + + return ( + + + + + + + + + ); +} + +export default function Example() { + return ( + + + + ); +} diff --git a/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/ComposedLocationsView.tsx b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/ComposedLocationsView.tsx new file mode 100644 index 00000000000..35e2162f9ae --- /dev/null +++ b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/ComposedLocationsView.tsx @@ -0,0 +1,26 @@ +import * as React from 'react'; +import { Flex } from '@aws-amplify/ui-react'; +import { StorageBrowser, useView } from './MockStorageBrowser'; + +function LocationsView() { + const state = useView('Locations'); + + return ( + + + + + + + + + ); +} + +export default function Example() { + return ( + + + + ); +} diff --git a/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/ComposedUploadView.tsx b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/ComposedUploadView.tsx new file mode 100644 index 00000000000..9a6617c570b --- /dev/null +++ b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/ComposedUploadView.tsx @@ -0,0 +1,20 @@ +import * as React from 'react'; +import { Flex } from '@aws-amplify/ui-react'; +import { StorageBrowser, useView } from './MockStorageBrowser'; + +export function ComposedUploadView({ onExit }: { onExit: () => void }) { + const state = useView('Upload'); + + return ( + + + + + + + + + + + ); +} diff --git a/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/Custom.tsx b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/Custom.tsx new file mode 100644 index 00000000000..e338937dc67 --- /dev/null +++ b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/Custom.tsx @@ -0,0 +1,68 @@ +import * as React from 'react'; +import { StorageBrowser, useView } from './MockStorageBrowser'; +import { CustomDeleteView } from './CustomDeleteView'; +import { CustomCopyView } from './CustomCopyView'; +import { CustomCreateFolderView } from './CustomCreateFolderView'; +import { CustomUploadView } from './CustomUploadView'; +import { CustomLocationsView } from './CustomLocationsView'; + +function MyLocationActionView({ + type, + onExit, +}: { + type?: string; + onExit: () => void; +}) { + let DialogContent = null; + if (!type) return DialogContent; + + switch (type) { + case 'copy': + return ; + case 'createFolder': + return ; + case 'delete': + return ; + case 'upload': + return ; + default: + return null; + } +} + +function MyStorageBrowser() { + const state = useView('LocationDetail'); + const [currentAction, setCurrentAction] = React.useState(); + + if (!state.location.current) { + return ; + } + + if (currentAction) { + return ( + { + setCurrentAction(undefined); + }} + /> + ); + } + + return ( + { + setCurrentAction(action); + }} + /> + ); +} + +export default function Example() { + return ( + + + + ); +} diff --git a/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/CustomCopyView.tsx b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/CustomCopyView.tsx new file mode 100644 index 00000000000..1cabc0c4ce2 --- /dev/null +++ b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/CustomCopyView.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; +import { Button, Flex, Text } from '@aws-amplify/ui-react'; +import { useView } from './MockStorageBrowser'; + +export function CustomCopyView({ onExit }: { onExit: () => void }) { + const state = useView('Copy'); + + return ( + + + {state.tasks.map((task) => ( + + {task.data.key} + + ))} + + + ); +} diff --git a/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/CustomCreateFolderView.tsx b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/CustomCreateFolderView.tsx new file mode 100644 index 00000000000..d44e3a9f841 --- /dev/null +++ b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/CustomCreateFolderView.tsx @@ -0,0 +1,34 @@ +import * as React from 'react'; +import { Button, Flex, TextField } from '@aws-amplify/ui-react'; +import { useView } from './MockStorageBrowser'; + +export function CustomCreateFolderView({ onExit }: { onExit: () => void }) { + const state = useView('CreateFolder'); + + return ( + { + e.preventDefault(); + try { + state.onActionStart(); + } catch (error) { + console.log(error); + } + }} + direction="column" + > + + { + state.onFolderNameChange(e.target.value); + }} + outerEndComponent={} + /> + + ); +} diff --git a/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/CustomDeleteView.tsx b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/CustomDeleteView.tsx new file mode 100644 index 00000000000..e7a146c5cb4 --- /dev/null +++ b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/CustomDeleteView.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; +import { Button, Flex, Text } from '@aws-amplify/ui-react'; +import { StorageBrowser, useView } from './MockStorageBrowser'; + +export function CustomDeleteView({ onExit }: { onExit: () => void }) { + const state = useView('Delete'); + + return ( + + + {state.tasks.map((task) => ( + + {task.data.key} + + ))} + + + ); +} diff --git a/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/CustomLocationsView.tsx b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/CustomLocationsView.tsx new file mode 100644 index 00000000000..6ba0b2f69b2 --- /dev/null +++ b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/CustomLocationsView.tsx @@ -0,0 +1,34 @@ +import * as React from 'react'; +import { Button, Flex, Text } from '@aws-amplify/ui-react'; +import { FiChevronRight } from 'react-icons/fi'; +import { StorageBrowser, useView } from './MockStorageBrowser'; + +export function CustomLocationsView() { + const state = useView('Locations'); + + return ( + + Locations + {state.pageItems.map((location) => { + return ( + + ); + })} + + ); +} diff --git a/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/CustomUploadView.tsx b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/CustomUploadView.tsx new file mode 100644 index 00000000000..ca32e58fabe --- /dev/null +++ b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/CustomUploadView.tsx @@ -0,0 +1,30 @@ +import * as React from 'react'; +import { Button, Flex, Text, View } from '@aws-amplify/ui-react'; +import { useView } from './MockStorageBrowser'; + +export function CustomUploadView({ onExit }: { onExit: () => void }) { + const state = useView('Upload'); + + return ( + + + + {state.tasks.map((task) => { + return ( + + {task.data.key} + {task.progress} + + ); + })} + + ); +} diff --git a/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/Default.tsx b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/Default.tsx new file mode 100644 index 00000000000..bd30e05b513 --- /dev/null +++ b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/Default.tsx @@ -0,0 +1,6 @@ +import * as React from 'react'; +import { StorageBrowser } from './MockStorageBrowser'; + +export default function Example() { + return ; +} diff --git a/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/DisplayText.tsx b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/DisplayText.tsx new file mode 100644 index 00000000000..07995a71b3d --- /dev/null +++ b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/DisplayText.tsx @@ -0,0 +1,17 @@ +import * as React from 'react'; +import { StorageBrowser } from './MockStorageBrowser'; + +export default function Example() { + return ( + permissions.join('/'), + }, + }} + /> + ); +} diff --git a/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/Icons.tsx b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/Icons.tsx new file mode 100644 index 00000000000..d752f1c065d --- /dev/null +++ b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/Icons.tsx @@ -0,0 +1,36 @@ +import * as React from 'react'; +import { + FcAlphabeticalSortingAz, + FcAlphabeticalSortingZa, + FcMinus, + FcNext, + FcPrevious, + FcRefresh, + FcSearch, +} from 'react-icons/fc'; +import { StorageBrowser } from './MockStorageBrowser'; // IGNORE +import { IconsProvider } from '@aws-amplify/ui-react'; + +export default function Example() { + return ( + , + 'sort-indeterminate': , + 'sort-ascending': , + 'sort-descending': , + }, + searchField: { + search: , + }, + pagination: { + next: , + previous: , + }, + }} + > + + + ); +} diff --git a/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/MockStorageBrowser.tsx b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/MockStorageBrowser.tsx new file mode 100644 index 00000000000..1dfb2678487 --- /dev/null +++ b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/MockStorageBrowser.tsx @@ -0,0 +1,10 @@ +import { createStorageBrowser } from '@aws-amplify/ui-react-storage/browser'; +import { mockConfig } from './mockConfig'; +import { defaultActions } from './defaultActions'; + +export const { StorageBrowser, useAction, useView } = createStorageBrowser({ + config: mockConfig, + actions: { + default: defaultActions, + }, +}); diff --git a/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/Theming.tsx b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/Theming.tsx new file mode 100644 index 00000000000..1ceec458d95 --- /dev/null +++ b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/Theming.tsx @@ -0,0 +1,42 @@ +import * as React from 'react'; +import { StorageBrowser } from './MockStorageBrowser'; // IGNORE +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 ( + + + + + ); +} diff --git a/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/defaultActions.ts b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/defaultActions.ts new file mode 100644 index 00000000000..6729cceca57 --- /dev/null +++ b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/defaultActions.ts @@ -0,0 +1,154 @@ +import { CreateStorageBrowserInput } from '@aws-amplify/ui-react-storage/dist/types/components/StorageBrowser'; +import { + DeleteHandler, + DeleteHandlerOutput, + UploadHandlerOutput, +} from '@aws-amplify/ui-react-storage/dist/types/components/StorageBrowser/actions'; +import uniqueId from 'lodash/uniqueId'; + +export const defaultActions: CreateStorageBrowserInput['actions']['default'] = { + copy: { + actionListItem: { + icon: 'copy-file', + label: 'Copy', + }, + handler: ({ data }) => { + const { key } = data; + return { + result: Promise.resolve({ + status: 'COMPLETE' as const, + value: { key }, + }), + }; + }, + viewName: 'CopyView', + }, + createFolder: { + actionListItem: { + icon: 'create-folder', + label: 'Create Folder', + }, + handler: ({ data }) => { + const { key } = data; + return { + result: Promise.resolve({ + status: 'COMPLETE' as const, + value: { key }, + }), + }; + }, + viewName: 'CreateFolderView', + }, + delete: { + actionListItem: { + icon: 'delete-file', + label: 'Delete', + }, + handler: ({ data }) => { + const { key } = data; + const result: DeleteHandlerOutput['result'] = new Promise((resolve) => { + setTimeout(() => { + resolve({ + status: 'COMPLETE' as const, + value: { key }, + }); + }, 500); + }); + return { + result, + }; + }, + viewName: 'DeleteView', + }, + download: () => { + return { + result: Promise.resolve({ + status: 'COMPLETE' as const, + value: { url: new URL('') }, + }), + }; + }, + upload: { + actionListItem: { + icon: 'upload-file', + label: 'Upload', + }, + handler: ({ data, options }) => { + // This code is modified from storageMock.ts + // it mimicks uploading a file + const { key, id } = data; + const delay = Math.random() * 100; + + let progress = 0; + let tick; + let interval; + + const tickCreator = (cb?: () => void, increment?: number) => () => { + if (progress < 0.9) { + progress += increment || 0.1; + if (typeof options.onProgress === 'function') { + options.onProgress({ key, id }, progress); + } + } else { + clearInterval(interval); + if (typeof cb === 'function') cb(); + } + }; + + const result: UploadHandlerOutput['result'] = new Promise( + (resolve, reject) => { + clearInterval(interval); + tick = tickCreator(() => { + resolve({ + status: 'COMPLETE', + value: { key }, + }); + }, 0.1); + interval = setInterval(tick, delay); + } + ); + + return { + result, + }; + }, + viewName: 'UploadView', + }, + listLocationItems: (props) => { + return new Promise((resolve) => { + setTimeout(() => { + resolve({ + items: [ + { + id: uniqueId(), + key: `${props.prefix}test-file.txt`, + lastModified: new Date(), + size: 1008, + type: 'FILE' as const, + }, + { + id: uniqueId(), + key: `${props.prefix}test-file2.txt`, + lastModified: new Date(), + size: 23456, + type: 'FILE' as const, + }, + { + id: uniqueId(), + key: `${props.prefix}test-file3.txt`, + lastModified: new Date(), + size: 43456, + type: 'FILE' as const, + }, + { + id: uniqueId(), + key: `${props.prefix}test/`, + type: 'FOLDER' as const, + }, + ], + nextToken: undefined, + }); + }, 1000); + }); + }, +}; diff --git a/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/i18n.tsx b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/i18n.tsx new file mode 100644 index 00000000000..60f3bc31109 --- /dev/null +++ b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/i18n.tsx @@ -0,0 +1,30 @@ +import * as React from 'react'; +import { StorageBrowser } from './MockStorageBrowser'; // IGNORE +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 ( + <> + setLanguage(value)} + > + En + Es + + + + ); +} diff --git a/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/index.ts b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/index.ts new file mode 100644 index 00000000000..bcc89969978 --- /dev/null +++ b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/index.ts @@ -0,0 +1,9 @@ +export { default as I18n } from './i18n'; +export { default as Composed } from './Composed'; +export { default as Custom } from './Custom'; +export { default as ComposedLocationsView } from './ComposedLocationsView'; +export { default as ComposedLocationDetailView } from './ComposedLocationDetailView'; +export { default as DisplayText } from './DisplayText'; +export { default as Theming } from './Theming'; +export { default as Icons } from './Icons'; +export { default as Default } from './Default'; diff --git a/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/locations.ts b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/locations.ts new file mode 100644 index 00000000000..0d2b5215e8a --- /dev/null +++ b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/locations.ts @@ -0,0 +1,25 @@ +import { ListLocationsOutput } from '@aws-amplify/ui-react-storage/dist/types/components/StorageBrowser/actions'; + +export const locations: ListLocationsOutput['items'] = [ + { + bucket: 'test', + id: '1234', + permissions: ['delete', 'get', 'list', 'write'], + prefix: 'test/', + type: 'PREFIX', + }, + { + bucket: 'test-bucket', + id: '4567', + permissions: ['get', 'list'], + prefix: '', + type: 'BUCKET', + }, + { + bucket: 'test', + id: '', + permissions: ['delete', 'get', 'list', 'write'], + prefix: 'some/path/', + type: 'PREFIX', + }, +]; diff --git a/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/mockConfig.ts b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/mockConfig.ts new file mode 100644 index 00000000000..a36cedae422 --- /dev/null +++ b/docs/src/pages/[platform]/connected-components/storage/storage-browser/examples/mockConfig.ts @@ -0,0 +1,22 @@ +import { CreateStorageBrowserInput } from '@aws-amplify/ui-react-storage/dist/types/components/StorageBrowser'; +import { locations } from './locations'; + +export const mockConfig: CreateStorageBrowserInput['config'] = { + region: '', + registerAuthListener: () => {}, + getLocationCredentials: () => + Promise.resolve({ + scope: '', + credentials: { + secretAccessKey: '', + sessionToken: '', + accessKeyId: '', + expiration: new Date(), + }, + }), + listLocations: (input) => + Promise.resolve({ + items: locations, + nextToken: null, + }), +}; diff --git a/docs/src/pages/[platform]/connected-components/storage/storage-browser/index.page.mdx b/docs/src/pages/[platform]/connected-components/storage/storage-browser/index.page.mdx new file mode 100644 index 00000000000..8918e0bc1c8 --- /dev/null +++ b/docs/src/pages/[platform]/connected-components/storage/storage-browser/index.page.mdx @@ -0,0 +1,21 @@ +--- +title: Storage Browser for Amazon S3 +description: The StorageBrowser component gives your end users a simple, visual interface for working with data stored in S3. +reactSource: packages/react-storage/src/components/StorageBrowser/StorageBrowser.tsx +supportedFrameworks: react +--- + +import { getCustomStaticPath } from "@/utils/getCustomStaticPath"; +import ReactPage from './react.mdx'; + +export async function getStaticPaths() { + return getCustomStaticPath(frontmatter.supportedFrameworks); +} + +{/* `getStaticProps` is required to prevent "Error: getStaticPaths was added without a getStaticProps. Without getStaticProps, getStaticPaths does nothing" */} + +export async function getStaticProps() { + return { props: {} } +} + + diff --git a/docs/src/pages/[platform]/connected-components/storage/storage-browser/props.ts b/docs/src/pages/[platform]/connected-components/storage/storage-browser/props.ts new file mode 100644 index 00000000000..7b4bcf22f84 --- /dev/null +++ b/docs/src/pages/[platform]/connected-components/storage/storage-browser/props.ts @@ -0,0 +1,19 @@ +import { DefaultStorageBrowserDisplayText } from '@aws-amplify/ui-react-storage/browser'; + +function displayTextToProps([key, value]: [string, unknown]) { + const isString = typeof value === 'string'; + return { + name: key, + description: isString ? value : '', + type: isString ? 'string' : 'function', + }; +} + +export const DISPLAY_TEXT = Object.entries( + DefaultStorageBrowserDisplayText +).map(([key, value]) => { + return { + key, + props: Object.entries(value).map(displayTextToProps), + }; +}); diff --git a/docs/src/pages/[platform]/connected-components/storage/storage-browser/react.mdx b/docs/src/pages/[platform]/connected-components/storage/storage-browser/react.mdx new file mode 100644 index 00000000000..877f985782e --- /dev/null +++ b/docs/src/pages/[platform]/connected-components/storage/storage-browser/react.mdx @@ -0,0 +1,694 @@ +import { Accordion, Message, Tabs, View } from '@aws-amplify/ui-react'; +import ReactPropsTable from '@/components/propsTable/ReactPropsTable'; +import { Example, ExampleCode } from '@/components/Example'; +import { DISPLAY_TEXT } from './props.ts' +import { + Default, + I18n, + Custom, + Composed, + DisplayText, + Theming, + Icons, +} from './examples' + + + + + + ```jsx file=./examples/Default.tsx + ``` + + + +## 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. + +## 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: + + +1. **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. +2. **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. +3. **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. + + + +```json +[ + { + "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 + } +] +``` + + + + +To reinforce security on your S3 bucket, we recommend that you define the AllowedOrigins element with request restrictions. You can restrict bucket requests so that only the application URL that you want to accept requests from are allowed. + + +### Amplify Auth + +Make sure you have an Amplify Gen2 project started, by following the [getting started guides](https://docs.amplify.aws/react/start/quickstart/). Then create an S3 bucket with access rules by [defining a storage resource and adding authorization rules](https://docs.amplify.aws/react/build-a-backend/storage/authorization/): + + + + +```ts +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 [``](/react/connected-components/authenticator) component to easily add authentication flows to your app. + + + +```ts +import { + createAmplifyAuthAdapter, + createStorageBrowser, +} from '@aws-amplify/ui-react-storage/browser'; +import "@aws-amplify/ui-react-storage/styles.css"; + +import config from './amplify_outputs.json'; + +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](https://docs.aws.amazon.com/AmazonS3/latest/userguide/storage-browser.html). + + + +```ts +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. + + + +```ts +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 { + locations: [ + { + bucketName: '[bucket name]', + prefix: '', // empty path means bucket root + id: 'XXXXXXX', // unique identifier + type: 'BUCKET', + permission: ['delete', 'get', 'list', 'write'], + }, + { + bucketName: '[bucket name]', + prefix: 'some/path', + id: 'XXXXXXX', // unique identifier + type: 'PREFIX', + permission: ['delete', 'get', 'list', 'write'], + } + ] + } + }, + getLocationCredentials: async ({ scope, permission }) => { + // get credentials for specified scope and permission + return { + credentials: { + accessKeyId: '', + secretAccessKey: '', + sessionToken: '', + expiration: new Date(), + } + } + }, + registerAuthListener: (onStateChange) => { + + } + }, +}) +``` + + + +You will also need to provide these config values to `createStorageBrowser`: + + + +```ts +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: [`