feat(studio): SQL Editor IA improvements

Refactor the SQL Editor query bar, results panel, and sidebar chrome for
clearer run controls, post-run metadata, and consolidated query actions.
Includes warehouse routing/lag display in results for prototype demos.
This commit is contained in:
Danny White committed 2026-06-30 13:27:41 -06:00
1 parent 963ab3e63b
commit 302abc5e27
12 files changed
+594 -629

No files matched your search

@@ -100,7 +100,7 @@ export const ASSISTANT_TEMPLATES = [
]
export const ROWS_PER_PAGE_OPTIONS = [
{ value: -1, label: 'No limit' },
{ value: -1, label: 'None' },
{ value: 100, label: '100 rows' },
{ value: 500, label: '500 rows' },
{ value: 1000, label: '1,000 rows' },
@@ -9,32 +9,17 @@ import {
import { wrapWithRollback } from '@supabase/pg-meta/src/query'
import { useQueryClient } from '@tanstack/react-query'
import { IS_PLATFORM, LOCAL_STORAGE_KEYS, useFlag, useParams } from 'common'
import { ChevronUp, Loader2 } from 'lucide-react'
import { Loader2 } from 'lucide-react'
import dynamic from 'next/dynamic'
import { useRouter } from 'next/router'
import { useCallback, useEffect, useEffectEvent, useMemo, useRef, useState } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { toast } from 'sonner'
import {
Button,
cn,
DropdownMenu,
DropdownMenuContent,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
Tooltip,
TooltipContent,
TooltipTrigger,
} from 'ui'
import { cn, ResizableHandle, ResizablePanel, ResizablePanelGroup } from 'ui'
import { useSqlEditorDiff, useSqlEditorPrompt } from './hooks'
import { RunQueryWarningModal } from './RunQueryWarningModal'
import {
generateSnippetTitle,
ROWS_PER_PAGE_OPTIONS,
sqlAiDisclaimerComment,
untitledSnippetTitle,
} from './SQLEditor.constants'
@@ -56,6 +41,7 @@ import {
isUpdateWithoutWhere,
suffixWithLimit,
} from './SQLEditor.utils'
import { SqlEditorQueryBar } from './SqlEditorQueryBar'
import { useAddDefinitions } from './useAddDefinitions'
import { UtilityPanel } from './UtilityPanel/UtilityPanel'
import {
@@ -93,7 +79,6 @@ import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { useShortcut } from '@/state/shortcuts/useShortcut'
import { useSidebarManagerSnapshot } from '@/state/sidebar-manager-state'
import { getSqlEditorV2StateSnapshot, useSqlEditorV2StateSnapshot } from '@/state/sql-editor-v2'
import { useSqlEditorDiffRequestSnapshot } from '@/state/sql-editor/sql-editor-diff-request'
import { useSqlEditorSessionSnapshot } from '@/state/sql-editor/sql-editor-session-state'
import { createTabId, useTabsStateSnapshot } from '@/state/tabs'
@@ -122,7 +107,6 @@ export const SQLEditor = () => {
const { openSidebar } = useSidebarManagerSnapshot()
const snapV2 = useSqlEditorV2StateSnapshot()
const sessionSnap = useSqlEditorSessionSnapshot()
const diffRequest = useSqlEditorDiffRequestSnapshot()
const getImpersonatedRoleState = useGetImpersonatedRoleState()
const databaseSelectorState = useDatabaseSelectorStateSnapshot()
const { aiOptInLevel } = useOrgAiOptInLevel()
@@ -156,9 +140,6 @@ export const SQLEditor = () => {
const [potentialIssues, setPotentialIssues] = useState<PotentialIssues>()
const [showWidget, setShowWidget] = useState(false)
// Bumped on every editor mount (including the keyed remount on snippet switch)
// so a diff request that arrived before the editor was ready gets re-processed.
const [editorMountCount, setEditorMountCount] = useState(0)
const [activeUtilityTab, setActiveUtilityTab] = useState<string>('results')
const refocusEditor = useCallback(() => {
@@ -205,7 +186,6 @@ export const SQLEditor = () => {
const id = !urlId || urlId === 'new' ? generatedId : urlId
const limit = sessionSnap.limit
const results = sessionSnap.results[id]?.[0]
const snippetIsLoading = !(
id in snapV2.snippets && snapV2.snippets[id].snippet.content !== undefined
)
@@ -544,7 +524,7 @@ export const SQLEditor = () => {
databaseSelectorState.selectedDatabaseId,
databases,
lineHighlights,
sessionSnap,
snapV2,
])
useShortcut(SHORTCUT_IDS.SQL_EDITOR_EXPLAIN, executeExplainQuery, {
@@ -577,8 +557,6 @@ export const SQLEditor = () => {
)
const onMount = (editor: IStandaloneCodeEditor) => {
setEditorMountCount((count) => count + 1)
const tabId = createTabId('sql', { id })
const tabData = tabs.tabsMap[tabId]
@@ -834,40 +812,30 @@ export const SQLEditor = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isSuccessReadReplicas, databases, ref])
const drainDiffRequest = useEffectEvent(() => {
const request = diffRequest.pending
if (request === undefined) return
const editorModel = editorRef.current?.getModel()
// Editor isn't ready yet; leave the request pending. editorMountCount bumps
// on mount and re-runs this effect, so the request applies once mounted.
if (!editorModel) return
const { diffType, sql } = request
const existingValue = editorRef.current?.getValue() ?? ''
if (existingValue.length === 0) {
// if the editor is empty, just copy over the code
editorRef.current?.executeEdits('apply-ai-message', [
{
text: `${sql}`,
range: editorModel.getFullModelRange(),
},
])
} else {
const currentSql = editorRef.current?.getValue()
const diff = { original: currentSql || '', modified: sql }
setSourceSqlDiff(diff)
setSelectedDiffType(diffType)
}
// One-shot: drain the request so it can't re-apply to a later editor or session.
diffRequest.consumeDiffRequest()
})
useEffect(() => {
drainDiffRequest()
// until we can upgrade eslint to ignore useEffectEvent
if (snapV2.diffContent !== undefined) {
const { diffType, sql }: { diffType: DiffType; sql: string } = snapV2.diffContent
const editorModel = editorRef.current?.getModel()
if (!editorModel) return
const existingValue = editorRef.current?.getValue() ?? ''
if (existingValue.length === 0) {
// if the editor is empty, just copy over the code
editorRef.current?.executeEdits('apply-ai-message', [
{
text: `${sql}`,
range: editorModel.getFullModelRange(),
},
])
} else {
const currentSql = editorRef.current?.getValue()
const diff = { original: currentSql || '', modified: sql }
setSourceSqlDiff(diff)
setSelectedDiffType(diffType)
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [diffRequest.pending, editorMountCount])
}, [snapV2.diffContent])
// We want to check if the diff editor is mounted and if it is, we want to show the widget
// We also want to cleanup the widget when the diff editor is closed
@@ -881,6 +849,121 @@ export const SQLEditor = () => {
}
}, [isDiffOpen, isDiffEditorMounted])
const queryResult = sessionSnap.results[id]?.[0]
const explainResult = sessionSnap.explainResults[id]
const hasExplainResult =
explainResult?.error !== undefined || (explainResult?.rows?.length ?? 0) > 0
const showUtilityPanel =
isExecuting || isExplainExecuting || queryResult !== undefined || hasExplainResult
const sqlEditorWorkspace = isLoading ? (
<div className="flex h-full w-full items-center justify-center">
<Loader2 className="animate-spin text-brand" />
</div>
) : (
<>
{isDiffOpen && (
<div className="w-full h-full">
<DiffEditor
language="pgsql"
original={defaultSqlDiff.original}
modified={defaultSqlDiff.modified}
onMount={(editor) => {
diffEditorRef.current = editor
setIsDiffEditorMounted(true)
}}
/>
{showWidget && (
<ResizableAIWidget
editor={diffEditorRef.current!}
id="ask-ai-diff"
value={promptInput}
onChange={setPromptInput}
onSubmit={(prompt: string) => {
handlePrompt(prompt, {
beforeSelection: promptState.beforeSelection,
selection: promptState.selection || defaultSqlDiff.modified,
afterSelection: promptState.afterSelection,
})
}}
onAccept={acceptAiHandler}
onReject={discardAiHandler}
onCancel={resetPrompt}
isDiffVisible={true}
isLoading={isCompletionLoading}
startLineNumber={Math.max(0, promptState.startLineNumber)}
endLineNumber={promptState.endLineNumber}
/>
)}
</div>
)}
<div key={id} className="w-full h-full relative">
<MonacoEditor
autoFocus
placeholder={
!promptState.isOpen && !editorRef.current?.getValue()
? 'Hit ' +
(os === 'macos' ? 'CMD+SHIFT+K' : `CTRL+SHIFT+K`) +
' to generate query or just start typing'
: ''
}
id={id}
snippetName={
urlId === 'new'
? generatedNewSnippetName
: (snapV2.snippets[id]?.snippet.name ?? generatedNewSnippetName)
}
className={cn(isDiffOpen && 'hidden')}
editorRef={editorRef}
monacoRef={monacoRef}
executeQuery={executeQuery}
executeExplainQuery={executeExplainQuery}
showExplainAction={!disablePrettyExplain}
prettifyQuery={prettifyQuery}
onHasSelection={setHasSelection}
onMount={onMount}
onPrompt={({
selection,
beforeSelection,
afterSelection,
startLineNumber,
endLineNumber,
}) => {
setPromptState((prev) => ({
...prev,
isOpen: true,
selection,
beforeSelection,
afterSelection,
startLineNumber,
endLineNumber,
}))
}}
/>
{editorRef.current && promptState.isOpen && !isDiffOpen && (
<ResizableAIWidget
editor={editorRef.current}
id="ask-ai"
value={promptInput}
onChange={setPromptInput}
onSubmit={(prompt: string) => {
handlePrompt(prompt, {
beforeSelection: promptState.beforeSelection,
selection: promptState.selection,
afterSelection: promptState.afterSelection,
})
}}
onCancel={resetPrompt}
isDiffVisible={false}
isLoading={isCompletionLoading}
startLineNumber={Math.max(0, promptState.startLineNumber)}
endLineNumber={promptState.endLineNumber}
/>
)}
</div>
</>
)
return (
<>
<RunQueryWarningModal
@@ -914,140 +997,35 @@ export const SQLEditor = () => {
}}
/>
<div className="flex h-full">
<ResizablePanelGroup
className="relative"
orientation="vertical"
autoSaveId={LOCAL_STORAGE_KEYS.SQL_EDITOR_SPLIT_SIZE}
>
<ResizablePanel defaultSize="50" maxSize="70">
<div className="grow overflow-y-auto border-b h-full">
{isLoading ? (
<div className="flex h-full w-full items-center justify-center">
<Loader2 className="animate-spin text-brand" />
</div>
) : (
<>
{isDiffOpen && (
<div className="w-full h-full">
<DiffEditor
language="pgsql"
original={defaultSqlDiff.original}
modified={defaultSqlDiff.modified}
onMount={(editor) => {
diffEditorRef.current = editor
setIsDiffEditorMounted(true)
}}
/>
{showWidget && (
<ResizableAIWidget
editor={diffEditorRef.current!}
id="ask-ai-diff"
value={promptInput}
onChange={setPromptInput}
onSubmit={(prompt: string) => {
handlePrompt(prompt, {
beforeSelection: promptState.beforeSelection,
selection: promptState.selection || defaultSqlDiff.modified,
afterSelection: promptState.afterSelection,
})
}}
onAccept={acceptAiHandler}
onReject={discardAiHandler}
onCancel={resetPrompt}
isDiffVisible={true}
isLoading={isCompletionLoading}
startLineNumber={Math.max(0, promptState.startLineNumber)}
endLineNumber={promptState.endLineNumber}
/>
)}
</div>
)}
<div key={id} className="w-full h-full relative">
<MonacoEditor
autoFocus
placeholder={
!promptState.isOpen && !editorRef.current?.getValue()
? 'Hit ' +
(os === 'macos' ? 'CMD+SHIFT+K' : `CTRL+SHIFT+K`) +
' to generate query or just start typing'
: ''
}
id={id}
snippetName={
urlId === 'new'
? generatedNewSnippetName
: (snapV2.snippets[id]?.snippet.name ?? generatedNewSnippetName)
}
className={cn(isDiffOpen && 'hidden')}
editorRef={editorRef}
monacoRef={monacoRef}
executeQuery={executeQuery}
executeExplainQuery={executeExplainQuery}
showExplainAction={!disablePrettyExplain}
prettifyQuery={prettifyQuery}
onHasSelection={setHasSelection}
onMount={onMount}
onPrompt={({
selection,
beforeSelection,
afterSelection,
startLineNumber,
endLineNumber,
}) => {
setPromptState((prev) => ({
...prev,
isOpen: true,
selection,
beforeSelection,
afterSelection,
startLineNumber,
endLineNumber,
}))
}}
/>
{editorRef.current && promptState.isOpen && !isDiffOpen && (
<ResizableAIWidget
editor={editorRef.current}
id="ask-ai"
value={promptInput}
onChange={setPromptInput}
onSubmit={(prompt: string) => {
handlePrompt(prompt, {
beforeSelection: promptState.beforeSelection,
selection: promptState.selection,
afterSelection: promptState.afterSelection,
})
}}
onCancel={resetPrompt}
isDiffVisible={false}
isLoading={isCompletionLoading}
startLineNumber={Math.max(0, promptState.startLineNumber)}
endLineNumber={promptState.endLineNumber}
/>
)}
</div>
</>
)}
</div>
</ResizablePanel>
<div className="flex h-full flex-col">
{!isLoading && (
<SqlEditorQueryBar
id={id}
isExecuting={isExecuting}
isDisabled={isDiffOpen}
hasSelection={hasSelection}
prettifyQuery={prettifyQuery}
executeQuery={executeQueryFromButton}
/>
)}
{showUtilityPanel ? (
<ResizablePanelGroup
className="relative min-h-0 flex-1"
orientation="vertical"
autoSaveId={LOCAL_STORAGE_KEYS.SQL_EDITOR_SPLIT_SIZE}
>
<ResizablePanel defaultSize="50" maxSize="70">
<div className="grow overflow-y-auto h-full">{sqlEditorWorkspace}</div>
</ResizablePanel>
<ResizableHandle withHandle />
<ResizableHandle withHandle />
<ResizablePanel defaultSize="50" maxSize="70">
{isLoading ? (
<div className="flex h-full w-full items-center justify-center">
<Loader2 className="animate-spin text-brand" />
</div>
) : (
<ResizablePanel defaultSize="50" maxSize="70">
<UtilityPanel
id={id}
isExecuting={isExecuting}
isExplainExecuting={isExplainExecuting}
isDisabled={isDiffOpen}
hasSelection={hasSelection}
prettifyQuery={prettifyQuery}
executeQuery={executeQueryFromButton}
executeExplainQuery={executeExplainQuery}
showExplainTab={!disablePrettyExplain}
onDebug={onDebug}
@@ -1055,66 +1033,11 @@ export const SQLEditor = () => {
activeTab={activeUtilityTab}
onActiveTabChange={setActiveUtilityTab}
/>
)}
</ResizablePanel>
<div className="h-9">
{results?.rows !== undefined && !isExecuting && (
<GridFooter className="flex items-center justify-between gap-2">
<Tooltip>
<TooltipTrigger>
<p className="text-xs">
<span className="text-foreground">
{results.rows.length} row{results.rows.length > 1 ? 's' : ''}
</span>
<span className="text-foreground-lighter ml-1">
{results.autoLimit !== undefined &&
` (Limited to only ${results.autoLimit} rows)`}
</span>
</p>
</TooltipTrigger>
<TooltipContent className="max-w-xs">
<p className="flex flex-col gap-y-1">
<span>
Results are automatically limited to preserve browser performance, in
particular if your query returns an exceptionally large number of rows.
</span>
<span className="text-foreground-light">
You may change or remove this limit from the dropdown on the right
</span>
</p>
</TooltipContent>
</Tooltip>
{results.autoLimit !== undefined && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="default" iconRight={<ChevronUp size={14} />}>
Limit results to:{' '}
{
ROWS_PER_PAGE_OPTIONS.find((opt) => opt.value === sessionSnap.limit)
?.label
}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-40" align="end">
<DropdownMenuRadioGroup
value={sessionSnap.limit.toString()}
onValueChange={(val) => sessionSnap.setLimit(Number(val))}
>
{ROWS_PER_PAGE_OPTIONS.map((option) => (
<DropdownMenuRadioItem key={option.label} value={option.value.toString()}>
{option.label}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
)}
</GridFooter>
)}
</div>
</ResizablePanelGroup>
</ResizablePanel>
</ResizablePanelGroup>
) : (
<div className="relative min-h-0 flex-1 overflow-y-auto">{sqlEditorWorkspace}</div>
)}
</div>
</>
)
@@ -0,0 +1,145 @@
import { Hotkey } from '@tanstack/react-hotkeys'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { AlignLeft, Check, Heart, Keyboard, MoreVertical } from 'lucide-react'
import { toast } from 'sonner'
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
KeyboardShortcut,
} from 'ui'
import { SqlRunButton } from './UtilityPanel/RunButton'
import SavingIndicator from './UtilityPanel/SavingIndicator'
import { SqlEditorLimitSelector } from './UtilityPanel/SqlEditorLimitSelector'
import { RoleImpersonationPopover } from '@/components/interfaces/RoleImpersonationSelector/RoleImpersonationPopover'
import { DatabaseSelector } from '@/components/ui/DatabaseSelector'
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
import { IS_PLATFORM } from '@/lib/constants'
import { hotkeyToKeys } from '@/state/shortcuts/formatShortcut'
import { SHORTCUT_DEFINITIONS, SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { useSqlEditorV2StateSnapshot } from '@/state/sql-editor-v2'
export type SqlEditorQueryBarProps = {
id: string
isExecuting?: boolean
isDisabled?: boolean
hasSelection?: boolean
prettifyQuery: () => void
executeQuery: () => void
}
export function SqlEditorQueryBar({
id,
isExecuting = false,
isDisabled = false,
hasSelection = false,
prettifyQuery,
executeQuery,
}: SqlEditorQueryBarProps) {
const { ref } = useParams()
const snapV2 = useSqlEditorV2StateSnapshot()
const [intelliSenseEnabled, setIntellisenseEnabled] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.SQL_EDITOR_INTELLISENSE,
true
)
const [lastSelectedDb, setLastSelectedDb] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.SQL_EDITOR_LAST_SELECTED_DB(ref as string),
''
)
const snippet = snapV2.snippets[id]
const isFavorite = snippet !== undefined ? snippet.snippet.favorite : false
const hotkeySequnece: Hotkey | undefined =
SHORTCUT_DEFINITIONS[SHORTCUT_IDS.SQL_EDITOR_FORMAT].sequence[0]
const formatKeys = hotkeySequnece ? hotkeyToKeys(hotkeySequnece) : undefined
const toggleIntellisense = () => {
setIntellisenseEnabled(!intelliSenseEnabled)
toast.success(
`Successfully ${intelliSenseEnabled ? 'disabled' : 'enabled'} intellisense. ${intelliSenseEnabled ? 'Please refresh your browser for changes to take place.' : ''}`
)
}
const toggleFavorite = () => {
if (isFavorite) snapV2.removeFavorite(id)
else snapV2.addFavorite(id)
}
const onSelectDatabase = (databaseId: string) => {
snapV2.resetResults(id)
setLastSelectedDb(databaseId)
}
return (
<div className="flex h-10 shrink-0 items-center justify-between border-b bg-dash-sidebar px-2 dark:bg-surface-100">
<div className="flex min-w-0 items-center">{IS_PLATFORM && <SavingIndicator id={id} />}</div>
<div className="flex items-center gap-x-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
data-testid="sql-editor-utility-actions"
variant="default"
icon={<MoreVertical />}
className="h-[26px] w-[26px]"
aria-label="Query options"
/>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-52">
<DropdownMenuItem className="justify-between" onClick={prettifyQuery}>
<span className="flex items-center gap-x-2">
<AlignLeft size={14} strokeWidth={2} className="text-foreground-light" />
Prettify SQL
</span>
{formatKeys && <KeyboardShortcut keys={formatKeys} />}
</DropdownMenuItem>
{IS_PLATFORM && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem className="gap-x-2" onClick={toggleFavorite}>
<Heart
size={14}
strokeWidth={2}
className={
isFavorite ? 'fill-brand stroke-none' : 'fill-none stroke-foreground-light'
}
/>
{isFavorite ? 'Remove from' : 'Add to'} favorites
</DropdownMenuItem>
</>
)}
<DropdownMenuSeparator />
<DropdownMenuItem className="justify-between" onClick={toggleIntellisense}>
<span className="flex items-center gap-x-2">
<Keyboard size={14} className="text-foreground-light" />
IntelliSense enabled
</span>
{intelliSenseEnabled && <Check className="text-brand" size={16} />}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{IS_PLATFORM && (
<DatabaseSelector
selectedDatabaseId={lastSelectedDb.length === 0 ? undefined : lastSelectedDb}
onSelectId={onSelectDatabase}
/>
)}
<RoleImpersonationPopover serviceRoleLabel="postgres" header="Run SQL query as a role" />
<SqlEditorLimitSelector />
<SqlRunButton
hasSelection={hasSelection}
isDisabled={isDisabled || isExecuting}
isExecuting={isExecuting}
onClick={executeQuery}
/>
</div>
</div>
)
}
@@ -1,5 +1,8 @@
import { Loader2 } from 'lucide-react'
import { Button, KeyboardShortcut } from 'ui'
import { CirclePlay, Loader2 } from 'lucide-react'
import { Button } from 'ui'
import { ShortcutTooltip } from '@/components/ui/ShortcutTooltip'
import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
interface SqlRunButtonProps {
isDisabled?: boolean
@@ -17,22 +20,27 @@ export const SqlRunButton = ({
onClick,
}: SqlRunButtonProps) => {
return (
<Button
onClick={onClick}
disabled={isDisabled}
variant="primary"
size="tiny"
data-testid="sql-run-button"
iconRight={
isExecuting ? (
<Loader2 className="animate-spin" size={10} strokeWidth={1.5} />
) : (
<KeyboardShortcut keys={['Meta', 'Enter']} variant="inline" />
)
}
className={className}
<ShortcutTooltip
shortcutId={SHORTCUT_IDS.SQL_EDITOR_RUN}
label={hasSelection ? 'Run selected' : undefined}
>
{hasSelection ? 'Run selected' : 'Run'}
</Button>
<Button
onClick={onClick}
disabled={isDisabled}
variant="primary"
size="tiny"
data-testid="sql-run-button"
icon={
isExecuting ? (
<Loader2 className="animate-spin" size={10} strokeWidth={1.5} />
) : (
<CirclePlay size={12} strokeWidth={1.5} />
)
}
className={className}
>
{hasSelection ? 'Run selected' : 'Run'}
</Button>
</ShortcutTooltip>
)
}
@@ -0,0 +1,58 @@
import { ChevronDown } from 'lucide-react'
import {
Button,
cn,
DropdownMenu,
DropdownMenuContent,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from 'ui'
import { ROWS_PER_PAGE_OPTIONS } from '@/components/interfaces/SQLEditor/SQLEditor.constants'
import { useSqlEditorSessionSnapshot } from '@/state/sql-editor/sql-editor-session-state'
interface SqlEditorLimitSelectorProps {
variant?: 'regular' | 'connected-on-right' | 'connected-on-both'
}
export function SqlEditorLimitSelector({ variant = 'regular' }: SqlEditorLimitSelectorProps) {
const sessionSnap = useSqlEditorSessionSnapshot()
const selectedLabel =
ROWS_PER_PAGE_OPTIONS.find((opt) => opt.value === sessionSnap.limit)?.label ?? '100 rows'
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="button"
size="tiny"
variant="default"
className={cn(
'h-[26px] justify-start gap-0 pr-3',
variant === 'connected-on-right' && 'rounded-r-none border-r-0',
variant === 'connected-on-both' && 'rounded-none border-x-0'
)}
>
<div className="flex items-center gap-1">
<span className="text-foreground-muted">Limit</span>
<span>{selectedLabel}</span>
<ChevronDown className="text-muted" strokeWidth={1} size={12} />
</div>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-40" align="end">
<DropdownMenuRadioGroup
value={sessionSnap.limit.toString()}
onValueChange={(val) => sessionSnap.setLimit(Number(val))}
>
{ROWS_PER_PAGE_OPTIONS.map((option) => (
<DropdownMenuRadioItem key={option.label} value={option.value.toString()}>
{option.label}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
)
}
@@ -0,0 +1,39 @@
import { Tooltip, TooltipContent, TooltipTrigger } from 'ui'
interface SqlResultsSummaryProps {
rowCount: number
autoLimit?: number
}
export function SqlResultsSummary({ rowCount, autoLimit }: SqlResultsSummaryProps) {
return (
<p className="flex min-w-0 items-center truncate text-xs text-foreground-lighter">
{autoLimit !== undefined ? (
<Tooltip>
<TooltipTrigger asChild>
<span className="truncate text-foreground">
{rowCount} row{rowCount > 1 ? 's' : ''}
<span className="text-foreground-lighter ml-1">
{` (Limited to only ${autoLimit} rows)`}
</span>
</span>
</TooltipTrigger>
<TooltipContent className="max-w-xs">
<p className="flex flex-col gap-y-1">
<span>
Results are limited to preserve browser performance when a query returns many rows.
</span>
<span className="text-foreground-light">
Change or remove this limit from the dropdown near Run.
</span>
</p>
</TooltipContent>
</Tooltip>
) : (
<span className="truncate text-foreground">
{rowCount} row{rowCount > 1 ? 's' : ''}
</span>
)}
</p>
)
}
@@ -1,232 +0,0 @@
import { Hotkey } from '@tanstack/react-hotkeys'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { AlignLeft, Check, Heart, Keyboard, MoreVertical } from 'lucide-react'
import { toast } from 'sonner'
import {
Button,
cn,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
KeyboardShortcut,
Tooltip,
TooltipContent,
TooltipTrigger,
} from 'ui'
import { SqlRunButton } from './RunButton'
import SavingIndicator from './SavingIndicator'
import { RoleImpersonationPopover } from '@/components/interfaces/RoleImpersonationSelector/RoleImpersonationPopover'
import { DatabaseSelector } from '@/components/ui/DatabaseSelector'
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
import { IS_PLATFORM } from '@/lib/constants'
import { hotkeyToKeys } from '@/state/shortcuts/formatShortcut'
import { SHORTCUT_DEFINITIONS, SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { useSqlEditorV2StateSnapshot } from '@/state/sql-editor-v2'
import { useSqlEditorSessionSnapshot } from '@/state/sql-editor/sql-editor-session-state'
export type UtilityActionsProps = {
id: string
isExecuting?: boolean
isDisabled?: boolean
hasSelection?: boolean
prettifyQuery: () => void
executeQuery: () => void
}
export const UtilityActions = ({
id,
isExecuting = false,
isDisabled = false,
hasSelection = false,
prettifyQuery,
executeQuery,
}: UtilityActionsProps) => {
const { ref } = useParams()
const snapV2 = useSqlEditorV2StateSnapshot()
const sessionSnap = useSqlEditorSessionSnapshot()
const [isAiOpen] = useLocalStorageQuery(LOCAL_STORAGE_KEYS.SQL_EDITOR_AI_OPEN, true)
const [intellisenseEnabled, setIntellisenseEnabled] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.SQL_EDITOR_INTELLISENSE,
true
)
const [lastSelectedDb, setLastSelectedDb] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.SQL_EDITOR_LAST_SELECTED_DB(ref as string),
''
)
const snippet = snapV2.snippets[id]
const isFavorite = snippet !== undefined ? snippet.snippet.favorite : false
const hotkeySequnece: Hotkey | undefined =
SHORTCUT_DEFINITIONS[SHORTCUT_IDS.SQL_EDITOR_FORMAT].sequence[0]
const formatKeys = hotkeySequnece ? hotkeyToKeys(hotkeySequnece) : undefined
const toggleIntellisense = () => {
setIntellisenseEnabled(!intellisenseEnabled)
toast.success(
`Successfully ${intellisenseEnabled ? 'disabled' : 'enabled'} intellisense. ${intellisenseEnabled ? 'Please refresh your browser for changes to take place.' : ''}`
)
}
const addFavorite = () => snapV2.addFavorite(id)
const removeFavorite = () => snapV2.removeFavorite(id)
const onSelectDatabase = (databaseId: string) => {
sessionSnap.resetResults(id)
setLastSelectedDb(databaseId)
}
return (
<div className="inline-flex items-center justify-end gap-x-2">
{IS_PLATFORM && <SavingIndicator id={id} />}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
data-testid="sql-editor-utility-actions"
variant="default"
className={cn('px-1', isAiOpen ? 'block 2xl:hidden' : 'hidden')}
icon={<MoreVertical className="text-foreground-light" />}
/>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-48">
<DropdownMenuItem className="justify-between" onClick={toggleIntellisense}>
<span className="flex items-center gap-x-2">
<Keyboard size={14} className="text-foreground-light" />
Intellisense enabled
</span>
{intellisenseEnabled && <Check className="text-brand" size={16} />}
</DropdownMenuItem>
{IS_PLATFORM && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem
className="gap-x-2"
onClick={() => {
if (isFavorite) removeFavorite()
else addFavorite()
}}
>
<Heart
size={14}
strokeWidth={2}
className={
isFavorite ? 'fill-brand stroke-none' : 'fill-none stroke-foreground-light'
}
/>
{isFavorite ? 'Remove from' : 'Add to'} favorites
</DropdownMenuItem>
</>
)}
<DropdownMenuItem className="justify-between" onClick={prettifyQuery}>
<span className="flex items-center gap-x-2">
<AlignLeft size={14} strokeWidth={2} className="text-foreground-light" />
Prettify SQL
</span>
{formatKeys && <KeyboardShortcut keys={formatKeys} />}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<div className={cn('items-center gap-x-2', isAiOpen ? 'hidden 2xl:flex' : 'flex')}>
<DropdownMenu>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<Button
variant="text"
className="px-1"
icon={<Keyboard className="text-foreground-light" />}
aria-label="Enable Intellisense"
/>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent side="bottom">Enable Intellisense</TooltipContent>
</Tooltip>
<DropdownMenuContent className="w-48">
<DropdownMenuItem className="justify-between" onClick={toggleIntellisense}>
Intellisense enabled
{intellisenseEnabled && <Check className="text-brand" size={16} />}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
{IS_PLATFORM && (
<Tooltip>
<TooltipTrigger asChild>
{isFavorite ? (
<Button
variant="text"
size="tiny"
onClick={removeFavorite}
className="px-1"
icon={<Heart className="fill-brand stroke-none" />}
aria-label="Remove from favorites"
/>
) : (
<Button
variant="text"
size="tiny"
onClick={addFavorite}
className="px-1"
icon={<Heart className="fill-none stroke-foreground-light" />}
aria-label="Add to favorites"
/>
)}
</TooltipTrigger>
<TooltipContent side="bottom">
{isFavorite ? 'Remove from' : 'Add to'} favorites
</TooltipContent>
</Tooltip>
)}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="text"
onClick={prettifyQuery}
className="px-1"
icon={<AlignLeft strokeWidth={2} className="text-foreground-light" />}
aria-label="Prettify SQL"
/>
</TooltipTrigger>
<TooltipContent side="bottom" className="p-1 pl-2.5">
<div className="flex items-center gap-2.5">
<span>Prettify SQL</span>
{formatKeys && <KeyboardShortcut keys={formatKeys} />}
</div>
</TooltipContent>
</Tooltip>
</div>
<div className="flex items-center justify-between gap-x-2">
<div className="flex items-center">
{IS_PLATFORM && (
<DatabaseSelector
selectedDatabaseId={lastSelectedDb.length === 0 ? undefined : lastSelectedDb}
variant="connected-on-right"
onSelectId={onSelectDatabase}
/>
)}
<RoleImpersonationPopover
serviceRoleLabel="postgres"
header="Run SQL query as a role"
variant={IS_PLATFORM ? 'connected-on-both' : 'connected-on-right'}
/>
<SqlRunButton
hasSelection={hasSelection}
isDisabled={isDisabled || isExecuting}
isExecuting={isExecuting}
className="rounded-l-none"
onClick={executeQuery}
/>
</div>
</div>
</div>
)
}
@@ -3,7 +3,7 @@ import { toast } from 'sonner'
import { Tabs_Shadcn_, TabsContent_Shadcn_, TabsList_Shadcn_, TabsTrigger_Shadcn_ } from 'ui'
import { ChartConfig } from './ChartConfig'
import { UtilityActions } from './UtilityActions'
import { SqlResultsSummary } from './SqlResultsSummary'
import { UtilityTabExplain } from './UtilityTabExplain'
import { UtilityTabResults } from './UtilityTabResults'
import { DownloadResultsButton } from '@/components/ui/DownloadResultsButton'
@@ -19,9 +19,6 @@ export type UtilityPanelProps = {
isExplainExecuting?: boolean
isDebugging?: boolean
isDisabled?: boolean
hasSelection: boolean
prettifyQuery: () => void
executeQuery: () => void
executeExplainQuery: () => void
showExplainTab?: boolean
onDebug: () => void
@@ -45,9 +42,6 @@ export const UtilityPanel = ({
isExplainExecuting,
isDebugging,
isDisabled,
hasSelection,
prettifyQuery,
executeQuery,
executeExplainQuery,
showExplainTab = true,
onDebug,
@@ -62,6 +56,11 @@ export const UtilityPanel = ({
const snippet = snapV2.snippets[id]?.snippet
const result = sessionSnap.results[id]?.[0]
const showResultsSummary =
(activeTab === 'results' || activeTab === 'chart') &&
result?.rows !== undefined &&
!isExecuting &&
!result.error
const handleTabChange = (tab: string) => {
// When switching to the explain tab, trigger the explain query
@@ -135,8 +134,8 @@ export const UtilityPanel = ({
onValueChange={handleTabChange}
className="w-full h-full flex flex-col"
>
<TabsList_Shadcn_ className="flex justify-between gap-2 px-4 overflow-x-auto min-h-[42px]">
<div className="flex items-center gap-4">
<div className="flex min-h-[42px] shrink-0 items-center justify-between gap-2 overflow-x-auto border-b pl-3 pr-2">
<TabsList_Shadcn_ className="h-auto gap-4 border-b-0 p-0">
<TabsTrigger_Shadcn_ className="py-3 text-xs" value="results">
<span className="translate-y-px">Results</span>
</TabsTrigger_Shadcn_>
@@ -148,12 +147,18 @@ export const UtilityPanel = ({
<TabsTrigger_Shadcn_ className="py-3 text-xs" value="chart">
<span className="translate-y-px">Chart</span>
</TabsTrigger_Shadcn_>
</TabsList_Shadcn_>
<div className="flex min-w-0 items-center gap-x-3">
{showResultsSummary && (
<SqlResultsSummary rowCount={result.rows.length} autoLimit={result.autoLimit} />
)}
{result?.rows && (
<DownloadResultsButton
variant="text"
results={result.rows as any[]}
fileName={`Supabase Snippet ${snippet.name}`}
align="end"
onDownloadAsCSV={() => track('sql_editor_result_download_csv_clicked')}
onCopyAsMarkdown={() => track('sql_editor_result_copy_markdown_clicked')}
onCopyAsJSON={() => track('sql_editor_result_copy_json_clicked')}
@@ -161,16 +166,7 @@ export const UtilityPanel = ({
/>
)}
</div>
<UtilityActions
id={id}
isExecuting={isExecuting}
isDisabled={isDisabled}
hasSelection={hasSelection}
prettifyQuery={prettifyQuery}
executeQuery={executeQuery}
/>
</TabsList_Shadcn_>
</div>
<TabsContent_Shadcn_ asChild value="results" className="mt-0 grow">
<UtilityTabResults
@@ -1,7 +1,7 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useDebounce } from '@uidotdev/usehooks'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import { FilePlus, FolderPlus, Plus, X } from 'lucide-react'
import { ChevronDown, FolderPlus, List, Plus, X } from 'lucide-react'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import { toast } from 'sonner'
@@ -45,7 +45,6 @@ export const SQLEditorMenu = () => {
'inserted_at'
)
const appState = getAppStateSnapshot()
const debouncedSearch = useDebounce(search, 500)
const { can: canCreateSQLSnippet } = useAsyncCheckPermissions(
@@ -80,95 +79,100 @@ export const SQLEditorMenu = () => {
}
}
const openRunningQueries = () => {
getAppStateSnapshot().setOnGoingQueriesPanelOpen(true)
}
useEffect(() => {
setShowSearch(debouncedSearch.length > 0)
}, [debouncedSearch])
return (
<div className="h-full flex flex-col justify-between">
<div className="flex flex-col gap-y-4 grow">
<div className="mt-4 mx-4 flex items-center justify-between gap-x-2">
<InnerSideBarFilters className="w-full p-0 gap-0">
<InnerSideBarFilterSearchInput
name="search-queries"
placeholder="Search queries..."
aria-labelledby="Search queries"
value={search}
onChange={(e) => {
const value = e.target.value
setSearch(value)
if (value.length === 0) setShowSearch(false)
}}
onKeyDown={(e) => {
if (e.code === 'Escape') {
setSearch('')
setShowSearch(false)
}
}}
>
{showSearch ? (
<Tooltip>
<TooltipTrigger
className="absolute right-1 top-[.4rem] md:top-[.3rem] transition-colors text-foreground-light hover:text-foreground"
onClick={() => {
setSearch('')
setShowSearch(false)
}}
>
<X size={18} />
</TooltipTrigger>
<TooltipContent>Clear search</TooltipContent>
</Tooltip>
) : (
<InnerSideBarFilterSortDropdown
value={sort}
onValueChange={(value: any) => setSort(value)}
>
<InnerSideBarFilterSortDropdownItem key="name" value="name">
Alphabetical
</InnerSideBarFilterSortDropdownItem>
<InnerSideBarFilterSortDropdownItem key="inserted_at" value="inserted_at">
Created At
</InnerSideBarFilterSortDropdownItem>
</InnerSideBarFilterSortDropdown>
)}
</InnerSideBarFilterSearchInput>
</InnerSideBarFilters>
<DropdownMenu>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<Button
data-testid="sql-editor-new-query-button"
variant="default"
icon={<Plus className="text-foreground" />}
className="w-[26px]"
aria-label="Create a new query"
/>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent side="bottom">Create a new query</TooltipContent>
</Tooltip>
<DropdownMenuContent align="end" side="bottom" className="w-48">
<DropdownMenuItem className="gap-x-2" onClick={() => handleNewQuery()}>
<FilePlus size={14} />
Create a new snippet
</DropdownMenuItem>
<DropdownMenuItem className="gap-x-2" onClick={() => createNewFolder()}>
<FolderPlus size={14} />
Create a new folder
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
{showSearch ? <SearchList search={debouncedSearch} /> : <SQLEditorNav sort={sort} />}
<div className="flex h-full grow flex-col gap-2 pt-5">
<div className="mx-4 flex min-w-0 shrink-0 items-center">
<Button
data-testid="sql-editor-new-query-button"
type="button"
size="tiny"
variant="default"
className="h-7 min-w-0 flex-1 justify-start rounded-r-none hover:z-10"
icon={<Plus size={14} strokeWidth={1.5} className="text-foreground-muted" />}
onClick={handleNewQuery}
>
New query
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="button"
variant="default"
aria-label="More query actions"
className="h-7 w-7 shrink-0 rounded-l-none -ml-px px-0"
icon={<ChevronDown size={14} strokeWidth={1.5} />}
/>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" side="bottom" className="w-52">
<DropdownMenuItem className="gap-x-2" onClick={createNewFolder}>
<FolderPlus size={14} />
New folder
</DropdownMenuItem>
<DropdownMenuItem className="gap-x-2" onClick={openRunningQueries}>
<List size={14} />
View running queries
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="p-4 border-t sticky bottom-0 bg-studio">
<Button block variant="default" onClick={() => appState.setOnGoingQueriesPanelOpen(true)}>
View running queries
</Button>
<div className="flex min-h-0 grow flex-col gap-2 pb-4">
<InnerSideBarFilters className="mx-2">
<InnerSideBarFilterSearchInput
name="search-queries"
placeholder="Search queries..."
aria-labelledby="Search queries"
value={search}
onChange={(e) => {
const value = e.target.value
setSearch(value)
if (value.length === 0) setShowSearch(false)
}}
onKeyDown={(e) => {
if (e.code === 'Escape') {
setSearch('')
setShowSearch(false)
}
}}
>
{showSearch ? (
<Tooltip>
<TooltipTrigger
className="absolute right-1 top-[.4rem] transition-colors text-foreground-light hover:text-foreground md:top-[.3rem]"
onClick={() => {
setSearch('')
setShowSearch(false)
}}
>
<X size={18} />
</TooltipTrigger>
<TooltipContent>Clear search</TooltipContent>
</Tooltip>
) : (
<InnerSideBarFilterSortDropdown
value={sort}
onValueChange={(value: 'name' | 'inserted_at') => setSort(value)}
>
<InnerSideBarFilterSortDropdownItem key="name" value="name">
Alphabetical
</InnerSideBarFilterSortDropdownItem>
<InnerSideBarFilterSortDropdownItem key="inserted_at" value="inserted_at">
Created At
</InnerSideBarFilterSortDropdownItem>
</InnerSideBarFilterSortDropdown>
)}
</InnerSideBarFilterSearchInput>
</InnerSideBarFilters>
{showSearch ? <SearchList search={debouncedSearch} /> : <SQLEditorNav sort={sort} />}
</div>
</div>
)
@@ -1,6 +1,5 @@
import { keepPreviousData } from '@tanstack/react-query'
import { IS_PLATFORM, LOCAL_STORAGE_KEYS, useParams } from 'common'
import { Heart } from 'lucide-react'
import { useRouter } from 'next/router'
import { useEffect, useMemo, useState } from 'react'
import { toast } from 'sonner'
@@ -559,9 +558,8 @@ export const SQLEditorNav = ({ sort = 'inserted_at' }: SQLEditorNavProps) => {
className="mx-2 px-3"
description={
<>
Save a query to favorites for easy accessibility by clicking the{' '}
<Heart size={12} className="inline-block relative align-center -top-px" />{' '}
icon.
Save a query to favorites from the query options menu in the editor, or by
right-clicking a query in the sidebar.
</>
}
/>
+43 -24
View File
@@ -80,15 +80,27 @@ export const DatabaseSelector = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [_selectedDatabaseId])
const selectedDatabaseLabel = selectedAdditionalOption ? (
<span>{selectedAdditionalOption.name}</span>
) : (
<>
<span className="capitalize">
{isLoading || selectedDatabase?.identifier === projectRef
? 'Primary database'
: 'Read replica'}
</span>
{isSuccess && selectedDatabase?.identifier !== projectRef && (
<span>
({selectedDatabaseRegion} - {formattedDatabaseId})
</span>
)}
</>
)
return (
<Popover open={open} onOpenChange={setOpen} modal={false}>
<PopoverTrigger asChild>
<div className={cn('flex cursor-pointer', className)}>
{!isForm && (
<span className="flex items-center text-foreground-lighter px-3 rounded-lg rounded-r-none text-xs border border-button border-r-0">
Source
</span>
)}
{isForm ? (
<Button
variant="default"
icon={isLoading && <Loader2 className="animate-spin" />}
@@ -96,31 +108,38 @@ export const DatabaseSelector = ({
{...buttonProps}
className={cn(
'justify-start',
!isForm && 'rounded-l-none',
variant === 'connected-on-right' && 'rounded-r-none',
variant === 'connected-on-left' && 'rounded-l-none border-l-0',
variant === 'connected-on-both' && 'rounded-none border-x-0',
className,
buttonProps?.className
)}
>
{selectedAdditionalOption ? (
<span>{selectedAdditionalOption.name}</span>
) : (
<>
<span className="capitalize">
{isLoading || selectedDatabase?.identifier === projectRef
? 'Primary database'
: 'Read replica'}
</span>{' '}
{isSuccess && selectedDatabase?.identifier !== projectRef && (
<span>
({selectedDatabaseRegion} - {formattedDatabaseId})
</span>
)}
</>
)}
{selectedDatabaseLabel}
</Button>
</div>
) : (
<Button
size="tiny"
variant="default"
{...buttonProps}
className={cn(
'h-[26px] justify-start gap-0 pr-3',
variant === 'connected-on-right' && 'rounded-r-none border-r-0',
variant === 'connected-on-left' && 'rounded-l-none border-l-0',
variant === 'connected-on-both' && 'rounded-none border-x-0',
className,
buttonProps?.className
)}
>
<div className="flex items-center gap-1">
<span className="text-foreground-muted">Source</span>
<span className="flex items-center gap-1">
{isLoading ? <Loader2 className="animate-spin" size={12} /> : selectedDatabaseLabel}
</span>
<ChevronDown className="text-muted" strokeWidth={1} size={12} />
</div>
</Button>
)}
</PopoverTrigger>
<PopoverContent className="p-0 w-64" side="bottom" align={align}>
<Command>
@@ -6,6 +6,7 @@ export const SQL_EDITOR_SHORTCUT_IDS = {
SQL_EDITOR_FORMAT: 'sql-editor.format',
SQL_EDITOR_EXPLAIN: 'sql-editor.explain',
SQL_EDITOR_NEW_SNIPPET: 'sql-editor.new-snippet',
SQL_EDITOR_RUN: 'sql-editor.run',
}
export type SqlEditorShortcutId =
@@ -42,4 +43,10 @@ export const sqlEditorRegistry: RegistryDefinations<SqlEditorShortcutId> = {
sequence: ['Shift+N'],
showInSettings: false,
},
[SQL_EDITOR_SHORTCUT_IDS.SQL_EDITOR_RUN]: {
id: SQL_EDITOR_SHORTCUT_IDS.SQL_EDITOR_RUN,
label: 'Run query',
sequence: ['Mod+Enter'],
showInSettings: false,
},
}