Files
supabase/apps/studio/components/interfaces/QueryPerformance/QueryPerformanceMetrics.tsx
T
Danny White e3d7267845 fix(studio): chip away explicit-tabindex ratchet debt (#48040)
## What kind of change does this PR introduce?

A11y cleanup follow-up to #47984 /
[DEPR-626](https://linear.app/supabase/issue/DEPR-626).

## What is the current behavior?

Studio had 82 ratcheted `supabase/require-explicit-tabindex` violations
(raw `<button>` / `role="button"` without explicit `tabIndex`).

## What is the new behavior?

- Explicit `tabIndex={0}` (or disabled → `-1`) on those Studio call
sites across nav, `components/ui`, Database, Storage, and the remainder
- Ratchet baseline cleared (**82 → 0**) and the rule **removed from the
Studio ratchet** (debt is gone; ratchet is temporary)
- Rule remains a shared **`warn`** for now — promoting to `error` (and
sweeping www/docs/design-system) is a follow-up
- Also fixed the learn/ui-library call sites that surfaced while
experimenting with error promotion
- Small follow-ups where making controls focusable exposed gaps:
accessible names, disabled/focus consistency, focus-ring polish on
To-test surfaces, home section `KeyboardSensor`, and an E2E locator
tightened after `aria-label="Remove column"`

Prefer migrating to `Button` from `ui` in future touch-ups; this PR
takes the minimal path so Studio debt can stay at zero.

## Additional context

Batches landed together so baseline conflicts stayed simple while
chipping away:

- Hotspots / nav (FirstLevelNav, Marketplace, AttachmentUpload, Column,
Tabs, …)
- `components/ui` shared
- Database + Storage
- Remainder

**Out of scope / intentional deferrals**

- Promoting `supabase/require-explicit-tabindex` to a lint **error**
(follow-up after www/docs/design-system sweeps)
- Tabs/Radio roving, tooltips, context menus, in-menu items
- Full keyboard-accessible tab-close UX (close stays hover +
`tabIndex={-1}`; context menu still closes tabs)
- Data API docs links (`/project/<ref>/api` redirect)

**Reviewer notes**

- Rule only flags raw `<button>` / `role="button"` without a `tabIndex`
prop. `Button` from `ui` already bakes this in
- `tabIndex={-1}` is intentional for disabled controls, in-menu /
roving-focus children, and hover-only tab close
- For dnd-kit grips, put `tabIndex` **after** `{...attributes}` so it
isn’t overwritten (TS2783)

### To test

Use **Safari** with macOS Keyboard navigation **off** (System Settings →
Keyboard). Chrome once for a sanity pass. For each surface below: Tab
until the control is focused, then activate with Enter/Space where
relevant.

1. **API Docs side panel** (Table Editor → open a table → **API docs**)
- Floating API Docs panel — **not** `/project/<ref>/api` (that redirects
to Data API docs; language ToggleGroup uses arrow keys; links are out of
scope)
- Left nav buttons — Tab through several and activate one; active
highlight / navigation still works

2. **Integrations → Marketplace**
- Enable **Integrations layout** feature preview first (avatar menu →
Feature previews)
   - `/org/<slug>/integrations` or project integrations marketplace
   - “Clear all”, grid/list toggles — Tab + activate

3. **Table Editor → create a table → Columns**
- Drag handles only appear while **creating** (not when editing an
existing table)
   - Tab to grip / remove (X) / sensitive-data eye if shown

4. **Project Home** — section drag handles
   - Tab to a grip (visible focus ring)
- Optional: Space to pick up, arrows to move, Space/Esc to drop
(KeyboardSensor added)
   - Mouse dnd still works

5. **Storage → Policies** — expand/collapse bucket list chevron
(design-system focus ring, no stuck grey open bg)

6. **Support form** (Help → Support) — attachment remove (×) and
add-attachment control when visible

Disabled controls should be **skipped** by Tab.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Accessibility Improvements**
* Improved keyboard navigation throughout Studio by explicitly managing
focus (`tabIndex`) across many interactive controls (menus, tabs,
tables, charts, dialogs, navigation, and form actions).
* Disabled or non-interactive controls are now removed from the tab
order (or made unfocusable), while available actions remain reachable.
* Ensured `type="button"` on relevant controls to prevent unintended
submissions, and refined keyboard focus behavior for various toggles and
copy/remove actions.
* **Chores**
* Updated the ESLint rule baseline configuration to match the new focus
behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-21 08:22:43 +10:00

118 lines
4.9 KiB
TypeScript

import { Info } from 'lucide-react'
import { parseAsJson, useQueryStates } from 'nuqs'
import React, { useMemo } from 'react'
import { cn, Skeleton, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
import { useQueryPerformanceQuery } from './useQueryPerformanceQuery'
import { NumericFilter } from '@/components/interfaces/Reports/v2/ReportsNumericFilter'
export const QueryPerformanceMetrics = () => {
const { data: queryMetrics, isLoading } = useQueryPerformanceQuery({ preset: 'queryMetrics' })
const [, setSearchParams] = useQueryStates({
totalTimeFilter: parseAsJson<NumericFilter | null>((value) =>
value === null || value === undefined ? null : (value as NumericFilter)
),
})
const stats = useMemo(() => {
const slowQueriesTitle = queryMetrics?.[0]?.slow_queries === 1 ? 'Slow Query' : 'Slow Queries'
const slowQueriesValue = queryMetrics?.[0]?.slow_queries || '0'
return [
{
title: slowQueriesTitle,
value: slowQueriesValue,
onClick: () => {
setSearchParams({
totalTimeFilter: {
operator: '>',
value: 1000,
} as NumericFilter,
})
},
},
{
title: 'Cache Hit Rate',
value: queryMetrics?.[0]?.cache_hit_rate || '0%',
tooltip:
'Percentage of data read from cache vs disk. Higher is better - it means faster queries and less database load.',
},
{
title: 'Avg. Rows Per Call',
value: queryMetrics?.[0]?.avg_rows_per_call || '0',
tooltip:
'Average number of rows returned per query execution. Helps identify queries that return too much or too little data.',
},
]
}, [queryMetrics, setSearchParams])
return (
<section className="px-6 pt-2 pb-4 flex flex-wrap gap-x-6 gap-y-2 w-full">
{stats.map((card, i) => (
<React.Fragment key={i}>
<div
className={cn('flex items-baseline gap-2 heading-subSection text-foreground-light', {
'cursor-pointer hover:text-foreground transition-colors': card.onClick,
})}
onClick={card.onClick}
>
{isLoading ? (
<Skeleton className="h-5 w-24" />
) : (
<>
<span className="text-foreground">{card.value}</span>
<span className="flex items-center gap-1">
{card.title}
{(card.title === 'Slow Queries' || card.title === 'Slow Query') && (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
tabIndex={0}
aria-label="How are slow queries calculated?"
className="inline-flex h-4 w-4 items-center justify-center rounded-full bg-surface-200 text-foreground-lighter transition-colors hover:bg-surface-300 hover:text-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-foreground-lighter"
onClick={(e) => {
e.stopPropagation()
}}
>
<Info size={12} />
</button>
</TooltipTrigger>
<TooltipContent side="top" align="start" className="max-w-xs text-xs">
Slow queries are those with total execution time (execution time + planning
time) greater than 1000ms.
</TooltipContent>
</Tooltip>
)}
{card.tooltip && (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
tabIndex={0}
aria-label={`What is ${card.title}?`}
className="inline-flex h-4 w-4 items-center justify-center rounded-full bg-surface-200 text-foreground-lighter transition-colors hover:bg-surface-300 hover:text-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-foreground-lighter"
onClick={(e) => {
e.stopPropagation()
}}
>
<Info size={12} />
</button>
</TooltipTrigger>
<TooltipContent side="top" align="start" className="max-w-xs text-xs">
{card.tooltip}
</TooltipContent>
</Tooltip>
)}
</span>
</>
)}
</div>
{i < stats.length - 1 && <span className="text-foreground-muted">/</span>}
</React.Fragment>
))}
</section>
)
}