Skip to content

Commit 4b6fd9c

Browse files
chore: Improve Table task flow capture (#3516)
1 parent 3cab2eb commit 4b6fd9c

File tree

8 files changed

+279
-72
lines changed

8 files changed

+279
-72
lines changed

pages/funnel-analytics/table.page.tsx

Lines changed: 0 additions & 39 deletions
This file was deleted.
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
import React, { useState } from 'react';
4+
5+
import { useCollection } from '@cloudscape-design/collection-hooks';
6+
7+
import {
8+
Button,
9+
CollectionPreferences,
10+
CollectionPreferencesProps,
11+
Header,
12+
Pagination,
13+
Table,
14+
TableProps,
15+
TextFilter,
16+
} from '~components';
17+
import { setComponentMetrics } from '~components/internal/analytics';
18+
19+
import { contentDisplayPreferenceI18nStrings } from '../common/i18n-strings';
20+
import { generateItems, Instance } from '../table/generate-data';
21+
import {
22+
columnsConfig,
23+
contentDisplayPreference,
24+
defaultPreferences,
25+
EmptyState,
26+
getMatchesCountText,
27+
pageSizeOptions,
28+
paginationLabels,
29+
} from '../table/shared-configs';
30+
31+
const componentMetricsLog: any[] = [];
32+
(window as any).__awsuiComponentlMetrics__ = componentMetricsLog;
33+
34+
setComponentMetrics({
35+
componentMounted: props => {
36+
componentMetricsLog.push({ name: 'componentMounted', detail: { ...props } });
37+
return props.taskInteractionId || 'mocked-task-interaction-id';
38+
},
39+
componentUpdated: props => {
40+
componentMetricsLog.push({ name: 'componentUpdated', detail: { ...props } });
41+
},
42+
});
43+
44+
const allItems = generateItems();
45+
46+
export default function WithTablePage() {
47+
const [selectedItems, setSelectedItems] = useState<TableProps['selectedItems']>([]);
48+
const [preferences, setPreferences] = useState<CollectionPreferencesProps.Preferences>(defaultPreferences);
49+
const { items, actions, filteredItemsCount, collectionProps, filterProps, paginationProps } = useCollection(
50+
allItems,
51+
{
52+
filtering: {
53+
empty: (
54+
<EmptyState
55+
title="No resources"
56+
subtitle="No resources to display."
57+
action={<Button>Create resource</Button>}
58+
/>
59+
),
60+
noMatch: (
61+
<EmptyState
62+
title="No matches"
63+
subtitle="We can’t find a match."
64+
action={<Button onClick={() => actions.setFiltering('')}>Clear filter</Button>}
65+
/>
66+
),
67+
},
68+
pagination: { pageSize: preferences.pageSize },
69+
sorting: {},
70+
}
71+
);
72+
73+
return (
74+
<>
75+
<Table<Instance>
76+
ariaLabels={{
77+
selectionGroupLabel: 'selectionGroupLabel',
78+
activateEditLabel: () => 'activateEditLabel',
79+
cancelEditLabel: () => 'cancelEditLabel',
80+
submitEditLabel: () => 'submitEditLabel',
81+
allItemsSelectionLabel: () => 'allItemsSelectionLabel',
82+
itemSelectionLabel: () => 'itemSelectionLabel',
83+
tableLabel: 'tableLabel',
84+
expandButtonLabel: () => 'expand row',
85+
collapseButtonLabel: () => 'collapse row',
86+
}}
87+
{...collectionProps}
88+
analyticsMetadata={{
89+
resourceType: 'table-resource-type',
90+
flowType: 'view-resource',
91+
}}
92+
selectionType="multi"
93+
header={
94+
<Header headingTagOverride="h1" counter={`(${allItems.length})`}>
95+
Table title
96+
</Header>
97+
}
98+
selectedItems={selectedItems}
99+
columnDefinitions={columnsConfig}
100+
items={items}
101+
pagination={<Pagination {...paginationProps} ariaLabels={paginationLabels} />}
102+
filter={
103+
<TextFilter
104+
{...filterProps!}
105+
countText={getMatchesCountText(filteredItemsCount!)}
106+
filteringAriaLabel="Filter instances"
107+
/>
108+
}
109+
columnDisplay={preferences.contentDisplay}
110+
preferences={
111+
<CollectionPreferences
112+
title="Preferences"
113+
confirmLabel="Confirm"
114+
cancelLabel="Cancel"
115+
onConfirm={({ detail }) => setPreferences(detail)}
116+
preferences={preferences}
117+
pageSizePreference={{
118+
title: 'Select page size',
119+
options: pageSizeOptions,
120+
}}
121+
contentDisplayPreference={{
122+
...contentDisplayPreference,
123+
...contentDisplayPreferenceI18nStrings,
124+
}}
125+
wrapLinesPreference={{
126+
label: 'Wrap lines',
127+
description: 'Wrap lines description',
128+
}}
129+
/>
130+
}
131+
stickyHeader={true}
132+
onSelectionChange={({ detail }) => setSelectedItems(detail.selectedItems)}
133+
/>
134+
</>
135+
);
136+
}

src/internal/context/table-component-context.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@ import { createContext, RefObject, useContext } from 'react';
44

55
export interface FilterRef {
66
filterText?: string;
7+
countText?: string;
78
}
89

910
export interface PaginationRef {
1011
currentPageIndex?: number;
1112
totalPageCount?: number;
13+
openEnd?: boolean;
1214
}
1315
interface TableComponentsContextProps {
1416
filterRef: RefObject<FilterRef>;

src/internal/hooks/use-table-interaction-metrics/__tests__/use-table-interaction-metrics.test.tsx

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,34 +17,38 @@ import {
1717
import { renderHook, RenderHookOptions } from '../../../../__tests__/render-hook';
1818
import { mockFunnelMetrics, mockPerformanceMetrics } from '../../../analytics/__tests__/mocks';
1919

20-
type RenderProps = Partial<UseTableInteractionMetricsProps>;
20+
type RenderProps<T> = Partial<UseTableInteractionMetricsProps<T>>;
2121

2222
const defaultProps = {
2323
getComponentConfiguration: () => ({}),
2424
getComponentIdentifier: () => 'My resources',
25+
items: [],
2526
itemCount: 10,
2627
loading: undefined,
2728
instanceIdentifier: undefined,
2829
interactionMetadata: () => '',
29-
} satisfies RenderProps;
30+
} satisfies RenderProps<any>;
3031

31-
function renderUseTableInteractionMetricsHook(props: RenderProps, wrapper?: RenderHookOptions<RenderProps>['wrapper']) {
32+
function renderUseTableInteractionMetricsHook(
33+
props: RenderProps<any>,
34+
wrapper?: RenderHookOptions<RenderProps<any>>['wrapper']
35+
) {
3236
const elementRef = createRef<HTMLElement>();
3337

34-
const { result, rerender, unmount } = renderHook(useTableInteractionMetrics, {
38+
const { result, rerender, unmount } = renderHook(useTableInteractionMetrics<any>, {
3539
initialProps: { elementRef, ...defaultProps, ...props },
3640
wrapper,
3741
});
3842

3943
return {
4044
tableInteractionAttributes: result.current.tableInteractionAttributes,
4145
setLastUserAction: (name: string) => result.current.setLastUserAction(name),
42-
rerender: (props: RenderProps) => rerender({ elementRef, ...defaultProps, ...props }),
46+
rerender: (props: RenderProps<any>) => rerender({ elementRef, ...defaultProps, ...props }),
4347
unmount,
4448
};
4549
}
4650

47-
function TestComponent(props: RenderProps) {
51+
function TestComponent(props: RenderProps<any>) {
4852
const elementRef = useRef<HTMLDivElement>(null);
4953
const { tableInteractionAttributes } = useTableInteractionMetrics({ elementRef, ...defaultProps, ...props });
5054
return <div {...tableInteractionAttributes} ref={elementRef} data-testid="element" />;
@@ -67,12 +71,16 @@ setComponentMetrics({
6771
});
6872

6973
beforeEach(() => {
74+
jest.useFakeTimers();
7075
jest.resetAllMocks();
7176
mockPerformanceMetrics();
7277
mockFunnelMetrics();
7378
});
7479

75-
jest.useFakeTimers();
80+
afterEach(() => {
81+
jest.runOnlyPendingTimers();
82+
jest.useRealTimers();
83+
});
7684

7785
describe('useTableInteractionMetrics', () => {
7886
test('should emit componentMount event on mount', () => {
@@ -142,6 +150,7 @@ describe('useTableInteractionMetrics', () => {
142150
})
143151
);
144152

153+
jest.runAllTimers();
145154
expect(ComponentMetrics.componentUpdated).toHaveBeenCalledTimes(1);
146155
expect(ComponentMetrics.componentUpdated).toHaveBeenCalledWith({
147156
taskInteractionId: expect.any(String),
@@ -156,9 +165,8 @@ describe('useTableInteractionMetrics', () => {
156165

157166
setLastUserAction('filter');
158167
rerender({ loading: true });
159-
160-
jest.advanceTimersByTime(3456);
161168
rerender({ loading: false });
169+
jest.runAllTimers();
162170
expect(ComponentMetrics.componentUpdated).toHaveBeenCalledTimes(0);
163171
});
164172

@@ -195,6 +203,14 @@ describe('useTableInteractionMetrics', () => {
195203
userAction: 'pagination',
196204
})
197205
);
206+
207+
jest.runAllTimers();
208+
expect(ComponentMetrics.componentUpdated).toHaveBeenCalledTimes(1);
209+
expect(ComponentMetrics.componentUpdated).toHaveBeenCalledWith(
210+
expect.objectContaining({
211+
actionType: 'pagination',
212+
})
213+
);
198214
});
199215

200216
test('user actions during the loading state should be ignored', () => {
@@ -209,13 +225,6 @@ describe('useTableInteractionMetrics', () => {
209225

210226
rerender({ loading: false });
211227

212-
expect(ComponentMetrics.componentUpdated).toHaveBeenCalledTimes(1);
213-
expect(ComponentMetrics.componentUpdated).toHaveBeenCalledWith(
214-
expect.objectContaining({
215-
actionType: 'filter',
216-
})
217-
);
218-
219228
expect(PerformanceMetrics.tableInteraction).toHaveBeenCalledTimes(1);
220229
expect(PerformanceMetrics.tableInteraction).toHaveBeenCalledWith(
221230
expect.objectContaining({
@@ -255,6 +264,7 @@ describe('useTableInteractionMetrics', () => {
255264
getComponentConfiguration: () => componentConfiguration,
256265
});
257266

267+
jest.runAllTimers();
258268
expect(ComponentMetrics.componentUpdated).toHaveBeenCalledTimes(1);
259269
expect(ComponentMetrics.componentUpdated).toHaveBeenCalledWith(
260270
expect.objectContaining({

src/internal/hooks/use-table-interaction-metrics/index.ts

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useEffect, useRef } from 'react';
66
import { ComponentMetrics, PerformanceMetrics } from '../../analytics';
77
import { useFunnel } from '../../analytics/hooks/use-funnel';
88
import { JSONObject } from '../../analytics/interfaces';
9+
import { useDebounceCallback } from '../use-debounce-callback';
910
import { useDOMAttribute } from '../use-dom-attribute';
1011
import { useEffectOnUpdate } from '../use-effect-on-update';
1112
import { useRandomId } from '../use-unique-id';
@@ -16,25 +17,27 @@ to be the cause of the current loading state.
1617
*/
1718
const USER_ACTION_TIME_LIMIT = 1_000;
1819

19-
export interface UseTableInteractionMetricsProps {
20+
export interface UseTableInteractionMetricsProps<T> {
2021
elementRef: React.RefObject<HTMLElement>;
2122
instanceIdentifier: string | undefined;
2223
loading: boolean | undefined;
24+
items: readonly T[];
2325
itemCount: number;
2426
getComponentIdentifier: () => string | undefined;
2527
getComponentConfiguration: () => JSONObject;
2628
interactionMetadata: () => string;
2729
}
2830

29-
export function useTableInteractionMetrics({
31+
export function useTableInteractionMetrics<T>({
3032
elementRef,
33+
items,
3134
itemCount,
3235
instanceIdentifier,
3336
getComponentIdentifier,
3437
getComponentConfiguration,
3538
loading = false,
3639
interactionMetadata,
37-
}: UseTableInteractionMetricsProps) {
40+
}: UseTableInteractionMetricsProps<T>) {
3841
const taskInteractionId = useRandomId();
3942
const tableInteractionAttributes = useDOMAttribute(
4043
elementRef,
@@ -86,18 +89,27 @@ export function useTableInteractionMetrics({
8689
instanceIdentifier,
8790
noOfResourcesInTable: metadata.current.itemCount,
8891
});
89-
90-
if (!isInFunnel) {
91-
ComponentMetrics.componentUpdated({
92-
taskInteractionId,
93-
componentName: 'table',
94-
actionType: capturedUserAction.current ?? '',
95-
componentConfiguration: metadata.current.getComponentConfiguration(),
96-
});
97-
}
9892
}
9993
}, [instanceIdentifier, loading, taskInteractionId, isInFunnel]);
10094

95+
const debouncedUpdated = useDebounceCallback(() => {
96+
ComponentMetrics.componentUpdated({
97+
taskInteractionId,
98+
componentName: 'table',
99+
actionType: lastUserAction.current?.name ?? '',
100+
componentConfiguration: metadata.current.getComponentConfiguration(),
101+
});
102+
});
103+
104+
useEffectOnUpdate(() => {
105+
if (isInFunnel || loading) {
106+
return;
107+
}
108+
109+
debouncedUpdated();
110+
// Note: items used as a dependency here to trigger updates as a side effect
111+
}, [taskInteractionId, isInFunnel, loading, items, debouncedUpdated]);
112+
101113
return {
102114
tableInteractionAttributes,
103115
setLastUserAction: (name: string) => void (lastUserAction.current = { name, time: performance.now() }),

src/pagination/internal.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ export default function InternalPagination({
151151
if (tableComponentContext?.paginationRef?.current) {
152152
tableComponentContext.paginationRef.current.currentPageIndex = currentPageIndex;
153153
tableComponentContext.paginationRef.current.totalPageCount = pagesCount;
154+
tableComponentContext.paginationRef.current.openEnd = openEnd;
154155
}
155156
return (
156157
<ul

0 commit comments

Comments
 (0)