Files
supabase/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.utils.ts
Joshen Lim eb915e99f8 Joshen/debug 76 fix sticky hover state on log rows (#45933)
## Context

The original problem was that some log rows remain in the hover state
after the cursor moves away

I was just cleaning up some of the styles and noticed that it doesn't
seem to happen anymore so thinking this might have fixed it haha 😅

## Changes involved
- Remove `group-hover` background color change behaviour in `TableCell`
in `ui`
- `TableRow` should handle the background color instead, hence the
changes in `UnifiedLogs.utils` too
- Remove unnecessary `renderLiveRows` in `DataTableInfinite` -> the prop
isn't being passed anywhere

## To test
- [ ] The sticky hover state happens when you hover over the rows very
quickly, so just make sure there's no "stale" hover state in any row eg:
<img width="400" alt="image"
src="https://github.com/user-attachments/assets/b4a91bc6-d269-4ee6-b222-b0476b9feffa"
/>


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

* **Refactor**
* Standardized hover styling for log rows and table cells to use direct
hover rules for more consistent visual feedback.
* Adjusted table cell hover/transition behavior to simplify styling
while preserving layout and checkbox spacing.
* Streamlined the data table API by removing the custom live-row
rendering override.
* Made the resizable panel’s minimum size dynamic based on dock
position.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/45933)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-05-14 22:00:04 +08:00

118 lines
3.9 KiB
TypeScript

import { type Table as TTable } from '@tanstack/react-table'
import { cn } from 'ui'
import { FacetMetadataSchema } from './UnifiedLogs.schema'
import { LEVELS } from '@/components/ui/DataTable/DataTable.constants'
export const logEventBus = {
listeners: new Map<string, Set<(rowId: string) => void>>(),
on(event: 'selectTraceTab', callback: (rowId: string) => void) {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set())
}
this.listeners.get(event)?.add(callback)
return () => this.listeners.get(event)?.delete(callback)
},
emit(event: 'selectTraceTab', rowId: string) {
this.listeners.get(event)?.forEach((callback) => callback(rowId))
},
}
export const getFacetedUniqueValues = <TData>(facets?: Record<string, FacetMetadataSchema>) => {
return (_table: TTable<TData>, columnId: string) => {
return new Map(facets?.[columnId]?.rows?.map(({ value, total }) => [value, total]) || [])
}
}
export const getFacetedMinMaxValues = <TData>(facets?: Record<string, FacetMetadataSchema>) => {
return (_table: TTable<TData>, columnId: string) => {
const min = facets?.[columnId]?.min
const max = facets?.[columnId]?.max
if (typeof min === 'number' && typeof max === 'number') return [min, max]
if (typeof min === 'number') return [min, min]
if (typeof max === 'number') return [max, max]
return undefined
}
}
/**
* Returns a unified-logs row's timestamp in epoch milliseconds.
*
* The row mapper attaches a pre-parsed `date` (works for both BigQuery
* microsecond timestamps and OTEL ISO strings); fall back to the raw
* `timestamp` value when it's a number (older BQ-style microseconds).
*/
export function getRowTimestampMs(
row: { date?: Date | null; timestamp?: number | string | null } | null | undefined
): number | null {
if (row?.date instanceof Date) return row.date.getTime()
if (typeof row?.timestamp === 'number') return row.timestamp / 1000
return null
}
export const getLevelLabel = (value: (typeof LEVELS)[number]): string => {
switch (value) {
case 'success':
return '2xx'
case 'warning':
return '4xx'
case 'error':
return '5xx'
}
}
// Helper function to determine level from HTTP status code
export const getStatusLevel = (status?: number | string): string => {
if (!status) return 'success'
const statusNum = Number(status)
if (statusNum >= 500) return 'error'
if (statusNum >= 400) return 'warning'
if (statusNum >= 300) return 'info' // 3xx redirects are informational
if (statusNum >= 200) return 'success'
if (statusNum >= 100) return 'info'
return 'success'
}
export function getLevelRowClassName(value: (typeof LEVELS)[number]): string {
switch (value) {
case 'success':
return ''
case 'warning':
return cn(
'bg-warning/5 hover:bg-warning/10',
'data-[state=selected]:bg-warning/20 focus-visible:bg-warning/10',
'dark:bg-warning/10 dark:hover:bg-warning/20 dark:data-[state=selected]:bg-warning/30 dark:focus-visible:bg-warning/20'
)
case 'error':
return cn(
'bg-destructive/5 hover:bg-destructive/10',
'data-[state=selected]:bg-destructive/20 focus-visible:bg-destructive/10',
'dark:bg-error/10 dark:hover:bg-destructive/20 dark:data-[state=selected]:bg-destructive/30 dark:focus-visible:bg-destructive/20'
)
default:
return ''
}
}
/**
* Formats service type strings for display purposes
* Handles special cases like "edge function" -> "Edge Function"
* and applies proper capitalization to other service types
*/
export function formatServiceTypeForDisplay(serviceType: string): string {
if (!serviceType) return ''
// Handle special cases
const specialCases: Record<string, string> = {
'edge function': 'Edge Function',
postgrest: 'PostgREST',
postgres: 'Postgres',
auth: 'Auth',
storage: 'Storage',
}
return specialCases[serviceType.toLowerCase()] || serviceType
}