mirror of
https://github.com/supabase/supabase.git
synced 2026-10-12 04:45:08 +03:00
polish
This commit is contained in:
1 parent
3e73fca908
commit
0f37d29f63
13 files changed
+653
-146
No files matched your search
+4
-1
@@ -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
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
|
||||
@@ -42,6 +42,11 @@ import {
|
||||
suffixWithLimit,
|
||||
} from './SQLEditor.utils'
|
||||
import { SqlEditorQueryBar } from './SqlEditorQueryBar'
|
||||
import {
|
||||
DEFAULT_SQL_EDITOR_SEARCH_PATH,
|
||||
getSqlEditorSearchPathStorageKey,
|
||||
} from './SqlEditorSearchPathSelector'
|
||||
import { resolveSqlWarehouseResultSource } from './SqlEditorWarehouseDemo'
|
||||
import { useAddDefinitions } from './useAddDefinitions'
|
||||
import { UtilityPanel } from './UtilityPanel/UtilityPanel'
|
||||
import {
|
||||
@@ -58,6 +63,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'
|
||||
@@ -112,6 +118,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()
|
||||
|
||||
@@ -220,7 +230,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)
|
||||
@@ -268,7 +278,7 @@ export const SQLEditor = () => {
|
||||
}
|
||||
}
|
||||
|
||||
sessionSnap.addResultError(id, error, vars.autoLimit)
|
||||
sessionSnap.addResultError(id, error, vars.autoLimit, vars.warehouseResultSource)
|
||||
}
|
||||
|
||||
refocusEditorAfterRunIfNeeded()
|
||||
@@ -418,12 +428,14 @@ export const SQLEditor = () => {
|
||||
const userSql = rawSql(sql)
|
||||
const { appendAutoLimit } = checkIfAppendLimitRequired(userSql, limit)
|
||||
const formattedSql = suffixWithLimit(userSql, limit)
|
||||
const warehouseResultSource = resolveSqlWarehouseResultSource(userSql, searchPath)
|
||||
|
||||
execute({
|
||||
projectRef: project.ref,
|
||||
connectionString: connectionString,
|
||||
sql: wrapWithRoleImpersonation(formattedSql, impersonatedRoleState),
|
||||
autoLimit: appendAutoLimit ? limit : undefined,
|
||||
warehouseResultSource,
|
||||
isRoleImpersonationEnabled: isRoleImpersonationEnabled(impersonatedRoleState.role),
|
||||
isStatementTimeoutDisabled: true,
|
||||
contextualInvalidation: true,
|
||||
@@ -449,6 +461,7 @@ export const SQLEditor = () => {
|
||||
databases,
|
||||
eventTriggers,
|
||||
limit,
|
||||
searchPath,
|
||||
track,
|
||||
]
|
||||
)
|
||||
|
||||
@@ -1,18 +1,30 @@
|
||||
import { Hotkey } from '@tanstack/react-hotkeys'
|
||||
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
|
||||
import { AlignLeft, Check, Heart, Keyboard, MoreVertical } from 'lucide-react'
|
||||
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 { SqlEditorSearchPathSelector } from './SqlEditorSearchPathSelector'
|
||||
import {
|
||||
DEFAULT_SQL_EDITOR_SEARCH_PATH,
|
||||
getSqlEditorSearchPathStorageKey,
|
||||
SqlEditorSearchPathSelector,
|
||||
} from './SqlEditorSearchPathSelector'
|
||||
import { sqlEditorWarehouseDemoStore } from './SqlEditorWarehouseDemo'
|
||||
import { SqlRunButton } from './UtilityPanel/RunButton'
|
||||
import SavingIndicator from './UtilityPanel/SavingIndicator'
|
||||
import { SqlEditorLimitSelector } from './UtilityPanel/SqlEditorLimitSelector'
|
||||
@@ -20,6 +32,7 @@ import { RoleImpersonationPopover } from '@/components/interfaces/RoleImpersonat
|
||||
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'
|
||||
@@ -34,6 +47,8 @@ export type SqlEditorQueryBarProps = {
|
||||
executeQuery: () => void
|
||||
}
|
||||
|
||||
const SEPARATE_QUERY_CONTROLS_INLINE_MIN_WIDTH = 896
|
||||
|
||||
export function SqlEditorQueryBar({
|
||||
id,
|
||||
isExecuting = false,
|
||||
@@ -45,6 +60,10 @@ export function SqlEditorQueryBar({
|
||||
const { ref } = useParams()
|
||||
const snapV2 = useSqlEditorV2StateSnapshot()
|
||||
const sessionSnap = useSqlEditorSessionSnapshot()
|
||||
const roleImpersonationState = useRoleImpersonationStateSnapshot()
|
||||
const warehouseDemoSnap = useSnapshot(sqlEditorWarehouseDemoStore)
|
||||
const queryBarRef = useRef<HTMLDivElement>(null)
|
||||
const [areSeparateQueryControlsCollapsed, setAreSeparateQueryControlsCollapsed] = useState(false)
|
||||
|
||||
const [intelliSenseEnabled, setIntellisenseEnabled] = useLocalStorageQuery(
|
||||
LOCAL_STORAGE_KEYS.SQL_EDITOR_INTELLISENSE,
|
||||
@@ -54,24 +73,34 @@ export function SqlEditorQueryBar({
|
||||
LOCAL_STORAGE_KEYS.SQL_EDITOR_LAST_SELECTED_DB(ref as string),
|
||||
''
|
||||
)
|
||||
const [searchPath] = useLocalStorageQuery(
|
||||
getSqlEditorSearchPathStorageKey(ref ?? 'unknown'),
|
||||
DEFAULT_SQL_EDITOR_SEARCH_PATH
|
||||
)
|
||||
|
||||
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 toggleIntellisense = () => {
|
||||
setIntellisenseEnabled(!intelliSenseEnabled)
|
||||
const handleIntelliSenseChange = (checked: boolean) => {
|
||||
setIntellisenseEnabled(checked)
|
||||
toast.success(
|
||||
`Successfully ${intelliSenseEnabled ? 'disabled' : 'enabled'} intellisense. ${intelliSenseEnabled ? 'Please refresh your browser for changes to take place.' : ''}`
|
||||
`Successfully ${checked ? 'enabled' : 'disabled'} IntelliSense. ${checked ? '' : 'Please refresh your browser for changes to take place.'}`
|
||||
)
|
||||
}
|
||||
|
||||
const toggleFavorite = () => {
|
||||
if (isFavorite) snapV2.removeFavorite(id)
|
||||
else snapV2.addFavorite(id)
|
||||
if (isFavorite) {
|
||||
snapV2.removeFavorite(id)
|
||||
toast.success('Removed from favourites')
|
||||
} else {
|
||||
snapV2.addFavorite(id)
|
||||
toast.success('Added to favourites')
|
||||
}
|
||||
}
|
||||
|
||||
const onSelectDatabase = (databaseId: string) => {
|
||||
@@ -79,8 +108,36 @@ export function SqlEditorQueryBar({
|
||||
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 (
|
||||
<div className="flex h-10 shrink-0 items-center justify-between border-b bg-dash-sidebar px-2 dark:bg-surface-100">
|
||||
<div
|
||||
ref={queryBarRef}
|
||||
className="flex h-10 shrink-0 items-center justify-between border-b bg-dash-sidebar px-2 dark:bg-surface-100 @container"
|
||||
>
|
||||
<div className="flex min-w-0 items-center">{IS_PLATFORM && <SavingIndicator id={id} />}</div>
|
||||
|
||||
<div className="flex items-center gap-x-2">
|
||||
@@ -94,7 +151,23 @@ export function SqlEditorQueryBar({
|
||||
aria-label="Query options"
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-52">
|
||||
<DropdownMenuContent align="end" className="w-72">
|
||||
{shouldShowCollapsedQueryControls && (
|
||||
<>
|
||||
<div className="space-y-1 px-1 py-1.5">
|
||||
<p className="px-2 py-1 text-xs text-foreground-lighter">Query context</p>
|
||||
<SeparateQueryControls
|
||||
onSelectDatabase={onSelectDatabase}
|
||||
selectedDatabaseId={lastSelectedDb}
|
||||
fullWidth
|
||||
/>
|
||||
</div>
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuLabel className="text-xs font-normal text-foreground-lighter">
|
||||
Actions
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuItem className="justify-between" onClick={prettifyQuery}>
|
||||
<span className="flex items-center gap-x-2">
|
||||
<AlignLeft size={14} strokeWidth={2} className="text-foreground-light" />
|
||||
@@ -102,40 +175,39 @@ export function SqlEditorQueryBar({
|
||||
</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>
|
||||
<DropdownMenuLabel className="text-xs font-normal text-foreground-lighter">
|
||||
Preferences
|
||||
</DropdownMenuLabel>
|
||||
{IS_PLATFORM && (
|
||||
<DropdownMenuCheckboxItem checked={isFavorite} onCheckedChange={toggleFavorite}>
|
||||
Favourite query
|
||||
</DropdownMenuCheckboxItem>
|
||||
)}
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={intelliSenseEnabled}
|
||||
onCheckedChange={handleIntelliSenseChange}
|
||||
>
|
||||
IntelliSense
|
||||
</DropdownMenuCheckboxItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{IS_PLATFORM && (
|
||||
<DatabaseSelector
|
||||
selectedDatabaseId={lastSelectedDb.length === 0 ? undefined : lastSelectedDb}
|
||||
onSelectId={onSelectDatabase}
|
||||
{isSeparateQueryControlMode ? (
|
||||
<div className="hidden items-center gap-x-2 @4xl:flex">
|
||||
<SeparateQueryControls
|
||||
onSelectDatabase={onSelectDatabase}
|
||||
selectedDatabaseId={lastSelectedDb}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<QueryContextDropdown
|
||||
currentRole={currentRole}
|
||||
onSelectDatabase={onSelectDatabase}
|
||||
searchPath={searchPath}
|
||||
selectedDatabaseId={lastSelectedDb}
|
||||
/>
|
||||
)}
|
||||
<SqlEditorSearchPathSelector />
|
||||
<RoleImpersonationPopover serviceRoleLabel="postgres" header="Run SQL query as a role" />
|
||||
<SqlEditorLimitSelector />
|
||||
<SqlRunButton
|
||||
hasSelection={hasSelection}
|
||||
@@ -147,3 +219,110 @@ export function SqlEditorQueryBar({
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
type QueryContextDropdownProps = {
|
||||
currentRole: string
|
||||
onSelectDatabase: (databaseId: string) => void
|
||||
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 && (
|
||||
<DatabaseSelector
|
||||
selectedDatabaseId={selectedDatabaseId.length === 0 ? undefined : selectedDatabaseId}
|
||||
onSelectId={onSelectDatabase}
|
||||
className={controlClassName}
|
||||
align="end"
|
||||
/>
|
||||
)}
|
||||
<SqlEditorSearchPathSelector className={controlClassName} align="end" />
|
||||
<RoleImpersonationPopover
|
||||
serviceRoleLabel="postgres"
|
||||
header="Run SQL query as a role"
|
||||
align="end"
|
||||
className={fullWidth ? 'w-full justify-start [&>span]:w-full' : undefined}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function QueryContextDropdown({
|
||||
currentRole,
|
||||
onSelectDatabase,
|
||||
searchPath,
|
||||
selectedDatabaseId,
|
||||
}: QueryContextDropdownProps) {
|
||||
const { ref } = useParams()
|
||||
const isPrimaryConnection = selectedDatabaseId.length === 0 || selectedDatabaseId === ref
|
||||
const selectedConnectionLabel = isPrimaryConnection ? 'Primary' : 'Replica'
|
||||
const activeContextLabels = [
|
||||
!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 (
|
||||
<Popover modal={false}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button type="button" size="tiny" variant="default" className="h-[26px] justify-start pr-3">
|
||||
<div className="flex min-w-0 items-center gap-1">
|
||||
<span className="text-foreground-muted">Query</span>
|
||||
{contextSummary !== undefined && (
|
||||
<span className="hidden max-w-40 truncate text-foreground @2xl:inline">
|
||||
{contextSummary}
|
||||
</span>
|
||||
)}
|
||||
<ChevronDown className="shrink-0 text-muted" strokeWidth={1} size={12} />
|
||||
</div>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80 p-0" align="end" side="bottom">
|
||||
<div className="border-b px-4 py-3">
|
||||
<p className="text-sm text-foreground">Query context</p>
|
||||
<p className="text-xs leading-snug text-foreground-light">
|
||||
These settings apply together when this query runs.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-1 p-2">
|
||||
{IS_PLATFORM && (
|
||||
<DatabaseSelector
|
||||
selectedDatabaseId={selectedDatabaseId.length === 0 ? undefined : selectedDatabaseId}
|
||||
onSelectId={onSelectDatabase}
|
||||
className="w-full [&>span]:w-full"
|
||||
align="end"
|
||||
label="Connection"
|
||||
/>
|
||||
)}
|
||||
<SqlEditorSearchPathSelector className="w-full [&>span]:w-full" align="end" />
|
||||
<RoleImpersonationPopover
|
||||
serviceRoleLabel="postgres"
|
||||
header="Run SQL query as a role"
|
||||
align="end"
|
||||
className="w-full justify-start [&>span]:w-full"
|
||||
/>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
|
||||
import { useParams } from 'common'
|
||||
import { ChevronDown } from 'lucide-react'
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
Button,
|
||||
cn,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
@@ -12,59 +14,111 @@ import {
|
||||
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
|
||||
description?: string
|
||||
}
|
||||
|
||||
type SqlEditorSearchPathSelectorProps = {
|
||||
className?: string
|
||||
align?: 'start' | 'end'
|
||||
}
|
||||
|
||||
const SEARCH_PATH_OPTIONS: SearchPathOption[] = [
|
||||
{
|
||||
value: 'public',
|
||||
label: 'public',
|
||||
description: 'Default Postgres tables',
|
||||
},
|
||||
{
|
||||
value: 'public_warehouse',
|
||||
label: 'public_warehouse',
|
||||
description: 'Linked Warehouse tables',
|
||||
description: 'Warehouse linked schema',
|
||||
},
|
||||
{
|
||||
value: 'auth',
|
||||
label: 'auth',
|
||||
description: 'Auth schema',
|
||||
},
|
||||
{
|
||||
value: 'storage',
|
||||
label: 'storage',
|
||||
description: 'Storage schema',
|
||||
},
|
||||
{
|
||||
value: 'custom',
|
||||
label: 'Custom',
|
||||
description: 'Multiple schemas, e.g. "$user", public',
|
||||
},
|
||||
]
|
||||
|
||||
const DEFAULT_SEARCH_PATH = 'public'
|
||||
export const DEFAULT_SQL_EDITOR_SEARCH_PATH = 'public'
|
||||
const COMMON_SCHEMA_NAMES = ['public', 'auth', 'storage']
|
||||
export const getSqlEditorSearchPathStorageKey = (ref: string) => `sql-editor-search-path-${ref}`
|
||||
|
||||
function getSearchPathOption(value: string) {
|
||||
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 (
|
||||
SEARCH_PATH_OPTIONS.find((option) => option.value === value) ??
|
||||
SEARCH_PATH_OPTIONS.find((option) => option.value === 'custom')!
|
||||
options.find((option) => option.value === value) ?? {
|
||||
value,
|
||||
label: value,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export function SqlEditorSearchPathSelector() {
|
||||
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(
|
||||
LOCAL_STORAGE_KEYS.SQL_EDITOR_SEARCH_PATH(ref ?? 'unknown'),
|
||||
DEFAULT_SEARCH_PATH
|
||||
getSqlEditorSearchPathStorageKey(ref ?? 'unknown'),
|
||||
DEFAULT_SQL_EDITOR_SEARCH_PATH
|
||||
)
|
||||
const selectedOption = getSearchPathOption(searchPath)
|
||||
const isWarehousePath = selectedOption.value.endsWith('_warehouse')
|
||||
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 (
|
||||
<DropdownMenu>
|
||||
@@ -73,49 +127,83 @@ export function SqlEditorSearchPathSelector() {
|
||||
type="button"
|
||||
size="tiny"
|
||||
variant="default"
|
||||
className="h-[26px] justify-start gap-0 pr-3"
|
||||
className={cn('h-[26px] justify-start gap-0 pr-3', className)}
|
||||
>
|
||||
<div className="flex min-w-0 items-center gap-1">
|
||||
<span className="text-foreground-muted">Search path</span>
|
||||
<span
|
||||
className={cn(
|
||||
'max-w-32 truncate font-mono',
|
||||
isWarehousePath ? 'text-brand-link' : 'text-foreground'
|
||||
)}
|
||||
>
|
||||
{selectedOption.label}
|
||||
</span>
|
||||
<span className="max-w-32 truncate text-foreground">{selectedOption.label}</span>
|
||||
<ChevronDown className="shrink-0 text-muted" strokeWidth={1} size={12} />
|
||||
</div>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-72" align="end">
|
||||
<DropdownMenuContent className="w-72" align={align}>
|
||||
<DropdownMenuLabel className="flex flex-col gap-1">
|
||||
<span>Search path</span>
|
||||
<span className="text-xs font-normal leading-snug text-foreground-light">
|
||||
Unqualified table names resolve against this schema first. Custom represents a
|
||||
multi-schema path.
|
||||
Choose where your query looks first for unqualified table names.{' '}
|
||||
<a
|
||||
href="https://www.postgresql.org/docs/current/ddl-schemas.html#DDL-SCHEMAS-PATH"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-foreground underline decoration-foreground-lighter underline-offset-2 hover:decoration-foreground"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</span>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuRadioGroup value={selectedOption.value} onValueChange={setSearchPath}>
|
||||
{SEARCH_PATH_OPTIONS.map((option) => (
|
||||
<DropdownMenuRadioItem key={option.value} value={option.value}>
|
||||
<div className="flex min-w-0 flex-col">
|
||||
<span
|
||||
className={cn(
|
||||
'truncate',
|
||||
option.value.endsWith('_warehouse') ? 'text-brand-link' : undefined
|
||||
)}
|
||||
{standardSchemaOptions.length > 0 && (
|
||||
<>
|
||||
<DropdownMenuLabel className="text-xs font-normal text-foreground-lighter">
|
||||
{warehouseSchemaOptions.length > 0 ? 'Postgres schemas' : 'Schemas'}
|
||||
</DropdownMenuLabel>
|
||||
{visibleStandardSchemaOptions.map((option) => (
|
||||
<DropdownMenuRadioItem key={option.value} value={option.value}>
|
||||
<div className="flex min-w-0 flex-col">
|
||||
<span className="truncate">{option.label}</span>
|
||||
{option.description !== undefined && (
|
||||
<span className="truncate text-xs text-foreground-lighter">
|
||||
{option.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
{hiddenStandardSchemaCount > 0 && (
|
||||
<DropdownMenuItem
|
||||
className="text-foreground-light"
|
||||
onSelect={(event) => {
|
||||
event.preventDefault()
|
||||
setIsShowingMoreSchemas(true)
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
</span>
|
||||
<span className="truncate text-xs text-foreground-lighter">
|
||||
{option.description}
|
||||
</span>
|
||||
</div>
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
Show {hiddenStandardSchemaCount} more schema
|
||||
{hiddenStandardSchemaCount === 1 ? '' : 's'}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{warehouseSchemaOptions.length > 0 && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel className="text-xs font-normal text-foreground-lighter">
|
||||
Warehouse schemas
|
||||
</DropdownMenuLabel>
|
||||
{warehouseSchemaOptions.map((option) => (
|
||||
<DropdownMenuRadioItem key={option.value} value={option.value}>
|
||||
<div className="flex min-w-0 flex-col">
|
||||
<span className="truncate">{option.label}</span>
|
||||
{option.description !== undefined && (
|
||||
<span className="truncate text-xs text-foreground-lighter">
|
||||
{option.description}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from 'ui'
|
||||
import { proxy, useSnapshot } from 'valtio'
|
||||
|
||||
export type SqlWarehouseHealthPreset = 'healthy' | 'catching_up' | 'degraded' | 'error'
|
||||
export type SqlEditorQueryControlMode = 'compact' | 'separate'
|
||||
|
||||
export type SqlWarehouseResultSource =
|
||||
| { 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<SqlWarehouseHealthPreset, string> = {
|
||||
healthy: '',
|
||||
catching_up: 'Catching up',
|
||||
degraded: 'Degraded',
|
||||
error: 'Error',
|
||||
}
|
||||
|
||||
const SQL_WAREHOUSE_HEALTH_TOOLTIPS: Record<SqlWarehouseHealthPreset, string> = {
|
||||
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<SqlWarehouseHealthPreset, string> = {
|
||||
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
|
||||
): SqlWarehouseResultSource | undefined {
|
||||
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 === '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 (
|
||||
<span className="inline-flex min-w-0 items-center gap-1.5 truncate text-xs">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="group inline-flex cursor-default text-foreground-light transition-colors hover:text-foreground">
|
||||
<span className="border-b border-dotted border-current/40 transition-colors group-hover:border-current/70">
|
||||
Warehouse
|
||||
</span>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" className="max-w-72">
|
||||
{getWarehouseResultSourceTooltip(source)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
{shouldShowHealth && (
|
||||
<>
|
||||
<span className="shrink-0 text-foreground-muted">·</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span
|
||||
className={`group inline-flex cursor-default transition-colors ${SQL_WAREHOUSE_HEALTH_CLASS_NAMES[health]}`}
|
||||
>
|
||||
<span className="border-b border-dotted border-current/40 transition-colors group-hover:border-current/70">
|
||||
{SQL_WAREHOUSE_HEALTH_LABELS[health]}
|
||||
</span>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" className="max-w-72">
|
||||
{SQL_WAREHOUSE_HEALTH_TOOLTIPS[health]}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -1,63 +1,47 @@
|
||||
import { cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from 'ui'
|
||||
|
||||
interface SqlResultsSummaryProps {
|
||||
rowCount: number
|
||||
autoLimit?: number
|
||||
}
|
||||
|
||||
type ResultsHealth = 'complete' | 'limited'
|
||||
|
||||
const RESULTS_HEALTH_LABEL: Record<ResultsHealth, string> = {
|
||||
complete: 'Complete',
|
||||
limited: 'Limited',
|
||||
}
|
||||
|
||||
const RESULTS_HEALTH_CLASS_NAME: Record<ResultsHealth, string> = {
|
||||
complete: 'text-foreground-light hover:text-foreground',
|
||||
limited: 'text-warning hover:text-warning-600',
|
||||
}
|
||||
|
||||
function formatRowCount(rowCount: number) {
|
||||
return `${rowCount.toLocaleString()} row${rowCount === 1 ? '' : 's'}`
|
||||
}
|
||||
|
||||
export function SqlResultsSummary({ rowCount, autoLimit }: SqlResultsSummaryProps) {
|
||||
const health: ResultsHealth = autoLimit !== undefined ? 'limited' : 'complete'
|
||||
const hasReachedLimit = autoLimit !== undefined && rowCount >= autoLimit
|
||||
|
||||
return (
|
||||
<p className="flex min-w-0 items-center gap-1.5 truncate text-xs text-foreground-lighter">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span
|
||||
className={cn(
|
||||
'group inline-flex cursor-default transition-colors',
|
||||
RESULTS_HEALTH_CLASS_NAME[health]
|
||||
)}
|
||||
>
|
||||
<span className="border-b border-dotted border-current/40 transition-colors group-hover:border-current/70">
|
||||
{RESULTS_HEALTH_LABEL[health]}
|
||||
</span>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-xs">
|
||||
{health === 'limited' ? (
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<span>
|
||||
Results are limited to preserve browser performance when a query returns many rows.
|
||||
{hasReachedLimit && (
|
||||
<>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="group inline-flex cursor-default text-warning transition-colors hover:text-warning-600">
|
||||
<span className="border-b border-dotted border-current/40 transition-colors group-hover:border-current/70">
|
||||
Limited
|
||||
</span>
|
||||
</span>
|
||||
<span className="text-foreground-light">
|
||||
Change or remove this limit from the dropdown near Run.
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
'Query completed without applying the SQL Editor row limit.'
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<span className="shrink-0 text-foreground-muted">·</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-xs">
|
||||
<div 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>
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<span className="shrink-0 text-foreground-muted">·</span>
|
||||
</>
|
||||
)}
|
||||
<span className="truncate text-foreground">
|
||||
{formatRowCount(rowCount)}
|
||||
{autoLimit !== undefined && (
|
||||
{hasReachedLimit && (
|
||||
<span className="ml-1 text-foreground-lighter">(limit {autoLimit.toLocaleString()})</span>
|
||||
)}
|
||||
</span>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useParams } from 'common'
|
||||
import { toast } from 'sonner'
|
||||
import { Tabs_Shadcn_, TabsContent_Shadcn_, TabsList_Shadcn_, TabsTrigger_Shadcn_ } from 'ui'
|
||||
|
||||
import { SqlWarehouseResultStatus } from '../SqlEditorWarehouseDemo'
|
||||
import { ChartConfig } from './ChartConfig'
|
||||
import { SqlResultsSummary } from './SqlResultsSummary'
|
||||
import { UtilityTabExplain } from './UtilityTabExplain'
|
||||
@@ -151,7 +152,12 @@ export const UtilityPanel = ({
|
||||
|
||||
<div className="flex min-w-0 items-center gap-x-3">
|
||||
{showResultsSummary && (
|
||||
<SqlResultsSummary rowCount={result.rows.length} autoLimit={result.autoLimit} />
|
||||
<>
|
||||
{result.warehouseResultSource && (
|
||||
<SqlWarehouseResultStatus source={result.warehouseResultSource} />
|
||||
)}
|
||||
<SqlResultsSummary rowCount={result.rows.length} autoLimit={result.autoLimit} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{result?.rows && (
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
|
||||
import { ButtonTooltip } from '../ui/ButtonTooltip'
|
||||
import { useFeaturePreviewModal } from './App/FeaturePreview/FeaturePreviewContext'
|
||||
import { SqlEditorWarehouseDemoMenu } from './UserDropdown/SqlEditorWarehouseDemoMenu'
|
||||
import { TimezoneDropdown } from './UserDropdown/TimezoneDropdown'
|
||||
import { ProfileImage } from '@/components/ui/ProfileImage'
|
||||
import { UpgradePlanButton } from '@/components/ui/UpgradePlanButton'
|
||||
@@ -136,6 +137,7 @@ export function UserDropdown({
|
||||
Changelog
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<SqlEditorWarehouseDemoMenu />
|
||||
<DropdownMenuSeparator />
|
||||
</DropdownMenuGroup>
|
||||
</>
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger className="flex gap-2">
|
||||
<Database size={14} strokeWidth={1.5} className="text-foreground-lighter" />
|
||||
SQL Warehouse demo
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent className="w-52">
|
||||
<DropdownMenuLabel className="text-xs text-foreground-lighter">
|
||||
Results health
|
||||
</DropdownMenuLabel>
|
||||
{WAREHOUSE_HEALTH_PRESETS.map(({ preset, label }) => (
|
||||
<DropdownMenuItem
|
||||
key={preset}
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
setSqlEditorWarehouseHealth(preset)
|
||||
toast.message(`SQL Warehouse demo: ${label}`)
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel className="text-xs text-foreground-lighter">
|
||||
Query control
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuRadioGroup
|
||||
value={snap.queryControlMode}
|
||||
onValueChange={(value) => {
|
||||
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 }) => (
|
||||
<DropdownMenuRadioItem key={mode} value={mode} className="cursor-pointer">
|
||||
{label}
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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()
|
||||
@@ -85,9 +87,7 @@ export const DatabaseSelector = ({
|
||||
) : (
|
||||
<>
|
||||
<span className="capitalize">
|
||||
{isLoading || selectedDatabase?.identifier === projectRef
|
||||
? 'Primary database'
|
||||
: 'Read replica'}
|
||||
{isLoading || selectedDatabase?.identifier === projectRef ? 'Primary' : 'Read replica'}
|
||||
</span>
|
||||
{isSuccess && selectedDatabase?.identifier !== projectRef && (
|
||||
<span>
|
||||
@@ -132,7 +132,7 @@ export const DatabaseSelector = ({
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-foreground-muted">Source</span>
|
||||
<span className="text-foreground-muted">{label}</span>
|
||||
<span className="flex items-center gap-1">
|
||||
{isLoading ? <Loader2 className="animate-spin" size={12} /> : selectedDatabaseLabel}
|
||||
</span>
|
||||
@@ -222,7 +222,7 @@ export const DatabaseSelector = ({
|
||||
<div className="w-full flex items-center justify-between">
|
||||
<p>
|
||||
{database.identifier === projectRef
|
||||
? 'Primary database'
|
||||
? 'Primary'
|
||||
: `Read replica (${region} - ${id})`}
|
||||
</p>
|
||||
{database.identifier === selectedDatabaseId && <Check size={16} />}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in new issue
Block a user