Skip to content
Draft
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
1 change: 0 additions & 1 deletion packages/api/src/routers/external-api/v2/utils/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,6 @@ export async function runSearchConfig({
dateRange: [startDate, endDate],
});

// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
const chartConfig: ChartConfigWithDateRange = {
...searchBase,
connection: source.connection.toString(),
Expand Down
22 changes: 22 additions & 0 deletions packages/app/src/DBSearchPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -832,6 +832,26 @@ export function DBSearchPage() {
id: searchedConfig.source,
kinds: [SourceKind.Log, SourceKind.Trace],
});

const { data: searchedSourceColumns } = useColumns(
{
databaseName: searchedSource?.from?.databaseName ?? '',
tableName: searchedSource?.from?.tableName ?? '',
connectionId: searchedSource?.connection ?? '',
},
{ enabled: !!searchedSource },
);
const showMs = useMemo(() => {
if (!searchedSource || !searchedSourceColumns) return true;
const tsParts = splitAndTrimWithBracket(
searchedSource.timestampValueExpression,
);
return tsParts.some(part => {
const colMeta = searchedSourceColumns.find(c => c.name === part);
return colMeta?.type?.startsWith('DateTime64') ?? false;
});
}, [searchedSource, searchedSourceColumns]);

const directTraceSource =
directTraceId != null && searchedSource?.kind === SourceKind.Trace
? searchedSource
Expand Down Expand Up @@ -946,6 +966,7 @@ export function DBSearchPage() {
showRelativeInterval: isLive ?? true,
setDisplayedTimeInputValue,
updateInput: !isLive,
showMs,
});

// Sync url state back with form state
Expand Down Expand Up @@ -1996,6 +2017,7 @@ export function DBSearchPage() {
onRelativeSearch={onTimePickerRelativeSearch}
showLive={analysisMode === 'results'}
isLiveMode={isLive}
showMs={showMs}
// Default to relative time mode if the user has made changes to interval and reloaded.
defaultRelativeTimeMode={
isLive && interval !== LIVE_TAIL_DURATION_MS
Expand Down
32 changes: 26 additions & 6 deletions packages/app/src/components/TimePicker/TimePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,10 @@ const modeAtom = atomWithStorage<TimePickerMode>(
TimePickerMode.Range,
);

const DATE_INPUT_PLACEHOLDER = 'YYYY-MM-DD HH:mm:ss';
const DATE_INPUT_FORMAT = 'YYYY-MM-DD HH:mm:ss';
const DATE_INPUT_PLACEHOLDER_MS = 'YYYY-MM-DD HH:mm:ss.SSS';
const DATE_INPUT_FORMAT_MS = 'YYYY-MM-DD HH:mm:ss.SSS';
const DATE_INPUT_PLACEHOLDER_SEC = 'YYYY-MM-DD HH:mm:ss';
const DATE_INPUT_FORMAT_SEC = 'YYYY-MM-DD HH:mm:ss';

/** Ensure a value is a Date object (Mantine v9 DateInput returns strings). */
const toDate = (v: Date | string | null): Date | null =>
Expand Down Expand Up @@ -75,8 +77,8 @@ const DateInputCmp = ({
size="xs"
highlightToday
withTime
placeholder={DATE_INPUT_PLACEHOLDER}
valueFormat={DATE_INPUT_FORMAT}
placeholder={DATE_INPUT_PLACEHOLDER_MS}
valueFormat={DATE_INPUT_FORMAT_MS}
variant="filled"
dateParser={dateParser}
onKeyDown={e => {
Expand Down Expand Up @@ -105,6 +107,7 @@ const TimePickerComponent = ({
showLive = false,
isLiveMode = false,
defaultRelativeTimeMode = false,
showMs = true,
width = 350,
}: {
inputValue: string;
Expand All @@ -115,12 +118,18 @@ const TimePickerComponent = ({
showLive?: boolean;
isLiveMode?: boolean;
defaultRelativeTimeMode?: boolean;
showMs?: boolean;
width?: number | string;
}) => {
const {
userPreferences: { timeFormat },
} = useUserPreferences();

const dateInputPlaceholder = showMs
? DATE_INPUT_PLACEHOLDER_MS
: DATE_INPUT_PLACEHOLDER_SEC;
const dateInputFormat = showMs ? DATE_INPUT_FORMAT_MS : DATE_INPUT_FORMAT_SEC;

const [opened, { close, toggle }] = useDisclosure(false);

useHotkeys('d', () => toggle(), { preventDefault: true }, [toggle]);
Expand Down Expand Up @@ -187,8 +196,13 @@ const TimePickerComponent = ({
if (!from || !to) {
return;
}
const formatStr =
timeFormat === '24h' ? 'MMM d HH:mm:ss' : 'MMM d h:mm:ss a';
const formatStr = showMs
? timeFormat === '24h'
? 'MMM d HH:mm:ss.SSS'
: 'MMM d h:mm:ss.SSS a'
: timeFormat === '24h'
? 'MMM d HH:mm:ss'
: 'MMM d h:mm:ss a';
const rangeStr = [from, to]
.map(d => d && format(d, formatStr))
.join(' - ');
Expand Down Expand Up @@ -433,6 +447,8 @@ const TimePickerComponent = ({
popoverProps={dateComponentPopoverProps}
maxDate={today}
mb="xs"
placeholder={dateInputPlaceholder}
valueFormat={dateInputFormat}
{...form.getInputProps('startDate')}
/>
<H>End time</H>
Expand All @@ -441,6 +457,8 @@ const TimePickerComponent = ({
maxDate={today}
minDate={form.values.startDate ?? undefined}
disabled={isRelative}
placeholder={dateInputPlaceholder}
valueFormat={dateInputFormat}
{...form.getInputProps('endDate')}
/>
</>
Expand All @@ -452,6 +470,8 @@ const TimePickerComponent = ({
popoverProps={dateComponentPopoverProps}
maxDate={today}
mb="xs"
placeholder={dateInputPlaceholder}
valueFormat={dateInputFormat}
{...form.getInputProps('startDate')}
/>
<H>Duration ±</H>
Expand Down
30 changes: 30 additions & 0 deletions packages/app/src/components/TimePicker/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,18 @@ describe('dateParser', () => {
const result = dateParser('Jan 16 22:00:01');
expect(result).toEqual(new Date('2024-01-16T22:00:01'));
});

it('parses date with milliseconds (YYYY-MM-DD HH:mm:ss.SSS)', () => {
const result = dateParser('2024-01-15 14:37:42.123');
expect(result).toEqual(new Date('2024-01-15T14:37:42.123'));
expect(result?.getMilliseconds()).toBe(123);
});

it('parses date with milliseconds (MMM d HH:mm:ss.SSS)', () => {
const result = dateParser('Jan 10 14:37:42.456');
expect(result).toEqual(new Date('2025-01-10T14:37:42.456'));
expect(result?.getMilliseconds()).toBe(456);
});
});

describe('parseTimeRangeInput', () => {
Expand Down Expand Up @@ -153,4 +165,22 @@ describe('parseTimeRangeInput', () => {
new Date('2024-01-16T12:00:00'),
]);
});

it('parses a range with milliseconds correctly', () => {
const [start, end] = parseTimeRangeInput(
'Jan 10 14:37:42.123 - Jan 10 15:37:42.456',
);
expect(start).toEqual(new Date('2025-01-10T14:37:42.123'));
expect(start?.getMilliseconds()).toBe(123);
expect(end).toEqual(new Date('2025-01-10T15:37:42.456'));
expect(end?.getMilliseconds()).toBe(456);
});

it('parses a range with YYYY-MM-DD and milliseconds', () => {
const [start, end] = parseTimeRangeInput(
'2024-01-15 14:37:42.100 - 2024-01-15 15:37:42.999',
);
expect(start?.getMilliseconds()).toBe(100);
expect(end?.getMilliseconds()).toBe(999);
});
});
27 changes: 19 additions & 8 deletions packages/app/src/timeQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,19 @@ import { usePrevious } from './utils';
const LIVE_TAIL_TIME_QUERY = 'Live Tail';
const LIVE_TAIL_REFRESH_INTERVAL_MS = 1000;

export const dateRangeToString = (range: [Date, Date], isUTC: boolean) => {
export const dateRangeToString = (
range: [Date, Date],
isUTC: boolean,
showMs = true,
) => {
const fmt = showMs ? 'withMs' : 'normal';
return `${formatDate(range[0], {
isUTC,
format: 'normal',
format: fmt,
clock: '24h',
})} - ${formatDate(range[1], {
isUTC,
format: 'normal',
format: fmt,
clock: '24h',
})}`;
};
Expand Down Expand Up @@ -385,6 +390,8 @@ export type UseTimeQueryInputType = {
showRelativeInterval?: boolean;
setDisplayedTimeInputValue?: (value: string) => void;
updateInput?: boolean;
/** When true, format displayed time strings with millisecond precision. */
showMs?: boolean;
};

export type UseTimeQueryReturnType = {
Expand Down Expand Up @@ -420,6 +427,7 @@ export function useNewTimeQuery({
showRelativeInterval,
setDisplayedTimeInputValue,
updateInput,
showMs = true,
}: UseTimeQueryInputType): UseTimeQueryReturnType {
const router = useRouter();
// We need to return true in SSR to prevent mismatch issues
Expand All @@ -433,7 +441,9 @@ export function useNewTimeQuery({
deprecatedDisplayedTimeInputValue,
deprecatedSetDisplayedTimeInputValue,
] = useState<string>(() => {
return initialDisplayValue ?? dateRangeToString(initialTimeRange, isUTC);
return (
initialDisplayValue ?? dateRangeToString(initialTimeRange, isUTC, showMs)
);
});

const _setDisplayedTimeInputValue =
Expand Down Expand Up @@ -472,14 +482,14 @@ export function useNewTimeQuery({
const relativeInterval =
showRelativeInterval && getRelativeInterval(start, end);
const dateRangeStr =
relativeInterval || dateRangeToString([start, end], isUTC);
relativeInterval || dateRangeToString([start, end], isUTC, showMs);
if (updateInput !== false) {
_setDisplayedTimeInputValue(dateRangeStr);
}
}
} else if (from == null && to == null && isReady) {
setSearchedTimeRange(initialTimeRange);
const dateRangeStr = dateRangeToString(initialTimeRange, isUTC);
const dateRangeStr = dateRangeToString(initialTimeRange, isUTC, showMs);
if (updateInput !== false) {
if (!showRelativeInterval) {
_setDisplayedTimeInputValue(dateRangeStr);
Expand All @@ -498,6 +508,7 @@ export function useNewTimeQuery({
showRelativeInterval,
_setDisplayedTimeInputValue,
updateInput,
showMs,
]);

return {
Expand All @@ -512,12 +523,12 @@ export function useNewTimeQuery({
(start: Date, end: Date, displayedTimeInputValue?: string | null) => {
setTimeRangeQuery({ from: start.getTime(), to: end.getTime() });
setSearchedTimeRange([start, end]);
const dateRangeStr = dateRangeToString([start, end], isUTC);
const dateRangeStr = dateRangeToString([start, end], isUTC, showMs);
if (displayedTimeInputValue !== null) {
_setDisplayedTimeInputValue(displayedTimeInputValue ?? dateRangeStr);
}
},
[setTimeRangeQuery, isUTC, _setDisplayedTimeInputValue],
[setTimeRangeQuery, isUTC, _setDisplayedTimeInputValue, showMs],
),
};
}
Expand Down
4 changes: 2 additions & 2 deletions packages/app/tests/e2e/components/TimePickerComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@
* Check if relative time mode is enabled
*/
async isRelativeTimeEnabled(): Promise<boolean> {
return await this.relativeTimeSwitch.isChecked();

Check failure on line 97 in packages/app/tests/e2e/components/TimePickerComponent.ts

View workflow job for this annotation

GitHub Actions / e2e-tests / E2E Tests - Shard 3

[chromium] › tests/e2e/features/search/relative-time.spec.ts:165:9 › Relative Time Picker › Live Mode Integration › should resume live tail with selected interval @relative-time

2) [chromium] › tests/e2e/features/search/relative-time.spec.ts:165:9 › Relative Time Picker › Live Mode Integration › should resume live tail with selected interval @relative-time › Pause live tail by selecting absolute time Error: locator.isChecked: Test timeout of 60000ms exceeded. Call log: - waiting for getByTestId('time-picker-relative-switch') at components/TimePickerComponent.ts:97 95 | */ 96 | async isRelativeTimeEnabled(): Promise<boolean> { > 97 | return await this.relativeTimeSwitch.isChecked(); | ^ 98 | } 99 | 100 | /** at TimePickerComponent.isRelativeTimeEnabled (/home/runner/work/hyperdx/hyperdx/packages/app/tests/e2e/components/TimePickerComponent.ts:97:42) at TimePickerComponent.disableRelativeTime (/home/runner/work/hyperdx/hyperdx/packages/app/tests/e2e/components/TimePickerComponent.ts:114:34) at /home/runner/work/hyperdx/hyperdx/packages/app/tests/e2e/features/search/relative-time.spec.ts:174:37 at /home/runner/work/hyperdx/hyperdx/packages/app/tests/e2e/features/search/relative-time.spec.ts:172:7

Check failure on line 97 in packages/app/tests/e2e/components/TimePickerComponent.ts

View workflow job for this annotation

GitHub Actions / e2e-tests / E2E Tests - Shard 3

[chromium] › tests/e2e/features/search/relative-time.spec.ts:53:9 › Relative Time Picker › Relative Time Options › should select different relative time intervals @relative-time

1) [chromium] › tests/e2e/features/search/relative-time.spec.ts:53:9 › Relative Time Picker › Relative Time Options › should select different relative time intervals @relative-time › Select Last 30 minutes Error: locator.isChecked: Test timeout of 60000ms exceeded. Call log: - waiting for getByTestId('time-picker-relative-switch') at components/TimePickerComponent.ts:97 95 | */ 96 | async isRelativeTimeEnabled(): Promise<boolean> { > 97 | return await this.relativeTimeSwitch.isChecked(); | ^ 98 | } 99 | 100 | /** at TimePickerComponent.isRelativeTimeEnabled (/home/runner/work/hyperdx/hyperdx/packages/app/tests/e2e/components/TimePickerComponent.ts:97:42) at TimePickerComponent.enableRelativeTime (/home/runner/work/hyperdx/hyperdx/packages/app/tests/e2e/components/TimePickerComponent.ts:104:34) at /home/runner/work/hyperdx/hyperdx/packages/app/tests/e2e/features/search/relative-time.spec.ts:66:39 at /home/runner/work/hyperdx/hyperdx/packages/app/tests/e2e/features/search/relative-time.spec.ts:64:20
}

/**
Expand Down Expand Up @@ -131,7 +131,7 @@
// already open from open(), and waiting for network idle can coincide
// with React re-renders of the popover content, causing the button to
// briefly detach from the DOM right before the click.
await intervalButton.waitFor({ state: 'visible', timeout: 5000 });

Check failure on line 134 in packages/app/tests/e2e/components/TimePickerComponent.ts

View workflow job for this annotation

GitHub Actions / e2e-tests / E2E Tests - Shard 3

[chromium] › tests/e2e/features/search/relative-time.spec.ts:287:9 › Relative Time Picker › Search Integration › should update search results when switching between intervals @relative-time

3) [chromium] › tests/e2e/features/search/relative-time.spec.ts:287:9 › Relative Time Picker › Search Integration › should update search results when switching between intervals @relative-time › Search with Last 15 minutes TimeoutError: locator.waitFor: Timeout 5000ms exceeded. Call log: - waiting for getByTestId('time-picker-popover').getByRole('button', { name: 'Last 15 minutes' }) to be visible at components/TimePickerComponent.ts:134 132 | // with React re-renders of the popover content, causing the button to 133 | // briefly detach from the DOM right before the click. > 134 | await intervalButton.waitFor({ state: 'visible', timeout: 5000 }); | ^ 135 | // Use a longer click timeout so Playwright can retry if the element 136 | // briefly detaches due to an ongoing render cycle. 137 | await intervalButton.click({ timeout: 10000 }); at TimePickerComponent.selectTimeInterval (/home/runner/work/hyperdx/hyperdx/packages/app/tests/e2e/components/TimePickerComponent.ts:134:26) at /home/runner/work/hyperdx/hyperdx/packages/app/tests/e2e/features/search/relative-time.spec.ts:299:39 at /home/runner/work/hyperdx/hyperdx/packages/app/tests/e2e/features/search/relative-time.spec.ts:297:9
// Use a longer click timeout so Playwright can retry if the element
// briefly detaches due to an ongoing render cycle.
await intervalButton.click({ timeout: 10000 });
Expand Down Expand Up @@ -183,14 +183,14 @@
* the single DateInput is also the first (and only).
*/
get startDateInput() {
return this.pickerPopover.getByPlaceholder('YYYY-MM-DD HH:mm:ss').nth(0);
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);
return this.pickerPopover.getByPlaceholder(/^YYYY-MM-DD HH:mm:ss/).nth(1);
}

/**
Expand Down
16 changes: 12 additions & 4 deletions packages/app/tests/e2e/features/search/custom-time-range.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,12 @@ test.describe('Custom Time Range', { tag: '@custom-time-range' }, () => {
});

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 expect(searchPage.timePicker.startDateInput).toHaveValue(
`${start}.000`,
);
await expect(searchPage.timePicker.endDateInput).toHaveValue(
`${end}.000`,
);
});

await test.step('Apply and confirm times propagate to the picker input', async () => {
Expand Down Expand Up @@ -73,8 +77,12 @@ test.describe('Custom Time Range', { tag: '@custom-time-range' }, () => {

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);
await expect(searchPage.timePicker.startDateInput).toHaveValue(
`${start}.000`,
);
await expect(searchPage.timePicker.endDateInput).toHaveValue(
`${end}.000`,
);
});
});

Expand Down
2 changes: 1 addition & 1 deletion packages/common-utils/src/core/eventDeltas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
*/
export function flattenData(data: Record<string, any>): Record<string, any> {
const result: Record<string, any> = {};
// eslint-disable-next-line security/detect-object-injection -- prop is built from known object keys via recursion, not user input

function recurse(cur: Record<string, any>, prop: string) {
if (Object(cur) !== cur) {
result[prop] = cur; // eslint-disable-line security/detect-object-injection
Expand Down
Loading