diff --git a/locales/en/plugin__odf-console.json b/locales/en/plugin__odf-console.json
index 01a5a03c2..6f0c7b1b6 100644
--- a/locales/en/plugin__odf-console.json
+++ b/locales/en/plugin__odf-console.json
@@ -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",
@@ -1125,7 +1124,6 @@
"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",
@@ -1292,6 +1290,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",
@@ -1444,7 +1457,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",
diff --git a/package.json b/package.json
index 5f5d27eeb..9ad42f1b4 100644
--- a/package.json
+++ b/package.json
@@ -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",
@@ -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",
@@ -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",
diff --git a/packages/odf/components/s3-browser/bucket-overview/BucketOverview.tsx b/packages/odf/components/s3-browser/bucket-overview/BucketOverview.tsx
index 4d5f50389..6e08a9a34 100644
--- a/packages/odf/components/s3-browser/bucket-overview/BucketOverview.tsx
+++ b/packages/odf/components/s3-browser/bucket-overview/BucketOverview.tsx
@@ -180,7 +180,6 @@ const BucketOverview: React.FC<{}> = () => {
bucketName={bucketName}
foldersPath={foldersPath}
currentFolder={currentFolder}
- fresh={fresh}
isCreatedByOBC={isCreatedByOBC}
noobaaObjectBucket={noobaaObjectBucket}
/>
diff --git a/packages/odf/components/s3-browser/bucket-overview/PageTitle.tsx b/packages/odf/components/s3-browser/bucket-overview/PageTitle.tsx
index e06db22d7..1f2b39169 100644
--- a/packages/odf/components/s3-browser/bucket-overview/PageTitle.tsx
+++ b/packages/odf/components/s3-browser/bucket-overview/PageTitle.tsx
@@ -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<{}> = () => (
-
-);
-
-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 ? (
-
- ) : (
-
- {t('Created on: ') + bucketCreatedOn?.toString()}
-
- );
-};
-
const BucketResourceStatus: React.FC<{ resourceStatus: string }> = ({
resourceStatus,
}) => (
@@ -60,7 +29,6 @@ export const PageTitle: React.FC = ({
bucketName,
foldersPath,
currentFolder,
- fresh,
isCreatedByOBC,
noobaaObjectBucket,
}) => {
@@ -99,8 +67,6 @@ export const PageTitle: React.FC = ({
>
)}
- {!foldersPath &&
- (fresh ? : )}
{t('Object path: ')}
{objectPath}
diff --git a/packages/odf/components/s3-browser/objects-list/ObjectsList.tsx b/packages/odf/components/s3-browser/objects-list/ObjectsList.tsx
index 8d17fbe3d..966f4704e 100644
--- a/packages/odf/components/s3-browser/objects-list/ObjectsList.tsx
+++ b/packages/odf/components/s3-browser/objects-list/ObjectsList.tsx
@@ -319,7 +319,7 @@ export const ObjectsList: React.FC<{}> = () => {
loaded={!isMutating}
loadError={error}
isRowSelectable={isRowSelectable}
- extraProps={{ launcher, bucketName, foldersPath }}
+ extraProps={{ launcher, bucketName, foldersPath, noobaaS3 }}
emptyRowMessage={EmptyPage}
/>
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 0f4b42c79..0cc917a41 100644
--- a/packages/odf/components/s3-browser/objects-list/table-components.tsx
+++ b/packages/odf/components/s3-browser/objects-list/table-components.tsx
@@ -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';
@@ -19,6 +20,10 @@ import { ActionsColumn, Td, IAction } from '@patternfly/react-table';
import { BUCKETS_BASE_ROUTE, PREFIX } from '../../../constants';
import { ObjectCrFormat } from '../../../types';
+const LazyPresignedURLModal = React.lazy(
+ () => import('../../../modals/s3-browser/presigned-url/PresignedURLModal')
+);
+
const getColumnNames = (t: TFunction): string[] => [
t('Name'),
t('Size'),
@@ -29,25 +34,27 @@ const getColumnNames = (t: TFunction): string[] => [
const getInlineActionsItems = (
t: TFunction,
- _launcher: LaunchModal,
- _object: ObjectCrFormat
+ launcher: LaunchModal,
+ bucketName: string,
+ object: ObjectCrFormat,
+ noobaaS3: S3Commands
): IAction[] => [
- // ToDo: add inline download, copy, preview, share & delete options
+ // ToDo: add inline download, preview & delete options
{
title: t('Download'),
onClick: () => undefined,
},
- {
- title: t('Copy Object URL'),
- onClick: () => undefined,
- },
{
title: t('Preview'),
onClick: () => undefined,
},
{
title: t('Share with presigned URL'),
- onClick: () => undefined,
+ onClick: () =>
+ launcher(LazyPresignedURLModal, {
+ isOpen: true,
+ extraProps: { bucketName, object, noobaaS3 },
+ }),
},
{
title: t('Delete'),
@@ -87,7 +94,7 @@ export const TableRow: React.FC> = ({
}) => {
const { t } = useCustomTranslation();
- const { launcher, bucketName, foldersPath } = extraProps;
+ const { launcher, bucketName, foldersPath, noobaaS3 } = extraProps;
const isFolder = object.isFolder;
const name = getName(object).replace(foldersPath, '');
const prefix = !!foldersPath
@@ -120,7 +127,13 @@ export const TableRow: React.FC> = ({
{isFolder ? null : (
)}
diff --git a/packages/odf/modals/s3-browser/presigned-url/PresignedURLModal.tsx b/packages/odf/modals/s3-browser/presigned-url/PresignedURLModal.tsx
new file mode 100644
index 000000000..d0cc27525
--- /dev/null
+++ b/packages/odf/modals/s3-browser/presigned-url/PresignedURLModal.tsx
@@ -0,0 +1,235 @@
+import * as React from 'react';
+import StaticDropdown from '@odf/shared/dropdown/StaticDropdown';
+import { ButtonBar } from '@odf/shared/generic/ButtonBar';
+import { CommonModalProps } from '@odf/shared/modals';
+import { S3Commands } from '@odf/shared/s3';
+import { getName } from '@odf/shared/selectors';
+import { useCustomTranslation } from '@odf/shared/useCustomTranslationHook';
+import { numberInputOnChange } from '@odf/shared/utils';
+import { DateTime } from 'luxon';
+import {
+ Modal,
+ ModalVariant,
+ Button,
+ ButtonVariant,
+ Alert,
+ AlertVariant,
+ FormGroup,
+ FormHelperText,
+ HelperText,
+ HelperTextItem,
+ ValidatedOptions,
+ NumberInput,
+ ClipboardCopy,
+} from '@patternfly/react-core';
+import { ObjectCrFormat } from '../../../types';
+import './presigned-modal.scss';
+
+// using same "key" and "value" in the enum on purpose, due to how "StaticDropdown" is implemented
+enum TimeUnits {
+ Hours = 'Hours',
+ Minutes = 'Minutes',
+}
+
+type PresignedURLModalProps = {
+ bucketName: string;
+ object: ObjectCrFormat;
+ noobaaS3: S3Commands;
+};
+
+type URLExpiration = {
+ value: number;
+ unit: TimeUnits;
+};
+
+type URLDetails = {
+ url: string;
+ validUntil: string;
+};
+
+type ExpirationInputProps = {
+ urlExpiration: URLExpiration;
+ setURLExpiration: React.Dispatch>;
+};
+
+type CopyURLProps = {
+ urlDetails: React.MutableRefObject;
+};
+
+const ExpirationInput: React.FC = ({
+ urlExpiration,
+ setURLExpiration,
+}) => {
+ const { t } = useCustomTranslation();
+
+ const inputValue = urlExpiration.value;
+ const minValue = 1;
+ const maxValue = urlExpiration.unit === TimeUnits.Minutes ? 720 : 12;
+
+ const onInputChange = (value: number) =>
+ setURLExpiration({ ...urlExpiration, value });
+ const onUnitChange = (unit: TimeUnits) =>
+ setURLExpiration({ value: minValue, unit });
+
+ return (
+
+ onInputChange(inputValue - 1)}
+ onPlus={(): void => onInputChange(inputValue + 1)}
+ className="pf-v5-u-mr-xs"
+ />
+
+
+
+
+ {t('Validity period of the presigned URL.')}
+
+
+
+
+ );
+};
+
+const CopyURL: React.FC = ({ urlDetails }) => {
+ const { t } = useCustomTranslation();
+
+ return (
+
+
+ {t('Valid until: ')}
+ {urlDetails.current.validUntil}{' '}
+
+
+ {urlDetails.current.url}
+
+
+
+ );
+};
+
+const PresignedURLModal: React.FC> = ({
+ closeModal,
+ isOpen,
+ extraProps: { bucketName, object, noobaaS3 },
+}) => {
+ const { t } = useCustomTranslation();
+
+ const urlDetails = React.useRef({ url: null, validUntil: null });
+ const [urlExpiration, setURLExpiration] = React.useState({
+ value: 1,
+ unit: TimeUnits.Minutes,
+ });
+ const [inProgress, setInProgress] = React.useState(false);
+ const [error, setError] = React.useState();
+
+ const objectName = getName(object);
+ const urlCreated = !!urlDetails.current.url;
+
+ const onCopy = () => {
+ navigator.clipboard.writeText(urlDetails.current.url);
+ };
+
+ const onCreate = (event) => {
+ event.preventDefault();
+ setInProgress(true);
+
+ // in seconds
+ const expiresIn =
+ urlExpiration.value *
+ (urlExpiration.unit === TimeUnits.Minutes ? 60 : 3600);
+
+ noobaaS3
+ .getSignedUrl({ Bucket: bucketName, Key: objectName }, expiresIn)
+ .then((url) => {
+ urlDetails.current.url = url;
+ urlDetails.current.validUntil = DateTime.now()
+ .plus({ seconds: expiresIn })
+ .toLocaleString(DateTime.DATETIME_FULL);
+ setInProgress(false);
+ })
+ .catch((err) => {
+ setError(err);
+ setInProgress(false);
+ });
+ };
+
+ return (
+
+ {t('Grant third-party access to an object for a limited time.')}
+
+ }
+ variant={ModalVariant.medium}
+ className="object-presigned-url--height"
+ actions={[
+
+
+
+
+
+ ,
+ ]}
+ >
+
+ {t('Object: ')}
+ {objectName}
+
+
+ {!error &&
+ (urlCreated ? (
+
+ ) : (
+
+ ))}
+
+ );
+};
+
+export default PresignedURLModal;
diff --git a/packages/odf/modals/s3-browser/presigned-url/presigned-modal.scss b/packages/odf/modals/s3-browser/presigned-url/presigned-modal.scss
new file mode 100644
index 000000000..aecc62f5a
--- /dev/null
+++ b/packages/odf/modals/s3-browser/presigned-url/presigned-modal.scss
@@ -0,0 +1,3 @@
+.object-presigned-url--height {
+ height: 26rem;
+}
diff --git a/packages/shared/src/s3/commands.ts b/packages/shared/src/s3/commands.ts
index efdc3e604..eea9f488e 100644
--- a/packages/shared/src/s3/commands.ts
+++ b/packages/shared/src/s3/commands.ts
@@ -2,8 +2,10 @@ import {
S3Client,
ListBucketsCommand,
ListObjectsV2Command,
+ GetObjectCommand,
} from '@aws-sdk/client-s3';
-import { ListBuckets, ListObjectsV2 } from './types';
+import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
+import { ListBuckets, ListObjectsV2, GetSignedUrl } from './types';
export class S3Commands {
private s3Client: S3Client;
@@ -28,4 +30,7 @@ export class S3Commands {
// Object command members
listObjects: ListObjectsV2 = (input) =>
this.s3Client.send(new ListObjectsV2Command(input));
+
+ getSignedUrl: GetSignedUrl = (input, expiresIn) =>
+ getSignedUrl(this.s3Client, new GetObjectCommand(input), { expiresIn });
}
diff --git a/packages/shared/src/s3/types.ts b/packages/shared/src/s3/types.ts
index bad495736..78444bc61 100644
--- a/packages/shared/src/s3/types.ts
+++ b/packages/shared/src/s3/types.ts
@@ -3,6 +3,7 @@ import {
ListBucketsCommandOutput,
ListObjectsV2CommandInput,
ListObjectsV2CommandOutput,
+ GetObjectCommandInput,
} from '@aws-sdk/client-s3';
// Bucket command types
@@ -14,3 +15,8 @@ export type ListBuckets = (
export type ListObjectsV2 = (
input: ListObjectsV2CommandInput
) => Promise;
+
+export type GetSignedUrl = (
+ input: GetObjectCommandInput,
+ expiresIn: number
+) => Promise;
diff --git a/packages/shared/src/utils/common.ts b/packages/shared/src/utils/common.ts
index cbf99deb8..a09aec3bc 100644
--- a/packages/shared/src/utils/common.ts
+++ b/packages/shared/src/utils/common.ts
@@ -172,3 +172,12 @@ export const parseOprMajorMinorVersion = (version: string): string => {
export const getOprMajorMinorVersion = (operator: K8sResourceKind): string =>
parseOprMajorMinorVersion(getOprVersionFromCSV(operator));
+
+export const numberInputOnChange =
+ (min: number, max: number, onChange: (value: number) => void) =>
+ (input: React.FormEvent): void => {
+ const inputValue = +(input.target as HTMLInputElement)?.value;
+ if (!!min && inputValue < min) onChange(min);
+ else if (!!max && inputValue > max) onChange(max);
+ else onChange(inputValue);
+ };
diff --git a/yarn.lock b/yarn.lock
index 955be995c..f3f6fc4f2 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -524,6 +524,20 @@
"@smithy/util-middleware" "^3.0.3"
tslib "^2.6.2"
+"@aws-sdk/s3-request-presigner@3.614.0":
+ version "3.614.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.614.0.tgz#9987c353c39d9771a73230a4fe3bc5eacc389895"
+ integrity sha512-KCXFfnkW8QVtigvStA3zvIHBp/FmwwCBcMgp3WjJNNPVKit3RM70veAWJBZUghHmHtd9fTijO2uwzHtusjkyHw==
+ dependencies:
+ "@aws-sdk/signature-v4-multi-region" "3.614.0"
+ "@aws-sdk/types" "3.609.0"
+ "@aws-sdk/util-format-url" "3.609.0"
+ "@smithy/middleware-endpoint" "^3.0.5"
+ "@smithy/protocol-http" "^4.0.3"
+ "@smithy/smithy-client" "^3.1.7"
+ "@smithy/types" "^3.3.0"
+ tslib "^2.6.2"
+
"@aws-sdk/signature-v4-multi-region@3.614.0":
version "3.614.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.614.0.tgz#fabcef8b1c8ff052d48221dbff66390d6de89b4e"
@@ -572,6 +586,16 @@
"@smithy/util-endpoints" "^2.0.5"
tslib "^2.6.2"
+"@aws-sdk/util-format-url@3.609.0":
+ version "3.609.0"
+ resolved "https://registry.yarnpkg.com/@aws-sdk/util-format-url/-/util-format-url-3.609.0.tgz#f53907193bb636b52b61c81bbe6d7bd5ddc76c68"
+ integrity sha512-fuk29BI/oLQlJ7pfm6iJ4gkEpHdavffAALZwXh9eaY1vQ0ip0aKfRTiNudPoJjyyahnz5yJ1HkmlcDitlzsOrQ==
+ dependencies:
+ "@aws-sdk/types" "3.609.0"
+ "@smithy/querystring-builder" "^3.0.3"
+ "@smithy/types" "^3.3.0"
+ tslib "^2.6.2"
+
"@aws-sdk/util-locate-window@^3.0.0":
version "3.568.0"
resolved "https://registry.yarnpkg.com/@aws-sdk/util-locate-window/-/util-locate-window-3.568.0.tgz#2acc4b2236af0d7494f7e517401ba6b3c4af11ff"
@@ -3062,6 +3086,11 @@
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.191.tgz#09511e7f7cba275acd8b419ddac8da9a6a79e2fa"
integrity sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==
+"@types/luxon@^3.3.1":
+ version "3.4.2"
+ resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-3.4.2.tgz#e4fc7214a420173cea47739c33cdf10874694db7"
+ integrity sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==
+
"@types/mdast@^3.0.0":
version "3.0.7"
resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.7.tgz#cba63d0cc11eb1605cea5c0ad76e02684394166b"
@@ -9350,6 +9379,11 @@ lru-cache@^6.0.0:
dependencies:
yallist "^4.0.0"
+luxon@^3.3.0:
+ version "3.5.0"
+ resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.5.0.tgz#6b6f65c5cd1d61d1fd19dbf07ee87a50bf4b8e20"
+ integrity sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==
+
lz-string@^1.4.4:
version "1.4.4"
resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26"