diff --git a/package.json b/package.json index 078f42599..e906c7c51 100644 --- a/package.json +++ b/package.json @@ -13,10 +13,10 @@ "generate:idl": "mkdir -p src/__generated__/idl && npm run generate:idl:proto && npm run generate:idl:proto:types", "generate:idl:proto": "rm -rf src/__generated__/idl/proto && cp -R node_modules/cadence-idl/proto src/__generated__/idl/proto", "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) ", - "test": "jest --config jest.config.ts && npm run test:types", + "test": "TZ=UTC jest --config jest.config.ts && npm run test:types", "test:unit": "npm run test:unit:browser && npm run test:unit:node", - "test:unit:browser": "jest --config jest/browser/jest.config.ts", - "test:unit:node": "jest --config jest/node/jest.config.ts", + "test:unit:browser": "TZ=UTC jest --config jest/browser/jest.config.ts", + "test:unit:node": "TZ=UTC jest --config jest/node/jest.config.ts", "test:types": "tstyche" }, "engines": { diff --git a/src/components/date-filter-v2/__tests__/date-filter-v2.test.tsx b/src/components/date-filter-v2/__tests__/date-filter-v2.test.tsx new file mode 100644 index 000000000..0a3b54e8d --- /dev/null +++ b/src/components/date-filter-v2/__tests__/date-filter-v2.test.tsx @@ -0,0 +1,265 @@ +import React, { useState } from 'react'; + +import { type StatefulPopoverProps } from 'baseui/popover'; +import { type TimePickerProps } from 'baseui/timepicker'; + +import { render, screen, fireEvent, act, waitFor } from '@/test-utils/rtl'; + +import dayjs from '@/utils/datetime/dayjs'; + +import DateFilterV2 from '../date-filter-v2'; +import { type DateFilterRange } from '../date-filter-v2.types'; + +jest.useFakeTimers().setSystemTime(new Date('2023-05-25')); + +// Mock StatefulPopover to render content immediately in tests +jest.mock('baseui/popover', () => { + const originalModule = jest.requireActual('baseui/popover'); + return { + ...originalModule, + StatefulPopover: ({ content, children }: StatefulPopoverProps) => { + const [isShown, setIsShown] = useState(false); + + return ( +
setIsShown(true)}> + {children} + {isShown ? ( +
+ {typeof content === 'function' && + content({ close: () => setIsShown(false) })} +
+ ) : null} +
+ ); + }, + }; +}); + +jest.mock('baseui/timepicker', () => ({ + TimePicker: jest.fn( + ({ value, onChange, disabled }: TimePickerProps) => + onChange && ( + onChange(new Date(e.target.value))} + disabled={disabled} + /> + ) + ), +})); + +const mockDateOverrides: DateFilterRange = { + start: dayjs('2023-05-23T00:00:00.000Z'), + end: dayjs('2023-05-24T00:00:00.000Z'), +}; + +describe(DateFilterV2.name, () => { + it('displays the date filter component with placeholder when no dates are provided', () => { + setup({}); + expect(screen.getByPlaceholderText('Mock placeholder')).toBeInTheDocument(); + }); + + it('renders without errors when dates are already provided', () => { + setup({ + overrides: mockDateOverrides, + }); + + expect( + screen.getByDisplayValue('23 May, 00:00:00 UTC - 24 May, 00:00:00 UTC') + ).toBeInTheDocument(); + }); + + it('opens a popover when clicked', () => { + setup({}); + const datePicker = screen.getByPlaceholderText('Mock placeholder'); + + act(() => { + fireEvent.click(datePicker); + }); + + // Check for elements in the popover + expect(screen.getByText('Quick Range')).toBeInTheDocument(); + expect(screen.getByText('Custom Range')).toBeInTheDocument(); + expect(screen.getByText('Last 5 minutes')).toBeInTheDocument(); + }); + + it('selects a relative time range when clicking a quick range button', () => { + const { mockOnChangeDates } = setup({}); + const datePicker = screen.getByPlaceholderText('Mock placeholder'); + + act(() => { + fireEvent.click(datePicker); + }); + + const lastFiveMinutesButton = screen.getByText('Last 5 minutes'); + + act(() => { + fireEvent.click(lastFiveMinutesButton); + }); + + expect(mockOnChangeDates).toHaveBeenCalledWith({ + start: 'now-5m', + end: 'now', + }); + }); + + it('allows selecting a custom date range via the calendar', () => { + const { mockOnChangeDates } = setup({}); + const datePicker = screen.getByPlaceholderText('Mock placeholder'); + + act(() => { + fireEvent.click(datePicker); + }); + + const saveButton = screen.getByText('Save'); + const timePickers = screen.getAllByTestId('time-picker'); + + act(() => { + fireEvent.click(screen.getByLabelText(/May 13th 2023/)); + }); + + expect(saveButton).toBeDisabled(); + timePickers.forEach((picker) => { + expect(picker).toBeDisabled(); + }); + + act(() => { + fireEvent.click(screen.getByLabelText(/May 14th 2023/)); + }); + + expect(saveButton).not.toBeDisabled(); + timePickers.forEach((picker) => { + expect(picker).not.toBeDisabled(); + }); + + act(() => { + fireEvent.click(saveButton); + }); + + expect(mockOnChangeDates).toHaveBeenCalledWith({ + start: dayjs('2023-05-13'), + end: dayjs('2023-05-14'), + }); + }); + + it('handles single date selection (same start and end date)', () => { + const { mockOnChangeDates } = setup({}); + const datePicker = screen.getByPlaceholderText('Mock placeholder'); + + act(() => { + fireEvent.click(datePicker); + }); + + act(() => { + fireEvent.click(screen.getByLabelText(/May 13th 2023/)); + }); + + act(() => { + fireEvent.click(screen.getByLabelText(/May 13th 2023/)); + }); + + const saveButton = screen.getByText('Save'); + act(() => { + fireEvent.click(saveButton); + }); + + expect(mockOnChangeDates).toHaveBeenCalledWith({ + start: dayjs('2023-05-13'), + end: dayjs('2023-05-13').endOf('day'), + }); + }); + + it('allows time adjustment after date selection', () => { + const { mockOnChangeDates } = setup({}); + const datePicker = screen.getByPlaceholderText('Mock placeholder'); + + act(() => { + fireEvent.click(datePicker); + }); + + act(() => { + fireEvent.click(screen.getByLabelText(/May 13th 2023/)); + }); + + act(() => { + fireEvent.click(screen.getByLabelText(/May 14th 2023/)); + }); + + const timePickers = screen.getAllByTestId('time-picker'); + expect(timePickers).toHaveLength(2); + + act(() => { + fireEvent.change(timePickers[0], { + target: { value: '2023-05-13 11:45' }, + }); + fireEvent.change(timePickers[1], { + target: { value: '2023-05-14 15:45' }, + }); + }); + + const saveButton = screen.getByText('Save'); + act(() => { + fireEvent.click(saveButton); + }); + + expect(mockOnChangeDates).toHaveBeenCalledWith( + expect.objectContaining({ + start: dayjs('2023-05-13 11:45'), + end: dayjs('2023-05-14 15:45'), + }) + ); + }); + + it('displays the correct format when using relative date values', () => { + setup({ + overrides: { + start: 'now-1h', + end: 'now', + }, + }); + + expect(screen.getByDisplayValue('Last 1 hour')).toBeInTheDocument(); + }); + + it('closes popover when clicking the close button', () => { + setup({}); + const datePicker = screen.getByPlaceholderText('Mock placeholder'); + + act(() => { + fireEvent.click(datePicker); + }); + + const quickRangeHeader = screen.getByText('Quick Range'); + expect(quickRangeHeader).toBeInTheDocument(); + + const closeButton = screen.getByTestId('close-button'); + + act(() => { + fireEvent.click(closeButton); + }); + + waitFor(() => { + expect(quickRangeHeader).not.toBeInTheDocument(); + }); + }); +}); + +function setup({ overrides }: { overrides?: Partial }) { + const mockOnChangeDates = jest.fn(); + + const result = render( + + ); + + return { mockOnChangeDates, ...result }; +} diff --git a/src/components/date-filter-v2/date-filter-v2.constants.ts b/src/components/date-filter-v2/date-filter-v2.constants.ts new file mode 100644 index 000000000..54e8637c4 --- /dev/null +++ b/src/components/date-filter-v2/date-filter-v2.constants.ts @@ -0,0 +1,12 @@ +import { type RelativeDurationConfig } from './date-filter-v2.types'; + +export const DATE_FILTER_RELATIVE_VALUES = { + 'now-5m': { label: 'Last 5 minutes', durationSeconds: 5 * 60 }, + 'now-15m': { label: 'Last 15 minutes', durationSeconds: 15 * 60 }, + 'now-1h': { label: 'Last 1 hour', durationSeconds: 1 * 60 * 60 }, + 'now-6h': { label: 'Last 6 hours', durationSeconds: 6 * 60 * 60 }, + 'now-12h': { label: 'Last 12 hours', durationSeconds: 12 * 60 * 60 }, + 'now-1d': { label: 'Last 1 day', durationSeconds: 1 * 24 * 60 * 60 }, + 'now-7d': { label: 'Last 7 days', durationSeconds: 7 * 24 * 60 * 60 }, + 'now-30d': { label: 'Last 30 days', durationSeconds: 30 * 24 * 60 * 60 }, +} as const satisfies Record<`now-${string}`, RelativeDurationConfig>; diff --git a/src/components/date-filter-v2/date-filter-v2.styles.ts b/src/components/date-filter-v2/date-filter-v2.styles.ts new file mode 100644 index 000000000..0f120fbe9 --- /dev/null +++ b/src/components/date-filter-v2/date-filter-v2.styles.ts @@ -0,0 +1,115 @@ +import { styled as createStyled, type Theme } from 'baseui'; +import { type ButtonOverrides } from 'baseui/button'; +import { type DatepickerOverrides } from 'baseui/datepicker'; +import type { FormControlOverrides } from 'baseui/form-control/types'; +import { type PopoverOverrides } from 'baseui/popover'; +import { type StyleObject } from 'styletron-react'; + +export const overrides = { + dateFormControl: { + Label: { + style: ({ $theme }: { $theme: Theme }): StyleObject => ({ + ...$theme.typography.LabelXSmall, + }), + }, + ControlContainer: { + style: (): StyleObject => ({ + margin: '0px', + }), + }, + } satisfies FormControlOverrides, + timeFormControl: { + Label: { + style: ({ $theme }: { $theme: Theme }): StyleObject => ({ + ...$theme.typography.LabelXSmall, + }), + }, + ControlContainer: { + style: (): StyleObject => ({ + margin: '0px', + display: 'flex', + flexDirection: 'column', + }), + }, + } satisfies FormControlOverrides, + popover: { + Inner: { + style: ({ $theme }: { $theme: Theme }): StyleObject => ({ + ...$theme.typography.LabelSmall, + borderRadius: $theme.borders.radius400, + background: $theme.colors.backgroundPrimary, + boxShadow: $theme.lighting.shallowBelow, + }), + }, + } satisfies PopoverOverrides, + menuItemButton: { + BaseButton: { + style: ({ $theme }: { $theme: Theme }): StyleObject => ({ + ...$theme.typography.LabelSmall, + justifyContent: 'flex-start', + whiteSpace: 'nowrap', + }), + }, + } satisfies ButtonOverrides, + calendar: { + Root: { + style: { + paddingLeft: 0, + paddingRight: 0, + paddingTop: 0, + paddingBottom: 0, + }, + }, + } satisfies DatepickerOverrides, +}; + +export const styled = { + PopoverContentContainer: createStyled('div', { + display: 'flex', + flexDirection: 'row', + position: 'relative', + }), + CloseButtonContainer: createStyled('div', ({ $theme }) => ({ + position: 'absolute', + top: $theme.sizing.scale400, + right: $theme.sizing.scale400, + })), + ContentHeader: createStyled('div', ({ $theme }) => ({ + ...$theme.typography.LabelMedium, + paddingLeft: $theme.sizing.scale500, + })), + ContentColumn: createStyled('div', ({ $theme }) => ({ + display: 'flex', + flexDirection: 'column', + gap: $theme.sizing.scale600, + paddingTop: $theme.sizing.scale700, + paddingBottom: $theme.sizing.scale700, + paddingLeft: $theme.sizing.scale600, + paddingRight: $theme.sizing.scale600, + ':not(:last-child)': { + borderRight: `1px solid ${$theme.colors.borderOpaque}`, + }, + })), + MenuContainer: createStyled('div', { + display: 'flex', + flexDirection: 'column', + flexGrow: 1, + justifyContent: 'space-between', + }), + MenuItemsContainer: createStyled('div', ({ $theme }) => ({ + display: 'flex', + flexDirection: 'column', + gap: $theme.sizing.scale100, + })), + TimeInputsContainer: createStyled('div', ({ $theme }) => ({ + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-evenly', + gap: $theme.sizing.scale300, + })), + TimeInputContainer: createStyled('div', { + display: 'flex', + flexDirection: 'column', + flexBasis: '50%', + }), +}; diff --git a/src/components/date-filter-v2/date-filter-v2.tsx b/src/components/date-filter-v2/date-filter-v2.tsx new file mode 100644 index 000000000..a90a4492f --- /dev/null +++ b/src/components/date-filter-v2/date-filter-v2.tsx @@ -0,0 +1,211 @@ +import { useCallback, useEffect, useMemo, useState } from 'react'; + +import { Button } from 'baseui/button'; +import { StatefulCalendar, TimePicker } from 'baseui/datepicker'; +import { FormControl } from 'baseui/form-control'; +import { Input } from 'baseui/input'; +import { StatefulPopover } from 'baseui/popover'; +import { MdClose } from 'react-icons/md'; + +import dayjs from '@/utils/datetime/dayjs'; + +import { DATE_FILTER_RELATIVE_VALUES } from './date-filter-v2.constants'; +import { overrides, styled } from './date-filter-v2.styles'; +import { + type Props, + type DateFilterRange, + type RelativeDateFilterValue, +} from './date-filter-v2.types'; +import isRelativeDateFilterValue from './helpers/is-relative-date-filter-value'; +import stringifyDateFilterValue from './helpers/stringify-date-filter-value'; + +export default function DateFilterV2({ + label, + placeholder, + dates, + onChangeDates, +}: Props) { + const [tempDates, setTempDates] = useState<{ + start: dayjs.Dayjs | undefined; + end: dayjs.Dayjs | undefined; + }>({ + start: undefined, + end: undefined, + }); + const areTempDatesInvalid = useMemo( + () => Boolean(tempDates.end?.isBefore(tempDates.start)), + [tempDates] + ); + + useEffect(() => { + if (dayjs.isDayjs(dates.start) && dayjs.isDayjs(dates.end)) + setTempDates({ + start: dates.start, + end: dates.end, + }); + }, [dates]); + + const [canSaveDate, setCanSaveDate] = useState(false); + + const saveDates = useCallback( + (dates: DateFilterRange) => { + onChangeDates(dates); + setCanSaveDate(false); + }, + [onChangeDates] + ); + + const displayValue = useMemo(() => { + if (!dates.end || !dates.start) return 'Unknown'; + if (dates.end === 'now' && isRelativeDateFilterValue(dates.start)) + return DATE_FILTER_RELATIVE_VALUES[dates.start].label; + + return `${stringifyDateFilterValue(dates.start, 'pretty')} - ${stringifyDateFilterValue(dates.end, 'pretty')}`; + }, [dates]); + + return ( + + ( + + + Quick Range + + + {Object.entries(DATE_FILTER_RELATIVE_VALUES).map( + ([key, { label }]) => ( + + ) + )} + + + + + + Custom Range + { + if (!newDates || !Array.isArray(newDates)) { + return; + } + + if (newDates.length === 2 && newDates[0] && newDates[1]) { + setTempDates({ + start: dayjs(newDates[0]), + end: + newDates[0].getTime() === newDates[1].getTime() + ? dayjs(newDates[1]).endOf('day') + : dayjs(newDates[1]), + }); + setCanSaveDate(true); + } + }} + range + /> + + + + + setTempDates((oldDates) => ({ + ...oldDates, + start: dayjs(newStart), + })) + } + /> + + + + + + setTempDates((oldDates) => ({ + ...oldDates, + end: dayjs(newEnd), + })) + } + /> + + + + + + + + + )} + > +
+ +
+
+
+ ); +} diff --git a/src/components/date-filter-v2/date-filter-v2.types.ts b/src/components/date-filter-v2/date-filter-v2.types.ts new file mode 100644 index 000000000..0e57b3393 --- /dev/null +++ b/src/components/date-filter-v2/date-filter-v2.types.ts @@ -0,0 +1,24 @@ +import { type Dayjs } from 'dayjs'; + +import { type DATE_FILTER_RELATIVE_VALUES } from './date-filter-v2.constants'; + +export type RelativeDurationConfig = { + label: string; + durationSeconds: number; +}; + +export type RelativeDateFilterValue = keyof typeof DATE_FILTER_RELATIVE_VALUES; + +export type DateFilterValue = Dayjs | 'now' | RelativeDateFilterValue; + +export type DateFilterRange = { + start: DateFilterValue | undefined; + end: DateFilterValue | undefined; +}; + +export type Props = { + label: string; + placeholder: string; + dates: DateFilterRange; + onChangeDates: (v: DateFilterRange) => void; +}; diff --git a/src/components/date-filter-v2/helpers/__tests__/get-dayjs-from-date-filter-value.test.ts b/src/components/date-filter-v2/helpers/__tests__/get-dayjs-from-date-filter-value.test.ts new file mode 100644 index 000000000..d2be06217 --- /dev/null +++ b/src/components/date-filter-v2/helpers/__tests__/get-dayjs-from-date-filter-value.test.ts @@ -0,0 +1,50 @@ +import dayjs from '@/utils/datetime/dayjs'; + +import { DATE_FILTER_RELATIVE_VALUES } from '../../date-filter-v2.constants'; +import { type RelativeDateFilterValue } from '../../date-filter-v2.types'; +import getDayjsFromDateFilterValue from '../get-dayjs-from-date-filter-value'; + +describe('getDayjsFromDateFilterValue', () => { + const now = dayjs('2023-05-25T12:00:00.000Z'); + + it('returns the same dayjs object when input is a dayjs object', () => { + const date = dayjs('2023-05-23T10:30:00.000Z'); + const result = getDayjsFromDateFilterValue(date, now); + expect(result).toBe(date); + }); + + it('returns the "now" value when input is "now"', () => { + const result = getDayjsFromDateFilterValue('now', now); + expect(result).toBe(now); + }); + + it('calculates correct relative times for relative date values', () => { + // Test each relative date value + Object.entries(DATE_FILTER_RELATIVE_VALUES).forEach( + ([key, { durationSeconds }]) => { + const result = getDayjsFromDateFilterValue( + key as RelativeDateFilterValue, + now + ); + + const expected = now.subtract(durationSeconds, 'seconds'); + + expect(result.unix()).toBe(expected.unix()); + } + ); + }); + + it('correctly calculates specific relative date examples', () => { + // 5 minutes ago + const fiveMinAgo = getDayjsFromDateFilterValue('now-5m', now); + expect(fiveMinAgo.unix()).toBe(now.subtract(5, 'minutes').unix()); + + // 1 hour ago + const oneHourAgo = getDayjsFromDateFilterValue('now-1h', now); + expect(oneHourAgo.unix()).toBe(now.subtract(1, 'hour').unix()); + + // 1 day ago + const oneDayAgo = getDayjsFromDateFilterValue('now-1d', now); + expect(oneDayAgo.unix()).toBe(now.subtract(1, 'day').unix()); + }); +}); diff --git a/src/components/date-filter-v2/helpers/__tests__/is-relative-date-filter-value.test.ts b/src/components/date-filter-v2/helpers/__tests__/is-relative-date-filter-value.test.ts new file mode 100644 index 000000000..95773ee05 --- /dev/null +++ b/src/components/date-filter-v2/helpers/__tests__/is-relative-date-filter-value.test.ts @@ -0,0 +1,26 @@ +import { DATE_FILTER_RELATIVE_VALUES } from '../../date-filter-v2.constants'; +import isRelativeDateFilterValue from '../is-relative-date-filter-value'; + +describe('isRelativeDateFilterValue', () => { + it('returns true for valid relative date filter values', () => { + // Test all keys from the constants + Object.keys(DATE_FILTER_RELATIVE_VALUES).forEach((key) => { + expect(isRelativeDateFilterValue(key)).toBe(true); + }); + }); + + it('returns false for invalid relative date filter values', () => { + expect(isRelativeDateFilterValue('invalid-value')).toBe(false); + expect(isRelativeDateFilterValue('now-invalid')).toBe(false); + expect(isRelativeDateFilterValue('now')).toBe(false); + expect(isRelativeDateFilterValue('')).toBe(false); + }); + + it('returns false for non-string values', () => { + expect(isRelativeDateFilterValue(123)).toBe(false); + expect(isRelativeDateFilterValue(null)).toBe(false); + expect(isRelativeDateFilterValue(undefined)).toBe(false); + expect(isRelativeDateFilterValue({})).toBe(false); + expect(isRelativeDateFilterValue([])).toBe(false); + }); +}); diff --git a/src/components/date-filter-v2/helpers/__tests__/parse-date-filter-value.test.ts b/src/components/date-filter-v2/helpers/__tests__/parse-date-filter-value.test.ts new file mode 100644 index 000000000..bfe8d776e --- /dev/null +++ b/src/components/date-filter-v2/helpers/__tests__/parse-date-filter-value.test.ts @@ -0,0 +1,36 @@ +import dayjs from '@/utils/datetime/dayjs'; + +import { type DateFilterValue } from '../../date-filter-v2.types'; +import parseDateFilterValue from '../parse-date-filter-value'; + +// Mock is-relative-date-filter-value +jest.mock('../is-relative-date-filter-value', () => ({ + __esModule: true, + default: (v: string) => v.startsWith('now-'), +})); + +describe('parseDateFilterValue', () => { + it('returns the value as is for relative date values', () => { + const fallback: DateFilterValue = 'now'; + expect(parseDateFilterValue('now-5m', fallback)).toBe('now-5m'); + expect(parseDateFilterValue('now-1h', fallback)).toBe('now-1h'); + }); + + it('parses string dates into dayjs objects when format is valid', () => { + const fallback: DateFilterValue = 'now'; + const result = parseDateFilterValue('2023-05-23T10:30:00.000Z', fallback); + expect(dayjs.isDayjs(result)).toBe(true); + expect((result as dayjs.Dayjs).format('YYYY-MM-DD')).toBe('2023-05-23'); + }); + + it('returns the fallback when date format is invalid', () => { + const fallback = dayjs('2023-01-01'); + const result = parseDateFilterValue('invalid-date', fallback); + expect(result).toBe(fallback); + }); + + it('returns the fallback for empty strings', () => { + const fallback: DateFilterValue = 'now'; + expect(parseDateFilterValue('', fallback)).toBe(fallback); + }); +}); diff --git a/src/components/date-filter-v2/helpers/__tests__/stringify-date-filter-value.test.ts b/src/components/date-filter-v2/helpers/__tests__/stringify-date-filter-value.test.ts new file mode 100644 index 000000000..f80db36ea --- /dev/null +++ b/src/components/date-filter-v2/helpers/__tests__/stringify-date-filter-value.test.ts @@ -0,0 +1,32 @@ +import dayjs from '@/utils/datetime/dayjs'; + +import stringifyDateFilterValue from '../stringify-date-filter-value'; + +// Mock the current date to be fixed +jest.useFakeTimers().setSystemTime(new Date('2023-05-25')); + +describe('stringifyDateFilterValue', () => { + it('returns the value as is for string values', () => { + expect(stringifyDateFilterValue('now')).toBe('now'); + expect(stringifyDateFilterValue('now-5m')).toBe('now-5m'); + }); + + it('returns ISO string for dayjs objects when prettyPrint is not specified', () => { + const date = dayjs('2023-05-23T10:30:00.000Z'); + expect(stringifyDateFilterValue(date)).toBe('2023-05-23T10:30:00.000Z'); + }); + + it('formats dates from current year with just month and day when prettyPrint=pretty', () => { + const date = dayjs('2023-05-23T10:30:00.000Z'); + expect(stringifyDateFilterValue(date, 'pretty')).toBe( + '23 May, 10:30:00 UTC' + ); + }); + + it('includes year for dates not in current year when prettyPrint=pretty', () => { + const date = dayjs('2022-05-23T10:30:00.000Z'); + expect(stringifyDateFilterValue(date, 'pretty')).toBe( + '23 May 2022, 10:30:00 UTC' + ); + }); +}); diff --git a/src/components/date-filter-v2/helpers/get-dayjs-from-date-filter-value.ts b/src/components/date-filter-v2/helpers/get-dayjs-from-date-filter-value.ts new file mode 100644 index 000000000..d03793892 --- /dev/null +++ b/src/components/date-filter-v2/helpers/get-dayjs-from-date-filter-value.ts @@ -0,0 +1,22 @@ +import dayjs from '@/utils/datetime/dayjs'; + +import { DATE_FILTER_RELATIVE_VALUES } from '../date-filter-v2.constants'; +import { type DateFilterValue } from '../date-filter-v2.types'; + +export default function getDayjsFromDateFilterValue( + v: DateFilterValue, + now: dayjs.Dayjs +) { + if (dayjs.isDayjs(v)) { + return v; + } + + if (v === 'now') { + return now; + } + + return now.subtract( + DATE_FILTER_RELATIVE_VALUES[v].durationSeconds, + 'seconds' + ); +} diff --git a/src/components/date-filter-v2/helpers/is-relative-date-filter-value.ts b/src/components/date-filter-v2/helpers/is-relative-date-filter-value.ts new file mode 100644 index 000000000..e70cc1626 --- /dev/null +++ b/src/components/date-filter-v2/helpers/is-relative-date-filter-value.ts @@ -0,0 +1,8 @@ +import { DATE_FILTER_RELATIVE_VALUES } from '../date-filter-v2.constants'; +import { type RelativeDateFilterValue } from '../date-filter-v2.types'; + +export default function isRelativeDateFilterValue( + v: any +): v is RelativeDateFilterValue { + return Object.hasOwn(DATE_FILTER_RELATIVE_VALUES, v); +} diff --git a/src/components/date-filter-v2/helpers/parse-date-filter-value.ts b/src/components/date-filter-v2/helpers/parse-date-filter-value.ts new file mode 100644 index 000000000..9b3f6f826 --- /dev/null +++ b/src/components/date-filter-v2/helpers/parse-date-filter-value.ts @@ -0,0 +1,14 @@ +import dayjs from '@/utils/datetime/dayjs'; + +import { type DateFilterValue } from '../date-filter-v2.types'; + +import isRelativeDateFilterValue from './is-relative-date-filter-value'; + +export default function parseDateFilterValue( + v: string, + fallback: DateFilterValue +): DateFilterValue { + if (isRelativeDateFilterValue(v)) return v; + const day = dayjs(v); + return day.isValid() ? day : fallback; +} diff --git a/src/components/date-filter-v2/helpers/stringify-date-filter-value.ts b/src/components/date-filter-v2/helpers/stringify-date-filter-value.ts new file mode 100644 index 000000000..2e31302c8 --- /dev/null +++ b/src/components/date-filter-v2/helpers/stringify-date-filter-value.ts @@ -0,0 +1,22 @@ +import dayjs from '@/utils/datetime/dayjs'; + +import { type DateFilterValue } from '../date-filter-v2.types'; + +export default function stringifyDateFilterValue( + v: DateFilterValue, + prettyPrint?: 'pretty' +): string { + const now = dayjs(); + + if (dayjs.isDayjs(v)) { + return prettyPrint === 'pretty' + ? v.format( + v.isSame(now, 'year') + ? 'DD MMM, HH:mm:ss z' + : 'DD MMM YYYY, HH:mm:ss z' + ) + : v.toISOString(); + } + + return v; +} diff --git a/src/hooks/use-merged-infinite-queries/__tests__/use-merged-infinite-queries.test.ts b/src/hooks/use-merged-infinite-queries/__tests__/use-merged-infinite-queries.test.ts index 1faa35bed..711db8db1 100644 --- a/src/hooks/use-merged-infinite-queries/__tests__/use-merged-infinite-queries.test.ts +++ b/src/hooks/use-merged-infinite-queries/__tests__/use-merged-infinite-queries.test.ts @@ -12,7 +12,7 @@ type MockAPIResponse = { }; const MOCK_QUERY_CONFIG: Array< - SingleInfiniteQueryOptions + SingleInfiniteQueryOptions > = [ { queryKey: ['even-numbers'], @@ -35,7 +35,7 @@ const MOCK_QUERY_CONFIG: Array< ]; const MOCK_QUERY_CONFIG_WITH_ERROR: Array< - SingleInfiniteQueryOptions + SingleInfiniteQueryOptions > = [ { queryKey: ['even-numbers'], diff --git a/src/hooks/use-merged-infinite-queries/use-merged-infinite-queries.ts b/src/hooks/use-merged-infinite-queries/use-merged-infinite-queries.ts index 5ced67c71..9b22d8fe1 100644 --- a/src/hooks/use-merged-infinite-queries/use-merged-infinite-queries.ts +++ b/src/hooks/use-merged-infinite-queries/use-merged-infinite-queries.ts @@ -1,6 +1,10 @@ import { useMemo, useState, useEffect, useCallback } from 'react'; -import { InfiniteQueryObserver, useQueryClient } from '@tanstack/react-query'; +import { + InfiniteQueryObserver, + type QueryKey, + useQueryClient, +} from '@tanstack/react-query'; import mergeSortedArrays from '@/utils/merge-sorted-arrays'; @@ -35,12 +39,17 @@ import { * - `mergedQueryResults`: The merged and sorted results from all queries. * - `queryResults`: An array containing individual results from each query. */ -export default function useMergedInfiniteQueries({ +export default function useMergedInfiniteQueries< + TData, + TResponse, + TPageParam, + TQueryKey extends QueryKey, +>({ queries, pageSize, flattenResponse, compare, -}: Props): [ +}: Props): [ MergedQueriesResults, Array>, ] { diff --git a/src/hooks/use-merged-infinite-queries/use-merged-infinite-queries.types.ts b/src/hooks/use-merged-infinite-queries/use-merged-infinite-queries.types.ts index bd711b6c7..5c2f36210 100644 --- a/src/hooks/use-merged-infinite-queries/use-merged-infinite-queries.types.ts +++ b/src/hooks/use-merged-infinite-queries/use-merged-infinite-queries.types.ts @@ -21,22 +21,25 @@ export type MergedQueriesResults = { refetch: () => void; }; -export type SingleInfiniteQueryOptions = - UseInfiniteQueryOptions< - TResponse, - Error, - InfiniteData, - TResponse, - QueryKey, - TPageParam - >; +export type SingleInfiniteQueryOptions< + TResponse, + TPageParam, + TQueryKey extends QueryKey, +> = UseInfiniteQueryOptions< + TResponse, + Error, + InfiniteData, + TResponse, + TQueryKey, + TPageParam +>; export type SingleInfiniteQueryResult = ReturnType< typeof useInfiniteQuery >; -export type Props = { - queries: Array>; +export type Props = { + queries: Array>; pageSize: number; flattenResponse: (queryResult: TResponse) => Array; compare: (a: TData, b: TData) => number; diff --git a/src/views/domain-page/__fixtures__/domain-page-query-params.ts b/src/views/domain-page/__fixtures__/domain-page-query-params.ts index bc4fe2e56..0c2bafdc5 100644 --- a/src/views/domain-page/__fixtures__/domain-page-query-params.ts +++ b/src/views/domain-page/__fixtures__/domain-page-query-params.ts @@ -13,8 +13,8 @@ export const mockDomainPageQueryParamsValues = { workflowId: '', workflowType: '', statusBasic: undefined, - timeRangeStartBasic: new Date('2024-11-17T03:24:00'), - timeRangeEndBasic: new Date('2024-12-17T03:24:00'), + timeRangeStartBasic: 'now', + timeRangeEndBasic: 'now-7d', inputTypeArchival: 'search', searchArchival: '', statusesArchival: undefined, diff --git a/src/views/domain-page/config/domain-page-query-params.config.ts b/src/views/domain-page/config/domain-page-query-params.config.ts index f19419a0c..336986879 100644 --- a/src/views/domain-page/config/domain-page-query-params.config.ts +++ b/src/views/domain-page/config/domain-page-query-params.config.ts @@ -1,3 +1,5 @@ +import { type DateFilterValue } from '@/components/date-filter-v2/date-filter-v2.types'; +import parseDateFilterValue from '@/components/date-filter-v2/helpers/parse-date-filter-value'; import { type PageQueryParamMultiValue, type PageQueryParam, @@ -7,7 +9,6 @@ import parseDateQueryParam from '@/utils/datetime/parse-date-query-param'; import { type SortOrder } from '@/utils/sort-by'; import DOMAIN_WORKFLOWS_ARCHIVAL_START_DAYS_CONFIG from '@/views/domain-workflows-archival/config/domain-workflows-archival-start-days.config'; import { type WorkflowStatusClosed } from '@/views/domain-workflows-archival/domain-workflows-archival-header/domain-workflows-archival-header.types'; -import DOMAIN_WORKFLOWS_BASIC_START_DAYS_CONFIG from '@/views/domain-workflows-basic/config/domain-workflows-basic-start-days.config'; import { type WorkflowStatusBasicVisibility } from '@/views/domain-workflows-basic/domain-workflows-basic-filters/domain-workflows-basic-filters.types'; import isWorkflowStatusBasicVisibility from '@/views/domain-workflows-basic/domain-workflows-basic-filters/helpers/is-workflow-status-basic-visibility'; import isWorkflowStatus from '@/views/shared/workflow-status-tag/helpers/is-workflow-status'; @@ -31,8 +32,8 @@ const domainPageQueryParamsConfig: [ PageQueryParam<'workflowId', string>, PageQueryParam<'workflowType', string>, PageQueryParam<'statusBasic', WorkflowStatusBasicVisibility | undefined>, - PageQueryParam<'timeRangeStartBasic', Date>, - PageQueryParam<'timeRangeEndBasic', Date>, + PageQueryParam<'timeRangeStartBasic', DateFilterValue>, + PageQueryParam<'timeRangeEndBasic', DateFilterValue>, // Archival inputs PageQueryParam<'inputTypeArchival', WorkflowsHeaderInputType>, PageQueryParam<'searchArchival', string>, @@ -106,18 +107,14 @@ const domainPageQueryParamsConfig: [ { key: 'timeRangeStartBasic', queryParamKey: 'start', - defaultValue: now - .subtract(DOMAIN_WORKFLOWS_BASIC_START_DAYS_CONFIG, 'days') - .toDate(), - parseValue: (v) => - parseDateQueryParam(v) ?? - now.subtract(DOMAIN_WORKFLOWS_BASIC_START_DAYS_CONFIG, 'days').toDate(), + defaultValue: 'now-7d', + parseValue: (v) => parseDateFilterValue(v, 'now-7d'), }, { key: 'timeRangeEndBasic', queryParamKey: 'end', - defaultValue: now.toDate(), - parseValue: (v) => parseDateQueryParam(v) ?? now.toDate(), + defaultValue: 'now', + parseValue: (v) => parseDateFilterValue(v, 'now'), }, { key: 'inputTypeArchival', diff --git a/src/views/domain-workflows-basic/config/domain-workflows-basic-filters.config.ts b/src/views/domain-workflows-basic/config/domain-workflows-basic-filters.config.ts index 88b171902..2a9424231 100644 --- a/src/views/domain-workflows-basic/config/domain-workflows-basic-filters.config.ts +++ b/src/views/domain-workflows-basic/config/domain-workflows-basic-filters.config.ts @@ -1,6 +1,8 @@ import { createElement } from 'react'; -import DateFilter from '@/components/date-filter/date-filter'; +import DateFilterV2 from '@/components/date-filter-v2/date-filter-v2'; +import { type DateFilterValue } from '@/components/date-filter-v2/date-filter-v2.types'; +import stringifyDateFilterValue from '@/components/date-filter-v2/helpers/stringify-date-filter-value'; import ListFilter from '@/components/list-filter/list-filter'; import { type PageFilterConfig } from '@/components/page-filters/page-filters.types'; import type domainPageQueryParamsConfig from '@/views/domain-page/config/domain-page-query-params.config'; @@ -16,8 +18,8 @@ const domainWorkflowsBasicFiltersConfig: [ PageFilterConfig< typeof domainPageQueryParamsConfig, { - timeRangeStartBasic: Date | undefined; - timeRangeEndBasic: Date | undefined; + timeRangeStartBasic: DateFilterValue | undefined; + timeRangeEndBasic: DateFilterValue | undefined; } >, ] = [ @@ -41,12 +43,16 @@ const domainWorkflowsBasicFiltersConfig: [ timeRangeEndBasic: v.timeRangeEndBasic, }), formatValue: (v) => ({ - timeRangeStartBasic: v.timeRangeStartBasic?.toISOString(), - timeRangeEndBasic: v.timeRangeEndBasic?.toISOString(), + timeRangeStartBasic: v.timeRangeStartBasic + ? stringifyDateFilterValue(v.timeRangeStartBasic) + : undefined, + timeRangeEndBasic: v.timeRangeEndBasic + ? stringifyDateFilterValue(v.timeRangeEndBasic) + : undefined, }), component: ({ value, setValue }) => - createElement(DateFilter, { - label: 'Dates', + createElement(DateFilterV2, { + label: 'Time range', placeholder: 'Select time range', dates: { start: value.timeRangeStartBasic, @@ -57,7 +63,6 @@ const domainWorkflowsBasicFiltersConfig: [ timeRangeStartBasic: start, timeRangeEndBasic: end, }), - clearable: false, }), }, ] as const; diff --git a/src/views/domain-workflows-basic/config/domain-workflows-basic-start-days.config.ts b/src/views/domain-workflows-basic/config/domain-workflows-basic-start-days.config.ts deleted file mode 100644 index 0719cdfe9..000000000 --- a/src/views/domain-workflows-basic/config/domain-workflows-basic-start-days.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -const DOMAIN_WORKFLOWS_BASIC_START_DAYS_CONFIG = 30; - -export default DOMAIN_WORKFLOWS_BASIC_START_DAYS_CONFIG; diff --git a/src/views/domain-workflows-basic/domain-workflows-basic-table/__tests__/domain-workflows-basic-table.test.tsx b/src/views/domain-workflows-basic/domain-workflows-basic-table/__tests__/domain-workflows-basic-table.test.tsx index d38799119..555701be3 100644 --- a/src/views/domain-workflows-basic/domain-workflows-basic-table/__tests__/domain-workflows-basic-table.test.tsx +++ b/src/views/domain-workflows-basic/domain-workflows-basic-table/__tests__/domain-workflows-basic-table.test.tsx @@ -14,6 +14,11 @@ jest.mock('@/components/error-panel/error-panel', () => jest.fn(({ message }: { message: string }) =>
{message}
) ); +jest.mock('../../config/domain-workflows-basic-page-size.config', () => ({ + __esModule: true, + default: 5, +})); + jest.mock('../helpers/get-workflows-basic-error-panel-props', () => jest.fn().mockImplementation(({ error }: { error: Error }) => { return { @@ -52,13 +57,18 @@ describe(DomainWorkflowsBasicTable.name, () => { it('renders workflows without error', async () => { const { user } = setup({}); + // Load 1 page of open workflows first expect(await screen.findByText('Mock end message: OK')).toBeInTheDocument(); - expect(screen.getByText(`mock-workflow-id-0-0-open`)).toBeInTheDocument(); expect(screen.getByText(`mock-workflow-id-0-1-open`)).toBeInTheDocument(); expect(screen.getByText(`mock-workflow-id-0-2-open`)).toBeInTheDocument(); expect(screen.getByText(`mock-workflow-id-0-3-open`)).toBeInTheDocument(); expect(screen.getByText(`mock-workflow-id-0-4-open`)).toBeInTheDocument(); + + await user.click(screen.getByTestId('mock-loader')); + + // Load page 1 of closed workflows + expect(await screen.findByText('Mock end message: OK')).toBeInTheDocument(); expect(screen.getByText(`mock-workflow-id-0-0-closed`)).toBeInTheDocument(); expect(screen.getByText(`mock-workflow-id-0-1-closed`)).toBeInTheDocument(); expect(screen.getByText(`mock-workflow-id-0-2-closed`)).toBeInTheDocument(); @@ -67,17 +77,12 @@ describe(DomainWorkflowsBasicTable.name, () => { await user.click(screen.getByTestId('mock-loader')); - expect(await screen.findByText('Mock end message: OK')).toBeInTheDocument(); - expect(screen.getByText(`mock-workflow-id-0-5-open`)).toBeInTheDocument(); - expect(screen.getByText(`mock-workflow-id-0-6-open`)).toBeInTheDocument(); - expect(screen.getByText(`mock-workflow-id-0-7-open`)).toBeInTheDocument(); - expect(screen.getByText(`mock-workflow-id-0-8-open`)).toBeInTheDocument(); - expect(screen.getByText(`mock-workflow-id-0-9-open`)).toBeInTheDocument(); - expect(screen.getByText(`mock-workflow-id-0-5-closed`)).toBeInTheDocument(); - expect(screen.getByText(`mock-workflow-id-0-6-closed`)).toBeInTheDocument(); - expect(screen.getByText(`mock-workflow-id-0-7-closed`)).toBeInTheDocument(); - expect(screen.getByText(`mock-workflow-id-0-8-closed`)).toBeInTheDocument(); - expect(screen.getByText(`mock-workflow-id-0-9-closed`)).toBeInTheDocument(); + // Then load page 2 of closed workflows + expect(screen.getByText(`mock-workflow-id-1-0-closed`)).toBeInTheDocument(); + expect(screen.getByText(`mock-workflow-id-1-1-closed`)).toBeInTheDocument(); + expect(screen.getByText(`mock-workflow-id-1-2-closed`)).toBeInTheDocument(); + expect(screen.getByText(`mock-workflow-id-1-3-closed`)).toBeInTheDocument(); + expect(screen.getByText(`mock-workflow-id-1-4-closed`)).toBeInTheDocument(); }); it('renders error panel if the initial call fails', async () => { @@ -94,42 +99,6 @@ describe(DomainWorkflowsBasicTable.name, () => { expect(await screen.findByText('No workflows found')).toBeInTheDocument(); }); - it('renders workflows and allows the user to try again if there is an error', async () => { - const { user } = setup({ errorCase: 'subsequent-fetch-error' }); - - expect(await screen.findByText('Mock end message: OK')).toBeInTheDocument(); - expect(screen.getByText(`mock-workflow-id-0-0-open`)).toBeInTheDocument(); - expect(screen.getByText(`mock-workflow-id-0-1-open`)).toBeInTheDocument(); - expect(screen.getByText(`mock-workflow-id-0-2-open`)).toBeInTheDocument(); - expect(screen.getByText(`mock-workflow-id-0-3-open`)).toBeInTheDocument(); - expect(screen.getByText(`mock-workflow-id-0-4-open`)).toBeInTheDocument(); - expect(screen.getByText(`mock-workflow-id-0-0-closed`)).toBeInTheDocument(); - expect(screen.getByText(`mock-workflow-id-0-1-closed`)).toBeInTheDocument(); - expect(screen.getByText(`mock-workflow-id-0-2-closed`)).toBeInTheDocument(); - expect(screen.getByText(`mock-workflow-id-0-3-closed`)).toBeInTheDocument(); - expect(screen.getByText(`mock-workflow-id-0-4-closed`)).toBeInTheDocument(); - - await user.click(screen.getByTestId('mock-loader')); - - expect( - await screen.findByText('Mock end message: Error') - ).toBeInTheDocument(); - - await user.click(screen.getByTestId('mock-loader')); - - expect(await screen.findByText('Mock end message: OK')).toBeInTheDocument(); - expect(screen.getByText(`mock-workflow-id-0-5-open`)).toBeInTheDocument(); - expect(screen.getByText(`mock-workflow-id-0-6-open`)).toBeInTheDocument(); - expect(screen.getByText(`mock-workflow-id-0-7-open`)).toBeInTheDocument(); - expect(screen.getByText(`mock-workflow-id-0-8-open`)).toBeInTheDocument(); - expect(screen.getByText(`mock-workflow-id-0-9-open`)).toBeInTheDocument(); - expect(screen.getByText(`mock-workflow-id-0-5-closed`)).toBeInTheDocument(); - expect(screen.getByText(`mock-workflow-id-0-6-closed`)).toBeInTheDocument(); - expect(screen.getByText(`mock-workflow-id-0-7-closed`)).toBeInTheDocument(); - expect(screen.getByText(`mock-workflow-id-0-8-closed`)).toBeInTheDocument(); - expect(screen.getByText(`mock-workflow-id-0-9-closed`)).toBeInTheDocument(); - }); - it('calls only listOpen if Running status is selected', async () => { jest.spyOn(usePageQueryParamsModule, 'default').mockReturnValue([ { @@ -147,11 +116,6 @@ describe(DomainWorkflowsBasicTable.name, () => { expect(screen.getByText(`mock-workflow-id-0-2-open`)).toBeInTheDocument(); expect(screen.getByText(`mock-workflow-id-0-3-open`)).toBeInTheDocument(); expect(screen.getByText(`mock-workflow-id-0-4-open`)).toBeInTheDocument(); - expect(screen.getByText(`mock-workflow-id-0-5-open`)).toBeInTheDocument(); - expect(screen.getByText(`mock-workflow-id-0-6-open`)).toBeInTheDocument(); - expect(screen.getByText(`mock-workflow-id-0-7-open`)).toBeInTheDocument(); - expect(screen.getByText(`mock-workflow-id-0-8-open`)).toBeInTheDocument(); - expect(screen.getByText(`mock-workflow-id-0-9-open`)).toBeInTheDocument(); }); it('calls only listClosed if a close status is selected', async () => { @@ -172,11 +136,39 @@ describe(DomainWorkflowsBasicTable.name, () => { expect(screen.getByText(`mock-workflow-id-0-2-closed`)).toBeInTheDocument(); expect(screen.getByText(`mock-workflow-id-0-3-closed`)).toBeInTheDocument(); expect(screen.getByText(`mock-workflow-id-0-4-closed`)).toBeInTheDocument(); - expect(screen.getByText(`mock-workflow-id-0-5-closed`)).toBeInTheDocument(); - expect(screen.getByText(`mock-workflow-id-0-6-closed`)).toBeInTheDocument(); - expect(screen.getByText(`mock-workflow-id-0-7-closed`)).toBeInTheDocument(); - expect(screen.getByText(`mock-workflow-id-0-8-closed`)).toBeInTheDocument(); - expect(screen.getByText(`mock-workflow-id-0-9-closed`)).toBeInTheDocument(); + }); + + it('renders workflows and allows the user to try again if there is an error', async () => { + jest.spyOn(usePageQueryParamsModule, 'default').mockReturnValue([ + { + ...mockDomainPageQueryParamsValues, + statusBasic: 'WORKFLOW_EXECUTION_CLOSE_STATUS_COMPLETED', + }, + mockSetQueryParams, + ]); + const { user } = setup({ errorCase: 'subsequent-fetch-error' }); + + expect(await screen.findByText('Mock end message: OK')).toBeInTheDocument(); + expect(screen.getByText(`mock-workflow-id-0-0-closed`)).toBeInTheDocument(); + expect(screen.getByText(`mock-workflow-id-0-1-closed`)).toBeInTheDocument(); + expect(screen.getByText(`mock-workflow-id-0-2-closed`)).toBeInTheDocument(); + expect(screen.getByText(`mock-workflow-id-0-3-closed`)).toBeInTheDocument(); + expect(screen.getByText(`mock-workflow-id-0-4-closed`)).toBeInTheDocument(); + + await user.click(screen.getByTestId('mock-loader')); + + expect( + await screen.findByText('Mock end message: Error') + ).toBeInTheDocument(); + + await user.click(screen.getByTestId('mock-loader')); + + expect(await screen.findByText('Mock end message: OK')).toBeInTheDocument(); + expect(screen.getByText(`mock-workflow-id-1-0-closed`)).toBeInTheDocument(); + expect(screen.getByText(`mock-workflow-id-1-1-closed`)).toBeInTheDocument(); + expect(screen.getByText(`mock-workflow-id-1-2-closed`)).toBeInTheDocument(); + expect(screen.getByText(`mock-workflow-id-1-3-closed`)).toBeInTheDocument(); + expect(screen.getByText(`mock-workflow-id-1-4-closed`)).toBeInTheDocument(); }); }); @@ -185,7 +177,7 @@ function setup({ }: { errorCase?: 'initial-fetch-error' | 'subsequent-fetch-error' | 'no-workflows'; }) { - const openPages = generateWorkflowPages(2, true); + const openPages = generateWorkflowPages(1, true); const closedPages = generateWorkflowPages(2); let currentEventIndexOpen = 0; @@ -261,7 +253,7 @@ function generateWorkflowPages( const pages = Array.from( { length: count }, (_, pageIndex): ListWorkflowsResponse => ({ - workflows: Array.from({ length: 10 }, (_, index) => ({ + workflows: Array.from({ length: 5 }, (_, index) => ({ workflowID: `mock-workflow-id-${pageIndex}-${index}-${isOpen ? 'open' : 'closed'}`, runID: `mock-run-id-${pageIndex}-${index}`, workflowName: `mock-workflow-name-${pageIndex}-${index}`, diff --git a/src/views/domain-workflows-basic/hooks/helpers/get-list-workflows-basic-query-options.ts b/src/views/domain-workflows-basic/hooks/helpers/get-list-workflows-basic-query-options.ts index baaa080f7..eac99947d 100644 --- a/src/views/domain-workflows-basic/hooks/helpers/get-list-workflows-basic-query-options.ts +++ b/src/views/domain-workflows-basic/hooks/helpers/get-list-workflows-basic-query-options.ts @@ -15,13 +15,26 @@ export default function getListWorkflowsBasicQueryOptions({ domain: string; cluster: string; requestQueryParams: ListWorkflowsBasicRequestQueryParams; -}): SingleInfiniteQueryOptions { +}): SingleInfiniteQueryOptions< + ListWorkflowsBasicResponse, + string | undefined, + [ + string, + { + domain: string; + cluster: string; + } & ListWorkflowsBasicRequestQueryParams, + ] +> { return { queryKey: [ 'listWorkflowsBasic', { domain, cluster, ...requestQueryParams }, ], - queryFn: async ({ pageParam }) => + queryFn: async ({ + pageParam, + queryKey: [_, { domain, cluster, ...requestQueryParams }], + }) => request( queryString.stringifyUrl({ url: `/api/domains/${domain}/${cluster}/workflows-basic`, diff --git a/src/views/domain-workflows-basic/hooks/use-list-workflows-basic.ts b/src/views/domain-workflows-basic/hooks/use-list-workflows-basic.ts index d3274b570..9504122ab 100644 --- a/src/views/domain-workflows-basic/hooks/use-list-workflows-basic.ts +++ b/src/views/domain-workflows-basic/hooks/use-list-workflows-basic.ts @@ -2,9 +2,11 @@ import { useMemo } from 'react'; +import getDayjsFromDateFilterValue from '@/components/date-filter-v2/helpers/get-dayjs-from-date-filter-value'; import useMergedInfiniteQueries from '@/hooks/use-merged-infinite-queries/use-merged-infinite-queries'; import usePageQueryParams from '@/hooks/use-page-query-params/use-page-query-params'; import { type ListWorkflowsBasicRequestQueryParams } from '@/route-handlers/list-workflows-basic/list-workflows-basic.types'; +import dayjs from '@/utils/datetime/dayjs'; import domainPageQueryParamsConfig from '@/views/domain-page/config/domain-page-query-params.config'; import DOMAIN_WORKFLOWS_BASIC_PAGE_SIZE from '../config/domain-workflows-basic-page-size.config'; @@ -28,12 +30,26 @@ export default function useListWorkflowsBasic({ queryParams.statusBasic === undefined || queryParams.statusBasic !== 'WORKFLOW_EXECUTION_CLOSE_STATUS_INVALID'; + const timeRangeParams = useMemo(() => { + const now = dayjs(); + + return { + timeRangeStart: getDayjsFromDateFilterValue( + queryParams.timeRangeStartBasic, + now + ).toISOString(), + timeRangeEnd: getDayjsFromDateFilterValue( + queryParams.timeRangeEndBasic, + now + ).toISOString(), + }; + }, [queryParams.timeRangeStartBasic, queryParams.timeRangeEndBasic]); + const queryConfigs = useMemo(() => { const requestQueryParamsBase = { workflowId: queryParams.workflowId, workflowType: queryParams.workflowType, - timeRangeStart: queryParams.timeRangeStartBasic.toISOString(), - timeRangeEnd: queryParams.timeRangeEndBasic.toISOString(), + ...timeRangeParams, pageSize: pageSize.toString(), ...(queryParams.statusBasic !== 'ALL_CLOSED' && queryParams.statusBasic !== 'WORKFLOW_EXECUTION_CLOSE_STATUS_INVALID' @@ -77,8 +93,7 @@ export default function useListWorkflowsBasic({ loadClosedWorkflows, queryParams.workflowId, queryParams.workflowType, - queryParams.timeRangeStartBasic, - queryParams.timeRangeEndBasic, + timeRangeParams, queryParams.statusBasic, ]);