mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
refactor(studio): share Explorer query editor (#49041)
## Stack Depends on #49027. Followed by #49038. ## Summary - extract a controlled `QueryEditor` from the existing notebook query cell - reuse it from `QueryCell`, leaving notebook persistence and sortable-block behavior in the adapter - make table/chart result settings controlled so other query surfaces can share them - persist notebook SQL on editor blur and query execution ## To test 1. Open a notebook query cell, edit and rename it, then run the query and confirm results appear. 2. Switch between table and chart results and confirm notebook move/delete actions still work. ## Why Notebooks, query tabs, and future chat tabs need consistent query actions and result rendering without duplicating the notebook implementation. ## Impact This is primarily a refactor of the existing notebook query experience. It introduces no new query-tab routes or source-selection behavior. ## Validation - fresh non-incremental Studio TypeScript check - focused NotebookEditor component tests <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a shared query editor with SQL editing, execution, validation, visibility controls, editable titles, row limits, and loading/error states. * Added table and chart result views, including customizable bar and line charts. * Added support for switching display modes and updating chart settings. * **Improvements** * Improved query result handling and display-setting updates. * Repositioned the logarithmic-scale tooltip for better visibility. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
1 parent
cc6fe2100a
commit
ace9422bfd
6 files changed
+253
-198
No files matched your search
@@ -18,34 +18,29 @@ import {
|
||||
TooltipTrigger,
|
||||
} from 'ui'
|
||||
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
||||
import { type Snapshot } from 'valtio'
|
||||
|
||||
import { ExplorerToolbarAction } from '../ExplorerToolbar'
|
||||
import { type QueryResult } from '../types'
|
||||
import { type QueryChartConfig, type QueryDisplay, type QueryResult } from '../types'
|
||||
import { checkHasNonPositiveValues } from '@/components/ui/QueryBlock/QueryBlock.utils'
|
||||
import { type DatabaseCell as DatabaseCellSchema } from '@/data/content/notebooks/notebook-schema'
|
||||
import { useCurrentNotebook, useNotebooksStateSnapshot } from '@/state/notebooks/notebooks-state'
|
||||
|
||||
interface DisplaySettingsButtonProps {
|
||||
cell: Snapshot<DatabaseCellSchema>
|
||||
display: QueryDisplay
|
||||
result?: QueryResult
|
||||
columns: string[]
|
||||
disabled: boolean
|
||||
onChange: (display: QueryDisplay) => void
|
||||
}
|
||||
|
||||
// [Joshen] TODO support multiple y axis charts
|
||||
|
||||
export const DisplaySettingsButton = ({
|
||||
cell,
|
||||
display,
|
||||
result,
|
||||
columns,
|
||||
disabled,
|
||||
onChange,
|
||||
}: DisplaySettingsButtonProps) => {
|
||||
const snap = useNotebooksStateSnapshot()
|
||||
const currentNotebook = useCurrentNotebook()
|
||||
const cells = currentNotebook?.notebook.content?.cells ?? []
|
||||
|
||||
const { view, chart } = cell
|
||||
const { view, chart } = display
|
||||
const {
|
||||
type = 'bar',
|
||||
x_column,
|
||||
@@ -66,44 +61,22 @@ export const DisplaySettingsButton = ({
|
||||
}, [hasNonPositiveValues, result, y_columns.length])
|
||||
|
||||
const onChangeView = (view: 'table' | 'chart') => {
|
||||
const notebookId = currentNotebook?.notebook.id
|
||||
if (!notebookId) return
|
||||
|
||||
const nextCells = cells.map((c) =>
|
||||
c.id === cell.id && c._tag === 'database_cell' ? { ...c, view } : c
|
||||
)
|
||||
snap.updateCells({ id: notebookId, cells: nextCells })
|
||||
onChange({ ...display, view })
|
||||
}
|
||||
|
||||
const onUpdateChartConfig = (
|
||||
payload:
|
||||
| { type: 'bar' | 'line' }
|
||||
| { x_column: string }
|
||||
| { y_columns: string[] }
|
||||
| { cumulative: boolean }
|
||||
| { show_labels: boolean }
|
||||
| { scale: 'linear' | 'log' }
|
||||
) => {
|
||||
const notebookId = currentNotebook?.notebook.id
|
||||
if (!notebookId) return
|
||||
|
||||
const nextCells = cells.map((c) => {
|
||||
if (c.id !== cell.id || c._tag !== 'database_cell') return c
|
||||
|
||||
return {
|
||||
...c,
|
||||
chart: {
|
||||
type: c.chart?.type ?? 'bar',
|
||||
x_column: c.chart?.x_column ?? '',
|
||||
y_columns: c.chart?.y_columns ?? [],
|
||||
cumulative: c.chart?.cumulative ?? false,
|
||||
scale: c.chart?.scale ?? 'linear',
|
||||
show_labels: c.chart?.show_labels ?? false,
|
||||
...payload,
|
||||
},
|
||||
}
|
||||
const onUpdateChartConfig = (payload: Partial<QueryChartConfig>) => {
|
||||
onChange({
|
||||
...display,
|
||||
chart: {
|
||||
type: chart?.type ?? 'bar',
|
||||
x_column: chart?.x_column ?? '',
|
||||
y_columns: chart?.y_columns ?? [],
|
||||
cumulative: chart?.cumulative ?? false,
|
||||
scale: chart?.scale ?? 'linear',
|
||||
show_labels: chart?.show_labels ?? false,
|
||||
...payload,
|
||||
},
|
||||
})
|
||||
snap.updateCells({ id: notebookId, cells: nextCells })
|
||||
}
|
||||
|
||||
const resetToLinearScale = useEffectEvent(() => {
|
||||
@@ -230,7 +203,7 @@ export const DisplaySettingsButton = ({
|
||||
</SelectItem>
|
||||
</TooltipTrigger>
|
||||
{!canToggleLogScale && (
|
||||
<TooltipContent side="right">
|
||||
<TooltipContent side="left">
|
||||
{y_columns.length === 0
|
||||
? 'Select a column for the Y axis first'
|
||||
: 'Data contains zero or negative values'}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { useMemo } from 'react'
|
||||
import { Chart, ChartBar, ChartCard, ChartContent, ChartLine } from 'ui-patterns/Chart'
|
||||
import { type Snapshot } from 'valtio'
|
||||
|
||||
import { type QueryResult } from '../types'
|
||||
import { type QueryChartConfig, type QueryResult } from '../types'
|
||||
import NoDataPlaceholder from '@/components/ui/Charts/NoDataPlaceholder'
|
||||
import { formatLogTick, getCumulativeResults } from '@/components/ui/QueryBlock/QueryBlock.utils'
|
||||
import { type DatabaseCell as DatabaseCellSchema } from '@/data/content/notebooks/notebook-schema'
|
||||
|
||||
interface QueryResultChartProps {
|
||||
cell: Snapshot<DatabaseCellSchema>
|
||||
chart?: QueryChartConfig
|
||||
result?: QueryResult
|
||||
}
|
||||
|
||||
@@ -21,8 +19,7 @@ const toChartValue = (value: unknown): string | number => {
|
||||
return String(value)
|
||||
}
|
||||
|
||||
export const QueryResultChart = ({ cell, result }: QueryResultChartProps) => {
|
||||
const { chart } = cell
|
||||
export const QueryResultChart = ({ chart, result }: QueryResultChartProps) => {
|
||||
const { type, x_column, y_columns = [], cumulative, show_labels, scale } = chart ?? {}
|
||||
|
||||
const hasConfig = !!x_column && y_columns.length > 0
|
||||
|
||||
@@ -1,177 +1,76 @@
|
||||
import { acceptUntrustedSql, untrustedSql } from '@supabase/pg-meta'
|
||||
import { CodeSquare, Eye, EyeOff, Play } from 'lucide-react'
|
||||
import { untrustedSql } from '@supabase/pg-meta'
|
||||
import { useState } from 'react'
|
||||
import { cn } from 'ui'
|
||||
import { type Snapshot } from 'valtio'
|
||||
|
||||
import {
|
||||
ExplorerQuery,
|
||||
ExplorerQueryEditor,
|
||||
ExplorerQueryFooter,
|
||||
ExplorerQueryResults,
|
||||
} from '../ExplorerQuery'
|
||||
import {
|
||||
ExplorerToolbar,
|
||||
ExplorerToolbarAction,
|
||||
ExplorerToolbarActions,
|
||||
ExplorerToolbarIcon,
|
||||
ExplorerToolbarTitle,
|
||||
} from '../ExplorerToolbar'
|
||||
import { QueryResultTable } from '../QueryResultTable'
|
||||
import { type QueryResult } from '../types'
|
||||
import { DisplaySettingsButton } from './DisplaySettingsButton'
|
||||
import { QueryResultChart } from './QueryResultChart'
|
||||
import { CodeEditor } from '@/components/ui/CodeEditor/CodeEditor'
|
||||
import { QueryEditor } from '../QueryEditor'
|
||||
import { type QueryDisplay, type QueryResult } from '../types'
|
||||
import { SortableSection } from '@/components/ui/SortableSection'
|
||||
import { type DatabaseCell as DatabaseCellSchema } from '@/data/content/notebooks/notebook-schema'
|
||||
import { useExecuteSqlMutation } from '@/data/sql/execute-sql-mutation'
|
||||
import { useLatest } from '@/hooks/misc/useLatest'
|
||||
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
||||
import { useCurrentNotebook, useNotebooksStateSnapshot } from '@/state/notebooks/notebooks-state'
|
||||
import { type ResponseError } from '@/types'
|
||||
|
||||
interface QueryCellProps {
|
||||
cell: Snapshot<DatabaseCellSchema>
|
||||
}
|
||||
|
||||
/**
|
||||
* [Joshen] Aiming to keep PRs small so the following are deliberating missing for now:
|
||||
* - Auto limit logic
|
||||
* - Database selection logic
|
||||
* - Data display logic
|
||||
*
|
||||
* QueryCell atm minimally supports running queries and rendering results
|
||||
*/
|
||||
type QueryCellUpdate = { sql: string } | { title: string } | { display: QueryDisplay }
|
||||
|
||||
/** Notebook adapter around the shared QueryEditor. */
|
||||
export const QueryCell = ({ cell }: QueryCellProps) => {
|
||||
const snap = useNotebooksStateSnapshot()
|
||||
const currentNotebook = useCurrentNotebook()
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
const cells = currentNotebook?.notebook.content?.cells ?? []
|
||||
|
||||
const { title = 'Untitled snippet', row_limit, view } = cell
|
||||
|
||||
const [showQuery, setShowQuery] = useState(true)
|
||||
const [value, setValue] = useState<string>(cell.unchecked_sql)
|
||||
const [sql, setSql] = useState<string>(cell.unchecked_sql)
|
||||
const [result, setResult] = useState<QueryResult>()
|
||||
const columns = Object.keys(result?.rows?.[0] ?? {})
|
||||
|
||||
const valueRef = useLatest(value)
|
||||
|
||||
const { mutateAsync: executeQuery, isPending: isExecuting } = useExecuteSqlMutation({
|
||||
onSuccess: (data) =>
|
||||
setResult({
|
||||
rows: data.result,
|
||||
error: undefined,
|
||||
autoLimit: undefined,
|
||||
}),
|
||||
onError: (error) =>
|
||||
setResult({
|
||||
rows: undefined,
|
||||
error: error as unknown as ResponseError,
|
||||
autoLimit: undefined,
|
||||
}),
|
||||
})
|
||||
|
||||
const onRunQuery = async () => {
|
||||
if (!project) return console.error('Project is required')
|
||||
|
||||
handleUpdateCell({ sql: value })
|
||||
|
||||
executeQuery({
|
||||
projectRef: project?.ref,
|
||||
connectionString: project?.connectionString,
|
||||
sql: acceptUntrustedSql(untrustedSql(value)),
|
||||
})
|
||||
const title = cell.title ?? 'Untitled snippet'
|
||||
const display: QueryDisplay = {
|
||||
view: cell.view ?? 'table',
|
||||
chart: cell.chart ? { ...cell.chart, y_columns: [...cell.chart.y_columns] } : undefined,
|
||||
}
|
||||
|
||||
const handleUpdateCell = (payload: { sql: string } | { title: string }) => {
|
||||
const handleUpdateCell = (payload: QueryCellUpdate) => {
|
||||
const notebookId = currentNotebook?.notebook.id
|
||||
if (!notebookId) return
|
||||
|
||||
const nextCells = cells.map((c) => {
|
||||
if (c.id !== cell.id || c._tag !== 'database_cell') {
|
||||
return c
|
||||
}
|
||||
const nextCells = cells.map((candidate) => {
|
||||
if (candidate.id !== cell.id || candidate._tag !== 'database_cell') return candidate
|
||||
|
||||
if ('sql' in payload) {
|
||||
return { ...c, unchecked_sql: untrustedSql(payload.sql) }
|
||||
return { ...candidate, unchecked_sql: untrustedSql(payload.sql) }
|
||||
}
|
||||
|
||||
const trimmedTitle = payload.title.trim()
|
||||
return trimmedTitle ? { ...c, title: trimmedTitle } : c
|
||||
if ('title' in payload) {
|
||||
const nextTitle = payload.title.trim()
|
||||
return nextTitle ? { ...candidate, title: nextTitle } : candidate
|
||||
}
|
||||
|
||||
return {
|
||||
...candidate,
|
||||
view: payload.display.view,
|
||||
chart: payload.display.chart,
|
||||
}
|
||||
})
|
||||
|
||||
snap.updateCells({ id: notebookId, cells: nextCells })
|
||||
}
|
||||
|
||||
const handleUpdateCellRef = useLatest(handleUpdateCell)
|
||||
|
||||
return (
|
||||
<SortableSection gripClassName="mt-2.5" id={cell.id}>
|
||||
<ExplorerQuery className="max-w-4xl mx-auto">
|
||||
<ExplorerToolbar>
|
||||
<ExplorerToolbarIcon>
|
||||
<CodeSquare size={14} />
|
||||
</ExplorerToolbarIcon>
|
||||
<ExplorerToolbarTitle onSaveTitle={(newTitle) => handleUpdateCell({ title: newTitle })}>
|
||||
{title}
|
||||
</ExplorerToolbarTitle>
|
||||
<ExplorerToolbarActions>
|
||||
<DisplaySettingsButton
|
||||
cell={cell}
|
||||
result={result}
|
||||
columns={columns}
|
||||
disabled={(result?.rows ?? []).length === 0}
|
||||
/>
|
||||
<ExplorerToolbarAction
|
||||
icon={showQuery ? <EyeOff /> : <Eye />}
|
||||
tooltip={showQuery ? 'Hide query' : 'Show query'}
|
||||
onClick={() => setShowQuery((prev) => !prev)}
|
||||
/>
|
||||
<ExplorerToolbarAction
|
||||
loading={isExecuting}
|
||||
icon={<Play />}
|
||||
tooltip="Run query"
|
||||
onClick={onRunQuery}
|
||||
/>
|
||||
</ExplorerToolbarActions>
|
||||
</ExplorerToolbar>
|
||||
|
||||
{showQuery && (
|
||||
<ExplorerQueryEditor>
|
||||
<CodeEditor
|
||||
language="pgsql"
|
||||
value={value}
|
||||
onInputChange={(v) => setValue(v ?? '')}
|
||||
className="h-32"
|
||||
actions={{ runQuery: { enabled: true, callback: onRunQuery } }}
|
||||
onMount={(editor) => {
|
||||
editor.onDidBlurEditorWidget(() =>
|
||||
handleUpdateCellRef.current({ sql: valueRef.current })
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</ExplorerQueryEditor>
|
||||
)}
|
||||
|
||||
<ExplorerQueryResults
|
||||
className={cn(
|
||||
(result?.rows ?? []).length === 0 && view === 'table'
|
||||
? 'flex items-center justify-center'
|
||||
: 'overflow-x-auto'
|
||||
)}
|
||||
>
|
||||
{view === 'table' && <QueryResultTable result={result} />}
|
||||
{view === 'chart' && <QueryResultChart cell={cell} result={result} />}
|
||||
</ExplorerQueryResults>
|
||||
|
||||
<ExplorerQueryFooter className="flex items-center gap-x-2">
|
||||
<p>{(result?.rows ?? []).length.toLocaleString()} rows</p>
|
||||
<p>·</p>
|
||||
<p>Limit {row_limit} rows</p>
|
||||
</ExplorerQueryFooter>
|
||||
</ExplorerQuery>
|
||||
<QueryEditor
|
||||
id={cell.id}
|
||||
variant="embedded"
|
||||
title={title}
|
||||
sql={sql}
|
||||
result={result}
|
||||
rowLimit={cell.row_limit}
|
||||
display={display}
|
||||
onTitleChange={(title) => handleUpdateCell({ title })}
|
||||
onSqlChange={setSql}
|
||||
onSqlCommit={(sql) => handleUpdateCell({ sql })}
|
||||
onResultChange={setResult}
|
||||
onDisplayChange={(display) => handleUpdateCell({ display })}
|
||||
/>
|
||||
</SortableSection>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import { acceptUntrustedSql, untrustedSql } from '@supabase/pg-meta'
|
||||
import { CodeSquare, Eye, EyeOff, Play } from 'lucide-react'
|
||||
import { useState, type ReactNode } from 'react'
|
||||
import { cn } from 'ui'
|
||||
|
||||
import {
|
||||
ExplorerQuery,
|
||||
ExplorerQueryEditor,
|
||||
ExplorerQueryFooter,
|
||||
ExplorerQueryResults,
|
||||
ExplorerQueryViewport,
|
||||
} from './ExplorerQuery'
|
||||
import {
|
||||
ExplorerToolbar,
|
||||
ExplorerToolbarAction,
|
||||
ExplorerToolbarActions,
|
||||
ExplorerToolbarIcon,
|
||||
ExplorerToolbarTitle,
|
||||
} from './ExplorerToolbar'
|
||||
import { DisplaySettingsButton } from './QueryCell/DisplaySettingsButton'
|
||||
import { QueryResultChart } from './QueryCell/QueryResultChart'
|
||||
import { QueryResultTable } from './QueryResultTable'
|
||||
import { type QueryDisplay, type QueryResult } from './types'
|
||||
import { applyAutoLimit } from '@/components/interfaces/SQLEditor/SQLEditor.utils'
|
||||
import { CodeEditor } from '@/components/ui/CodeEditor/CodeEditor'
|
||||
import { useExecuteSqlMutation } from '@/data/sql/execute-sql-mutation'
|
||||
import { useLatest } from '@/hooks/misc/useLatest'
|
||||
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
||||
|
||||
export type QueryEditorProps = {
|
||||
id: string
|
||||
variant: 'embedded' | 'viewport'
|
||||
title: string
|
||||
sql: string
|
||||
result?: QueryResult
|
||||
rowLimit: number
|
||||
display?: QueryDisplay
|
||||
toolbarActions?: ReactNode
|
||||
onTitleChange: (title: string) => void
|
||||
onSqlChange: (sql: string) => void
|
||||
onSqlCommit?: (sql: string) => void
|
||||
onResultChange: (result: QueryResult) => void
|
||||
onDisplayChange?: (display: QueryDisplay) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared query editor used by query tabs, notebook cells, and other Explorer surfaces.
|
||||
* The consuming surface owns persistence and surrounding chrome; this component owns
|
||||
* query-level UI and execution behavior.
|
||||
*/
|
||||
export const QueryEditor = ({
|
||||
id,
|
||||
variant,
|
||||
title,
|
||||
sql,
|
||||
result,
|
||||
rowLimit,
|
||||
display,
|
||||
toolbarActions,
|
||||
onTitleChange,
|
||||
onSqlChange,
|
||||
onSqlCommit,
|
||||
onResultChange,
|
||||
onDisplayChange,
|
||||
}: QueryEditorProps) => {
|
||||
const sqlRef = useLatest(sql)
|
||||
const onSqlCommitRef = useLatest(onSqlCommit)
|
||||
|
||||
const { data: project, isPending: isLoadingProject } = useSelectedProjectQuery()
|
||||
|
||||
const view = display?.view ?? 'table'
|
||||
const columns = Object.keys(result?.rows?.[0] ?? {})
|
||||
|
||||
const [showQuery, setShowQuery] = useState(true)
|
||||
|
||||
const { mutate: executeSql, isPending: isExecuting } = useExecuteSqlMutation({
|
||||
onSuccess: (data) => onResultChange({ rows: data.result }),
|
||||
onError: (error) => onResultChange({ error }),
|
||||
})
|
||||
|
||||
const handleRunQuery = (sqlToRun: string = sql) => {
|
||||
if (!project || isLoadingProject || isExecuting || sqlToRun.trim().length === 0) return
|
||||
|
||||
onSqlCommit?.(sql)
|
||||
|
||||
const safeSql = acceptUntrustedSql(untrustedSql(sqlToRun))
|
||||
const limitedSql = applyAutoLimit(safeSql, rowLimit)
|
||||
|
||||
executeSql({
|
||||
projectRef: project.ref,
|
||||
connectionString: project.connectionString,
|
||||
sql: limitedSql.sql,
|
||||
autoLimit: limitedSql.appendAutoLimit ? rowLimit : undefined,
|
||||
contextualInvalidation: true,
|
||||
isStatementTimeoutDisabled: true,
|
||||
})
|
||||
}
|
||||
|
||||
const Shell = variant === 'viewport' ? ExplorerQueryViewport : ExplorerQuery
|
||||
|
||||
return (
|
||||
<Shell className={variant === 'embedded' ? 'mx-auto max-w-4xl' : undefined}>
|
||||
<ExplorerToolbar>
|
||||
<ExplorerToolbarIcon>
|
||||
<CodeSquare size={14} />
|
||||
</ExplorerToolbarIcon>
|
||||
<ExplorerToolbarTitle onSaveTitle={onTitleChange}>{title}</ExplorerToolbarTitle>
|
||||
<ExplorerToolbarActions>
|
||||
{toolbarActions}
|
||||
{display && onDisplayChange && (
|
||||
<DisplaySettingsButton
|
||||
result={result}
|
||||
display={display}
|
||||
columns={columns}
|
||||
disabled={(result?.rows ?? []).length === 0}
|
||||
onChange={onDisplayChange}
|
||||
/>
|
||||
)}
|
||||
<ExplorerToolbarAction
|
||||
icon={showQuery ? <EyeOff /> : <Eye />}
|
||||
tooltip={showQuery ? 'Hide query' : 'Show query'}
|
||||
onClick={() => setShowQuery((value) => !value)}
|
||||
/>
|
||||
<ExplorerToolbarAction
|
||||
loading={isExecuting || isLoadingProject}
|
||||
icon={<Play />}
|
||||
tooltip="Run query"
|
||||
disabled={isLoadingProject || isExecuting || sql.trim().length === 0}
|
||||
onClick={() => handleRunQuery()}
|
||||
>
|
||||
Run
|
||||
</ExplorerToolbarAction>
|
||||
</ExplorerToolbarActions>
|
||||
</ExplorerToolbar>
|
||||
|
||||
{showQuery && (
|
||||
<ExplorerQueryEditor className={variant === 'viewport' ? 'h-[45%] min-h-48' : undefined}>
|
||||
<CodeEditor
|
||||
id={`explorer-query-${id}`}
|
||||
language="pgsql"
|
||||
value={sql}
|
||||
placeholder="select * from your_table limit 100;"
|
||||
className={variant === 'embedded' ? 'h-32' : undefined}
|
||||
actions={{ runQuery: { enabled: true, callback: handleRunQuery } }}
|
||||
options={{ minimap: { enabled: false }, padding: { top: 8 } }}
|
||||
onInputChange={(value) => onSqlChange(value ?? '')}
|
||||
onMount={(editor) => {
|
||||
editor.onDidBlurEditorWidget(() => onSqlCommitRef.current?.(sqlRef.current))
|
||||
}}
|
||||
/>
|
||||
</ExplorerQueryEditor>
|
||||
)}
|
||||
|
||||
<ExplorerQueryResults
|
||||
className={cn(
|
||||
(result?.rows ?? []).length === 0 && view === 'table'
|
||||
? 'items-center justify-center'
|
||||
: 'overflow-x-auto'
|
||||
)}
|
||||
>
|
||||
{view === 'table' && <QueryResultTable result={result} />}
|
||||
{view === 'chart' && <QueryResultChart chart={display?.chart} result={result} />}
|
||||
</ExplorerQueryResults>
|
||||
|
||||
<ExplorerQueryFooter className="flex items-center gap-x-2">
|
||||
<p>{(result?.rows ?? []).length.toLocaleString()} rows</p>
|
||||
<p>·</p>
|
||||
<p>Limit {rowLimit} rows</p>
|
||||
</ExplorerQueryFooter>
|
||||
</Shell>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,19 @@
|
||||
import { type ResponseError } from '@/types'
|
||||
|
||||
export type QueryResult = {
|
||||
rows?: Record<string, unknown>[]
|
||||
error?: ResponseError
|
||||
rows?: readonly Record<string, unknown>[]
|
||||
error?: { message: string; formattedError?: string }
|
||||
autoLimit?: number
|
||||
}
|
||||
|
||||
export type QueryChartConfig = {
|
||||
type: 'bar' | 'line'
|
||||
x_column: string
|
||||
y_columns: string[]
|
||||
cumulative: boolean
|
||||
scale: 'linear' | 'log'
|
||||
show_labels: boolean
|
||||
}
|
||||
|
||||
export type QueryDisplay = {
|
||||
view: 'table' | 'chart'
|
||||
chart?: QueryChartConfig
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
export const checkHasNonPositiveValues = (data: Record<string, unknown>[], key: string): boolean =>
|
||||
data.some((row) => (row[key] as number) <= 0)
|
||||
export const checkHasNonPositiveValues = (
|
||||
data: readonly Record<string, unknown>[],
|
||||
key: string
|
||||
): boolean => data.some((row) => (row[key] as number) <= 0)
|
||||
|
||||
export const formatYAxisTick = (value: number): string => {
|
||||
if (Math.abs(value) >= 1_000_000) {
|
||||
|
||||
Reference in new issue
Block a user