From e4098557a2a6c4dd48cbf21d694ac5d683cb3099 Mon Sep 17 00:00:00 2001 From: SanjalKatiyar Date: Thu, 3 Oct 2024 10:57:35 +0530 Subject: [PATCH] Add object actions - download and preview --- locales/en/plugin__odf-console.json | 1 - .../download-and-preview.ts | 95 +++++++++++++++++++ .../s3-browser/objects-list/ObjectsList.tsx | 6 +- .../objects-list/table-components.tsx | 31 +++++- packages/shared/src/s3/commands.ts | 5 +- packages/shared/src/s3/types.ts | 5 + 6 files changed, 131 insertions(+), 12 deletions(-) create mode 100644 packages/odf/components/s3-browser/download-and-preview/download-and-preview.ts diff --git a/locales/en/plugin__odf-console.json b/locales/en/plugin__odf-console.json index 6f0c7b1b6..e51de7f18 100644 --- a/locales/en/plugin__odf-console.json +++ b/locales/en/plugin__odf-console.json @@ -1117,7 +1117,6 @@ "Create via Object Bucket Claim": "Create via Object Bucket Claim", "Ideal for Kubernetes environments providing a more abstracted approach to managing storage resources and leveraging dynamic provisioning.": "Ideal for Kubernetes environments providing a more abstracted approach to managing storage resources and leveraging dynamic provisioning.", "OBC references a StorageClass that uses a provisioner to interact with the S3 API and create the bucket. Kubernetes binds the OBC to the bucket, making it accessible to applications.": "OBC references a StorageClass that uses a provisioner to interact with the S3 API and create the bucket. Kubernetes binds the OBC to the bucket, making it accessible to applications.", - "Download objects": "Download objects", "Delete objects": "Delete objects", "Actions": "Actions", "Create folder": "Create folder", diff --git a/packages/odf/components/s3-browser/download-and-preview/download-and-preview.ts b/packages/odf/components/s3-browser/download-and-preview/download-and-preview.ts new file mode 100644 index 000000000..db0043058 --- /dev/null +++ b/packages/odf/components/s3-browser/download-and-preview/download-and-preview.ts @@ -0,0 +1,95 @@ +import { GetObjectCommandOutput } from '@aws-sdk/client-s3'; +import { S3Commands } from '@odf/shared/s3'; +import { getName } from '@odf/shared/selectors'; +import { ObjectCrFormat } from '../../../types'; + +type DownloadAndPreviewFunction = ( + bucketName: string, + object: ObjectCrFormat, + noobaaS3: S3Commands, + setDownloadAndPreview: React.Dispatch< + React.SetStateAction + > +) => void; + +type GetObjectURL = ( + bucketName: string, + object: ObjectCrFormat, + noobaaS3: S3Commands +) => Promise; + +export type DownloadAndPreviewState = { + isDownloading: boolean; + isPreviewing: boolean; +}; + +const getObjectURL: GetObjectURL = async (bucketName, object, noobaaS3) => { + const responseStream: GetObjectCommandOutput = await noobaaS3.getObject({ + Bucket: bucketName, + Key: getName(object), + }); + const blob = await new Response(responseStream.Body as ReadableStream).blob(); + + return window.URL.createObjectURL(blob); +}; + +export const onDownload: DownloadAndPreviewFunction = async ( + bucketName, + object, + noobaaS3, + setDownloadAndPreview +) => { + try { + setDownloadAndPreview((downloadAndPreview) => ({ + ...downloadAndPreview, + isDownloading: true, + })); + + const objectURL = await getObjectURL(bucketName, object, noobaaS3); + + // create a download element and trigger download + const downloadLink = document.createElement('a'); + downloadLink.href = objectURL; + downloadLink.download = getName(object); + document.body.appendChild(downloadLink); + downloadLink.click(); + document.body.removeChild(downloadLink); + + window.URL.revokeObjectURL(objectURL); + } catch (err) { + // eslint-disable-next-line no-console + console.error('Error fetching S3 object:', err); + } finally { + setDownloadAndPreview((downloadAndPreview) => ({ + ...downloadAndPreview, + isDownloading: false, + })); + } +}; + +export const onPreview: DownloadAndPreviewFunction = async ( + bucketName, + object, + noobaaS3, + setDownloadAndPreview +) => { + try { + setDownloadAndPreview((downloadAndPreview) => ({ + ...downloadAndPreview, + isPreviewing: true, + })); + + const objectURL = await getObjectURL(bucketName, object, noobaaS3); + + // open the object URL in a new browser tab + window.open(objectURL, '_blank'); + } catch (err) { + // eslint-disable-next-line no-console + console.error('Error fetching S3 object:', err); + } finally { + setDownloadAndPreview((downloadAndPreview) => ({ + ...downloadAndPreview, + isPreviewing: false, + })); + } +}; diff --git a/packages/odf/components/s3-browser/objects-list/ObjectsList.tsx b/packages/odf/components/s3-browser/objects-list/ObjectsList.tsx index 966f4704e..d113ef44b 100644 --- a/packages/odf/components/s3-browser/objects-list/ObjectsList.tsx +++ b/packages/odf/components/s3-browser/objects-list/ObjectsList.tsx @@ -114,11 +114,7 @@ const getBulkActionsItems = ( _launcher: LaunchModal, _selectedRows: unknown[] ): IAction[] => [ - // ToDo: add bulk download & delete options - { - title: t('Download objects'), - onClick: () => undefined, - }, + // ToDo: add bulk delete option { title: t('Delete objects'), onClick: () => undefined, diff --git a/packages/odf/components/s3-browser/objects-list/table-components.tsx b/packages/odf/components/s3-browser/objects-list/table-components.tsx index 0cc917a41..b23e32231 100644 --- a/packages/odf/components/s3-browser/objects-list/table-components.tsx +++ b/packages/odf/components/s3-browser/objects-list/table-components.tsx @@ -19,6 +19,11 @@ import { CubesIcon } from '@patternfly/react-icons'; import { ActionsColumn, Td, IAction } from '@patternfly/react-table'; import { BUCKETS_BASE_ROUTE, PREFIX } from '../../../constants'; import { ObjectCrFormat } from '../../../types'; +import { + DownloadAndPreviewState, + onDownload, + onPreview, +} from '../download-and-preview/download-and-preview'; const LazyPresignedURLModal = React.lazy( () => import('../../../modals/s3-browser/presigned-url/PresignedURLModal') @@ -37,16 +42,24 @@ const getInlineActionsItems = ( launcher: LaunchModal, bucketName: string, object: ObjectCrFormat, - noobaaS3: S3Commands + noobaaS3: S3Commands, + downloadAndPreview: DownloadAndPreviewState, + setDownloadAndPreview: React.Dispatch< + React.SetStateAction + > ): IAction[] => [ - // ToDo: add inline download, preview & delete options + // ToDo: add inline delete option { title: t('Download'), - onClick: () => undefined, + onClick: () => + onDownload(bucketName, object, noobaaS3, setDownloadAndPreview), + isDisabled: downloadAndPreview.isDownloading, }, { title: t('Preview'), - onClick: () => undefined, + onClick: () => + onPreview(bucketName, object, noobaaS3, setDownloadAndPreview), + isDisabled: downloadAndPreview.isPreviewing, }, { title: t('Share with presigned URL'), @@ -94,6 +107,12 @@ export const TableRow: React.FC> = ({ }) => { const { t } = useCustomTranslation(); + const [downloadAndPreview, setDownloadAndPreview] = + React.useState({ + isDownloading: false, + isPreviewing: false, + }); + const { launcher, bucketName, foldersPath, noobaaS3 } = extraProps; const isFolder = object.isFolder; const name = getName(object).replace(foldersPath, ''); @@ -132,7 +151,9 @@ export const TableRow: React.FC> = ({ launcher, bucketName, object, - noobaaS3 + noobaaS3, + downloadAndPreview, + setDownloadAndPreview )} /> )} diff --git a/packages/shared/src/s3/commands.ts b/packages/shared/src/s3/commands.ts index eea9f488e..a29e7f5e0 100644 --- a/packages/shared/src/s3/commands.ts +++ b/packages/shared/src/s3/commands.ts @@ -5,7 +5,7 @@ import { GetObjectCommand, } from '@aws-sdk/client-s3'; import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; -import { ListBuckets, ListObjectsV2, GetSignedUrl } from './types'; +import { ListBuckets, ListObjectsV2, GetSignedUrl, GetObject } from './types'; export class S3Commands { private s3Client: S3Client; @@ -33,4 +33,7 @@ export class S3Commands { getSignedUrl: GetSignedUrl = (input, expiresIn) => getSignedUrl(this.s3Client, new GetObjectCommand(input), { expiresIn }); + + getObject: GetObject = (input) => + this.s3Client.send(new GetObjectCommand(input)); } diff --git a/packages/shared/src/s3/types.ts b/packages/shared/src/s3/types.ts index 78444bc61..1f5c26d3e 100644 --- a/packages/shared/src/s3/types.ts +++ b/packages/shared/src/s3/types.ts @@ -4,6 +4,7 @@ import { ListObjectsV2CommandInput, ListObjectsV2CommandOutput, GetObjectCommandInput, + GetObjectCommandOutput, } from '@aws-sdk/client-s3'; // Bucket command types @@ -20,3 +21,7 @@ export type GetSignedUrl = ( input: GetObjectCommandInput, expiresIn: number ) => Promise; + +export type GetObject = ( + input: GetObjectCommandInput +) => Promise;