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 25, 2024
1 parent 88059b6 commit 9d5cc02
Show file tree
Hide file tree
Showing 7 changed files with 138 additions and 4 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',

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
77 changes: 75 additions & 2 deletions src/s3-resource-selector/s3-modal/__tests__/fetching.test.tsx
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down Expand Up @@ -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(<S3Modal {...modalDefaultProps} />);
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(<S3Modal {...modalDefaultProps} />);
const lastUpdated = wrapper.findByClassName(styles['last-updated-caption'])!.find('[aria-live]')!.getElement();

await waitFor(() => {
/**
* <LiveRegion /> 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(<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 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(
<S3Modal
{...modalDefaultProps}
i18nStrings={{ ...modalDefaultProps.i18nStrings, modalLastUpdatedText: undefined }}
/>
);

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

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();
});
});
49 changes: 47 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,19 +5,25 @@ 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';
import InternalTextFilter from '../../text-filter/internal';
import { S3ResourceSelectorProps } from '../interfaces';
import { EmptyState } from './empty-state';

import styles from './styles.css.js';

interface BasicS3TableStrings<T> {
labelRefresh?: string;
labelsPagination?: PaginationProps.Labels;
Expand All @@ -27,6 +33,7 @@ interface BasicS3TableStrings<T> {
filteringAriaLabel?: string;
filteringClearAriaLabel?: string;
filteringCounterText?: S3ResourceSelectorProps.I18nStrings['filteringCounterText'];
lastUpdatedText?: string;
emptyText?: string;
noMatchTitle?: string;
noMatchSubtitle?: string;
Expand All @@ -51,7 +58,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 +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),
};
}

Expand All @@ -80,13 +88,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 +152,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 +182,38 @@ 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;
}

const formattedDate = formatDateLocalized({
date: lastUpdated.toString(),
isDateOnly: false,
});

return (
<InternalBox className={styles['last-updated-caption']}>
{i18nStrings.lastUpdatedText}
<br />
{formattedDate}
<LiveRegion visible={false} source={[i18nStrings.lastUpdatedText, formattedDate]} />
</InternalBox>
);
}

return (
<InternalSpaceBetween size="s" direction="horizontal" alignItems="center">
{getLastUpdated()}
<InternalButton iconName="refresh" ariaLabel={i18nStrings.labelRefresh} onClick={loadData} />
</InternalSpaceBetween>
);
}
8 changes: 8 additions & 0 deletions src/s3-resource-selector/s3-modal/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}

0 comments on commit 9d5cc02

Please sign in to comment.