mirror of
https://github.com/supabase/supabase.git
synced 2026-10-15 06:09:08 +03:00
Midway
This commit is contained in:
1 parent
cf32793020
commit
dcc6239397
39 files changed
+664
-1321
No files matched your search
+1
-1
@@ -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>
|
||||
)
|
||||
}
|
||||
+29
-28
@@ -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)
|
||||
// }
|
||||
+4
-67
@@ -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>
|
||||
)
|
||||
}
|
||||
+6
-111
@@ -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]',
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+4
-12
@@ -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!
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+1
-1
@@ -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'
|
||||
+14
-19
@@ -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 {
|
||||
+3
-8
@@ -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',
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user