mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 01:45:10 +03:00
chore: start log preview renderers
This commit is contained in:
1 parent
6c2a86ee1f
commit
3d8bc63f26
15 files changed
+913
-130
No files matched your search
+22
@@ -0,0 +1,22 @@
|
||||
import dayjs from 'dayjs'
|
||||
import { ResponseCodeFormatter } from '../LogsFormatters'
|
||||
|
||||
export default [
|
||||
{
|
||||
formatter: (data: any) => (
|
||||
<div className="flex h-full w-full items-center justify-between gap-3">
|
||||
<div className="flex h-full w-full items-center gap-4">
|
||||
<ResponseCodeFormatter row={data} value={data.row.metadata[0].response[0].status_code} />
|
||||
<span className="text-xs w-14">{data.row.metadata[0].request[0].method}</span>
|
||||
<span className="font-mono text-xs">{data.row.metadata[0].request[0].path}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="flex w-full h-full items-center gap-1">
|
||||
<span className="text-xs">{dayjs(data?.row?.timestamp / 1000).format('DD MMM')}</span>
|
||||
<span className="text-xs">{dayjs(data?.row?.timestamp / 1000).format('HH:mm:ss')}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import dayjs from 'dayjs'
|
||||
import { SeverityFormatter } from '../LogsFormatters'
|
||||
|
||||
export default [
|
||||
{
|
||||
formatter: (data: any) => (
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-20 flex items-center gap-1">
|
||||
<span className="text-xs">{dayjs(data?.row?.timestamp / 1000).format('DD MMM')}</span>
|
||||
<span className="text-xs">{dayjs(data?.row?.timestamp / 1000).format('HH:mm:ss')}</span>
|
||||
</span>
|
||||
<div className="w-16 flex justify-center">
|
||||
<SeverityFormatter value={data?.row?.metadata[0].parsed[0].error_severity} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex truncate">
|
||||
<span className="font-mono text-xs truncate">{data.row.event_message}</span>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
@@ -2,67 +2,158 @@ import dayjs from 'dayjs'
|
||||
import { FC } from 'react'
|
||||
import { Typography, IconX, Input } from '@supabase/ui'
|
||||
|
||||
import { LogData } from './Logs.types'
|
||||
import { LogData, QueryType } from './Logs.types'
|
||||
import JsonEditor from 'components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/JsonEditor/JsonCodeEditor'
|
||||
import { jsonSyntaxHighlight } from './LogsFormatters'
|
||||
import DatabaseApiSelectionRender, {
|
||||
DatabaseApiSelectionHeaderRender,
|
||||
} from './LogSelectionRenderers/DatabaseApiSelectionRender'
|
||||
import DatabasePostgresSelectionRender from './LogSelectionRenderers/DatabasePostgresSelectionRender'
|
||||
|
||||
interface Props {
|
||||
log: LogData
|
||||
onClose: () => void
|
||||
queryType: QueryType
|
||||
}
|
||||
|
||||
/**
|
||||
* Log selection display
|
||||
*/
|
||||
const LogSelection: FC<Props> = ({ log, onClose }) => {
|
||||
const LogSelection: FC<Props> = ({ log, onClose, queryType }) => {
|
||||
// console.log('log in selecion', log)
|
||||
|
||||
console.log('log selection in component', log)
|
||||
// @ts-ignore
|
||||
console.log('log response', log?.response)
|
||||
// @ts-ignore
|
||||
console.log('log request', log?.request)
|
||||
|
||||
// response highlight
|
||||
// @ts-ignore
|
||||
const response = log?.response ? jsonSyntaxHighlight(log.response) : ''
|
||||
|
||||
console.log('output', response)
|
||||
|
||||
const Formatter = () => {
|
||||
switch (queryType) {
|
||||
case 'api':
|
||||
return <DatabaseApiSelectionRender log={log} />
|
||||
break
|
||||
|
||||
case 'database':
|
||||
return <DatabasePostgresSelectionRender log={log} />
|
||||
break
|
||||
|
||||
default:
|
||||
return null
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function header() {
|
||||
switch (queryType) {
|
||||
case 'api':
|
||||
return DatabaseApiSelectionHeaderRender(log)
|
||||
break
|
||||
|
||||
default:
|
||||
return null
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
'h-full flex flex-col flex-grow border border-l-0',
|
||||
'h-full flex flex-col flex-grow border border-l',
|
||||
'border-panel-border-light dark:border-panel-border-dark',
|
||||
'bg-gray-200',
|
||||
].join(' ')}
|
||||
>
|
||||
<div
|
||||
className={[
|
||||
'bg-panel-header-light dark:bg-panel-header-dark',
|
||||
'border-b border-panel-border-interior-light',
|
||||
'dark:border-panel-border-interior-dark',
|
||||
].join(' ')}
|
||||
>
|
||||
<div className="px-6 py-4 flex items-center">
|
||||
<div className="flex flex-row justify-between items-center w-full">
|
||||
<Typography.Title level={5}>{dayjs(log.timestamp / 1000).toString()}</Typography.Title>
|
||||
<div className="cursor-pointer" onClick={onClose}>
|
||||
<IconX size={14} />
|
||||
{log ? (
|
||||
<>
|
||||
<div
|
||||
className={[
|
||||
'bg-panel-header-light dark:bg-panel-header-dark',
|
||||
'border-b border-panel-border-interior-light',
|
||||
'dark:border-panel-border-interior-dark',
|
||||
].join(' ')}
|
||||
>
|
||||
<div className="px-6 py-4 flex items-center">
|
||||
<div className="flex flex-row justify-between items-center w-full">
|
||||
{/* <Typography.Title level={5}>
|
||||
{dayjs(log.timestamp / 1000).toString()}
|
||||
</Typography.Title> */}
|
||||
<h3 className="text-xl font-semibold">{header()}</h3>
|
||||
<div className="cursor-pointer" onClick={onClose}>
|
||||
<IconX size={14} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-grow overflow-y-auto bg-panel-body-light dark:bg-panel-body-dark">
|
||||
<div className="p-4 flex-col space-y-4">
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm">Event message</p>
|
||||
<Input
|
||||
copy
|
||||
readOnly
|
||||
size="small"
|
||||
layout="vertical"
|
||||
className="text-sm input-mono"
|
||||
value={log.event_message}
|
||||
/>
|
||||
<div
|
||||
className="
|
||||
flex-grow overflow-y-auto
|
||||
bg-panel-body-light dark:bg-panel-body-dark
|
||||
space-y-6
|
||||
py-8
|
||||
"
|
||||
>
|
||||
{/* <div className="p-4 flex-col space-y-4">
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm">Event message</p>
|
||||
<Input
|
||||
copy
|
||||
readOnly
|
||||
size="small"
|
||||
layout="vertical"
|
||||
className="text-sm input-mono"
|
||||
value={log.event_message}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm">Metadata</p>
|
||||
<Input.TextArea
|
||||
copy
|
||||
readOnly
|
||||
size="small"
|
||||
layout="vertical"
|
||||
rows={20}
|
||||
className="input-mono"
|
||||
value={JSON.stringify(log.metadata, null, 2)}
|
||||
/>
|
||||
</div>
|
||||
</div> */}
|
||||
<Formatter />
|
||||
{/* <pre className="text-sm px-5">
|
||||
<h3 className="text-xl text-scale-1200">Request</h3>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: log.request ? jsonSyntaxHighlight(log.request) : '',
|
||||
}}
|
||||
/>
|
||||
</pre>
|
||||
<pre className="text-sm px-5">
|
||||
<h3 className="text-xl text-scale-1200">Request</h3>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: log.response ? jsonSyntaxHighlight(log.response) : '',
|
||||
}}
|
||||
/>
|
||||
</pre> */}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm">Metadata</p>
|
||||
<Input.TextArea
|
||||
copy
|
||||
readOnly
|
||||
size="small"
|
||||
layout="vertical"
|
||||
rows={20}
|
||||
className="input-mono"
|
||||
value={JSON.stringify(log.metadata, null, 2)}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<div className="w-1/2 text-center flex flex-col gap-2">
|
||||
<h3 className="text-sm text-scale-1200">Select an Event</h3>
|
||||
<p className="text-xs text-scale-1100">
|
||||
Select an Event to view the code snippet (pretty view) or complete JSON payload (raw
|
||||
view).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { QueryType } from './Logs.types'
|
||||
|
||||
const SelectionRenderHandler = ({ type }: { type: QueryType }) => {
|
||||
switch (type) {
|
||||
case 'api':
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
export default SelectionRenderHandler
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
import React from 'react'
|
||||
import { jsonSyntaxHighlight, ResponseCodeFormatter } from '../LogsFormatters'
|
||||
|
||||
const DatabaseApiSelectionRender = ({ log }: any) => {
|
||||
const request = log?.metadata[0]?.request[0]
|
||||
const response = log?.metadata[0]?.response[0]
|
||||
const method = log?.metadata[0]?.request[0]?.method
|
||||
const status = log?.metadata[0]?.response[0].status_code
|
||||
const ipAddress = log?.metadata[0]?.request[0].headers[0]?.cf_connecting_ip
|
||||
const countryOrigin = log?.metadata[0]?.request[0].headers[0]?.cf_ipcountry
|
||||
const clientInfo = log?.metadata[0]?.request[0].headers[0]?.x_client_info
|
||||
const referer = log?.metadata[0]?.request[0].headers[0]?.referer
|
||||
|
||||
const DetailedRow = ({ label, value }: { label: string; value: string | React.ReactNode }) => {
|
||||
return (
|
||||
<div className="grid grid-cols-12">
|
||||
<span className="text-scale-900 text-sm col-span-4">{label}</span>
|
||||
<span className="text-scale-1200 text-base col-span-8">{value}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="px-5 space-y-2">
|
||||
<DetailedRow label="Status" value={<ResponseCodeFormatter value={status} />} />
|
||||
<DetailedRow label="Method" value={method} />
|
||||
<DetailedRow label="Timestamp" value={log.timestamp} />
|
||||
<DetailedRow label="IP Address" value={ipAddress} />
|
||||
<DetailedRow label="Origin Country" value={countryOrigin} />
|
||||
{clientInfo && <DetailedRow label="Client" value={clientInfo} />}
|
||||
{referer && <DetailedRow label="Referer" value={referer} />}
|
||||
</div>
|
||||
<div className="px-5">
|
||||
<h3 className="text-lg text-scale-1200 mb-4">Request body</h3>
|
||||
<pre className="text-sm syntax-highlight overflow-x-auto">
|
||||
<div
|
||||
className="text-wrap"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: request ? jsonSyntaxHighlight(log.metadata[0].request[0]) : '',
|
||||
}}
|
||||
/>
|
||||
</pre>
|
||||
</div>
|
||||
<div className="px-5">
|
||||
<h3 className="text-lg text-scale-1200 mb-4">
|
||||
Response{method ? ` ${method}` : null} body
|
||||
</h3>
|
||||
<pre className="text-sm syntax-highlight overflow-x-auto">
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: response ? jsonSyntaxHighlight(log.metadata[0].response[0]) : '',
|
||||
}}
|
||||
/>
|
||||
</pre>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const DatabaseApiSelectionHeaderRender = (log: any) => {
|
||||
const method = log?.metadata[0]?.request[0]?.method
|
||||
const path = log?.metadata[0]?.request[0]?.path
|
||||
|
||||
return `${method} ${path}`
|
||||
}
|
||||
|
||||
export default DatabaseApiSelectionRender
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
import { Alert } from '@supabase/ui'
|
||||
import React from 'react'
|
||||
import { jsonSyntaxHighlight, SeverityFormatter } from '../LogsFormatters'
|
||||
|
||||
const DatabasePostgresSelectionRender = ({ log }: any) => {
|
||||
const postgresUsername = log?.metadata[0]?.parsed[0]?.user_name
|
||||
const sessionId = log?.metadata[0]?.parsed[0]?.session_id
|
||||
const hint = log?.metadata[0]?.parsed[0]?.hint
|
||||
|
||||
const DetailedRow = ({ label, value }: { label: string; value: string | React.ReactNode }) => {
|
||||
return (
|
||||
<div className="grid grid-cols-12">
|
||||
<span className="text-scale-900 text-sm col-span-4">{label}</span>
|
||||
<span className="text-scale-1200 text-base col-span-8">{value}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="px-5">
|
||||
<span className="text-scale-900 text-sm col-span-4">Event message</span>
|
||||
|
||||
<div
|
||||
className="text-wrap font-mono text-scale-1200 mt-2"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: log.event_message,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-px w-full bg-panel-border-interior-light dark:bg-panel-border-interior-dark"></div>
|
||||
<div className="px-5 space-y-2">
|
||||
<DetailedRow label="Severity" value={<SeverityFormatter value={log.error_severity} />} />
|
||||
<DetailedRow label="Postgres Username" value={postgresUsername} />
|
||||
<DetailedRow label="Session ID" value={sessionId} />
|
||||
</div>
|
||||
|
||||
{hint && (
|
||||
<div className="mt-4 px-5">
|
||||
<Alert variant="warning" withIcon title={log?.metadata[0]?.parsed[0]?.hint} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="h-px w-full bg-panel-border-interior-light dark:bg-panel-border-interior-dark"></div>
|
||||
<div className="px-5">
|
||||
<h3 className="text-lg text-scale-1200 mb-4">Metadata</h3>
|
||||
<pre className="text-sm syntax-highlight overflow-x-auto">
|
||||
<div
|
||||
className="text-wrap"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: log.metadata ? jsonSyntaxHighlight(log.metadata[0]) : '',
|
||||
}}
|
||||
/>
|
||||
</pre>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const DatabaseApiSelectionHeaderRender = (log: any) => {
|
||||
const method = log?.metadata[0]?.request[0]?.method
|
||||
const path = log?.metadata[0]?.request[0]?.path
|
||||
|
||||
return `${method} ${path}`
|
||||
}
|
||||
|
||||
export default DatabasePostgresSelectionRender
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { jsonSyntaxHighlight } from '../LogsFormatters'
|
||||
|
||||
const FunctionInvocationSelectionRender = ({ log }: any) => {
|
||||
return (
|
||||
<>
|
||||
<pre className="text-sm px-5">
|
||||
<h3 className="text-xl text-scale-1200">Request</h3>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: log.request ? jsonSyntaxHighlight(log.request) : '',
|
||||
}}
|
||||
/>
|
||||
</pre>
|
||||
<pre className="text-sm px-5">
|
||||
<h3 className="text-xl text-scale-1200">Response</h3>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: log.response ? jsonSyntaxHighlight(log.response) : '',
|
||||
}}
|
||||
/>
|
||||
</pre>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default FunctionInvocationSelectionRender
|
||||
@@ -1,15 +1,21 @@
|
||||
import dayjs from 'dayjs'
|
||||
import { useEffect, useState, useMemo } from 'react'
|
||||
import { Typography } from '@supabase/ui'
|
||||
import DataGrid from '@supabase/react-data-grid'
|
||||
import { Badge, Button, IconDownload, IconDownloadCloud, IconEye, Typography } from '@supabase/ui'
|
||||
import DataGrid, { Row } from '@supabase/react-data-grid'
|
||||
|
||||
import LogSelection from './LogSelection'
|
||||
import { LogData } from './Logs.types'
|
||||
import { LogData, Mode, QueryType } from './Logs.types'
|
||||
import { isNil } from 'lodash'
|
||||
import { SeverityFormatter, ResponseCodeFormatter, HeaderFormmater } from './LogsFormatters'
|
||||
|
||||
// column renders
|
||||
import DatabaseApiColumnRender from './LogColumnRenderers/DatabaseApiColumnRender'
|
||||
import DatabasePostgresColumnRender from './LogColumnRenderers/DatabasePostgresColumnRender'
|
||||
|
||||
interface Props {
|
||||
isCustomQuery: boolean
|
||||
data?: Array<LogData | Object>
|
||||
queryType: QueryType
|
||||
}
|
||||
type LogMap = { [id: string]: LogData }
|
||||
|
||||
@@ -18,43 +24,180 @@ type LogMap = { [id: string]: LogData }
|
||||
*
|
||||
* When in custom data display mode, the side panel will not open when focusing on logs.
|
||||
*/
|
||||
const LogTable = ({ isCustomQuery, data = [] }: Props) => {
|
||||
const LogTable = ({ isCustomQuery, data = [], queryType }: Props) => {
|
||||
const [focusedLog, setFocusedLog] = useState<LogData | null>(null)
|
||||
const columnNames = Object.keys(data[0] || {})
|
||||
// whether the data structure is LogData format.
|
||||
const hasLogDataFormat =
|
||||
columnNames.includes('timestamp') &&
|
||||
columnNames.includes('event_message') &&
|
||||
columnNames.length === 4
|
||||
|
||||
const columns = (hasLogDataFormat ? ['timestamp', 'event_message'] : columnNames).map((v) => ({
|
||||
key: v,
|
||||
name: v,
|
||||
width: hasLogDataFormat && v === 'timestamp' ? 210 : undefined,
|
||||
resizable: true,
|
||||
headerRenderer: () => {
|
||||
return <div className="flex items-center text-xs font-mono h-full">{v}</div>
|
||||
},
|
||||
formatter: ({ row }: any) => {
|
||||
let value = row[v]
|
||||
if (hasLogDataFormat && v === 'timestamp') {
|
||||
value = dayjs(Number(row['timestamp']) / 1000).toISOString()
|
||||
console.log('queryType', queryType)
|
||||
|
||||
// console.log('focusedLog', focusedLog)
|
||||
|
||||
isCustomQuery = false
|
||||
|
||||
// whether the data structure is LogData format.
|
||||
// const hasLogDataFormat =
|
||||
// columnNames.includes('timestamp') &&
|
||||
// columnNames.includes('event_message') &&
|
||||
// columnNames.length === 4
|
||||
|
||||
const hasLogDataFormat = !isCustomQuery
|
||||
|
||||
const DEFAULT_COLUMNS = (hasLogDataFormat ? ['timestamp', 'event_message'] : columnNames).map(
|
||||
(v) => ({
|
||||
key: v,
|
||||
name: v,
|
||||
width: hasLogDataFormat && v === 'timestamp' ? 210 : undefined,
|
||||
resizable: true,
|
||||
headerRenderer: () => {
|
||||
return <div className="flex items-center text-xs font-mono h-full">{v}</div>
|
||||
},
|
||||
formatter: ({ row }: any) => {
|
||||
let value = row[v]
|
||||
if (hasLogDataFormat && v === 'timestamp') {
|
||||
value = dayjs(Number(row['timestamp']) / 1000).toISOString()
|
||||
}
|
||||
return (
|
||||
<p
|
||||
className={[
|
||||
'block whitespace-wrap font-mono',
|
||||
`${hasLogDataFormat && row.id === focusedLog?.id ? 'font-bold' : ''}`,
|
||||
`${hasLogDataFormat && v === 'timestamp' ? 'text-green-900' : ''}`,
|
||||
].join(' ')}
|
||||
>
|
||||
{value}
|
||||
</p>
|
||||
)
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
let columns
|
||||
|
||||
console.log('QUERY TYPE', queryType)
|
||||
|
||||
switch (queryType) {
|
||||
case 'api':
|
||||
if (isCustomQuery) {
|
||||
columns = DEFAULT_COLUMNS
|
||||
break
|
||||
}
|
||||
return (
|
||||
<p
|
||||
className={[
|
||||
'block whitespace-wrap font-mono',
|
||||
`${hasLogDataFormat && row.id === focusedLog?.id ? 'font-bold' : ''}`,
|
||||
`${hasLogDataFormat && v === 'timestamp' ? 'text-green-900' : ''}`,
|
||||
].join(' ')}
|
||||
>
|
||||
{value}
|
||||
</p>
|
||||
)
|
||||
},
|
||||
}))
|
||||
|
||||
const stringData = JSON.stringify(data)
|
||||
|
||||
console.log('is api, running column')
|
||||
columns = DatabaseApiColumnRender
|
||||
|
||||
break
|
||||
|
||||
case 'database':
|
||||
if (isCustomQuery) {
|
||||
columns = DEFAULT_COLUMNS
|
||||
break
|
||||
}
|
||||
|
||||
console.log('is api, running column')
|
||||
columns = DatabasePostgresColumnRender
|
||||
|
||||
break
|
||||
|
||||
case 'fn_edge':
|
||||
if (isCustomQuery) {
|
||||
columns = DEFAULT_COLUMNS
|
||||
break
|
||||
}
|
||||
|
||||
columns = [
|
||||
{
|
||||
key: 'timestamp',
|
||||
headerRenderer: () => (
|
||||
<div className="flex w-full justify-end h-full">
|
||||
<HeaderFormmater value={'timestamp'} />
|
||||
</div>
|
||||
),
|
||||
name: 'timestamp',
|
||||
formatter: (data: any) => (
|
||||
<span className="flex w-full h-full items-center gap-1">
|
||||
<span className="text-xs">{dayjs(data?.row?.timestamp / 1000).format('DD MMM')}</span>
|
||||
<span className="text-xs">
|
||||
{dayjs(data?.row?.timestamp / 1000).format('HH:mm:ss')}
|
||||
</span>
|
||||
{/* {data?.row?.timestamp} */}
|
||||
</span>
|
||||
),
|
||||
width: 128,
|
||||
},
|
||||
{
|
||||
key: 'status_code',
|
||||
headerRenderer: () => <HeaderFormmater value={'Status'} />,
|
||||
name: 'status_code',
|
||||
formatter: (data: any) => (
|
||||
<ResponseCodeFormatter row={data} value={data.row.status_code} />
|
||||
),
|
||||
|
||||
width: 0,
|
||||
resizable: true,
|
||||
},
|
||||
{
|
||||
key: 'method',
|
||||
headerRenderer: () => <HeaderFormmater value={'method'} />,
|
||||
width: 0,
|
||||
resizable: true,
|
||||
},
|
||||
{
|
||||
key: 'id',
|
||||
headerRenderer: () => <HeaderFormmater value={'id'} />,
|
||||
name: 'id',
|
||||
resizable: true,
|
||||
},
|
||||
]
|
||||
break
|
||||
case 'functions':
|
||||
if (isCustomQuery) {
|
||||
columns = DEFAULT_COLUMNS
|
||||
break
|
||||
}
|
||||
columns = [
|
||||
{
|
||||
key: 'timestamp',
|
||||
headerRenderer: () => (
|
||||
<div className="flex w-full justify-end h-full">
|
||||
<HeaderFormmater value={'timestamp'} />
|
||||
</div>
|
||||
),
|
||||
name: 'timestamp',
|
||||
formatter: (data: any) => (
|
||||
<span className="flex w-full h-full items-center gap-1">
|
||||
<span className="text-xs !text-scale-1100">
|
||||
{dayjs(data?.row?.timestamp / 1000).format('DD MMM')}
|
||||
</span>
|
||||
<span className="text-xs !text-scale-1100">
|
||||
{dayjs(data?.row?.timestamp / 1000).format('HH:mm:ss')}
|
||||
</span>
|
||||
{/* {data?.row?.timestamp} */}
|
||||
</span>
|
||||
),
|
||||
width: 128,
|
||||
resizable: true,
|
||||
},
|
||||
{
|
||||
key: 'level',
|
||||
headerRenderer: () => <HeaderFormmater value={'Level'} />,
|
||||
name: 'level',
|
||||
formatter: (data: any) => <SeverityFormatter value={data.row.level} />,
|
||||
width: 24,
|
||||
resizable: true,
|
||||
},
|
||||
{
|
||||
key: 'event_message',
|
||||
headerRenderer: () => <HeaderFormmater value={'Event message'} />,
|
||||
resizable: true,
|
||||
},
|
||||
]
|
||||
break
|
||||
|
||||
default:
|
||||
columns = DEFAULT_COLUMNS
|
||||
break
|
||||
}
|
||||
|
||||
const logMap = useMemo(() => {
|
||||
if (!hasLogDataFormat) return {} as LogMap
|
||||
const logData = data as LogData[]
|
||||
@@ -62,17 +205,23 @@ const LogTable = ({ isCustomQuery, data = [] }: Props) => {
|
||||
acc[d.id] = d
|
||||
return acc
|
||||
}, {}) as LogMap
|
||||
}, [stringData])
|
||||
}, [JSON.stringify(data)])
|
||||
|
||||
const stringData = JSON.stringify(data)
|
||||
useEffect(() => {
|
||||
if (!hasLogDataFormat) return
|
||||
if (isNil(data)) return
|
||||
if (focusedLog && !(focusedLog.id in logMap)) {
|
||||
setFocusedLog(null)
|
||||
}
|
||||
}
|
||||
}, [stringData])
|
||||
|
||||
if (!data) return null
|
||||
|
||||
// console.log(data)
|
||||
// console.log('logMap', logMap)
|
||||
// console.log(columns)
|
||||
|
||||
// [Joshen] Hmm quite hacky now, but will do
|
||||
const maxHeight = isCustomQuery ? 'calc(100vh - 42px - 10rem)' : 'calc(100vh - 42px - 3rem)'
|
||||
|
||||
@@ -81,45 +230,103 @@ const LogTable = ({ isCustomQuery, data = [] }: Props) => {
|
||||
return Object.values(logMap).sort((a, b) => b.timestamp - a.timestamp)
|
||||
}, [stringData])
|
||||
return (
|
||||
<section className="flex flex-1 flex-row" style={{ maxHeight }}>
|
||||
<DataGrid
|
||||
style={{ height: '100%' }}
|
||||
className="flex-grow flex-1"
|
||||
onSelectedCellChange={({ idx, rowIdx }) => {
|
||||
if (!hasLogDataFormat) return
|
||||
setFocusedLog(data[rowIdx] as LogData)
|
||||
}}
|
||||
noRowsFallback={
|
||||
<div className="p-4">
|
||||
<Typography.Text type="secondary" small className="font-mono">
|
||||
No data returned from query
|
||||
</Typography.Text>
|
||||
<>
|
||||
{isCustomQuery && (
|
||||
<div
|
||||
className="
|
||||
w-full bg-scale-300 rounded
|
||||
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">
|
||||
<Button type="default" icon={<IconEye />}>
|
||||
Histogram
|
||||
</Button>
|
||||
<Button type="default" icon={<IconDownloadCloud />}>
|
||||
Download
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
columns={columns as any}
|
||||
rowClass={(r) => {
|
||||
if (!hasLogDataFormat) return 'cursor-pointer'
|
||||
const row = r as LogData
|
||||
return `${row.id === focusedLog?.id ? 'bg-green-800' : 'cursor-pointer'}`
|
||||
}}
|
||||
rows={logDataRows}
|
||||
rowKeyGetter={(r) => {
|
||||
if (!hasLogDataFormat) return Object.keys(r)[0]
|
||||
const row = r as LogData
|
||||
return row.id
|
||||
}}
|
||||
onRowClick={(r) => {
|
||||
if (!hasLogDataFormat) return
|
||||
const row = r as LogData
|
||||
setFocusedLog(logMap[row.id])
|
||||
}}
|
||||
/>
|
||||
{focusedLog && (
|
||||
<div className="w-2/5 flex flex-col">
|
||||
<LogSelection onClose={() => setFocusedLog(null)} log={focusedLog} />
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
<section className="flex flex-1 flex-row" style={{ maxHeight }}>
|
||||
<DataGrid
|
||||
style={{ height: '100%' }}
|
||||
className={`
|
||||
flex-grow flex-1
|
||||
${isCustomQuery ? '' : ' data-grid--simple-logs'}
|
||||
`}
|
||||
rowHeight={40}
|
||||
headerRowHeight={0}
|
||||
onSelectedCellChange={({ idx, rowIdx }) => {
|
||||
if (!hasLogDataFormat) return
|
||||
setFocusedLog(data[rowIdx] as LogData)
|
||||
}}
|
||||
noRowsFallback={
|
||||
<div className="p-4">
|
||||
<Typography.Text type="secondary" small className="font-mono">
|
||||
No data returned from query
|
||||
</Typography.Text>
|
||||
</div>
|
||||
}
|
||||
columns={columns as any}
|
||||
rowClass={(r) => {
|
||||
// if (!hasLogDataFormat) return 'cursor-pointer'
|
||||
const row = r as LogData
|
||||
|
||||
let classes = []
|
||||
|
||||
//@ts-ignore
|
||||
// if (row.status_code) {
|
||||
// const response = parseResponseCode(row.status_code)
|
||||
// if (response == 'warning') {
|
||||
// classes.push('!bg-yellow-200 !dark:bg-yellow-100')
|
||||
// }
|
||||
// if (response == 'error') {
|
||||
// classes.push('!bg-red-200')
|
||||
// }
|
||||
// }
|
||||
|
||||
classes.push(
|
||||
`${row.id === focusedLog?.id ? '!bg-scale-400 rdg-row--focussed' : 'cursor-pointer'}`
|
||||
)
|
||||
|
||||
return classes.join(' ')
|
||||
}}
|
||||
rows={logDataRows}
|
||||
rowKeyGetter={(r) => {
|
||||
if (!hasLogDataFormat) return Object.keys(r)[0]
|
||||
const row = r as LogData
|
||||
return row.id
|
||||
}}
|
||||
onRowClick={(r) => {
|
||||
// if (!hasLogDataFormat) return
|
||||
const row = r as LogData
|
||||
setFocusedLog(logMap[row.id])
|
||||
}}
|
||||
/>
|
||||
{/* {hasLogDataFormat ||
|
||||
(focusedLog && ( */}
|
||||
<div className="w-1/2 flex flex-col">
|
||||
<LogSelection
|
||||
onClose={() => setFocusedLog(null)}
|
||||
log={focusedLog}
|
||||
queryType={queryType}
|
||||
/>
|
||||
</div>
|
||||
{/* ))} */}
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
export default LogTable
|
||||
@@ -172,28 +172,30 @@ LIMIT 100
|
||||
]
|
||||
|
||||
export const LOG_TYPE_LABEL_MAPPING: { [k: string]: string } = {
|
||||
explorer: "Explorer",
|
||||
explorer: 'Explorer',
|
||||
api: 'API',
|
||||
database: 'Database',
|
||||
}
|
||||
|
||||
export const genDefaultQuery = (table: string, where: string = ''): string => `SELECT
|
||||
id, timestamp, event_message, metadata
|
||||
FROM
|
||||
${table}${where ? ' WHERE\n ' + where : ''}
|
||||
export const genDefaultQuery = (table: LogsTableName, where: string = '') => {
|
||||
console.log('genDefaultQuery table', table)
|
||||
switch (table) {
|
||||
case 'edge_logs':
|
||||
return `SELECT id, timestamp, event_message, metadata
|
||||
FROM ${table}
|
||||
${where ? ' WHERE\n ' + where : ''}
|
||||
LIMIT 100
|
||||
`
|
||||
export const cleanQuery = (str: string) => str.replace(/\n/g, ' ')
|
||||
// .replace(/\n.*\-\-.*(\n)?$?/, "")
|
||||
|
||||
|
||||
export enum LogsTableName {
|
||||
EDGE = 'edge_logs',
|
||||
POSTGRES = 'postgres_logs',
|
||||
}
|
||||
export const genCountQuery = (table: string): string => `SELECT count(*) as count FROM ${table}`
|
||||
|
||||
export const genQueryParams = (params: { [k: string ]: string }) => {
|
||||
export const genQueryParams = (params: { [k: string]: string }) => {
|
||||
// remove keys which are empty strings, null, or undefined
|
||||
for (const k in params) {
|
||||
const v = params[k]
|
||||
|
||||
@@ -3,6 +3,7 @@ interface Metadata {
|
||||
}
|
||||
export type LogSearchCallback = (filters: { query: string; to?: string; from?: string; fromMicro?: number, toMicro?: number }) => void
|
||||
|
||||
export type LogSearchCallback = (filters: { query: string; to?: string; toMicro?: number }) => void
|
||||
export interface LogsEndpointParams {
|
||||
// project ref
|
||||
project: string
|
||||
@@ -44,3 +45,9 @@ type LFResponse<T> = {
|
||||
export type Count = LFResponse<CountData>
|
||||
|
||||
export type Logs = LFResponse<LogData>
|
||||
|
||||
export type QueryType = 'api' | 'database' | 'functions' | 'fn_edge' | 'sql'
|
||||
|
||||
export type Mode = 'simple' | 'custom'
|
||||
|
||||
export type Table = 'edge_logs' | 'postgres_logs'
|
||||
@@ -0,0 +1,207 @@
|
||||
/*
|
||||
* Response Code
|
||||
*
|
||||
* for http response codes
|
||||
*/
|
||||
|
||||
import { IconAlertCircle, IconInfo } from '@supabase/ui'
|
||||
|
||||
export const ResponseCodeFormatter = ({ value }: any) => {
|
||||
if (!value) {
|
||||
return (
|
||||
<div>
|
||||
<label className="text-xs text-scale-800">No data</label>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const split = value.toString().split('')[0]
|
||||
|
||||
// console.log(split)
|
||||
|
||||
switch (split) {
|
||||
// 2XX || 1XX responses
|
||||
case '1':
|
||||
case '2':
|
||||
return (
|
||||
<div className="flex items-center h-full">
|
||||
<div
|
||||
className="relative rounded px-2 py-1 text-center h-6 flex justify-center items-center
|
||||
bg-scale-300 dark:bg-scale-600
|
||||
|
||||
"
|
||||
>
|
||||
<label className="block font-mono text-sm text-scale-900">{value}</label>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
break
|
||||
// 5XX responses
|
||||
case '5':
|
||||
return (
|
||||
<div className="flex items-center h-full">
|
||||
<div
|
||||
className="relative rounded px-2 py-1 text-center h-6 flex justify-center items-center
|
||||
bg-red-400
|
||||
|
||||
"
|
||||
>
|
||||
<label className="block font-mono text-sm text-red-1100">{value}</label>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
break
|
||||
// 4XX || 3XX responses
|
||||
case '4':
|
||||
case '3':
|
||||
return (
|
||||
<div className="flex items-center h-full">
|
||||
<div
|
||||
className="relative rounded px-2 py-1 text-center h-6 flex justify-center items-center
|
||||
bg-amber-400
|
||||
|
||||
"
|
||||
>
|
||||
<label className="block font-mono text-sm text-amber-1100 dark:text-amber-900">
|
||||
{value}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
break
|
||||
// All other responses
|
||||
default:
|
||||
return (
|
||||
<div className="flex items-center h-full">
|
||||
<div
|
||||
className="relative rounded px-2 py-1 text-center h-6 flex justify-center items-center
|
||||
bg-scale-300
|
||||
|
||||
"
|
||||
>
|
||||
<label className="block font-mono text-sm text-scale-900">{value}</label>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Response Code
|
||||
*
|
||||
* for http response codes
|
||||
*/
|
||||
|
||||
export const SeverityFormatter = ({ value }: { value: string }) => {
|
||||
if (!value) {
|
||||
return (
|
||||
<div>
|
||||
<label className="text-xs text-scale-800">No data</label>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
value = value.toLowerCase()
|
||||
|
||||
switch (value) {
|
||||
case 'error':
|
||||
return (
|
||||
<div className="flex items-center h-full gap-1">
|
||||
<div className=" p-0.5 rounded !text-red-900">
|
||||
<IconAlertCircle size={14} strokeWidth={2} />
|
||||
</div>
|
||||
<span className="!text-red-900 !block titlecase">{value}</span>
|
||||
</div>
|
||||
)
|
||||
break
|
||||
|
||||
case 'info':
|
||||
|
||||
case 'debug':
|
||||
return (
|
||||
<div className="flex items-center h-full gap-1">
|
||||
<div className=" p-0.5 rounded !text-blue-900">
|
||||
<IconAlertCircle size={14} strokeWidth={2} />
|
||||
</div>
|
||||
<span className="!text-blue-900 !block titlecase">{value}</span>
|
||||
</div>
|
||||
)
|
||||
break
|
||||
|
||||
case 'log':
|
||||
return (
|
||||
<div className="flex items-center h-full gap-1">
|
||||
<div className=" p-0.5 rounded !text-blue-900">
|
||||
<IconInfo size={14} strokeWidth={2} />
|
||||
</div>
|
||||
<span className="!text-blue-900 !block titlecase">{value}</span>
|
||||
</div>
|
||||
)
|
||||
break
|
||||
|
||||
// All other responses
|
||||
default:
|
||||
return (
|
||||
<div className="flex items-center h-full">
|
||||
<div
|
||||
className="relative rounded px-2 py-1 text-center h-6 flex justify-center items-center
|
||||
bg-scale-300
|
||||
|
||||
"
|
||||
>
|
||||
<label className="block font-mono text-sm text-scale-900">{value}</label>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Header Formatter
|
||||
*
|
||||
* for http response codes
|
||||
*/
|
||||
|
||||
export const HeaderFormmater = ({ value }: any) => {
|
||||
return <div className="text-scale-900 font-normal flex items-center text-xs h-full">{value}</div>
|
||||
}
|
||||
|
||||
/*
|
||||
* JSON Syntax Highlighter
|
||||
*
|
||||
* for http response codes
|
||||
*/
|
||||
|
||||
export function jsonSyntaxHighlight(json: string) {
|
||||
json = JSON.stringify(json, null, 2)
|
||||
json = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
|
||||
console.log('json', json)
|
||||
const newJson = json.replace(
|
||||
/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,
|
||||
|
||||
function (match) {
|
||||
var cls = 'number text-tomato-900'
|
||||
if (/^"/.test(match)) {
|
||||
if (/:$/.test(match)) {
|
||||
cls = 'key text-scale-1200'
|
||||
} else {
|
||||
cls = 'string text-brand-1100'
|
||||
}
|
||||
} else if (/true|false/.test(match)) {
|
||||
cls = 'boolean text-blue-900'
|
||||
} else if (/null/.test(match)) {
|
||||
cls = 'null text-amber-1100'
|
||||
}
|
||||
return '<span class="' + cls + '">' + match + '</span>'
|
||||
}
|
||||
)
|
||||
|
||||
const jsonWithLineWraps = newJson.split(`\n`).map((x) => {
|
||||
return `<span class="line">${x}</span>`
|
||||
})
|
||||
|
||||
return jsonWithLineWraps.join('\n')
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import 'styles/toast.scss'
|
||||
import 'styles/code.scss'
|
||||
import 'styles/monaco.scss'
|
||||
import 'styles/contextMenu.scss'
|
||||
import 'styles/react-data-grid-logs.scss'
|
||||
|
||||
import Head from 'next/head'
|
||||
import type { AppProps } from 'next/app'
|
||||
|
||||
@@ -125,7 +125,11 @@ export const LogPage: NextPage = () => {
|
||||
|
||||
return (
|
||||
<SettingsLayout title={title}>
|
||||
<div className="h-full flex flex-col flex-grow px-5 md:px-5 xl:px-16 py-8">
|
||||
<div
|
||||
className="h-full flex flex-col flex-grow px-5 md:px-5 xl:px-16 py-8
|
||||
space-y-4
|
||||
"
|
||||
>
|
||||
<PreviewFilterPanel
|
||||
isShowingEventChart={showChart}
|
||||
onToggleEventChart={() => setShowChart(!showChart)}
|
||||
@@ -153,7 +157,7 @@ export const LogPage: NextPage = () => {
|
||||
}}
|
||||
onSelectTemplate={onSelectTemplate}
|
||||
/>
|
||||
{showChart && (
|
||||
{/* {showChart && (
|
||||
<div>
|
||||
<LogEventChart
|
||||
data={!isLoading ? logData : undefined}
|
||||
@@ -162,7 +166,7 @@ export const LogPage: NextPage = () => {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
)} */}
|
||||
<div className="flex flex-col flex-grow relative">
|
||||
{isLoading && (
|
||||
<div
|
||||
@@ -175,7 +179,7 @@ export const LogPage: NextPage = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<LogTable data={logData} isCustomQuery={false} />
|
||||
<LogTable data={logData} isCustomQuery={false} queryType={type} />
|
||||
<div className="p-2">
|
||||
<Button onClick={() => loadOlder()} icon={<IconRewind />} type="default">
|
||||
Load older
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
.data-grid--simple-logs {
|
||||
.rdg-cell,
|
||||
.rdg-cell span {
|
||||
@apply text-scale-1100 font-normal #{!important};
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.rdg-cell:first-child {
|
||||
@apply pl-5;
|
||||
}
|
||||
.rdg-cell:last-child {
|
||||
@apply pr-5;
|
||||
}
|
||||
|
||||
.rdg-row {
|
||||
&.rdg-row--focussed {
|
||||
@apply border-r border-brand-900;
|
||||
border-right-width: 4px;
|
||||
}
|
||||
|
||||
&.rdg-row--focussed .rdg-cell,
|
||||
&.rdg-row--focussed .rdg-cell span {
|
||||
@apply text-scale-1200 font-semibold #{!important};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.syntax-highlight {
|
||||
counter-reset: line-numbering;
|
||||
font-family: Menlo, Monaco, monospace;
|
||||
color: var(--colors-scale10);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.syntax-highlight .line::before {
|
||||
content: counter(line-numbering);
|
||||
counter-increment: line-numbering;
|
||||
padding-right: 0px;
|
||||
display: inline-block;
|
||||
color: var(--colors-scale8);
|
||||
/* space after numbers */
|
||||
width: 2rem;
|
||||
text-align: left;
|
||||
}
|
||||
@@ -77,7 +77,7 @@ module.exports = ui({
|
||||
}),
|
||||
divideColor: (theme) => ({
|
||||
...theme('colors'),
|
||||
DEFAULT: 'var(--colors-scale3)',
|
||||
DEFAULT: 'var(--colors-scale6)',
|
||||
dark: 'var(--colors-scale2)',
|
||||
}),
|
||||
extend: {
|
||||
@@ -222,7 +222,7 @@ module.exports = ui({
|
||||
dark: 'var(--colors-scale4)', // '#1a1a1a',
|
||||
},
|
||||
'table-border': {
|
||||
light: 'var(--colors-scale4)', // gray[100],
|
||||
light: 'var(--colors-scale5)', // gray[100],
|
||||
dark: 'var(--colors-scale4)', // '#1a1a1a',
|
||||
},
|
||||
|
||||
@@ -240,11 +240,11 @@ module.exports = ui({
|
||||
dark: 'var(--colors-scale3)',
|
||||
},
|
||||
'panel-border': {
|
||||
light: 'var(--colors-scale4)',
|
||||
light: 'var(--colors-scale5)',
|
||||
dark: 'var(--colors-scale4)',
|
||||
},
|
||||
'panel-border-interior': {
|
||||
light: 'var(--colors-scale2)',
|
||||
light: 'var(--colors-scale4)',
|
||||
dark: 'var(--colors-scale4)',
|
||||
},
|
||||
'panel-border-hover': {
|
||||
|
||||
Reference in new issue
Block a user