Chore/unified logs fixes 02 (#36710)

* Temporarily disable cmd K shortcut for searching unified logs

* Update placeholder for search input

* Fix getLogsCountQuery for method

* Standardize pathname parameter

* Fix refresh button
This commit is contained in:
Joshen Lim authored and GitHub committed 2025-06-27 18:58:50 +08:00
1 parent 3b3a75202e
commit 307f2843e1
7 files changed
+65 -43

No files matched your search

@@ -52,9 +52,10 @@ export const filterFields = [
return (
<div className="flex w-full items-center justify-between gap-2 font-mono">
<span className="capitalize text-foreground/70 group-hover:text-accent-foreground text-xs">
{props.label}
{props.label.replace('_', ' ')}
</span>
<span className="text-xs text-muted-foreground/70">{props.value}</span>
{/* [Joshen] Temporarily hiding, this feels excessive */}
{/* <span className="text-xs text-muted-foreground/70">{props.value}</span> */}
</div>
)
},
@@ -35,7 +35,11 @@ const buildQueryConditions = (search: QuerySearchParamsType) => {
// Handle scalar values
if (value !== null && value !== undefined) {
whereConditions.push(`${key} = '${value}'`)
if (['host', 'pathname'].includes(key)) {
whereConditions.push(`${key} LIKE '%${value}%'`)
} else {
whereConditions.push(`${key} = '${value}'`)
}
}
})
@@ -234,7 +238,7 @@ const getEdgeLogsQuery = () => {
WHEN edge_logs_response.status_code >= 500 THEN 'error'
ELSE 'success'
END as level,
edge_logs_request.path as path,
edge_logs_request.path as pathname,
edge_logs_request.host as host,
null as event_message,
edge_logs_request.method as method,
@@ -273,7 +277,7 @@ const getPostgrestLogsQuery = () => {
WHEN edge_logs_response.status_code >= 500 THEN 'error'
ELSE 'success'
END as level,
edge_logs_request.path as path,
edge_logs_request.path as pathname,
edge_logs_request.host as host,
null as event_message,
edge_logs_request.method as method,
@@ -311,7 +315,7 @@ const getPostgresLogsQuery = () => {
WHEN pgl_parsed.error_severity = 'ERROR' THEN 'error'
ELSE null
END as level,
null as path,
null as pathname,
null as host,
event_message as event_message,
null as method,
@@ -341,7 +345,7 @@ const getEdgeFunctionLogsQuery = () => {
WHEN fel_response.status_code >= 500 THEN 'error'
ELSE 'success'
END as level,
fel_request.url as path,
fel_request.url as pathname,
fel_request.host as host,
COALESCE(function_logs_agg.last_event_message, '') as event_message,
fel_request.method as method,
@@ -387,7 +391,7 @@ const getAuthLogsQuery = () => {
WHEN el_in_al_response.status_code >= 500 THEN 'error'
ELSE 'success'
END as level,
el_in_al_request.path as path,
el_in_al_request.path as pathname,
el_in_al_request.host as host,
null as event_message,
el_in_al_request.method as method,
@@ -428,7 +432,7 @@ const getSupavisorLogsQuery = () => {
WHEN LOWER(svl_metadata.level) = 'warn' OR LOWER(svl_metadata.level) = 'warning' THEN 'warning'
ELSE 'success'
END as level,
null as path,
null as pathname,
null as host,
null as event_message,
null as method,
@@ -478,7 +482,7 @@ SELECT
log_type,
status,
level,
path,
pathname,
host,
event_message,
method,
@@ -498,8 +502,9 @@ ${finalWhere}
* Also returns facets for all filter dimensions
*/
export const getLogsCountQuery = (search: QuerySearchParamsType): string => {
// Use the buildQueryConditions helper
const { finalWhere } = buildQueryConditions(search)
const methodWhere =
finalWhere.length > 0 ? `${finalWhere} AND method is NOT NULL` : 'WHERE method IS NOT NULL'
// Create a count query using the same unified logs CTE
const sql = `
@@ -530,8 +535,7 @@ UNION ALL
-- Get counts by method
SELECT 'method' as dimension, method as value, COUNT(*) as count
FROM unified_logs
${finalWhere}
WHERE method IS NOT NULL
${methodWhere}
GROUP BY method
`
@@ -27,7 +27,6 @@ import { FilterSideBar } from 'components/ui/DataTable/FilterSideBar'
import { LiveButton } from 'components/ui/DataTable/LiveButton'
import { LiveRow } from 'components/ui/DataTable/LiveRow'
import { DataTableProvider } from 'components/ui/DataTable/providers/DataTableProvider'
import { RefreshButton } from 'components/ui/DataTable/RefreshButton'
import { TimelineChart } from 'components/ui/DataTable/TimelineChart'
import { useUnifiedLogsChartQuery } from 'data/logs/unified-logs-chart-query'
import { useUnifiedLogsCountQuery } from 'data/logs/unified-logs-count-query'
@@ -45,6 +44,7 @@ import {
TabsList_Shadcn_ as TabsList,
TabsTrigger_Shadcn_ as TabsTrigger,
} from 'ui'
import { RefreshButton } from '../../ui/DataTable/RefreshButton'
import { UNIFIED_LOGS_COLUMNS } from './components/Columns'
import { MemoizedDataTableSheetContent } from './components/DataTableSheetContent'
import { FunctionLogsTab } from './components/FunctionLogsTab'
@@ -100,19 +100,36 @@ export const UnifiedLogs = () => {
isLoading,
isFetching,
hasNextPage,
refetch,
refetch: refetchLogs,
fetchNextPage,
fetchPreviousPage,
} = useUnifiedLogsInfiniteQuery({ projectRef, search: searchParameters })
const { data: counts, isLoading: isLoadingCounts } = useUnifiedLogsCountQuery({
const {
data: counts,
isLoading: isLoadingCounts,
isFetching: isFetchingCounts,
refetch: refetchCounts,
} = useUnifiedLogsCountQuery({
projectRef,
search: searchParameters,
})
const { data: unifiedLogsChart = [] } = useUnifiedLogsChartQuery({
const {
data: unifiedLogsChart = [],
isFetching: isFetchingCharts,
refetch: refetchCharts,
} = useUnifiedLogsChartQuery({
projectRef,
search: searchParameters,
})
const refetchAllData = () => {
refetchLogs()
refetchCounts()
refetchCharts()
}
const isRefetchingData = isFetching || isFetchingCounts || isFetchingCharts
const rawFlatData = useMemo(() => {
return unifiedLogsData?.pages?.flatMap((page) => page.data ?? []) ?? []
}, [unifiedLogsData?.pages])
@@ -275,10 +292,13 @@ export const UnifiedLogs = () => {
<FilterSideBar />
<div className="flex max-w-full flex-1 flex-col border-border sm:border-l overflow-hidden">
<DataTableHeaderLayout setTopBarHeight={setTopBarHeight}>
<DataTableFilterCommand searchParamsParser={SEARCH_PARAMS_PARSER} />
<DataTableFilterCommand
placeholder="Search logs..."
searchParamsParser={SEARCH_PARAMS_PARSER}
/>
<DataTableToolbar
renderActions={() => [
<RefreshButton key="refresh" onClick={refetch} />,
<RefreshButton isLoading={isRefetchingData} onRefresh={refetchAllData} />,
fetchPreviousPage ? (
<LiveButton
key="live"
@@ -4,7 +4,6 @@ import { ParserBuilder } from 'nuqs'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useLocalStorage } from 'hooks/misc/useLocalStorage'
import { useHotKey } from 'hooks/ui/useHotKey'
import {
cn,
Command_Shadcn_ as Command,
@@ -33,9 +32,13 @@ import {
interface DataTableFilterCommandProps {
// TODO: maybe use generics for the parser
searchParamsParser: Record<string, ParserBuilder<any>>
placeholder?: string
}
export function DataTableFilterCommand({ searchParamsParser }: DataTableFilterCommandProps) {
export function DataTableFilterCommand({
searchParamsParser,
placeholder = 'Search data table...',
}: DataTableFilterCommandProps) {
const { table, isLoading, filterFields: _filterFields, getFacetedUniqueValues } = useDataTable()
const columnFilters = table.getState().columnFilters
const inputRef = useRef<HTMLInputElement>(null)
@@ -59,7 +62,8 @@ export function DataTableFilterCommand({ searchParamsParser }: DataTableFilterCo
const trimmedInputValue = inputValue.trim()
useHotKey(() => setOpen((open) => !open), 'k')
// [Joshen] Temporarily disabling as this conflicts with our current CMD K behaviour
// useHotKey(() => setOpen((open) => !open), 'k')
useEffect(() => {
// TODO: we could check for ARRAY_DELIMITER or SLIDER_DELIMITER to auto-set filter when typing
@@ -139,13 +143,15 @@ export function DataTableFilterCommand({ searchParamsParser }: DataTableFilterCo
trimmedInputValue ? 'text-foreground' : 'text-foreground-light'
)}
>
{trimmedInputValue ? inputValue : 'Search data table...'}
{trimmedInputValue ? inputValue : placeholder}
</span>
<Kbd className="ml-auto text-muted-foreground group-hover:text-accent-foreground">
{/* [Joshen] Temporarily disabling as this conflicts with existing CMD K shortcut */}
{/* <Kbd className="ml-auto text-muted-foreground group-hover:text-accent-foreground">
<span className="mr-1">⌘</span>
<span>K</span>
</Kbd>
</Kbd> */}
</button>
<Command
className={cn(
'overflow-visible rounded-lg border border-border shadow-md [&>div]:border-none bg',
@@ -185,7 +191,7 @@ export function DataTableFilterCommand({ searchParamsParser }: DataTableFilterCo
const word = getWordByCaretPosition({ value, caretPosition })
setCurrentWord(word)
}}
placeholder="Search data table..."
placeholder={placeholder}
className="text-foreground"
/>
<div className="relative">
@@ -9,8 +9,6 @@ export function DataTableFilterResetButton<TData>({ value: _value }: DataTableFi
const value = _value as string
const column = table.getColumn(value)
const filterValue = columnFilters.find((f) => f.id === value)?.value
// TODO: check if we could useMemo
const filters = filterValue ? (Array.isArray(filterValue) ? filterValue : [filterValue]) : []
if (filters.length === 0) return null
@@ -18,7 +16,8 @@ export function DataTableFilterResetButton<TData>({ value: _value }: DataTableFi
return (
<Button
type="outline"
className="h-5 rounded-full px-1.5 py-1 font-mono text-[10px]"
icon={<X />}
className="h-5 rounded-full px-1.5 py-1 font-mono text-[10px] [&>span]:-translate-y-[0.6px] space-x-1"
onClick={(e) => {
e.stopPropagation()
column?.setFilterValue(undefined)
@@ -29,13 +28,8 @@ export function DataTableFilterResetButton<TData>({ value: _value }: DataTableFi
column?.setFilterValue(undefined)
}
}}
icon={<X className="h-2.5 w-2.5 text-muted-foreground" />}
asChild
>
{/* REMINDER: `AccordionTrigger` is also a button(!) and we get Hydration error when rendering button within button */}
<div role="button" tabIndex={0}>
<span>{filters.length}</span>
</div>
{filters.length}
</Button>
)
}
@@ -1,21 +1,18 @@
import { LoaderCircle, RefreshCcw } from 'lucide-react'
import { Button } from 'ui'
import { useDataTable } from './providers/DataTableProvider'
interface RefreshButtonProps {
onClick: () => void
isLoading: boolean
onRefresh: () => void
}
export function RefreshButton({ onClick }: RefreshButtonProps) {
const { isLoading } = useDataTable()
export const RefreshButton = ({ isLoading, onRefresh }: RefreshButtonProps) => {
return (
<Button
size="tiny"
type="outline"
disabled={isLoading}
onClick={onClick}
onClick={onRefresh}
className="w-[26px]"
icon={
isLoading ? (
@@ -116,7 +116,7 @@ async function getUnifiedLogs(
status: row.status || 200,
method: row.method,
host: row.host,
pathname: (row.url || '').replace(/^https?:\/\/[^\/]+/, '') || row.path || '',
pathname: (row.url || '').replace(/^https?:\/\/[^\/]+/, '') || row.pathname || '',
event_message: row.event_message || row.body || '',
headers:
typeof row.headers === 'string' ? JSON.parse(row.headers || '{}') : row.headers || {},