mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 09:55:06 +03:00
<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>
142 lines
4.3 KiB
TypeScript
142 lines
4.3 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
|
|
import { buildLogsPrompt, formatLogsAsJson } from '@/components/interfaces/Settings/Logs/Logs.utils'
|
|
import { parseSelectedLogs } from '@/components/interfaces/UnifiedLogs/LogSelectionActions.utils'
|
|
import type { ColumnSchema } from '@/components/interfaces/UnifiedLogs/UnifiedLogs.schema'
|
|
import { mapUnifiedLogRow, parseUnifiedLogsQueryRows } from '@/data/logs/unified-logs.utils'
|
|
|
|
const row: ColumnSchema = {
|
|
id: 'log-id',
|
|
log_type: 'realtime',
|
|
timestamp: 1_000_000,
|
|
date: new Date(1000),
|
|
event_message: 'Connection opened',
|
|
method: null,
|
|
pathname: null,
|
|
level: null,
|
|
status: null,
|
|
}
|
|
|
|
describe('parseSelectedLogs', () => {
|
|
it.each(['edge', 'auth', 'compute', 'realtime'])(
|
|
'accepts mapped %s logs with numeric and OTEL timestamps',
|
|
(logType) => {
|
|
const timestamps = [
|
|
1788424716876000,
|
|
'1788424716876000',
|
|
'2026-09-03T09:58:36.876000',
|
|
'2026-09-03T09:58:36.876Z',
|
|
]
|
|
const rows = parseUnifiedLogsQueryRows(
|
|
timestamps.map((timestamp) => ({
|
|
id: `${logType}-${timestamp}`,
|
|
log_type: logType,
|
|
timestamp,
|
|
event_message: 'Connection opened',
|
|
method: null,
|
|
pathname: null,
|
|
status: '200',
|
|
level: null,
|
|
log_count: null,
|
|
logs: null,
|
|
}))
|
|
).map(mapUnifiedLogRow)
|
|
|
|
const result = parseSelectedLogs(rows, true)
|
|
|
|
expect(result.success).toBe(true)
|
|
if (!result.success) throw result.error
|
|
expect(result.data.map((log) => log.timestamp)).toEqual(timestamps)
|
|
expect(JSON.parse(formatLogsAsJson(result.data))).toHaveLength(timestamps.length)
|
|
expect(buildLogsPrompt(result.data)).toContain('Connection opened')
|
|
if (logType !== 'compute') expect(result.data[0].log_count).toBeNull()
|
|
}
|
|
)
|
|
|
|
it.each([false, true])('preserves log fields with metadata visibility %s', (metadataVisible) => {
|
|
const input = {
|
|
...row,
|
|
metadata: { host: 'host' },
|
|
raw_log_data: { message: 'original', metadata: { request_id: 'request' } },
|
|
custom_field: 'custom',
|
|
}
|
|
|
|
const result = parseSelectedLogs([input], metadataVisible)
|
|
|
|
expect(result).toMatchObject({
|
|
success: true,
|
|
data: [
|
|
{
|
|
...input,
|
|
metadata: metadataVisible ? input.metadata : undefined,
|
|
raw_log_data: {
|
|
message: 'original',
|
|
metadata: metadataVisible ? input.raw_log_data.metadata : undefined,
|
|
},
|
|
},
|
|
],
|
|
})
|
|
expect(input.raw_log_data.metadata).toEqual({ request_id: 'request' })
|
|
expect(input.metadata).toEqual({ host: 'host' })
|
|
})
|
|
|
|
it.each([false, true])(
|
|
'normalizes compute logs with metadata visibility %s',
|
|
(metadataVisible) => {
|
|
const result = parseSelectedLogs(
|
|
[{ ...row, log_type: 'compute', event_message: undefined, metadata: { host: 'host' } }],
|
|
metadataVisible
|
|
)
|
|
|
|
expect(result).toEqual({
|
|
success: true,
|
|
data: [
|
|
{
|
|
id: row.id,
|
|
timestamp: row.timestamp,
|
|
event_message: '',
|
|
metadata: metadataVisible ? { host: 'host' } : undefined,
|
|
},
|
|
],
|
|
})
|
|
}
|
|
)
|
|
|
|
it('accepts an empty selection', () => {
|
|
expect(parseSelectedLogs([], true)).toEqual({ success: true, data: [] })
|
|
})
|
|
|
|
it('normalizes nullable metadata', () => {
|
|
expect(parseSelectedLogs([{ ...row, metadata: null }], true)).toMatchObject({
|
|
success: true,
|
|
data: [{ metadata: undefined }],
|
|
})
|
|
})
|
|
|
|
it.each([
|
|
null,
|
|
{},
|
|
[null],
|
|
[{ ...row, id: undefined }],
|
|
[{ ...row, event_message: 123 }],
|
|
[{ ...row, log_type: 'unknown' }],
|
|
[{ ...row, metadata: 'invalid' }],
|
|
[{ ...row, timestamp: true }],
|
|
[{ ...row, timestamp: 'invalid timestamp' }],
|
|
[{ ...row, timestamp: '' }],
|
|
])('returns a validation error for malformed input: %j', (input) => {
|
|
expect(parseSelectedLogs(input, false).success).toBe(false)
|
|
})
|
|
|
|
it('validates the transformed output and rejects the whole selection', () => {
|
|
const result = parseSelectedLogs([row, { ...row, timestamp: Infinity }], true)
|
|
|
|
expect(result.success).toBe(false)
|
|
if (!result.success) {
|
|
expect(result.error.issues).toEqual([
|
|
expect.objectContaining({ code: 'not_finite', path: [1, 'timestamp'] }),
|
|
])
|
|
}
|
|
})
|
|
})
|