Skip to content

Commit

Permalink
Merge pull request #1604 from SanjalKatiyar/actions_presigned_url
Browse files Browse the repository at this point in the history
Add object actions - presigned URL, download and preview
  • Loading branch information
openshift-merge-bot[bot] authored Oct 4, 2024
2 parents 2c5dfc0 + 607cd34 commit abb8239
Show file tree
Hide file tree
Showing 13 changed files with 464 additions and 58 deletions.
19 changes: 15 additions & 4 deletions locales/en/plugin__odf-console.json
Original file line number Diff line number Diff line change
Expand Up @@ -1104,7 +1104,6 @@
"Edit bucket": "Edit bucket",
"Objects": "Objects",
"Refresh": "Refresh",
"Created on: ": "Created on: ",
"Created via OBC": "Created via OBC",
"Created via S3": "Created via S3",
"MCG": "MCG",
Expand All @@ -1126,14 +1125,12 @@
"No tags are attached to this bucket.": "No tags are attached to this bucket.",
"Add tag": "Add tag",
"Value (optional)": "Value (optional)",
"Download objects": "Download objects",
"Delete objects": "Delete objects",
"Actions": "Actions",
"Create folder": "Create folder",
"Size": "Size",
"Last modified": "Last modified",
"Download": "Download",
"Copy Object URL": "Copy Object URL",
"Preview": "Preview",
"Share with presigned URL": "Share with presigned URL",
"No objects found": "No objects found",
Expand Down Expand Up @@ -1300,6 +1297,21 @@
"and": "and",
"GiB RAM": "GiB RAM",
"Configure Performance": "Configure Performance",
"Expires after": "Expires after",
"minus": "minus",
"plus": "plus",
"Validity period of the presigned URL.": "Validity period of the presigned URL.",
"Share link": "Share link",
"Valid until: ": "Valid until: ",
"Copy": "Copy",
"Copied": "Copied",
"This URL will automatically expire based on your configured time or when your current session expires.": "This URL will automatically expire based on your configured time or when your current session expires.",
"Share object with a presigned URL": "Share object with a presigned URL",
"Grant third-party access to an object for a limited time.": "Grant third-party access to an object for a limited time.",
"Copy presigned URL to clipboard": "Copy presigned URL to clipboard",
"Create presigned URL": "Create presigned URL",
"Object: ": "Object: ",
"A third-party entity can access the object using this presigned URL, which allows sharing without requiring a login, until the URL expires.": "A third-party entity can access the object using this presigned URL, which allows sharing without requiring a login, until the URL expires.",
"hr": "hr",
"min": "min",
"Select at least 2 Backing Store resources": "Select at least 2 Backing Store resources",
Expand Down Expand Up @@ -1452,7 +1464,6 @@
"Reason": "Reason",
"Message": "Message",
"No conditions found": "No conditions found",
"Copied": "Copied",
"View documentation": "View documentation",
"Oh no! Something went wrong.": "Oh no! Something went wrong.",
"Copied to clipboard": "Copied to clipboard",
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
},
"dependencies": {
"@aws-sdk/client-s3": "3.614.0",
"@aws-sdk/s3-request-presigner": "3.614.0",
"@openshift-console/dynamic-plugin-sdk": "1.3.0",
"@openshift-console/dynamic-plugin-sdk-internal": "1.0.0",
"@openshift-console/dynamic-plugin-sdk-webpack": "1.1.1",
Expand Down Expand Up @@ -91,6 +92,7 @@
"js-base64": "^2.1.9",
"js-yaml": "^3.13.1",
"lodash-es": "^4.17.21",
"luxon": "^3.3.0",
"murmurhash-js": "^1.0.0",
"react": "^17.0.1",
"react-copy-to-clipboard": "5.x",
Expand Down Expand Up @@ -133,6 +135,7 @@
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "^14.5.1",
"@types/jest": "29.2.2",
"@types/luxon": "^3.3.1",
"@types/node": "^14.14.34",
"@types/react": "16.8.13",
"@types/react-helmet": "^6.1.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,6 @@ const BucketOverview: React.FC<{}> = () => {
bucketName={bucketName}
foldersPath={foldersPath}
currentFolder={currentFolder}
fresh={fresh}
isCreatedByOBC={isCreatedByOBC}
noobaaObjectBucket={noobaaObjectBucket}
/>
Expand Down
36 changes: 1 addition & 35 deletions packages/odf/components/s3-browser/bucket-overview/PageTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,50 +4,19 @@ import { K8sResourceKind } from '@odf/shared/types';
import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook';
import { ResourceStatus } from '@openshift-console/dynamic-plugin-sdk';
import Status from '@openshift-console/dynamic-plugin-sdk/lib/app/components/status/Status';
import useSWR from 'swr';
import { Skeleton, Label, Button, ButtonVariant } from '@patternfly/react-core';
import { Label, Button, ButtonVariant } from '@patternfly/react-core';
import { CopyIcon } from '@patternfly/react-icons';
import { LIST_BUCKET } from '../../../constants';
import { getPath } from '../../../utils';
import { NoobaaS3Context } from '../noobaa-context';
import './bucket-overview.scss';

type TitleProps = {
bucketName: string;
foldersPath: string;
currentFolder: string;
fresh: boolean;
isCreatedByOBC: boolean;
noobaaObjectBucket: K8sResourceKind;
};

const CreatedOnSkeleton: React.FC<{}> = () => (
<Skeleton width="25%" height="15%" />
);

const CreatedOn: React.FC<{ bucketName: string }> = ({ bucketName }) => {
const { t } = useCustomTranslation();

const { noobaaS3 } = React.useContext(NoobaaS3Context);
const { data, error, isLoading } = useSWR(LIST_BUCKET, () =>
noobaaS3.listBuckets()
);

const bucketCreatedOn =
!isLoading && !error
? data?.Buckets?.find((bucket) => bucket?.Name === bucketName)
?.CreationDate
: null;

return isLoading ? (
<CreatedOnSkeleton />
) : (
<h4 className="text-muted">
{t('Created on: ') + bucketCreatedOn?.toString()}
</h4>
);
};

const BucketResourceStatus: React.FC<{ resourceStatus: string }> = ({
resourceStatus,
}) => (
Expand All @@ -60,7 +29,6 @@ export const PageTitle: React.FC<TitleProps> = ({
bucketName,
foldersPath,
currentFolder,
fresh,
isCreatedByOBC,
noobaaObjectBucket,
}) => {
Expand Down Expand Up @@ -99,8 +67,6 @@ export const PageTitle: React.FC<TitleProps> = ({
</>
)}
</div>
{!foldersPath &&
(fresh ? <CreatedOn bucketName={bucketName} /> : <CreatedOnSkeleton />)}
<h4>
{t('Object path: ')}
<span className="text-muted">{objectPath}</span>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<DownloadAndPreviewState>
>
) => void;

type GetObjectURL = (
bucketName: string,
object: ObjectCrFormat,
noobaaS3: S3Commands
) => Promise<string>;

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,
}));
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -319,7 +315,7 @@ export const ObjectsList: React.FC<{}> = () => {
loaded={!isMutating}
loadError={error}
isRowSelectable={isRowSelectable}
extraProps={{ launcher, bucketName, foldersPath }}
extraProps={{ launcher, bucketName, foldersPath, noobaaS3 }}
emptyRowMessage={EmptyPage}
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from 'react';
import { S3Commands } from '@odf/shared/s3';
import { getName } from '@odf/shared/selectors';
import { RowComponentType } from '@odf/shared/table';
import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook';
Expand All @@ -18,6 +19,15 @@ 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')
);

const getColumnNames = (t: TFunction): string[] => [
t('Name'),
Expand All @@ -29,25 +39,35 @@ const getColumnNames = (t: TFunction): string[] => [

const getInlineActionsItems = (
t: TFunction,
_launcher: LaunchModal,
_object: ObjectCrFormat
launcher: LaunchModal,
bucketName: string,
object: ObjectCrFormat,
noobaaS3: S3Commands,
downloadAndPreview: DownloadAndPreviewState,
setDownloadAndPreview: React.Dispatch<
React.SetStateAction<DownloadAndPreviewState>
>
): IAction[] => [
// ToDo: add inline download, copy, preview, share & delete options
// ToDo: add inline delete option
{
title: t('Download'),
onClick: () => undefined,
},
{
title: t('Copy Object URL'),
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'),
onClick: () => undefined,
onClick: () =>
launcher(LazyPresignedURLModal, {
isOpen: true,
extraProps: { bucketName, object, noobaaS3 },
}),
},
{
title: t('Delete'),
Expand Down Expand Up @@ -87,7 +107,13 @@ export const TableRow: React.FC<RowComponentType<ObjectCrFormat>> = ({
}) => {
const { t } = useCustomTranslation();

const { launcher, bucketName, foldersPath } = extraProps;
const [downloadAndPreview, setDownloadAndPreview] =
React.useState<DownloadAndPreviewState>({
isDownloading: false,
isPreviewing: false,
});

const { launcher, bucketName, foldersPath, noobaaS3 } = extraProps;
const isFolder = object.isFolder;
const name = getName(object).replace(foldersPath, '');
const prefix = !!foldersPath
Expand Down Expand Up @@ -120,7 +146,15 @@ export const TableRow: React.FC<RowComponentType<ObjectCrFormat>> = ({
{isFolder ? null : (
<ActionsColumn
translate={null}
items={getInlineActionsItems(t, launcher, object)}
items={getInlineActionsItems(
t,
launcher,
bucketName,
object,
noobaaS3,
downloadAndPreview,
setDownloadAndPreview
)}
/>
)}
</Td>
Expand Down
Loading

0 comments on commit abb8239

Please sign in to comment.