Files
Saxon FletcherandClaude Opus 5 71a252a445 feat(studio): show overview and raw JSON tabs for every log (#50687)
<img width="1086" height="810" alt="image"
src="https://github.com/user-attachments/assets/8a9f0c64-145e-4475-a3df-67bd815e2200"
/>

Stack 2/4 · previous: #50686 · next: #50688

## Problem

The detail panel only showed an Overview tab for log types that have an
inspection query. Every other type opened straight to Raw JSON. The
panel also had no header saying which log was open.

## Solution

- Adds `LogDetail`, which owns the inspection query, loading and error
states, and the Raw JSON view. Errors now use `AlertError`.
- Adds `LogOverview`, which maps each log type to its overview renderer.
Types without one fall back to the new `LogFields`, a generic tree of
expandable key/value rows built on `LogFieldRow`.
- `ServiceFlowPanel` now shows a header (level dot + event message)
above Overview / Raw JSON tabs for every log.
- Moves `getLogDataForMetadataVisibility` to `ServiceFlowPanel.utils.ts`
and adds `LogLevelDot`.

## Review instructions

1. Select a Realtime (or any other non-inspected) log. The Overview tab
should list its fields, with nested objects that expand.
2. Select a Postgres log. The overview should look as before, and Raw
JSON should still show the enriched log.
3. Switch between logs and confirm the tabs reset their scroll.

## 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

- **New Features**
- Added structured log overviews for Postgres, PostgREST, Auth, Storage,
and Edge Functions, with a Raw JSON view for log details.
- Added expandable log fields with filtering and copy actions, plus
visual indicators for log severity.
- **Bug Fixes**
  - Improved display of scalar, nested, date, and empty log values.
- **Privacy**
  - Raw log data respects metadata visibility settings.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-24 08:56:22 +10:00

69 lines
3.2 KiB
TypeScript

import { fireEvent, render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { describe, expect, it, vi } from 'vitest'
import { LogFields } from './LogFields'
describe('LogFields', () => {
it('renders scalars, empty values, and dates without losing false or zero', () => {
render(
<LogFields
data={{
count: 0,
enabled: false,
absent: null,
emptyArray: [],
emptyObject: {},
timestamp: new Date('2026-09-22T00:00:00Z'),
omitted: undefined,
}}
/>
)
expect(screen.getByRole('button', { name: 'count 0' })).toBeVisible()
expect(screen.getByText('false')).toBeInTheDocument()
expect(screen.getByText('null')).toBeInTheDocument()
expect(screen.getByText('2026-09-22T00:00:00.000Z')).toBeInTheDocument()
expect(screen.queryByText('omitted')).not.toBeInTheDocument()
expect(screen.queryByRole('button', { name: 'Expand emptyArray' })).not.toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: 'Expand emptyObject' }))
expect(screen.getByText('[]')).toBeInTheDocument()
expect(screen.getByText('{}')).toBeInTheDocument()
})
it('expands nested objects and displays arrays inline with their full value available to copy', async () => {
const user = userEvent.setup()
const copy = vi.spyOn(navigator.clipboard, 'writeText')
const headers = [{ authorization: 'example' }]
render(<LogFields data={{ request: { headers } }} />)
expect(screen.queryByText(JSON.stringify(headers))).not.toBeInTheDocument()
fireEvent.click(screen.getByRole('button', { name: 'Expand request' }))
expect(screen.queryByRole('button', { name: 'Expand headers' })).not.toBeInTheDocument()
expect(screen.getByText(JSON.stringify(headers))).toBeVisible()
await user.click(screen.getByText('headers'))
await user.click(screen.getByRole('menuitem', { name: 'Copy headers' }))
expect(copy).toHaveBeenCalledWith(JSON.stringify(headers, null, 2))
})
it('opens a copy menu from a scalar row without a separate action button', async () => {
const user = userEvent.setup()
const copy = vi.spyOn(navigator.clipboard, 'writeText')
render(<LogFields data={{ enabled: false }} />)
expect(screen.queryByRole('button', { name: /copy/i })).not.toBeInTheDocument()
await user.click(screen.getByText('enabled'))
expect(screen.queryByRole('menuitem', { name: /filter/i })).not.toBeInTheDocument()
await user.click(screen.getByRole('menuitem', { name: 'Copy enabled' }))
expect(copy).toHaveBeenCalledWith('false')
})
it('opens a menu for a nested object independently of expanding it', async () => {
const user = userEvent.setup()
const copy = vi.spyOn(navigator.clipboard, 'writeText')
render(<LogFields data={{ request: { status: 200 } }} />)
await user.click(screen.getByText('request'))
await user.click(screen.getByRole('menuitem', { name: 'Copy request' }))
expect(copy).toHaveBeenCalledWith(JSON.stringify({ status: 200 }, null, 2))
expect(screen.queryByText('200')).not.toBeInTheDocument()
await user.click(screen.getByRole('button', { name: 'Expand request' }))
expect(screen.getByText('200')).toBeVisible()
})
})