mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 09:55:06 +03:00
Merge branch 'supabase:master' into master
This commit is contained in:
18 files changed
+281
-199
No files matched your search
@@ -134,7 +134,7 @@ Our approach for client libraries is modular. Each sub-library is a standalone i
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td><a href="https://github.com/supabase-community/storage-go" target="_blank" rel="noopener noreferrer">storage-go</a></td>
|
||||
<td>-</td>
|
||||
<td><a href="https://github.com/supabase-community/functions-go" target="_blank" rel="noopener noreferrer">functions-go</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Java</td>
|
||||
|
||||
@@ -33,7 +33,7 @@ const LogEventChart: React.FC<Props> = ({ data, onBarClick }) => {
|
||||
chartSize="tiny"
|
||||
data={transformedData}
|
||||
attribute="count"
|
||||
label="Events"
|
||||
label="Logs / Time"
|
||||
onBarClick={(v?: { activePayload?: { payload: any }[] }) => {
|
||||
if (!v || !v?.activePayload?.[0]?.payload) return
|
||||
const unixOrIsoTimestamp = v.activePayload[0].payload.timestamp
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FC } from 'react'
|
||||
import { IconX } from 'ui'
|
||||
import { Button, Divider, IconX } from 'ui'
|
||||
|
||||
import { LogData, QueryType } from './Logs.types'
|
||||
|
||||
@@ -9,9 +9,15 @@ import FunctionInvocationSelectionRender from './LogSelectionRenderers/FunctionI
|
||||
import FunctionLogsSelectionRender from './LogSelectionRenderers/FunctionLogsSelectionRender'
|
||||
import DefaultExplorerSelectionRenderer from './LogSelectionRenderers/DefaultExplorerSelectionRenderer'
|
||||
import DefaultPreviewSelectionRenderer from './LogSelectionRenderers/DefaultPreviewSelectionRenderer'
|
||||
import { isDefaultLogPreviewFormat, LogsEndpointParams } from '.'
|
||||
import {
|
||||
isDefaultLogPreviewFormat,
|
||||
isUnixMicro,
|
||||
LogsEndpointParams,
|
||||
unixMicroToIsoTimestamp,
|
||||
} from '.'
|
||||
import useSingleLog from 'hooks/analytics/useSingleLog'
|
||||
import Connecting from 'components/ui/Loading/Loading'
|
||||
import CopyButton from 'components/ui/CopyButton'
|
||||
|
||||
export interface LogSelectionProps {
|
||||
log: LogData | null
|
||||
@@ -64,6 +70,23 @@ const LogSelection: FC<LogSelectionProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
const selectionText = () => {
|
||||
if (!fullLog) return ''
|
||||
if (queryType) {
|
||||
return `Log ID
|
||||
${fullLog.id}\n
|
||||
Log Timestamp (UTC)
|
||||
${isUnixMicro(fullLog.timestamp) ? unixMicroToIsoTimestamp(fullLog.timestamp) : fullLog.timestamp}\n
|
||||
Log Event Message
|
||||
${fullLog.event_message}\n
|
||||
Log Metadata
|
||||
${JSON.stringify(fullLog.metadata, null, 2)}
|
||||
`
|
||||
}
|
||||
|
||||
return JSON.stringify(fullLog, null, 2)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={[
|
||||
@@ -123,22 +146,24 @@ const LogSelection: FC<LogSelectionProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="
|
||||
relative
|
||||
h-px
|
||||
flex-grow
|
||||
bg-scale-300
|
||||
"
|
||||
>
|
||||
<div
|
||||
className="absolute top-6 right-6 cursor-pointer text-scale-900 transition hover:text-scale-1200"
|
||||
onClick={onClose}
|
||||
>
|
||||
<IconX size={14} strokeWidth={2} />
|
||||
<div className="relative h-px flex-grow bg-scale-300">
|
||||
<div className="pt-4 px-4 flex flex-col gap-4">
|
||||
<div className="flex flex-row justify-between items-center">
|
||||
<div className={`transition ${!isLoading ? 'opacity-100' : 'opacity-0'}`}>
|
||||
<CopyButton text={selectionText()} type="default" title="Copy log to clipboard" />
|
||||
</div>
|
||||
<Button
|
||||
type="text"
|
||||
className="cursor-pointer transition hover:text-scale-1200 h-8 w-8 px-0 py-0 flex items-center justify-center"
|
||||
onClick={onClose}
|
||||
>
|
||||
<IconX size={14} strokeWidth={2} className="text-scale-900" />
|
||||
</Button>
|
||||
</div>
|
||||
<div className="h-px w-full bg-scale-600 rounded " />
|
||||
</div>
|
||||
{isLoading && <Connecting />}
|
||||
<div className="flex flex-col space-y-6 bg-scale-300 py-8">
|
||||
<div className="flex flex-col space-y-6 bg-scale-300 py-4">
|
||||
{!isLoading && <Formatter />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+8
-19
@@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import { LOGS_TAILWIND_CLASSES } from '../Logs.constants'
|
||||
import LogsDivider from '../Logs.Divider'
|
||||
import { jsonSyntaxHighlight, ResponseCodeFormatter } from '../LogsFormatters'
|
||||
import { jsonSyntaxHighlight, ResponseCodeFormatter, SelectionDetailedRow } from '../LogsFormatters'
|
||||
|
||||
const DatabaseApiSelectionRender = ({ log }: any) => {
|
||||
const request = log?.metadata[0]?.request?.[0]
|
||||
@@ -13,27 +13,16 @@ const DatabaseApiSelectionRender = ({ log }: any) => {
|
||||
const clientInfo = request?.headers?.[0]?.x_client_info
|
||||
const referer = request?.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 whitespace-pre-wrap">{label}</span>
|
||||
<span className="text-scale-1200 text-sm col-span-8 whitespace-pre-wrap break-all">
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`${LOGS_TAILWIND_CLASSES.log_selection_x_padding} 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} />}
|
||||
<SelectionDetailedRow label="Status" value={status} valueRender={<ResponseCodeFormatter value={status} />} />
|
||||
<SelectionDetailedRow label="Method" value={method} />
|
||||
<SelectionDetailedRow label="Timestamp" value={log.timestamp} />
|
||||
<SelectionDetailedRow label="IP Address" value={ipAddress} />
|
||||
<SelectionDetailedRow label="Origin Country" value={countryOrigin} />
|
||||
{clientInfo && <SelectionDetailedRow label="Client" value={clientInfo} />}
|
||||
{referer && <SelectionDetailedRow label="Referer" value={referer} />}
|
||||
</div>
|
||||
<LogsDivider />
|
||||
<div className={`${LOGS_TAILWIND_CLASSES.log_selection_x_padding}`}>
|
||||
|
||||
+10
-14
@@ -2,21 +2,13 @@ import { Alert } from 'ui'
|
||||
import React from 'react'
|
||||
import { LOGS_TAILWIND_CLASSES } from '../Logs.constants'
|
||||
import LogsDivider from '../Logs.Divider'
|
||||
import { jsonSyntaxHighlight, SeverityFormatter } from '../LogsFormatters'
|
||||
import { jsonSyntaxHighlight, SelectionDetailedRow, 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">
|
||||
<p className="col-span-4 whitespace-pre-wrap text-sm text-scale-900">{label}</p>
|
||||
<p className="col-span-8 whitespace-pre-wrap text-base text-scale-1200">{value}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const errorSeverity = log?.metadata[0]?.parsed[0]?.error_severity
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -29,13 +21,17 @@ const DatabasePostgresSelectionRender = ({ log }: any) => {
|
||||
</div>
|
||||
<LogsDivider />
|
||||
<div className={`${LOGS_TAILWIND_CLASSES.log_selection_x_padding} space-y-2`}>
|
||||
<DetailedRow label="Severity" value={<SeverityFormatter value={log.error_severity} />} />
|
||||
<DetailedRow label="Postgres Username" value={postgresUsername} />
|
||||
<DetailedRow label="Session ID" value={sessionId} />
|
||||
<SelectionDetailedRow
|
||||
label="Severity"
|
||||
value={errorSeverity}
|
||||
valueRender={<SeverityFormatter value={errorSeverity} />}
|
||||
/>
|
||||
<SelectionDetailedRow label="Postgres Username" value={postgresUsername} />
|
||||
<SelectionDetailedRow label="Session ID" value={sessionId} />
|
||||
</div>
|
||||
{hint && (
|
||||
<div className={`mt-4 ${LOGS_TAILWIND_CLASSES.log_selection_x_padding}`}>
|
||||
<Alert variant="warning" withIcon title={log?.metadata[0]?.parsed[0]?.hint} />
|
||||
<Alert variant="warning" withIcon title={hint} />
|
||||
</div>
|
||||
)}
|
||||
<LogsDivider />
|
||||
|
||||
+2
-25
@@ -1,30 +1,7 @@
|
||||
import { LOGS_TAILWIND_CLASSES } from '../Logs.constants'
|
||||
import { jsonSyntaxHighlight } from '../LogsFormatters'
|
||||
import { jsonSyntaxHighlight, SelectionDetailedRow } from '../LogsFormatters'
|
||||
|
||||
const DefaultExplorerSelectionRenderer = ({ log }: any) => {
|
||||
const DetailedRow = ({
|
||||
label,
|
||||
value,
|
||||
code,
|
||||
}: {
|
||||
label: string
|
||||
value: string | React.ReactNode
|
||||
code?: boolean
|
||||
}) => {
|
||||
return (
|
||||
<div className="grid grid-cols-12">
|
||||
<span className="text-scale-900 text-sm col-span-4 whitespace-prep-wrap">{label}</span>
|
||||
<span
|
||||
className={`text-scale-1200 text-sm col-span-8 whitespace-prep-wrap ${
|
||||
code && 'text-xs font-mono'
|
||||
}`}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const DetailedJsonRow = ({
|
||||
label,
|
||||
value,
|
||||
@@ -63,7 +40,7 @@ const DefaultExplorerSelectionRenderer = ({ log }: any) => {
|
||||
{value && typeof value === 'object' ? (
|
||||
<DetailedJsonRow label={key} value={value} />
|
||||
) : (
|
||||
<DetailedRow label={key} value={value === null ? 'NULL ' : String(value)} />
|
||||
<SelectionDetailedRow label={key} value={value === null ? 'NULL ' : String(value)} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
+24
-49
@@ -1,55 +1,30 @@
|
||||
import { isUnixMicro, unixMicroToIsoTimestamp } from '..'
|
||||
import { isUnixMicro, PreviewLogData, unixMicroToIsoTimestamp } from '..'
|
||||
import { LOGS_TAILWIND_CLASSES } from '../Logs.constants'
|
||||
import { jsonSyntaxHighlight } from '../LogsFormatters'
|
||||
import { jsonSyntaxHighlight, SelectionDetailedRow } from '../LogsFormatters'
|
||||
|
||||
const DefaultPreviewSelectionRenderer = ({ log }: any) => {
|
||||
const DetailedRow = ({
|
||||
label,
|
||||
value,
|
||||
code,
|
||||
}: {
|
||||
label: string
|
||||
value: string | React.ReactNode
|
||||
code?: boolean
|
||||
}) => {
|
||||
return (
|
||||
<div className="grid grid-cols-12">
|
||||
<span className="text-scale-900 text-sm col-span-4 whitespace-prep-wrap">{label}</span>
|
||||
<span
|
||||
className={`text-scale-1200 text-sm col-span-8 whitespace-prep-wrap ${
|
||||
code && 'text-xs font-mono'
|
||||
}`}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${LOGS_TAILWIND_CLASSES.log_selection_x_padding} space-y-6`}>
|
||||
<div className="flex flex-col gap-3">
|
||||
<h3 className="text-scale-900 text-sm">Event Message</h3>
|
||||
<div className="text-xs text-wrap font-mono text-scale-1200 whitespace-pre-wrap overflow-x-auto">
|
||||
{log.event_message}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DetailedRow
|
||||
label="ISO Timestamp"
|
||||
value={isUnixMicro(log.timestamp) ? unixMicroToIsoTimestamp(log.timestamp) : log.timestamp}
|
||||
/>
|
||||
<div className="flex flex-col gap-3">
|
||||
<h3 className="text-scale-900 text-sm">Metadata</h3>
|
||||
<pre
|
||||
className=" className={`text-scale-1200 text-sm col-span-8 overflow-x-auto text-xs font-mono`}"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: jsonSyntaxHighlight(log.metadata),
|
||||
}}
|
||||
/>
|
||||
const DefaultPreviewSelectionRenderer = ({ log }: { log: PreviewLogData }) => (
|
||||
<div className={`${LOGS_TAILWIND_CLASSES.log_selection_x_padding} space-y-6`}>
|
||||
<div className="flex flex-col gap-3">
|
||||
<h3 className="text-scale-900 text-sm">Event Message</h3>
|
||||
<div className="text-xs text-wrap font-mono text-scale-1200 whitespace-pre-wrap overflow-x-auto">
|
||||
{log.event_message}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<SelectionDetailedRow
|
||||
label="ISO Timestamp"
|
||||
value={isUnixMicro(log.timestamp) ? unixMicroToIsoTimestamp(log.timestamp) : String(log.timestamp)}
|
||||
/>
|
||||
<div className="flex flex-col gap-3">
|
||||
<h3 className="text-scale-900 text-sm">Metadata</h3>
|
||||
<pre
|
||||
className=" className={`text-scale-1200 text-sm col-span-8 overflow-x-auto text-xs font-mono`}"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: jsonSyntaxHighlight(log.metadata!),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
export default DefaultPreviewSelectionRenderer
|
||||
+14
-31
@@ -2,7 +2,7 @@ import dayjs from 'dayjs'
|
||||
import { filterFunctionsRequestResponse } from 'lib/logs'
|
||||
import { PreviewLogData } from '..'
|
||||
import { LOGS_TAILWIND_CLASSES } from '../Logs.constants'
|
||||
import { jsonSyntaxHighlight, ResponseCodeFormatter } from '../LogsFormatters'
|
||||
import { jsonSyntaxHighlight, ResponseCodeFormatter, SelectionDetailedRow } from '../LogsFormatters'
|
||||
|
||||
const FunctionInvocationSelectionRender = ({ log }: { log: PreviewLogData }) => {
|
||||
const metadata = log.metadata?.[0]
|
||||
@@ -15,39 +15,22 @@ const FunctionInvocationSelectionRender = ({ log }: { log: PreviewLogData }) =>
|
||||
const deploymentId = metadata.deployment_id
|
||||
const timestamp = dayjs(log.timestamp / 1000)
|
||||
|
||||
const DetailedRow = ({
|
||||
label,
|
||||
value,
|
||||
code,
|
||||
}: {
|
||||
label: string
|
||||
value: string | React.ReactNode
|
||||
code?: boolean
|
||||
}) => {
|
||||
return (
|
||||
<div className="grid grid-cols-12">
|
||||
<span className="text-scale-900 text-sm col-span-4 whitespace-pre-wrap">{label}</span>
|
||||
<span
|
||||
className={`text-scale-1200 text-sm col-span-8 whitespace-pre-wrap ${
|
||||
code && 'text-xs font-mono'
|
||||
}`}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`${LOGS_TAILWIND_CLASSES.log_selection_x_padding} space-y-2`}>
|
||||
<DetailedRow label="Status" value={<ResponseCodeFormatter value={status} />} />
|
||||
<DetailedRow label="Method" value={method} />
|
||||
<DetailedRow label="Timestamp" value={dayjs(timestamp).format('DD MMM, YYYY HH:mm')} />
|
||||
<DetailedRow label="Execution Time" value={`${executionTimeMs}ms`} />
|
||||
<DetailedRow label="Deployment ID" value={deploymentId} />
|
||||
<DetailedRow label="Log ID" value={log.id} />
|
||||
<DetailedRow label="Request Path" value={requestUrl.pathname + requestUrl.search} />
|
||||
<SelectionDetailedRow label="Status" value={status} valueRender={<ResponseCodeFormatter value={status} />} />
|
||||
<SelectionDetailedRow label="Method" value={method} />
|
||||
<SelectionDetailedRow
|
||||
label="Timestamp"
|
||||
value={dayjs(timestamp).format('DD MMM, YYYY HH:mm')}
|
||||
/>
|
||||
<SelectionDetailedRow label="Execution Time" value={`${executionTimeMs}ms`} />
|
||||
<SelectionDetailedRow label="Deployment ID" value={deploymentId} />
|
||||
<SelectionDetailedRow label="Log ID" value={log.id} />
|
||||
<SelectionDetailedRow
|
||||
label="Request Path"
|
||||
value={requestUrl.pathname + requestUrl.search}
|
||||
/>
|
||||
</div>
|
||||
<div className={`${LOGS_TAILWIND_CLASSES.log_selection_x_padding}`}>
|
||||
<h3 className="text-lg text-scale-1200 mb-4">Request body</h3>
|
||||
|
||||
+10
-29
@@ -1,34 +1,11 @@
|
||||
import dayjs from 'dayjs'
|
||||
import { LOGS_TAILWIND_CLASSES } from '../Logs.constants'
|
||||
import { jsonSyntaxHighlight, SeverityFormatter } from '../LogsFormatters'
|
||||
import { jsonSyntaxHighlight, SelectionDetailedRow, SeverityFormatter } from '../LogsFormatters'
|
||||
|
||||
const FunctionLogsSelectionRender = ({ log }: any) => {
|
||||
const timestamp = dayjs(log.timestamp / 1000)
|
||||
const metadata = log.metadata[0]
|
||||
|
||||
const DetailedRow = ({
|
||||
label,
|
||||
value,
|
||||
code,
|
||||
}: {
|
||||
label: string
|
||||
value: string | React.ReactNode
|
||||
code?: boolean
|
||||
}) => {
|
||||
return (
|
||||
<div className="grid grid-cols-12">
|
||||
<span className="text-scale-900 text-sm col-span-4 whitespace-pre-wrap">{label}</span>
|
||||
<span
|
||||
className={`text-scale-1200 text-sm col-span-8 whitespace-pre-wrap ${
|
||||
code && 'text-xs font-mono'
|
||||
}`}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={`${LOGS_TAILWIND_CLASSES.log_selection_x_padding}`}>
|
||||
@@ -39,11 +16,15 @@ const FunctionLogsSelectionRender = ({ log }: any) => {
|
||||
</div>
|
||||
<div className="h-px w-full bg-panel-border-interior-light dark:bg-panel-border-interior-dark"></div>
|
||||
<div className={`${LOGS_TAILWIND_CLASSES.log_selection_x_padding} space-y-2`}>
|
||||
<DetailedRow label="Severity" value={<SeverityFormatter value={metadata.level} />} />
|
||||
<DetailedRow label="Deployment version" value={metadata.version} />
|
||||
<DetailedRow label="Timestamp" value={timestamp.format('DD MMM, YYYY HH:mm')} />
|
||||
<DetailedRow label="Execution ID" value={metadata.execution_id} />
|
||||
<DetailedRow label="Deployment ID" value={metadata.deployment_id} />
|
||||
<SelectionDetailedRow
|
||||
label="Severity"
|
||||
value={metadata.level}
|
||||
valueRender={<SeverityFormatter value={metadata.level} />}
|
||||
/>
|
||||
<SelectionDetailedRow label="Deployment version" value={metadata.version} />
|
||||
<SelectionDetailedRow label="Timestamp" value={timestamp.format('DD MMM, YYYY HH:mm')} />
|
||||
<SelectionDetailedRow label="Execution ID" value={metadata.execution_id} />
|
||||
<SelectionDetailedRow label="Deployment ID" value={metadata.deployment_id} />
|
||||
</div>
|
||||
<div className={`${LOGS_TAILWIND_CLASSES.log_selection_x_padding}`}>
|
||||
<h3 className="text-lg text-scale-1200 mb-4">Metadata</h3>
|
||||
|
||||
@@ -215,7 +215,9 @@ export const SQL_FILTER_TEMPLATES: any = {
|
||||
|
||||
'method.get': `request.method = 'GET'`,
|
||||
'method.post': `request.method = 'POST'`,
|
||||
'method.del': `request.method = 'DEL'`,
|
||||
'method.put': `request.method = 'PUT'`,
|
||||
'method.patch': `request.method = 'PATCH'`,
|
||||
'method.delete': `request.method = 'DELETE'`,
|
||||
'method.options': `request.method = 'OPTIONS'`,
|
||||
},
|
||||
function_edge_logs: {
|
||||
@@ -373,11 +375,26 @@ export const FILTER_OPTIONS: FilterTableSet = {
|
||||
label: 'OPTIONS',
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
key: 'put',
|
||||
label: 'PUT',
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
key: 'post',
|
||||
label: 'POST',
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
key: 'patch',
|
||||
label: 'PATCH',
|
||||
description: '',
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
label: 'DELETE',
|
||||
description: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -8,11 +8,41 @@ import { IconAlertCircle, IconInfo } from 'ui'
|
||||
import dayjs from 'dayjs'
|
||||
import React from 'react'
|
||||
import { isUnixMicro, unixMicroToIsoTimestamp } from '.'
|
||||
import CopyButton from 'components/ui/CopyButton'
|
||||
|
||||
export const RowLayout: React.FC = ({ children }) => (
|
||||
<div className="flex h-full w-full items-center gap-4">{children}</div>
|
||||
)
|
||||
|
||||
export const SelectionDetailedRow = ({
|
||||
label,
|
||||
value,
|
||||
valueRender,
|
||||
}: {
|
||||
label: string
|
||||
value: string
|
||||
valueRender?: React.ReactNode
|
||||
}) => {
|
||||
return (
|
||||
<div className="grid grid-cols-12 group">
|
||||
<span className="text-scale-900 text-sm col-span-4 whitespace-pre-wrap">{label}</span>
|
||||
<span className="text-scale-1200 text-sm col-span-6 whitespace-pre-wrap break-all">
|
||||
{valueRender ?? value}
|
||||
</span>
|
||||
<CopyButton
|
||||
bounceIconOnCopy
|
||||
text={value}
|
||||
className="group-hover:opacity-100 opacity-0 my-auto transition col-span-2 h-4 w-4 px-0 py-0"
|
||||
type="text"
|
||||
title="Copy to clipboard"
|
||||
>
|
||||
{''}
|
||||
</CopyButton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// used for column renderers
|
||||
export const TextFormatter: React.FC<{ className?: string; value: string }> = ({
|
||||
value,
|
||||
className,
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import Split from 'react-split'
|
||||
import Editor from '@monaco-editor/react'
|
||||
import DataGrid from '@supabase/react-data-grid'
|
||||
import classNames from 'classnames'
|
||||
import { CSVLink } from 'react-csv'
|
||||
import { debounce } from 'lodash'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { Button, Dropdown, IconChevronDown } from 'ui'
|
||||
import { Button, Dropdown, IconCheck, IconChevronDown, IconClipboard } from 'ui'
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
import { useKeyboardShortcuts, useStore, useWindowDimensions, checkPermissions } from 'hooks'
|
||||
@@ -306,21 +307,38 @@ const UtilityTabResults = observer(() => {
|
||||
|
||||
const Results = ({ results }) => {
|
||||
const [cellPosition, setCellPosition] = useState(undefined)
|
||||
const [copiedCell, setCopiedCell] = useState(undefined)
|
||||
|
||||
useKeyboardShortcuts(
|
||||
{
|
||||
'Command+c': (event) => {
|
||||
event.stopPropagation()
|
||||
onCopyCell()
|
||||
onCopySelectedCell()
|
||||
},
|
||||
'Control+c': (event) => {
|
||||
event.stopPropagation()
|
||||
onCopyCell()
|
||||
onCopySelectedCell()
|
||||
},
|
||||
},
|
||||
['INPUT', 'TEXTAREA']
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
let timeoutId = 0
|
||||
|
||||
if (copiedCell) {
|
||||
timeoutId = setTimeout(() => {
|
||||
setCopiedCell(undefined)
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
return () => {
|
||||
// we need to clear previous timeout to prevent checkmark flickering
|
||||
// when clicking `Copy` button multiple times in a short time
|
||||
timeoutId && clearTimeout(timeoutId)
|
||||
}
|
||||
}, [copiedCell])
|
||||
|
||||
if (results?.error) {
|
||||
return (
|
||||
<div className="bg-table-header-light dark:bg-table-header-dark">
|
||||
@@ -336,8 +354,33 @@ const Results = ({ results }) => {
|
||||
)
|
||||
}
|
||||
|
||||
const formatter = (column, row) => {
|
||||
return <span className="font-mono text-xs">{JSON.stringify(row[column])}</span>
|
||||
const handleCopyClick = (column, row, rowIndex) => {
|
||||
copyToClipboard(formatClipboardValue(row[column]), () => {
|
||||
setCopiedCell(`${column},${rowIndex}`)
|
||||
})
|
||||
}
|
||||
|
||||
const formatter = (column, row, rowIndex) => {
|
||||
const isCopied = copiedCell === `${column},${rowIndex}`
|
||||
|
||||
return (
|
||||
<div className="group sb-grid-select-cell__formatter overflow-hidden">
|
||||
<span className="font-mono text-xs truncate">{JSON.stringify(row[column])}</span>
|
||||
|
||||
{row[column] && (
|
||||
<Button
|
||||
type="outline"
|
||||
icon={isCopied ? <IconCheck size="tiny" /> : <IconClipboard size="tiny" />}
|
||||
onClick={() => handleCopyClick(column, row, rowIndex)}
|
||||
className={classNames(
|
||||
'mx-1 group-hover:block group-hover:opacity-50 hover:opacity-100',
|
||||
!isCopied && 'hidden'
|
||||
)}
|
||||
title="Copy"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const columnRender = (name) => {
|
||||
return <div className="flex h-full items-center justify-center font-mono">{name}</div>
|
||||
@@ -345,7 +388,7 @@ const Results = ({ results }) => {
|
||||
const columns = Object.keys(results[0]).map((key) => ({
|
||||
key,
|
||||
name: key,
|
||||
formatter: ({ row }) => formatter(key, row),
|
||||
formatter: ({ row }) => formatter(key, row, results.indexOf(row)),
|
||||
headerRenderer: () => columnRender(key),
|
||||
resizable: true,
|
||||
width: 120,
|
||||
@@ -355,7 +398,7 @@ const Results = ({ results }) => {
|
||||
setCellPosition(position)
|
||||
}
|
||||
|
||||
function onCopyCell() {
|
||||
function onCopySelectedCell() {
|
||||
if (columns && cellPosition) {
|
||||
const { idx, rowIdx } = cellPosition
|
||||
const column = columns[idx]
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Button, ButtonProps } from 'ui'
|
||||
import { copyToClipboard } from 'lib/helpers'
|
||||
import { IconClipboard } from 'ui'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export interface CopyButtonProps extends ButtonProps {
|
||||
text: string
|
||||
// used for text-less feedback
|
||||
bounceIconOnCopy?: boolean
|
||||
}
|
||||
const CopyButton: React.FC<CopyButtonProps> = ({
|
||||
text,
|
||||
children,
|
||||
onClick,
|
||||
bounceIconOnCopy,
|
||||
...props
|
||||
}) => {
|
||||
const [showCopied, setShowCopied] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!showCopied) return
|
||||
const timer = setTimeout(() => setShowCopied(false), 2000)
|
||||
return () => clearTimeout(timer)
|
||||
}, [showCopied])
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
setShowCopied(true)
|
||||
copyToClipboard(text)
|
||||
onClick?.(e)
|
||||
}}
|
||||
icon={
|
||||
<IconClipboard
|
||||
size="tiny"
|
||||
className={[showCopied && bounceIconOnCopy ? 'animate-bounce' : '', 'transition'].join(
|
||||
' '
|
||||
)}
|
||||
/>
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
{children ?? (showCopied ? 'Copied' : 'Copy')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
export default CopyButton
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
LogsTableName,
|
||||
PREVIEWER_DATEPICKER_HELPERS,
|
||||
} from 'components/interfaces/Settings/Logs'
|
||||
import { Dispatch, SetStateAction, useEffect, useState } from 'react'
|
||||
import { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react'
|
||||
import useSWR from 'swr'
|
||||
import useSWRInfinite, { SWRInfiniteKeyLoader } from 'swr/infinite'
|
||||
import { API_URL } from 'lib/constants'
|
||||
@@ -47,18 +47,21 @@ function useLogsPreview(
|
||||
const [latestRefresh, setLatestRefresh] = useState<string>(new Date().toISOString())
|
||||
|
||||
const [filters, setFilters] = useState<Filters>({ ...filterOverride })
|
||||
const isFirstRender = useRef<boolean>(true)
|
||||
|
||||
const [params, setParams] = useState<LogsEndpointParams>({
|
||||
project: projectRef,
|
||||
sql: '',
|
||||
sql: genDefaultQuery(table, filters),
|
||||
iso_timestamp_start: defaultHelper.calcFrom(),
|
||||
iso_timestamp_end: defaultHelper.calcTo(),
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (filters !== {}) {
|
||||
refresh()
|
||||
if (isFirstRender.current) {
|
||||
isFirstRender.current = false
|
||||
return
|
||||
}
|
||||
refresh()
|
||||
}, [JSON.stringify(filters)])
|
||||
|
||||
// handle url generation for log pagination
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { screen } from '@testing-library/dom'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import CopyButton from 'components/ui/CopyButton'
|
||||
import { render } from 'tests/helpers'
|
||||
|
||||
test('shows copied text', async () => {
|
||||
const callback = jest.fn()
|
||||
render(<CopyButton text="some text" onClick={callback} />)
|
||||
userEvent.click(await screen.findByText('Copy'))
|
||||
await screen.findByText('Copied')
|
||||
expect(callback).toBeCalled()
|
||||
})
|
||||
@@ -34,5 +34,5 @@ test('renders chart', async () => {
|
||||
// )
|
||||
// userEvent.click(paths[0])
|
||||
// expect(mock).toBeCalledTimes(1)
|
||||
await screen.findByText('Events')
|
||||
await screen.findByText(/Logs \/ Time/)
|
||||
})
|
||||
@@ -23,6 +23,10 @@ test('can display log data', async () => {
|
||||
userEvent.click(row)
|
||||
await screen.findByText(/my_key/)
|
||||
await screen.findByText(/something_value/)
|
||||
|
||||
// render copy button
|
||||
userEvent.click(await screen.findByText(/Copy/))
|
||||
await screen.findByText(/Copied/)
|
||||
})
|
||||
|
||||
test('dedupes log lines with exact id', async () => {
|
||||
|
||||
@@ -309,7 +309,7 @@ test('log event chart hide', async () => {
|
||||
return { result: [] }
|
||||
})
|
||||
render(<LogsPreviewer projectRef="123" tableName={LogsTableName.EDGE} />)
|
||||
await screen.findByText('Events')
|
||||
await screen.findByText(/Logs \/ Time/)
|
||||
const toggle = await screen.findByText(/Chart/)
|
||||
userEvent.click(toggle)
|
||||
await expect(screen.findByText('Events')).rejects.toThrow()
|
||||
|
||||
Reference in new issue
Block a user