import * as Tooltip from '@radix-ui/react-tooltip' import { PermissionAction } from '@supabase/shared-types/out/constants' import { IS_PLATFORM } from 'common' import { isEqual } from 'lodash' import { Play } from 'lucide-react' import { Key, ReactNode, useCallback, useEffect, useMemo, useRef, useState } from 'react' import DataGrid, { Column, RenderRowProps, Row } from 'react-data-grid' import toast from 'react-hot-toast' import CSVButton from 'components/ui/CSVButton' import { useCheckPermissions } from 'hooks/misc/useCheckPermissions' import { copyToClipboard } from 'lib/helpers' import { useProfile } from 'lib/profile' import { Alert, Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, IconChevronDown, IconClipboard, IconDownload, IconEye, IconEyeOff, ResizableHandle, ResizablePanel, ResizablePanelGroup, cn, } from 'ui' import AuthColumnRenderer from './LogColumnRenderers/AuthColumnRenderer' import DatabaseApiColumnRender from './LogColumnRenderers/DatabaseApiColumnRender' import DatabasePostgresColumnRender from './LogColumnRenderers/DatabasePostgresColumnRender' import DefaultPreviewColumnRenderer from './LogColumnRenderers/DefaultPreviewColumnRenderer' import FunctionsEdgeColumnRender from './LogColumnRenderers/FunctionsEdgeColumnRender' import FunctionsLogsColumnRender from './LogColumnRenderers/FunctionsLogsColumnRender' import LogSelection, { LogSelectionProps } from './LogSelection' import type { LogData, LogQueryError, QueryType } from './Logs.types' import { isDefaultLogPreviewFormat } from './Logs.utils' import DefaultErrorRenderer from './LogsErrorRenderers/DefaultErrorRenderer' import ResourcesExceededErrorRenderer from './LogsErrorRenderers/ResourcesExceededErrorRenderer' interface Props { data?: Array onHistogramToggle?: () => void isHistogramShowing?: boolean isLoading?: boolean error?: LogQueryError | null showDownload?: boolean queryType?: QueryType projectRef: string params: LogSelectionProps['params'] onRun?: () => void onSave?: () => void hasEditorValue?: boolean maxHeight?: string className?: string collectionName?: string // Used for warehouse queries warehouseError?: string emptyState?: ReactNode showHeader?: boolean showHistogramToggle?: boolean } type LogMap = { [id: string]: LogData } /** * 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, error, projectRef, params, onRun, onSave, hasEditorValue, maxHeight, className, collectionName, emptyState, showHeader = true, showHistogramToggle = true, }: Props) => { const [focusedLog, setFocusedLog] = useState(null) const firstRow: LogData | undefined = data?.[0] as LogData const { profile } = useProfile() const canCreateLogQuery = useCheckPermissions(PermissionAction.CREATE, 'user_content', { resource: { type: 'log_sql', owner_id: profile?.id }, subject: { id: profile?.id }, }) // move timestamp to the first column function getFirstRow() { if (!firstRow) return {} const { timestamp, ...rest } = firstRow || {} if (!timestamp) return firstRow return { timestamp, ...rest } } const columnNames = Object.keys(getFirstRow() || {}) const hasId = columnNames.includes('id') const hasTimestamp = columnNames.includes('timestamp') const DEFAULT_COLUMNS = columnNames.map((v: keyof LogData, idx) => { const result: Column = { key: `logs-column-${idx}`, name: v as string, resizable: true, renderCell: (props) => { const value = props.row?.[v] if (value && typeof value === 'object') { return JSON.stringify(value) } else if (value === null) { return 'NULL' } else { return String(value) } }, renderHeaderCell: (props) => { return
{v}
}, minWidth: 128, maxWidth: v === 'timestamp' ? 240 : 2400, // Without this, the column flickers on first render } return result }) let columns = DEFAULT_COLUMNS if (!queryType) { columns } else { switch (queryType) { case 'warehouse': columns = DEFAULT_COLUMNS break case 'api': columns = DatabaseApiColumnRender break case 'database': columns = DatabasePostgresColumnRender break case 'fn_edge': columns = FunctionsEdgeColumnRender break case 'functions': columns = FunctionsLogsColumnRender break case 'auth': columns = AuthColumnRenderer 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] }, [data, hasId]) useEffect(() => { if (!data) return const found = data.find((datum) => isEqual(datum, focusedLog)) if (!found) { // close selection panel if not found in dataset setFocusedLog(null) } }, [data, focusedLog, stringData]) const logDataRows = useMemo(() => { if (hasId && hasTimestamp) { return Object.values(logMap).sort((a, b) => b.timestamp - a.timestamp) } else { return dedupedData } }, [dedupedData, hasId, hasTimestamp, logMap]) const RowRenderer = useCallback<(key: Key, props: RenderRowProps) => ReactNode>( (key, props) => { return }, [] ) const copyResultsToClipboard = () => { copyToClipboard(stringData, () => { toast.success('Results copied to clipboard') }) } const downloadCsvRef = useRef(null) function downloadCSV() { downloadCsvRef.current?.click() } const LogsExplorerTableHeader = () => (
Download CSV
Copy to clipboard
{/* Using .hidden with a ref so I don't have to duplicate the code to download the CSV - Jordi */}
Download CSV
{showHistogramToggle && (
)}
{IS_PLATFORM && ( {!canCreateLogQuery && (
You need additional permissions to save your query
)}
)}
) 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 (
) } const renderNoResultAlert = () => { if (emptyState) return emptyState else return (

No results found

Try another search or adjust the filters

) } if (!data) return null return (
{!queryType && }
{ setFocusedLog(row.row as LogData) }} selectedRows={new Set([])} columns={columns} rowClass={(row: LogData) => [ 'font-mono tracking-tight', isEqual(row, focusedLog) ? '!bg-surface-300 rdg-row--focused' : ' !bg-studio hover:!bg-surface-100 cursor-pointer', ].join(' ') } rows={logDataRows} rowKeyGetter={(r) => { if (!hasId) return JSON.stringify(r) const row = r as LogData return row.id }} renderers={{ renderRow: RowRenderer, noRowsFallback: !isLoading ? (
{!error && renderNoResultAlert()} {error && renderErrorAlert()}
) : null, }} />
{focusedLog && ( setFocusedLog(null)} log={focusedLog} queryType={queryType} params={params} collectionName={collectionName} /> )}
) } export default LogTable