mirror of
https://github.com/hyperdxio/hyperdx
synced 2026-04-21 13:37:15 +00:00
fix: time selector always resets to 00:00
This commit is contained in:
parent
5885d47964
commit
4bf759f76e
4 changed files with 157 additions and 7 deletions
5
.changeset/short-tools-sleep.md
Normal file
5
.changeset/short-tools-sleep.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@hyperdx/app": patch
|
||||
---
|
||||
|
||||
fix: time selector always resets to 00:00
|
||||
|
|
@ -43,7 +43,7 @@ const modeAtom = atomWithStorage<TimePickerMode>(
|
|||
TimePickerMode.Range,
|
||||
);
|
||||
|
||||
const DATE_INPUT_PLACEHOLDER = 'YYY-MM-DD HH:mm:ss';
|
||||
const DATE_INPUT_PLACEHOLDER = 'YYYY-MM-DD HH:mm:ss';
|
||||
const DATE_INPUT_FORMAT = 'YYYY-MM-DD HH:mm:ss';
|
||||
|
||||
/** Ensure a value is a Date object (Mantine v9 DateInput returns strings). */
|
||||
|
|
@ -55,6 +55,11 @@ const toDate = (v: Date | string | null): Date | null =>
|
|||
* Mantine v9 DateInput expects/emits string values, but the TimePickerForm
|
||||
* stores Date objects (used by date-fns). This wrapper converts in both
|
||||
* directions: value (Date → string) and onChange (string → Date).
|
||||
*
|
||||
* `withTime` is required: by default DateInput strips the time part and
|
||||
* normalizes values to midnight, even when `valueFormat` includes time
|
||||
* tokens. Setting `withTime` preserves HH:mm:ss so manually-typed times
|
||||
* survive blur/commit.
|
||||
*/
|
||||
type DateInputCmpProps = Omit<DateInputProps, 'value' | 'onChange'> & {
|
||||
value?: Date | null;
|
||||
|
|
@ -69,6 +74,7 @@ const DateInputCmp = ({
|
|||
<DateInput
|
||||
size="xs"
|
||||
highlightToday
|
||||
withTime
|
||||
placeholder={DATE_INPUT_PLACEHOLDER}
|
||||
valueFormat={DATE_INPUT_FORMAT}
|
||||
variant="filled"
|
||||
|
|
|
|||
|
|
@ -177,14 +177,64 @@ export class TimePickerComponent {
|
|||
}
|
||||
|
||||
/**
|
||||
* Set a custom time range and apply
|
||||
* Locator for the Start time / "Time" DateInput inside the picker popover.
|
||||
* Both Start and End inputs share the same placeholder, so we disambiguate
|
||||
* by ordinal. In Range mode this is the first DateInput; in Around mode
|
||||
* the single DateInput is also the first (and only).
|
||||
*/
|
||||
get startDateInput() {
|
||||
return this.pickerPopover.getByPlaceholder('YYYY-MM-DD HH:mm:ss').nth(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Locator for the End time DateInput. Only present in Range mode.
|
||||
*/
|
||||
get endDateInput() {
|
||||
return this.pickerPopover.getByPlaceholder('YYYY-MM-DD HH:mm:ss').nth(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch the picker to "Time range" mode (two date inputs).
|
||||
* Safe to call when already in Range mode (SegmentedControl is idempotent).
|
||||
*
|
||||
* Mantine's SegmentedControl visually hides the underlying radio inputs,
|
||||
* so `getByRole('radio')` resolves to an element that Playwright considers
|
||||
* not visible. Click the associated label instead — same pattern as
|
||||
* ChartEditorComponent.switchToSqlMode().
|
||||
*/
|
||||
async selectRangeMode() {
|
||||
const rangeLabel = this.pickerPopover.locator(
|
||||
'.mantine-SegmentedControl-label:has-text("Time range")',
|
||||
);
|
||||
await rangeLabel.waitFor({ state: 'visible', timeout: 5000 });
|
||||
await rangeLabel.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill the Start time (or, in Around mode, the "Time") input with a
|
||||
* datetime string. Uses fill() + Enter to trigger the component's blur
|
||||
* handler, which commits the parsed value back to form state.
|
||||
*/
|
||||
async fillStartDate(value: string) {
|
||||
await this.startDateInput.fill(value);
|
||||
await this.startDateInput.press('Enter');
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill the End time input with a datetime string. Only valid in Range mode.
|
||||
*/
|
||||
async fillEndDate(value: string) {
|
||||
await this.endDateInput.fill(value);
|
||||
await this.endDateInput.press('Enter');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a custom absolute time range via the Start/End inputs and apply.
|
||||
* Assumes the popover is already open and relative mode is disabled.
|
||||
*/
|
||||
async setCustomTimeRange(from: string, to: string) {
|
||||
await this.open();
|
||||
// This would need to be implemented based on actual UI
|
||||
// Just a placeholder for the pattern
|
||||
await this.page.getByTestId('custom-from').fill(from);
|
||||
await this.page.getByTestId('custom-to').fill(to);
|
||||
await this.fillStartDate(from);
|
||||
await this.fillEndDate(to);
|
||||
await this.apply();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,89 @@
|
|||
import { SearchPage } from '../../page-objects/SearchPage';
|
||||
import { expect, test } from '../../utils/base-test';
|
||||
|
||||
/**
|
||||
* Regression coverage for the "manually typed time resets to 00:00" bug.
|
||||
*
|
||||
* Before the fix, Mantine's DateInput (used in `DateInputCmp`) was invoked
|
||||
* without the `withTime` prop. Even though `valueFormat` included HH:mm:ss,
|
||||
* DateInput stripped the time portion on blur and normalized values to
|
||||
* midnight, so any manually typed Start/End time was lost.
|
||||
*
|
||||
* These tests type non-midnight times into the Start and End inputs and
|
||||
* assert the values survive both the immediate commit (Enter → blur) and a
|
||||
* full close/reopen of the picker popover.
|
||||
*/
|
||||
test.describe('Custom Time Range', { tag: '@custom-time-range' }, () => {
|
||||
let searchPage: SearchPage;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
searchPage = new SearchPage(page);
|
||||
await searchPage.goto();
|
||||
await expect(searchPage.form).toBeVisible();
|
||||
await searchPage.timePicker.open();
|
||||
// Search page defaults to live/relative mode; switch to absolute so the
|
||||
// Range inputs become editable.
|
||||
await searchPage.timePicker.disableRelativeTime();
|
||||
// Guard against prior tests leaving the mode atom on "Around a time".
|
||||
await searchPage.timePicker.selectRangeMode();
|
||||
});
|
||||
|
||||
test('should preserve manually typed Start and End times', async () => {
|
||||
const start = '2026-04-15 14:37:42';
|
||||
const end = '2026-04-15 15:09:11';
|
||||
|
||||
await test.step('Type the datetimes into the inputs', async () => {
|
||||
await searchPage.timePicker.fillStartDate(start);
|
||||
await searchPage.timePicker.fillEndDate(end);
|
||||
});
|
||||
|
||||
await test.step('Inputs retain the typed values before applying', async () => {
|
||||
await expect(searchPage.timePicker.startDateInput).toHaveValue(start);
|
||||
await expect(searchPage.timePicker.endDateInput).toHaveValue(end);
|
||||
});
|
||||
|
||||
await test.step('Apply and confirm times propagate to the picker input', async () => {
|
||||
await searchPage.timePicker.apply();
|
||||
|
||||
// The main picker input re-formats the range via date-fns, so the
|
||||
// literal YYYY-MM-DD HH:mm:ss string won't appear. Instead, assert
|
||||
// the typed HH:mm:ss components are present and the range is NOT
|
||||
// collapsed to two midnights (the old broken behavior).
|
||||
const inputValue = await searchPage.timePicker.input.inputValue();
|
||||
expect(inputValue).toContain('14:37:42');
|
||||
expect(inputValue).toContain('15:09:11');
|
||||
});
|
||||
|
||||
await test.step('URL reflects an absolute range', async () => {
|
||||
await searchPage.page.waitForURL('**/search**from=**to=**');
|
||||
const url = searchPage.page.url();
|
||||
expect(url).toContain('from=');
|
||||
expect(url).toContain('to=');
|
||||
expect(url).toContain('isLive=false');
|
||||
});
|
||||
});
|
||||
|
||||
test('should preserve typed times after closing and reopening the picker', async () => {
|
||||
const start = '2026-04-10 09:15:30';
|
||||
const end = '2026-04-10 11:45:20';
|
||||
|
||||
await searchPage.timePicker.fillStartDate(start);
|
||||
await searchPage.timePicker.fillEndDate(end);
|
||||
await searchPage.timePicker.apply();
|
||||
|
||||
await test.step('Reopen picker and verify inputs still show typed times', async () => {
|
||||
await searchPage.timePicker.open();
|
||||
await expect(searchPage.timePicker.startDateInput).toHaveValue(start);
|
||||
await expect(searchPage.timePicker.endDateInput).toHaveValue(end);
|
||||
});
|
||||
});
|
||||
|
||||
test('should accept chrono natural-language times with non-midnight hours', async () => {
|
||||
// "dateParser" in utils.ts uses chrono-node; this spec guards that natural
|
||||
// language input still resolves to a non-midnight time after the fix.
|
||||
await searchPage.timePicker.fillStartDate('yesterday at 3:22 pm');
|
||||
// Format produced by the DateInput re-serialization is YYYY-MM-DD HH:mm:ss
|
||||
// (24h). 3:22 pm → 15:22.
|
||||
await expect(searchPage.timePicker.startDateInput).toHaveValue(/15:22/);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue