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,
]);