From 302abc5e27ab23c86bf5f986ca2f8c2b07cabc14 Mon Sep 17 00:00:00 2001 From: Danny White <3104761+dnywh@users.noreply.github.com> Date: Tue, 30 Jun 2026 13:18:06 -0600 Subject: [PATCH] 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. --- .../SQLEditor/SQLEditor.constants.ts | 2 +- .../interfaces/SQLEditor/SQLEditor.tsx | 415 +++++++----------- .../SQLEditor/SqlEditorQueryBar.tsx | 145 ++++++ .../SQLEditor/UtilityPanel/RunButton.tsx | 44 +- .../UtilityPanel/SqlEditorLimitSelector.tsx | 58 +++ .../UtilityPanel/SqlResultsSummary.tsx | 39 ++ .../SQLEditor/UtilityPanel/UtilityActions.tsx | 232 ---------- .../SQLEditor/UtilityPanel/UtilityPanel.tsx | 36 +- .../layouts/SQLEditorLayout/SQLEditorMenu.tsx | 172 ++++---- .../SQLEditorNavV2/SQLEditorNav.tsx | 6 +- .../studio/components/ui/DatabaseSelector.tsx | 67 ++- .../state/shortcuts/registry/sql-editor.ts | 7 + 12 files changed, 594 insertions(+), 629 deletions(-) create mode 100644 apps/studio/components/interfaces/SQLEditor/SqlEditorQueryBar.tsx create mode 100644 apps/studio/components/interfaces/SQLEditor/UtilityPanel/SqlEditorLimitSelector.tsx create mode 100644 apps/studio/components/interfaces/SQLEditor/UtilityPanel/SqlResultsSummary.tsx delete mode 100644 apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityActions.tsx diff --git a/apps/studio/components/interfaces/SQLEditor/SQLEditor.constants.ts b/apps/studio/components/interfaces/SQLEditor/SQLEditor.constants.ts index 6d06780a2ae..2738399bd0a 100644 --- a/apps/studio/components/interfaces/SQLEditor/SQLEditor.constants.ts +++ b/apps/studio/components/interfaces/SQLEditor/SQLEditor.constants.ts @@ -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' }, diff --git a/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx b/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx index 6d1f261f253..75b0f255ee0 100644 --- a/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx +++ b/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx @@ -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() 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('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 ? ( +
+ +
+ ) : ( + <> + {isDiffOpen && ( +
+ { + diffEditorRef.current = editor + setIsDiffEditorMounted(true) + }} + /> + {showWidget && ( + { + 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} + /> + )} +
+ )} +
+ { + setPromptState((prev) => ({ + ...prev, + isOpen: true, + selection, + beforeSelection, + afterSelection, + startLineNumber, + endLineNumber, + })) + }} + /> + {editorRef.current && promptState.isOpen && !isDiffOpen && ( + { + 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} + /> + )} +
+ + ) + return ( <> { }} /> -
- - -
- {isLoading ? ( -
- -
- ) : ( - <> - {isDiffOpen && ( -
- { - diffEditorRef.current = editor - setIsDiffEditorMounted(true) - }} - /> - {showWidget && ( - { - 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} - /> - )} -
- )} -
- { - setPromptState((prev) => ({ - ...prev, - isOpen: true, - selection, - beforeSelection, - afterSelection, - startLineNumber, - endLineNumber, - })) - }} - /> - {editorRef.current && promptState.isOpen && !isDiffOpen && ( - { - 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} - /> - )} -
- - )} -
-
+
+ {!isLoading && ( + + )} + {showUtilityPanel ? ( + + +
{sqlEditorWorkspace}
+
- + - - {isLoading ? ( -
- -
- ) : ( + { activeTab={activeUtilityTab} onActiveTabChange={setActiveUtilityTab} /> - )} - - -
- {results?.rows !== undefined && !isExecuting && ( - - - -

- - {results.rows.length} row{results.rows.length > 1 ? 's' : ''} - - - {results.autoLimit !== undefined && - ` (Limited to only ${results.autoLimit} rows)`} - -

-
- -

- - Results are automatically limited to preserve browser performance, in - particular if your query returns an exceptionally large number of rows. - - - - You may change or remove this limit from the dropdown on the right - -

-
-
- {results.autoLimit !== undefined && ( - - - - - - sessionSnap.setLimit(Number(val))} - > - {ROWS_PER_PAGE_OPTIONS.map((option) => ( - - {option.label} - - ))} - - - - )} -
- )} -
-
+ + + ) : ( +
{sqlEditorWorkspace}
+ )}
) diff --git a/apps/studio/components/interfaces/SQLEditor/SqlEditorQueryBar.tsx b/apps/studio/components/interfaces/SQLEditor/SqlEditorQueryBar.tsx new file mode 100644 index 00000000000..c789b5f78ce --- /dev/null +++ b/apps/studio/components/interfaces/SQLEditor/SqlEditorQueryBar.tsx @@ -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 ( +
+
{IS_PLATFORM && }
+ +
+ + +
+
+ ) +} diff --git a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/RunButton.tsx b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/RunButton.tsx index 861481f457d..02c901c61c7 100644 --- a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/RunButton.tsx +++ b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/RunButton.tsx @@ -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 ( - + + ) } diff --git a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/SqlEditorLimitSelector.tsx b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/SqlEditorLimitSelector.tsx new file mode 100644 index 00000000000..e439bb4c7ff --- /dev/null +++ b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/SqlEditorLimitSelector.tsx @@ -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 ( + + + + + + sessionSnap.setLimit(Number(val))} + > + {ROWS_PER_PAGE_OPTIONS.map((option) => ( + + {option.label} + + ))} + + + + ) +} diff --git a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/SqlResultsSummary.tsx b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/SqlResultsSummary.tsx new file mode 100644 index 00000000000..4a4a818e92e --- /dev/null +++ b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/SqlResultsSummary.tsx @@ -0,0 +1,39 @@ +import { Tooltip, TooltipContent, TooltipTrigger } from 'ui' + +interface SqlResultsSummaryProps { + rowCount: number + autoLimit?: number +} + +export function SqlResultsSummary({ rowCount, autoLimit }: SqlResultsSummaryProps) { + return ( +

+ {autoLimit !== undefined ? ( + + + + {rowCount} row{rowCount > 1 ? 's' : ''} + + {` (Limited to only ${autoLimit} rows)`} + + + + +

+ + Results are limited to preserve browser performance when a query returns many rows. + + + Change or remove this limit from the dropdown near Run. + +

+ + + ) : ( + + {rowCount} row{rowCount > 1 ? 's' : ''} + + )} +

+ ) +} diff --git a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityActions.tsx b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityActions.tsx deleted file mode 100644 index 58b7f93131f..00000000000 --- a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityActions.tsx +++ /dev/null @@ -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 ( -
- {IS_PLATFORM && } - - - -
- -
-
- {IS_PLATFORM && ( - - )} - - -
-
-
- ) -} diff --git a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityPanel.tsx b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityPanel.tsx index 98af2875443..4c55266e376 100644 --- a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityPanel.tsx +++ b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityPanel.tsx @@ -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" > - -
+
+ Results @@ -148,12 +147,18 @@ export const UtilityPanel = ({ Chart + + +
+ {showResultsSummary && ( + + )} {result?.rows && ( 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 = ({ /> )}
- - - +
{ '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 ( -
-
-
- - { - const value = e.target.value - setSearch(value) - if (value.length === 0) setShowSearch(false) - }} - onKeyDown={(e) => { - if (e.code === 'Escape') { - setSearch('') - setShowSearch(false) - } - }} - > - {showSearch ? ( - - { - setSearch('') - setShowSearch(false) - }} - > - - - Clear search - - ) : ( - setSort(value)} - > - - Alphabetical - - - Created At - - - )} - - - - - - -
- - {showSearch ? : } +
+
+ + + +
-
- +
+ + { + const value = e.target.value + setSearch(value) + if (value.length === 0) setShowSearch(false) + }} + onKeyDown={(e) => { + if (e.code === 'Escape') { + setSearch('') + setShowSearch(false) + } + }} + > + {showSearch ? ( + + { + setSearch('') + setShowSearch(false) + }} + > + + + Clear search + + ) : ( + setSort(value)} + > + + Alphabetical + + + Created At + + + )} + + + + {showSearch ? : }
) diff --git a/apps/studio/components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorNav.tsx b/apps/studio/components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorNav.tsx index 0d574cf6c1f..3d401883e78 100644 --- a/apps/studio/components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorNav.tsx +++ b/apps/studio/components/layouts/SQLEditorLayout/SQLEditorNavV2/SQLEditorNav.tsx @@ -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{' '} - {' '} - icon. + Save a query to favorites from the query options menu in the editor, or by + right-clicking a query in the sidebar. } /> diff --git a/apps/studio/components/ui/DatabaseSelector.tsx b/apps/studio/components/ui/DatabaseSelector.tsx index 0543f8f87dd..01788bdbf0c 100644 --- a/apps/studio/components/ui/DatabaseSelector.tsx +++ b/apps/studio/components/ui/DatabaseSelector.tsx @@ -80,15 +80,27 @@ export const DatabaseSelector = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [_selectedDatabaseId]) + const selectedDatabaseLabel = selectedAdditionalOption ? ( + {selectedAdditionalOption.name} + ) : ( + <> + + {isLoading || selectedDatabase?.identifier === projectRef + ? 'Primary database' + : 'Read replica'} + + {isSuccess && selectedDatabase?.identifier !== projectRef && ( + + ({selectedDatabaseRegion} - {formattedDatabaseId}) + + )} + + ) + return ( -
- {!isForm && ( - - Source - - )} + {isForm ? ( -
+ ) : ( + + )}
diff --git a/apps/studio/state/shortcuts/registry/sql-editor.ts b/apps/studio/state/shortcuts/registry/sql-editor.ts index c2d91122ed9..e6409505516 100644 --- a/apps/studio/state/shortcuts/registry/sql-editor.ts +++ b/apps/studio/state/shortcuts/registry/sql-editor.ts @@ -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 = { 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, + }, }