diff --git a/.changeset/popular-rabbits-argue.md b/.changeset/popular-rabbits-argue.md new file mode 100644 index 00000000..6b622363 --- /dev/null +++ b/.changeset/popular-rabbits-argue.md @@ -0,0 +1,6 @@ +--- +"@hyperdx/app": patch +"@hyperdx/common-utils": patch +--- + +fix: Preserve original select from time chart event selection diff --git a/packages/app/src/DBSearchPage.tsx b/packages/app/src/DBSearchPage.tsx index b9cfc6c3..a13dc9a2 100644 --- a/packages/app/src/DBSearchPage.tsx +++ b/packages/app/src/DBSearchPage.tsx @@ -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>( e => { diff --git a/packages/app/src/components/DBTimeChart.tsx b/packages/app/src/components/DBTimeChart.tsx index b8fa4e4b..e71ef52f 100644 --- a/packages/app/src/components/DBTimeChart.tsx +++ b/packages/app/src/components/DBTimeChart.tsx @@ -168,14 +168,21 @@ function DBTimeChartComponent({ where = config.select[0].aggCondition ?? ''; whereLanguage = config.select[0].aggConditionLanguage ?? 'lucene'; } - return new URLSearchParams({ + const params: Record = { 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({ }} > setActiveClickPayload(undefined)} diff --git a/packages/app/tests/e2e/features/search/search.spec.ts b/packages/app/tests/e2e/features/search/search.spec.ts index 8dd6f201..c62d2953 100644 --- a/packages/app/tests/e2e/features/search/search.spec.ts +++ b/packages/app/tests/e2e/features/search/search.spec.ts @@ -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); + }); + }); }); }); diff --git a/packages/common-utils/src/types.ts b/packages/common-utils/src/types.ts index 29c16c5d..ac817fe9 100644 --- a/packages/common-utils/src/types.ts +++ b/packages/common-utils/src/types.ts @@ -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.