@@ -345,7 +347,7 @@ export default function DashboardBuilder({ project }: DashboardBuilderProps) {
navigate(-1)}
+ onClick={() => goBack()}
>
Cancel
diff --git a/web/src/pages/IpGeolocationDetail.tsx b/web/src/pages/IpGeolocationDetail.tsx
index 99f98bfb2..f1ad36fb9 100644
--- a/web/src/pages/IpGeolocationDetail.tsx
+++ b/web/src/pages/IpGeolocationDetail.tsx
@@ -1,5 +1,6 @@
import { useEffect } from 'react'
-import { useParams, useNavigate } from 'react-router'
+import { useParams } from 'react-router'
+import { useGoBack } from '@/hooks/useGoBack'
import { useQuery } from '@tanstack/react-query'
import { getIpGeolocationOptions } from '@/api/client/@tanstack/react-query.gen'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
@@ -13,7 +14,7 @@ import { usePageTitle } from '@/hooks/usePageTitle'
export default function IpGeolocationDetail() {
const { ip } = useParams<{ ip: string }>()
- const navigate = useNavigate()
+ const goBack = useGoBack('/proxy-logs')
const { setBreadcrumbs } = useBreadcrumbs()
usePageTitle(`IP Geolocation - ${ip}`)
@@ -36,7 +37,7 @@ export default function IpGeolocationDetail() {
}, [setBreadcrumbs])
const handleBack = () => {
- navigate(-1)
+ goBack()
}
if (error) {
diff --git a/web/src/pages/MetricAlertForm.tsx b/web/src/pages/MetricAlertForm.tsx
index ac87fca50..e45f12117 100644
--- a/web/src/pages/MetricAlertForm.tsx
+++ b/web/src/pages/MetricAlertForm.tsx
@@ -93,6 +93,7 @@ import {
import { useEffect, useMemo, useState } from 'react'
import { useForm } from 'react-hook-form'
import { Link, useNavigate, useParams } from 'react-router'
+import { useGoBack } from '@/hooks/useGoBack'
import { toast } from 'sonner'
import { z } from 'zod'
@@ -215,6 +216,7 @@ interface AlertFormBodyProps {
*/
function AlertFormBody({ project, isEditing, id, existing }: AlertFormBodyProps) {
const navigate = useNavigate()
+ const goBack = useGoBack(`/projects/${project.slug}/metrics/alerts`)
const queryClient = useQueryClient()
const namesQuery = useQuery({
@@ -417,7 +419,7 @@ function AlertFormBody({ project, isEditing, id, existing }: AlertFormBodyProps)
return key === 'listAlerts' || key === 'getAlert'
},
})
- navigate(-1)
+ goBack()
},
})
@@ -492,7 +494,7 @@ function AlertFormBody({ project, isEditing, id, existing }: AlertFormBodyProps)
return (
-
navigate(-1)}>
+ goBack()}>
@@ -1323,7 +1325,7 @@ function AlertFormBody({ project, isEditing, id, existing }: AlertFormBodyProps)
? 'Save changes'
: 'Create alert'}
- navigate(-1)}>
+ goBack()}>
Cancel
diff --git a/web/src/pages/TraceDetail.tsx b/web/src/pages/TraceDetail.tsx
index 6e7bde896..c88748e41 100644
--- a/web/src/pages/TraceDetail.tsx
+++ b/web/src/pages/TraceDetail.tsx
@@ -65,6 +65,7 @@ import {
} from 'lucide-react'
import { useCallback, useMemo, type ReactNode } from 'react'
import { Link, useNavigate, useParams, useSearchParams } from 'react-router'
+import { useGoBack } from '@/hooks/useGoBack'
interface TraceDetailProps {
project: ProjectResponse
@@ -476,6 +477,7 @@ function CrossProjectBar({
export default function TraceDetail({ project }: TraceDetailProps) {
const { traceId } = useParams()
const navigate = useNavigate()
+ const goBack = useGoBack(`/projects/${project.slug}/traces`)
const { data, isLoading, isFetching, error, refetch } = useQuery({
...getTraceOptions({
@@ -673,7 +675,7 @@ export default function TraceDetail({ project }: TraceDetailProps) {
navigate(-1)}
+ onClick={() => goBack()}
className="gap-2"
>
@@ -700,7 +702,7 @@ export default function TraceDetail({ project }: TraceDetailProps) {
navigate(-1)}
+ onClick={() => goBack()}
className="gap-2"
>
@@ -724,7 +726,7 @@ export default function TraceDetail({ project }: TraceDetailProps) {
navigate(-1)}
+ onClick={() => goBack()}
className="shrink-0 gap-2"
>
From afa8457835f70a1ca0bfb19be2a3767978ae67c0 Mon Sep 17 00:00:00 2001
From: David Viejo
Date: Thu, 6 Aug 2026 13:51:35 +0200
Subject: [PATCH 5/6] revert(web): drop the data-browser tree navigateTo
routing
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Rebasing onto main brought in the data-browser redesign (#557), which had
already fixed the stale-sort symptom independently: effectiveSortField
validates the sort column against the entity's own field names and falls
back to unsorted when it doesn't match.
Routing the three tree-selection handlers through navigateTo on top of that
collapsed the tree on entity selection — verified by running the same
switch-tables flow against unmodified origin/main (passes) and against this
branch (fails, 'public' collapses so sibling tables are unreachable). Since
the reported bug is already fixed upstream, reverting is strictly better
than shipping a working fix plus a new regression.
The residual gap is the stale *filter*, which still carries across a table
switch; that needs a fix derived from the redesigned navigation and is left
as a follow-up rather than guessed at here.
---
web/src/pages/ServiceDataBrowser.tsx | 28 +++++++++++++++++-----------
1 file changed, 17 insertions(+), 11 deletions(-)
diff --git a/web/src/pages/ServiceDataBrowser.tsx b/web/src/pages/ServiceDataBrowser.tsx
index 468a4b649..53e95ea7c 100644
--- a/web/src/pages/ServiceDataBrowser.tsx
+++ b/web/src/pages/ServiceDataBrowser.tsx
@@ -1311,10 +1311,10 @@ export function ServiceDataBrowser() {
!hasLoadedChildren
if (isLeafContainer) {
- // navigateTo (not a bare setSearchParams) so the previous container's
- // sort column and filter don't follow us to a container that has no
- // such field.
- navigateTo(node.path)
+ // Update URL params - use replace to avoid page reload
+ setSearchParams({ path: node.path }, { replace: true })
+ setPage(1)
+ commitActiveTab({ path: node.path, entity: undefined, page: 1 })
// Don't expand in tree, just select it
// The main content area will show the entities table via ContainerEntitiesView
@@ -1357,7 +1357,9 @@ export function ServiceDataBrowser() {
}
} else {
// Different container - select it and expand if not already expanded
- navigateTo(node.path)
+ setSearchParams({ path: node.path }, { replace: true })
+ setPage(1)
+ commitActiveTab({ path: node.path, entity: undefined, page: 1 })
// If not currently expanded, expand it
if (!isCurrentlyExpanded) {
@@ -1385,13 +1387,17 @@ export function ServiceDataBrowser() {
}
}
} else if (node.type === 'entity') {
- // Switching tables must drop the outgoing table's sort column and
- // filter: they name fields the incoming table may not have, and the
- // query then sorts/filters on something that doesn't exist. navigateTo
- // resets sort/filter/page for the new target; a bare setSearchParams
- // only moved the selection and left both behind.
+ // Update URL params for entity selection - use replace to avoid page reload
const parentPath = node.path.split('/').slice(0, -1).join('/')
- navigateTo(parentPath, node.name)
+ setSearchParams(
+ {
+ path: parentPath,
+ entity: node.name,
+ },
+ { replace: true }
+ )
+ setPage(1)
+ commitActiveTab({ path: parentPath, entity: node.name, page: 1 })
// Close sidebar on mobile when selecting an entity
if (window.innerWidth < 768) {
From 9ef9d3ad13699121276cbfa811c6d2692c0f078b Mon Sep 17 00:00:00 2001
From: David Viejo
Date: Thu, 6 Aug 2026 13:55:57 +0200
Subject: [PATCH 6/6] fix(web): address self-review findings on the shortcut
hook and project dot
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- useKeyboardShortcut re-registered its keydown listener on every render:
callers pass an inline arrow, so `callback` was a new identity each time
and the effect tore the listener down and re-added it. Held in a ref
instead. Affects all 24 call sites, including the 14 that predate this
branch.
- ProjectDot carried an aria-label on a role-less span, which most screen
readers ignore — with the slug text now gone that left the project
unreadable. Added role="img".
- resizable.tsx used the React UMD global for a type; import it explicitly.
---
web/src/components/traces/ProjectBadge.tsx | 4 ++++
web/src/components/ui/resizable.tsx | 5 +++--
web/src/hooks/useKeyboardShortcut.ts | 18 ++++++++++++++----
3 files changed, 21 insertions(+), 6 deletions(-)
diff --git a/web/src/components/traces/ProjectBadge.tsx b/web/src/components/traces/ProjectBadge.tsx
index d8888b680..45bf1a365 100644
--- a/web/src/components/traces/ProjectBadge.tsx
+++ b/web/src/components/traces/ProjectBadge.tsx
@@ -73,6 +73,10 @@ export function ProjectDot({
className={cn('h-2.5 w-2.5 shrink-0 rounded-full', className)}
style={{ backgroundColor: projectColor(projectId) }}
title={name}
+ // role="img" so the label is actually announced — an aria-label on a
+ // role-less generic element is ignored by most screen readers, which
+ // would leave the project unreadable once the slug text is gone.
+ role="img"
aria-label={`Project: ${name}`}
/>
)
diff --git a/web/src/components/ui/resizable.tsx b/web/src/components/ui/resizable.tsx
index e7ab393d4..9d90786e0 100644
--- a/web/src/components/ui/resizable.tsx
+++ b/web/src/components/ui/resizable.tsx
@@ -1,3 +1,4 @@
+import type { ComponentProps } from 'react'
import { GripVertical } from 'lucide-react'
import { Group, Panel, Separator } from 'react-resizable-panels'
import { cn } from '@/lib/utils'
@@ -13,7 +14,7 @@ import { cn } from '@/lib/utils'
function ResizablePanelGroup({
className,
...props
-}: React.ComponentProps) {
+}: ComponentProps) {
return (
& { withHandle?: boolean }) {
+}: ComponentProps & { withHandle?: boolean }) {
return (
setOpen(true)}`), so a new
+ // identity every render. Keeping it in a ref stops the effect from tearing
+ // the keydown listener down and re-adding it on each render. The write is
+ // in an effect, not in render — mutating a ref during render is unsafe
+ // under concurrent rendering.
+ const callbackRef = useRef(callback)
+ useEffect(() => {
+ callbackRef.current = callback
+ }, [callback])
+
useEffect(() => {
if (!enabled) return
@@ -79,8 +89,8 @@ export function useKeyboardShortcut({
) {
e.preventDefault()
- if (callback) {
- callback()
+ if (callbackRef.current) {
+ callbackRef.current()
} else if (path) {
navigate(path)
}
@@ -89,5 +99,5 @@ export function useKeyboardShortcut({
document.addEventListener('keydown', handleKeyDown)
return () => document.removeEventListener('keydown', handleKeyDown)
- }, [key, path, callback, enabled, navigate])
+ }, [key, path, enabled, navigate])
}