Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add object actions - presigned URL, download and preview #1604

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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",
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reason why we need this: https://stackoverflow.com/a/1214753

Copy link
Collaborator

@alfonsomthd alfonsomthd Oct 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Luxon is a modern replacement for deprecated Moment.js

"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>
);
};

Comment on lines -24 to -50
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can not be determined that easily, decided to remove it instead...

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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can consider to allow error console method in order to avoid adding eslint-disable comments...
If we go ahead this can be done in a cleanup PR.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make sense to me !!

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
Loading