From 2ba2c37163b528ee23da48cd1d0655a5291871dc Mon Sep 17 00:00:00 2001 From: Jordi Enric <37541088+jordienr@users.noreply.github.com> Date: Tue, 4 Aug 2026 12:37:59 +0200 Subject: [PATCH] fix(studio): preserve deep-linked date range in Unified Logs FE-4020 (#48685) ## Problem Clicking a bar in a usage chart (e.g. the Postgres activity chart on the project dashboard) navigates to Unified Logs with the log_type filter applied correctly, but the clicked bar's time range is silently dropped: the page falls back to the default last-hour window. If the actual matching logs are outside that window, the main list shows "No results found" even though the sidebar facet count (computed from the correct deep-linked range) shows a nonzero count. Root cause: the table's initial `columnFilters` state was seeded only from the `filter` URL param, never from `date`. A debounced effect syncs `columnFilters` back into the URL shortly after mount, and for the `date` field it treats a missing `columnFilters` entry as a cleared brush, overwriting the deep-linked `date` param with null. ## Fix Added `buildDefaultColumnFilters` in `UnifiedLogs.filters.ts`, which seeds a `date` entry into the initial `columnFilters` from `search.date` when present, alongside the existing filter-param seeding. `UnifiedLogs.tsx` now uses this helper instead of building `defaultColumnFilters` inline, so a deep-linked range survives the debounced round-trip instead of getting nulled out. ## How to test - On the project dashboard, click a bar in a usage chart (e.g. Postgres activity) for a time period further back than the last hour. - Expected result: Unified Logs opens with both the log_type filter and the clicked bar's date range applied, and the row list matches the sidebar facet count instead of showing "No results found". - `UnifiedLogs.filters.test.ts` has unit tests covering the new seeding behavior. ## Summary by CodeRabbit * **Bug Fixes** * Improved log filtering from URL parameters. * Preserved valid date ranges when opening deep-linked log views. * Prevented malformed or duplicate date filters from appearing in the logs table. * **Tests** * Added coverage for valid, missing, malformed, and duplicate date filter scenarios. --- .../UnifiedLogs/UnifiedLogs.filters.test.ts | 44 +++++++++++++++++++ .../UnifiedLogs/UnifiedLogs.filters.ts | 13 ++++++ .../interfaces/UnifiedLogs/UnifiedLogs.tsx | 4 +- 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.filters.test.ts b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.filters.test.ts index 68538d1680d..d78b2f75b86 100644 --- a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.filters.test.ts +++ b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.filters.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest' import { + buildDefaultColumnFilters, buildFilterSearchUpdate, columnFiltersToLogsFilters, logsFiltersToColumnFilters, @@ -122,3 +123,46 @@ describe('buildFilterSearchUpdate', () => { expect(update.date).toBeNull() }) }) + +describe('buildDefaultColumnFilters', () => { + const fields = [ + { value: 'date', type: 'timerange' }, + { value: 'log_type', type: 'checkbox' }, + ] + + it('seeds a deep-linked `date` range so it survives the debounced sync back to the URL', () => { + const range = [new Date('2026-05-08T00:00:00Z'), new Date('2026-05-08T01:00:00Z')] + const columnFilters = buildDefaultColumnFilters({ + filter: ['log_type:eq:postgres'], + date: range, + }) + expect(columnFilters).toEqual([ + { id: 'log_type', value: ['postgres'] }, + { id: 'date', value: range }, + ]) + + // Regression guard: without the `date` entry above, this would null out the range. + const update = buildFilterSearchUpdate(columnFilters, fields) + expect(update.date).toBe(range) + }) + + it('omits `date` when no range is present, matching the pre-existing no-filter case', () => { + expect(buildDefaultColumnFilters({ filter: ['log_type:eq:postgres'], date: null })).toEqual([ + { id: 'log_type', value: ['postgres'] }, + ]) + }) + + it('omits `date` for a malformed single-element range', () => { + const columnFilters = buildDefaultColumnFilters({ + filter: ['log_type:eq:postgres'], + date: [new Date('2026-05-08T00:00:00Z')], + }) + expect(columnFilters).toEqual([{ id: 'log_type', value: ['postgres'] }]) + }) + + it('does not duplicate the `date` id when a hand-crafted `filter` param also targets it', () => { + const range = [new Date('2026-05-08T00:00:00Z'), new Date('2026-05-08T01:00:00Z')] + const columnFilters = buildDefaultColumnFilters({ filter: ['date:eq:123'], date: range }) + expect(columnFilters).toEqual([{ id: 'date', value: range }]) + }) +}) diff --git a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.filters.ts b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.filters.ts index 32614bf9088..92a4549d208 100644 --- a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.filters.ts +++ b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.filters.ts @@ -89,6 +89,19 @@ export const logsFiltersToColumnFilters = ( ) } +// Seeds `date` too, so `logsFiltersToColumnFilters` (which only covers the `filter` +// param) doesn't leave it out and get nulled by the debounced sync back to `search`. +export const buildDefaultColumnFilters = (search: { + filter?: string[] | null + date?: Date[] | null +}): { id: string; value: unknown }[] => { + const filters: { id: string; value: unknown }[] = logsFiltersToColumnFilters( + parseLogsFilterUrlParams(search.filter) + ).filter((f) => f.id !== 'date') + if (search.date?.length === 2) filters.push({ id: 'date', value: search.date }) + return filters +} + export const columnFiltersToLogsFilters = ( columnFilters: { id: string; value: unknown }[], filterableNames?: Set diff --git a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.tsx b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.tsx index 7981955ec75..d9f4ed0f001 100644 --- a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.tsx +++ b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.tsx @@ -38,8 +38,8 @@ import { ServiceFlowPanel } from './ServiceFlowPanel' import { SEARCH_PARAMS_PARSER } from './UnifiedLogs.constants' import { filterFields as defaultFilterFields } from './UnifiedLogs.fields' import { + buildDefaultColumnFilters, buildFilterSearchUpdate, - logsFiltersToColumnFilters, parseLogsFilterUrlParams, } from './UnifiedLogs.filters' import { useLiveMode, useResetFocus } from './UnifiedLogs.hooks' @@ -96,7 +96,7 @@ export const UnifiedLogs = () => { const defaultColumnSorting = search.sort ? [search.sort] : [] const defaultColumnVisibility = { uuid: false } - const defaultColumnFilters = logsFiltersToColumnFilters(parseLogsFilterUrlParams(search.filter)) + const defaultColumnFilters = buildDefaultColumnFilters(search) const [topBarHeight, setTopBarHeight] = useState(0) const topBarRef = useRef(null)