Files
kemal.earthandAli Waseem b2e5476146 feat(studio): tidy up bottom tray in logs interface (#45371)
## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES

## What kind of change does this PR introduce?

Tidying up the bottom panel in unified logs. Taking care of some visual
quirks etc. Also preparing this area to house some other future concepts
via tabs.

| Before | After |
|--------|--------|
| <img width="828" height="384" alt="Screenshot 2026-04-30 at 11 24 09"
src="https://github.com/user-attachments/assets/804bdf1c-7cdb-4dd8-bf1e-31c434ef1436"
/> | <img width="830" height="407" alt="Screenshot 2026-04-30 at 11 22
53"
src="https://github.com/user-attachments/assets/28555efe-f893-4bae-bcb0-284e6db733e6"
/> |




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

* **New Features**
  * Redesigned service flow panel with Overview and Raw JSON tabs
* Added Previous/Next navigation controls with Arrow Up/Down keyboard
support
* New detail components and section headers with icons for clearer
organization
  * Improved Postgres detail view and message/session display

* **Bug Fixes / Changes**
  * Removed legacy header UI and related controls

* **UI / Style**
  * Enhanced copy-to-clipboard feedback animation
  * Updated "Load more" button styling
  * Adjusted panel sizing for improved resizing behavior
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Ali Waseem <waseema393@gmail.com>
2026-05-06 16:32:31 +01:00

750 lines
22 KiB
TypeScript

import { BlockFieldConfig } from '../types'
import { getStorageMetadata } from '../utils/storageUtils'
import { formatBytes } from '@/lib/helpers'
// Helper functions that avoid duplication with existing storage utilities
const getFileName = (path: string): string => {
if (!path) return ''
// Remove query parameters and hash
const cleanPath = path.split('?')[0].split('#')[0]
// Get the last part after the last slash
const segments = cleanPath.split('/')
return segments[segments.length - 1] || ''
}
const formatStorageDate = (dateString: string): string => {
try {
return new Date(dateString).toLocaleString()
} catch {
return dateString
}
}
// =============================================================================
// NETWORK FIELDS
// =============================================================================
// Field configurations - using filterable field IDs where possible
export const originFields: BlockFieldConfig[] = [
{
id: 'date', // Matches filterFields 'date' (timerange) - FILTERABLE
label: 'Time',
getValue: (data) => {
if (!data?.timestamp && !data?.date) return null
try {
const timestamp = data?.timestamp || data?.date
return new Date(timestamp).toLocaleString()
} catch {
return 'Invalid date'
}
},
},
]
// Primary Network Fields (Always Visible) - FILTERABLE
export const networkPrimaryFields: BlockFieldConfig[] = [
{
id: 'host', // Matches filterFields 'host' (input) - FILTERABLE
label: 'Host',
getValue: (data, enrichedData) =>
enrichedData?.request_host || enrichedData?.host || data?.host,
},
{
id: 'method', // Matches filterFields 'method' (checkbox) - FILTERABLE
label: 'Method',
getValue: (data, enrichedData) =>
enrichedData?.request_method || enrichedData?.method || data?.method,
},
{
id: 'pathname', // Matches filterFields 'pathname' (input) - FILTERABLE
label: 'Path',
getValue: (data, enrichedData) =>
enrichedData?.request_path || enrichedData?.pathname || data?.pathname,
},
{
id: 'user_agent',
label: 'Client',
getValue: (_data, enrichedData) => {
const userAgent = enrichedData?.headers_user_agent
if (!userAgent) return null
// TODO: Parse user agent for nice display with icons
return userAgent.length > 50 ? userAgent.substring(0, 50) + '...' : userAgent
},
requiresEnrichedData: true,
},
]
// Primary API Key Field (Always Visible) - Shows API key type only
export const apiKeyPrimaryField: BlockFieldConfig = {
id: 'api_key_role',
label: 'API Key',
getValue: (_data, enrichedData) => {
const prefix = enrichedData?.api_key_prefix
const jwtRole = enrichedData?.jwt_key_role
const authRole = enrichedData?.authorization_role
if (prefix) {
// Extract key type from prefix
if (prefix.includes('publishable')) return 'publishable'
else if (prefix.includes('secret')) return 'secret'
return 'unknown'
}
// Fallback to JWT apikey postgres role if no API key prefix
if (jwtRole) {
return jwtRole
}
// Fallback to authorization postgres role if no apikey JWT
if (authRole) {
return authRole
}
return null
},
requiresEnrichedData: true,
}
// Additional API Key Fields (Collapsible)
export const apiKeyAdditionalFields: BlockFieldConfig[] = [
{
id: 'api_key_prefix_full',
label: 'API Key Prefix',
getValue: (_data, enrichedData) => {
const prefix = enrichedData?.api_key_prefix
return prefix || null
},
requiresEnrichedData: true,
},
{
id: 'postgres_role',
label: 'Postgres Role',
getValue: (_data, enrichedData) => enrichedData?.jwt_key_role,
requiresEnrichedData: true,
},
{
id: 'api_key_error',
label: 'API Key Error',
getValue: (_data, enrichedData) => enrichedData?.api_key_error,
requiresEnrichedData: true,
},
{
id: 'api_key_hash',
label: 'API Key Hash',
getValue: (_data, enrichedData) => {
const hash = enrichedData?.api_key_hash
return hash ? `${hash.substring(0, 12)}...` : null
},
requiresEnrichedData: true,
},
{
id: 'authorization_role',
label: 'Postgres Role',
getValue: (_data, enrichedData) => enrichedData?.authorization_role,
requiresEnrichedData: true,
},
]
// Primary User Field (Always Visible)
export const userPrimaryField: BlockFieldConfig = {
id: 'user_id',
label: 'User',
getValue: (_data, enrichedData) => {
const userId = enrichedData?.user_id
return userId ? `${userId.substring(0, 8)}...` : null
},
requiresEnrichedData: true,
}
// Additional User Fields (Collapsible)
export const userAdditionalFields: BlockFieldConfig[] = [
{
id: 'auth_user', // Matches filterFields 'auth_user' (input) - FILTERABLE
label: 'Auth User',
getValue: (data, enrichedData) => enrichedData?.auth_user || data?.auth_user,
},
{
id: 'user_email',
label: 'User Email',
getValue: (_data, enrichedData) => enrichedData?.user_email,
requiresEnrichedData: true,
},
]
// Primary Location Field (Always Visible)
export const locationPrimaryField: BlockFieldConfig = {
id: 'client_country',
label: 'Location',
getValue: (_data, enrichedData) => {
const country = enrichedData?.client_country || enrichedData?.cf_country
const city = enrichedData?.client_city
if (country && city) return `${city}, ${country}`
if (country) return country
if (city) return city
return null
},
requiresEnrichedData: true,
}
// Additional Location Fields (Collapsible) - includes IP addresses
export const locationAdditionalFields: BlockFieldConfig[] = [
{
id: 'client_continent',
label: 'Continent',
getValue: (_data, enrichedData) => enrichedData?.client_continent,
requiresEnrichedData: true,
},
{
id: 'client_region',
label: 'Region',
getValue: (_data, enrichedData) => enrichedData?.client_region,
requiresEnrichedData: true,
},
{
id: 'client_timezone',
label: 'Timezone',
getValue: (_data, enrichedData) => enrichedData?.client_timezone,
requiresEnrichedData: true,
},
{
id: 'x_real_ip',
label: 'Real IP',
getValue: (_data, enrichedData) => enrichedData?.headers_x_real_ip,
requiresEnrichedData: true,
},
{
id: 'client_ip',
label: 'Client IP',
getValue: (_data, enrichedData) => enrichedData?.client_ip,
requiresEnrichedData: true,
},
]
// Authorization Fields (Collapsible) - JWT authorization details
export const authorizationFields: BlockFieldConfig[] = [
{
id: 'jwt_auth_key_id',
label: 'Key ID',
getValue: (_data, enrichedData) =>
enrichedData?.jwt_auth_key_id || enrichedData?.jwt_apikey_key_id,
requiresEnrichedData: true,
},
{
id: 'jwt_auth_session_id',
label: 'Session ID',
getValue: (_data, enrichedData) =>
enrichedData?.jwt_auth_session_id || enrichedData?.jwt_apikey_session_id,
requiresEnrichedData: true,
},
{
id: 'jwt_auth_subject',
label: 'Subject',
getValue: (_data, enrichedData) =>
enrichedData?.jwt_auth_subject || enrichedData?.jwt_apikey_subject,
requiresEnrichedData: true,
},
{
id: 'jwt_auth_issuer',
label: 'Issuer',
getValue: (_data, enrichedData) =>
enrichedData?.jwt_auth_issuer || enrichedData?.jwt_apikey_issuer,
requiresEnrichedData: true,
},
{
id: 'jwt_auth_algorithm',
label: 'Algorithm',
getValue: (_data, enrichedData) =>
enrichedData?.jwt_auth_algorithm || enrichedData?.jwt_apikey_algorithm,
requiresEnrichedData: true,
},
{
id: 'jwt_auth_expires_at',
label: 'Expires At',
getValue: (_data, enrichedData) => {
const expiresAt = enrichedData?.jwt_auth_expires_at || enrichedData?.jwt_apikey_expires_at
if (!expiresAt) return null
try {
return new Date(expiresAt * 1000).toLocaleString()
} catch {
return expiresAt
}
},
requiresEnrichedData: true,
},
]
// Tech Details Fields (Collapsible)
export const techDetailsFields: BlockFieldConfig[] = [
{
id: 'network_protocol',
label: 'Protocol',
getValue: (_data, enrichedData) => enrichedData?.network_protocol,
requiresEnrichedData: true,
},
{
id: 'cf_datacenter',
label: 'Datacenter',
getValue: (_data, enrichedData) =>
enrichedData?.network_datacenter || enrichedData?.cf_datacenter,
requiresEnrichedData: true,
},
{
id: 'cache_status',
label: 'Cache Status',
getValue: (_data, enrichedData) => enrichedData?.response_cache_status,
requiresEnrichedData: true,
},
{
id: 'cf_ray',
label: 'CF-Ray',
getValue: (_data, enrichedData) => enrichedData?.cf_ray,
requiresEnrichedData: true,
},
{
id: 'x_client_info',
label: 'SDK',
getValue: (_data, enrichedData) => enrichedData?.headers_x_client_info,
requiresEnrichedData: true,
},
{
id: 'x_forwarded_proto',
label: 'Forwarded Proto',
getValue: (_data, enrichedData) => enrichedData?.headers_x_forwarded_proto,
requiresEnrichedData: true,
},
]
// =============================================================================
// POSTGREST FIELDS
// =============================================================================
// Primary PostgREST Fields (Always Visible) - FILTERABLE
export const postgrestPrimaryFields: BlockFieldConfig[] = [
{
id: 'status', // Matches filterFields 'status' (checkbox) - FILTERABLE
label: 'Status',
getValue: (data, enrichedData) => enrichedData?.status || data?.status,
},
{
id: 'postgres_role',
label: 'Postgres Role',
getValue: (_data, enrichedData) => enrichedData?.api_role,
requiresEnrichedData: true,
},
{
id: 'response_time',
label: 'Response Time',
getValue: (data, enrichedData) => {
const time = enrichedData?.response_origin_time || data?.response_time_ms
return time ? `${time}ms` : null
},
requiresEnrichedData: true,
},
]
// PostgREST Response Details (Collapsible)
export const postgrestResponseFields: BlockFieldConfig[] = [
{
id: 'query_params',
label: 'Query',
getValue: (_data, enrichedData) => enrichedData?.request_search,
requiresEnrichedData: true,
},
{
id: 'content_type',
label: 'Content Type',
getValue: (_data, enrichedData) => enrichedData?.response_content_type,
requiresEnrichedData: true,
},
{
id: 'message',
label: 'Message',
getValue: (_data, enrichedData) => enrichedData?.message,
requiresEnrichedData: true,
},
]
// =============================================================================
// AUTH FIELDS
// =============================================================================
// Primary GoTrue/Auth Fields (Always Visible)
export const authPrimaryFields: BlockFieldConfig[] = [
{
id: 'auth_path',
label: 'Auth Path',
getValue: (data, enrichedData) => {
return enrichedData?.path || enrichedData?.request_path || data?.path
},
requiresEnrichedData: true,
},
{
id: 'log_id',
label: 'Log ID',
getValue: (data, enrichedData) => {
const logId = data?.id || enrichedData?.id
return logId ? `${logId.substring(0, 8)}...` : null
},
},
{
id: 'referer',
label: 'Referer',
getValue: (_data, enrichedData) => {
return enrichedData?.headers_referer || null
},
requiresEnrichedData: true,
},
]
// =============================================================================
// EDGE FUNCTION FIELDS
// =============================================================================
// Primary Edge Function Fields (Always Visible)
export const edgeFunctionPrimaryFields: BlockFieldConfig[] = [
{
id: 'status',
label: 'Status',
getValue: (data, enrichedData) => enrichedData?.status || data?.status,
},
{
id: 'function_path',
label: 'Function Path',
getValue: (data, enrichedData) => {
return (
enrichedData?.path || enrichedData?.request_path || data?.path || enrichedData?.request_url
)
},
requiresEnrichedData: false,
},
{
id: 'execution_time',
label: 'Execution Time',
getValue: (data, enrichedData) => {
const time = enrichedData?.execution_time_ms || data?.execution_time_ms
return time ? `${time}ms` : null
},
requiresEnrichedData: false,
},
{
id: 'execution_id',
label: 'Execution ID',
getValue: (data, enrichedData) => {
const execId = enrichedData?.execution_id || data?.execution_id
return execId ? `${execId.substring(0, 8)}...` : null
},
requiresEnrichedData: false,
},
]
// Edge Function Details (Collapsible)
export const edgeFunctionDetailsFields: BlockFieldConfig[] = [
{
id: 'function_id',
label: 'Function ID',
getValue: (data, enrichedData) => {
const funcId = enrichedData?.function_id || data?.function_id
return funcId ? `${funcId.substring(0, 8)}...` : null
},
requiresEnrichedData: false,
},
{
id: 'execution_region',
label: 'Execution Region',
getValue: (data, enrichedData) => {
return enrichedData?.execution_region || data?.execution_region || null
},
requiresEnrichedData: false,
},
{
id: 'function_log_count',
label: 'Function Logs',
getValue: (data, enrichedData) => {
const count = enrichedData?.function_log_count || data?.function_log_count || data?.log_count
return count ? `${count} logs` : 'No logs'
},
requiresEnrichedData: false,
},
{
id: 'method',
label: 'Method',
getValue: (data, enrichedData) => enrichedData?.method || data?.method,
requiresEnrichedData: false,
},
{
id: 'user_agent',
label: 'User Agent',
getValue: (data, enrichedData) => {
const ua = enrichedData?.headers_user_agent || data?.headers_user_agent
return ua ? (ua.length > 30 ? `${ua.substring(0, 30)}...` : ua) : null
},
requiresEnrichedData: false,
},
]
// =============================================================================
// STORAGE FIELDS
// =============================================================================
// Primary Storage Fields (Always Visible)
export const storagePrimaryFields: BlockFieldConfig[] = [
{
id: 'status',
label: 'Status',
getValue: (data, enrichedData) => enrichedData?.status || data?.status,
},
{
id: 'filename',
label: 'File Name',
getValue: (data, enrichedData) => {
const path = enrichedData?.path || enrichedData?.request_path || data?.path
return path ? getFileName(path) : null
},
requiresEnrichedData: false,
},
{
id: 'content_type_size',
label: 'Type & Size',
getValue: (data, enrichedData) => {
const status = enrichedData?.status || data?.status
const isObjectDeleted = status === 404 || status === '404'
const hasError = status && Number(status) >= 400
// For deleted objects, show status message
if (isObjectDeleted) {
return 'Object deleted'
} else if (hasError) {
return `Error ${status}`
}
// Try to get metadata like PreviewPane does
const metadata = getStorageMetadata(data, enrichedData)
const mimeType = metadata?.mimetype
const size = metadata?.size
// Fallback to headers if metadata not available
const contentType =
mimeType ||
enrichedData?.response_content_type ||
enrichedData?.storage_request_content_type
const contentLength =
size ||
(enrichedData?.storage_content_length
? parseInt(enrichedData.storage_content_length)
: null)
if (contentType && contentLength) {
return `${contentType} - ${formatBytes(contentLength)}`
} else if (contentType) {
return contentType
} else if (contentLength) {
return formatBytes(contentLength)
}
return 'Unknown type'
},
requiresEnrichedData: true,
},
{
id: 'response_time',
label: 'Response Time',
getValue: (data, enrichedData) => {
const time = enrichedData?.response_origin_time || data?.response_time_ms
return time ? `${time}ms` : null
},
requiresEnrichedData: true,
},
]
// Storage Details (Collapsible)
export const storageDetailsFields: BlockFieldConfig[] = [
{
id: 'storage_path',
label: 'Storage Path',
getValue: (data, enrichedData) => {
const path = enrichedData?.path || enrichedData?.request_path || data?.path
// Extract just the object path from the full storage path
const match = path?.match(/\/storage\/v1\/object\/([^\/]+)\/(.+)/)
if (match) {
const [, bucketName, objectPath] = match
return `${bucketName}/${objectPath}`
}
return path
},
requiresEnrichedData: false,
},
{
id: 'last_modified',
label: 'Last Modified',
getValue: (data, enrichedData) => {
const status = enrichedData?.status || data?.status
const isObjectDeleted = status === 404 || status === '404'
const hasError = status && Number(status) >= 400
if (isObjectDeleted) {
return 'Object deleted'
} else if (hasError) {
return 'Unavailable'
}
// Try to get dates from metadata like PreviewPane does
const metadata = getStorageMetadata(data, enrichedData)
const updatedAt = metadata?.updated_at || data?.updated_at || enrichedData?.updated_at
const lastModified = enrichedData?.storage_last_modified || updatedAt
return lastModified ? formatStorageDate(lastModified) : null
},
requiresEnrichedData: true,
},
{
id: 'etag',
label: 'ETag',
getValue: (data, enrichedData) => {
const status = enrichedData?.status || data?.status
const isObjectDeleted = status === 404 || status === '404'
const hasError = status && Number(status) >= 400
if (isObjectDeleted) {
return 'Object deleted'
} else if (hasError) {
return 'Unavailable'
}
const etag = enrichedData?.storage_etag
return etag ? `${etag.substring(0, 12)}...` : null
},
requiresEnrichedData: true,
},
{
id: 'content_disposition',
label: 'Content Disposition',
getValue: (data, enrichedData) => {
const status = enrichedData?.status || data?.status
const isObjectDeleted = status === 404 || status === '404'
const hasError = status && Number(status) >= 400
if (isObjectDeleted) {
return 'Object deleted'
} else if (hasError) {
return 'Unavailable'
}
return enrichedData?.storage_content_disposition || null
},
requiresEnrichedData: true,
},
{
id: 'method',
label: 'Method',
getValue: (data, enrichedData) => enrichedData?.method || data?.method,
requiresEnrichedData: false,
},
{
id: 'user_agent',
label: 'User Agent',
getValue: (data, enrichedData) => {
const ua = enrichedData?.headers_user_agent || data?.headers_user_agent
return ua ? (ua.length > 30 ? `${ua.substring(0, 30)}...` : ua) : null
},
requiresEnrichedData: false,
},
]
// =============================================================================
// POSTGRES FIELDS
// =============================================================================
// Primary Postgres Fields (Always Visible)
export const postgresPrimaryFields: BlockFieldConfig[] = [
{
id: 'event_message',
label: 'Message',
getValue: (data, enrichedData) => enrichedData?.event_message || data?.event_message,
wrap: true,
},
{
id: 'status',
label: 'Status',
getValue: (data, enrichedData) => enrichedData?.status || data?.status,
},
{
id: 'command_tag',
label: 'Command',
getValue: (data, enrichedData) => enrichedData?.command_tag || data?.command_tag,
requiresEnrichedData: true,
},
{
id: 'database_name',
label: 'Database',
getValue: (data, enrichedData) => enrichedData?.database_name || data?.database_name,
requiresEnrichedData: true,
},
{
id: 'database_user',
label: 'User',
getValue: (data, enrichedData) => enrichedData?.database_user || data?.database_user,
requiresEnrichedData: true,
},
]
// Postgres Details (Collapsible)
export const postgresDetailsFields: BlockFieldConfig[] = [
{
id: 'backend_type',
label: 'Backend Type',
getValue: (data, enrichedData) => enrichedData?.backend_type || data?.backend_type,
requiresEnrichedData: true,
},
{
id: 'connection_from',
label: 'Connection From',
getValue: (data, enrichedData) => enrichedData?.connection_from || data?.connection_from,
requiresEnrichedData: true,
},
{
id: 'session_id',
label: 'Session ID',
getValue: (data, enrichedData) => enrichedData?.session_id || data?.session_id,
requiresEnrichedData: true,
},
{
id: 'process_id',
label: 'Process ID',
getValue: (data, enrichedData) => enrichedData?.process_id || data?.process_id,
requiresEnrichedData: true,
},
{
id: 'query_id',
label: 'Query ID',
getValue: (data, enrichedData) => {
const queryId = enrichedData?.query_id || data?.query_id
return queryId ? String(queryId) : null
},
requiresEnrichedData: true,
},
{
id: 'transaction_id',
label: 'Transaction ID',
getValue: (data, enrichedData) => {
const txId = enrichedData?.transaction_id || data?.transaction_id
return txId ? String(txId) : null
},
requiresEnrichedData: true,
},
{
id: 'virtual_transaction_id',
label: 'Virtual TX ID',
getValue: (data, enrichedData) =>
enrichedData?.virtual_transaction_id || data?.virtual_transaction_id,
requiresEnrichedData: true,
},
{
id: 'session_start_time',
label: 'Session Started',
getValue: (data, enrichedData) => {
const startTime = enrichedData?.session_start_time || data?.session_start_time
return startTime ? new Date(startTime).toLocaleString() : null
},
requiresEnrichedData: true,
},
]