mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
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:
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={
|
||||
|
||||
Reference in new issue
Block a user