diff --git a/apps/studio/components/interfaces/RoleImpersonationSelector/RoleImpersonationPopover.tsx b/apps/studio/components/interfaces/RoleImpersonationSelector/RoleImpersonationPopover.tsx
index 27d10d83561..724232ae8c8 100644
--- a/apps/studio/components/interfaces/RoleImpersonationSelector/RoleImpersonationPopover.tsx
+++ b/apps/studio/components/interfaces/RoleImpersonationSelector/RoleImpersonationPopover.tsx
@@ -13,6 +13,7 @@ export interface RoleImpersonationPopoverProps {
variant?: 'regular' | 'connected-on-right' | 'connected-on-left' | 'connected-on-both'
align?: 'center' | 'start' | 'end'
disallowAuthenticatedOption?: boolean
+ className?: string
}
export const RoleImpersonationPopover = ({
@@ -21,6 +22,7 @@ export const RoleImpersonationPopover = ({
variant = 'regular',
align = 'end',
disallowAuthenticatedOption = false,
+ className,
}: RoleImpersonationPopoverProps) => {
const state = useRoleImpersonationStateSnapshot()
@@ -38,7 +40,8 @@ export const RoleImpersonationPopover = ({
'h-[26px] pr-3 gap-0',
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'
+ variant === 'connected-on-both' && 'rounded-none border-x-0',
+ className
)}
>
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 23dc1fa3575..a4f424a68a8 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 { 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,7 +41,13 @@ import {
isUpdateWithoutWhere,
suffixWithLimit,
} from './SQLEditor.utils'
-import { useSqlQueryTarget } from './SqlWarehouseTargetToggle'
+import { SqlEditorQueryBar } from './SqlEditorQueryBar'
+import { useSqlQueryTarget } from './SqlEditorQueryTargetSelector'
+import {
+ DEFAULT_SQL_EDITOR_SEARCH_PATH,
+ getSqlEditorSearchPathStorageKey,
+} from './SqlEditorSearchPathSelector'
+import { resolveSqlWarehouseResultSource } from './SqlEditorWarehouseDemo'
import { useAddDefinitions } from './useAddDefinitions'
import { UtilityPanel } from './UtilityPanel/UtilityPanel'
import {
@@ -66,7 +57,6 @@ import {
} from '@/components/interfaces/ExplainVisualizer/ExplainVisualizer.utils'
import { SIDEBAR_KEYS } from '@/components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
import ResizableAIWidget from '@/components/ui/AIEditor/ResizableAIWidget'
-import { GridFooter } from '@/components/ui/GridFooter'
import { useSqlTitleGenerateMutation } from '@/data/ai/sql-title-mutation'
import { useDatabaseEventTriggersQuery } from '@/data/database-event-triggers/database-event-triggers-query'
import { constructHeaders, isValidConnString } from '@/data/fetchers'
@@ -74,6 +64,7 @@ import { lintKeys } from '@/data/lint/keys'
import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query'
import { useExecuteSqlMutation } from '@/data/sql/execute-sql-mutation'
import { isError } from '@/data/utils/error-check'
+import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
import { useOrgAiOptInLevel } from '@/hooks/misc/useOrgOptedIntoAi'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
@@ -128,6 +119,10 @@ export const SQLEditor = () => {
const sessionSnap = useSqlEditorSessionSnapshot()
const diffRequest = useSqlEditorDiffRequestSnapshot()
const getImpersonatedRoleState = useGetImpersonatedRoleState()
+ const [searchPath] = useLocalStorageQuery(
+ getSqlEditorSearchPathStorageKey(ref ?? 'unknown'),
+ DEFAULT_SQL_EDITOR_SEARCH_PATH
+ )
const databaseSelectorState = useDatabaseSelectorStateSnapshot()
const { aiOptInLevel } = useOrgAiOptInLevel()
const sqlQueryTarget = useSqlQueryTarget()
@@ -209,7 +204,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
)
@@ -238,7 +232,7 @@ export const SQLEditor = () => {
const { mutate: execute, isPending: isExecuting } = useExecuteSqlMutation({
onSuccess(data, vars) {
if (id) {
- sessionSnap.addResult(id, data.result, vars.autoLimit)
+ sessionSnap.addResult(id, data.result, vars.autoLimit, vars.warehouseResultSource)
if (!disablePrettyExplain && isExplainQuery(data.result)) {
sessionSnap.addExplainResult(id, data.result)
@@ -286,7 +280,7 @@ export const SQLEditor = () => {
}
}
- sessionSnap.addResultError(id, error, vars.autoLimit)
+ sessionSnap.addResultError(id, error, vars.autoLimit, vars.warehouseResultSource)
}
refocusEditorAfterRunIfNeeded()
@@ -436,12 +430,18 @@ export const SQLEditor = () => {
const userSql = rawSql(sql)
const { appendAutoLimit } = checkIfAppendLimitRequired(userSql, limit)
const formattedSql = suffixWithLimit(userSql, limit)
+ const warehouseResultSource = resolveSqlWarehouseResultSource(
+ userSql,
+ searchPath,
+ sqlQueryTarget
+ )
execute({
projectRef: project.ref,
connectionString: connectionString,
sql: wrapWithRoleImpersonation(formattedSql, impersonatedRoleState),
autoLimit: appendAutoLimit ? limit : undefined,
+ warehouseResultSource,
isRoleImpersonationEnabled: isRoleImpersonationEnabled(impersonatedRoleState.role),
isStatementTimeoutDisabled: true,
contextualInvalidation: true,
@@ -467,6 +467,8 @@ export const SQLEditor = () => {
databases,
eventTriggers,
limit,
+ searchPath,
+ sqlQueryTarget,
track,
]
)
@@ -548,7 +550,7 @@ export const SQLEditor = () => {
databaseSelectorState.selectedDatabaseId,
databases,
lineHighlights,
- sessionSnap,
+ snapV2,
])
useShortcut(SHORTCUT_IDS.SQL_EDITOR_EXPLAIN, executeExplainQuery, {
@@ -885,6 +887,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
-
-
-
-
- {sqlQueryTarget === 'warehouse' && (
-
Served by: Warehouse
- )}
-
- {results.autoLimit !== undefined && (
-
-
- }>
- Limit results to:{' '}
- {
- ROWS_PER_PAGE_OPTIONS.find((opt) => opt.value === sessionSnap.limit)
- ?.label
- }
-
-
-
- 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..c88c63482ba
--- /dev/null
+++ b/apps/studio/components/interfaces/SQLEditor/SqlEditorQueryBar.tsx
@@ -0,0 +1,345 @@
+import { Hotkey } from '@tanstack/react-hotkeys'
+import { LOCAL_STORAGE_KEYS, useParams } from 'common'
+import { AlignLeft, ChevronDown, MoreVertical } from 'lucide-react'
+import { useEffect, useRef, useState } from 'react'
+import { toast } from 'sonner'
+import {
+ Button,
+ DropdownMenu,
+ DropdownMenuCheckboxItem,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+ KeyboardShortcut,
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from 'ui'
+import { useSnapshot } from 'valtio'
+
+import { SqlEditorQueryTargetSelector, useSqlQueryTarget } from './SqlEditorQueryTargetSelector'
+import {
+ DEFAULT_SQL_EDITOR_SEARCH_PATH,
+ getSqlEditorSearchPathStorageKey,
+ SqlEditorSearchPathSelector,
+} from './SqlEditorSearchPathSelector'
+import { sqlEditorWarehouseDemoStore } from './SqlEditorWarehouseDemo'
+import { AutosaveStatus } from './UtilityPanel/AutosaveStatus'
+import { SqlRunButton } from './UtilityPanel/RunButton'
+import { SqlSaveButton } from './UtilityPanel/SaveButton'
+import SavingIndicator from './UtilityPanel/SavingIndicator'
+import { SqlEditorLimitSelector } from './UtilityPanel/SqlEditorLimitSelector'
+import { useIsSqlEditorManualSaveEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
+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 { useRoleImpersonationStateSnapshot } from '@/state/role-impersonation-state'
+import { hotkeyToKeys } from '@/state/shortcuts/formatShortcut'
+import { SHORTCUT_DEFINITIONS, SHORTCUT_IDS } from '@/state/shortcuts/registry'
+import { useSqlEditorSessionSnapshot } from '@/state/sql-editor/sql-editor-session-state'
+import { useSqlEditorV2StateSnapshot } from '@/state/sql-editor/sql-editor-state'
+
+export type SqlEditorQueryBarProps = {
+ id: string
+ isExecuting?: boolean
+ isDisabled?: boolean
+ hasSelection?: boolean
+ prettifyQuery: () => void
+ executeQuery: () => void
+}
+
+const SEPARATE_QUERY_CONTROLS_INLINE_MIN_WIDTH = 896
+
+export function SqlEditorQueryBar({
+ id,
+ isExecuting = false,
+ isDisabled = false,
+ hasSelection = false,
+ prettifyQuery,
+ executeQuery,
+}: SqlEditorQueryBarProps) {
+ const { ref } = useParams()
+ const snapV2 = useSqlEditorV2StateSnapshot()
+ const sessionSnap = useSqlEditorSessionSnapshot()
+ const isManualSaveEnabled = useIsSqlEditorManualSaveEnabled()
+ const roleImpersonationState = useRoleImpersonationStateSnapshot()
+ const warehouseDemoSnap = useSnapshot(sqlEditorWarehouseDemoStore)
+ const queryBarRef = useRef(null)
+ const [areSeparateQueryControlsCollapsed, setAreSeparateQueryControlsCollapsed] = useState(false)
+
+ 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 [searchPath] = useLocalStorageQuery(
+ getSqlEditorSearchPathStorageKey(ref ?? 'unknown'),
+ DEFAULT_SQL_EDITOR_SEARCH_PATH
+ )
+ const queryTarget = useSqlQueryTarget()
+
+ const snippet = snapV2.snippets[id]
+ const isFavorite = snippet !== undefined ? snippet.snippet.favorite : false
+ const currentRole = roleImpersonationState.role?.role ?? 'postgres'
+
+ const hotkeySequnece: Hotkey | undefined =
+ SHORTCUT_DEFINITIONS[SHORTCUT_IDS.SQL_EDITOR_FORMAT].sequence[0]
+ const formatKeys = hotkeySequnece ? hotkeyToKeys(hotkeySequnece) : undefined
+
+ const handleIntelliSenseChange = (checked: boolean) => {
+ setIntellisenseEnabled(checked)
+ toast.success(
+ `Successfully ${checked ? 'enabled' : 'disabled'} IntelliSense. ${checked ? '' : 'Please refresh your browser for changes to take place.'}`
+ )
+ }
+
+ const toggleFavorite = () => {
+ if (isFavorite) {
+ snapV2.removeFavorite(id)
+ toast.success('Removed from favourites')
+ } else {
+ snapV2.addFavorite(id)
+ toast.success('Added to favourites')
+ }
+ }
+
+ const onSelectDatabase = (databaseId: string) => {
+ sessionSnap.resetResults(id)
+ setLastSelectedDb(databaseId)
+ }
+
+ useEffect(() => {
+ const queryBar = queryBarRef.current
+ if (!queryBar) return
+
+ const updateQueryControlsVisibility = (width: number) => {
+ const shouldCollapse = width < SEPARATE_QUERY_CONTROLS_INLINE_MIN_WIDTH
+ setAreSeparateQueryControlsCollapsed((current) =>
+ current === shouldCollapse ? current : shouldCollapse
+ )
+ }
+
+ updateQueryControlsVisibility(queryBar.getBoundingClientRect().width)
+
+ const observer = new ResizeObserver(([entry]) => {
+ updateQueryControlsVisibility(entry.contentRect.width)
+ })
+ observer.observe(queryBar)
+
+ return () => observer.disconnect()
+ }, [])
+
+ const isSeparateQueryControlMode = warehouseDemoSnap.queryControlMode === 'separate'
+ const shouldShowCollapsedQueryControls =
+ isSeparateQueryControlMode && areSeparateQueryControlsCollapsed
+
+ return (
+
+
+ {IS_PLATFORM &&
}
+ {/* SavingIndicator reports auto-save progress. In manual mode the Save button owns status. */}
+ {IS_PLATFORM && !isManualSaveEnabled &&
}
+
+
+
+
+
+ }
+ className="h-[26px] w-[26px]"
+ aria-label="Query options"
+ />
+
+
+ {shouldShowCollapsedQueryControls && (
+ <>
+
+
+ >
+ )}
+
+ Actions
+
+
+
+
+ Prettify SQL
+
+ {formatKeys && }
+
+
+
+ Preferences
+
+ {IS_PLATFORM && (
+
+ Favourite query
+
+ )}
+
+ IntelliSense
+
+
+
+
+ {isSeparateQueryControlMode ? (
+
+
+
+ ) : (
+
+ )}
+
+ {isManualSaveEnabled &&
}
+
+
+
+ )
+}
+
+type QueryContextDropdownProps = {
+ currentRole: string
+ onSelectDatabase: (databaseId: string) => void
+ queryTarget: ReturnType
+ searchPath: string
+ selectedDatabaseId: string
+}
+
+type SeparateQueryControlsProps = {
+ fullWidth?: boolean
+ onSelectDatabase: (databaseId: string) => void
+ selectedDatabaseId: string
+}
+
+function SeparateQueryControls({
+ fullWidth = false,
+ onSelectDatabase,
+ selectedDatabaseId,
+}: SeparateQueryControlsProps) {
+ const controlClassName = fullWidth ? 'w-full [&>span]:w-full' : undefined
+
+ return (
+ <>
+ {IS_PLATFORM && (
+
+ )}
+
+
+ span]:w-full' : undefined}
+ />
+ >
+ )
+}
+
+function QueryContextDropdown({
+ currentRole,
+ onSelectDatabase,
+ queryTarget,
+ searchPath,
+ selectedDatabaseId,
+}: QueryContextDropdownProps) {
+ const { ref } = useParams()
+ const isPrimaryConnection = selectedDatabaseId.length === 0 || selectedDatabaseId === ref
+ const selectedConnectionLabel = isPrimaryConnection ? 'Primary' : 'Replica'
+ const activeContextLabels = [
+ queryTarget === 'warehouse' ? 'Warehouse' : undefined,
+ !isPrimaryConnection ? selectedConnectionLabel : undefined,
+ searchPath !== DEFAULT_SQL_EDITOR_SEARCH_PATH ? searchPath : undefined,
+ currentRole !== 'postgres' ? currentRole : undefined,
+ ].filter((label): label is string => label !== undefined)
+ const contextSummary =
+ activeContextLabels.length === 0
+ ? undefined
+ : activeContextLabels.length === 1
+ ? activeContextLabels[0]
+ : `${activeContextLabels.length} changes`
+
+ return (
+
+
+
+
+ Query
+ {contextSummary !== undefined && (
+
+ {contextSummary}
+
+ )}
+
+
+
+
+
+
+
Query context
+
+ These settings apply together when this query runs.
+
+
+
+ {IS_PLATFORM && (
+
+ )}
+
+
+
+
+
+
+ )
+}
diff --git a/apps/studio/components/interfaces/SQLEditor/SqlEditorQueryTargetSelector.tsx b/apps/studio/components/interfaces/SQLEditor/SqlEditorQueryTargetSelector.tsx
new file mode 100644
index 00000000000..421f92b65b8
--- /dev/null
+++ b/apps/studio/components/interfaces/SQLEditor/SqlEditorQueryTargetSelector.tsx
@@ -0,0 +1,108 @@
+import { LOCAL_STORAGE_KEYS, useParams } from 'common'
+import { ChevronDown } from 'lucide-react'
+import {
+ Button,
+ cn,
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuLabel,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from 'ui'
+
+import { isWarehouseProjectEnabled } from '@/components/interfaces/Database/Warehouse/warehouseDemoStore'
+import { useIsWarehouseEnabled } from '@/hooks/misc/useIsWarehouseEnabled'
+import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
+
+export type SqlQueryTarget = 'postgres' | 'warehouse'
+
+const QUERY_TARGET_OPTIONS: { value: SqlQueryTarget; label: string; description: string }[] = [
+ {
+ value: 'postgres',
+ label: 'Postgres',
+ description: 'Primary database connection',
+ },
+ {
+ value: 'warehouse',
+ label: 'Warehouse',
+ description: 'Warehouse endpoint with 1:1 schema names',
+ },
+]
+
+type SqlEditorQueryTargetSelectorProps = {
+ className?: string
+ align?: 'start' | 'end'
+}
+
+export function SqlEditorQueryTargetSelector({
+ className,
+ align = 'end',
+}: SqlEditorQueryTargetSelectorProps) {
+ const { ref: projectRef } = useParams()
+ const isWarehouseFeatureEnabled = useIsWarehouseEnabled()
+ const warehouseEnabled = isWarehouseProjectEnabled(projectRef)
+
+ const [queryTarget, setQueryTarget] = useLocalStorageQuery(
+ LOCAL_STORAGE_KEYS.SQL_EDITOR_QUERY_TARGET(projectRef as string),
+ 'postgres'
+ )
+
+ if (!isWarehouseFeatureEnabled || !warehouseEnabled) return null
+
+ const selectedOption =
+ QUERY_TARGET_OPTIONS.find((option) => option.value === queryTarget) ?? QUERY_TARGET_OPTIONS[0]
+
+ return (
+
+
+
+
+ Database
+ {selectedOption.label}
+
+
+
+
+
+
+ Query database
+
+ Choose whether this query runs against Postgres or the Warehouse endpoint.
+
+
+
+ setQueryTarget(value as SqlQueryTarget)}
+ >
+ {QUERY_TARGET_OPTIONS.map((option) => (
+
+
+ {option.label}
+
+ {option.description}
+
+
+
+ ))}
+
+
+
+ )
+}
+
+export function useSqlQueryTarget(): SqlQueryTarget {
+ const { ref: projectRef } = useParams()
+ const [queryTarget] = useLocalStorageQuery(
+ LOCAL_STORAGE_KEYS.SQL_EDITOR_QUERY_TARGET(projectRef as string),
+ 'postgres'
+ )
+ return queryTarget
+}
diff --git a/apps/studio/components/interfaces/SQLEditor/SqlEditorSearchPathSelector.tsx b/apps/studio/components/interfaces/SQLEditor/SqlEditorSearchPathSelector.tsx
new file mode 100644
index 00000000000..4f204854eda
--- /dev/null
+++ b/apps/studio/components/interfaces/SQLEditor/SqlEditorSearchPathSelector.tsx
@@ -0,0 +1,207 @@
+import { LOCAL_STORAGE_KEYS, useParams } from 'common'
+import { ChevronDown } from 'lucide-react'
+import { useState } from 'react'
+import {
+ Button,
+ cn,
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from 'ui'
+
+import { useSchemasQuery } from '@/data/database/schemas-query'
+import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
+import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
+
+type SearchPathOption = {
+ value: string
+ label: string
+ description?: string
+}
+
+type SqlEditorSearchPathSelectorProps = {
+ className?: string
+ align?: 'start' | 'end'
+}
+
+const SEARCH_PATH_OPTIONS: SearchPathOption[] = [
+ {
+ value: 'public',
+ label: 'public',
+ },
+ {
+ value: 'auth',
+ label: 'auth',
+ },
+ {
+ value: 'storage',
+ label: 'storage',
+ },
+]
+
+export const DEFAULT_SQL_EDITOR_SEARCH_PATH = 'public'
+const COMMON_SCHEMA_NAMES = ['public', 'auth', 'storage']
+export const getSqlEditorSearchPathStorageKey = (ref: string) =>
+ LOCAL_STORAGE_KEYS.SQL_EDITOR_SEARCH_PATH(ref)
+
+function getSchemaDescription(schemaName: string) {
+ if (schemaName.endsWith('_warehouse')) return 'Warehouse linked schema'
+
+ return undefined
+}
+
+function getSchemaSortPriority(schemaName: string) {
+ const commonIndex = COMMON_SCHEMA_NAMES.indexOf(schemaName)
+ if (commonIndex !== -1) return commonIndex
+ if (schemaName.endsWith('_warehouse')) return COMMON_SCHEMA_NAMES.length
+
+ return COMMON_SCHEMA_NAMES.length + 1
+}
+
+function getSearchPathOption(value: string, options: SearchPathOption[]) {
+ return (
+ options.find((option) => option.value === value) ?? {
+ value,
+ label: value,
+ }
+ )
+}
+
+export function SqlEditorSearchPathSelector({
+ className,
+ align = 'end',
+}: SqlEditorSearchPathSelectorProps) {
+ const { ref } = useParams()
+ const [isShowingMoreSchemas, setIsShowingMoreSchemas] = useState(false)
+ const { data: project } = useSelectedProjectQuery()
+ const { data: schemaData } = useSchemasQuery({
+ projectRef: project?.ref,
+ connectionString: project?.connectionString,
+ })
+ const [searchPath, setSearchPath] = useLocalStorageQuery(
+ getSqlEditorSearchPathStorageKey(ref ?? 'unknown'),
+ DEFAULT_SQL_EDITOR_SEARCH_PATH
+ )
+ const schemaOptions =
+ schemaData !== undefined && schemaData.length > 0
+ ? schemaData
+ .map((schema) => ({
+ value: schema.name,
+ label: schema.name,
+ description: getSchemaDescription(schema.name),
+ }))
+ .sort((a, b) => {
+ const priority = getSchemaSortPriority(a.label) - getSchemaSortPriority(b.label)
+ return priority !== 0 ? priority : a.label.localeCompare(b.label)
+ })
+ : SEARCH_PATH_OPTIONS
+ const selectedOption = getSearchPathOption(searchPath, schemaOptions)
+ const standardSchemaOptions = schemaOptions.filter(
+ (option) => !option.value.endsWith('_warehouse')
+ )
+ const warehouseSchemaOptions = schemaOptions.filter((option) =>
+ option.value.endsWith('_warehouse')
+ )
+ const visibleStandardSchemaOptions = standardSchemaOptions.filter(
+ (option) =>
+ isShowingMoreSchemas ||
+ COMMON_SCHEMA_NAMES.includes(option.value) ||
+ option.value === selectedOption.value
+ )
+ const hiddenStandardSchemaCount =
+ standardSchemaOptions.length - visibleStandardSchemaOptions.length
+
+ return (
+
+
+
+
+ Search path
+ {selectedOption.label}
+
+
+
+
+
+
+ Search path
+
+ Choose where your query looks first for unqualified table names.{' '}
+
+ Learn more
+
+
+
+
+
+ {standardSchemaOptions.length > 0 && (
+ <>
+
+ {warehouseSchemaOptions.length > 0 ? 'Postgres schemas' : 'Schemas'}
+
+ {visibleStandardSchemaOptions.map((option) => (
+
+
+ {option.label}
+ {option.description !== undefined && (
+
+ {option.description}
+
+ )}
+
+
+ ))}
+ {hiddenStandardSchemaCount > 0 && (
+ {
+ event.preventDefault()
+ setIsShowingMoreSchemas(true)
+ }}
+ >
+ Show {hiddenStandardSchemaCount} more schema
+ {hiddenStandardSchemaCount === 1 ? '' : 's'}
+
+ )}
+ >
+ )}
+ {warehouseSchemaOptions.length > 0 && (
+ <>
+
+
+ Warehouse schemas
+
+ {warehouseSchemaOptions.map((option) => (
+
+
+ {option.label}
+ {option.description !== undefined && (
+
+ {option.description}
+
+ )}
+
+
+ ))}
+ >
+ )}
+
+
+
+ )
+}
diff --git a/apps/studio/components/interfaces/SQLEditor/SqlEditorWarehouseDemo.tsx b/apps/studio/components/interfaces/SQLEditor/SqlEditorWarehouseDemo.tsx
new file mode 100644
index 00000000000..a2b3eed9428
--- /dev/null
+++ b/apps/studio/components/interfaces/SQLEditor/SqlEditorWarehouseDemo.tsx
@@ -0,0 +1,126 @@
+import { Tooltip, TooltipContent, TooltipTrigger } from 'ui'
+import { proxy, useSnapshot } from 'valtio'
+
+import type { SqlQueryTarget } from './SqlEditorQueryTargetSelector'
+
+export type SqlWarehouseHealthPreset = 'healthy' | 'catching_up' | 'degraded' | 'error'
+export type SqlEditorQueryControlMode = 'compact' | 'separate'
+
+export type SqlWarehouseResultSource =
+ | { type: 'query_target' }
+ | { type: 'search_path'; schema: string }
+ | { type: 'query_reference' }
+
+const SQL_WAREHOUSE_SCHEMA_PATTERN = /\b[a-zA-Z_][\w$]*_warehouse\s*\./i
+
+const SQL_WAREHOUSE_HEALTH_LABELS: Record = {
+ healthy: '',
+ catching_up: 'Catching up',
+ degraded: 'Degraded',
+ error: 'Error',
+}
+
+const SQL_WAREHOUSE_HEALTH_TOOLTIPS: Record = {
+ healthy: 'Warehouse replication is caught up with Postgres.',
+ catching_up:
+ 'Warehouse replication is catching up. Recent writes may not appear in linked tables yet.',
+ degraded: 'Warehouse replication is degraded. Query results may be stale.',
+ error: 'Warehouse replication encountered an error. Check replication logs.',
+}
+
+const SQL_WAREHOUSE_HEALTH_CLASS_NAMES: Record = {
+ healthy: 'text-foreground-light hover:text-foreground',
+ catching_up: 'text-warning hover:text-warning-600',
+ degraded: 'text-destructive hover:text-destructive-600',
+ error: 'text-destructive hover:text-destructive-600',
+}
+
+export const sqlEditorWarehouseDemoStore = proxy<{
+ health: SqlWarehouseHealthPreset
+ queryControlMode: SqlEditorQueryControlMode
+}>({
+ health: 'healthy',
+ queryControlMode: 'separate',
+})
+
+export function setSqlEditorWarehouseHealth(health: SqlWarehouseHealthPreset) {
+ sqlEditorWarehouseDemoStore.health = health
+}
+
+export function setSqlEditorQueryControlMode(mode: SqlEditorQueryControlMode) {
+ sqlEditorWarehouseDemoStore.queryControlMode = mode
+}
+
+export function resolveSqlWarehouseResultSource(
+ sql: string,
+ searchPath: string,
+ queryTarget: SqlQueryTarget = 'postgres'
+): SqlWarehouseResultSource | undefined {
+ if (queryTarget === 'warehouse') {
+ return { type: 'query_target' }
+ }
+
+ if (searchPath.endsWith('_warehouse')) {
+ return { type: 'search_path', schema: searchPath }
+ }
+
+ if (SQL_WAREHOUSE_SCHEMA_PATTERN.test(sql)) {
+ return { type: 'query_reference' }
+ }
+
+ return undefined
+}
+
+function getWarehouseResultSourceTooltip(source: SqlWarehouseResultSource) {
+ if (source.type === 'query_target') {
+ return 'Results are served from the Warehouse endpoint. Schemas and table names match Postgres.'
+ }
+
+ if (source.type === 'search_path') {
+ return `Results are served from Warehouse because the search path starts with ${source.schema}.`
+ }
+
+ return 'Results include a linked Warehouse table referenced directly in the SQL.'
+}
+
+export function SqlWarehouseResultStatus({ source }: { source: SqlWarehouseResultSource }) {
+ const snap = useSnapshot(sqlEditorWarehouseDemoStore)
+ const health = snap.health as SqlWarehouseHealthPreset
+ const shouldShowHealth = health !== 'healthy'
+
+ return (
+
+
+
+
+
+ Warehouse
+
+
+
+
+ {getWarehouseResultSourceTooltip(source)}
+
+
+ {shouldShowHealth && (
+ <>
+ ·
+
+
+
+
+ {SQL_WAREHOUSE_HEALTH_LABELS[health]}
+
+
+
+
+ {SQL_WAREHOUSE_HEALTH_TOOLTIPS[health]}
+
+
+ >
+ )}
+
+ )
+}
diff --git a/apps/studio/components/interfaces/SQLEditor/SqlWarehouseTargetToggle.tsx b/apps/studio/components/interfaces/SQLEditor/SqlWarehouseTargetToggle.tsx
deleted file mode 100644
index 673d5ee6df5..00000000000
--- a/apps/studio/components/interfaces/SQLEditor/SqlWarehouseTargetToggle.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-import { LOCAL_STORAGE_KEYS, useParams } from 'common'
-import { cn } from 'ui'
-
-import { isWarehouseProjectEnabled } from '@/components/interfaces/Database/Warehouse/warehouseDemoStore'
-import { useIsWarehouseEnabled } from '@/hooks/misc/useIsWarehouseEnabled'
-import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
-
-export type SqlQueryTarget = 'postgres' | 'warehouse'
-
-interface SqlWarehouseTargetToggleProps {
- className?: string
-}
-
-export function SqlWarehouseTargetToggle({ className }: SqlWarehouseTargetToggleProps) {
- const { ref: projectRef } = useParams()
- const isWarehouseFeatureEnabled = useIsWarehouseEnabled()
- const warehouseEnabled = isWarehouseProjectEnabled(projectRef)
-
- const [queryTarget, setQueryTarget] = useLocalStorageQuery(
- LOCAL_STORAGE_KEYS.SQL_EDITOR_QUERY_TARGET(projectRef as string),
- 'postgres'
- )
-
- if (!isWarehouseFeatureEnabled || !warehouseEnabled) return null
-
- return (
-
- setQueryTarget('postgres')}
- >
- Postgres
-
- setQueryTarget('warehouse')}
- >
- Warehouse
-
-
- )
-}
-
-export function useSqlQueryTarget(): SqlQueryTarget {
- const { ref: projectRef } = useParams()
- const [queryTarget] = useLocalStorageQuery(
- LOCAL_STORAGE_KEYS.SQL_EDITOR_QUERY_TARGET(projectRef as string),
- 'postgres'
- )
- return queryTarget
-}
diff --git a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/AutosaveStatus.tsx b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/AutosaveStatus.tsx
index abff6c14b7a..b6603e19cfe 100644
--- a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/AutosaveStatus.tsx
+++ b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/AutosaveStatus.tsx
@@ -8,13 +8,8 @@ import {
} from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { IS_PLATFORM } from '@/lib/constants'
import { useTrack } from '@/lib/telemetry/track'
-import { hasUnsavedChanges } from '@/state/sql-editor/sql-editor-lifecycle'
-import { useSqlEditorV2StateSnapshot } from '@/state/sql-editor/sql-editor-state'
-export type AutosaveStatusProps = { id: string }
-
-export const AutosaveStatus = ({ id }: AutosaveStatusProps) => {
- const snapV2 = useSqlEditorV2StateSnapshot()
+export const AutosaveStatus = () => {
const track = useTrack()
const isManualSaveEnabled = useIsSqlEditorManualSaveEnabled()
const { selectFeaturePreview } = useFeaturePreviewModal()
@@ -27,21 +22,7 @@ export const AutosaveStatus = ({ id }: AutosaveStatusProps) => {
const sqlEditorManualSaveFlag = useFlag('sqlEditorManualSave')
const canEnableManualSave = IS_PLATFORM && sqlEditorManualSaveFlag
- if (isManualSaveEnabled) {
- const snippet = snapV2.snippets[id]
- // A snippet only enters the store on its first edit, so a snippet that
- // isn't in the store yet is a fresh, blank, untouched "new query" tab —
- // there's nothing to report a save status for.
- if (snippet === undefined) return null
-
- const unsavedChanges = hasUnsavedChanges(snippet.snippet.status)
-
- return (
-
- {unsavedChanges ? 'Unsaved edits' : 'Saved'}
-
- )
- }
+ if (isManualSaveEnabled) return null
return (
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 (
-
- ) : (
-
- )
- }
- className={className}
+
- {hasSelection ? 'Run selected' : 'Run'}
-
+
+ ) : (
+
+ )
+ }
+ className={className}
+ >
+ {hasSelection ? 'Run selected' : 'Run'}
+
+
)
}
diff --git a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/SaveButton.tsx b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/SaveButton.tsx
index 87ce9d5e08b..4be5930ff1e 100644
--- a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/SaveButton.tsx
+++ b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/SaveButton.tsx
@@ -1,16 +1,17 @@
import { Loader2 } from 'lucide-react'
-import { Button, KeyboardShortcut } from 'ui'
+import { Button, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
+import { ShortcutTooltip } from '@/components/ui/ShortcutTooltip'
+import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
import { hasUnsavedChanges, isSaving } from '@/state/sql-editor/sql-editor-lifecycle'
import { useSqlEditorSaveCoordinator } from '@/state/sql-editor/sql-editor-save-coordinator'
import { useSqlEditorV2StateSnapshot } from '@/state/sql-editor/sql-editor-state'
interface SqlSaveButtonProps {
id: string
- className?: string
}
-export const SqlSaveButton = ({ id, className }: SqlSaveButtonProps) => {
+export const SqlSaveButton = ({ id }: SqlSaveButtonProps) => {
const snapV2 = useSqlEditorV2StateSnapshot()
const { requestSave } = useSqlEditorSaveCoordinator()
@@ -18,23 +19,31 @@ export const SqlSaveButton = ({ id, className }: SqlSaveButtonProps) => {
const saving = isSaving(status)
const isDirty = hasUnsavedChanges(status) && !saving
- return (
+ const button = (
requestSave(id)}
disabled={!isDirty}
variant="default"
size="tiny"
data-testid="sql-save-button"
- iconRight={
- saving ? (
-
- ) : (
-
- )
- }
- className={className}
+ icon={saving ? : undefined}
>
Save
)
+
+ if (isDirty) {
+ return
{button}
+ }
+
+ return (
+
+
+ {button}
+
+
+ {saving ? 'Saving changes...' : 'All changes saved'}
+
+
+ )
}
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 (
+
+
+
+
+ Limit
+ {selectedLabel}
+
+
+
+
+
+ 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..448fec0c6ad
--- /dev/null
+++ b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/SqlResultsSummary.tsx
@@ -0,0 +1,50 @@
+import { Tooltip, TooltipContent, TooltipTrigger } from 'ui'
+
+interface SqlResultsSummaryProps {
+ rowCount: number
+ autoLimit?: number
+}
+
+function formatRowCount(rowCount: number) {
+ return `${rowCount.toLocaleString()} row${rowCount === 1 ? '' : 's'}`
+}
+
+export function SqlResultsSummary({ rowCount, autoLimit }: SqlResultsSummaryProps) {
+ const hasReachedLimit = autoLimit !== undefined && rowCount >= autoLimit
+
+ return (
+
+ {hasReachedLimit && (
+ <>
+
+
+
+
+ Limited
+
+
+
+
+
+
+ Results are limited to preserve browser performance when a query returns many
+ rows.
+
+
+ Change or remove this limit from the dropdown near Run.
+
+
+
+
+ ·
+ >
+ )}
+
+ {formatRowCount(rowCount)}
+ {hasReachedLimit && (
+ (limit {autoLimit.toLocaleString()})
+ )}
+
+
+ )
+}
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 8b2a507b953..00000000000
--- a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityActions.tsx
+++ /dev/null
@@ -1,245 +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 { SqlWarehouseTargetToggle } from '../SqlWarehouseTargetToggle'
-import { AutosaveStatus } from './AutosaveStatus'
-import { SqlRunButton } from './RunButton'
-import { SqlSaveButton } from './SaveButton'
-import SavingIndicator from './SavingIndicator'
-import { useIsSqlEditorManualSaveEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
-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 { useSqlEditorSessionSnapshot } from '@/state/sql-editor/sql-editor-session-state'
-import { useSqlEditorV2StateSnapshot } from '@/state/sql-editor/sql-editor-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 isManualSaveEnabled = useIsSqlEditorManualSaveEnabled()
-
- 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 (
-
-
- {/* SavingIndicator reports auto-save progress (spinner/checkmark). In manual
- mode AutosaveStatus + the Save button own the status, so hide it there. */}
- {IS_PLATFORM && !isManualSaveEnabled &&
}
-
-
-
- }
- />
-
-
-
-
-
- Intellisense enabled
-
- {intellisenseEnabled && }
-
- {IS_PLATFORM && (
- <>
-
- {
- if (isFavorite) removeFavorite()
- else addFavorite()
- }}
- >
-
- {isFavorite ? 'Remove from' : 'Add to'} favorites
-
- >
- )}
-
-
-
- Prettify SQL
-
- {formatKeys && }
-
-
-
-
-
-
-
-
-
- }
- aria-label="Enable Intellisense"
- />
-
-
- Enable Intellisense
-
-
-
- Intellisense enabled
- {intellisenseEnabled && }
-
-
-
-
- {IS_PLATFORM && (
-
-
- {isFavorite ? (
- }
- aria-label="Remove from favorites"
- />
- ) : (
- }
- aria-label="Add to favorites"
- />
- )}
-
-
- {isFavorite ? 'Remove from' : 'Add to'} favorites
-
-
- )}
-
-
-
- }
- aria-label="Prettify SQL"
- />
-
-
-
- Prettify SQL
- {formatKeys && }
-
-
-
-
-
-
-
-
- {IS_PLATFORM && (
-
- )}
-
-
-
-
- {isManualSaveEnabled && }
-
-
-
-
- )
-}
diff --git a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityPanel.tsx b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityPanel.tsx
index 125905c3b97..11edc683253 100644
--- a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityPanel.tsx
+++ b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityPanel.tsx
@@ -2,8 +2,9 @@ import { useParams } from 'common'
import { toast } from 'sonner'
import { Tabs, TabsContent, TabsList, TabsTrigger } from 'ui'
+import { SqlWarehouseResultStatus } from '../SqlEditorWarehouseDemo'
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 +20,6 @@ export type UtilityPanelProps = {
isExplainExecuting?: boolean
isDebugging?: boolean
isDisabled?: boolean
- hasSelection: boolean
- prettifyQuery: () => void
- executeQuery: () => void
executeExplainQuery: () => void
showExplainTab?: boolean
onDebug: () => void
@@ -45,9 +43,6 @@ export const UtilityPanel = ({
isExplainExecuting,
isDebugging,
isDisabled,
- hasSelection,
- prettifyQuery,
- executeQuery,
executeExplainQuery,
showExplainTab = true,
onDebug,
@@ -62,6 +57,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
@@ -131,8 +131,8 @@ export const UtilityPanel = ({
return (
-
-
+
+
Results
@@ -144,12 +144,23 @@ export const UtilityPanel = ({
Chart
+
+
+
+ {showResultsSummary && (
+ <>
+ {result.warehouseResultSource && (
+
+ )}
+
+ >
+ )}
{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')}
@@ -157,16 +168,7 @@ export const UtilityPanel = ({
/>
)}
-
-
-
+
+
>
diff --git a/apps/studio/components/interfaces/UserDropdown/SqlEditorWarehouseDemoMenu.tsx b/apps/studio/components/interfaces/UserDropdown/SqlEditorWarehouseDemoMenu.tsx
new file mode 100644
index 00000000000..ed1c7e53a4a
--- /dev/null
+++ b/apps/studio/components/interfaces/UserDropdown/SqlEditorWarehouseDemoMenu.tsx
@@ -0,0 +1,89 @@
+import { Database } from 'lucide-react'
+import { toast } from 'sonner'
+import {
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+ DropdownMenuSeparator,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+} from 'ui'
+import { useSnapshot } from 'valtio'
+
+import {
+ setSqlEditorQueryControlMode,
+ setSqlEditorWarehouseHealth,
+ sqlEditorWarehouseDemoStore,
+ type SqlEditorQueryControlMode,
+ type SqlWarehouseHealthPreset,
+} from '@/components/interfaces/SQLEditor/SqlEditorWarehouseDemo'
+
+const WAREHOUSE_HEALTH_PRESETS: { preset: SqlWarehouseHealthPreset; label: string }[] = [
+ { preset: 'healthy', label: 'Live' },
+ { preset: 'catching_up', label: 'Catching up' },
+ { preset: 'degraded', label: 'Degraded' },
+ { preset: 'error', label: 'Error' },
+]
+
+const QUERY_CONTROL_MODES: { mode: SqlEditorQueryControlMode; label: string }[] = [
+ { mode: 'compact', label: 'Compact Query dropdown' },
+ { mode: 'separate', label: 'Separate controls' },
+]
+
+export function SqlEditorWarehouseDemoMenu() {
+ const snap = useSnapshot(sqlEditorWarehouseDemoStore)
+
+ return (
+ <>
+
+
+
+
+ SQL Warehouse demo
+
+
+
+ Results health
+
+ {WAREHOUSE_HEALTH_PRESETS.map(({ preset, label }) => (
+ {
+ setSqlEditorWarehouseHealth(preset)
+ toast.message(`SQL Warehouse demo: ${label}`)
+ }}
+ >
+ {label}
+
+ ))}
+
+
+ Query control
+
+ {
+ if (value === 'compact' || value === 'separate') {
+ setSqlEditorQueryControlMode(value)
+ toast.message(
+ `SQL Warehouse demo: ${
+ value === 'compact' ? 'Compact Query dropdown' : 'Separate controls'
+ }`
+ )
+ }
+ }}
+ >
+ {QUERY_CONTROL_MODES.map(({ mode, label }) => (
+
+ {label}
+
+ ))}
+
+
+
+ >
+ )
+}
diff --git a/apps/studio/components/layouts/SQLEditorLayout/SQLEditorMenu.tsx b/apps/studio/components/layouts/SQLEditorLayout/SQLEditorMenu.tsx
index 2dc29b01f0d..e2c04267d3d 100644
--- a/apps/studio/components/layouts/SQLEditorLayout/SQLEditorMenu.tsx
+++ b/apps/studio/components/layouts/SQLEditorLayout/SQLEditorMenu.tsx
@@ -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,102 @@ 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
-
-
- )}
-
-
-
-
-
-
- }
- className="w-[26px]"
- aria-label="Create a new query"
- />
-
-
- Create a new query
-
-
- handleNewQuery()}>
-
- Create a new snippet
-
- createNewFolder()}>
-
- Create a new folder
-
-
-
-
-
- {showSearch ?
:
}
+
+
+ }
+ onClick={handleNewQuery}
+ >
+ New query
+
+
+
+ }
+ />
+
+
+
+
+ New folder
+
+
+
+ View running queries
+
+
+
-
-
appState.setOnGoingQueriesPanelOpen(true)}>
- View running queries
-
+
+
+ {
+ 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
+
+ ) : (
+ {
+ if (value === 'name' || value === 'inserted_at') 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 9a536480ccb..6530e9eca16 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'
@@ -565,9 +564,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..a810e324268 100644
--- a/apps/studio/components/ui/DatabaseSelector.tsx
+++ b/apps/studio/components/ui/DatabaseSelector.tsx
@@ -39,6 +39,7 @@ interface DatabaseSelectorProps {
className?: string
align?: 'start' | 'end'
isForm?: boolean
+ label?: string
}
export const DatabaseSelector = ({
@@ -50,6 +51,7 @@ export const DatabaseSelector = ({
align = 'end',
className,
isForm = false,
+ label = 'Source',
}: DatabaseSelectorProps) => {
const router = useRouter()
const { ref: projectRef } = useParams()
@@ -80,15 +82,25 @@ export const DatabaseSelector = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [_selectedDatabaseId])
+ const selectedDatabaseLabel = selectedAdditionalOption ? (
+
{selectedAdditionalOption.name}
+ ) : (
+ <>
+
+ {isLoading || selectedDatabase?.identifier === projectRef ? 'Primary' : 'Read replica'}
+
+ {isSuccess && selectedDatabase?.identifier !== projectRef && (
+
+ ({selectedDatabaseRegion} - {formattedDatabaseId})
+
+ )}
+ >
+ )
+
return (
-
- {!isForm && (
-
- Source
-
- )}
+ {isForm ? (
}
@@ -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 ? (
- {selectedAdditionalOption.name}
- ) : (
- <>
-
- {isLoading || selectedDatabase?.identifier === projectRef
- ? 'Primary database'
- : 'Read replica'}
- {' '}
- {isSuccess && selectedDatabase?.identifier !== projectRef && (
-
- ({selectedDatabaseRegion} - {formattedDatabaseId})
-
- )}
- >
- )}
+ {selectedDatabaseLabel}
-
+ ) : (
+
+
+ {label}
+
+ {isLoading ? : selectedDatabaseLabel}
+
+
+
+
+ )}
@@ -203,7 +222,7 @@ export const DatabaseSelector = ({
{database.identifier === projectRef
- ? 'Primary database'
+ ? 'Primary'
: `Read replica (${region} - ${id})`}
{database.identifier === selectedDatabaseId &&
}
diff --git a/apps/studio/data/sql/execute-sql-mutation.ts b/apps/studio/data/sql/execute-sql-mutation.ts
index 3718fac1f59..e8861d01686 100644
--- a/apps/studio/data/sql/execute-sql-mutation.ts
+++ b/apps/studio/data/sql/execute-sql-mutation.ts
@@ -11,6 +11,7 @@ import {
calculateSummary,
createNodeTree,
} from '@/components/interfaces/ExplainVisualizer/ExplainVisualizer.parser'
+import type { SqlWarehouseResultSource } from '@/components/interfaces/SQLEditor/SqlEditorWarehouseDemo'
import { handleError as handleErrorFetchers, post } from '@/data/fetchers'
import { MB } from '@/lib/constants'
import { sqlEventParser } from '@/lib/sql-event-parser'
@@ -65,6 +66,7 @@ type ExecuteSqlVariables = {
type ExecuteSqlMutationVariables = ExecuteSqlVariables & {
autoLimit?: number
contextualInvalidation?: boolean
+ warehouseResultSource?: SqlWarehouseResultSource
}
/**
diff --git a/apps/studio/state/shortcuts/registry/sql-editor.ts b/apps/studio/state/shortcuts/registry/sql-editor.ts
index c2d91122ed9..24bb0863f74 100644
--- a/apps/studio/state/shortcuts/registry/sql-editor.ts
+++ b/apps/studio/state/shortcuts/registry/sql-editor.ts
@@ -6,6 +6,8 @@ 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',
+ SQL_EDITOR_SAVE: 'sql-editor.save',
}
export type SqlEditorShortcutId =
@@ -42,4 +44,16 @@ 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,
+ },
+ [SQL_EDITOR_SHORTCUT_IDS.SQL_EDITOR_SAVE]: {
+ id: SQL_EDITOR_SHORTCUT_IDS.SQL_EDITOR_SAVE,
+ label: 'Save edits',
+ sequence: ['Mod+S'],
+ showInSettings: false,
+ },
}
diff --git a/apps/studio/state/sql-editor/sql-editor-session-state.test.ts b/apps/studio/state/sql-editor/sql-editor-session-state.test.ts
index 46768b2a979..7c5353d3d0d 100644
--- a/apps/studio/state/sql-editor/sql-editor-session-state.test.ts
+++ b/apps/studio/state/sql-editor/sql-editor-session-state.test.ts
@@ -22,6 +22,19 @@ describe('sqlEditorSessionState', () => {
expect(result?.error).toBeUndefined()
})
+ it('addResult stores Warehouse result metadata', () => {
+ sqlEditorSessionState.addResult('a', [{ x: 1 }], undefined, {
+ type: 'search_path',
+ schema: 'public_warehouse',
+ })
+
+ const result = sqlEditorSessionState.results['a']?.[0]
+ expect(result?.warehouseResultSource).toEqual({
+ type: 'search_path',
+ schema: 'public_warehouse',
+ })
+ })
+
it('addResultError stores the error with empty rows', () => {
sqlEditorSessionState.addResultError('a', { message: 'boom' }, 25)
diff --git a/apps/studio/state/sql-editor/sql-editor-session-state.ts b/apps/studio/state/sql-editor/sql-editor-session-state.ts
index 8cb9618c4fa..46173d49d08 100644
--- a/apps/studio/state/sql-editor/sql-editor-session-state.ts
+++ b/apps/studio/state/sql-editor/sql-editor-session-state.ts
@@ -1,6 +1,14 @@
import { proxy, ref, snapshot, useSnapshot } from 'valtio'
import type { QueryPlanRow } from '@/components/interfaces/ExplainVisualizer/ExplainVisualizer.types'
+import type { SqlWarehouseResultSource } from '@/components/interfaces/SQLEditor/SqlEditorWarehouseDemo'
+
+export interface SqlEditorQueryResult {
+ rows: any[]
+ error?: any
+ autoLimit?: number
+ warehouseResultSource?: SqlWarehouseResultSource
+}
/**
* Ephemeral, per-session SQL editor state that is NOT persisted: query results,
@@ -15,11 +23,7 @@ export const sqlEditorSessionState = proxy({
* a single query with multiple statements can return multiple results.
*/
results: {} as {
- [snippetId: string]: {
- rows: any[]
- error?: any
- autoLimit?: number
- }[]
+ [snippetId: string]: SqlEditorQueryResult[]
},
/** EXPLAIN results, if any, keyed by snippet id. */
@@ -39,17 +43,27 @@ export const sqlEditorSessionState = proxy({
setLimit: (value: number) => (sqlEditorSessionState.limit = value),
- addResult: (id: string, results: any[], autoLimit?: number) => {
+ addResult: (
+ id: string,
+ results: any[],
+ autoLimit?: number,
+ warehouseResultSource?: SqlWarehouseResultSource
+ ) => {
// Use ref() to prevent Valtio from creating proxies for each row object.
// This is critical for large result sets - without ref(), Valtio wraps every
// row and nested property in a Proxy, causing massive memory overhead.
// Alright to use ref() in this case as the data is meant to be read-only and we
// don't need to track changes to the underlying data
- sqlEditorSessionState.results[id] = [{ rows: ref(results), autoLimit }]
+ sqlEditorSessionState.results[id] = [{ rows: ref(results), autoLimit, warehouseResultSource }]
},
- addResultError: (id: string, error: any, autoLimit?: number) => {
- sqlEditorSessionState.results[id] = [{ rows: ref([]), error, autoLimit }]
+ addResultError: (
+ id: string,
+ error: any,
+ autoLimit?: number,
+ warehouseResultSource?: SqlWarehouseResultSource
+ ) => {
+ sqlEditorSessionState.results[id] = [{ rows: ref([]), error, autoLimit, warehouseResultSource }]
},
resetResult: (id: string) => {
diff --git a/apps/www/data/Investors.tsx b/apps/www/data/Investors.tsx
index a149f1efb4a..ed1adf033e6 100644
--- a/apps/www/data/Investors.tsx
+++ b/apps/www/data/Investors.tsx
@@ -4,26 +4,62 @@ type Investor = {
title: string
img: string
lead: boolean
+ scale?: number
+ objectPosition?: string
twitter?: string | null
linkedIn?: string | null
}
const data: Investor[] = [
+ // Row 1
+ {
+ name: 'GIC',
+ round: 'B',
+ title: 'GIC',
+ img: '/images/logos/gic--grey.png',
+ lead: true,
+ scale: 1.5,
+ twitter: null,
+ linkedIn: null,
+ },
+ {
+ name: 'Peak XV',
+ round: 'B',
+ title: 'Peak XV Partners',
+ img: '/images/logos/peakxv--grey.png',
+ lead: true,
+ scale: 0.85,
+ twitter: null,
+ linkedIn: null,
+ },
+ {
+ name: 'Accel',
+ round: 'B',
+ title: 'Accel',
+ img: '/images/logos/accel--grey.png',
+ lead: true,
+ scale: 1.12,
+ twitter: null,
+ linkedIn: null,
+ },
+ {
+ name: 'Craft',
+ round: 'B',
+ title: 'Craft Ventures',
+ img: '/images/logos/craft--grey.png',
+ lead: true,
+ scale: 0.61,
+ twitter: null,
+ linkedIn: null,
+ },
+ // Row 2
{
name: 'Coatue',
round: 'SEED',
title: 'Coatue Growth Fund',
img: '/images/logos/coatue.png',
lead: true,
- twitter: null,
- linkedIn: null,
- },
- {
- name: 'Mozilla',
- round: 'SEED',
- title: 'Mozilla',
- img: '/images/logos/mozilla--grey.png',
- lead: true,
+ scale: 0.95,
twitter: null,
linkedIn: null,
},
@@ -36,6 +72,16 @@ const data: Investor[] = [
twitter: null,
linkedIn: null,
},
+ {
+ name: 'Stripe',
+ round: 'B',
+ title: 'Stripe',
+ img: '/images/logos/stripe--grey.png',
+ lead: true,
+ scale: 1.15,
+ twitter: null,
+ linkedIn: null,
+ },
{
name: 'Felicis',
round: 'B',
@@ -45,6 +91,45 @@ const data: Investor[] = [
twitter: null,
linkedIn: null,
},
+ // Row 3
+ {
+ name: 'Salesforce Ventures',
+ round: 'B',
+ title: 'Salesforce Ventures',
+ img: '/images/logos/salesforce--grey.png',
+ lead: true,
+ scale: 2.01,
+ twitter: null,
+ linkedIn: null,
+ },
+ {
+ name: 'Georgian',
+ round: 'B',
+ title: 'Georgian',
+ img: '/images/logos/georgian--grey.png',
+ lead: true,
+ twitter: null,
+ linkedIn: null,
+ },
+ {
+ name: 'Figma',
+ round: 'B',
+ title: 'Figma',
+ img: '/images/logos/figma--grey.png',
+ lead: true,
+ twitter: null,
+ linkedIn: null,
+ },
+ {
+ name: 'Mozilla',
+ round: 'SEED',
+ title: 'Mozilla',
+ img: '/images/logos/mozilla--grey.png',
+ lead: true,
+ scale: 0.9,
+ twitter: null,
+ linkedIn: null,
+ },
{
name: 'Tom Preston-Werner',
round: 'A',
diff --git a/apps/www/pages/company.tsx b/apps/www/pages/company.tsx
index 544f0813e95..f73c19d9aa9 100644
--- a/apps/www/pages/company.tsx
+++ b/apps/www/pages/company.tsx
@@ -171,24 +171,18 @@ const Investors = () => {
{InvestorData.filter((x) => x.lead === true).map((x) => (
-
-
+
diff --git a/apps/www/public/images/logos/accel--grey.png b/apps/www/public/images/logos/accel--grey.png
new file mode 100644
index 00000000000..f0590898664
Binary files /dev/null and b/apps/www/public/images/logos/accel--grey.png differ
diff --git a/apps/www/public/images/logos/craft--grey.png b/apps/www/public/images/logos/craft--grey.png
new file mode 100644
index 00000000000..a8e2061077d
Binary files /dev/null and b/apps/www/public/images/logos/craft--grey.png differ
diff --git a/apps/www/public/images/logos/figma--grey.png b/apps/www/public/images/logos/figma--grey.png
new file mode 100644
index 00000000000..1fddee5c818
Binary files /dev/null and b/apps/www/public/images/logos/figma--grey.png differ
diff --git a/apps/www/public/images/logos/georgian--grey.png b/apps/www/public/images/logos/georgian--grey.png
new file mode 100644
index 00000000000..a305e592939
Binary files /dev/null and b/apps/www/public/images/logos/georgian--grey.png differ
diff --git a/apps/www/public/images/logos/gic--grey.png b/apps/www/public/images/logos/gic--grey.png
new file mode 100644
index 00000000000..00343538e11
Binary files /dev/null and b/apps/www/public/images/logos/gic--grey.png differ
diff --git a/apps/www/public/images/logos/peakxv--grey.png b/apps/www/public/images/logos/peakxv--grey.png
new file mode 100644
index 00000000000..fdb8969dd16
Binary files /dev/null and b/apps/www/public/images/logos/peakxv--grey.png differ
diff --git a/apps/www/public/images/logos/salesforce--grey.png b/apps/www/public/images/logos/salesforce--grey.png
new file mode 100644
index 00000000000..a190ef879b8
Binary files /dev/null and b/apps/www/public/images/logos/salesforce--grey.png differ
diff --git a/apps/www/public/images/logos/stripe--grey.png b/apps/www/public/images/logos/stripe--grey.png
new file mode 100644
index 00000000000..d0fc63c4cc4
Binary files /dev/null and b/apps/www/public/images/logos/stripe--grey.png differ
diff --git a/packages/common/constants/local-storage.ts b/packages/common/constants/local-storage.ts
index 833846a82ba..4f061550821 100644
--- a/packages/common/constants/local-storage.ts
+++ b/packages/common/constants/local-storage.ts
@@ -47,6 +47,7 @@ export const LOCAL_STORAGE_KEYS = {
SQL_EDITOR_AI_SCHEMA: (ref: string) => `supabase_sql-editor-ai-schema-enabled-${ref}`,
SQL_EDITOR_AI_OPEN: 'supabase_sql-editor-ai-open',
SQL_EDITOR_LAST_SELECTED_DB: (ref: string) => `sql-editor-last-selected-db-${ref}`,
+ SQL_EDITOR_SEARCH_PATH: (ref: string) => `sql-editor-search-path-${ref}`,
SQL_EDITOR_QUERY_TARGET: (ref: string) => `sql-editor-query-target-${ref}`,
SQL_EDITOR_SQL_BLOCK_ACKNOWLEDGED: (ref: string) => `sql-editor-sql-block-acknowledged-${ref}`,
SQL_EDITOR_SECTION_STATE: (ref: string) => `sql-editor-section-state-${ref}`,