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..d74799875 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,28 +1,24 @@ +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, } from '@/hooks/use-page-query-params/use-page-query-params.types'; -import dayjs from '@/utils/datetime/dayjs'; -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'; import { type WorkflowStatus } from '@/views/shared/workflow-status-tag/workflow-status-tag.types'; import { type WorkflowsHeaderInputType } from '@/views/shared/workflows-header/workflows-header.types'; -const now = dayjs(); - const domainPageQueryParamsConfig: [ PageQueryParam<'inputType', WorkflowsHeaderInputType>, // Search input PageQueryParam<'search', string>, PageQueryParamMultiValue<'statuses', Array | undefined>, - PageQueryParam<'timeRangeStart', Date | undefined>, - PageQueryParam<'timeRangeEnd', Date>, + PageQueryParam<'timeRangeStart', DateFilterValue>, + PageQueryParam<'timeRangeEnd', DateFilterValue>, PageQueryParam<'sortColumn', string>, PageQueryParam<'sortOrder', SortOrder>, // Query input @@ -31,8 +27,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>, @@ -40,8 +36,8 @@ const domainPageQueryParamsConfig: [ 'statusesArchival', Array | undefined >, - PageQueryParam<'timeRangeStartArchival', Date>, - PageQueryParam<'timeRangeEndArchival', Date>, + PageQueryParam<'timeRangeStartArchival', DateFilterValue>, + PageQueryParam<'timeRangeEndArchival', DateFilterValue>, PageQueryParam<'sortColumnArchival', string>, PageQueryParam<'sortOrderArchival', SortOrder>, PageQueryParam<'queryArchival', string>, @@ -66,13 +62,14 @@ const domainPageQueryParamsConfig: [ { key: 'timeRangeStart', queryParamKey: 'start', - parseValue: parseDateQueryParam, + defaultValue: 'now-7d', + parseValue: (v) => parseDateFilterValue(v, 'now-7d'), }, { key: 'timeRangeEnd', queryParamKey: 'end', - defaultValue: now.toDate(), - parseValue: (v) => parseDateQueryParam(v) ?? now.toDate(), + defaultValue: 'now', + parseValue: (v) => parseDateFilterValue(v, 'now'), }, { key: 'sortColumn', @@ -106,18 +103,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', @@ -146,20 +139,14 @@ const domainPageQueryParamsConfig: [ { key: 'timeRangeStartArchival', queryParamKey: 'astart', - defaultValue: now - .subtract(DOMAIN_WORKFLOWS_ARCHIVAL_START_DAYS_CONFIG, 'days') - .toDate(), - parseValue: (v) => - parseDateQueryParam(v) ?? - now - .subtract(DOMAIN_WORKFLOWS_ARCHIVAL_START_DAYS_CONFIG, 'days') - .toDate(), + defaultValue: 'now-7d', + parseValue: (v) => parseDateFilterValue(v, 'now-7d'), }, { key: 'timeRangeEndArchival', queryParamKey: 'aend', - defaultValue: now.toDate(), - parseValue: (v) => parseDateQueryParam(v) ?? now.toDate(), + defaultValue: 'now', + parseValue: (v) => parseDateFilterValue(v, 'now'), }, { key: 'sortColumnArchival', diff --git a/src/views/domain-workflows-archival/config/domain-workflows-archival-filters.config.ts b/src/views/domain-workflows-archival/config/domain-workflows-archival-filters.config.ts index 6eb4fe288..bf4d2fe80 100644 --- a/src/views/domain-workflows-archival/config/domain-workflows-archival-filters.config.ts +++ b/src/views/domain-workflows-archival/config/domain-workflows-archival-filters.config.ts @@ -2,7 +2,9 @@ import { createElement } from 'react'; import { omit } from 'lodash'; -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 ListFilterMulti from '@/components/list-filter-multi/list-filter-multi'; import { type PageFilterConfig } from '@/components/page-filters/page-filters.types'; import type domainPageQueryParamsConfig from '@/views/domain-page/config/domain-page-query-params.config'; @@ -18,8 +20,8 @@ const domainWorkflowsArchivalFiltersConfig: [ PageFilterConfig< typeof domainPageQueryParamsConfig, { - timeRangeStartArchival: Date | undefined; - timeRangeEndArchival: Date | undefined; + timeRangeStartArchival: DateFilterValue | undefined; + timeRangeEndArchival: DateFilterValue | undefined; } >, ] = [ @@ -41,14 +43,21 @@ const domainWorkflowsArchivalFiltersConfig: [ }, { id: 'dates', - getValue: (v) => v, + getValue: (v) => ({ + timeRangeStartArchival: v.timeRangeStartArchival, + timeRangeEndArchival: v.timeRangeEndArchival, + }), formatValue: (v) => ({ - timeRangeStartArchival: v.timeRangeStartArchival?.toISOString(), - timeRangeEndArchival: v.timeRangeEndArchival?.toISOString(), + timeRangeStartArchival: v.timeRangeStartArchival + ? stringifyDateFilterValue(v.timeRangeStartArchival) + : undefined, + timeRangeEndArchival: v.timeRangeEndArchival + ? stringifyDateFilterValue(v.timeRangeEndArchival) + : undefined, }), component: ({ value, setValue }) => - createElement(DateFilter, { - label: 'Dates', + createElement(DateFilterV2, { + label: 'Time range', placeholder: 'Select time range', dates: { start: value.timeRangeStartArchival, @@ -59,7 +68,6 @@ const domainWorkflowsArchivalFiltersConfig: [ timeRangeStartArchival: start, timeRangeEndArchival: end, }), - clearable: false, }), }, ] as const; diff --git a/src/views/domain-workflows-archival/domain-workflows-archival-header/__tests__/domain-workflows-archival-header.test.tsx b/src/views/domain-workflows-archival/domain-workflows-archival-header/__tests__/domain-workflows-archival-header.test.tsx deleted file mode 100644 index 7ef26a2fc..000000000 --- a/src/views/domain-workflows-archival/domain-workflows-archival-header/__tests__/domain-workflows-archival-header.test.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { render, screen } from '@/test-utils/rtl'; - -import { mockDomainPageQueryParamsValues } from '../../../domain-page/__fixtures__/domain-page-query-params'; -import DomainWorkflowsArchivalHeader from '../domain-workflows-archival-header'; - -jest.useFakeTimers().setSystemTime(new Date('2023-05-25')); - -const mockSetQueryParams = jest.fn(); -jest.mock('@/hooks/use-page-query-params/use-page-query-params', () => - jest.fn(() => [mockDomainPageQueryParamsValues, mockSetQueryParams]) -); - -jest.mock('@/views/shared/workflows-header/workflows-header', () => - jest.fn(() =>
Workflows Header
) -); - -jest.mock('@/views/shared/hooks/use-list-workflows', () => - jest.fn(() => ({ - refetch: jest.fn(), - isFetching: false, - })) -); - -describe(DomainWorkflowsArchivalHeader.name, () => { - it('renders workflows header', async () => { - render( - - ); - - expect(screen.getByText('Workflows Header')).toBeInTheDocument(); - }); -}); diff --git a/src/views/domain-workflows-archival/domain-workflows-archival-header/domain-workflows-archival-header.tsx b/src/views/domain-workflows-archival/domain-workflows-archival-header/domain-workflows-archival-header.tsx index 4515856c7..39b9d041e 100644 --- a/src/views/domain-workflows-archival/domain-workflows-archival-header/domain-workflows-archival-header.tsx +++ b/src/views/domain-workflows-archival/domain-workflows-archival-header/domain-workflows-archival-header.tsx @@ -13,6 +13,8 @@ import { type Props } from './domain-workflows-archival-header.types'; export default function DomainWorkflowsArchivalHeader({ domain, cluster, + timeRangeStart, + timeRangeEnd, }: Props) { const [queryParams] = usePageQueryParams(domainPageQueryParamsConfig); @@ -24,8 +26,8 @@ export default function DomainWorkflowsArchivalHeader({ inputType: queryParams.inputTypeArchival, search: queryParams.searchArchival, statuses: queryParams.statusesArchival, - timeRangeStart: queryParams.timeRangeStartArchival, - timeRangeEnd: queryParams.timeRangeEndArchival, + timeRangeStart, + timeRangeEnd, sortColumn: queryParams.sortColumnArchival, sortOrder: queryParams.sortOrderArchival, query: queryParams.queryArchival, diff --git a/src/views/domain-workflows-archival/domain-workflows-archival-header/domain-workflows-archival-header.types.ts b/src/views/domain-workflows-archival/domain-workflows-archival-header/domain-workflows-archival-header.types.ts index 042c6d33b..7d2daac02 100644 --- a/src/views/domain-workflows-archival/domain-workflows-archival-header/domain-workflows-archival-header.types.ts +++ b/src/views/domain-workflows-archival/domain-workflows-archival-header/domain-workflows-archival-header.types.ts @@ -3,6 +3,8 @@ import { type WorkflowStatus } from '@/views/shared/workflow-status-tag/workflow export type Props = { domain: string; cluster: string; + timeRangeStart: string; + timeRangeEnd: string; }; export type WorkflowStatusClosed = Exclude< diff --git a/src/views/domain-workflows-archival/domain-workflows-archival-table/__tests__/domain-workflows-archival-table.test.tsx b/src/views/domain-workflows-archival/domain-workflows-archival-table/__tests__/domain-workflows-archival-table.test.tsx index 7bddcc303..01b5e9af5 100644 --- a/src/views/domain-workflows-archival/domain-workflows-archival-table/__tests__/domain-workflows-archival-table.test.tsx +++ b/src/views/domain-workflows-archival/domain-workflows-archival-table/__tests__/domain-workflows-archival-table.test.tsx @@ -131,6 +131,8 @@ function setup({ , { endpointsMocks: [ diff --git a/src/views/domain-workflows-archival/domain-workflows-archival-table/domain-workflows-archival-table.tsx b/src/views/domain-workflows-archival/domain-workflows-archival-table/domain-workflows-archival-table.tsx index d9cd1895b..ff128a7cc 100644 --- a/src/views/domain-workflows-archival/domain-workflows-archival-table/domain-workflows-archival-table.tsx +++ b/src/views/domain-workflows-archival/domain-workflows-archival-table/domain-workflows-archival-table.tsx @@ -18,6 +18,8 @@ import getArchivalErrorPanelProps from './helpers/get-archival-error-panel-props export default function DomainWorkflowsArchivalTable({ domain, cluster, + timeRangeStart, + timeRangeEnd, }: Props) { const [queryParams, setQueryParams] = usePageQueryParams( domainPageQueryParamsConfig @@ -39,8 +41,8 @@ export default function DomainWorkflowsArchivalTable({ inputType: queryParams.inputTypeArchival, search: queryParams.searchArchival, statuses: queryParams.statusesArchival, - timeRangeStart: queryParams.timeRangeStartArchival, - timeRangeEnd: queryParams.timeRangeEndArchival, + timeRangeStart, + timeRangeEnd, sortColumn: queryParams.sortColumnArchival, sortOrder: queryParams.sortOrderArchival, query: queryParams.queryArchival, diff --git a/src/views/domain-workflows-archival/domain-workflows-archival-table/domain-workflows-archival-table.types.ts b/src/views/domain-workflows-archival/domain-workflows-archival-table/domain-workflows-archival-table.types.ts index 2adbd04c7..7c3616b74 100644 --- a/src/views/domain-workflows-archival/domain-workflows-archival-table/domain-workflows-archival-table.types.ts +++ b/src/views/domain-workflows-archival/domain-workflows-archival-table/domain-workflows-archival-table.types.ts @@ -1,4 +1,6 @@ export type Props = { domain: string; cluster: string; + timeRangeStart: string; + timeRangeEnd: string; }; diff --git a/src/views/domain-workflows-archival/domain-workflows-archival.tsx b/src/views/domain-workflows-archival/domain-workflows-archival.tsx index f23ce387f..669dcebee 100644 --- a/src/views/domain-workflows-archival/domain-workflows-archival.tsx +++ b/src/views/domain-workflows-archival/domain-workflows-archival.tsx @@ -1,7 +1,12 @@ -import React from 'react'; +import React, { useMemo } from 'react'; +import dayjs from 'dayjs'; + +import getDayjsFromDateFilterValue from '@/components/date-filter-v2/helpers/get-dayjs-from-date-filter-value'; +import usePageQueryParams from '@/hooks/use-page-query-params/use-page-query-params'; import { type DomainPageTabContentProps } from '@/views/domain-page/domain-page-content/domain-page-content.types'; +import domainPageQueryParamsConfig from '../domain-page/config/domain-page-query-params.config'; import useSuspenseDomainDescription from '../shared/hooks/use-domain-description/use-suspense-domain-description'; import DomainWorkflowsArchivalDisabledPanel from './domain-workflows-archival-disabled-panel/domain-workflows-archival-disabled-panel'; @@ -15,6 +20,23 @@ export default function DomainWorkflowsArchival( data: { historyArchivalStatus, visibilityArchivalStatus }, } = useSuspenseDomainDescription(props); + const [queryParams] = usePageQueryParams(domainPageQueryParamsConfig); + + const timeRangeParams = useMemo(() => { + const now = dayjs(); + + return { + timeRangeStart: getDayjsFromDateFilterValue( + queryParams.timeRangeStartArchival, + now + ).toISOString(), + timeRangeEnd: getDayjsFromDateFilterValue( + queryParams.timeRangeEndArchival, + now + ).toISOString(), + }; + }, [queryParams.timeRangeStartArchival, queryParams.timeRangeEndArchival]); + if ( historyArchivalStatus !== 'ARCHIVAL_STATUS_ENABLED' || visibilityArchivalStatus !== 'ARCHIVAL_STATUS_ENABLED' @@ -27,10 +49,12 @@ export default function DomainWorkflowsArchival( ); 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, ]); diff --git a/src/views/domain-workflows/config/domain-workflows-filters.config.ts b/src/views/domain-workflows/config/domain-workflows-filters.config.ts index ed3743e30..3363b5d23 100644 --- a/src/views/domain-workflows/config/domain-workflows-filters.config.ts +++ b/src/views/domain-workflows/config/domain-workflows-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 ListFilterMulti from '@/components/list-filter-multi/list-filter-multi'; import { type PageFilterConfig } from '@/components/page-filters/page-filters.types'; import type domainPageQueryParamsConfig from '@/views/domain-page/config/domain-page-query-params.config'; @@ -14,7 +16,10 @@ const domainWorkflowsFiltersConfig: [ >, PageFilterConfig< typeof domainPageQueryParamsConfig, - { timeRangeStart: Date | undefined; timeRangeEnd: Date | undefined } + { + timeRangeStart: DateFilterValue | undefined; + timeRangeEnd: DateFilterValue | undefined; + } >, ] = [ { @@ -37,19 +42,26 @@ const domainWorkflowsFiltersConfig: [ timeRangeEnd: v.timeRangeEnd, }), formatValue: (v) => ({ - timeRangeStart: v.timeRangeStart?.toISOString(), - timeRangeEnd: v.timeRangeEnd?.toISOString(), + timeRangeStart: v.timeRangeStart + ? stringifyDateFilterValue(v.timeRangeStart) + : undefined, + timeRangeEnd: v.timeRangeEnd + ? stringifyDateFilterValue(v.timeRangeEnd) + : undefined, }), component: ({ value, setValue }) => - createElement(DateFilter, { - label: 'Dates', + createElement(DateFilterV2, { + label: 'Time range', placeholder: 'Select time range', dates: { start: value.timeRangeStart, end: value.timeRangeEnd, }, onChangeDates: ({ start, end }) => - setValue({ timeRangeStart: start, timeRangeEnd: end }), + setValue({ + timeRangeStart: start, + timeRangeEnd: end, + }), }), }, ] as const; diff --git a/src/views/domain-workflows/domain-workflows-advanced/domain-workflows-advanced.tsx b/src/views/domain-workflows/domain-workflows-advanced/domain-workflows-advanced.tsx new file mode 100644 index 000000000..9743945a4 --- /dev/null +++ b/src/views/domain-workflows/domain-workflows-advanced/domain-workflows-advanced.tsx @@ -0,0 +1,45 @@ +import { useMemo } from 'react'; + +import getDayjsFromDateFilterValue from '@/components/date-filter-v2/helpers/get-dayjs-from-date-filter-value'; +import usePageQueryParams from '@/hooks/use-page-query-params/use-page-query-params'; +import dayjs from '@/utils/datetime/dayjs'; +import domainPageQueryParamsConfig from '@/views/domain-page/config/domain-page-query-params.config'; + +import DomainWorkflowsHeader from '../domain-workflows-header/domain-workflows-header'; +import DomainWorkflowsTable from '../domain-workflows-table/domain-workflows-table'; + +import { type Props } from './domain-workflows-advanced.types'; + +export default function DomainWorkflowsAdvanced({ domain, cluster }: Props) { + const [queryParams] = usePageQueryParams(domainPageQueryParamsConfig); + + const timeRangeParams = useMemo(() => { + const now = dayjs(); + + return { + timeRangeStart: getDayjsFromDateFilterValue( + queryParams.timeRangeStartBasic, + now + ).toISOString(), + timeRangeEnd: getDayjsFromDateFilterValue( + queryParams.timeRangeEndBasic, + now + ).toISOString(), + }; + }, [queryParams.timeRangeStartBasic, queryParams.timeRangeEndBasic]); + + return ( + <> + + + + ); +} diff --git a/src/views/domain-workflows/domain-workflows-advanced/domain-workflows-advanced.types.ts b/src/views/domain-workflows/domain-workflows-advanced/domain-workflows-advanced.types.ts new file mode 100644 index 000000000..2adbd04c7 --- /dev/null +++ b/src/views/domain-workflows/domain-workflows-advanced/domain-workflows-advanced.types.ts @@ -0,0 +1,4 @@ +export type Props = { + domain: string; + cluster: string; +}; diff --git a/src/views/domain-workflows/domain-workflows-header/domain-workflows-header.tsx b/src/views/domain-workflows/domain-workflows-header/domain-workflows-header.tsx index fcffd7217..b6a595fe3 100644 --- a/src/views/domain-workflows/domain-workflows-header/domain-workflows-header.tsx +++ b/src/views/domain-workflows/domain-workflows-header/domain-workflows-header.tsx @@ -9,7 +9,12 @@ import DOMAIN_WORKFLOWS_PAGE_SIZE from '../config/domain-workflows-page-size.con import { type Props } from './domain-workflows-header.types'; -export default function DomainWorkflowsHeader({ domain, cluster }: Props) { +export default function DomainWorkflowsHeader({ + domain, + cluster, + timeRangeStart, + timeRangeEnd, +}: Props) { const [queryParams] = usePageQueryParams(domainPageQueryParamsConfig); const { refetch, isFetching } = useListWorkflows({ @@ -20,8 +25,8 @@ export default function DomainWorkflowsHeader({ domain, cluster }: Props) { inputType: queryParams.inputType, search: queryParams.search, statuses: queryParams.statuses, - timeRangeStart: queryParams.timeRangeStart, - timeRangeEnd: queryParams.timeRangeEnd, + timeRangeStart, + timeRangeEnd, sortColumn: queryParams.sortColumn, sortOrder: queryParams.sortOrder, query: queryParams.query, diff --git a/src/views/domain-workflows/domain-workflows-header/domain-workflows-header.types.ts b/src/views/domain-workflows/domain-workflows-header/domain-workflows-header.types.ts index 2adbd04c7..7c3616b74 100644 --- a/src/views/domain-workflows/domain-workflows-header/domain-workflows-header.types.ts +++ b/src/views/domain-workflows/domain-workflows-header/domain-workflows-header.types.ts @@ -1,4 +1,6 @@ export type Props = { domain: string; cluster: string; + timeRangeStart: string; + timeRangeEnd: string; }; diff --git a/src/views/domain-workflows/domain-workflows-table/__tests__/domain-workflows-table.test.tsx b/src/views/domain-workflows/domain-workflows-table/__tests__/domain-workflows-table.test.tsx index ca10350f8..2f923558a 100644 --- a/src/views/domain-workflows/domain-workflows-table/__tests__/domain-workflows-table.test.tsx +++ b/src/views/domain-workflows/domain-workflows-table/__tests__/domain-workflows-table.test.tsx @@ -152,49 +152,57 @@ function setup({ let currentEventIndex = 0; const user = userEvent.setup(); - render(, { - endpointsMocks: [ - { - path: '/api/domains/:domain/:cluster/workflows', - httpMethod: 'GET', - mockOnce: false, - httpResolver: async () => { - const index = currentEventIndex; - currentEventIndex++; - - switch (errorCase) { - case 'no-workflows': - return HttpResponse.json({ - workflows: [], - nextPage: undefined, - }); - case 'initial-fetch-error': - return HttpResponse.json( - { message: 'Request failed' }, - { status: 500 } - ); - case 'subsequent-fetch-error': - if (index === 0) { - return HttpResponse.json(pages[0]); - } else if (index === 1) { + render( + , + { + endpointsMocks: [ + { + path: '/api/domains/:domain/:cluster/workflows', + httpMethod: 'GET', + mockOnce: false, + httpResolver: async () => { + const index = currentEventIndex; + currentEventIndex++; + + switch (errorCase) { + case 'no-workflows': + return HttpResponse.json({ + workflows: [], + nextPage: undefined, + }); + case 'initial-fetch-error': return HttpResponse.json( { message: 'Request failed' }, { status: 500 } ); - } else { - return HttpResponse.json(pages[1]); - } - default: - if (index === 0) { - return HttpResponse.json(pages[0]); - } else { - return HttpResponse.json(pages[1]); - } - } + case 'subsequent-fetch-error': + if (index === 0) { + return HttpResponse.json(pages[0]); + } else if (index === 1) { + return HttpResponse.json( + { message: 'Request failed' }, + { status: 500 } + ); + } else { + return HttpResponse.json(pages[1]); + } + default: + if (index === 0) { + return HttpResponse.json(pages[0]); + } else { + return HttpResponse.json(pages[1]); + } + } + }, }, - }, - ] as MSWMocksHandlersProps['endpointsMocks'], - }); + ] as MSWMocksHandlersProps['endpointsMocks'], + } + ); return { user }; } diff --git a/src/views/domain-workflows/domain-workflows-table/domain-workflows-table.tsx b/src/views/domain-workflows/domain-workflows-table/domain-workflows-table.tsx index 31f16fb81..bf1ecda09 100644 --- a/src/views/domain-workflows/domain-workflows-table/domain-workflows-table.tsx +++ b/src/views/domain-workflows/domain-workflows-table/domain-workflows-table.tsx @@ -15,7 +15,12 @@ import DOMAIN_WORKFLOWS_PAGE_SIZE from '../config/domain-workflows-page-size.con import { type Props } from './domain-workflows-table.types'; import getWorkflowsErrorPanelProps from './helpers/get-workflows-error-panel-props'; -export default function DomainWorkflowsTable({ domain, cluster }: Props) { +export default function DomainWorkflowsTable({ + domain, + cluster, + timeRangeStart, + timeRangeEnd, +}: Props) { const [queryParams, setQueryParams] = usePageQueryParams( domainPageQueryParamsConfig ); @@ -36,8 +41,8 @@ export default function DomainWorkflowsTable({ domain, cluster }: Props) { inputType: queryParams.inputType, search: queryParams.search, statuses: queryParams.statuses, - timeRangeStart: queryParams.timeRangeStart, - timeRangeEnd: queryParams.timeRangeEnd, + timeRangeStart, + timeRangeEnd, sortColumn: queryParams.sortColumn, sortOrder: queryParams.sortOrder, query: queryParams.query, diff --git a/src/views/domain-workflows/domain-workflows-table/domain-workflows-table.types.ts b/src/views/domain-workflows/domain-workflows-table/domain-workflows-table.types.ts index 2adbd04c7..7c3616b74 100644 --- a/src/views/domain-workflows/domain-workflows-table/domain-workflows-table.types.ts +++ b/src/views/domain-workflows/domain-workflows-table/domain-workflows-table.types.ts @@ -1,4 +1,6 @@ export type Props = { domain: string; cluster: string; + timeRangeStart: string; + timeRangeEnd: string; }; diff --git a/src/views/domain-workflows/domain-workflows.tsx b/src/views/domain-workflows/domain-workflows.tsx index 8eba0835b..06b4ee95d 100644 --- a/src/views/domain-workflows/domain-workflows.tsx +++ b/src/views/domain-workflows/domain-workflows.tsx @@ -13,12 +13,8 @@ const DomainWorkflowsBasic = dynamic( () => import('@/views/domain-workflows-basic/domain-workflows-basic') ); -const DomainWorkflowsHeader = dynamic( - () => import('./domain-workflows-header/domain-workflows-header') -); - -const DomainWorkflowsTable = dynamic( - () => import('./domain-workflows-table/domain-workflows-table') +const DomainWorkflowsAdvanced = dynamic( + () => import('./domain-workflows-advanced/domain-workflows-advanced') ); export default function DomainWorkflows(props: DomainPageTabContentProps) { @@ -32,16 +28,11 @@ export default function DomainWorkflows(props: DomainPageTabContentProps) { return isClusterAdvancedVisibilityEnabled(data); }, [data]); - if (!isAdvancedVisibilityEnabled) { - return ( - - ); - } + const DomainWorkflowsComponent = isAdvancedVisibilityEnabled + ? DomainWorkflowsAdvanced + : DomainWorkflowsBasic; return ( - <> - - - + ); } diff --git a/src/views/shared/hooks/use-list-workflows.ts b/src/views/shared/hooks/use-list-workflows.ts index 5a2b2dc66..043d75291 100644 --- a/src/views/shared/hooks/use-list-workflows.ts +++ b/src/views/shared/hooks/use-list-workflows.ts @@ -42,8 +42,8 @@ export default function useListWorkflows({ statuses, sortColumn, sortOrder, - timeRangeStart: timeRangeStart?.toISOString(), - timeRangeEnd: timeRangeEnd?.toISOString(), + timeRangeStart, + timeRangeEnd, }), }; diff --git a/src/views/shared/hooks/use-list-workflows.types.ts b/src/views/shared/hooks/use-list-workflows.types.ts index cc6ff16fe..004b6ef5e 100644 --- a/src/views/shared/hooks/use-list-workflows.types.ts +++ b/src/views/shared/hooks/use-list-workflows.types.ts @@ -15,8 +15,8 @@ export type UseListWorkflowsParams = ListWorkflowsRouteParams & { listType: ListType; search?: string; statuses?: Array; - timeRangeStart?: Date; - timeRangeEnd?: Date; + timeRangeStart?: string; + timeRangeEnd?: string; sortColumn?: string; sortOrder?: SortOrder; query?: string;