Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 33 additions & 14 deletions galasa-ui/src/components/test-runs/timeframe/TimeFrameContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import styles from '@/styles/test-runs/timeframe/TimeFrameContent.module.css';
import { TimeFrameValues } from '@/utils/interfaces';
import { useState, useCallback, useEffect } from 'react';
import { useState, useCallback, useEffect, useRef } from 'react';
import TimeFrameFilter from './TimeFrameFilter';
import { dateTimeLocal2UTC, dateTimeUTC2Local } from '@/utils/timeOperations';
import { InlineNotification } from '@carbon/react';
Expand Down Expand Up @@ -127,6 +127,7 @@ export default function TimeFrameContent({ values, setValues }: TimeFrameContent
const [selectedToOption, setSelectedToOption] = useState<ToSelectionOptions>(
ToSelectionOptions.now
);
const isSyncingFromUrl = useRef(false);

const handleValueChange = useCallback(
(field: keyof TimeFrameValues, value: any) => {
Expand Down Expand Up @@ -185,29 +186,47 @@ export default function TimeFrameContent({ values, setValues }: TimeFrameContent
// Update the state with the corrected values only if the notification is not an error
if (validationNotification?.kind !== 'error') {
const finalState = calculateSynchronizedState(correctedFrom, correctedTo, timezone);
setValues((prevValues) => ({ ...prevValues, ...finalState }));
setValues((prevValues) => ({
...prevValues,
...finalState,
fromOption: selectedFromOption,
toOption: selectedToOption,
}));
}
},
[values, translations, setValues, getResolvedTimeZone, selectedFromOption]
[values, translations, setValues, getResolvedTimeZone, selectedFromOption, selectedToOption]
);

// Update the isRelativeToNow state when the selectedToOption changes
useEffect(() => {
const isRelativeToNow = selectedToOption === ToSelectionOptions.now;
setValues((prevValues) => ({ ...prevValues, isRelativeToNow }));
}, [selectedToOption, setValues]);
// Don't update if we're currently syncing from URL to avoid circular updates
if (isSyncingFromUrl.current) {
return;
}

// Sync selected options whenever isRelativeToNow changes (from URL or user interaction)
const isRelativeToNow = selectedToOption === ToSelectionOptions.now;
setValues((prevValues) => ({
...prevValues,
isRelativeToNow,
fromOption: selectedFromOption,
toOption: selectedToOption,
}));
}, [selectedFromOption, selectedToOption, setValues]);

// Sync radio button selections from URL parameters when query in view changes
useEffect(() => {
if (values.isRelativeToNow !== undefined) {
const toOption = values.isRelativeToNow
? ToSelectionOptions.now
: ToSelectionOptions.specificToTime;
if (values.fromOption) {
isSyncingFromUrl.current = true;
setSelectedFromOption(values.fromOption as FromSelectionOptions);
}

// Always sync the "To" option with isRelativeToNow
setSelectedToOption(toOption);
if (values.toOption) {
isSyncingFromUrl.current = true;
setSelectedToOption(values.toOption as ToSelectionOptions);
}
}, [values.isRelativeToNow]);
// Reset the ref after state updates have been processed
isSyncingFromUrl.current = false;
}, [values.fromOption, values.toOption]);

return (
<div className={styles.timeFrameContainer}>
Expand Down
12 changes: 12 additions & 0 deletions galasa-ui/src/contexts/TestRunsQueryParamsContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ export function TestRunsQueryParamsProvider({ children }: TestRunsQueryParamsPro
const fromParam = params.get(TEST_RUNS_QUERY_PARAMS.FROM);
const toParam = params.get(TEST_RUNS_QUERY_PARAMS.TO);
const durationParam = params.get(TEST_RUNS_QUERY_PARAMS.DURATION);
const fromOptionParam = params.get(TEST_RUNS_QUERY_PARAMS.FROM_OPTION);
const toOptionParam = params.get(TEST_RUNS_QUERY_PARAMS.TO_OPTION);

let toDate: Date,
fromDate: Date,
Expand All @@ -153,6 +155,8 @@ export function TestRunsQueryParamsProvider({ children }: TestRunsQueryParamsPro
const timeframe = {
...calculateSynchronizedState(fromDate, toDate, timezone),
isRelativeToNow,
fromOption: fromOptionParam || undefined,
toOption: toOptionParam || undefined,
};

// Search Criteria
Expand Down Expand Up @@ -286,6 +290,14 @@ export function TestRunsQueryParamsProvider({ children }: TestRunsQueryParamsPro
params.set(TEST_RUNS_QUERY_PARAMS.TO, timeframeValues.toDate.toISOString());
}

// Radio button selections
if (timeframeValues.fromOption) {
params.set(TEST_RUNS_QUERY_PARAMS.FROM_OPTION, timeframeValues.fromOption);
}
if (timeframeValues.toOption) {
params.set(TEST_RUNS_QUERY_PARAMS.TO_OPTION, timeframeValues.toOption);
}

// Search Criteria
Object.entries(searchCriteria).forEach(([key, value]) => {
if (value) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,7 @@ describe('applyTimeFrameRules', () => {
});
});

test('should synchronize the "To" option when the query changes to be relative to now', async () => {
test('should preserve radio button selections when switching between queries', async () => {
const initialFrom = '2025-08-10T12:00:00.000Z';
const initialTo = '2025-08-13T12:00:00.000Z';

Expand All @@ -503,14 +503,30 @@ describe('applyTimeFrameRules', () => {
return (
<>
<TimeFrameContent values={values} setValues={setValues} />
{/* Add a button to simulate a query change */}
{/* Add a button to simulate loading a different query */}
<button
role="toggle-isRelativeToNow"
role="load-query-with-now"
onClick={() =>
setValues((prev) => ({ ...prev, isRelativeToNow: !prev.isRelativeToNow }))
setValues((prev) => ({
...prev,
isRelativeToNow: true,
toOption: ToSelectionOptions.now,
}))
}
>
Toggle isRelativeToNow
Load Query with Now
</button>
<button
role="load-query-with-specific"
onClick={() =>
setValues((prev) => ({
...prev,
isRelativeToNow: false,
toOption: ToSelectionOptions.specificToTime,
}))
}
>
Load Query with Specific Time
</button>
</>
);
Expand All @@ -535,20 +551,21 @@ describe('applyTimeFrameRules', () => {
expect(nowRadio).not.toBeChecked();
});

// Simulate a query change
const toggleButton = screen.getByRole('toggle-isRelativeToNow');
fireEvent.click(toggleButton);
// Simulate loading a query that uses "Now"
const loadNowButton = screen.getByRole('load-query-with-now');
fireEvent.click(loadNowButton);

// 'To' option should have switched from specific time to 'now'
// 'To' option should sync to 'now' based on the loaded query
await waitFor(() => {
expect(nowRadio).toBeChecked();
expect(specificToTimeRadio).not.toBeChecked();
});

// Simulate another query change
fireEvent.click(toggleButton);
// Simulate loading a query that uses "Specific Time"
const loadSpecificButton = screen.getByRole('load-query-with-specific');
fireEvent.click(loadSpecificButton);

// 'To' option should have switched back to specific time
// 'To' option should sync to 'specific time' based on the loaded query
await waitFor(() => {
expect(specificToTimeRadio).toBeChecked();
expect(nowRadio).not.toBeChecked();
Expand Down
2 changes: 2 additions & 0 deletions galasa-ui/src/utils/constants/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ const TEST_RUNS_QUERY_PARAMS = {
FROM: 'from',
TO: 'to',
DURATION: 'duration',
FROM_OPTION: 'fromOption',
TO_OPTION: 'toOption',
RUN_NAME: 'runName',
REQUESTOR: 'requestor',
USER: 'user',
Expand Down
2 changes: 2 additions & 0 deletions galasa-ui/src/utils/interfaces/testRuns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export interface TimeFrameValues {
durationHours: number;
durationMinutes: number;
isRelativeToNow?: boolean;
fromOption?: string;
toOption?: string;
}

export interface RunLog {
Expand Down
Loading