Adjust connection logs toggle for unified logs, flip it to true by default (#47258)

## Context

For unified logs, postgres connection logs are being filtered out by
default previously from this
[PR](https://github.com/supabase/supabase/pull/46371) due to its noise.
We're opting to show the connection logs by default instead so this PR
changes that behaviour + adjusts the connection logs filter UI

In particular this is timely as we're adjusting how the DB will log
connections based on this
[changelog](https://github.com/orgs/supabase/discussions/47197), and
we'd want to make sure that users can find their connection logs easily

## Changes involved
- [ ] Search parameter renamed to `show_connection_logs` so that we
don't need to flip its boolean value for the checkbox
- [ ] `show_connection_logs` is subsequently `true` by default
- [ ] Shift connection logs filter to a nested option under Postgres log
type
- Makes it more visual that connection logs are related to the Postgres
service
- Currently its hidden all the way in the bottom under "Misc" which can
be easily missed
- <img width="302" height="151" alt="image"
src="https://github.com/user-attachments/assets/e3e61ac7-aa16-4769-a89e-e911daacea27"
/>
- <img width="289" height="156" alt="image"
src="https://github.com/user-attachments/assets/c70ac7c4-d2f7-4961-a6d6-6653c59d8548"
/>



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

* **New Features**
* Unified Logs now supports expandable, nested “Log Type” filter options
for drilling into connection-related entries.
* **UI Improvements**
* Connection logs are visible by default; the visibility control has
been integrated into the main filter experience.
* Filter panels are now streamlined, and key filters (such as “Level”,
“Status”, and “Method”) have been reordered and adjusted for a cleaner
default state.
* **Bug Fixes**
* Updated Unified Logs query/test behavior to match the new
connection-log visibility logic.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Joshen Lim authored and GitHub committed 2026-06-25 15:08:31 +08:00
1 parent 3c26fd071b
commit 772b3bb36e
9 files changed
+139 -100

No files matched your search

@@ -71,7 +71,7 @@ export const SEARCH_PARAMS_PARSER = {
id: parseAsString,
// View options
hide_connection_logs: parseAsBoolean.withDefault(true),
show_connection_logs: parseAsBoolean.withDefault(true),
}
const POSTGRES_STATUS_CODE_LABELS = {
@@ -22,10 +22,17 @@ export const filterFields = [
value: 'log_type',
type: 'checkbox',
defaultOpen: true,
options: Object.entries(LOG_TYPES_LABELS).map(([value, label]) => ({ label, value })),
options: Object.entries(LOG_TYPES_LABELS).map(([value, label]) => ({
label,
value,
options:
// [Joshen] Nested options are treated as just boolean toggles atm for simplicity
// Refer to DataTableFilterCheckbox for their logic
value === 'postgres' ? [{ label: 'Connection logs', value: 'show_connection_logs' }] : [],
})),
component: (props: Option) => {
return (
<div className="flex w-full items-center justify-between gap-2">
<div className="flex items-center w-full justify-between gap-2">
<span className="text-foreground/70 group-hover:text-accent-foreground text-xs">
{props.label}
</span>
@@ -33,32 +40,6 @@ export const filterFields = [
)
},
},
{
label: 'Status',
value: 'status',
type: 'checkbox',
defaultOpen: true,
options: [],
hasDynamicOptions: true,
component: (props: Option) => {
if (typeof props.value === 'boolean') return null
if (typeof props.value === 'undefined') return null
const statusValue = String(props.value)
const statusLabel = STATUS_CODE_LABELS[statusValue as keyof typeof STATUS_CODE_LABELS]
return (
<div className="flex items-center gap-2 w-full min-w-0">
<span className="shrink-0 text-foreground">{statusValue}</span>
{statusLabel && (
<span className="text-[0.7rem] text-foreground-lighter truncate" title={statusLabel}>
{statusLabel}
</span>
)}
</div>
)
},
},
{
label: 'Level',
value: 'level',
@@ -81,11 +62,37 @@ export const filterFields = [
)
},
},
{
label: 'Status',
value: 'status',
type: 'checkbox',
defaultOpen: false,
options: [],
hasDynamicOptions: true,
component: (props: Option) => {
if (typeof props.value === 'boolean') return null
if (typeof props.value === 'undefined') return null
const statusValue = String(props.value)
const statusLabel = STATUS_CODE_LABELS[statusValue as keyof typeof STATUS_CODE_LABELS]
return (
<div className="flex items-center gap-2 w-full min-w-0">
<span className="shrink-0 text-foreground">{statusValue}</span>
{statusLabel && (
<span className="text-[0.7rem] text-foreground-lighter truncate" title={statusLabel}>
{statusLabel}
</span>
)}
</div>
)
},
},
{
label: 'Method',
value: 'method',
type: 'checkbox',
defaultOpen: true,
defaultOpen: false,
options: METHODS.map((method) => ({ label: method, value: method })),
component: (props: Option) => {
return (
@@ -114,16 +114,16 @@ describe('UnifiedLogs.queries (OTEL flat)', () => {
expect(sql).not.toContain(`'%error%%'`)
})
it('excludes connection log messages by default (hide_connection_logs=true)', () => {
const sql = getUnifiedLogsQuery({ ...baseSearch, hide_connection_logs: true } as any)
it('excludes connection log messages when show_connection_logs=false', () => {
const sql = getUnifiedLogsQuery({ ...baseSearch, show_connection_logs: false } as any)
expect(sql).toContain("source != 'postgres_logs'")
expect(sql).toContain("event_message NOT LIKE 'connection received%'")
expect(sql).toContain("event_message NOT LIKE 'connection authenticated%'")
expect(sql).toContain("event_message NOT LIKE 'connection authorized%'")
})
it('includes connection log messages when hide_connection_logs=false', () => {
const sql = getUnifiedLogsQuery({ ...baseSearch, hide_connection_logs: false } as any)
it('includes connection log messages by default (show_connection_logs=true)', () => {
const sql = getUnifiedLogsQuery({ ...baseSearch, show_connection_logs: true } as any)
expect(sql).not.toContain("event_message NOT LIKE 'connection received%'")
})
@@ -174,7 +174,7 @@ describe('UnifiedLogs.queries (OTEL flat)', () => {
})
it('applies the connection-logs filter to every count scan so badges match the list', () => {
const sql = getLogsCountQuery({ ...baseSearch, hide_connection_logs: true } as any)
const sql = getLogsCountQuery({ ...baseSearch, show_connection_logs: false } as any)
const scans = sql.split(/\bUNION ALL\b/)
expect(scans.length).toBeGreaterThan(1)
for (const scan of scans) {
@@ -307,7 +307,8 @@ const buildBaseWhere = (
* by the connection rows the list hides).
*/
const connectionLogsFilter = (search: QuerySearchParamsType): SafeLogSqlFragment | null => {
if (!search.hide_connection_logs) return null
// Visible by default — only an explicit `false` hides connection logs.
if (search.show_connection_logs !== false) return null
return safeSql`(source != 'postgres_logs' OR (
event_message NOT LIKE 'connection received%' AND
event_message NOT LIKE 'connection authenticated%' AND
@@ -29,7 +29,6 @@ import {
import { RefreshButton } from '../../ui/DataTable/RefreshButton'
import { generateDynamicColumns, UNIFIED_LOGS_COLUMNS } from './components/Columns'
import { ConnectionLogsToggle } from './components/ConnectionLogsToggle'
import { DownloadLogsButton } from './components/DownloadLogsButton'
import { LogsFilterBar } from './components/LogsFilterBar'
import { LogsListPanel } from './components/LogsListPanel'
@@ -377,7 +376,6 @@ export const UnifiedLogs = () => {
isFilterBarOpen={isFilterBarOpen}
setIsFilterBarOpen={setIsFilterBarOpen}
dateRangeDisabled={{ after: new Date() }}
afterFilters={<ConnectionLogsToggle />}
/>
<ResizableHandle withHandle />
<ResizablePanel
@@ -1,41 +0,0 @@
import { useQueryStates } from 'nuqs'
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, Checkbox, Label } from 'ui'
import { SEARCH_PARAMS_PARSER } from '../UnifiedLogs.constants'
export const ConnectionLogsToggle = () => {
const [{ hide_connection_logs }, setSearch] = useQueryStates(SEARCH_PARAMS_PARSER)
return (
<Accordion type="multiple" defaultValue={['misc']}>
<AccordionItem value="misc" className="border-none">
<div className="flex items-center gap-2 pr-2">
<AccordionTrigger className="flex-1 px-2 py-0 hover:no-underline data-[state=closed]:text-muted-foreground data-open:text-foreground focus-within:data-closed:text-foreground hover:data-closed:text-foreground">
<div className="flex items-center gap-2 truncate py-2">
<p className="text-sm">Misc</p>
</div>
</AccordionTrigger>
</div>
<AccordionContent>
<div className="p-1">
<div className="rounded-sm border border-border">
<div className="group relative flex items-center space-x-2 px-2 py-2 hover:bg-accent/50">
<Checkbox
id="connection-logs"
checked={!hide_connection_logs}
onCheckedChange={(checked) => setSearch({ hide_connection_logs: !checked })}
/>
<Label
htmlFor="connection-logs"
className="flex w-full cursor-pointer items-center justify-between gap-2 text-[0.8rem] font-normal text-foreground/70 group-hover:text-accent-foreground"
>
Connection logs
</Label>
</div>
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
)
}
@@ -4,6 +4,7 @@ import type { ReactNode } from 'react'
export type Option = {
label: string
value: string
options?: Option[]
}
export type DatePreset = {
@@ -1,4 +1,5 @@
import { Search } from 'lucide-react'
import { Minus, Plus, Search } from 'lucide-react'
import { useQueryStates } from 'nuqs'
import { useState } from 'react'
import { Checkbox, cn, Label, Skeleton } from 'ui'
@@ -7,6 +8,7 @@ import { formatCompactNumber } from '../DataTable.utils'
import { InputWithAddons } from '../primitives/InputWithAddons'
import { useDataTable } from '../providers/DataTableProvider'
import { DataTableFilterCheckboxLoader } from './DataTableFilterCheckboxLoader'
import { SEARCH_PARAMS_PARSER } from '@/components/interfaces/UnifiedLogs/UnifiedLogs.constants'
export function DataTableFilterCheckbox<TData>({
value: _value,
@@ -15,6 +17,22 @@ export function DataTableFilterCheckbox<TData>({
}: DataTableCheckboxFilterField<TData>) {
const value = _value as string
const [inputValue, setInputValue] = useState('')
const [expandedOptions, setExpandedOptions] = useState<Set<string>>(new Set())
const [searchParams, setSearchParams] = useQueryStates(SEARCH_PARAMS_PARSER)
// Nested options map 1:1 to a boolean search param via their `value`
// (e.g. `show_connection_logs`), so they can be read/written generically.
const getBooleanParam = (key: string) => Boolean(searchParams[key as keyof typeof searchParams])
const setBooleanParam = (key: string, val: boolean) =>
setSearchParams({ [key]: val } as Partial<typeof searchParams>)
const toggleExpanded = (key: string) =>
setExpandedOptions((prev) => {
const next = new Set(prev)
if (next.has(key)) next.delete(key)
else next.add(key)
return next
})
const { table, columnFilters, isLoading, isLoadingCounts, getFacetedUniqueValues } =
useDataTable()
@@ -65,18 +83,20 @@ export function DataTableFilterCheckbox<TData>({
</div>
</div>
) : (
filterOptions
// TODO: we shoudn't sort the options here, instead filterOptions should be sorted by default
// .sort((a, b) => a.label.localeCompare(b.label))
.map((option, index) => {
const checked = filters.includes(option.value)
filterOptions.map((option, index) => {
const checked = filters.includes(option.value)
const optionKey = String(option.value)
const hasNested = (option.options ?? []).length > 0
const isExpanded = expandedOptions.has(optionKey)
return (
return (
<div
key={String(option.value)}
className={cn('py-2', index !== filterOptions.length - 1 ? 'border-b' : undefined)}
>
<div
key={String(option.value)}
className={cn(
'group relative flex items-center space-x-2 px-2 py-2 hover:bg-accent/50',
index !== filterOptions.length - 1 ? 'border-b' : undefined
'group relative flex items-center space-x-2 px-2 hover:bg-accent/50'
)}
>
<Checkbox
@@ -100,6 +120,17 @@ export function DataTableFilterCheckbox<TData>({
<span className="truncate font-normal block">{option.label}</span>
)}
</div>
{hasNested && (
<button
type="button"
aria-label={isExpanded ? 'Collapse' : 'Expand'}
aria-expanded={isExpanded}
onClick={() => toggleExpanded(optionKey)}
className="flex h-4 w-4 shrink-0 items-center justify-center rounded-sm text-foreground-lighter hover:bg-selection hover:text-foreground mr-2.5"
>
{isExpanded ? <Minus size={12} /> : <Plus size={12} />}
</button>
)}
<span className="shrink-0 flex items-center justify-center font-mono text-xs">
{isLoadingCounts ? (
<Skeleton className="h-4 w-4" />
@@ -113,7 +144,7 @@ export function DataTableFilterCheckbox<TData>({
type="button"
onClick={() => column?.setFilterValue([option.value])}
className={cn(
'absolute inset-y-0 right-0 hidden font-normal text-muted-foreground backdrop-blur-xs hover:text-foreground group-hover:block',
'absolute inset-y-0 right-0 -top-0.5 hidden font-normal text-muted-foreground backdrop-blur-xs hover:text-foreground group-hover:block',
'rounded-md ring-offset-background focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2'
)}
>
@@ -121,8 +152,55 @@ export function DataTableFilterCheckbox<TData>({
</button>
</Label>
</div>
)
})
{hasNested &&
isExpanded &&
option.options?.map((optionNested, nestedIndex) => {
const nestedChecked = getBooleanParam(optionNested.value)
const isLastNested = nestedIndex === (option.options?.length ?? 0) - 1
return (
<div
key={optionNested.value}
className={cn(
'group/nested relative flex items-stretch',
nestedIndex === 0 && '[&>div:last-child]:pt-2'
)}
>
<div aria-hidden className="relative w-7 shrink-0">
{isLastNested ? (
<span
className={cn(
'absolute left-4 top-0 w-3 rounded-bl-sm border-b border-l border-border',
option.options?.length === 1 ? 'h-[60%]' : 'h-1/2'
)}
/>
) : (
<>
<span className="absolute left-4 top-0 h-full w-px bg-border" />
<span className="absolute left-4 top-[55%] h-px w-3 bg-border" />
</>
)}
</div>
<div className="flex flex-1 items-center gap-x-2 rounded-sm py-1 pr-2 hover:bg-accent/50 min-w-0">
<Checkbox
id={`${value}-${optionNested.value}`}
checked={nestedChecked}
onCheckedChange={(isChecked) =>
setBooleanParam(optionNested.value, Boolean(isChecked))
}
/>
<Label
htmlFor={`${value}-${optionNested.value}`}
className="flex w-full cursor-pointer items-center text-[0.8rem] font-normal text-foreground/70 group-hover/nested:text-accent-foreground min-w-0"
>
<span className="truncate text-xs">{optionNested.label}</span>
</Label>
</div>
</div>
)
})}
</div>
)
})
)}
</div>
</div>
@@ -1,7 +1,7 @@
import { useParams } from 'common'
import Link from 'next/link'
import { useRouter } from 'next/router'
import React, { useEffect, type ReactNode } from 'react'
import { cloneElement, Dispatch, SetStateAction, useEffect } from 'react'
import { Badge, Button, cn, ResizablePanel, usePanelRef } from 'ui'
import { FeaturePreviewSidebarPanel } from '../FeaturePreviewSidebarPanel'
@@ -15,16 +15,14 @@ import { UnifiedLogsBanner } from '@/components/interfaces/UnifiedLogs/UnifiedLo
interface FilterSideBarProps {
isFilterBarOpen: boolean
setIsFilterBarOpen: React.Dispatch<React.SetStateAction<boolean>>
setIsFilterBarOpen: Dispatch<SetStateAction<boolean>>
dateRangeDisabled?: DateRangeDisabled
afterFilters?: ReactNode
}
export function FilterSideBar({
isFilterBarOpen,
setIsFilterBarOpen,
dateRangeDisabled,
afterFilters,
}: FilterSideBarProps) {
const router = useRouter()
const { ref } = useParams()
@@ -84,7 +82,6 @@ export function FilterSideBar({
<div className="flex-1 p-2 sm:overflow-y-scroll">
<DataTableFilterControls dateRangeDisabled={dateRangeDisabled} />
{afterFilters}
<FeaturePreviewSidebarPanel
className="mx-2 my-4"
title="Capture your logs"
@@ -93,9 +90,7 @@ export function FilterSideBar({
<div className="flex items-center gap-4">
{LOG_DRAIN_TYPES.filter((t) =>
['datadog', 'sentry', 'webhook', 'loki'].includes(t.value)
).map((type) =>
React.cloneElement(type.icon, { height: 20, width: 20, key: type.value })
)}
).map((type) => cloneElement(type.icon, { height: 20, width: 20, key: type.value }))}
</div>
}
actions={