diff --git a/pages/s3-resource-selector/data/i18n-strings.ts b/pages/s3-resource-selector/data/i18n-strings.ts
index 061966ea69..193622d12f 100644
--- a/pages/s3-resource-selector/data/i18n-strings.ts
+++ b/pages/s3-resource-selector/data/i18n-strings.ts
@@ -17,6 +17,7 @@ export const i18nStrings: S3ResourceSelectorProps.I18nStrings = {
modalCancelButton: 'Cancel',
modalSubmitButton: 'Choose',
modalBreadcrumbRootItem: 'S3 buckets',
+ modalLastUpdatedText: 'Last updated',
selectionBuckets: 'Buckets',
selectionObjects: 'Objects',
diff --git a/src/__tests__/__snapshots__/documenter.test.ts.snap b/src/__tests__/__snapshots__/documenter.test.ts.snap
index 0c865f46ef..a319b2abbc 100644
--- a/src/__tests__/__snapshots__/documenter.test.ts.snap
+++ b/src/__tests__/__snapshots__/documenter.test.ts.snap
@@ -12859,6 +12859,11 @@ The function will be called when a user clicks on the trigger button.",
"optional": true,
"type": "string",
},
+ Object {
+ "name": "modalLastUpdatedText",
+ "optional": true,
+ "type": "string",
+ },
Object {
"name": "modalSubmitButton",
"optional": true,
diff --git a/src/s3-resource-selector/__tests__/fixtures.ts b/src/s3-resource-selector/__tests__/fixtures.ts
index 2c856bb455..0c5e09427d 100644
--- a/src/s3-resource-selector/__tests__/fixtures.ts
+++ b/src/s3-resource-selector/__tests__/fixtures.ts
@@ -23,6 +23,7 @@ export const i18nStrings: S3ResourceSelectorProps.I18nStrings = {
modalCancelButton: 'Cancel',
modalSubmitButton: 'Choose',
modalBreadcrumbRootItem: 'S3 buckets',
+ modalLastUpdatedText: 'Last updated',
selectionBuckets: 'Buckets',
selectionObjects: 'Objects',
diff --git a/src/s3-resource-selector/interfaces.ts b/src/s3-resource-selector/interfaces.ts
index d007e792b3..982381d1af 100644
--- a/src/s3-resource-selector/interfaces.ts
+++ b/src/s3-resource-selector/interfaces.ts
@@ -218,6 +218,7 @@ export namespace S3ResourceSelectorProps {
modalCancelButton?: string;
modalSubmitButton?: string;
modalBreadcrumbRootItem?: string;
+ modalLastUpdatedText?: string;
selectionBuckets?: string;
selectionObjects?: string;
diff --git a/src/s3-resource-selector/s3-modal/__tests__/fetching.test.tsx b/src/s3-resource-selector/s3-modal/__tests__/fetching.test.tsx
index 279237699e..b6643e4240 100644
--- a/src/s3-resource-selector/s3-modal/__tests__/fetching.test.tsx
+++ b/src/s3-resource-selector/s3-modal/__tests__/fetching.test.tsx
@@ -1,13 +1,15 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
-import { act, render } from '@testing-library/react';
+import { act, render, screen, waitFor } from '@testing-library/react';
import { S3Modal } from '../../../../lib/components/s3-resource-selector/s3-modal';
import createWrapper, { ElementWrapper } from '../../../../lib/components/test-utils/dom';
-import { buckets, objects, versions, waitForFetch } from '../../__tests__/fixtures';
+import { buckets, i18nStrings, objects, versions, waitForFetch } from '../../__tests__/fixtures';
import { getElementsText, modalDefaultProps, navigateToTableItem } from './utils';
+import styles from '../../../../lib/components/s3-resource-selector/s3-modal/styles.css.js';
+
jest.setTimeout(30_000);
async function renderModal(jsx: React.ReactElement) {
@@ -126,3 +128,74 @@ test('dives into folders containing slashes in names', async () => {
await navigateToTableItem(wrapper, 1);
expect(fetchObjectsSpy).toHaveBeenCalledWith('bucket-laborum', 'folder/folder///final');
});
+
+describe('last updated status text', () => {
+ beforeEach(() => {
+ const lastUpdatedDate = new Date('2024-01-02T10:00:00.000Z');
+ jest.useFakeTimers().setSystemTime(lastUpdatedDate);
+ });
+
+ afterEach(() => {
+ jest.useRealTimers();
+ });
+
+ test('shows the last updated once the initial fetch is done', async () => {
+ const wrapper = await renderModal();
+ const lastUpdated = wrapper.findByClassName(styles['last-updated-caption'])!.getElement();
+
+ expect(lastUpdated).toHaveTextContent('Last updatedJanuary 2, 2024, 10:00:00 (UTC)');
+ });
+
+ test('renders the last updated time within the aria-live region', async () => {
+ const wrapper = await renderModal();
+ const lastUpdated = wrapper.findByClassName(styles['last-updated-caption'])!.find('[aria-live]')!.getElement();
+
+ await waitFor(() => {
+ /**
+ * component is using innerText for setting the span element's text content.
+ * To populate the textContent from innerText, layout engine is required which JSDom doesn't have.
+ * So falling back to assert against innerText rather than the textContent.
+ */
+ expect(lastUpdated.innerText).toBe('Last updated January 2, 2024, 10:00:00 (UTC)');
+ });
+ });
+
+ test('updates the "last updated" once the refresh button is clicked and the request is settled', async () => {
+ const wrapper = await renderModal();
+
+ const refreshFetchDate = new Date('2024-01-02T11:00:00.000Z');
+ jest.useFakeTimers().setSystemTime(refreshFetchDate);
+ screen.getByRole('button', { name: i18nStrings.labelRefresh }).click();
+ await waitForFetch();
+
+ const lastUpdated = wrapper.findByClassName(styles['last-updated-caption'])!.getElement();
+ expect(lastUpdated).toHaveTextContent('Last updatedJanuary 2, 2024, 11:00:00 (UTC)');
+ });
+
+ test('does not render "Last updated" when the i18n label is not specified', async () => {
+ const wrapper = await renderModal(
+
+ );
+
+ const lastUpdated = wrapper.findByClassName(styles['last-updated-caption']);
+ expect(lastUpdated).toBeFalsy();
+ });
+
+ test('does not render "Last updated" while the initial loading is in progress', async () => {
+ const fetchBuckets = jest.fn().mockReturnValue(
+ new Promise(() => {
+ // never resolving promise
+ })
+ );
+ const wrapper = await renderModal();
+
+ const refreshFetchDate = new Date('2024-01-02T11:00:00.000Z');
+ jest.useFakeTimers().setSystemTime(refreshFetchDate);
+
+ const lastUpdated = wrapper.findByClassName(styles['last-updated-caption']);
+ expect(lastUpdated).toBeFalsy();
+ });
+});
diff --git a/src/s3-resource-selector/s3-modal/basic-table.tsx b/src/s3-resource-selector/s3-modal/basic-table.tsx
index ac7e5c36c0..66dc249747 100644
--- a/src/s3-resource-selector/s3-modal/basic-table.tsx
+++ b/src/s3-resource-selector/s3-modal/basic-table.tsx
@@ -5,12 +5,16 @@ import React, { useEffect, useRef, useState } from 'react';
import { useCollection } from '@cloudscape-design/collection-hooks';
import { useStableCallback } from '@cloudscape-design/component-toolkit/internal';
+import InternalBox from '../../box/internal';
import { InternalButton } from '../../button/internal';
import InternalHeader from '../../header/internal';
import { ComponentFormatFunction } from '../../i18n/context';
+import LiveRegion from '../../internal/components/live-region';
import useForwardFocus, { ForwardFocusRef } from '../../internal/hooks/forward-focus';
+import formatDateLocalized from '../../internal/utils/date-time/format-date-localized';
import { PaginationProps } from '../../pagination/interfaces';
import InternalPagination from '../../pagination/internal';
+import InternalSpaceBetween from '../../space-between/internal';
import { TableProps } from '../../table/interfaces';
import InternalTable from '../../table/internal';
import { TextFilterProps } from '../../text-filter/interfaces';
@@ -18,6 +22,8 @@ import InternalTextFilter from '../../text-filter/internal';
import { S3ResourceSelectorProps } from '../interfaces';
import { EmptyState } from './empty-state';
+import styles from './styles.css.js';
+
interface BasicS3TableStrings {
labelRefresh?: string;
labelsPagination?: PaginationProps.Labels;
@@ -27,6 +33,7 @@ interface BasicS3TableStrings {
filteringAriaLabel?: string;
filteringClearAriaLabel?: string;
filteringCounterText?: S3ResourceSelectorProps.I18nStrings['filteringCounterText'];
+ lastUpdatedText?: string;
emptyText?: string;
noMatchTitle?: string;
noMatchSubtitle?: string;
@@ -51,7 +58,7 @@ interface BasicS3TableProps {
export function getSharedI18Strings(
i18n: ComponentFormatFunction<'s3-resource-selector'>,
i18nStrings: S3ResourceSelectorProps.I18nStrings | undefined
-) {
+): BasicS3TableStrings {
return {
filteringCounterText: i18n(
'i18nStrings.filteringCounterText',
@@ -64,6 +71,7 @@ export function getSharedI18Strings(
noMatchSubtitle: i18n('i18nStrings.filteringCantFindMatch', i18nStrings?.filteringCantFindMatch),
clearFilterButtonText: i18n('i18nStrings.clearFilterButtonText', i18nStrings?.clearFilterButtonText),
filteringClearAriaLabel: i18nStrings?.labelClearFilter,
+ lastUpdatedText: i18n('i18nStrings.modalLastUpdatedText', i18nStrings?.modalLastUpdatedText),
};
}
@@ -80,6 +88,7 @@ export function BasicS3Table({
}: BasicS3TableProps) {
const [loading, setLoading] = useState(false);
const [allItems, setAllItems] = useState>([]);
+ const [lastUpdated, setLastUpdated] = useState();
const textFilterRef = useRef(null);
const onSelectLatest = useStableCallback(onSelect);
@@ -87,6 +96,7 @@ export function BasicS3Table({
setLoading(true);
fetchData()
.then(items => {
+ setLastUpdated(new Date());
setAllItems(items);
setLoading(false);
})
@@ -142,7 +152,7 @@ export function BasicS3Table({
}
+ actions={ loadData={loadData} i18nStrings={i18nStrings} lastUpdated={lastUpdated} />}
counter={selectedItem ? `(1/${allItems.length})` : `(${allItems.length})`}
>
{i18nStrings.header}
@@ -172,3 +182,38 @@ export function BasicS3Table({
/>
);
}
+
+interface InternalHeaderActionsProps {
+ loadData: () => void;
+ i18nStrings: BasicS3TableProps['i18nStrings'];
+ lastUpdated: Date | undefined;
+}
+
+export function InternalHeaderActions({ i18nStrings, loadData, lastUpdated }: InternalHeaderActionsProps) {
+ function getLastUpdated() {
+ if (!lastUpdated || !i18nStrings.lastUpdatedText) {
+ return null;
+ }
+
+ const formattedDate = formatDateLocalized({
+ date: lastUpdated.toString(),
+ isDateOnly: false,
+ });
+
+ return (
+
+ {i18nStrings.lastUpdatedText}
+
+ {formattedDate}
+
+
+ );
+ }
+
+ return (
+
+ {getLastUpdated()}
+
+
+ );
+}
diff --git a/src/s3-resource-selector/s3-modal/styles.scss b/src/s3-resource-selector/s3-modal/styles.scss
index abbce55297..755bde2754 100644
--- a/src/s3-resource-selector/s3-modal/styles.scss
+++ b/src/s3-resource-selector/s3-modal/styles.scss
@@ -2,6 +2,7 @@
Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
+@use '../../internal/styles/tokens.scss' as awsui;
.modal-actions {
justify-content: flex-end;
@@ -10,3 +11,10 @@
.submit-button {
/* used in test-utils */
}
+
+.last-updated-caption {
+ @include styles.font-heading-xs;
+
+ text-align: end;
+ color: awsui.$color-text-status-inactive;
+}