mirror of
https://github.com/supabase/supabase.git
synced 2026-10-14 05:39:11 +03:00
added initial test of notesbooks
This commit is contained in:
1 parent
bd1ce11d1c
commit
bcb045f1e3
14 files changed
+1193
-17
No files matched your search
@@ -0,0 +1,108 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import dayjs from 'dayjs'
|
||||
import { Bar, BarChart, CartesianGrid, Cell, Tooltip, XAxis, YAxis } from 'recharts'
|
||||
import { ChartContainer, ChartTooltipContent } from 'ui'
|
||||
import { CHART_COLORS } from 'components/ui/Charts/Charts.constants'
|
||||
import type { ChartConfig } from 'components/ui/QueryBlock/QueryBlock.types'
|
||||
|
||||
interface SQLQueryChartProps {
|
||||
results: any[]
|
||||
chartConfig: ChartConfig
|
||||
}
|
||||
|
||||
function getCumulativeResults(data: { rows: readonly any[] }, config: ChartConfig): any[] {
|
||||
const { xKey, yKey } = config
|
||||
if (!xKey || !yKey) return data.rows as any[]
|
||||
|
||||
let cumulative = 0
|
||||
return data.rows.map((row) => {
|
||||
cumulative += Number(row[yKey]) || 0
|
||||
return {
|
||||
...row,
|
||||
[yKey]: cumulative,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function SQLQueryChart({ results, chartConfig }: SQLQueryChartProps) {
|
||||
const { xKey, yKey, cumulative = false } = chartConfig
|
||||
const [focusDataIndex, setFocusDataIndex] = useState<number>()
|
||||
|
||||
const formattedQueryResult = useMemo(() => {
|
||||
return results?.map((row) => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(row).map(([key, value]) => {
|
||||
if (key === yKey) return [key, Number(value)]
|
||||
return [key, value]
|
||||
})
|
||||
)
|
||||
})
|
||||
}, [results, yKey])
|
||||
|
||||
const chartData = cumulative
|
||||
? getCumulativeResults({ rows: formattedQueryResult ?? [] }, chartConfig)
|
||||
: formattedQueryResult
|
||||
|
||||
const getDateFormat = (key: string) => {
|
||||
const value = chartData?.[0]?.[key] || ''
|
||||
if (typeof value === 'number') return 'number'
|
||||
if (dayjs(value).isValid()) return 'date'
|
||||
return 'string'
|
||||
}
|
||||
|
||||
const xKeyDateFormat = getDateFormat(xKey)
|
||||
|
||||
if (!xKey || !yKey) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<p className="text-sm text-foreground-light">Select columns for the X and Y axes</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-1 w-full">
|
||||
<ChartContainer
|
||||
className="aspect-auto px-3 py-2"
|
||||
style={{ height: '230px', minHeight: '230px' }}
|
||||
>
|
||||
<BarChart
|
||||
accessibilityLayer
|
||||
margin={{ left: -20, right: 0, top: 10 }}
|
||||
data={chartData}
|
||||
onMouseMove={(e: any) => {
|
||||
if (e.activeTooltipIndex !== focusDataIndex) {
|
||||
setFocusDataIndex(e.activeTooltipIndex)
|
||||
}
|
||||
}}
|
||||
onMouseLeave={() => setFocusDataIndex(undefined)}
|
||||
>
|
||||
<CartesianGrid vertical={false} stroke={CHART_COLORS.AXIS} />
|
||||
<XAxis
|
||||
dataKey={xKey}
|
||||
tickLine={{ stroke: CHART_COLORS.AXIS }}
|
||||
axisLine={{ stroke: CHART_COLORS.AXIS }}
|
||||
interval="preserveStartEnd"
|
||||
tickMargin={4}
|
||||
minTickGap={32}
|
||||
tickFormatter={(value) =>
|
||||
xKeyDateFormat === 'date' ? dayjs(value).format('MMM D YYYY HH:mm') : value
|
||||
}
|
||||
/>
|
||||
<YAxis tickLine={false} axisLine={false} tickMargin={4} />
|
||||
<Tooltip content={<ChartTooltipContent className="w-[150px]" />} />
|
||||
<Bar radius={1} dataKey={yKey}>
|
||||
{chartData?.map((_: any, index: number) => (
|
||||
<Cell
|
||||
key={`cell-${index}`}
|
||||
className="transition-all duration-100"
|
||||
fill="hsl(var(--chart-1))"
|
||||
opacity={focusDataIndex === undefined || focusDataIndex === index ? 1 : 0.4}
|
||||
/>
|
||||
))}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+299
@@ -0,0 +1,299 @@
|
||||
import { useState, useCallback, useEffect } from 'react'
|
||||
import {
|
||||
PluginElementRenderProps,
|
||||
useYooptaEditor,
|
||||
Elements,
|
||||
useYooptaReadOnly,
|
||||
} from '@yoopta/editor'
|
||||
import { Play, ChevronDown, ChevronUp, Table2, BarChart3, Loader2, Pencil } from 'lucide-react'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { useParams } from 'common'
|
||||
import { executeSql } from 'data/sql/execute-sql-query'
|
||||
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
|
||||
import Results from 'components/interfaces/SQLEditor/UtilityPanel/Results'
|
||||
import { Button, cn, Input_Shadcn_ } from 'ui'
|
||||
import { BlockViewConfiguration } from 'components/ui/QueryBlock/BlockViewConfiguration'
|
||||
import { DEFAULT_CHART_CONFIG, type ChartConfig } from 'components/ui/QueryBlock/QueryBlock.types'
|
||||
import { SQLQueryChart } from './SQLQueryChart'
|
||||
|
||||
export interface SQLQueryElementProps {
|
||||
sql: string
|
||||
label: string
|
||||
chartConfig: ChartConfig
|
||||
results?: any[]
|
||||
error?: string
|
||||
}
|
||||
|
||||
export const SQLQueryElement = ({
|
||||
element,
|
||||
children,
|
||||
attributes,
|
||||
blockId,
|
||||
}: PluginElementRenderProps) => {
|
||||
const editor = useYooptaEditor()
|
||||
const isReadOnly = useYooptaReadOnly()
|
||||
const { ref: projectRef } = useParams()
|
||||
const state = useDatabaseSelectorStateSnapshot()
|
||||
|
||||
const props = element.props as SQLQueryElementProps
|
||||
const { sql = '', label = 'Untitled Query', chartConfig = DEFAULT_CHART_CONFIG } = props
|
||||
|
||||
const [localSql, setLocalSql] = useState(sql)
|
||||
const [localLabel, setLocalLabel] = useState(label)
|
||||
const [localChartConfig, setLocalChartConfig] = useState<ChartConfig>(chartConfig)
|
||||
const [showSql, setShowSql] = useState(true)
|
||||
const [isEditingLabel, setIsEditingLabel] = useState(false)
|
||||
const [results, setResults] = useState<any[] | undefined>(props.results)
|
||||
const [error, setError] = useState<string | undefined>(props.error)
|
||||
|
||||
// Sync local state with element props
|
||||
useEffect(() => {
|
||||
setLocalSql(sql)
|
||||
setLocalLabel(label)
|
||||
setLocalChartConfig(chartConfig)
|
||||
setResults(props.results)
|
||||
setError(props.error)
|
||||
}, [sql, label, chartConfig, props.results, props.error])
|
||||
|
||||
const { mutate: executeQuery, isPending: isExecuting } = useMutation({
|
||||
mutationFn: async (query: string) => {
|
||||
const result = await executeSql({
|
||||
projectRef: projectRef!,
|
||||
connectionString: state.selectedDatabaseId,
|
||||
sql: query,
|
||||
})
|
||||
return result
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
const resultRows = Array.isArray(data.result) ? data.result : []
|
||||
setResults(resultRows)
|
||||
setError(undefined)
|
||||
|
||||
// Update element with results
|
||||
Elements.updateElement(editor, blockId, {
|
||||
type: 'sql-query',
|
||||
props: {
|
||||
...props,
|
||||
sql: localSql,
|
||||
label: localLabel,
|
||||
results: resultRows,
|
||||
error: undefined,
|
||||
},
|
||||
})
|
||||
},
|
||||
onError: (err: any) => {
|
||||
const errorMessage = err?.message || 'An error occurred while executing the query'
|
||||
setError(errorMessage)
|
||||
setResults(undefined)
|
||||
|
||||
// Update element with error
|
||||
Elements.updateElement(editor, blockId, {
|
||||
type: 'sql-query',
|
||||
props: {
|
||||
...props,
|
||||
sql: localSql,
|
||||
label: localLabel,
|
||||
results: undefined,
|
||||
error: errorMessage,
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const handleRunQuery = useCallback(() => {
|
||||
if (!localSql.trim() || !projectRef) return
|
||||
executeQuery(localSql)
|
||||
}, [localSql, projectRef, executeQuery])
|
||||
|
||||
const handleSqlChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const newSql = e.target.value
|
||||
setLocalSql(newSql)
|
||||
|
||||
// Debounced update to element
|
||||
Elements.updateElement(editor, blockId, {
|
||||
type: 'sql-query',
|
||||
props: {
|
||||
...props,
|
||||
sql: newSql,
|
||||
},
|
||||
})
|
||||
},
|
||||
[editor, blockId, props]
|
||||
)
|
||||
|
||||
const handleLabelChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newLabel = e.target.value
|
||||
setLocalLabel(newLabel)
|
||||
}, [])
|
||||
|
||||
const handleLabelBlur = useCallback(() => {
|
||||
setIsEditingLabel(false)
|
||||
Elements.updateElement(editor, blockId, {
|
||||
type: 'sql-query',
|
||||
props: {
|
||||
...props,
|
||||
label: localLabel,
|
||||
},
|
||||
})
|
||||
}, [editor, blockId, props, localLabel])
|
||||
|
||||
const handleChartConfigChange = useCallback(
|
||||
(config: Partial<ChartConfig>) => {
|
||||
const newConfig = { ...localChartConfig, ...config }
|
||||
setLocalChartConfig(newConfig)
|
||||
|
||||
Elements.updateElement(editor, blockId, {
|
||||
type: 'sql-query',
|
||||
props: {
|
||||
...props,
|
||||
chartConfig: newConfig,
|
||||
},
|
||||
})
|
||||
},
|
||||
[editor, blockId, props, localChartConfig]
|
||||
)
|
||||
|
||||
const hasResults = Array.isArray(results) && results.length > 0
|
||||
const view = localChartConfig.view || 'table'
|
||||
|
||||
return (
|
||||
<div
|
||||
{...attributes}
|
||||
contentEditable={false}
|
||||
className="yoopta-sql-query my-4 rounded-md border border-default bg-surface-100 overflow-hidden"
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-3 py-2 border-b border-default bg-surface-200">
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
{isEditingLabel && !isReadOnly ? (
|
||||
<Input_Shadcn_
|
||||
value={localLabel}
|
||||
onChange={handleLabelChange}
|
||||
onBlur={handleLabelBlur}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleLabelBlur()
|
||||
}
|
||||
}}
|
||||
className="h-6 text-sm font-medium max-w-[200px]"
|
||||
autoFocus
|
||||
/>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => !isReadOnly && setIsEditingLabel(true)}
|
||||
className={cn(
|
||||
'text-sm font-medium text-foreground truncate',
|
||||
!isReadOnly && 'hover:text-foreground-light cursor-pointer'
|
||||
)}
|
||||
>
|
||||
{localLabel}
|
||||
</button>
|
||||
)}
|
||||
{!isReadOnly && !isEditingLabel && (
|
||||
<button
|
||||
onClick={() => setIsEditingLabel(true)}
|
||||
className="text-foreground-lighter hover:text-foreground transition-colors"
|
||||
>
|
||||
<Pencil size={12} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
type="text"
|
||||
size="tiny"
|
||||
className="w-7 h-7"
|
||||
onClick={() => setShowSql(!showSql)}
|
||||
icon={showSql ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
|
||||
/>
|
||||
|
||||
{hasResults && (
|
||||
<BlockViewConfiguration
|
||||
view={view}
|
||||
isChart={view === 'chart'}
|
||||
lockColumns={false}
|
||||
chartConfig={localChartConfig}
|
||||
columns={Object.keys(results?.[0] ?? {})}
|
||||
changeView={(nextView) => handleChartConfigChange({ view: nextView })}
|
||||
updateChartConfig={handleChartConfigChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isReadOnly && (
|
||||
<Button
|
||||
type="text"
|
||||
size="tiny"
|
||||
className="w-7 h-7"
|
||||
onClick={handleRunQuery}
|
||||
disabled={isExecuting || !localSql.trim()}
|
||||
icon={
|
||||
isExecuting ? <Loader2 size={14} className="animate-spin" /> : <Play size={14} />
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SQL Editor */}
|
||||
{showSql && (
|
||||
<div className="border-b border-default">
|
||||
<textarea
|
||||
value={localSql}
|
||||
onChange={handleSqlChange}
|
||||
placeholder="SELECT * FROM your_table LIMIT 10"
|
||||
className={cn(
|
||||
'w-full min-h-[100px] p-3 font-mono text-sm bg-transparent resize-y',
|
||||
'focus:outline-none focus:ring-0 border-0',
|
||||
'text-foreground placeholder:text-foreground-muted'
|
||||
)}
|
||||
readOnly={isReadOnly}
|
||||
onKeyDown={(e) => {
|
||||
// Run query on Cmd/Ctrl + Enter
|
||||
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
handleRunQuery()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Loading State */}
|
||||
{isExecuting && (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 size={24} className="animate-spin text-foreground-muted" />
|
||||
<span className="ml-2 text-sm text-foreground-muted">Executing query...</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error State */}
|
||||
{!isExecuting && error && (
|
||||
<div className="p-3 bg-destructive-200">
|
||||
<p className="text-sm text-destructive font-mono">ERROR: {error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Results */}
|
||||
{!isExecuting && !error && results !== undefined && (
|
||||
<>
|
||||
{results.length === 0 ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<p className="text-sm text-foreground-light">No results returned from query</p>
|
||||
</div>
|
||||
) : view === 'chart' ? (
|
||||
<SQLQueryChart results={results} chartConfig={localChartConfig} />
|
||||
) : (
|
||||
<div className="max-h-64 overflow-auto">
|
||||
<Results rows={results} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Hidden children for Slate */}
|
||||
<div className="hidden">{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { YooptaPlugin, generateId } from '@yoopta/editor'
|
||||
import { SQLQueryElement, type SQLQueryElementProps } from './SQLQueryElement'
|
||||
import { DEFAULT_CHART_CONFIG } from 'components/ui/QueryBlock/QueryBlock.types'
|
||||
|
||||
const SQLQueryPlugin = new YooptaPlugin({
|
||||
type: 'SQLQuery',
|
||||
elements: {
|
||||
'sql-query': {
|
||||
render: SQLQueryElement,
|
||||
props: {
|
||||
sql: '',
|
||||
label: 'Untitled Query',
|
||||
chartConfig: DEFAULT_CHART_CONFIG,
|
||||
nodeType: 'void',
|
||||
},
|
||||
},
|
||||
},
|
||||
options: {
|
||||
display: {
|
||||
title: 'SQL Query',
|
||||
description: 'Execute SQL queries and visualize results',
|
||||
},
|
||||
shortcuts: ['sql', 'query', 'select'],
|
||||
},
|
||||
parsers: {
|
||||
html: {
|
||||
deserialize: {
|
||||
nodeNames: ['DIV'],
|
||||
parse: (el) => {
|
||||
if (el.getAttribute('data-type') === 'sql-query') {
|
||||
return {
|
||||
id: generateId(),
|
||||
type: 'sql-query',
|
||||
children: [{ text: '' }],
|
||||
props: {
|
||||
sql: el.getAttribute('data-sql') || '',
|
||||
label: el.getAttribute('data-label') || 'Untitled Query',
|
||||
chartConfig: DEFAULT_CHART_CONFIG,
|
||||
},
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
},
|
||||
serialize: (element) => {
|
||||
const props = element.props as SQLQueryElementProps
|
||||
return `<div data-type="sql-query" data-sql="${props.sql}" data-label="${props.label}">
|
||||
<pre><code class="language-sql">${props.sql}</code></pre>
|
||||
</div>`
|
||||
},
|
||||
},
|
||||
markdown: {
|
||||
serialize: (element) => {
|
||||
const props = element.props as SQLQueryElementProps
|
||||
return `\`\`\`sql\n${props.sql}\n\`\`\`\n`
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export { SQLQueryPlugin }
|
||||
Reference in new issue
Block a user