-
-
Event message
-
+
+ {/*
*/}
+
+ {/*
+ Request
+
+
+
+ Request
+
+
*/}
-
-
Metadata
-
+ >
+ ) : (
+
+
+
Select an Event
+
+ Select an Event to view the code snippet (pretty view) or complete JSON payload (raw
+ view).
+
-
+ )}
)
}
diff --git a/studio/components/interfaces/Settings/Logs/LogSelectionRendererHandler.tsx b/studio/components/interfaces/Settings/Logs/LogSelectionRendererHandler.tsx
new file mode 100644
index 00000000000..6efe69f9a2e
--- /dev/null
+++ b/studio/components/interfaces/Settings/Logs/LogSelectionRendererHandler.tsx
@@ -0,0 +1,13 @@
+import { QueryType } from './Logs.types'
+
+const SelectionRenderHandler = ({ type }: { type: QueryType }) => {
+ switch (type) {
+ case 'api':
+ break
+
+ default:
+ break
+ }
+}
+
+export default SelectionRenderHandler
diff --git a/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/DatabaseApiSelectionRender.tsx b/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/DatabaseApiSelectionRender.tsx
new file mode 100644
index 00000000000..962ab2afa6f
--- /dev/null
+++ b/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/DatabaseApiSelectionRender.tsx
@@ -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 (
+
+ {label}
+ {value}
+
+ )
+ }
+
+ return (
+ <>
+
+ } />
+
+
+
+
+ {clientInfo && }
+ {referer && }
+
+
+
+
+ Response{method ? ` ${method}` : null} body
+
+
+
+
+
+ >
+ )
+}
+
+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
diff --git a/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/DatabasePostgresSelectionRender.tsx b/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/DatabasePostgresSelectionRender.tsx
new file mode 100644
index 00000000000..ee7bef6c0f0
--- /dev/null
+++ b/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/DatabasePostgresSelectionRender.tsx
@@ -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 (
+
+ {label}
+ {value}
+
+ )
+ }
+
+ return (
+ <>
+
+
+
+ } />
+
+
+
+
+ {hint && (
+
+ )}
+
+
+
+ >
+ )
+}
+
+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
diff --git a/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/FunctionInvocationSelectionRender.tsx b/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/FunctionInvocationSelectionRender.tsx
new file mode 100644
index 00000000000..ab964221126
--- /dev/null
+++ b/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/FunctionInvocationSelectionRender.tsx
@@ -0,0 +1,26 @@
+import { jsonSyntaxHighlight } from '../LogsFormatters'
+
+const FunctionInvocationSelectionRender = ({ log }: any) => {
+ return (
+ <>
+
+ Request
+
+
+
+ Response
+
+
+ >
+ )
+}
+
+export default FunctionInvocationSelectionRender
diff --git a/studio/components/interfaces/Settings/Logs/LogTable.tsx b/studio/components/interfaces/Settings/Logs/LogTable.tsx
index 495cf253833..156fa69394a 100644
--- a/studio/components/interfaces/Settings/Logs/LogTable.tsx
+++ b/studio/components/interfaces/Settings/Logs/LogTable.tsx
@@ -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
+ 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(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 {v}
- },
- 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 {v}
+ },
+ formatter: ({ row }: any) => {
+ let value = row[v]
+ if (hasLogDataFormat && v === 'timestamp') {
+ value = dayjs(Number(row['timestamp']) / 1000).toISOString()
+ }
+ return (
+
+ {value}
+
+ )
+ },
+ })
+ )
+
+ let columns
+
+ console.log('QUERY TYPE', queryType)
+
+ switch (queryType) {
+ case 'api':
+ if (isCustomQuery) {
+ columns = DEFAULT_COLUMNS
+ break
}
- return (
-
- {value}
-
- )
- },
- }))
-
- 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: () => (
+
+
+
+ ),
+ name: 'timestamp',
+ formatter: (data: any) => (
+
+ {dayjs(data?.row?.timestamp / 1000).format('DD MMM')}
+
+ {dayjs(data?.row?.timestamp / 1000).format('HH:mm:ss')}
+
+ {/* {data?.row?.timestamp} */}
+
+ ),
+ width: 128,
+ },
+ {
+ key: 'status_code',
+ headerRenderer: () => ,
+ name: 'status_code',
+ formatter: (data: any) => (
+
+ ),
+
+ width: 0,
+ resizable: true,
+ },
+ {
+ key: 'method',
+ headerRenderer: () => ,
+ width: 0,
+ resizable: true,
+ },
+ {
+ key: 'id',
+ headerRenderer: () => ,
+ name: 'id',
+ resizable: true,
+ },
+ ]
+ break
+ case 'functions':
+ if (isCustomQuery) {
+ columns = DEFAULT_COLUMNS
+ break
+ }
+ columns = [
+ {
+ key: 'timestamp',
+ headerRenderer: () => (
+
+
+
+ ),
+ name: 'timestamp',
+ formatter: (data: any) => (
+
+
+ {dayjs(data?.row?.timestamp / 1000).format('DD MMM')}
+
+
+ {dayjs(data?.row?.timestamp / 1000).format('HH:mm:ss')}
+
+ {/* {data?.row?.timestamp} */}
+
+ ),
+ width: 128,
+ resizable: true,
+ },
+ {
+ key: 'level',
+ headerRenderer: () => ,
+ name: 'level',
+ formatter: (data: any) => ,
+ width: 24,
+ resizable: true,
+ },
+ {
+ key: 'event_message',
+ headerRenderer: () => ,
+ 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 (
-
- {
- if (!hasLogDataFormat) return
- setFocusedLog(data[rowIdx] as LogData)
- }}
- noRowsFallback={
-
-
- No data returned from query
-
+ <>
+ {isCustomQuery && (
+
+
+ {data && data.length ? (
+ <>
+ Query results
+ {data && data.length}
+ >
+ ) : (
+ Results will be shown below
+ )}
+
+
+ }>
+ Histogram
+
+ }>
+ Download
+
- }
- 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 && (
-
- setFocusedLog(null)} log={focusedLog} />
)}
-
+
+ {
+ if (!hasLogDataFormat) return
+ setFocusedLog(data[rowIdx] as LogData)
+ }}
+ noRowsFallback={
+
+
+ No data returned from query
+
+
+ }
+ 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 && ( */}
+
+ setFocusedLog(null)}
+ log={focusedLog}
+ queryType={queryType}
+ />
+
+ {/* ))} */}
+
+ >
)
}
export default LogTable
diff --git a/studio/components/interfaces/Settings/Logs/Logs.constants.ts b/studio/components/interfaces/Settings/Logs/Logs.constants.ts
index f1aafe8c6cf..df1808e4d45 100644
--- a/studio/components/interfaces/Settings/Logs/Logs.constants.ts
+++ b/studio/components/interfaces/Settings/Logs/Logs.constants.ts
@@ -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]
diff --git a/studio/components/interfaces/Settings/Logs/Logs.types.ts b/studio/components/interfaces/Settings/Logs/Logs.types.ts
index 06b868a0ff1..0a3a2d5617c 100644
--- a/studio/components/interfaces/Settings/Logs/Logs.types.ts
+++ b/studio/components/interfaces/Settings/Logs/Logs.types.ts
@@ -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
= {
export type Count = LFResponse
export type Logs = LFResponse
+
+export type QueryType = 'api' | 'database' | 'functions' | 'fn_edge' | 'sql'
+
+export type Mode = 'simple' | 'custom'
+
+export type Table = 'edge_logs' | 'postgres_logs'
diff --git a/studio/components/interfaces/Settings/Logs/LogsFormatters.tsx b/studio/components/interfaces/Settings/Logs/LogsFormatters.tsx
new file mode 100644
index 00000000000..d5379592c71
--- /dev/null
+++ b/studio/components/interfaces/Settings/Logs/LogsFormatters.tsx
@@ -0,0 +1,207 @@
+/*
+ * Response Code
+ *
+ * for http response codes
+ */
+
+import { IconAlertCircle, IconInfo } from '@supabase/ui'
+
+export const ResponseCodeFormatter = ({ value }: any) => {
+ if (!value) {
+ return (
+
+
+
+ )
+ }
+
+ const split = value.toString().split('')[0]
+
+ // console.log(split)
+
+ switch (split) {
+ // 2XX || 1XX responses
+ case '1':
+ case '2':
+ return (
+
+ )
+ break
+ // 5XX responses
+ case '5':
+ return (
+
+ )
+ break
+ // 4XX || 3XX responses
+ case '4':
+ case '3':
+ return (
+
+ )
+ break
+ // All other responses
+ default:
+ return (
+
+ )
+ break
+ }
+}
+
+/*
+ * Response Code
+ *
+ * for http response codes
+ */
+
+export const SeverityFormatter = ({ value }: { value: string }) => {
+ if (!value) {
+ return (
+
+
+
+ )
+ }
+
+ value = value.toLowerCase()
+
+ switch (value) {
+ case 'error':
+ return (
+
+ )
+ break
+
+ case 'info':
+
+ case 'debug':
+ return (
+
+ )
+ break
+
+ case 'log':
+ return (
+
+ )
+ break
+
+ // All other responses
+ default:
+ return (
+
+ )
+ break
+ }
+}
+
+/*
+ * Header Formatter
+ *
+ * for http response codes
+ */
+
+export const HeaderFormmater = ({ value }: any) => {
+ return {value}
+}
+
+/*
+ * JSON Syntax Highlighter
+ *
+ * for http response codes
+ */
+
+export function jsonSyntaxHighlight(json: string) {
+ json = JSON.stringify(json, null, 2)
+ json = json.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 '' + match + ''
+ }
+ )
+
+ const jsonWithLineWraps = newJson.split(`\n`).map((x) => {
+ return `${x}`
+ })
+
+ return jsonWithLineWraps.join('\n')
+}
diff --git a/studio/pages/_app.tsx b/studio/pages/_app.tsx
index 44600af84cc..c874287d0ca 100644
--- a/studio/pages/_app.tsx
+++ b/studio/pages/_app.tsx
@@ -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'
diff --git a/studio/pages/project/[ref]/settings/logs/[type].tsx b/studio/pages/project/[ref]/settings/logs/[type].tsx
index b1bd96e9d76..e8e9fdedc18 100644
--- a/studio/pages/project/[ref]/settings/logs/[type].tsx
+++ b/studio/pages/project/[ref]/settings/logs/[type].tsx
@@ -125,7 +125,11 @@ export const LogPage: NextPage = () => {
return (
-
+
setShowChart(!showChart)}
@@ -153,7 +157,7 @@ export const LogPage: NextPage = () => {
}}
onSelectTemplate={onSelectTemplate}
/>
- {showChart && (
+ {/* {showChart && (
{
}}
/>
- )}
+ )} */}
{isLoading && (
{
)}
-
+