fix: Preserve original select from time chart event selection (#1277)

FIxes: HDX-2606
This commit is contained in:
Tom Alexander 2025-10-17 12:57:01 -04:00 committed by GitHub
parent 6262ced8ce
commit 658728318b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 113 additions and 3 deletions

View file

@ -0,0 +1,6 @@
---
"@hyperdx/app": patch
"@hyperdx/common-utils": patch
---
fix: Preserve original select from time chart event selection

View file

@ -1145,9 +1145,17 @@ function DBSearchPage() {
dateRange: searchedTimeRange,
displayType: DisplayType.StackedBar,
with: aliasWith,
// Preserve the original table select string for "View Events" links
eventTableSelect: searchedConfig.select,
...variableConfig,
};
}, [chartConfig, searchedSource, aliasWith, searchedTimeRange]);
}, [
chartConfig,
searchedSource,
aliasWith,
searchedTimeRange,
searchedConfig.select,
]);
const onFormSubmit = useCallback<FormEventHandler<HTMLFormElement>>(
e => {

View file

@ -168,14 +168,21 @@ function DBTimeChartComponent({
where = config.select[0].aggCondition ?? '';
whereLanguage = config.select[0].aggConditionLanguage ?? 'lucene';
}
return new URLSearchParams({
const params: Record<string, string> = {
source: (isMetricChart ? source?.logSourceId : source?.id) ?? '',
where: where,
whereLanguage: whereLanguage,
filters: JSON.stringify(config.filters),
from: from.toString(),
to: to.toString(),
});
};
// Include the select parameter if provided to preserve custom columns
// eventTableSelect is used for charts that override select (like histograms with count)
// to preserve the original table's select expression
if (config.eventTableSelect) {
params.select = config.eventTableSelect;
}
return new URLSearchParams(params);
}, [clickedActiveLabelDate, config, granularity, source]);
return isLoading && !data ? (
@ -269,6 +276,7 @@ function DBTimeChartComponent({
}}
>
<Link
data-testid="chart-view-events-link"
href={`/search?${qparams?.toString()}`}
className="text-white-hover text-decoration-none"
onClick={() => setActiveClickPayload(undefined)}

View file

@ -259,5 +259,91 @@ test.describe('Search', { tag: '@search' }, () => {
expect(rowCount).toBeGreaterThan(0);
});
});
test('Histogram drag-to-zoom preserves custom SELECT columns', async ({
page,
}) => {
const CUSTOM_SELECT =
'Timestamp, ServiceName, Body as message, SeverityText';
await test.step('Perform initial search', async () => {
await expect(page.locator('[data-testid="search-form"]')).toBeVisible();
await page.locator('[data-testid="search-submit-button"]').click();
await page.waitForLoadState('networkidle');
});
await test.step('Setup custom SELECT columns', async () => {
// The SELECT field is the first CodeMirror editor (index 0)
const selectEditor = page.locator('.cm-content').first();
await expect(selectEditor).toBeVisible();
// Select all and replace with custom columns
await selectEditor.click({ clickCount: 3 });
await page.keyboard.type(CUSTOM_SELECT);
});
await test.step('Search with custom columns and wait for histogram', async () => {
await page.locator('[data-testid="search-submit-button"]').click();
await page.waitForLoadState('networkidle');
// Wait for histogram to render with data
await expect(
page.locator('.recharts-responsive-container').first(),
).toBeVisible();
});
await test.step('Drag on histogram to select time range', async () => {
const chartSurface = page.locator('.recharts-surface').first();
await expect(chartSurface).toBeVisible();
const box = await chartSurface.boundingBox();
expect(box).toBeTruthy();
// Drag from 25% to 75% of chart width to zoom into a time range
const startX = box!.x + box!.width * 0.25;
const endX = box!.x + box!.width * 0.75;
const y = box!.y + box!.height / 2;
await page.mouse.move(startX, y);
await page.mouse.down();
await page.mouse.move(endX, y, { steps: 10 });
await page.mouse.up();
// Wait for the zoom operation to complete
await page.waitForLoadState('networkidle');
});
await test.step('Verify custom SELECT columns are preserved', async () => {
// Check URL parameters
const url = page.url();
expect(url, 'URL should contain select parameter').toContain('select=');
expect(url, 'URL should contain alias "message"').toContain('message');
// Verify SELECT editor content
const selectEditor = page.locator('.cm-content').first();
await expect(selectEditor).toBeVisible();
const selectValue = await selectEditor.textContent();
expect(selectValue, 'SELECT should contain alias').toContain(
'Body as message',
);
expect(selectValue, 'SELECT should contain SeverityText').toContain(
'SeverityText',
);
});
await test.step('Verify search results are still displayed', async () => {
const searchResultsTable = page.locator(
'[data-testid="search-results-table"]',
);
await expect(
searchResultsTable,
'Search results table should be visible',
).toBeVisible();
const rowCount = await searchResultsTable.locator('tr').count();
expect(rowCount, 'Should have search results').toBeGreaterThan(0);
});
});
});
});

View file

@ -371,6 +371,8 @@ export const _ChartConfigSchema = z.object({
selectGroupBy: z.boolean().optional(),
metricTables: MetricTableSchema.optional(),
seriesReturnType: z.enum(['ratio', 'column']).optional(),
// Used to preserve original table select string when chart overrides it (e.g., histograms)
eventTableSelect: z.string().optional(),
});
// This is a ChartConfig type without the `with` CTE clause included.