diff --git a/locales/en/plugin__odf-console.json b/locales/en/plugin__odf-console.json
index e1f709c1b..501a48d05 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",
@@ -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",
@@ -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",
@@ -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",
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/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 8d17fbe3d..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,
@@ -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}
/>
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..b23e32231 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';
@@ -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'),
@@ -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
+ >
): 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'),
@@ -87,7 +107,13 @@ export const TableRow: React.FC> = ({
}) => {
const { t } = useCustomTranslation();
- const { launcher, bucketName, foldersPath } = extraProps;
+ 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, '');
const prefix = !!foldersPath
@@ -120,7 +146,15 @@ 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 06c34ada8..17461d097 100644
--- a/packages/shared/src/s3/commands.ts
+++ b/packages/shared/src/s3/commands.ts
@@ -4,12 +4,16 @@ import {
ListObjectsV2Command,
CreateBucketCommand,
PutBucketTaggingCommand,
+ GetObjectCommand,
} from '@aws-sdk/client-s3';
+import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import {
CreateBucket,
ListBuckets,
ListObjectsV2,
PutBucketTags,
+ GetSignedUrl,
+ GetObject,
} from './types';
export class S3Commands {
@@ -41,4 +45,10 @@ 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 });
+
+ 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 56ecb27fc..1a83fe614 100644
--- a/packages/shared/src/s3/types.ts
+++ b/packages/shared/src/s3/types.ts
@@ -7,6 +7,8 @@ import {
ListObjectsV2CommandOutput,
PutBucketTaggingCommandInput,
PutBucketTaggingCommandOutput,
+ GetObjectCommandInput,
+ GetObjectCommandOutput,
} from '@aws-sdk/client-s3';
// Bucket command types
@@ -26,3 +28,12 @@ export type PutBucketTags = (
export type ListObjectsV2 = (
input: ListObjectsV2CommandInput
) => Promise;
+
+export type GetSignedUrl = (
+ input: GetObjectCommandInput,
+ expiresIn: number
+) => Promise;
+
+export type GetObject = (
+ input: GetObjectCommandInput
+) => 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"