mirror of
https://github.com/supabase/supabase.git
synced 2026-07-05 02:24:20 +08:00
344 lines
11 KiB
TypeScript
344 lines
11 KiB
TypeScript
import { useEffect, useState, useMemo } from 'react'
|
|
import { Alert, Button, IconEye, IconEyeOff, Input } from '@supabase/ui'
|
|
import DataGrid from '@supabase/react-data-grid'
|
|
|
|
import LogSelection, { LogSelectionProps } from './LogSelection'
|
|
import { LogData, QueryType } from './Logs.types'
|
|
import { isDefaultLogPreviewFormat } from './Logs.utils'
|
|
import CSVButton from 'components/ui/CSVButton'
|
|
import DatabaseApiColumnRender from './LogColumnRenderers/DatabaseApiColumnRender'
|
|
import DatabasePostgresColumnRender from './LogColumnRenderers/DatabasePostgresColumnRender'
|
|
import DefaultPreviewColumnRenderer from './LogColumnRenderers/DefaultPreviewColumnRenderer'
|
|
import { LogQueryError } from '.'
|
|
import ResourcesExceededErrorRenderer from './LogsErrorRenderers/ResourcesExceededErrorRenderer'
|
|
import DefaultErrorRenderer from './LogsErrorRenderers/DefaultErrorRenderer'
|
|
import FunctionsLogsColumnRender from './LogColumnRenderers/FunctionsLogsColumnRender'
|
|
import FunctionsEdgeColumnRender from './LogColumnRenderers/FunctionsEdgeColumnRender'
|
|
|
|
interface Props {
|
|
data?: Array<LogData | Object>
|
|
onHistogramToggle?: () => void
|
|
isHistogramShowing?: boolean
|
|
isLoading?: boolean
|
|
error?: LogQueryError | null
|
|
showDownload?: boolean
|
|
// TODO: move all common params to a context to avoid prop drilling
|
|
queryType?: QueryType
|
|
projectRef: string
|
|
params: LogSelectionProps['params']
|
|
}
|
|
type LogMap = { [id: string]: LogData }
|
|
|
|
interface FormatterArg {
|
|
column: {
|
|
key: string
|
|
name: string
|
|
resizable: boolean
|
|
header: string
|
|
minWidth: number
|
|
idx: number
|
|
frozen: boolean
|
|
isLastFrozenColumn: boolean
|
|
rowGroup: boolean
|
|
sortable: boolean
|
|
}
|
|
isCellSelected: boolean
|
|
onRowChange: Function
|
|
row: any
|
|
}
|
|
|
|
/**
|
|
* Logs table view with focus side panel
|
|
*
|
|
* When in custom data display mode, the side panel will not open when focusing on logs.
|
|
*/
|
|
const LogTable = ({
|
|
data = [],
|
|
queryType,
|
|
onHistogramToggle,
|
|
isHistogramShowing,
|
|
isLoading,
|
|
showDownload,
|
|
error,
|
|
projectRef,
|
|
params,
|
|
}: Props) => {
|
|
const [focusedLog, setFocusedLog] = useState<LogData | null>(null)
|
|
const firstRow: LogData | undefined = data?.[0] as LogData
|
|
const columnNames = Object.keys(data[0] || {})
|
|
const hasId = columnNames.includes('id')
|
|
const hasTimestamp = columnNames.includes('timestamp')
|
|
|
|
const DEFAULT_COLUMNS = columnNames.map((v) => {
|
|
let formatter = undefined
|
|
|
|
formatter = (received: FormatterArg) => {
|
|
const value = received.row?.[v]
|
|
if (value && typeof value === 'object') {
|
|
return `[Object]`
|
|
} else if (value === null) {
|
|
return 'NULL'
|
|
} else {
|
|
return String(value)
|
|
}
|
|
}
|
|
return { key: v, name: v, resizable: true, formatter, header: v, minWidth: 128 }
|
|
})
|
|
|
|
let columns
|
|
if (!queryType) {
|
|
columns = DEFAULT_COLUMNS
|
|
} else {
|
|
switch (queryType) {
|
|
case 'api':
|
|
columns = DatabaseApiColumnRender
|
|
break
|
|
|
|
case 'database':
|
|
columns = DatabasePostgresColumnRender
|
|
break
|
|
|
|
case 'fn_edge':
|
|
columns = FunctionsEdgeColumnRender
|
|
break
|
|
case 'functions':
|
|
columns = FunctionsLogsColumnRender
|
|
break
|
|
|
|
default:
|
|
if (firstRow && isDefaultLogPreviewFormat(firstRow)) {
|
|
columns = DefaultPreviewColumnRenderer
|
|
} else {
|
|
columns = DEFAULT_COLUMNS
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
const stringData = JSON.stringify(data)
|
|
const [dedupedData, logMap] = useMemo<[LogData[], LogMap]>(() => {
|
|
const deduped = [...new Set(data)] as LogData[]
|
|
|
|
if (!hasId) {
|
|
return [deduped, {} as LogMap]
|
|
}
|
|
|
|
const map = deduped.reduce((acc: LogMap, d: LogData) => {
|
|
acc[d.id] = d
|
|
return acc
|
|
}, {}) as LogMap
|
|
|
|
return [deduped, map]
|
|
}, [stringData])
|
|
|
|
useEffect(() => {
|
|
if (!hasId || data === null) return
|
|
if (focusedLog && !(focusedLog.id in logMap)) {
|
|
setFocusedLog(null)
|
|
}
|
|
}, [stringData])
|
|
|
|
if (!data) return null
|
|
|
|
// [Joshen] Hmm quite hacky now, but will do
|
|
const maxHeight = !queryType ? 'calc(100vh - 42px - 10rem)' : 'calc(100vh - 42px - 3rem)'
|
|
|
|
const logDataRows = useMemo(() => {
|
|
if (hasId && hasTimestamp) {
|
|
return Object.values(logMap).sort((a, b) => b.timestamp - a.timestamp)
|
|
} else {
|
|
return dedupedData
|
|
}
|
|
}, [stringData])
|
|
|
|
const renderErrorAlert = () => {
|
|
if (!error) return null
|
|
const childProps = {
|
|
isCustomQuery: queryType ? false : true,
|
|
error: error!,
|
|
}
|
|
let Renderer = DefaultErrorRenderer
|
|
if (
|
|
typeof error === 'object' &&
|
|
error.error?.errors.find((err) => err.reason === 'resourcesExceeded')
|
|
) {
|
|
Renderer = ResourcesExceededErrorRenderer
|
|
}
|
|
|
|
return (
|
|
<div className="flex justify-center px-5">
|
|
<Alert
|
|
variant="danger"
|
|
title="Sorry! An error occured when fetching data."
|
|
withIcon
|
|
className="w-1/2"
|
|
>
|
|
<Renderer {...childProps} />
|
|
</Alert>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<section
|
|
className={'flex flex-col w-full ' + (!queryType ? 'shadow-lg' : '')}
|
|
style={{ maxHeight }}
|
|
>
|
|
{!queryType && (
|
|
<div>
|
|
<div
|
|
className="
|
|
w-full bg-scale-100 dark:bg-scale-300
|
|
rounded-tl rounded-tr
|
|
border-t
|
|
border-l
|
|
border-r
|
|
flex items-center justify-between
|
|
px-5 py-2
|
|
"
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
{data && data.length ? (
|
|
<>
|
|
<span className="text-sm text-scale-1200">Query results</span>
|
|
<span className="text-sm text-scale-1100">{data && data.length}</span>
|
|
</>
|
|
) : (
|
|
<span className="text-xs text-scale-1200">Results will be shown below</span>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{onHistogramToggle && (
|
|
<Button
|
|
type="default"
|
|
icon={isHistogramShowing ? <IconEye /> : <IconEyeOff />}
|
|
onClick={onHistogramToggle}
|
|
>
|
|
Histogram
|
|
</Button>
|
|
)}
|
|
{showDownload && <CSVButton data={data}>Download</CSVButton>}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
<div className={`flex flex-row h-full ${!queryType ? 'border-l border-r' : ''}`}>
|
|
<DataGrid
|
|
style={{ height: '100%' }}
|
|
className={`
|
|
flex-grow flex-1
|
|
${!queryType ? 'data-grid--logs-explorer' : ' data-grid--simple-logs'}
|
|
`}
|
|
rowHeight={40}
|
|
headerRowHeight={queryType ? 0 : 28}
|
|
onSelectedCellChange={({ idx, rowIdx }) => {
|
|
if (!hasId) return
|
|
setFocusedLog(data[rowIdx] as LogData)
|
|
}}
|
|
noRowsFallback={
|
|
!isLoading ? (
|
|
<>
|
|
<div className="py-4 w-full h-full flex-col space-y-12">
|
|
{!error && (
|
|
<div
|
|
className={`transition-all
|
|
duration-500
|
|
delay-200
|
|
|
|
flex
|
|
flex-col
|
|
items-center
|
|
|
|
gap-6
|
|
text-center
|
|
mt-16
|
|
opacity-100
|
|
scale-100
|
|
|
|
justify-center
|
|
`}
|
|
>
|
|
<>
|
|
<div className="flex flex-col gap-1">
|
|
<div className="relative border border-scale-600 border-dashed dark:border-scale-900 w-32 h-4 rounded px-2 flex items-center"></div>
|
|
<div className="relative border border-scale-600 border-dashed dark:border-scale-900 w-32 h-4 rounded px-2 flex items-center">
|
|
<div className="absolute right-1 -bottom-4">
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
className="h-6 w-6"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-col gap-1 px-5">
|
|
<h3 className="text-lg text-scale-1200">No results</h3>
|
|
<p className="text-sm text-scale-900">
|
|
Try another search, or adjusting the filters
|
|
</p>
|
|
</div>
|
|
</>
|
|
</div>
|
|
)}
|
|
{error && renderErrorAlert()}
|
|
</div>
|
|
</>
|
|
) : null
|
|
}
|
|
columns={columns as any}
|
|
rowClass={(r) => {
|
|
const row = r as LogData
|
|
|
|
let classes = []
|
|
classes.push(
|
|
`${
|
|
row.id === focusedLog?.id ? '!bg-scale-400 rdg-row--focussed' : 'cursor-pointer'
|
|
}`
|
|
)
|
|
|
|
return classes.join(' ')
|
|
}}
|
|
rows={logDataRows}
|
|
rowKeyGetter={(r) => {
|
|
if (!hasId) return Object.keys(r)[0]
|
|
const row = r as LogData
|
|
return row.id
|
|
}}
|
|
onRowClick={(r) => setFocusedLog(r)}
|
|
/>
|
|
{logDataRows.length > 0 ? (
|
|
<div
|
|
className={
|
|
queryType
|
|
? 'w-1/2 flex flex-col'
|
|
: focusedLog
|
|
? 'w-1/2 flex flex-col'
|
|
: 'w-0 hidden'
|
|
}
|
|
>
|
|
<LogSelection
|
|
projectRef={projectRef}
|
|
onClose={() => setFocusedLog(null)}
|
|
log={focusedLog}
|
|
queryType={queryType}
|
|
params={params}
|
|
/>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
</section>
|
|
</>
|
|
)
|
|
}
|
|
export default LogTable
|