This commit is contained in:
Joshen Lim committed 2025-06-06 21:36:21 +08:00
1 parent cf32793020
commit dcc6239397
39 files changed
+664 -1321

No files matched your search

@@ -3,7 +3,7 @@
import { Auth, EdgeFunctions, Storage } from 'icons'
import { BookHeart, Box, Cpu, Database, Globe } from 'lucide-react'
import { cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
import { type LOG_TYPES } from './schema'
import { type LOG_TYPES } from './UnifiedLogs.schema'
interface LogTypeIconProps {
type: (typeof LOG_TYPES)[number]
@@ -0,0 +1,10 @@
import { getLevelLabel } from './UnifiedLogs.utils'
export const TooltipLabel = ({ level }: { level: 'success' | 'warning' | 'error' }) => {
return (
<div className="mr-2 flex w-20 items-center justify-between gap-2 font-mono">
<div className="capitalize text-foreground/70">{level}</div>
<div className="text-xs text-muted-foreground/70">{getLevelLabel(level)}</div>
</div>
)
}
@@ -1,30 +1,42 @@
import {
createParser,
// createSearchParamsCache, // Removed for Pages Router compatibility
createSerializer, // Uncommented for Pages Router compatibility
parseAsArrayOf,
parseAsBoolean,
parseAsInteger,
parseAsString,
parseAsStringLiteral,
parseAsTimestamp,
type inferParserType,
useQueryStates, // Added for potential hook usage
} from 'nuqs'
// Note: import from 'nuqs/server' comment removed
import {
ARRAY_DELIMITER,
RANGE_DELIMITER,
SLIDER_DELIMITER,
SORT_DELIMITER,
} from 'components/interfaces/DataTableDemo/lib/delimiters'
import { REGIONS } from 'components/interfaces/DataTableDemo/constants/region'
import { METHODS } from 'components/interfaces/DataTableDemo/constants/method'
import { LEVELS } from 'components/interfaces/DataTableDemo/constants/levels'
// https://logs.run/i?sort=latency.desc
import { ChartConfig } from 'ui'
import { TooltipLabel } from './TooltipLabel'
export const parseAsSort = createParser({
export const CHART_CONFIG = {
success: {
label: <TooltipLabel level="success" />,
color: 'hsl(var(--foreground-muted))',
},
warning: {
label: <TooltipLabel level="warning" />,
color: 'hsl(var(--warning-default))',
},
error: {
label: <TooltipLabel level="error" />,
color: 'hsl(var(--destructive-default))',
},
} satisfies ChartConfig
export const ARRAY_DELIMITER = ','
const SLIDER_DELIMITER = '-'
const SPACE_DELIMITER = '_'
export const RANGE_DELIMITER = '-'
export const SORT_DELIMITER = '.'
const LEVELS = ['success', 'warning', 'error'] as const
const REGIONS = ['ams', 'fra', 'gru', 'hkg', 'iad', 'syd'] as const
const METHODS = ['GET', 'POST', 'PUT', 'DELETE'] as const
const parseAsSort = createParser({
parse(queryValue) {
const [id, desc] = queryValue.split(SORT_DELIMITER)
if (!id && !desc) return null
@@ -35,7 +47,7 @@ export const parseAsSort = createParser({
},
})
export const searchParamsParser = {
export const SEARCH_PARAMS_PARSER = {
// CUSTOM FILTERS
level: parseAsArrayOf(parseAsStringLiteral(LEVELS), ARRAY_DELIMITER),
log_type: parseAsArrayOf(parseAsString, ARRAY_DELIMITER),
@@ -62,14 +74,3 @@ export const searchParamsParser = {
// REQUIRED FOR SELECTION
uuid: parseAsString,
}
// Removed for Pages Router compatibility
// export const searchParamsCache = createSearchParamsCache(searchParamsParser)
export const searchParamsSerializer = createSerializer(searchParamsParser) // Uncommented
export type SearchParamsType = inferParserType<typeof searchParamsParser>
// Optional: Add a hook similar to the default demo if needed elsewhere
// export function useInfiniteSearchParams() {
// return useQueryStates(searchParamsParser)
// }
@@ -1,34 +1,9 @@
import {
ARRAY_DELIMITER,
RANGE_DELIMITER,
SLIDER_DELIMITER,
} from 'components/interfaces/DataTableDemo/lib/delimiters'
import { z } from 'zod'
import { LEVELS } from './constants/levels'
import { METHODS } from './constants/method'
import { REGIONS } from './constants/region'
import { z } from 'zod'
import { LEVELS } from './constants/levels'
// https://github.com/colinhacks/zod/issues/2985#issue-2008642190
const stringToBoolean = z
.string()
.toLowerCase()
.transform((val) => {
try {
return JSON.parse(val)
} catch (e) {
console.log(e)
return undefined
}
})
.pipe(z.boolean().optional())
// export const timingSchema = z.object({
// 'timing.dns': z.number(),
// 'timing.connection': z.number(),
// 'timing.tls': z.number(),
// 'timing.ttfb': z.number(),
// 'timing.transfer': z.number(),
// })
import { ARRAY_DELIMITER, RANGE_DELIMITER } from './UnifiedLogs.constants'
export const LOG_TYPES = [
'edge',
@@ -50,26 +25,18 @@ export const columnSchema = z.object({
host: z.string(),
pathname: z.string(),
level: z.enum(LEVELS),
// latency: z.number(),
status: z.number(),
// regions: z.enum(REGIONS).array(),
date: z.date(),
// headers: z.record(z.string()),
// message: z.string().optional(),
timestamp: z.number(),
event_message: z.string().optional(),
log_count: z.number().optional(), // used to count function logs for a given execution_id
// percentile: z.number().optional(),
has_trace: z.boolean().optional(),
logs: z.array(z.any()).optional(), // array of function logs
auth_user: z.string().optional(),
})
// .merge(timingSchema)
export type ColumnSchema = z.infer<typeof columnSchema>
// export type TimingSchema = z.infer<typeof timingSchema>
// TODO: can we get rid of this in favor of nuqs search-params?
export const columnFilterSchema = z.object({
level: z
.string()
@@ -83,36 +50,6 @@ export const columnFilterSchema = z.object({
.optional(),
host: z.string().optional(),
pathname: z.string().optional(),
// latency: z
// .string()
// .transform((val) => val.split(SLIDER_DELIMITER))
// .pipe(z.coerce.number().array().max(2))
// .optional(),
// 'timing.dns': z
// .string()
// .transform((val) => val.split(SLIDER_DELIMITER))
// .pipe(z.coerce.number().array().max(2))
// .optional(),
// 'timing.connection': z
// .string()
// .transform((val) => val.split(SLIDER_DELIMITER))
// .pipe(z.coerce.number().array().max(2))
// .optional(),
// 'timing.tls': z
// .string()
// .transform((val) => val.split(SLIDER_DELIMITER))
// .pipe(z.coerce.number().array().max(2))
// .optional(),
// 'timing.ttfb': z
// .string()
// .transform((val) => val.split(SLIDER_DELIMITER))
// .pipe(z.coerce.number().array().max(2))
// .optional(),
// 'timing.transfer': z
// .string()
// .transform((val) => val.split(SLIDER_DELIMITER))
// .pipe(z.coerce.number().array().max(2))
// .optional(),
status: z
.string()
.transform((val) => val.split(ARRAY_DELIMITER))
@@ -0,0 +1,10 @@
import { type inferParserType } from 'nuqs'
import { SEARCH_PARAMS_PARSER } from './UnifiedLogs.constants'
export type UnifiedLogsMeta = {
logTypeCounts: Record<string, number>
currentPercentiles: Record<string, number>
}
export type SearchParamsType = inferParserType<typeof SEARCH_PARAMS_PARSER>
@@ -0,0 +1,104 @@
// @ts-ignore
import { _LEVELS } from '@/constants/levels'
import { type Table as TTable } from '@tanstack/react-table'
import { useQueryState } from 'nuqs'
import { useHotKey } from 'hooks/ui/useHotKey'
import { useEffect, useMemo, useRef } from 'react'
import { cn } from 'ui'
import { SEARCH_PARAMS_PARSER } from './UnifiedLogs.constants'
import { FacetMetadataSchema } from './UnifiedLogs.schema'
export const useResetFocus = () => {
useHotKey(() => {
// FIXME: some dedicated div[tabindex="0"] do not auto-unblur (e.g. the DataTableFilterResetButton)
// REMINDER: we cannot just document.activeElement?.blur(); as the next tab will focus the next element in line,
// which is not what we want. We want to reset entirely.
document.body.setAttribute('tabindex', '0')
document.body.focus()
document.body.removeAttribute('tabindex')
}, '.')
}
// TODO: make a BaseObject (incl. date and uuid e.g. for every upcoming branch of infinite table)
export const useLiveMode = <TData extends { date: Date }>(data: TData[]) => {
const [live] = useQueryState('live', SEARCH_PARAMS_PARSER.live)
// REMINDER: used to capture the live mode on timestamp
const liveTimestamp = useRef<number | undefined>(live ? new Date().getTime() : undefined)
useEffect(() => {
if (live) liveTimestamp.current = new Date().getTime()
else liveTimestamp.current = undefined
}, [live])
const anchorRow = useMemo(() => {
if (!live) return undefined
const item = data.find((item) => {
// return first item that is there if not liveTimestamp
if (!liveTimestamp.current) return true
// return first item that is after the liveTimestamp
if (item.date.getTime() > liveTimestamp.current) return false
return true
// return first item if no liveTimestamp
})
return item
}, [live, data])
return { row: anchorRow, timestamp: liveTimestamp.current }
}
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
}
}
export const getLevelLabel = (value: (typeof _LEVELS)[number]): string => {
switch (value) {
case 'success':
return '2xx'
case 'warning':
return '4xx'
case 'error':
return '5xx'
default:
return 'Unknown'
}
}
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'
)
case 'info':
return cn(
'bg-info/5 hover:bg-info/10 data-[state=selected]:bg-info/20 focus-visible:bg-info/10',
'dark:bg-info/10 dark:hover:bg-info/20 dark:data-[state=selected]:bg-info/30 dark:focus-visible:bg-info/20'
)
default:
return ''
}
}
@@ -1,251 +0,0 @@
import dayjs from 'dayjs'
import { Rewind } from 'lucide-react'
import { useRouter } from 'next/router'
import { PropsWithChildren, useEffect, useState } from 'react'
import { useParams } from 'common'
import PreviewFilterPanel from 'components/interfaces/Settings/Logs/PreviewFilterPanel'
import LoadingOpacity from 'components/ui/LoadingOpacity'
import ShimmerLine from 'components/ui/ShimmerLine'
import useUnifiedLogs from 'hooks/analytics/useUnifiedLogs'
import { useLogsUrlState } from 'hooks/analytics/useLogsUrlState'
import { useSelectedLog } from 'hooks/analytics/useSelectedLog'
import useSingleLog from 'hooks/analytics/useSingleLog'
import { useFlag } from 'hooks/ui/useFlag'
import { Button } from 'ui'
import { LogsBarChart } from 'ui-patterns/LogsBarChart'
import LogTable from '../Settings/Logs/LogTable'
import { DatePickerValue } from '../Settings/Logs/Logs.DatePickers'
import { LogsTableName, PREVIEWER_DATEPICKER_HELPERS } from '../Settings/Logs/Logs.constants'
import type {
Filters,
LogSearchCallback,
LogTemplate,
QueryType,
} from '../Settings/Logs/Logs.types'
import { PreviewFilterPanelWithUniversal } from '../Settings/Logs/PreviewFilterPanelWithUniversal'
interface UnifiedLogsPreviewerProps {
projectRef: string
filterOverride?: Filters
condensedLayout?: boolean
EmptyState?: React.ReactNode
filterPanelClassName?: string
}
export const UnifiedLogsPreviewer = ({
projectRef,
filterOverride,
condensedLayout = false,
children,
EmptyState,
filterPanelClassName,
}: PropsWithChildren<UnifiedLogsPreviewerProps>) => {
const useUniversalFilterBar = useFlag('universalFilterBar')
const router = useRouter()
const { db } = useParams()
const [showChart, setShowChart] = useState(true)
const [selectedDatePickerValue, setSelectedDatePickerValue] = useState<DatePickerValue>(
getDefaultDatePickerValue()
)
const { search, setSearch, timestampStart, timestampEnd, setTimeRange, filters, setFilters } =
useLogsUrlState()
const [selectedLogId, setSelectedLogId] = useSelectedLog()
// Use the custom useUnifiedLogs hook
const {
error,
logData,
params,
newCount,
isLoading,
eventChartData,
isLoadingOlder,
loadOlder,
refresh,
} = useUnifiedLogs({ projectRef, filterOverride })
const {
data: selectedLog,
isLoading: isSelectedLogLoading,
error: selectedLogError,
} = useSingleLog({
projectRef,
id: selectedLogId ?? undefined,
queryType: 'unified',
paramsToMerge: params,
})
// TODO: Move this to useLogsUrlState to simplify LogsPreviewer
function getDefaultDatePickerValue() {
const iso_timestamp_start = router.query.iso_timestamp_start as string
const iso_timestamp_end = router.query.iso_timestamp_end as string
if (iso_timestamp_start && iso_timestamp_end) {
return {
to: iso_timestamp_end,
from: iso_timestamp_start,
text: `${dayjs(iso_timestamp_start).format('DD MMM, HH:mm')} - ${dayjs(iso_timestamp_end).format('DD MMM, HH:mm')}`,
isHelper: false,
}
}
const defaultDatePickerValue = PREVIEWER_DATEPICKER_HELPERS.find((x) => x.default)
return {
to: defaultDatePickerValue!.calcTo(),
from: defaultDatePickerValue!.calcFrom(),
text: defaultDatePickerValue!.text,
isHelper: true,
}
}
const onSelectTemplate = (template: LogTemplate) => {
setFilters({ ...filters, search_query: template.searchString })
}
// For helper date picker values, reset the timestamp start to prevent data caching
const handleRefresh = () => {
if (selectedDatePickerValue.isHelper) {
const helper = PREVIEWER_DATEPICKER_HELPERS.find(
(x) => x.text === selectedDatePickerValue.text
)
if (helper) {
const newTimestampStart = helper.calcFrom()
setTimeRange(newTimestampStart, timestampEnd)
}
}
refresh()
}
const handleSearch: LogSearchCallback = async (event, { query, to, from }) => {
if (event === 'search-input-change') {
setSearch(query || '')
setSelectedLogId(null)
} else if (event === 'event-chart-bar-click') {
setTimeRange(from || '', to || '')
} else if (event === 'datepicker-change') {
setTimeRange(from || '', to || '')
}
}
// Common props shared between both filter panel components to avoid duplication
const filterPanelProps = {
className: filterPanelClassName,
csvData: logData,
isLoading,
newCount,
onRefresh: handleRefresh,
onSearch: handleSearch,
defaultSearchValue: search,
defaultToValue: timestampEnd,
defaultFromValue: timestampStart,
queryUrl: `/project/${projectRef}/logs/explorer?q=${encodeURIComponent(
params.sql || ''
)}&its=${encodeURIComponent(timestampStart)}&ite=${encodeURIComponent(timestampEnd)}`,
onSelectTemplate,
filters,
onFiltersChange: setFilters,
table: LogsTableName.UNIFIED,
condensedLayout,
isShowingEventChart: showChart,
onToggleEventChart: () => setShowChart(!showChart),
onSelectedDatabaseChange: (id: string) => {
setFilters({ ...filters, database: id !== projectRef ? id : undefined })
const { db, ...params } = router.query
router.push({
pathname: router.pathname,
query: id !== projectRef ? { ...router.query, db: id } : params,
})
},
selectedDatePickerValue,
setSelectedDatePickerValue,
}
return (
<div className="flex-1 flex flex-col h-full">
{useUniversalFilterBar ? (
// Experimental Universal Filter Bar
<PreviewFilterPanelWithUniversal {...filterPanelProps} />
) : (
// Legacy Filter Panel
<PreviewFilterPanel {...filterPanelProps} />
)}
{children}
<div
className={
'transition-all duration-500 ' +
(showChart && logData.length > 0 ? 'mb-2 mt-1 opacity-100' : 'h-0 opacity-0')
}
>
<div className={condensedLayout ? 'px-3' : ''}>
{showChart && (
<LogsBarChart
data={eventChartData}
onBarClick={(datum) => {
if (!datum?.timestamp) return
const datumTimestamp = dayjs(datum.timestamp).toISOString()
const start = dayjs(datumTimestamp).subtract(1, 'minute').toISOString()
const end = dayjs(datumTimestamp).add(1, 'minute').toISOString()
handleSearch('event-chart-bar-click', {
query: filters.search_query?.toString(),
to: end,
from: start,
})
}}
EmptyState={
<div className="flex flex-col items-center justify-center h-[67px]">
<h2 className="text-foreground-light text-xs">No data</h2>
<p className="text-foreground-lighter text-xs">
It may take up to 24 hours for data to refresh
</p>
</div>
}
/>
)}
</div>
</div>
<div className="relative flex flex-col flex-grow flex-1 overflow-auto">
<ShimmerLine active={isLoading} />
<LoadingOpacity active={isLoading}>
<LogTable
projectRef={projectRef}
isLoading={isLoading}
data={logData}
queryType="unified"
isHistogramShowing={showChart}
onHistogramToggle={() => setShowChart(!showChart)}
error={error}
EmptyState={EmptyState}
onSelectedLogChange={(log) => setSelectedLogId(log?.id ?? null)}
selectedLog={selectedLog}
isSelectedLogLoading={isSelectedLogLoading}
selectedLogError={selectedLogError ?? undefined}
/>
</LoadingOpacity>
</div>
{!error && logData.length > 0 && (
<div className="border-t flex flex-row items-center gap-3 p-2">
<Button
onClick={loadOlder}
icon={<Rewind />}
type="default"
loading={isLoadingOlder}
disabled={isLoadingOlder}
>
Load older
</Button>
<div className="text-sm text-foreground-lighter">
Showing <span className="font-mono">{logData.length}</span> results
</div>
</div>
)}
</div>
)
}
export default UnifiedLogsPreviewer
@@ -0,0 +1,445 @@
import { useInfiniteQuery } from '@tanstack/react-query'
import {
ColumnFiltersState,
getCoreRowModel,
getFacetedRowModel,
getFilteredRowModel,
getSortedRowModel,
getFacetedMinMaxValues as getTTableFacetedMinMaxValues,
getFacetedUniqueValues as getTTableFacetedUniqueValues,
Row,
RowSelectionState,
SortingState,
useReactTable,
VisibilityState,
} from '@tanstack/react-table'
import { useQueryStates } from 'nuqs'
import { useEffect, useMemo, useState } from 'react'
// import { useLocalStorage } from 'components/interfaces/DataTableDemo/hooks/use-local-storage'
import { DataTableInfinite } from 'components/interfaces/data-table/data-table-infinite'
import { TimelineChart } from 'components/interfaces/data-table/timeline-chart'
import { DataTableFilterCommand } from 'components/interfaces/DataTableDemo/components/data-table/data-table-filter-command'
import { DataTableProvider } from 'components/interfaces/DataTableDemo/components/data-table/data-table-provider'
import { DataTableSheetDetails } from 'components/interfaces/DataTableDemo/components/data-table/data-table-sheet/data-table-sheet-details'
import { DataTableToolbar } from 'components/interfaces/DataTableDemo/components/data-table/data-table-toolbar'
import { LiveButton } from 'components/interfaces/DataTableDemo/infinite/_components/live-button'
import { LiveRow } from 'components/interfaces/DataTableDemo/infinite/_components/live-row'
import { RefreshButton } from 'components/interfaces/DataTableDemo/infinite/_components/refresh-button'
import { getLevelRowClassName } from 'components/interfaces/DataTableDemo/lib/request/level'
import { arrSome, inDateRange } from 'components/interfaces/DataTableDemo/lib/table/filterfns'
// components pulled out and modified
import { DataTableHeaderLayout } from '../data-table/data-table-header-layout'
import { DataTableSideBarLayout } from '../data-table/data-table-side-bar-layout'
import { FilterSideBar } from '../data-table/filter-side-bar'
// specific imports
import { useParams } from 'common'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import {
ChartConfig,
cn,
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
Separator,
Tabs_Shadcn_ as Tabs,
TabsContent_Shadcn_ as TabsContent,
TabsList_Shadcn_ as TabsList,
TabsTrigger_Shadcn_ as TabsTrigger,
} from 'ui'
import { columns, logEventBus } from './columns'
import { MemoizedDataTableSheetContent } from './components/data-table-sheet-content'
import { FunctionLogsTab } from './components/function-logs-tab'
import { TraceDetailTab } from './components/trace-detail-tab'
import { filterFields as defaultFilterFields, sheetFields } from './constants'
import { dataOptions, useChartData } from './query-options-new'
import { CHART_CONFIG, SEARCH_PARAMS_PARSER } from './UnifiedLogs.constants'
import { getFacetedUniqueValues, useLiveMode, useResetFocus } from './UnifiedLogs.utils'
// Debug mode flag - set to true to enable detailed logs
const DEBUG_FILTER_PROCESSING = false
/**
* [Joshen] Slowly... This branch is a huge mess... Needs a shit ton of refactoring
* - Deprecate infinite-logs-example
* - Deprecate data-table
* - Deprecate DataTableDemo
* - Deprecate Trace (?)
* - Rename logs-new to UnifiedLogs
* - Components all need to be renamed for follow convention
*/
export const UnifiedLogsTable = () => {
useResetFocus()
const { ref: projectRef } = useParams()
const [search, setSearch] = useQueryStates(SEARCH_PARAMS_PARSER)
const { sort, start, size, uuid, cursor, direction, live, ...filter } = search
const defaultColumnSorting = sort ? [sort] : []
const defaultColumnVisibility = { uuid: false }
const defaultRowSelection = search.uuid ? { [search.uuid]: true } : {}
const defaultColumnFilters = Object.entries(filter)
.map(([key, value]) => ({ id: key, value }))
.filter(({ value }) => value ?? undefined)
const [topBarHeight, setTopBarHeight] = useState(0)
const [activeTab, setActiveTab] = useState('details')
const [sorting, setSorting] = useState<SortingState>(defaultColumnSorting)
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>(defaultColumnFilters)
const [rowSelection, setRowSelection] = useState<RowSelectionState>(defaultRowSelection)
const [columnVisibility, setColumnVisibility] = useLocalStorageQuery<VisibilityState>(
'data-table-visibility',
defaultColumnVisibility
)
const [columnOrder, setColumnOrder] = useLocalStorageQuery<string[]>(
'data-table-column-order',
[]
)
const { data, isFetching, isLoading, fetchNextPage, hasNextPage, fetchPreviousPage, refetch } =
// @ts-ignore
useInfiniteQuery(dataOptions(search, projectRef ?? ''))
const flatData = useMemo(() => {
return data?.pages?.flatMap((page) => page.data ?? []) ?? []
}, [data?.pages])
const liveMode = useLiveMode(flatData)
// Add the chart data query for the entire time period
const { data: chartDataResult } = useChartData(search, projectRef ?? '')
// REMINDER: meta data is always the same for all pages as filters do not change(!)
const lastPage = data?.pages?.[data?.pages.length - 1]
// Use the totalCount from chartDataResult which gives us the actual count of logs in the time period
// instead of the hardcoded 10000 value
const totalDBRowCount = chartDataResult?.totalCount || lastPage?.meta?.totalRowCount
const filterDBRowCount = lastPage?.meta?.filterRowCount
const metadata = lastPage?.meta?.metadata
// Use chart data from the separate query if available, fallback to the default
const chartData = chartDataResult?.chartData || lastPage?.meta?.chartData
const facets = lastPage?.meta?.facets
const totalFetched = flatData?.length
// Create a filtered version of the chart config based on selected levels
const filteredChartConfig = useMemo(() => {
const levelFilter = search.level || ['success', 'warning', 'error']
return Object.fromEntries(
Object.entries(CHART_CONFIG).filter(([key]) => levelFilter.includes(key as any))
) as ChartConfig
}, [search.level])
const getRowClassName = <TData extends { date: Date; level: string; timestamp: number }>(
row: Row<TData>
) => {
const rowTimestamp = row.original.timestamp
const isPast = rowTimestamp <= (liveMode.timestamp || -1)
const levelClassName = getLevelRowClassName(row.original.level)
return cn(levelClassName, isPast ? 'opacity-50' : 'opacity-100')
}
const table = useReactTable({
data: flatData,
columns,
state: {
columnFilters,
sorting,
columnVisibility,
rowSelection,
columnOrder,
},
enableMultiRowSelection: false,
columnResizeMode: 'onChange',
filterFns: { inDateRange, arrSome },
meta: { getRowClassName },
getRowId: (row) => row.uuid,
onColumnVisibilityChange: setColumnVisibility,
onColumnFiltersChange: setColumnFilters,
onRowSelectionChange: setRowSelection,
onSortingChange: setSorting,
onColumnOrderChange: setColumnOrder,
getSortedRowModel: getSortedRowModel(),
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getTTableFacetedUniqueValues(),
getFacetedMinMaxValues: getTTableFacetedMinMaxValues(),
// Here, manually override the filter function for the level column
// to prevent client-side filtering since it's already filtered on the server
// Always return true to pass all level values
// columnFilterFns: { level: () => true },
// debugAll: process.env.NEXT_PUBLIC_TABLE_DEBUG === 'true',
})
const selectedRow = useMemo(() => {
if ((isLoading || isFetching) && !flatData.length) return
const selectedRowKey = Object.keys(rowSelection)?.[0]
return table.getCoreRowModel().flatRows.find((row) => row.id === selectedRowKey)
}, [rowSelection, table, isLoading, isFetching, flatData])
const selectedRowKey = Object.keys(rowSelection)?.[0]
// REMINDER: this is currently needed for the cmdk search
// TODO: auto search via API when the user changes the filter instead of hardcoded
const filterFields = useMemo(() => {
return defaultFilterFields.map((field) => {
const facetsField = facets?.[field.value]
if (!facetsField) return field
if (field.options && field.options.length > 0) return field
// REMINDER: if no options are set, we need to set them via the API
const options = facetsField.rows.map(({ value }) => ({ label: `${value}`, value }))
if (field.type === ('slider' as any)) {
return {
...(field as any),
min: facetsField.min ?? (field as any).min,
max: facetsField.max ?? (field as any).max,
options,
}
}
return { ...field, options }
})
}, [facets])
useEffect(() => {
if (DEBUG_FILTER_PROCESSING) console.log('========== FILTER CHANGE DETECTED ==========')
if (DEBUG_FILTER_PROCESSING) console.log('Raw columnFilters:', JSON.stringify(columnFilters))
// Check for level filters specifically
const levelColumnFilter = columnFilters.find((filter) => filter.id === 'level')
if (DEBUG_FILTER_PROCESSING) console.log('Level column filter:', levelColumnFilter)
const columnFiltersWithNullable = filterFields.map((field) => {
const filterValue = columnFilters.find((filter) => filter.id === field.value)
if (DEBUG_FILTER_PROCESSING) console.log(`Processing field ${field.value}:`, filterValue)
if (!filterValue) return { id: field.value, value: null }
return { id: field.value, value: filterValue.value }
})
// Debug level filter specifically
const levelFilter = columnFiltersWithNullable.find((f) => f.id === 'level')
if (DEBUG_FILTER_PROCESSING) console.log('Level filter after mapping:', levelFilter)
if (DEBUG_FILTER_PROCESSING)
console.log('All column filters after mapping:', columnFiltersWithNullable)
const search = columnFiltersWithNullable.reduce(
(prev, curr) => {
if (DEBUG_FILTER_PROCESSING)
console.log(`Processing filter for URL: ${curr.id}`, {
value: curr.value,
type: Array.isArray(curr.value) ? 'array' : typeof curr.value,
isEmpty: Array.isArray(curr.value) && curr.value.length === 0,
isNull: curr.value === null,
})
// Add to search parameters
prev[curr.id as string] = curr.value
return prev
},
{} as Record<string, unknown>
)
if (DEBUG_FILTER_PROCESSING) console.log('Final search object to be set in URL:', search)
if (DEBUG_FILTER_PROCESSING) console.log('Level value in final search:', search.level)
if (DEBUG_FILTER_PROCESSING) console.log('Is level in search object:', 'level' in search)
// Set the search state without any console logs
if (DEBUG_FILTER_PROCESSING) console.log('CALLING setSearch with:', JSON.stringify(search))
setSearch(search)
if (DEBUG_FILTER_PROCESSING) console.log('========== END FILTER PROCESSING ==========')
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [columnFilters])
useEffect(() => {
setSearch({ sort: sorting?.[0] || null })
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sorting])
// TODO: can only share uuid within the first batch
useEffect(() => {
if (isLoading || isFetching) return
if (Object.keys(rowSelection)?.length && !selectedRow) {
setSearch({ uuid: null })
setRowSelection({})
} else {
setSearch({ uuid: Object.keys(rowSelection)?.[0] || null })
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [rowSelection, selectedRow, isLoading, isFetching])
// Set up event listener for trace tab selection
useEffect(() => {
const unsubscribe = logEventBus.on('selectTraceTab', (rowId) => {
setRowSelection({ [rowId]: true })
setActiveTab('trace')
})
return () => {
unsubscribe()
}
}, [setRowSelection])
return (
<DataTableProvider
table={table}
columns={columns}
filterFields={filterFields}
columnFilters={columnFilters}
sorting={sorting}
rowSelection={rowSelection}
columnOrder={columnOrder}
columnVisibility={columnVisibility}
enableColumnOrdering={true}
isLoading={isFetching || isLoading}
getFacetedUniqueValues={getFacetedUniqueValues(facets)}
// getFacetedMinMaxValues={getFacetedMinMaxValues(facets)}
>
<DataTableSideBarLayout topBarHeight={topBarHeight}>
<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} />
<DataTableToolbar
renderActions={() => [
<RefreshButton key="refresh" onClick={refetch} />,
fetchPreviousPage ? (
<LiveButton key="live" fetchPreviousPage={fetchPreviousPage} />
) : null,
]}
/>
<TimelineChart
data={chartData ?? []}
className="-mb-2"
columnId="timestamp"
chartConfig={filteredChartConfig}
/>
</DataTableHeaderLayout>
<Separator />
<ResizablePanelGroup direction="horizontal" className="w-full h-full">
<ResizablePanel defaultSize={selectedRowKey ? 60 : 100} minSize={30} className="h-full">
<ResizablePanelGroup key="main-logs" direction="vertical" className="h-full">
<ResizablePanel defaultSize={100} minSize={30}>
<div className="h-full overflow-auto">
<DataTableInfinite
columns={columns}
totalRows={totalDBRowCount}
filterRows={filterDBRowCount}
totalRowsFetched={totalFetched}
isFetching={isFetching}
isLoading={isLoading}
fetchNextPage={fetchNextPage}
hasNextPage={hasNextPage}
renderLiveRow={(props) => {
if (!liveMode.timestamp) return null
if (props?.row.original.uuid !== liveMode?.row?.uuid) return null
return <LiveRow />
}}
setColumnOrder={setColumnOrder}
setColumnVisibility={setColumnVisibility}
/>
</div>
</ResizablePanel>
{selectedRow?.original?.logs && selectedRow?.original?.logs?.length > 0 && (
<>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={20} minSize={20}>
<div className="h-full flex flex-col overflow-hidden">
<div className="px-5 py-3 border-b border-border flex justify-between items-center">
<h3 className="text-sm font-medium">
Function Logs (
{selectedRow?.original?.logs &&
Array.isArray(selectedRow?.original?.logs)
? selectedRow?.original?.logs?.length
: 0}
)
</h3>
</div>
<div className="flex-grow overflow-auto">
<FunctionLogsTab logs={selectedRow?.original?.logs} />
</div>
</div>
</ResizablePanel>
</>
)}
</ResizablePanelGroup>
</ResizablePanel>
{selectedRowKey && (
<>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={25} minSize={25}>
<ResizablePanelGroup direction="vertical">
<ResizablePanel defaultSize={100} minSize={30}>
<div className="h-full overflow-auto">
<DataTableSheetDetails
title={selectedRow?.original?.pathname}
titleClassName="font-mono"
>
<Tabs
defaultValue="details"
value={activeTab}
onValueChange={setActiveTab}
className="w-full h-full flex flex-col pt-4"
>
<TabsList className="mb-2 flex gap-3 px-5">
<TabsTrigger value="details">Log Details</TabsTrigger>
<TabsTrigger value="trace">Trace</TabsTrigger>
</TabsList>
<TabsContent
value="details"
className="flex-grow overflow-auto data-[state=active]:flex-grow px-5"
>
<MemoizedDataTableSheetContent
table={table}
data={selectedRow?.original}
filterFields={filterFields}
fields={sheetFields}
metadata={{
totalRows: totalDBRowCount ?? 0,
filterRows: filterDBRowCount ?? 0,
totalRowsFetched: totalFetched ?? 0,
currentPercentiles: metadata?.currentPercentiles ?? ({} as any),
...metadata,
}}
/>
</TabsContent>
<TabsContent
value="trace"
className="flex-grow overflow-auto data-[state=active]:flex-grow h-full mt-0 px-5"
>
{selectedRow?.original?.has_trace ? (
<TraceDetailTab id={selectedRow?.original?.id} />
) : (
<div className="flex flex-col gap-2">
<p className="text-sm text-muted-foreground">
No trace found for this log
</p>
</div>
)}
</TabsContent>
</Tabs>
</DataTableSheetDetails>
</div>
</ResizablePanel>
</ResizablePanelGroup>
</ResizablePanel>
</>
)}
</ResizablePanelGroup>
</div>
</DataTableSideBarLayout>
</DataTableProvider>
)
}
@@ -1,35 +1,15 @@
'use client'
import type { ColumnDef } from '@tanstack/react-table'
import { Bolt } from 'lucide-react'
import { TextWithTooltip } from 'components/interfaces/DataTableDemo/components/custom/text-with-tooltip'
import { DataTableColumnHeader } from 'components/interfaces/DataTableDemo/components/data-table/data-table-column-header'
import { DataTableColumnLatency } from 'components/interfaces/DataTableDemo/components/data-table/data-table-column/data-table-column-latency'
import { DataTableColumnLevelIndicator } from 'components/interfaces/DataTableDemo/components/data-table/data-table-column/data-table-column-level-indicator'
import { DataTableColumnRegion } from 'components/interfaces/DataTableDemo/components/data-table/data-table-column/data-table-column-region'
import { DataTableColumnStatusCode } from 'components/interfaces/DataTableDemo/components/data-table/data-table-column/data-table-column-status-code'
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
TooltipTrigger,
Tooltip,
TooltipContent,
} from 'ui'
import {
getTimingColor,
getTimingLabel,
getTimingPercentage,
timingPhases,
} from 'components/interfaces/DataTableDemo/lib/request/timing'
import { cn } from 'ui'
import { HoverCardPortal } from '@radix-ui/react-hover-card'
import type { ColumnDef, FilterFn } from '@tanstack/react-table'
import { Bolt, Minus, Pyramid, User, Zap } from 'lucide-react'
import { HoverCardTimestamp } from 'components/interfaces/DataTableDemo/infinite/_components/hover-card-timestamp'
// custom imports
import type { ColumnSchema } from './schema'
import { LogTypeIcon } from './LogTypeIcon'
import { Tooltip, TooltipContent, TooltipTrigger } from 'ui'
import { AuthUserHoverCard } from './components/auth-user-hover-card'
import { LogTypeIcon } from './LogTypeIcon'
import type { ColumnSchema } from './UnifiedLogs.schema'
// Event broadcaster for selecting the trace tab
// This will be used to communicate between the columns and the client component
@@ -88,39 +68,6 @@ export const columns: ColumnDef<ColumnSchema>[] = [
'font-mono w-[--col-date-size] max-w-[--col-date-size] min-w-[--col-date-size]',
},
},
// {
// accessorKey: 'id',
// header: ({ column }) => <DataTableColumnHeader column={column} title="ID" />,
// cell: ({ row }) => {
// const value = row.getValue<ColumnSchema['id']>('id')
// return <TextWithTooltip text={value} />
// },
// size: 130,
// minSize: 130,
// meta: {
// headerClassName: 'w-[--header-id-size] max-w-[--header-id-size] min-w-[--header-id-size]',
// cellClassName: 'w-[--col-id-size] max-w-[--col-id-size] min-w-[--col-id-size]',
// },
// },
// {
// id: 'uuid',
// accessorKey: 'uuid',
// header: 'Request Id',
// cell: ({ row }) => {
// const value = row.getValue<ColumnSchema['uuid']>('uuid')
// return <TextWithTooltip text={value} />
// },
// enableResizing: false,
// size: 130,
// minSize: 130,
// meta: {
// label: 'Request Id',
// cellClassName:
// 'font-mono w-[--col-uuid-size] max-w-[--col-uuid-size] min-w-[--col-uuid-size]',
// headerClassName:
// 'min-w-[--header-uuid-size] w-[--header-uuid-size] max-w-[--header-uuid-size]',
// },
// },
{
accessorKey: 'log_type',
header: '',
@@ -238,57 +185,6 @@ export const columns: ColumnDef<ColumnSchema>[] = [
headerClassName: 'min-w-[--header-pathname-size] w-[--header-pathname-size]',
},
},
// {
// accessorKey: 'latency',
// // TODO: check if we can right align the table header/cell (makes is easier to read)
// header: ({ column }) => <DataTableColumnHeader column={column} title="Latency" />,
// cell: ({ row }) => {
// const value = row.getValue<ColumnSchema['latency']>('latency')
// return <DataTableColumnLatency value={value} />
// },
// filterFn: 'inNumberRange',
// enableResizing: false,
// size: 110,
// minSize: 110,
// meta: {
// headerClassName:
// 'w-[--header-latency-size] max-w-[--header-latency-size] min-w-[--header-latency-size]',
// cellClassName:
// 'font-mono w-[--col-latency-size] max-w-[--col-latency-size] min-w-[--col-latency-size]',
// },
// },
// {
// accessorKey: 'regions',
// header: 'Region',
// cell: ({ row }) => {
// const value = row.getValue<ColumnSchema['regions']>('regions')
// if (Array.isArray(value)) {
// if (value.length > 1) {
// return <div className="text-muted-foreground">{value.join(', ')}</div>
// } else {
// return (
// <div className="whitespace-nowrap">
// <DataTableColumnRegion value={value[0]} />
// </div>
// )
// }
// }
// if (typeof value === 'string') {
// return <DataTableColumnRegion value={value} />
// }
// return <Minus className="h-4 w-4 text-muted-foreground/50" />
// },
// filterFn: 'arrIncludesSome',
// enableResizing: false,
// size: 163,
// minSize: 163,
// meta: {
// headerClassName:
// 'w-[--header-regions-size] max-w-[--header-regions-size] min-w-[--header-regions-size]',
// cellClassName:
// 'font-mono w-[--col-regions-size] max-w-[--col-regions-size] min-w-[--col-regions-size]',
// },
// },
{
accessorKey: 'event_message',
header: ({ column }) => <DataTableColumnHeader column={column} title="event_message" />,
@@ -318,10 +214,9 @@ export const columns: ColumnDef<ColumnSchema>[] = [
</div>
)
},
enableResizing: true, // allow manual resizing
enableResizing: true,
size: 400,
minSize: 400,
// ❌ don't set maxSize — let it grow
meta: {
headerClassName:
'w-[--header-event_message-size] max-w-[--header-event_message-size] min-w-[--header-event_message-size]',
@@ -1,8 +1,6 @@
'use client'
import { format } from 'date-fns'
import { User } from 'lucide-react'
import { CopyToClipboardContainer } from 'components/interfaces/DataTableDemo/components/custom/copy-to-clipboard-container'
import { KVTabs } from 'components/interfaces/DataTableDemo/components/custom/kv-tabs'
import { DataTableColumnRegion } from 'components/interfaces/DataTableDemo/components/data-table/data-table-column/data-table-column-region'
import type {
DataTableFilterField,
Option,
@@ -10,20 +8,14 @@ import type {
} from 'components/interfaces/DataTableDemo/components/data-table/types'
import { LEVELS } from 'components/interfaces/DataTableDemo/constants/levels'
import { METHODS } from 'components/interfaces/DataTableDemo/constants/method'
import { REGIONS } from 'components/interfaces/DataTableDemo/constants/region'
import { formatMilliseconds } from 'components/interfaces/DataTableDemo/lib/format'
import { getLevelColor, getLevelLabel } from 'components/interfaces/DataTableDemo/lib/request/level'
import { getStatusColor } from 'components/interfaces/DataTableDemo/lib/request/status-code'
import { cn } from 'ui'
import { format } from 'date-fns'
import { PopoverPercentile } from 'components/interfaces/DataTableDemo/infinite/_components/popover-percentile'
import { SheetTimingPhases } from 'components/interfaces/DataTableDemo/infinite/_components/sheet-timing-phases'
import { User } from 'lucide-react'
// custom imports
import type { LogsMeta } from './query-options'
import { type ColumnSchema, LOG_TYPES } from './schema'
import { TraceButton } from './components/trace-button'
import type { LogsMeta } from './query-options'
import { type ColumnSchema, LOG_TYPES } from './UnifiedLogs.schema'
// instead of filterFields, maybe just 'fields' with a filterDisabled prop?
// that way, we could have 'message' or 'headers' field with label and value as well as type!
@@ -4,7 +4,7 @@
import dayjs from 'dayjs'
import { BASE_CONDITIONS_EXCLUDED_PARAMS, EXCLUDED_QUERY_PARAMS } from './constants/query-params'
import { SearchParamsType } from './search-params'
import { SearchParamsType } from './UnifiedLogs.types'
// Types for plan IDs - import actual type if available
type PlanId = 'free' | 'pro' | 'team' | 'enterprise'
@@ -1,11 +1,11 @@
import { useQuery } from '@tanstack/react-query'
import { ARRAY_DELIMITER, SORT_DELIMITER } from 'components/interfaces/DataTableDemo/lib/delimiters'
import { post } from 'data/fetchers'
import dayjs from 'dayjs'
import { post } from 'data/fetchers'
import { getLogsChartQuery, getLogsCountQuery, getUnifiedLogsQuery } from './logs.queries'
import type { BaseChartSchema, FacetMetadataSchema } from './schema'
import { ColumnSchema } from './schema'
import type { SearchParamsType } from './search-params'
import { ARRAY_DELIMITER, SORT_DELIMITER } from './UnifiedLogs.constants'
import { ColumnSchema, type BaseChartSchema, type FacetMetadataSchema } from './UnifiedLogs.schema'
import type { SearchParamsType, UnifiedLogsMeta } from './UnifiedLogs.types'
// Debug mode flag - set to true to enable detailed logs
const DEBUG_MODE = false
@@ -23,11 +23,6 @@ export type UnifiedLogSchema = {
auth_user: string | null
}
export type UnifiedLogsMeta = {
logTypeCounts: Record<string, number>
currentPercentiles: Record<string, number>
}
// Extended column schema to include raw timestamp
export type ExtendedColumnSchema = ColumnSchema & {
timestamp: string // Original database timestamp
@@ -279,15 +274,15 @@ export const dataOptions = (search: SearchParamsType, projectRef: string) => {
)
// Calculate the appropriate initial cursor based on the selected date range
const getInitialCursor = () => {
if (search.date && search.date.length === 2) {
// Use the end of the selected date range
return new Date(search.date[1]).getTime()
} else {
// Default to current time if no date range is selected
return new Date().getTime()
}
}
// const getInitialCursor = () => {
// if (search.date && search.date.length === 2) {
// // Use the end of the selected date range
// return new Date(search.date[1]).getTime()
// } else {
// // Default to current time if no date range is selected
// return new Date().getTime()
// }
// }
// Simply return the options object
return {
@@ -2,15 +2,10 @@ import type { Percentile } from 'components/interfaces/DataTableDemo/lib/request
// Removed keepPreviousData import as it causes persistent lint errors
// import { keepPreviousData } from '@tanstack/react-query'
import SuperJSON from 'superjson'
import type { BaseChartSchema, ColumnSchema, FacetMetadataSchema } from './schema'
import type { SearchParamsType } from './search-params'
import type { BaseChartSchema, ColumnSchema, FacetMetadataSchema } from './UnifiedLogs.schema'
// Removed: import { searchParamsSerializer } from './search-params'
import {
ARRAY_DELIMITER,
RANGE_DELIMITER,
SLIDER_DELIMITER,
SORT_DELIMITER,
} from 'components/interfaces/DataTableDemo/lib/delimiters'
import { ARRAY_DELIMITER, SORT_DELIMITER } from 'components/interfaces/DataTableDemo/lib/delimiters'
import { SearchParamsType } from './UnifiedLogs.types'
// Restore the helper function to create query params string
const createApiQueryString = (params: Record<string, any>): string => {
@@ -1,552 +0,0 @@
'use client'
import { useQueryState, useQueryStates } from 'nuqs'
import * as React from 'react'
import { useInfiniteQuery } from '@tanstack/react-query'
import {
ColumnFiltersState,
getCoreRowModel,
getFacetedRowModel,
getFilteredRowModel,
getSortedRowModel,
getFacetedMinMaxValues as getTTableFacetedMinMaxValues,
getFacetedUniqueValues as getTTableFacetedUniqueValues,
Row,
RowSelectionState,
SortingState,
useReactTable,
VisibilityState,
type Table as TTable,
} from '@tanstack/react-table'
import { DataTableInfinite } from 'components/interfaces/data-table/data-table-infinite'
import { TimelineChart } from 'components/interfaces/data-table/timeline-chart'
import { useHotKey } from 'components/interfaces/DataTableDemo/hooks/use-hot-key'
import { LiveRow } from 'components/interfaces/DataTableDemo/infinite/_components/live-row'
import {
getLevelLabel,
getLevelRowClassName,
} from 'components/interfaces/DataTableDemo/lib/request/level'
import { arrSome, inDateRange } from 'components/interfaces/DataTableDemo/lib/table/filterfns'
import { ChartConfig, cn, Separator } from 'ui'
import { DataTableFilterCommand } from 'components/interfaces/DataTableDemo/components/data-table/data-table-filter-command'
import {
DataTableProvider,
useDataTable,
} from 'components/interfaces/DataTableDemo/components/data-table/data-table-provider'
import { DataTableSheetDetails } from 'components/interfaces/DataTableDemo/components/data-table/data-table-sheet/data-table-sheet-details'
import { DataTableToolbar } from 'components/interfaces/DataTableDemo/components/data-table/data-table-toolbar'
import { useLocalStorage } from 'components/interfaces/DataTableDemo/hooks/use-local-storage'
import { LiveButton } from 'components/interfaces/DataTableDemo/infinite/_components/live-button'
import { RefreshButton } from 'components/interfaces/DataTableDemo/infinite/_components/refresh-button'
import { Percentile } from 'components/interfaces/DataTableDemo/lib/request/percentile'
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from 'ui/src/components/shadcn/ui/resizable'
import { Button } from 'ui'
import { X } from 'lucide-react'
// components pulled out and modified
import { FilterSideBar } from '../data-table/filter-side-bar'
import { DataTableHeaderLayout } from '../data-table/data-table-header-layout'
import { DataTableSideBarLayout } from '../data-table/data-table-side-bar-layout'
// specific imports
import { MemoizedDataTableSheetContent } from './components/data-table-sheet-content'
import type { FacetMetadataSchema, TimelineChartSchema } from './schema'
import { columns, logEventBus } from './columns'
import { searchParamsParser } from './search-params'
import { dataOptions, useChartData } from './query-options-new'
import { filterFields as defaultFilterFields, sheetFields } from './constants'
import { useParams } from 'common'
import { TraceDetailTab } from './components/trace-detail-tab'
import { Tabs, TabsContent, TabsList, TabsTrigger } from 'ui/src/components/shadcn/ui/tabs'
import { FunctionLogsTab } from './components/function-logs-tab'
// Debug mode flag - set to true to enable detailed logs
const DEBUG_FILTER_PROCESSING = false
function TooltipLabel({ level }: { level: keyof Omit<TimelineChartSchema, 'timestamp'> }) {
return (
<div className="mr-2 flex w-20 items-center justify-between gap-2 font-mono">
<div className="capitalize text-foreground/70">{level}</div>
<div className="text-xs text-muted-foreground/70">{getLevelLabel(level)}</div>
</div>
)
}
const chartConfig = {
success: {
label: <TooltipLabel level="success" />,
color: 'hsl(var(--foreground-muted))',
},
warning: {
label: <TooltipLabel level="warning" />,
color: 'hsl(var(--warning-default))',
},
error: {
label: <TooltipLabel level="error" />,
color: 'hsl(var(--destructive-default))',
},
} satisfies ChartConfig
export function Client() {
const [search, setSearch] = useQueryStates(searchParamsParser)
const { ref: projectRef } = useParams()
const [activeTab, setActiveTab] = React.useState('details')
const { data, isFetching, isLoading, fetchNextPage, hasNextPage, fetchPreviousPage, refetch } =
useInfiniteQuery(dataOptions(search, projectRef ?? ''))
// Add the chart data query for the entire time period
const { data: chartDataResult, isLoading: isChartLoading } = useChartData(
search,
projectRef ?? ''
)
useResetFocus()
const [topBarHeight, setTopBarHeight] = React.useState(0)
const flatData = React.useMemo(() => {
return data?.pages?.flatMap((page) => page.data ?? []) ?? []
}, [data?.pages])
const liveMode = useLiveMode(flatData)
// REMINDER: meta data is always the same for all pages as filters do not change(!)
const lastPage = data?.pages?.[data?.pages.length - 1]
// Use the totalCount from chartDataResult which gives us the actual count of logs in the time period
// instead of the hardcoded 10000 value
const totalDBRowCount = chartDataResult?.totalCount || lastPage?.meta?.totalRowCount
const filterDBRowCount = lastPage?.meta?.filterRowCount
const metadata = lastPage?.meta?.metadata
// Use chart data from the separate query if available, fallback to the default
const chartData = chartDataResult?.chartData || lastPage?.meta?.chartData
const facets = lastPage?.meta?.facets
const totalFetched = flatData?.length
const { sort, start, size, uuid, cursor, direction, live, ...filter } = search
// Create a filtered version of the chart config based on selected levels
const filteredChartConfig = React.useMemo(() => {
const levelFilter = search.level || ['success', 'warning', 'error']
return Object.fromEntries(
Object.entries(chartConfig).filter(([key]) => levelFilter.includes(key))
) as ChartConfig
}, [search.level])
const defaultColumnFilters = Object.entries(filter)
.map(([key, value]) => ({
id: key,
value,
}))
.filter(({ value }) => value ?? undefined)
const defaultColumnSorting = sort ? [sort] : []
const defaultColumnVisibility = {
uuid: false,
}
const defaultRowSelection = search.uuid ? { [search.uuid]: true } : {}
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(defaultColumnFilters)
const [sorting, setSorting] = React.useState<SortingState>(defaultColumnSorting)
const [columnVisibility, setColumnVisibility] = useLocalStorage<VisibilityState>(
'data-table-visibility',
defaultColumnVisibility
)
const [rowSelection, setRowSelection] = React.useState<RowSelectionState>(defaultRowSelection)
const [columnOrder, setColumnOrder] = useLocalStorage<string[]>('data-table-column-order', [])
// REMINDER: this is currently needed for the cmdk search
// TODO: auto search via API when the user changes the filter instead of hardcoded
const filterFields = React.useMemo(() => {
return defaultFilterFields.map((field) => {
const facetsField = facets?.[field.value]
if (!facetsField) return field
if (field.options && field.options.length > 0) return field
// REMINDER: if no options are set, we need to set them via the API
const options = facetsField.rows.map(({ value }) => {
return {
label: `${value}`,
value,
}
})
if (field.type === ('slider' as any)) {
return {
...(field as any),
min: facetsField.min ?? (field as any).min,
max: facetsField.max ?? (field as any).max,
options,
}
}
return { ...field, options }
})
}, [facets])
const getRowClassName = <TData extends { date: Date; level: string; timestamp: number }>(
row: Row<TData>
) => {
const rowTimestamp = row.original.timestamp
const isPast = rowTimestamp <= (liveMode.timestamp || -1)
const levelClassName = getLevelRowClassName(row.original.level)
return cn(levelClassName, isPast ? 'opacity-50' : 'opacity-100')
}
const table = useReactTable({
data: flatData,
columns,
state: {
columnFilters,
sorting,
columnVisibility,
rowSelection,
columnOrder,
},
enableMultiRowSelection: false,
columnResizeMode: 'onChange',
getRowId: (row) => row.uuid,
onColumnVisibilityChange: setColumnVisibility,
onColumnFiltersChange: setColumnFilters,
onRowSelectionChange: setRowSelection,
onSortingChange: setSorting,
onColumnOrderChange: setColumnOrder,
getSortedRowModel: getSortedRowModel(),
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getTTableFacetedUniqueValues(),
getFacetedMinMaxValues: getTTableFacetedMinMaxValues(),
filterFns: { inDateRange, arrSome },
// Here, manually override the filter function for the level column
// to prevent client-side filtering since it's already filtered on the server
columnFilterFns: {
level: () => true, // Always return true to pass all level values
},
// debugAll: process.env.NEXT_PUBLIC_TABLE_DEBUG === 'true',
meta: { getRowClassName },
})
const selectedRow = React.useMemo(() => {
if ((isLoading || isFetching) && !flatData.length) return
const selectedRowKey = Object.keys(rowSelection)?.[0]
return table.getCoreRowModel().flatRows.find((row) => row.id === selectedRowKey)
}, [rowSelection, table, isLoading, isFetching, flatData])
const selectedRowKey = Object.keys(rowSelection)?.[0]
React.useEffect(() => {
if (DEBUG_FILTER_PROCESSING) console.log('========== FILTER CHANGE DETECTED ==========')
if (DEBUG_FILTER_PROCESSING) console.log('Raw columnFilters:', JSON.stringify(columnFilters))
// Check for level filters specifically
const levelColumnFilter = columnFilters.find((filter) => filter.id === 'level')
if (DEBUG_FILTER_PROCESSING) console.log('Level column filter:', levelColumnFilter)
const columnFiltersWithNullable = filterFields.map((field) => {
const filterValue = columnFilters.find((filter) => filter.id === field.value)
if (DEBUG_FILTER_PROCESSING) console.log(`Processing field ${field.value}:`, filterValue)
if (!filterValue) return { id: field.value, value: null }
return { id: field.value, value: filterValue.value }
})
// Debug level filter specifically
const levelFilter = columnFiltersWithNullable.find((f) => f.id === 'level')
if (DEBUG_FILTER_PROCESSING) console.log('Level filter after mapping:', levelFilter)
if (DEBUG_FILTER_PROCESSING)
console.log('All column filters after mapping:', columnFiltersWithNullable)
const search = columnFiltersWithNullable.reduce(
(prev, curr) => {
if (DEBUG_FILTER_PROCESSING)
console.log(`Processing filter for URL: ${curr.id}`, {
value: curr.value,
type: Array.isArray(curr.value) ? 'array' : typeof curr.value,
isEmpty: Array.isArray(curr.value) && curr.value.length === 0,
isNull: curr.value === null,
})
// Add to search parameters
prev[curr.id as string] = curr.value
return prev
},
{} as Record<string, unknown>
)
if (DEBUG_FILTER_PROCESSING) console.log('Final search object to be set in URL:', search)
if (DEBUG_FILTER_PROCESSING) console.log('Level value in final search:', search.level)
if (DEBUG_FILTER_PROCESSING) console.log('Is level in search object:', 'level' in search)
// Set the search state without any console logs
if (DEBUG_FILTER_PROCESSING) console.log('CALLING setSearch with:', JSON.stringify(search))
setSearch(search)
if (DEBUG_FILTER_PROCESSING) console.log('========== END FILTER PROCESSING ==========')
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [columnFilters])
React.useEffect(() => {
setSearch({ sort: sorting?.[0] || null })
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sorting])
// TODO: can only share uuid within the first batch
React.useEffect(() => {
if (isLoading || isFetching) return
if (Object.keys(rowSelection)?.length && !selectedRow) {
setSearch({ uuid: null })
setRowSelection({})
} else {
setSearch({ uuid: Object.keys(rowSelection)?.[0] || null })
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [rowSelection, selectedRow, isLoading, isFetching])
// Set up event listener for trace tab selection
React.useEffect(() => {
const unsubscribe = logEventBus.on('selectTraceTab', (rowId) => {
// Select the row
setRowSelection({ [rowId]: true })
// Set the active tab to trace
setActiveTab('trace')
})
return () => {
unsubscribe()
}
}, [setRowSelection])
return (
<>
<DataTableProvider
table={table}
columns={columns}
filterFields={filterFields}
columnFilters={columnFilters}
sorting={sorting}
rowSelection={rowSelection}
columnOrder={columnOrder}
columnVisibility={columnVisibility}
enableColumnOrdering={true}
isLoading={isFetching || isLoading}
getFacetedUniqueValues={getFacetedUniqueValues(facets)}
// getFacetedMinMaxValues={getFacetedMinMaxValues(facets)}
>
<DataTableSideBarLayout topBarHeight={topBarHeight}>
<FilterSideBar />
<div className="flex max-w-full flex-1 flex-col border-border sm:border-l overflow-hidden">
<DataTableHeaderLayout setTopBarHeight={setTopBarHeight}>
<DataTableFilterCommand searchParamsParser={searchParamsParser} />
<DataTableToolbar
renderActions={() => [
<RefreshButton key="refresh" onClick={refetch} />,
fetchPreviousPage ? (
<LiveButton key="live" fetchPreviousPage={fetchPreviousPage} />
) : null,
]}
/>
<TimelineChart
data={chartData ?? []}
className="-mb-2"
columnId="timestamp"
chartConfig={filteredChartConfig}
/>
</DataTableHeaderLayout>
<Separator />
{/* Use ResizablePanelGroup for the log list and details */}
{/* <div className="flex flex-1 overflow-hidden"> */}
<ResizablePanelGroup direction="horizontal" className="w-full h-full">
<ResizablePanel
defaultSize={selectedRowKey ? 60 : 100}
minSize={30}
className="h-full"
>
<ResizablePanelGroup key="main-logs" direction="vertical" className="h-full">
<ResizablePanel defaultSize={100} minSize={30}>
<div className="h-full overflow-auto">
<DataTableInfinite
columns={columns}
totalRows={totalDBRowCount}
filterRows={filterDBRowCount}
totalRowsFetched={totalFetched}
isFetching={isFetching}
isLoading={isLoading}
fetchNextPage={fetchNextPage}
hasNextPage={hasNextPage}
renderLiveRow={(props) => {
if (!liveMode.timestamp) return null
if (props?.row.original.uuid !== liveMode?.row?.uuid) return null
return <LiveRow />
}}
setColumnOrder={setColumnOrder}
setColumnVisibility={setColumnVisibility}
/>
</div>
</ResizablePanel>
{/* <ResizableHandle withHandle />
<ResizablePanel defaultSize={20} minSize={20} className="bg">
hello world
<pre className="text-xs p-2">
{JSON.stringify(selectedRow?.original, null, 2)}
</pre>
</ResizablePanel> */}
{selectedRow?.original?.logs && selectedRow?.original?.logs?.length > 0 && (
<>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={20} minSize={20}>
<div className="h-full flex flex-col overflow-hidden">
<div className="px-5 py-3 border-b border-border flex justify-between items-center">
<h3 className="text-sm font-medium">
Function Logs (
{selectedRow?.original?.logs &&
Array.isArray(selectedRow?.original?.logs)
? selectedRow?.original?.logs?.length
: 0}
)
</h3>
</div>
<div className="flex-grow overflow-auto">
<FunctionLogsTab logs={selectedRow?.original?.logs} />
</div>
</div>
</ResizablePanel>
</>
)}
</ResizablePanelGroup>
</ResizablePanel>
{selectedRowKey && (
<>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={25} minSize={25}>
<ResizablePanelGroup direction="vertical">
<ResizablePanel defaultSize={100} minSize={30}>
<div className="h-full overflow-auto">
<DataTableSheetDetails
title={selectedRow?.original?.pathname}
titleClassName="font-mono"
>
<Tabs
defaultValue="details"
value={activeTab}
onValueChange={setActiveTab}
className="w-full h-full flex flex-col pt-4"
>
<TabsList className="mb-2 flex gap-3 px-5">
<TabsTrigger value="details">Log Details</TabsTrigger>
<TabsTrigger value="trace">Trace</TabsTrigger>
</TabsList>
<TabsContent
value="details"
className="flex-grow overflow-auto data-[state=active]:flex-grow px-5"
>
<MemoizedDataTableSheetContent
table={table}
data={selectedRow?.original}
filterFields={filterFields}
fields={sheetFields}
metadata={{
totalRows: totalDBRowCount ?? 0,
filterRows: filterDBRowCount ?? 0,
totalRowsFetched: totalFetched ?? 0,
currentPercentiles: metadata?.currentPercentiles ?? ({} as any),
...metadata,
}}
/>
</TabsContent>
<TabsContent
value="trace"
className="flex-grow overflow-auto data-[state=active]:flex-grow h-full mt-0 px-5"
>
{selectedRow?.original?.has_trace ? (
<TraceDetailTab id={selectedRow?.original?.id} />
) : (
<div className="flex flex-col gap-2">
<p className="text-sm text-muted-foreground">
No trace found for this log
</p>
</div>
)}
</TabsContent>
</Tabs>
</DataTableSheetDetails>
</div>
</ResizablePanel>
</ResizablePanelGroup>
</ResizablePanel>
</>
)}
</ResizablePanelGroup>
</div>
{/* </div> */}
</DataTableSideBarLayout>
</DataTableProvider>
</>
)
}
function useResetFocus() {
useHotKey(() => {
// FIXME: some dedicated div[tabindex="0"] do not auto-unblur (e.g. the DataTableFilterResetButton)
// REMINDER: we cannot just document.activeElement?.blur(); as the next tab will focus the next element in line,
// which is not what we want. We want to reset entirely.
document.body.setAttribute('tabindex', '0')
document.body.focus()
document.body.removeAttribute('tabindex')
}, '.')
}
// TODO: make a BaseObject (incl. date and uuid e.g. for every upcoming branch of infinite table)
export function useLiveMode<TData extends { date: Date }>(data: TData[]) {
const [live] = useQueryState('live', searchParamsParser.live)
// REMINDER: used to capture the live mode on timestamp
const liveTimestamp = React.useRef<number | undefined>(live ? new Date().getTime() : undefined)
React.useEffect(() => {
if (live) liveTimestamp.current = new Date().getTime()
else liveTimestamp.current = undefined
}, [live])
const anchorRow = React.useMemo(() => {
if (!live) return undefined
const item = data.find((item) => {
// return first item that is there if not liveTimestamp
if (!liveTimestamp.current) return true
// return first item that is after the liveTimestamp
if (item.date.getTime() > liveTimestamp.current) return false
return true
// return first item if no liveTimestamp
})
return item
}, [live, data])
return { row: anchorRow, timestamp: liveTimestamp.current }
}
export function getFacetedUniqueValues<TData>(facets?: Record<string, FacetMetadataSchema>) {
return (table: TTable<TData>, columnId: string) => {
return new Map(facets?.[columnId]?.rows?.map(({ value, total }) => [value, total]) || [])
}
}
export function 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
}
}
@@ -1,62 +0,0 @@
/**
* Utilities for log handling and display
*/
/**
* Determines the log level based on status code
* @param statusCode HTTP status code as number or string
* @returns 'success', 'info', 'warn', or 'error'
*/
export function determineLogLevelFromStatus(statusCode: string | number | undefined): string {
// Default to info if no status code
if (!statusCode) return 'info'
// Convert to number if it's a string
const code = typeof statusCode === 'string' ? parseInt(statusCode, 10) : statusCode
// Handle NaN case
if (isNaN(code)) return 'info'
// Determine level based on status code ranges
if (code >= 200 && code < 300) return 'success'
if (code >= 400 && code < 500) return 'warning'
if (code >= 500) return 'error'
// Default for other codes
return 'info'
}
/**
* Determines the log level based on log type and status
* Different log sources may have different level determination logic
*/
export function determineLogLevel(
logType: string,
statusCode: string | number | undefined,
defaultLevel?: string
): string {
// If a meaningful level is already provided, use it
if (defaultLevel && defaultLevel !== 'undefined') return defaultLevel
// Handle specific log types
switch (logType) {
case 'edge':
return determineLogLevelFromStatus(statusCode)
case 'auth':
case 'edge function':
return determineLogLevelFromStatus(statusCode)
case 'postgres':
// Could add postgres-specific error code handling here
return statusCode ? 'warn' : 'info'
case 'function events':
// Default to info if not specified
return defaultLevel || 'info'
// Add more log types as needed
default:
return 'info'
}
}
@@ -2,13 +2,13 @@ import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useRouter } from 'next/router'
import { PropsWithChildren, useEffect } from 'react'
import { LOCAL_STORAGE_KEYS } from 'common'
import NoPermission from 'components/ui/NoPermission'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { withAuth } from 'hooks/misc/withAuth'
import ProjectLayout from '../ProjectLayout/ProjectLayout'
import { LogsSidebarMenuV2 } from './LogsSidebarMenuV2'
import { LOCAL_STORAGE_KEYS } from 'common'
interface LogsLayoutProps {
title?: string
@@ -20,16 +20,15 @@ const LogsLayout = ({
children,
hideSidebar = false,
}: PropsWithChildren<LogsLayoutProps>) => {
const { isLoading, can: canUseLogsExplorer } = useAsyncCheckProjectPermissions(
PermissionAction.ANALYTICS_READ,
'logflare'
)
const router = useRouter()
const [_, setLastLogsPage] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.LAST_VISITED_LOGS_PAGE,
router.pathname.split('/logs/')[1] || ''
)
const { isLoading, can: canUseLogsExplorer } = useAsyncCheckProjectPermissions(
PermissionAction.ANALYTICS_READ,
'logflare'
)
useEffect(() => {
if (router.pathname.includes('/logs/')) {
@@ -80,22 +80,27 @@ export function SidebarCollapsible({
}
export function LogsSidebarMenuV2() {
const router = useRouter()
const { ref } = useParams() as { ref: string }
const warehouseEnabled = useFlag('warehouse')
const [searchText, setSearchText] = useState('')
const [createCollectionOpen, setCreateCollectionOpen] = useState(false)
const canCreateCollection = useCheckPermissions(PermissionAction.ANALYTICS_WRITE, 'logflare')
const router = useRouter()
const { ref } = useParams() as { ref: string }
const { data: tenantData } = useWarehouseTenantQuery({ projectRef: ref })
const {
projectAuthAll: authEnabled,
projectStorageAll: storageEnabled,
realtimeAll: realtimeEnabled,
} = useIsFeatureEnabled(['project_storage:all', 'project_auth:all', 'realtime:all'])
const warehouseEnabled = useFlag('warehouse')
const { data: whCollections, isLoading: whCollectionsLoading } = useWarehouseCollectionsQuery(
{ projectRef: ref },
{ enabled: IS_PLATFORM && warehouseEnabled && !!tenantData }
)
const { plan: orgPlan, isLoading: isOrgPlanLoading } = useCurrentOrgPlan()
const isFreePlan = !isOrgPlanLoading && orgPlan?.id === 'free'
@@ -155,12 +155,6 @@ export const generateOtherRoutes = (ref?: string, project?: Project): Route[] =>
icon: <Logs size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />,
link: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/unified-logs`),
},
{
key: 'unified-log-2',
label: 'Unified Logs Open Status',
icon: <Logs size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />,
link: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/logs-new`),
},
{
key: 'api',
label: 'API Docs',
+17
View File
@@ -0,0 +1,17 @@
import { useEffect } from 'react'
export function useHotKey(callback: () => void, key: string): void {
useEffect(() => {
function handler(e: KeyboardEvent) {
if (e.key === key && (e.metaKey || e.ctrlKey)) {
// e.preventDefault();
callback()
}
}
window.addEventListener('keydown', handler)
return () => {
window.removeEventListener('keydown', handler)
}
}, [key])
}
@@ -1,36 +0,0 @@
import { getQueryClient } from 'components/interfaces/DataTableDemo/providers/get-query-client'
// import { SearchParams } from 'nuqs' // No longer needed directly
import * as React from 'react'
import { Client } from 'components/interfaces/DataTableDemo/infinite/client'
import { dataOptions } from 'components/interfaces/DataTableDemo/light/query-options'
import { searchParamsParser } from 'components/interfaces/DataTableDemo/light/search-params' // Import the parser
// import { useSearchParams } from 'next/navigation' // Switch to useQueryStates
import { useQueryStates } from 'nuqs' // Import useQueryStates
import DefaultLayout from 'components/layouts/DefaultLayout'
import LogsLayout from 'components/layouts/LogsLayout/LogsLayout'
import type { NextPageWithLayout } from 'types'
export const LogPage: NextPageWithLayout = () => {
// const searchParams = useSearchParams() // Replaced
// const params = Object.fromEntries(searchParams.entries()) // Replaced
// const search = searchParamsCache.parse(params) // Replaced
const [search] = useQueryStates(searchParamsParser) // Use useQueryStates with the parser
const queryClient = getQueryClient()
React.useEffect(() => {
// Pass the parsed search object directly to dataOptions
queryClient.prefetchInfiniteQuery(dataOptions(search))
}, [search, queryClient]) // Add queryClient to dependencies
return <Client />
}
LogPage.getLayout = (page) => (
<DefaultLayout>
<LogsLayout hideSidebar={true}>{page}</LogsLayout>
</DefaultLayout>
)
export default LogPage
@@ -1,36 +0,0 @@
import { getQueryClient } from 'components/interfaces/DataTableDemo/providers/get-query-client'
// import { SearchParams } from 'nuqs' // No longer needed directly
import * as React from 'react'
import { Client } from 'components/interfaces/DataTableDemo/light/client'
import { dataOptions } from 'components/interfaces/DataTableDemo/light/query-options'
import { searchParamsParser } from 'components/interfaces/DataTableDemo/light/search-params' // Import the parser
// import { useSearchParams } from 'next/navigation' // Switch to useQueryStates
import { useQueryStates } from 'nuqs' // Import useQueryStates
import DefaultLayout from 'components/layouts/DefaultLayout'
import LogsLayout from 'components/layouts/LogsLayout/LogsLayout'
import type { NextPageWithLayout } from 'types'
export const LogPage: NextPageWithLayout = () => {
// const searchParams = useSearchParams() // Replaced
// const params = Object.fromEntries(searchParams.entries()) // Replaced
// const search = searchParamsCache.parse(params) // Replaced
const [search] = useQueryStates(searchParamsParser) // Use useQueryStates with the parser
const queryClient = getQueryClient()
React.useEffect(() => {
// Pass the parsed search object directly to dataOptions
queryClient.prefetchInfiniteQuery(dataOptions(search))
}, [search, queryClient]) // Add queryClient to dependencies
return <Client />
}
LogPage.getLayout = (page) => (
<DefaultLayout>
<LogsLayout hideSidebar={true}>{page}</LogsLayout>
</DefaultLayout>
)
export default LogPage
-38
View File
@@ -1,38 +0,0 @@
import DefaultLayout from 'components/layouts/DefaultLayout'
import LogsLayout from 'components/layouts/LogsLayout/LogsLayout'
import * as React from 'react'
import type { NextPageWithLayout } from 'types'
import { columns } from 'components/interfaces/DataTableDemo/default/columns'
import { filterFields } from 'components/interfaces/DataTableDemo/default/constants'
import { data } from 'components/interfaces/DataTableDemo/default/data'
import { DataTable } from 'components/interfaces/DataTableDemo/default/data-table'
import { Skeleton } from 'components/interfaces/DataTableDemo/default/skeleton'
import { useSearchParams } from 'components/interfaces/DataTableDemo/default/search-params'
export const LogPage: NextPageWithLayout = () => {
const [search] = useSearchParams()
return (
<React.Suspense fallback={<Skeleton />}>
<DataTable
columns={columns}
data={data}
filterFields={filterFields}
defaultColumnFilters={Object.entries(search)
.map(([key, value]) => ({
id: key,
value,
}))
.filter(({ value }) => value ?? undefined)}
/>
</React.Suspense>
)
}
LogPage.getLayout = (page) => (
<DefaultLayout>
<LogsLayout hideSidebar={true}>{page}</LogsLayout>
</DefaultLayout>
)
export default LogPage
@@ -1,39 +0,0 @@
import { getQueryClient } from 'components/interfaces/DataTableDemo/providers/get-query-client'
// import { SearchParams } from 'nuqs' // No longer needed directly
import * as React from 'react'
import { Client } from 'components/interfaces/logs-new/client'
import { dataOptions } from 'components/interfaces/logs-new/query-options-new'
import { searchParamsParser } from 'components/interfaces/logs-new/search-params' // Import the parser
// import { useSearchParams } from 'next/navigation' // Switch to useQueryStates
import { useQueryStates } from 'nuqs' // Import useQueryStates
import DefaultLayout from 'components/layouts/DefaultLayout'
import LogsLayout from 'components/layouts/LogsLayout/LogsLayout'
import type { NextPageWithLayout } from 'types'
import { useParams } from 'common'
export const LogPage: NextPageWithLayout = () => {
const { ref: projectRef } = useParams()
// const searchParams = useSearchParams() // Replaced
// const params = Object.fromEntries(searchParams.entries()) // Replaced
// const search = searchParamsCache.parse(params) // Replaced
const [search] = useQueryStates(searchParamsParser) // Use useQueryStates with the parser
const queryClient = getQueryClient()
// Remove prefetching to avoid duplicate requests
// React.useEffect(() => {
// // Pass the parsed search object directly to dataOptions
// queryClient.prefetchInfiniteQuery(dataOptions(search, projectRef ?? ''))
// }, [search, queryClient, projectRef]) // Add queryClient and projectRef to dependencies
return <Client />
}
LogPage.getLayout = (page) => (
<DefaultLayout>
<LogsLayout hideSidebar={true}>{page}</LogsLayout>
</DefaultLayout>
)
export default LogPage
-36
View File
@@ -1,36 +0,0 @@
import { getQueryClient } from 'components/interfaces/DataTableDemo/providers/get-query-client'
// import { SearchParams } from 'nuqs' // No longer needed directly
import * as React from 'react'
import { Client } from 'components/interfaces/infinite-logs-example/client'
import { dataOptions } from 'components/interfaces/DataTableDemo/light/query-options'
import { searchParamsParser } from 'components/interfaces/DataTableDemo/light/search-params' // Import the parser
// import { useSearchParams } from 'next/navigation' // Switch to useQueryStates
import { useQueryStates } from 'nuqs' // Import useQueryStates
import DefaultLayout from 'components/layouts/DefaultLayout'
import LogsLayout from 'components/layouts/LogsLayout/LogsLayout'
import type { NextPageWithLayout } from 'types'
export const LogPage: NextPageWithLayout = () => {
// const searchParams = useSearchParams() // Replaced
// const params = Object.fromEntries(searchParams.entries()) // Replaced
// const search = searchParamsCache.parse(params) // Replaced
const [search] = useQueryStates(searchParamsParser) // Use useQueryStates with the parser
const queryClient = getQueryClient()
React.useEffect(() => {
// Pass the parsed search object directly to dataOptions
queryClient.prefetchInfiniteQuery(dataOptions(search))
}, [search, queryClient]) // Add queryClient to dependencies
return <Client />
}
LogPage.getLayout = (page) => (
<DefaultLayout>
<LogsLayout hideSidebar={true}>{page}</LogsLayout>
</DefaultLayout>
)
export default LogPage
@@ -1,15 +1,9 @@
import { useRouter } from 'next/router'
import { UnifiedLogsTable } from 'components/interfaces/UnifiedLogs/client'
import DefaultLayout from 'components/layouts/DefaultLayout'
import LogsLayout from 'components/layouts/LogsLayout/LogsLayout'
import type { NextPageWithLayout } from 'types'
import DefaultLayout from 'components/layouts/DefaultLayout'
import UnifiedLogsPreviewer from 'components/interfaces/UnifiedLogs/UnifiedLogsPreviewer'
export const LogPage: NextPageWithLayout = () => {
const router = useRouter()
const { ref } = router.query
return <UnifiedLogsPreviewer condensedLayout projectRef={ref as string} />
}
export const LogPage: NextPageWithLayout = () => <UnifiedLogsTable />
LogPage.getLayout = (page) => (
<DefaultLayout>