add infinite and live logs example

This commit is contained in:
Jonathan Summers-Muir committed 2025-04-16 18:07:26 +08:00
1 parent 14c6f9a867
commit e744a409cb
11 files changed
+793 -32

No files matched your search

@@ -22,8 +22,8 @@ export function DataTableColumnHeader<TData, TValue>({
return (
<Button
variant="ghost"
size="sm"
type="text"
size="small"
onClick={() => {
column.toggleSorting(undefined)
}}
@@ -31,23 +31,25 @@ export function DataTableColumnHeader<TData, TValue>({
'py-0 px-0 h-7 hover:bg-transparent flex gap-2 items-center justify-between w-full',
className
)}
iconRight={
<span className="flex flex-col">
<ChevronUp
className={cn(
'-mb-0.5 h-3 w-3',
column.getIsSorted() === 'asc' ? 'text-foreground' : 'text-foreground-muted'
)}
/>
<ChevronDown
className={cn(
'-mt-0.5 h-3 w-3',
column.getIsSorted() === 'desc' ? 'text-foreground' : 'text-foreground-muted'
)}
/>
</span>
}
{...props}
>
<span>{title}</span>
<span className="flex flex-col">
<ChevronUp
className={cn(
'-mb-0.5 h-3 w-3',
column.getIsSorted() === 'asc' ? 'text-accent-foreground' : 'text-muted-foreground'
)}
/>
<ChevronDown
className={cn(
'-mt-0.5 h-3 w-3',
column.getIsSorted() === 'desc' ? 'text-accent-foreground' : 'text-muted-foreground'
)}
/>
</span>
</Button>
)
}
@@ -63,10 +63,12 @@ export function LiveButton({ fetchPreviousPage }: LiveButtonProps) {
<Button
className={cn(live && 'border-info text-info hover:text-info')}
onClick={handleClick}
variant="outline"
size="sm"
type="default"
size="small"
icon={
live ? <CirclePause className="mr-2 h-4 w-4" /> : <CirclePlay className="mr-2 h-4 w-4" />
}
>
{live ? <CirclePause className="mr-2 h-4 w-4" /> : <CirclePlay className="mr-2 h-4 w-4" />}
Live
</Button>
)
@@ -10,7 +10,13 @@ export function RefreshButton({ onClick }: RefreshButtonProps) {
const { isLoading } = useDataTable()
return (
<Button type="outline" size="small" disabled={isLoading} onClick={onClick} className="h-9 w-9">
<Button
type="outline"
size="small"
disabled={isLoading}
onClick={onClick}
className="h-9 w-9 px-0"
>
{isLoading ? (
<LoaderCircle className="h-4 w-4 animate-spin" />
) : (
@@ -68,6 +68,12 @@ export function Client() {
})
}, [facets])
// --- DEBUG: Check props before passing to DataTableInfinite ---
console.log('>>> [Client] flatData length:', flatData?.length)
console.log('>>> [Client] filterDBRowCount:', filterDBRowCount)
console.log('>>> [Client] data object:', data) // Log the raw react-query data
// --- END DEBUG ---
return (
<DataTableInfinite
columns={columns}
@@ -4,6 +4,7 @@ import type { Percentile } from 'components/interfaces/DataTableDemo/lib/request
import SuperJSON from 'superjson'
import type { BaseChartSchema, ColumnSchema, FacetMetadataSchema } from './schema'
import type { SearchParamsType } from './search-params'
// Removed: import { searchParamsSerializer } from './search-params'
import {
ARRAY_DELIMITER,
RANGE_DELIMITER,
@@ -11,14 +12,14 @@ import {
SORT_DELIMITER,
} from 'components/interfaces/DataTableDemo/lib/delimiters'
// Helper function to create query params string (assuming similar API conventions)
// Restore the helper function to create query params string
const createApiQueryString = (params: Record<string, any>): string => {
const queryParams = new URLSearchParams()
for (const [key, value] of Object.entries(params)) {
if (value === null || value === undefined) continue
// Add handling specific to infinite API if needed, otherwise assume similar to light
// Using logic similar to the original light version
if (key === 'date' && Array.isArray(value) && value.length === 2) {
queryParams.set('dateStart', value[0].getTime().toString())
queryParams.set('dateEnd', value[1].getTime().toString())
@@ -36,16 +37,23 @@ const createApiQueryString = (params: Record<string, any>): string => {
value.length > 0
) {
// Assuming slider or range gives min/max
queryParams.set(`${key}Start`, value[0].toString())
queryParams.set(`${key}End`, value[value.length - 1].toString())
// Use the correct delimiters if necessary based on how backend expects ranges
if (value.length === 1) {
// Handle single value if needed, maybe just set the key?
// queryParams.set(key, value[0].toString());
} else if (value.length >= 2) {
queryParams.set(`${key}Start`, value[0].toString())
queryParams.set(`${key}End`, value[value.length - 1].toString())
}
} else if (Array.isArray(value)) {
if (value.length > 0) {
queryParams.set(key, value.join(',')) // Use comma separation for arrays
// Use comma (ARRAY_DELIMITER) separation for arrays
queryParams.set(key, value.join(ARRAY_DELIMITER))
}
} else if (key === 'sort' && typeof value === 'object' && value !== null) {
queryParams.set(
key,
`${(value as { id: string; desc: boolean }).id}.${(value as { id: string; desc: boolean }).desc ? 'desc' : 'asc'}`
`${(value as { id: string; desc: boolean }).id}${SORT_DELIMITER}${(value as { id: string; desc: boolean }).desc ? 'desc' : 'asc'}`
)
} else if (value instanceof Date) {
queryParams.set(key, value.getTime().toString())
@@ -112,16 +120,15 @@ export const dataOptions = (search: SearchParamsType) => {
...(direction && { direction }), // Add direction if present
}
const queryString = createApiQueryString(apiParams)
// Assuming the API route is /api/data-table/infinite
const response = await fetch(`/api/data-table/infinite${queryString}`)
const response = await fetch(`/api/data-table/infinite${createApiQueryString(apiParams)}`)
if (!response.ok) {
throw new Error('Network response was not ok')
}
const json = await response.json()
return SuperJSON.parse<InfiniteQueryResponse<ColumnSchema[], LogsMeta>>(json)
const jsonString = await response.text()
return SuperJSON.parse<InfiniteQueryResponse<ColumnSchema[], LogsMeta>>(jsonString)
},
initialPageParam: { cursor: new Date().getTime(), direction: 'next' } as PageParam,
getPreviousPageParam: (
@@ -1,7 +1,7 @@
import {
createParser,
// createSearchParamsCache, // Removed for Pages Router compatibility
// createSerializer, // Removed for Pages Router compatibility
createSerializer, // Uncommented for Pages Router compatibility
parseAsArrayOf,
parseAsBoolean,
parseAsInteger,
@@ -64,7 +64,7 @@ export const searchParamsParser = {
// Removed for Pages Router compatibility
// export const searchParamsCache = createSearchParamsCache(searchParamsParser)
// export const searchParamsSerializer = createSerializer(searchParamsParser)
export const searchParamsSerializer = createSerializer(searchParamsParser) // Uncommented
export type SearchParamsType = inferParserType<typeof searchParamsParser>
+1
View File
@@ -22,6 +22,7 @@ const HOSTED_SUPPORTED_API_URLS = [
'/edge-functions/test',
'/edge-functions/body',
'/api/data-table/light',
'/api/data-table/infinite',
]
export function middleware(request: NextRequest) {
@@ -0,0 +1,324 @@
import { LEVELS } from 'components/interfaces/DataTableDemo/constants/levels'
import { REGIONS } from 'components/interfaces/DataTableDemo/constants/region'
import { isArrayOfDates, isArrayOfNumbers } from 'components/interfaces/DataTableDemo/lib/is-array'
import {
calculatePercentile,
calculateSpecificPercentile,
} from 'components/interfaces/DataTableDemo/lib/request/percentile'
import { addDays, addMilliseconds, differenceInMinutes, isSameDay } from 'date-fns'
import type {
ColumnSchema,
FacetMetadataSchema,
TimelineChartSchema,
} from 'components/interfaces/DataTableDemo/infinite/schema'
import type { SearchParamsType } from 'components/interfaces/DataTableDemo/infinite/search-params'
export const sliderFilterValues = [
'latency',
'timing.dns',
'timing.connection',
'timing.tls',
'timing.ttfb',
'timing.transfer',
] as const satisfies (keyof ColumnSchema)[]
export const filterValues = [
'level',
...sliderFilterValues,
'status',
'regions',
'method',
'host',
'pathname',
] as const satisfies (keyof ColumnSchema)[]
export function filterData(
data: ColumnSchema[],
search: Partial<SearchParamsType>
): ColumnSchema[] {
return data.filter((row) => {
for (const key in search) {
const filter = search[key as keyof typeof search]
if (filter === undefined || filter === null) continue
if (
(key === 'latency' ||
key === 'timing.dns' ||
key === 'timing.connection' ||
key === 'timing.tls' ||
key === 'timing.ttfb' ||
key === 'timing.transfer') &&
isArrayOfNumbers(filter) &&
filter.length > 0
) {
const rowValue = row[key as keyof ColumnSchema]
if (typeof rowValue !== 'number') return false
if (filter.length === 1 && rowValue !== filter[0]) return false
if (filter.length === 2 && (rowValue < filter[0] || rowValue > filter[1])) return false
continue
}
if (key === 'status' && isArrayOfNumbers(filter) && filter.length > 0) {
const rowValue = row[key as 'status']
if (typeof rowValue !== 'number') return false
if (!filter.includes(rowValue)) return false
continue
}
if (key === 'regions' && Array.isArray(filter) && filter.length > 0) {
const rowRegions = row[key as 'regions']
if (!Array.isArray(rowRegions) || rowRegions.length === 0) return false
if (!(filter as string[]).includes(rowRegions[0])) return false
continue
}
if (key === 'date' && isArrayOfDates(filter) && filter.length > 0) {
const rowValue = row[key as 'date']
if (!(rowValue instanceof Date)) return false
const rowTimestamp = rowValue.getTime()
if (filter.length === 1 && !isSameDay(rowValue, filter[0])) return false
if (
filter.length === 2 &&
(rowTimestamp < filter[0].getTime() || rowTimestamp > filter[1].getTime())
)
return false
continue
}
if (key === 'level' && Array.isArray(filter) && filter.length > 0) {
const rowValue = row[key as 'level']
if (typeof rowValue !== 'string') return false
if (!(filter as string[]).includes(rowValue)) return false
continue
}
if (key === 'host' && typeof filter === 'string' && filter.length > 0) {
const rowValue = row[key as 'host']
if (typeof rowValue !== 'string' || rowValue !== filter) return false
continue
}
if (key === 'pathname' && typeof filter === 'string' && filter.length > 0) {
const rowValue = row[key as 'pathname']
if (typeof rowValue !== 'string' || rowValue !== filter) return false
continue
}
}
return true
})
}
export function sortData(data: ColumnSchema[], sort: SearchParamsType['sort']) {
if (!sort) return data
return data.sort((a, b) => {
if (sort.desc) {
// @ts-ignore
return a?.[sort.id] < b?.[sort.id] ? 1 : -1
} else {
// @ts-ignore
return a?.[sort.id] > b?.[sort.id] ? 1 : -1
}
})
}
export function percentileData(data: ColumnSchema[]): ColumnSchema[] {
const latencies = data.map((row) => row.latency)
return data.map((row) => ({
...row,
percentile: calculatePercentile(latencies, row.latency),
}))
}
export function splitData(data: ColumnSchema[], search: SearchParamsType) {
let newData: ColumnSchema[] = []
const now = new Date()
const cursorTimestamp = search.cursor.getTime()
data.forEach((item) => {
const itemTimestamp = item.date.getTime()
if (search.direction === 'next') {
// If item is older than cursor and we need more items
if (itemTimestamp < cursorTimestamp && newData.length < search.size) {
newData.push(item)
// If item has the exact same timestamp as the last added item (to include ties)
} else if (
newData.length > 0 &&
itemTimestamp === newData[newData.length - 1].date.getTime()
) {
newData.push(item)
}
} else if (search.direction === 'prev') {
// If item is newer than cursor and not in the future
if (itemTimestamp > cursorTimestamp && itemTimestamp < now.getTime()) {
newData.push(item)
}
// Note: For 'prev', we might need to sort in reverse and take size later
// or handle it differently depending on desired behavior for ties.
// Current logic fetches all newer items, potentially more than size.
}
})
// For 'prev', ensure we only return the required size, potentially sorting again if order matters
if (search.direction === 'prev') {
// Assuming sortData handles default if search.sort is null
const sortedPrevData = sortData(newData, search.sort)
newData = sortedPrevData.slice(0, search.size) // Limit to size
}
return newData
}
export function getFacetsFromData(data: ColumnSchema[]) {
const valuesMap = data.reduce((prev, curr) => {
Object.entries(curr).forEach(([key, value]) => {
if (filterValues.includes(key as any)) {
// REMINDER: because regions is an array with a single value we need to convert to string
// TODO: we should make the region a single string instead of an array?!?
const _value = Array.isArray(value) ? value.toString() : value
const total = prev.get(key)?.get(_value) || 0
if (prev.has(key) && _value) {
prev.get(key)?.set(_value, total + 1)
} else if (_value) {
prev.set(key, new Map([[_value, 1]]))
}
}
})
return prev
}, new Map<string, Map<any, number>>())
const facets = Object.fromEntries(
Array.from(valuesMap.entries()).map(([key, valueMap]) => {
let min: number | undefined
let max: number | undefined
const rows = Array.from(valueMap.entries()).map(([value, total]) => {
if (typeof value === 'number') {
if (!min) min = value
else min = value < min ? value : min
if (!max) max = value
else max = value > max ? value : max
}
return {
value,
total,
}
})
const total = Array.from(valueMap.values()).reduce((a, b) => a + b, 0)
return [key, { rows, total, min, max }]
})
)
return facets satisfies Record<string, FacetMetadataSchema>
}
export function getPercentileFromData(data: ColumnSchema[]) {
const latencies = data.map((row) => row.latency)
const p50 = calculateSpecificPercentile(latencies, 50)
const p75 = calculateSpecificPercentile(latencies, 75)
const p90 = calculateSpecificPercentile(latencies, 90)
const p95 = calculateSpecificPercentile(latencies, 95)
const p99 = calculateSpecificPercentile(latencies, 99)
return { p50, p75, p90, p95, p99 }
}
export function groupChartData(data: ColumnSchema[], dates: Date[] | null): TimelineChartSchema[] {
console.log(`>>> [groupChartData] Input data length: ${data?.length}, Input dates:`, dates)
if (data?.length === 0 && !dates) {
console.log('>>> [groupChartData] Returning [] due to empty data and no dates.')
return []
}
const _dates = dates?.length === 1 ? [dates[0], addDays(dates[0], 1)] : dates
const between = _dates || (data?.length ? [data[data.length - 1].date, data[0].date] : [])
console.log(`>>> [groupChartData] Date range used ('between'):`, between)
if (!between.length || !(between[0] instanceof Date) || !(between[1] instanceof Date)) {
// Added validity check
console.log(`>>> [groupChartData] Returning [] due to invalid 'between' range.`)
return []
}
const interval = evaluateInterval(between)
console.log(`>>> [groupChartData] Calculated interval (ms): ${interval}`)
if (interval <= 0) {
// Added check for valid interval
console.log('>>> [groupChartData] Returning [] due to invalid interval.')
return []
}
const duration = Math.abs(between[0].getTime() - between[between.length - 1].getTime())
const steps = Math.floor(duration / interval)
console.log(`>>> [groupChartData] Calculated steps: ${steps}`)
if (steps <= 0) {
// Added check for valid steps
console.log('>>> [groupChartData] Returning [] due to zero steps.')
return []
}
const timestamps: { date: Date }[] = []
for (let i = 0; i < steps; i++) {
const newTimestamp = addMilliseconds(between[0], i * interval)
timestamps.push({ date: newTimestamp })
}
// TODO: make it dynamic to avoid havin 200, 400, 500 hardcoded
// TODO: make it more efficient
// e.g. make the "status" prop we use as T generic
const result = timestamps.map((timestamp, i) => {
const filteredData = data.filter((row) => {
const diff = row.date.getTime() - timestamp.date.getTime()
return diff < interval && diff >= 0
})
return {
timestamp: timestamp.date.getTime(), // TODO: use date-fns and interval to determine the format
success: filteredData.filter((row) => row.level === 'success').length,
warning: filteredData.filter((row) => row.level === 'warning').length,
error: filteredData.filter((row) => row.level === 'error').length,
}
})
console.log(`>>> [groupChartData] Result length: ${result.length}`)
return result
}
export function evaluateInterval(dates: Date[] | null): number {
if (!dates) return 0
if (dates.length < 1 || dates.length > 3) return 0
// Calculate the time difference in minutes
const timeDiffInMinutes = Math.abs(differenceInMinutes(dates[0], dates[1]))
// Define thresholds and their respective intervals in milliseconds
const intervals = [
{ threshold: 1, interval: 1000 }, // 1 second
{ threshold: 5, interval: 5000 }, // 5 seconds
{ threshold: 10, interval: 10000 }, // 10 seconds
{ threshold: 30, interval: 30000 }, // 30 seconds
{ threshold: 60, interval: 60000 }, // 1 minute
{ threshold: 120, interval: 120000 }, // 2 minutes
{ threshold: 240, interval: 240000 }, // 4 minutes
{ threshold: 480, interval: 480000 }, // 8 minutes
{ threshold: 1440, interval: 1440000 }, // 24 minutes
{ threshold: 2880, interval: 2880000 }, // 48 minutes
{ threshold: 5760, interval: 5760000 }, // 96 minutes
{ threshold: 11520, interval: 11520000 }, // 192 minutes
{ threshold: 23040, interval: 23040000 }, // 384 minutes
]
// Iterate over the intervals and return the matching one
for (const { threshold, interval } of intervals) {
if (timeDiffInMinutes < threshold) {
return interval
}
}
// Default to the largest interval if no match found
return 46080000 // 768 minutes
}
@@ -0,0 +1,149 @@
import { calculateSpecificPercentile } from 'components/interfaces/DataTableDemo/lib/request/percentile'
import { addDays } from 'date-fns'
import type { NextApiRequest, NextApiResponse } from 'next'
import SuperJSON from 'superjson'
import type {
InfiniteQueryResponse,
LogsMeta,
} from 'components/interfaces/DataTableDemo/infinite/query-options'
import type { ColumnSchema } from 'components/interfaces/DataTableDemo/infinite/schema'
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 {
filterData,
getFacetsFromData,
groupChartData,
percentileData,
sliderFilterValues,
sortData,
splitData,
} from './helpers'
import { mock, mockLive } from './mock'
import { searchParamsParser } from 'components/interfaces/DataTableDemo/infinite/search-params'
import type { inferParserType } from 'nuqs'
type SearchParamsType = inferParserType<typeof searchParamsParser>
export default async function handler(req: NextApiRequest, res: NextApiResponse): Promise<void> {
if (req.method !== 'GET') {
res.setHeader('Allow', ['GET'])
res.status(405).end(`Method ${req.method} Not Allowed`)
return
}
const query = req.query
// Parse the full search object to get defaults for pagination etc.
const search = Object.fromEntries(
Object.entries(searchParamsParser).map(([key, parser]) => {
const queryValue = query[key]
const value = Array.isArray(queryValue) ? queryValue[0] : queryValue
const parsedValue = value !== undefined ? parser.parse(value) : parser.parse('')
return [key, parsedValue]
})
) as SearchParamsType
// --- Build effectiveFilters ONLY from params present in the query --- //
const effectiveFilters: Partial<SearchParamsType> = {}
Object.keys(query).forEach((key) => {
// Only include keys that are actual filters (not pagination/sort etc.)
// and are defined in our parser
if (
key in searchParamsParser &&
!['start', 'size', 'sort', 'direction', 'cursor', 'live'].includes(key)
) {
// Use the fully parsed value (including defaults if needed for parsing logic)
// but only if the key was actually in the query
const parsedValue = search[key as keyof SearchParamsType]
// Only assign if the value is not null (as Partial<> expects Type | undefined)
if (parsedValue !== null) {
effectiveFilters[key as keyof typeof effectiveFilters] = parsedValue
}
}
})
// --- End of effectiveFilters build --- //
const totalData = [...mockLive, ...mock]
// Handle date separately IF it was in the query
const _date =
search.date?.length === 1 ? [search.date[0], addDays(search.date[0], 1)] : search.date
if (_date && effectiveFilters.date) {
effectiveFilters.date = _date // Use the calculated range
}
// Single filtering step using only filters from the query
const filteredData = filterData(totalData, effectiveFilters)
// DEBUG:
console.log('>>> [API] Filtered data length:', filteredData.length)
// --- Calculate Date Range for Chart --- //
let chartDateRange: Date[] | null = null
const explicitDateFilter = effectiveFilters.date as Date[] | undefined
if (explicitDateFilter && explicitDateFilter.length > 0) {
// Use the explicit date filter if provided
chartDateRange =
explicitDateFilter.length === 1
? [explicitDateFilter[0], addDays(explicitDateFilter[0], 1)]
: explicitDateFilter
} else if (filteredData.length > 0) {
// Otherwise, use the range from the filtered data
const sortedByDate = [...filteredData].sort((a, b) => a.date.getTime() - b.date.getTime())
chartDateRange = [sortedByDate[0].date, sortedByDate[sortedByDate.length - 1].date]
}
// If filteredData is empty and no explicit date, chartDateRange remains null
// --- End Date Range Calculation --- //
// REMINDER: We might still need the facets from data filtered *without* sliders
// if the UI sliders need the full min/max range.
// const _rest = Object.fromEntries(
// Object.entries(search).filter(([key]) => !sliderFilterValues.includes(key as any))
// )
// const withoutSliderData = filterData(totalData, { ..._rest, date: _date })
// const withoutSliderFacets = getFacetsFromData(withoutSliderData)
// Calculate facets based on the final filtered data
const facets = getFacetsFromData(filteredData)
// Sort the filtered data
const sortedData = sortData(filteredData, search.sort)
// Calculate percentile for each row IN the filtered data
const withPercentileData = percentileData(sortedData)
// Paginate the sorted, percentile-annotated data
const data = splitData(withPercentileData, search)
// Calculate aggregate percentiles based on the filtered data (before pagination)
const latencies = filteredData.map(({ latency }) => latency)
const currentPercentiles = {
50: calculateSpecificPercentile(latencies, 50),
75: calculateSpecificPercentile(latencies, 75),
90: calculateSpecificPercentile(latencies, 90),
95: calculateSpecificPercentile(latencies, 95),
99: calculateSpecificPercentile(latencies, 99),
}
const nextCursor = data.length > 0 ? data[data.length - 1].date.getTime() : null
const prevCursor = data.length > 0 ? data[0].date.getTime() : new Date().getTime()
const responsePayload = {
data,
meta: {
totalRowCount: totalData.length,
filterRowCount: filteredData.length,
chartData: groupChartData(filteredData, chartDateRange),
facets: {
...facets,
},
metadata: { currentPercentiles },
},
prevCursor,
nextCursor,
} satisfies InfiniteQueryResponse<ColumnSchema[], LogsMeta>
res.setHeader('Content-Type', 'application/json')
res.status(200).send(SuperJSON.stringify(responsePayload))
}
@@ -0,0 +1,228 @@
import { METHODS } from 'components/interfaces/DataTableDemo/constants/method'
import { ColumnSchema } from 'components/interfaces/DataTableDemo/infinite/schema'
import { subMinutes } from 'date-fns'
import { REGIONS } from 'components/interfaces/DataTableDemo/constants/region'
const DAYS = 20
function getRandomTiming(latency: number) {
// Generate random percentages within the specified ranges
const dns = Math.random() * (0.15 - 0.05) + 0.05 // 5% to 15%
const connection = Math.random() * (0.3 - 0.1) + 0.1 // 10% to 30%
const tls = Math.random() * (0.1 - 0.05) + 0.05 // 5% to 10%
const transfer = Math.random() * (0.004 - 0) + 0.004 // 0% to 0.4%
// Ensure the sum of dns, connection, tls, and transfer is subtracted from 100% for ttfb
const remaining = 1 - (dns + connection + tls + transfer) // Calculate remaining for ttfb
return {
'timing.dns': Math.round(latency * dns),
'timing.connection': Math.round(latency * connection),
'timing.tls': Math.round(latency * tls),
'timing.ttfb': Math.round(latency * remaining), // Use the remaining percentage for ttfb
'timing.transfer': Math.round(latency * transfer),
}
}
// REMINDER: for later
function getRandomMetadata(): Record<string, string> {
const rand = Math.random()
if (rand < 0.5) {
return {
env: 'production',
}
} else {
return {
env: 'staging',
}
}
}
function getLevel(status: number) {
if (`${status}`.startsWith('2')) return 'success'
if (`${status}`.startsWith('4')) return 'warning'
if (`${status}`.startsWith('5')) return 'error'
return 'error'
}
function getRandomStatusCode() {
const rand = Math.random()
if (rand < 0.9) {
return 200
} else if (rand < 0.96) {
if (Math.random() < 0.5) {
return 400
} else {
return 404
}
} else {
return 500
}
}
function getMessage() {
return 'ERR_INTERNAL_DISASTER: "The server spilled coffee on itself."'
}
const shopPathnames = [
'/bikes/gravel/road',
'/bikes/racing/track',
'/bikes/mountain/trail',
'/bikes/city/cargo',
]
const apiPathnames = ['/v1/products', '/v1/orders', '/v1/customers']
function getRandomRequestObject(): {
method: (typeof METHODS)[number]
host: string
pathname: string
} {
const rand = Math.random()
if (rand < 0.5) {
return {
method: 'POST',
host: 'api.acme-shop.com',
pathname: apiPathnames[Math.floor(Math.random() * apiPathnames.length)],
}
} else {
return {
method: 'GET',
host: 'acme-shop.com',
pathname: shopPathnames[Math.floor(Math.random() * shopPathnames.length)],
}
}
}
function getHeaders() {
return {
Age: '0',
'Cache-Control': 'private, no-cache, no-store, max-age=0, must-revalidate',
Server: 'Cloudflare',
}
}
const multiplier: Record<(typeof REGIONS)[number], number> = {
ams: 1,
iad: 0.6,
gru: 1.6,
syd: 1.3,
fra: 0.8,
hkg: 1.4,
}
export function createMockData({
minutes = 0,
}: {
size?: number
minutes?: number
}): ColumnSchema[] {
const date = subMinutes(new Date(), minutes)
const random = Math.random()
const statusCode = {
ams: getRandomStatusCode(),
iad: getRandomStatusCode(),
gru: getRandomStatusCode(),
syd: getRandomStatusCode(),
fra: getRandomStatusCode(),
hkg: getRandomStatusCode(),
}
const latency = {
ams: Math.round(1000 * (random * (1 - multiplier.ams) + multiplier.ams)),
iad: Math.round(1000 * (random * (1 - multiplier.iad) + multiplier.iad)),
gru: Math.round(1000 * (random * (1 - multiplier.gru) + multiplier.gru)),
syd: Math.round(1000 * (random * (1 - multiplier.syd) + multiplier.syd)),
fra: Math.round(1000 * (random * (1 - multiplier.fra) + multiplier.fra)),
hkg: Math.round(1000 * (random * (1 - multiplier.hkg) + multiplier.hkg)),
}
const requestObject = getRandomRequestObject()
const headers = getHeaders()
return [
{
uuid: crypto.randomUUID(),
level: getLevel(statusCode.ams),
latency: latency.ams,
regions: ['ams'],
status: statusCode.ams,
date,
headers,
message: 500 === statusCode.ams ? getMessage() : undefined,
...getRandomTiming(latency.ams),
...requestObject,
},
{
uuid: crypto.randomUUID(),
level: getLevel(statusCode.iad),
latency: latency.iad,
regions: ['iad'],
status: statusCode.iad,
date,
headers,
message: 500 === statusCode.iad ? getMessage() : undefined,
...getRandomTiming(latency.iad),
...requestObject,
},
{
uuid: crypto.randomUUID(),
level: getLevel(statusCode.gru),
latency: latency.gru,
regions: ['gru'],
status: statusCode.gru,
date,
headers,
message: 500 === statusCode.gru ? getMessage() : undefined,
...getRandomTiming(latency.gru),
...requestObject,
},
{
uuid: crypto.randomUUID(),
level: getLevel(statusCode.syd),
latency: latency.syd,
regions: ['syd'],
status: statusCode.syd,
date,
headers,
message: 500 === statusCode.syd ? getMessage() : undefined,
...getRandomTiming(latency.syd),
...requestObject,
},
{
uuid: crypto.randomUUID(),
level: getLevel(statusCode.fra),
latency: latency.fra,
regions: ['fra'],
status: statusCode.fra,
date,
headers,
message: 500 === statusCode.fra ? getMessage() : undefined,
...getRandomTiming(latency.fra),
...requestObject,
},
{
uuid: crypto.randomUUID(),
level: getLevel(statusCode.hkg),
latency: latency.hkg,
regions: ['hkg'],
status: statusCode.hkg,
date,
headers,
message: 500 === statusCode.hkg ? getMessage() : undefined,
...getRandomTiming(latency.hkg),
...requestObject,
},
]
}
export const mock = Array.from({ length: DAYS * 24 })
.map((_, i) => createMockData({ minutes: i * 60 }))
.reduce((prev, curr) => prev.concat(curr), []) satisfies ColumnSchema[]
export const mockLive = Array.from({ length: 10 })
// REMINDER: do not use random, otherwise data needs to be sorted
.map((_, i) => createMockData({ minutes: -((i + 1) * 0.3) }))
.reduce((prev, curr) => prev.concat(curr), [])
.reverse() satisfies ColumnSchema[]
@@ -0,0 +1,36 @@
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