diff --git a/README.md b/README.md index 4a667be65dc..711b0cf3b2c 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,7 @@ Our approach for client libraries is modular. Each sub-library is a standalone i - - storage-go - - + functions-go Java diff --git a/studio/components/interfaces/Settings/Logs/LogEventChart.tsx b/studio/components/interfaces/Settings/Logs/LogEventChart.tsx index ac4e51cca10..cc81a902c95 100644 --- a/studio/components/interfaces/Settings/Logs/LogEventChart.tsx +++ b/studio/components/interfaces/Settings/Logs/LogEventChart.tsx @@ -33,7 +33,7 @@ const LogEventChart: React.FC = ({ 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 diff --git a/studio/components/interfaces/Settings/Logs/LogSelection.tsx b/studio/components/interfaces/Settings/Logs/LogSelection.tsx index 01cc5473042..2ecb104e789 100644 --- a/studio/components/interfaces/Settings/Logs/LogSelection.tsx +++ b/studio/components/interfaces/Settings/Logs/LogSelection.tsx @@ -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 = ({ } } + 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 (
= ({
-
-
- +
+
+
+
+ +
+ +
+
{isLoading && } -
+
{!isLoading && }
diff --git a/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/DatabaseApiSelectionRender.tsx b/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/DatabaseApiSelectionRender.tsx index 16402c0e2e1..1c786f068ae 100644 --- a/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/DatabaseApiSelectionRender.tsx +++ b/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/DatabaseApiSelectionRender.tsx @@ -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 ( -
- {label} - - {value} - -
- ) - } - return ( <>
- } /> - - - - - {clientInfo && } - {referer && } + } /> + + + + + {clientInfo && } + {referer && }
diff --git a/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/DatabasePostgresSelectionRender.tsx b/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/DatabasePostgresSelectionRender.tsx index 1f341742ccc..c583f131055 100644 --- a/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/DatabasePostgresSelectionRender.tsx +++ b/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/DatabasePostgresSelectionRender.tsx @@ -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 ( -
-

{label}

-

{value}

-
- ) - } + const errorSeverity = log?.metadata[0]?.parsed[0]?.error_severity return ( <> @@ -29,13 +21,17 @@ const DatabasePostgresSelectionRender = ({ log }: any) => {
- } /> - - + } + /> + +
{hint && (
- +
)} diff --git a/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/DefaultExplorerSelectionRenderer.tsx b/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/DefaultExplorerSelectionRenderer.tsx index 479c27ceed6..a4be3324635 100644 --- a/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/DefaultExplorerSelectionRenderer.tsx +++ b/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/DefaultExplorerSelectionRenderer.tsx @@ -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 ( -
- {label} - - {value} - -
- ) - } - const DetailedJsonRow = ({ label, value, @@ -63,7 +40,7 @@ const DefaultExplorerSelectionRenderer = ({ log }: any) => { {value && typeof value === 'object' ? ( ) : ( - + )}
) diff --git a/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/DefaultPreviewSelectionRenderer.tsx b/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/DefaultPreviewSelectionRenderer.tsx index 35d8510e98e..4f2bfd73b5a 100644 --- a/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/DefaultPreviewSelectionRenderer.tsx +++ b/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/DefaultPreviewSelectionRenderer.tsx @@ -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 ( -
- {label} - - {value} - -
- ) - } - - return ( -
-
-

Event Message

-
- {log.event_message} -
-
- - -
-

Metadata

-
+const DefaultPreviewSelectionRenderer = ({ log }: { log: PreviewLogData }) => (
+  
+
+

Event Message

+
+ {log.event_message}
- ) -} + + +
+

Metadata

+
+    
+
+) export default DefaultPreviewSelectionRenderer diff --git a/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/FunctionInvocationSelectionRender.tsx b/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/FunctionInvocationSelectionRender.tsx index 51cfe6bb247..cb772f6964f 100644 --- a/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/FunctionInvocationSelectionRender.tsx +++ b/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/FunctionInvocationSelectionRender.tsx @@ -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 ( -
- {label} - - {value} - -
- ) - } - return ( <>
- } /> - - - - - - + } /> + + + + + +

Request body

diff --git a/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/FunctionLogsSelectionRender.tsx b/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/FunctionLogsSelectionRender.tsx index 9d371f162d7..fb5a925767b 100644 --- a/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/FunctionLogsSelectionRender.tsx +++ b/studio/components/interfaces/Settings/Logs/LogSelectionRenderers/FunctionLogsSelectionRender.tsx @@ -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 ( -
- {label} - - {value} - -
- ) - } - return ( <>
@@ -39,11 +16,15 @@ const FunctionLogsSelectionRender = ({ log }: any) => {
- } /> - - - - + } + /> + + + +

Metadata

diff --git a/studio/components/interfaces/Settings/Logs/Logs.constants.ts b/studio/components/interfaces/Settings/Logs/Logs.constants.ts index 85dc9ba6f89..6c50b7cd2d2 100644 --- a/studio/components/interfaces/Settings/Logs/Logs.constants.ts +++ b/studio/components/interfaces/Settings/Logs/Logs.constants.ts @@ -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: '', + }, ], }, }, diff --git a/studio/components/interfaces/Settings/Logs/LogsFormatters.tsx b/studio/components/interfaces/Settings/Logs/LogsFormatters.tsx index 82156ba5284..87ed2f6f855 100644 --- a/studio/components/interfaces/Settings/Logs/LogsFormatters.tsx +++ b/studio/components/interfaces/Settings/Logs/LogsFormatters.tsx @@ -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 }) => (
{children}
) +export const SelectionDetailedRow = ({ + label, + value, + valueRender, +}: { + label: string + value: string + valueRender?: React.ReactNode +}) => { + return ( +
+ {label} + + {valueRender ?? value} + + + {''} + +
+ ) +} + +// used for column renderers export const TextFormatter: React.FC<{ className?: string; value: string }> = ({ value, className, diff --git a/studio/components/to-be-cleaned/SqlEditor/TabSqlQuery.js b/studio/components/to-be-cleaned/SqlEditor/TabSqlQuery.js index dd0cdba43d2..28888b61c59 100644 --- a/studio/components/to-be-cleaned/SqlEditor/TabSqlQuery.js +++ b/studio/components/to-be-cleaned/SqlEditor/TabSqlQuery.js @@ -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 (
@@ -336,8 +354,33 @@ const Results = ({ results }) => { ) } - const formatter = (column, row) => { - return {JSON.stringify(row[column])} + const handleCopyClick = (column, row, rowIndex) => { + copyToClipboard(formatClipboardValue(row[column]), () => { + setCopiedCell(`${column},${rowIndex}`) + }) + } + + const formatter = (column, row, rowIndex) => { + const isCopied = copiedCell === `${column},${rowIndex}` + + return ( +
+ {JSON.stringify(row[column])} + + {row[column] && ( +
+ ) } const columnRender = (name) => { return
{name}
@@ -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] diff --git a/studio/components/ui/CopyButton.tsx b/studio/components/ui/CopyButton.tsx new file mode 100644 index 00000000000..690f3399d7e --- /dev/null +++ b/studio/components/ui/CopyButton.tsx @@ -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 = ({ + 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 ( + + ) +} +export default CopyButton diff --git a/studio/hooks/analytics/useLogsPreview.tsx b/studio/hooks/analytics/useLogsPreview.tsx index 6927a579c91..278a625cf5e 100644 --- a/studio/hooks/analytics/useLogsPreview.tsx +++ b/studio/hooks/analytics/useLogsPreview.tsx @@ -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(new Date().toISOString()) const [filters, setFilters] = useState({ ...filterOverride }) + const isFirstRender = useRef(true) const [params, setParams] = useState({ 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 diff --git a/studio/tests/components/CopyButton.test.tsx b/studio/tests/components/CopyButton.test.tsx new file mode 100644 index 00000000000..d8db3fc667e --- /dev/null +++ b/studio/tests/components/CopyButton.test.tsx @@ -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() + userEvent.click(await screen.findByText('Copy')) + await screen.findByText('Copied') + expect(callback).toBeCalled() +}) diff --git a/studio/tests/pages/projects/LogEventChart.test.js b/studio/tests/pages/projects/LogEventChart.test.js index 58f3862ada7..b9ea349a3ee 100644 --- a/studio/tests/pages/projects/LogEventChart.test.js +++ b/studio/tests/pages/projects/LogEventChart.test.js @@ -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/) }) diff --git a/studio/tests/pages/projects/LogTable.test.js b/studio/tests/pages/projects/LogTable.test.js index 804bfdbe685..94624e0b891 100644 --- a/studio/tests/pages/projects/LogTable.test.js +++ b/studio/tests/pages/projects/LogTable.test.js @@ -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 () => { diff --git a/studio/tests/pages/projects/LogsPreviewer.test.js b/studio/tests/pages/projects/LogsPreviewer.test.js index 058abca0a6c..eae71bf3abf 100644 --- a/studio/tests/pages/projects/LogsPreviewer.test.js +++ b/studio/tests/pages/projects/LogsPreviewer.test.js @@ -309,7 +309,7 @@ test('log event chart hide', async () => { return { result: [] } }) render() - 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()