Files
Saxon FletcherandClaude Opus 5 9b12165400 feat(studio): multi-select logs with click, shift, and keyboard ranges (#50689)
<img width="1454" height="786" alt="image"
src="https://github.com/user-attachments/assets/995790b1-a242-4a61-a0a0-349de7234923"
/>


Stack 4/4 · previous: #50688

## Problem

Selecting several logs to copy or send to the Assistant meant using a
separate checkbox column and a banner above the table. Clicking a row
and checking a row were two different selections.

## Solution

- Unified Logs uses `useTableRowSelection`, so a row click,
Cmd/Ctrl-click, Shift-click, and the checkbox all act on one selection.
The selection resets when the project or filters change, and a linked
`?id=` still restores its log.
- The level indicator and checkbox now share one column. The checkbox
shows on hover, focus, or when the row is selected.
- The detail panel shows every selected log: one log gets the tabs, and
two or more show their combined JSON. Copy-as-JSON and "Explain with AI"
move into the panel header (`LogSelectionActions`), replacing
`RowSelectionHeader`.
- New Shift+↑ / Shift+↓ shortcuts extend the selection, with a hint in
the panel footer. Prev/next navigation follows display order and keeps
the row in view.
- `LogTypeIcon` no longer puts a nested button inside clickable rows.

## Review instructions

1. Click a log, then Shift-click another. The range should be selected
and the panel should show the combined JSON.
2. Cmd-click to add or remove single logs. Use the copy button (or its
shortcut) and "Explain with AI".
3. With the panel open, press Shift+↑ / Shift+↓ to grow and shrink the
selection.
4. Change a filter. The selection should clear.

## Checklist

- [ ] I have read
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)


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

## Summary

* **New Features**
* Added multi-row selection in Unified Logs, including additive, range,
checkbox, and keyboard selection.
* Added bulk JSON viewing and copying, plus AI-assisted actions for
selected logs.
* Added Shift+Arrow shortcuts to extend selections and improved row
navigation.
* **Accessibility**
* Improved labels and focus behavior for log controls and log type
indicators.
* **Bug Fixes**
* Kept single-log Overview and Raw JSON views available alongside
multi-log selection.
* Applied metadata visibility settings to selected-log actions and
disabled actions for invalid log data.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-25 19:03:01 +10:00

129 lines
4.7 KiB
TypeScript

import { useState } from 'react'
import {
ResizableHandle,
ResizablePanel,
Tabs,
TabsContent,
TabsIndicator,
TabsList,
TabsTrigger,
} from 'ui'
import { CodeBlock } from 'ui-patterns/CodeBlock'
import { LogDetail } from './components/LogDetail'
import { LogLevelDot } from './components/LogLevelDot'
import { LogSelectionActions } from './LogSelectionActions'
import { ServiceFlowPanelControls } from './ServiceFlow/components/ServiceFlowPanelControls'
import { getLogDataForMetadataVisibility } from './ServiceFlowPanel.utils'
import { ColumnSchema } from './UnifiedLogs.schema'
import { QuerySearchParamsType } from './UnifiedLogs.types'
import { getEventMessageDisplay, getRawLogData } from './UnifiedLogs.utils'
import { ShortcutBadge } from '@/components/ui/ShortcutBadge'
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
interface ServiceFlowPanelProps {
dock: 'bottom' | 'right'
setDock: (value: 'bottom' | 'right') => void
selectedRows: ColumnSchema[]
searchParameters: QuerySearchParamsType
}
export function ServiceFlowPanel({
dock,
setDock,
selectedRows,
searchParameters,
}: ServiceFlowPanelProps) {
const [activeTab, setActiveTab] = useState('overview')
const hasMultiple = selectedRows.length > 1
const selectedRow = selectedRows[0]
const title = hasMultiple
? `${selectedRows.length} logs selected`
: getEventMessageDisplay(selectedRow?.log_type ?? '', selectedRow?.event_message).message ||
selectedRow?.id
const { logsMetadata } = useIsFeatureEnabled(['logs:metadata'])
const selectedJson = selectedRows.map((row) =>
getLogDataForMetadataVisibility(getRawLogData(row), logsMetadata)
)
return (
<>
<ResizableHandle withHandle />
<ResizablePanel
id="log-sidepanel"
defaultSize={400}
minSize={dock === 'bottom' ? 300 : 400}
className="bg-dash-sidebar"
>
<div className="flex h-full flex-col overflow-hidden">
<div className="flex min-w-0 items-center gap-6 px-4 py-2">
<div className="flex min-w-0 flex-1 items-center gap-2">
{!hasMultiple && (
<div className="flex h-4 w-4 shrink-0 items-center justify-center">
<LogLevelDot level={selectedRow?.level} />
</div>
)}
<span
className="min-w-0 flex-1 truncate heading-meta text-foreground"
role="status"
title={title}
>
{title}
</span>
</div>
<div className="flex shrink-0 items-center gap-1">
<LogSelectionActions rows={selectedRows} />
<ServiceFlowPanelControls dock={dock} setDock={setDock} />
</div>
</div>
{hasMultiple ? (
<div
className="min-h-0 flex-1 overflow-auto"
role="region"
aria-label="Selected logs JSON"
>
<CodeBlock
language="json"
hideCopy
wrapperClassName="!overflow-visible bg-surface-100/50 [&_pre]:!bg-surface-100/50"
className="rounded-none border-none !overflow-x-visible [&_code]:!leading-tight [&_pre]:!leading-tight"
>
{JSON.stringify(selectedJson, null, 2)}
</CodeBlock>
</div>
) : (
<Tabs
value={activeTab}
onValueChange={setActiveTab}
className="flex min-h-0 flex-1 flex-col"
>
<TabsList className="shrink-0 gap-x-4 px-4">
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="raw-json">Raw JSON</TabsTrigger>
<TabsIndicator />
</TabsList>
{['overview', 'raw-json'].map((tab) => (
<TabsContent
key={`${tab}-${selectedRow?.id}`}
value={tab}
className="mt-0 min-h-0 flex-1 overflow-auto"
>
{selectedRow && (
<LogDetail row={selectedRow} tab={tab} searchParameters={searchParameters} />
)}
</TabsContent>
))}
</Tabs>
)}
<div className="flex shrink-0 flex-wrap items-center gap-2 border-t px-4 py-2 text-xs text-foreground-lighter">
<ShortcutBadge shortcutId={SHORTCUT_IDS.UNIFIED_LOGS_EXTEND_PREV_ROW} />
<ShortcutBadge shortcutId={SHORTCUT_IDS.UNIFIED_LOGS_EXTEND_NEXT_ROW} />
<span>Extend selection</span>
</div>
</div>
</ResizablePanel>
</>
)
}