diff --git a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.constants.tsx b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.constants.tsx index 99307558bc8..be43282627f 100644 --- a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.constants.tsx +++ b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.constants.tsx @@ -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 = { diff --git a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.fields.tsx b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.fields.tsx index 90975d700a4..fcae710eebf 100644 --- a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.fields.tsx +++ b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.fields.tsx @@ -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 ( -
+
{props.label} @@ -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 ( -
- {statusValue} - {statusLabel && ( - - {statusLabel} - - )} -
- ) - }, - }, { 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 ( +
+ {statusValue} + {statusLabel && ( + + {statusLabel} + + )} +
+ ) + }, + }, { label: 'Method', value: 'method', type: 'checkbox', - defaultOpen: true, + defaultOpen: false, options: METHODS.map((method) => ({ label: method, value: method })), component: (props: Option) => { return ( diff --git a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.queries.test.ts b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.queries.test.ts index ae523692644..d0e75d0fd9c 100644 --- a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.queries.test.ts +++ b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.queries.test.ts @@ -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) { diff --git a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.queries.ts b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.queries.ts index d1fd27db286..d186951ffca 100644 --- a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.queries.ts +++ b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.queries.ts @@ -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 diff --git a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.tsx b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.tsx index 6d261b9ddce..8e72578a2a6 100644 --- a/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.tsx +++ b/apps/studio/components/interfaces/UnifiedLogs/UnifiedLogs.tsx @@ -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={} /> { - const [{ hide_connection_logs }, setSearch] = useQueryStates(SEARCH_PARAMS_PARSER) - - return ( - - -
- -
-

Misc

-
-
-
- -
-
-
- setSearch({ hide_connection_logs: !checked })} - /> - -
-
-
-
-
-
- ) -} diff --git a/apps/studio/components/ui/DataTable/DataTable.types.ts b/apps/studio/components/ui/DataTable/DataTable.types.ts index cb500d6aedb..ac3fd3937cb 100644 --- a/apps/studio/components/ui/DataTable/DataTable.types.ts +++ b/apps/studio/components/ui/DataTable/DataTable.types.ts @@ -4,6 +4,7 @@ import type { ReactNode } from 'react' export type Option = { label: string value: string + options?: Option[] } export type DatePreset = { diff --git a/apps/studio/components/ui/DataTable/DataTableFilters/DataTableFilterCheckbox.tsx b/apps/studio/components/ui/DataTable/DataTableFilters/DataTableFilterCheckbox.tsx index 0d5148b464b..b07542f36b5 100644 --- a/apps/studio/components/ui/DataTable/DataTableFilters/DataTableFilterCheckbox.tsx +++ b/apps/studio/components/ui/DataTable/DataTableFilters/DataTableFilterCheckbox.tsx @@ -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({ value: _value, @@ -15,6 +17,22 @@ export function DataTableFilterCheckbox({ }: DataTableCheckboxFilterField) { const value = _value as string const [inputValue, setInputValue] = useState('') + const [expandedOptions, setExpandedOptions] = useState>(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) + + 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({
) : ( - 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 ( +
({ {option.label} )}
+ {hasNested && ( + + )} {isLoadingCounts ? ( @@ -113,7 +144,7 @@ export function DataTableFilterCheckbox({ 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({
- ) - }) + {hasNested && + isExpanded && + option.options?.map((optionNested, nestedIndex) => { + const nestedChecked = getBooleanParam(optionNested.value) + const isLastNested = nestedIndex === (option.options?.length ?? 0) - 1 + return ( +
div:last-child]:pt-2' + )} + > +
+ {isLastNested ? ( + + ) : ( + <> + + + + )} +
+
+ + setBooleanParam(optionNested.value, Boolean(isChecked)) + } + /> + +
+
+ ) + })} + + ) + }) )} diff --git a/apps/studio/components/ui/DataTable/FilterSideBar.tsx b/apps/studio/components/ui/DataTable/FilterSideBar.tsx index afeb19bcd56..99166c61482 100644 --- a/apps/studio/components/ui/DataTable/FilterSideBar.tsx +++ b/apps/studio/components/ui/DataTable/FilterSideBar.tsx @@ -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> + setIsFilterBarOpen: Dispatch> 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({
- {afterFilters} {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 }))}
} actions={