Skip to content

Commit 6e2ae56

Browse files
Create improved DateFilterV2 component and update Basic Visibility filters (#896)
* Create updated DateFilterV2 component that supports dynamic quick ranges and has improved UX * Use new date filter in Basic Visibility workflow listing * Fix type for UseMergedInfiniteQueries query key * Fix unit tests to run in UTC * Fix unit tests for Basic Visibility table (they were broken ever since we updated the page size to 20)
1 parent 6e3f65f commit 6e2ae56

25 files changed

+1001
-112
lines changed

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@
1616
"generate:idl": "mkdir -p src/__generated__/idl && npm run generate:idl:proto && npm run generate:idl:proto:types",
1717
"generate:idl:proto": "rm -rf src/__generated__/idl/proto && cp -R node_modules/cadence-idl/proto src/__generated__/idl/proto",
1818
"generate:idl:proto:types": "rm -rf src/__generated__/proto-ts && ./node_modules/.bin/proto-loader-gen-types --includeDirs=src/__generated__/idl/proto/ --enums=String --longs=String --bytes=String --defaults --inputTemplate='%s__Input' --outputTemplate='%s' --oneofs --grpcLib=@grpc/grpc-js --outDir=src/__generated__/proto-ts/ $(npx glob --all --nodir --cwd=src/__generated__/idl/proto **/*.proto) ",
19-
"test": "jest --config jest.config.ts && npm run test:types",
19+
"test": "TZ=UTC jest --config jest.config.ts && npm run test:types",
2020
"test:unit": "npm run test:unit:browser && npm run test:unit:node",
21-
"test:unit:browser": "jest --config jest/browser/jest.config.ts",
22-
"test:unit:node": "jest --config jest/node/jest.config.ts",
21+
"test:unit:browser": "TZ=UTC jest --config jest/browser/jest.config.ts",
22+
"test:unit:node": "TZ=UTC jest --config jest/node/jest.config.ts",
2323
"test:types": "tstyche"
2424
},
2525
"engines": {
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
import React, { useState } from 'react';
2+
3+
import { type StatefulPopoverProps } from 'baseui/popover';
4+
import { type TimePickerProps } from 'baseui/timepicker';
5+
6+
import { render, screen, fireEvent, act, waitFor } from '@/test-utils/rtl';
7+
8+
import DateFilterV2 from '../date-filter-v2';
9+
import { type DateFilterRange } from '../date-filter-v2.types';
10+
11+
jest.useFakeTimers().setSystemTime(new Date('2023-05-25'));
12+
13+
// Mock StatefulPopover to render content immediately in tests
14+
jest.mock('baseui/popover', () => {
15+
const originalModule = jest.requireActual('baseui/popover');
16+
return {
17+
...originalModule,
18+
StatefulPopover: ({ content, children }: StatefulPopoverProps) => {
19+
const [isShown, setIsShown] = useState(false);
20+
21+
return (
22+
<div onClick={() => setIsShown(true)}>
23+
{children}
24+
{isShown ? (
25+
<div data-testid="popover-content">
26+
{typeof content === 'function' &&
27+
content({ close: () => setIsShown(false) })}
28+
</div>
29+
) : null}
30+
</div>
31+
);
32+
},
33+
};
34+
});
35+
36+
jest.mock('baseui/timepicker', () => ({
37+
TimePicker: jest.fn(
38+
({ value, onChange, disabled }: TimePickerProps) =>
39+
onChange && (
40+
<input
41+
data-testid="time-picker"
42+
value={value?.toTimeString() ?? ''}
43+
onChange={(e) => onChange(new Date(e.target.value))}
44+
disabled={disabled}
45+
/>
46+
)
47+
),
48+
}));
49+
50+
const mockDateOverrides: DateFilterRange = {
51+
start: new Date('2023-05-23T00:00:00.000Z'),
52+
end: new Date('2023-05-24T00:00:00.000Z'),
53+
};
54+
55+
describe(DateFilterV2.name, () => {
56+
it('displays the date filter component with placeholder when no dates are provided', () => {
57+
setup({});
58+
expect(screen.getByPlaceholderText('Mock placeholder')).toBeInTheDocument();
59+
});
60+
61+
it('renders without errors when dates are already provided', () => {
62+
setup({
63+
overrides: mockDateOverrides,
64+
});
65+
66+
expect(
67+
screen.getByDisplayValue('23 May, 00:00:00 UTC - 24 May, 00:00:00 UTC')
68+
).toBeInTheDocument();
69+
});
70+
71+
it('opens a popover when clicked', () => {
72+
setup({});
73+
const datePicker = screen.getByPlaceholderText('Mock placeholder');
74+
75+
act(() => {
76+
fireEvent.click(datePicker);
77+
});
78+
79+
// Check for elements in the popover
80+
expect(screen.getByText('Quick Range')).toBeInTheDocument();
81+
expect(screen.getByText('Custom Range')).toBeInTheDocument();
82+
expect(screen.getByText('Last 5 minutes')).toBeInTheDocument();
83+
});
84+
85+
it('selects a relative time range when clicking a quick range button', () => {
86+
const { mockOnChangeDates } = setup({});
87+
const datePicker = screen.getByPlaceholderText('Mock placeholder');
88+
89+
act(() => {
90+
fireEvent.click(datePicker);
91+
});
92+
93+
const lastFiveMinutesButton = screen.getByText('Last 5 minutes');
94+
95+
act(() => {
96+
fireEvent.click(lastFiveMinutesButton);
97+
});
98+
99+
expect(mockOnChangeDates).toHaveBeenCalledWith({
100+
start: 'now-5m',
101+
end: 'now',
102+
});
103+
});
104+
105+
it('allows selecting a custom date range via the calendar', () => {
106+
const { mockOnChangeDates } = setup({});
107+
const datePicker = screen.getByPlaceholderText('Mock placeholder');
108+
109+
act(() => {
110+
fireEvent.click(datePicker);
111+
});
112+
113+
const saveButton = screen.getByText('Save');
114+
const timePickers = screen.getAllByTestId('time-picker');
115+
116+
act(() => {
117+
fireEvent.click(screen.getByLabelText(/May 13th 2023/));
118+
});
119+
120+
expect(saveButton).toBeDisabled();
121+
timePickers.forEach((picker) => {
122+
expect(picker).toBeDisabled();
123+
});
124+
125+
act(() => {
126+
fireEvent.click(screen.getByLabelText(/May 14th 2023/));
127+
});
128+
129+
expect(saveButton).not.toBeDisabled();
130+
timePickers.forEach((picker) => {
131+
expect(picker).not.toBeDisabled();
132+
});
133+
134+
act(() => {
135+
fireEvent.click(saveButton);
136+
});
137+
138+
expect(mockOnChangeDates).toHaveBeenCalledWith({
139+
start: new Date('2023-05-13T00:00:00.000Z'),
140+
end: new Date('2023-05-14T00:00:00.000Z'),
141+
});
142+
});
143+
144+
it('handles single date selection (same start and end date)', () => {
145+
const { mockOnChangeDates } = setup({});
146+
const datePicker = screen.getByPlaceholderText('Mock placeholder');
147+
148+
act(() => {
149+
fireEvent.click(datePicker);
150+
});
151+
152+
act(() => {
153+
fireEvent.click(screen.getByLabelText(/May 13th 2023/));
154+
});
155+
156+
act(() => {
157+
fireEvent.click(screen.getByLabelText(/May 13th 2023/));
158+
});
159+
160+
const saveButton = screen.getByText('Save');
161+
act(() => {
162+
fireEvent.click(saveButton);
163+
});
164+
165+
expect(mockOnChangeDates).toHaveBeenCalledWith({
166+
start: new Date('2023-05-13T00:00:00.000Z'),
167+
end: new Date('2023-05-13T23:59:59.999Z'),
168+
});
169+
});
170+
171+
it('allows time adjustment after date selection', () => {
172+
const { mockOnChangeDates } = setup({});
173+
const datePicker = screen.getByPlaceholderText('Mock placeholder');
174+
175+
act(() => {
176+
fireEvent.click(datePicker);
177+
});
178+
179+
act(() => {
180+
fireEvent.click(screen.getByLabelText(/May 13th 2023/));
181+
});
182+
183+
act(() => {
184+
fireEvent.click(screen.getByLabelText(/May 14th 2023/));
185+
});
186+
187+
const timePickers = screen.getAllByTestId('time-picker');
188+
expect(timePickers).toHaveLength(2);
189+
190+
act(() => {
191+
fireEvent.change(timePickers[0], {
192+
target: { value: '2023-05-13 11:45' },
193+
});
194+
fireEvent.change(timePickers[1], {
195+
target: { value: '2023-05-14 15:45' },
196+
});
197+
});
198+
199+
const saveButton = screen.getByText('Save');
200+
act(() => {
201+
fireEvent.click(saveButton);
202+
});
203+
204+
expect(mockOnChangeDates).toHaveBeenCalledWith(
205+
expect.objectContaining({
206+
start: new Date('2023-05-13T11:45:00.000Z'),
207+
end: new Date('2023-05-14T15:45:00.000Z'),
208+
})
209+
);
210+
});
211+
212+
it('displays the correct format when using relative date values', () => {
213+
setup({
214+
overrides: {
215+
start: 'now-1h',
216+
end: 'now',
217+
},
218+
});
219+
220+
expect(screen.getByDisplayValue('Last 1 hour')).toBeInTheDocument();
221+
});
222+
223+
it('closes popover when clicking the close button', () => {
224+
setup({});
225+
const datePicker = screen.getByPlaceholderText('Mock placeholder');
226+
227+
act(() => {
228+
fireEvent.click(datePicker);
229+
});
230+
231+
const quickRangeHeader = screen.getByText('Quick Range');
232+
expect(quickRangeHeader).toBeInTheDocument();
233+
234+
const closeButton = screen.getByTestId('close-button');
235+
236+
act(() => {
237+
fireEvent.click(closeButton);
238+
});
239+
240+
waitFor(() => {
241+
expect(quickRangeHeader).not.toBeInTheDocument();
242+
});
243+
});
244+
});
245+
246+
function setup({ overrides }: { overrides?: Partial<DateFilterRange> }) {
247+
const mockOnChangeDates = jest.fn();
248+
249+
const result = render(
250+
<DateFilterV2
251+
label="Mock label"
252+
placeholder="Mock placeholder"
253+
dates={{
254+
start: undefined,
255+
end: undefined,
256+
...overrides,
257+
}}
258+
onChangeDates={mockOnChangeDates}
259+
/>
260+
);
261+
262+
return { mockOnChangeDates, ...result };
263+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { type RelativeDurationConfig } from './date-filter-v2.types';
2+
3+
export const DATE_FILTER_RELATIVE_VALUES = {
4+
'now-5m': { label: 'Last 5 minutes', durationSeconds: 5 * 60 },
5+
'now-15m': { label: 'Last 15 minutes', durationSeconds: 15 * 60 },
6+
'now-1h': { label: 'Last 1 hour', durationSeconds: 1 * 60 * 60 },
7+
'now-6h': { label: 'Last 6 hours', durationSeconds: 6 * 60 * 60 },
8+
'now-12h': { label: 'Last 12 hours', durationSeconds: 12 * 60 * 60 },
9+
'now-1d': { label: 'Last 1 day', durationSeconds: 1 * 24 * 60 * 60 },
10+
'now-7d': { label: 'Last 7 days', durationSeconds: 7 * 24 * 60 * 60 },
11+
'now-30d': { label: 'Last 30 days', durationSeconds: 30 * 24 * 60 * 60 },
12+
} as const satisfies Record<`now-${string}`, RelativeDurationConfig>;

0 commit comments

Comments
 (0)