Skip to content

Commit

Permalink
feat: Add last updated caption to the S3 selector.
Browse files Browse the repository at this point in the history
  • Loading branch information
orangevolon committed Jul 24, 2024
1 parent d2c6019 commit 3b3b8c7
Show file tree
Hide file tree
Showing 6 changed files with 116 additions and 3 deletions.
1 change: 1 addition & 0 deletions pages/s3-resource-selector/data/i18n-strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const i18nStrings: S3ResourceSelectorProps.I18nStrings = {
modalCancelButton: 'Cancel',
modalSubmitButton: 'Choose',
modalBreadcrumbRootItem: 'S3 buckets',
modalLastUpdatedText: 'Last updated at',

selectionBuckets: 'Buckets',
selectionObjects: 'Objects',
Expand Down
5 changes: 5 additions & 0 deletions src/__tests__/__snapshots__/documenter.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/s3-resource-selector/__tests__/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const i18nStrings: S3ResourceSelectorProps.I18nStrings = {
modalCancelButton: 'Cancel',
modalSubmitButton: 'Choose',
modalBreadcrumbRootItem: 'S3 buckets',
modalLastUpdatedText: 'Last updated',

selectionBuckets: 'Buckets',
selectionObjects: 'Objects',
Expand Down
1 change: 1 addition & 0 deletions src/s3-resource-selector/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ export namespace S3ResourceSelectorProps {
modalCancelButton?: string;
modalSubmitButton?: string;
modalBreadcrumbRootItem?: string;
modalLastUpdatedText?: string;

selectionBuckets?: string;
selectionObjects?: string;
Expand Down
63 changes: 62 additions & 1 deletion src/s3-resource-selector/s3-modal/__tests__/main.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import { act, render, screen } from '@testing-library/react';
import { act, render, screen, within } from '@testing-library/react';

import { S3Modal } from '../../../../lib/components/s3-resource-selector/s3-modal';
import createWrapper from '../../../../lib/components/test-utils/dom';
Expand Down Expand Up @@ -116,3 +116,64 @@ test('calls submit handler with selected version', async () => {
versionId: '6036589969ec3d9b2db8faa7',
});
});

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(<S3Modal {...modalDefaultProps} />);
const tableHeader = wrapper.findTable()!.findHeaderSlot()!.getElement();
const lastUpdated = within(tableHeader).getByRole('status');

expect(lastUpdated).toHaveTextContent('Last updatedJanuary 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(<S3Modal {...modalDefaultProps} />);

const refreshFetchDate = new Date('2024-01-02T11:00:00.000Z');
jest.useFakeTimers().setSystemTime(refreshFetchDate);
screen.getByRole('button', { name: i18nStrings.labelRefresh }).click();
await waitForFetch();

const tableHeader = wrapper.findTable()!.findHeaderSlot()!.getElement();
const lastUpdated = within(tableHeader).getByRole('status');
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(
<S3Modal
{...modalDefaultProps}
i18nStrings={{ ...modalDefaultProps.i18nStrings, modalLastUpdatedText: undefined }}
/>
);

const tableHeader = wrapper.findTable()!.findHeaderSlot()!.getElement();
const lastUpdated = within(tableHeader).queryByRole('status');
expect(lastUpdated).not.toBeInTheDocument();
});

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(<S3Modal {...modalDefaultProps} fetchBuckets={fetchBuckets} />);

const refreshFetchDate = new Date('2024-01-02T11:00:00.000Z');
jest.useFakeTimers().setSystemTime(refreshFetchDate);

const tableHeader = wrapper.findTable()!.findHeaderSlot()!.getElement();
const lastUpdated = within(tableHeader).queryByRole('status');
expect(lastUpdated).not.toBeInTheDocument();
});
});
48 changes: 46 additions & 2 deletions src/s3-resource-selector/s3-modal/basic-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -27,6 +31,7 @@ interface BasicS3TableStrings<T> {
filteringAriaLabel?: string;
filteringClearAriaLabel?: string;
filteringCounterText?: S3ResourceSelectorProps.I18nStrings['filteringCounterText'];
lastUpdatedText?: string;
emptyText?: string;
noMatchTitle?: string;
noMatchSubtitle?: string;
Expand All @@ -51,7 +56,7 @@ interface BasicS3TableProps<T> {
export function getSharedI18Strings(
i18n: ComponentFormatFunction<'s3-resource-selector'>,
i18nStrings: S3ResourceSelectorProps.I18nStrings | undefined
) {
): BasicS3TableStrings<unknown> {
return {
filteringCounterText: i18n(
'i18nStrings.filteringCounterText',
Expand All @@ -64,6 +69,7 @@ export function getSharedI18Strings(
noMatchSubtitle: i18n('i18nStrings.filteringCantFindMatch', i18nStrings?.filteringCantFindMatch),
clearFilterButtonText: i18n('i18nStrings.clearFilterButtonText', i18nStrings?.clearFilterButtonText),
filteringClearAriaLabel: i18nStrings?.labelClearFilter,
lastUpdatedText: i18nStrings?.modalLastUpdatedText,
};
}

Expand All @@ -80,13 +86,15 @@ export function BasicS3Table<T>({
}: BasicS3TableProps<T>) {
const [loading, setLoading] = useState(false);
const [allItems, setAllItems] = useState<ReadonlyArray<T>>([]);
const [lastUpdated, setLastUpdated] = useState<Date>();
const textFilterRef = useRef<TextFilterProps.Ref>(null);
const onSelectLatest = useStableCallback(onSelect);

function loadData() {
setLoading(true);
fetchData()
.then(items => {
setLastUpdated(new Date());
setAllItems(items);
setLoading(false);
})
Expand Down Expand Up @@ -142,7 +150,7 @@ export function BasicS3Table<T>({
<InternalHeader
variant={isVisualRefresh ? 'h3' : 'h2'}
headingTagOverride={'h3'}
actions={<InternalButton iconName="refresh" ariaLabel={i18nStrings.labelRefresh} onClick={loadData} />}
actions={<InternalHeaderActions<T> loadData={loadData} i18nStrings={i18nStrings} lastUpdated={lastUpdated} />}
counter={selectedItem ? `(1/${allItems.length})` : `(${allItems.length})`}
>
{i18nStrings.header}
Expand Down Expand Up @@ -172,3 +180,39 @@ export function BasicS3Table<T>({
/>
);
}

interface InternalHeaderActionsProps<T> {
loadData: () => void;
i18nStrings: BasicS3TableProps<T>['i18nStrings'];
lastUpdated: Date | undefined;
}

export function InternalHeaderActions<T>({ i18nStrings, loadData, lastUpdated }: InternalHeaderActionsProps<T>) {
function getLastUpdated() {
if (!lastUpdated || !i18nStrings.lastUpdatedText) {
return null;
}

return (
<InternalBox textAlign="right" color="text-status-inactive">
<LiveRegion visible={true}>
<span role="status">
{i18nStrings.lastUpdatedText}
<br />
{formatDateLocalized({
date: lastUpdated.toString(),
isDateOnly: false,
})}
</span>
</LiveRegion>
</InternalBox>
);
}

return (
<InternalSpaceBetween size="xs" direction="horizontal" alignItems="center">
{getLastUpdated()}
<InternalButton iconName="refresh" ariaLabel={i18nStrings.labelRefresh} onClick={loadData} />
</InternalSpaceBetween>
);
}

0 comments on commit 3b3b8c7

Please sign in to comment.