diff --git a/apps/docs/components/Navigation/NavigationMenu/useDropdownMenu.tsx b/apps/docs/components/Navigation/NavigationMenu/useDropdownMenu.tsx index ececa37033a04..ece8c083cd8bf 100644 --- a/apps/docs/components/Navigation/NavigationMenu/useDropdownMenu.tsx +++ b/apps/docs/components/Navigation/NavigationMenu/useDropdownMenu.tsx @@ -69,7 +69,7 @@ const useDropdownMenu = (user: User | null) => { ], [ { - label: 'Logout', + label: 'Sign out', type: 'button', icon: LogOut, onClick: async () => { diff --git a/apps/learn/components/side-navigation.tsx b/apps/learn/components/side-navigation.tsx index 3668830c1a3e1..de40a76561b3d 100644 --- a/apps/learn/components/side-navigation.tsx +++ b/apps/learn/components/side-navigation.tsx @@ -45,7 +45,7 @@ function SideNavigation({ internalPaths }: SideNavigationProps) { ], [ { - label: 'Logout', + label: 'Sign out', type: 'button', icon: LogOut, onClick: async () => { diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/AdvancedSettings.tsx b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/AdvancedSettings.tsx index 877915b1e54e8..23b147e9699f8 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/AdvancedSettings.tsx +++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/AdvancedSettings.tsx @@ -5,7 +5,6 @@ import { AccordionContent, AccordionItem, AccordionTrigger, - Badge, FormControl, FormField, FormInputGroupInput, @@ -42,8 +41,8 @@ export const AdvancedSettings = ({ }) => { const handleNumberChange = (field: { onChange: (value?: number) => void }) => (e: ChangeEvent) => { - const val = e.target.value - field.onChange(val === '' ? undefined : Number(val)) + const parsed = e.target.valueAsNumber + field.onChange(e.target.value === '' || Number.isNaN(parsed) ? undefined : parsed) } return ( @@ -59,7 +58,6 @@ export const AdvancedSettings = ({ - {/* Batch wait time - applies to all destinations */} {INVALIDATED_SLOT_BEHAVIOR_LABELS[field.value ?? 'error']} - +

Block startup

@@ -188,12 +186,7 @@ export const AdvancedSettings = ({ name="connectionPoolSize" render={({ field }) => ( - Connection pool size - BigQuery only - - } + label="Connection pool size" layout="horizontal" description="Number of BigQuery connections used for destination writes." > @@ -222,12 +215,7 @@ export const AdvancedSettings = ({ name="maxStalenessMins" render={({ field }) => ( - Maximum staleness - BigQuery only - - } + label="Maximum staleness" layout="horizontal" description="Set the maximum age of query results while BigQuery applies ongoing changes, or leave blank for the freshest results." > diff --git a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/useRefreshOnOpen.ts b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/useRefreshOnOpen.ts index 16394d33fa59a..5b02e4ddf81a1 100644 --- a/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/useRefreshOnOpen.ts +++ b/apps/studio/components/interfaces/Database/Replication/DestinationPanel/DestinationForm/useRefreshOnOpen.ts @@ -1,4 +1,4 @@ -import { useCallback } from 'react' +import { useMemo } from 'react' // Replication metadata (publication names, publication tables, source tables, // columns) and similar destination-form option lists follow one rule: @@ -26,12 +26,11 @@ interface UseRefreshOnOpenProps { } export const useRefreshOnOpen = ({ isEnabled = true, refetch }: UseRefreshOnOpenProps) => { - const handleOpenChange = useCallback( - (isOpen: boolean) => { + return useMemo(() => { + const handleOpenChange = (isOpen: boolean) => { if (isOpen && isEnabled) void refetch() - }, - [isEnabled, refetch] - ) + } - return { handleOpenChange } + return { handleOpenChange } + }, [isEnabled, refetch]) } diff --git a/apps/studio/components/interfaces/Explorer/QueryCell/index.tsx b/apps/studio/components/interfaces/Explorer/QueryCell/index.tsx index cdbff562c8fd1..73df59b831595 100644 --- a/apps/studio/components/interfaces/Explorer/QueryCell/index.tsx +++ b/apps/studio/components/interfaces/Explorer/QueryCell/index.tsx @@ -136,7 +136,7 @@ export const QueryCell = forwardRef(function onDisplayChange={handleDisplayChange} toolbarActions={ } + icon={} tooltip={

Prettify SQL diff --git a/apps/studio/components/interfaces/Explorer/QueryEditor/QueryResultChart.tsx b/apps/studio/components/interfaces/Explorer/QueryEditor/QueryResultChart.tsx index 095ad7af7623b..acfb7cd7e9465 100644 --- a/apps/studio/components/interfaces/Explorer/QueryEditor/QueryResultChart.tsx +++ b/apps/studio/components/interfaces/Explorer/QueryEditor/QueryResultChart.tsx @@ -1,10 +1,15 @@ import { useMemo } from 'react' -import { type ChartConfig as ChartSeriesConfig } from 'ui' +import { cn, type ChartConfig as ChartSeriesConfig } from 'ui' import { Chart, ChartBar, ChartCard, ChartContent, ChartLine } from 'ui-patterns/Chart' import { type QueryResult } from '../types' import NoDataPlaceholder from '@/components/ui/Charts/NoDataPlaceholder' -import { formatLogTick, getCumulativeResults } from '@/components/ui/QueryBlock/QueryBlock.utils' +import { + computeYAxisWidth, + formatLogTick, + formatYAxisTick, + getCumulativeResults, +} from '@/components/ui/QueryBlock/QueryBlock.utils' import { type ChartConfig } from '@/data/content/notebooks/notebook-schema' interface QueryResultChartProps { @@ -58,6 +63,20 @@ export const QueryResultChart = ({ chart, result }: QueryResultChartProps) => { ) const resultToRender = cumulative ? cumulativeResults : chartRows + const yAxisWidth = Math.max( + 36, + ...y_series.map((key) => + computeYAxisWidth(resultToRender, key, { isLogScale: effectiveScale === 'log' }) + ) + ) + + const yAxisProps = { + ...(show_labels ? { width: yAxisWidth } : {}), + scale: effectiveScale === 'log' ? 'log' : 'auto', + domain: effectiveScale === 'log' ? ([1, 'auto'] as const) : undefined, + tickFormatter: effectiveScale === 'log' ? formatLogTick : formatYAxisTick, + } + if (!result || (result?.rows && result.rows.length === 0)) { return ( { return ( - + {type === 'bar' && ( { showXAxis={show_labels} showYAxis={show_labels} data={resultToRender} - YAxisProps={{ - scale: effectiveScale === 'log' ? 'log' : 'auto', - domain: effectiveScale === 'log' ? [1, 'auto'] : undefined, - tickFormatter: effectiveScale === 'log' ? formatLogTick : undefined, - }} + YAxisProps={yAxisProps} /> )} {type === 'line' && ( @@ -113,11 +128,7 @@ export const QueryResultChart = ({ chart, result }: QueryResultChartProps) => { showXAxis={show_labels} showYAxis={show_labels} data={resultToRender} - YAxisProps={{ - scale: effectiveScale === 'log' ? 'log' : 'auto', - domain: effectiveScale === 'log' ? [1, 'auto'] : undefined, - tickFormatter: effectiveScale === 'log' ? formatLogTick : undefined, - }} + YAxisProps={yAxisProps} /> )} diff --git a/apps/studio/components/interfaces/Explorer/QueryEditor/QueryRunButton.tsx b/apps/studio/components/interfaces/Explorer/QueryEditor/QueryRunButton.tsx new file mode 100644 index 0000000000000..bac5df8a5a92c --- /dev/null +++ b/apps/studio/components/interfaces/Explorer/QueryEditor/QueryRunButton.tsx @@ -0,0 +1,84 @@ +import { ChevronDown, Play } from 'lucide-react' +import { + Button, + DropdownMenu, + DropdownMenuContent, + DropdownMenuTrigger, + KeyboardShortcut, +} from 'ui' + +import { ButtonTooltip } from '@/components/ui/ButtonTooltip' +import { DropdownMenuItemTooltip } from '@/components/ui/DropdownMenuItemTooltip' + +interface QueryRunButtonProps { + isExecuting: boolean + disabled: boolean + hasSelection: boolean + onRun: () => void + onRunSelected: () => void +} + +export const QueryRunButton = ({ + isExecuting, + disabled, + hasSelection, + onRun, + onRunSelected, +}: QueryRunButtonProps) => { + return ( +
+ } + className="rounded-r-none hover:z-10 focus-visible:z-10 focus-visible:rounded-r-sm" + onClick={onRun} + tooltip={{ + content: { + side: 'bottom', + text: ( +
+ Run query + +
+ ), + }, + }} + > + Run +
+ + +
+ ) +} diff --git a/apps/studio/components/interfaces/Explorer/QueryEditor/index.tsx b/apps/studio/components/interfaces/Explorer/QueryEditor/index.tsx index 761182822e76d..f8fc71760961e 100644 --- a/apps/studio/components/interfaces/Explorer/QueryEditor/index.tsx +++ b/apps/studio/components/interfaces/Explorer/QueryEditor/index.tsx @@ -1,7 +1,7 @@ import { useMonaco } from '@monaco-editor/react' import { acceptUntrustedSql, untrustedSql, type UntrustedSqlFragment } from '@supabase/pg-meta' import { useFlag } from 'common' -import { CodeSquare, Eye, EyeOff, Play } from 'lucide-react' +import { CodeSquare, Eye, EyeOff } from 'lucide-react' import type { editor as monacoEditor, Selection } from 'monaco-editor' import { forwardRef, @@ -12,7 +12,7 @@ import { useState, type ReactNode, } from 'react' -import { Button, cn, KeyboardShortcut } from 'ui' +import { Button, cn } from 'ui' import { resolveLogTimeRange } from '../../QuerySources/LogTimeRange.utils' import { @@ -32,6 +32,7 @@ import { import { type QueryDisplay, type QueryResult } from '../types' import { DisplaySettingsButton } from './DisplaySettingsButton' import { QueryResultRenderer } from './QueryResultRenderer' +import { QueryRunButton } from './QueryRunButton' import { QuerySourceMenu } from './QuerySourceMenu' import { useQueryEditorAi } from './useQueryEditorAi' import { LegacyLogsRewriteBanner } from '@/components/interfaces/Settings/Logs/LegacyLogsRewriteBanner' @@ -437,26 +438,19 @@ export const QueryEditor = forwardRef(funct {toolbarActions} - } - loading={isExecuting} - tooltip={ -
- {hasSelection ? 'Run selected query' : 'Run query'} - -
- } + { + hasSelection={showQuery && hasSelection} + onRun={() => handleRunQuery({ rawSql: sql })} + onRunSelected={() => { const editorInstance = editorInstanceRef.current const rawSql = editorInstance ? getEditorValueOrSelection(editorInstance) : sql handleRunQuery({ rawSql }) }} - > - {hasSelection ? 'Run selected' : 'Run'} -
+ /> diff --git a/apps/studio/components/interfaces/Explorer/__tests__/QueryTab.test.tsx b/apps/studio/components/interfaces/Explorer/__tests__/QueryTab.test.tsx index a7ddb608be280..e9c2dbf799f4d 100644 --- a/apps/studio/components/interfaces/Explorer/__tests__/QueryTab.test.tsx +++ b/apps/studio/components/interfaces/Explorer/__tests__/QueryTab.test.tsx @@ -429,7 +429,34 @@ describe('QueryTab execution', () => { expect(executedQueries[0]).toContain('ALTER TABLE foo ENABLE ROW LEVEL SECURITY;') }) - it('runs only the selected text, not the full editor content, when there is an active selection', async () => { + it('runs the full query from the Run button even when text is selected', async () => { + createDraft({ _tag: 'database' }, 'select 1;\nselect 2;') + testContext.selectedText = 'select 2;' + + const executedQueries: string[] = [] + addAPIMock({ + method: 'post', + path: '/platform/pg-meta/:ref/query', + response: async ({ request }) => { + const key = new URL(request.url).searchParams.get('key') + if (key !== '') return HttpResponse.json([]) + const { query } = (await request.json()) as { query: string } + executedQueries.push(query) + return HttpResponse.json([]) + }, + }) + + renderQueryTab() + const runButton = await screen.findByRole('button', { name: 'Run' }) + await waitFor(() => expect(runButton).toBeEnabled()) + await userEvent.click(runButton) + + await waitFor(() => expect(executedQueries).toHaveLength(1)) + expect(executedQueries[0]).toContain('select 1') + expect(executedQueries[0]).toContain('select 2') + }) + + it('runs only the selected text from the Run selected menu item', async () => { createDraft({ _tag: 'database' }, 'select 1;\nselect 2;') testContext.selectedText = 'select 2;' @@ -450,9 +477,11 @@ describe('QueryTab execution', () => { }) renderQueryTab() - const runButton = await screen.findByRole('button', { name: 'Run selected' }) + const runButton = await screen.findByRole('button', { name: 'Run' }) await waitFor(() => expect(runButton).toBeEnabled()) - await userEvent.click(runButton) + + await userEvent.click(screen.getByRole('button', { name: 'More actions' })) + await userEvent.click(await screen.findByRole('menuitem', { name: 'Run selected' })) await waitFor(() => expect(executedQueries).toHaveLength(1)) expect(executedQueries[0]).toContain('select 2') @@ -477,7 +506,9 @@ describe('QueryTab execution', () => { }) renderQueryTab() - expect(await screen.findByRole('button', { name: 'Run selected' })).toBeInTheDocument() + await userEvent.click(await screen.findByRole('button', { name: 'More actions' })) + expect(await screen.findByRole('menuitem', { name: 'Run selected' })).toBeEnabled() + await userEvent.keyboard('{Escape}') // Hiding the query panel unmounts CodeEditor entirely. Simulate the selection being // gone by the time it's shown again (a fresh editor instance has no selection yet). @@ -490,9 +521,14 @@ describe('QueryTab execution', () => { expect(showQueryButton).toBeInstanceOf(HTMLButtonElement) await userEvent.click(showQueryButton as HTMLButtonElement) - const runButton = await screen.findByRole('button', { name: 'Run' }) - expect(screen.queryByRole('button', { name: 'Run selected' })).not.toBeInTheDocument() + await userEvent.click(screen.getByRole('button', { name: 'More actions' })) + expect(await screen.findByRole('menuitem', { name: 'Run selected' })).toHaveAttribute( + 'aria-disabled', + 'true' + ) + await userEvent.keyboard('{Escape}') + const runButton = await screen.findByRole('button', { name: 'Run' }) await userEvent.click(runButton) await waitFor(() => expect(executedQueries).toHaveLength(1)) diff --git a/apps/studio/components/interfaces/UserDropdown.tsx b/apps/studio/components/interfaces/UserDropdown.tsx index 57ba1a63ff008..1aa8466326305 100644 --- a/apps/studio/components/interfaces/UserDropdown.tsx +++ b/apps/studio/components/interfaces/UserDropdown.tsx @@ -200,7 +200,7 @@ export function UserDropdown({ router.push('/logout') }} > - Log out + Sign out diff --git a/apps/studio/components/ui/AIAssistantPanel/AssistantChatForm.tsx b/apps/studio/components/ui/AIAssistantPanel/AssistantChatForm.tsx index de4cbaf973041..1f8b9edd767d3 100644 --- a/apps/studio/components/ui/AIAssistantPanel/AssistantChatForm.tsx +++ b/apps/studio/components/ui/AIAssistantPanel/AssistantChatForm.tsx @@ -122,7 +122,7 @@ const AssistantChatFormComponent = forwardRef( ref={textAreaRef} disabled={disabled} className={cn( - 'text-base md:text-sm pr-10 pb-9 max-h-64', + 'text-base md:text-sm pr-10 pb-9 max-h-64 rounded-lg', sqlSnippets && sqlSnippets.length > 0 && 'pt-10' )} placeholder={placeholder} diff --git a/apps/www/components/Nav/useDropdownMenu.tsx b/apps/www/components/Nav/useDropdownMenu.tsx index 7378aa483c7ad..81c18b1a462a0 100644 --- a/apps/www/components/Nav/useDropdownMenu.tsx +++ b/apps/www/components/Nav/useDropdownMenu.tsx @@ -44,7 +44,7 @@ const useDropdownMenu = (user: User | null) => { ], [ { - label: 'Logout', + label: 'Sign out', type: 'button', icon: LogOut, onClick: async () => { diff --git a/packages/ui-patterns/src/Chart/charts/chart-bar.tsx b/packages/ui-patterns/src/Chart/charts/chart-bar.tsx index 7367ca8efb865..49825a3626541 100644 --- a/packages/ui-patterns/src/Chart/charts/chart-bar.tsx +++ b/packages/ui-patterns/src/Chart/charts/chart-bar.tsx @@ -66,6 +66,12 @@ export interface ChartBarProps { showGrid?: boolean showYAxis?: boolean showXAxis?: boolean + XAxisProps?: { + tick?: boolean + tickFormatter?: (value: any) => string + height?: number + [key: string]: any + } YAxisProps?: { tick?: boolean tickFormatter?: (value: any) => string @@ -96,6 +102,7 @@ export const ChartBar = ({ showGrid = false, showYAxis = false, showXAxis = false, + XAxisProps, YAxisProps, }: ChartBarProps) => { const [focusDataIndex, setFocusDataIndex] = useState(null) @@ -132,8 +139,11 @@ export const ChartBar = ({ : false, hide: !showXAxis, interval: 'preserveStartEnd' as const, + tickMargin: showXAxis ? (XAxisProps?.tickMargin ?? 4) : 0, + height: showXAxis ? (XAxisProps?.height ?? 24) : 0, axisLine: { stroke: CHART_COLORS.AXIS }, tickLine: { stroke: CHART_COLORS.AXIS }, + ...XAxisProps, } const yAxisConfig = { diff --git a/packages/ui-patterns/src/Chart/charts/chart-line.tsx b/packages/ui-patterns/src/Chart/charts/chart-line.tsx index a2571ff78c540..c100f8100fde4 100644 --- a/packages/ui-patterns/src/Chart/charts/chart-line.tsx +++ b/packages/ui-patterns/src/Chart/charts/chart-line.tsx @@ -74,6 +74,12 @@ export interface ChartLineProps { showGrid?: boolean showXAxis?: boolean showYAxis?: boolean + XAxisProps?: { + tick?: boolean + tickFormatter?: (value: any) => string + height?: number + [key: string]: any + } YAxisProps?: { tick?: boolean tickFormatter?: (value: any) => string @@ -106,6 +112,7 @@ export const ChartLine = ({ showGrid = false, showXAxis = false, showYAxis = false, + XAxisProps, YAxisProps, strokeWidth = 1.5, referenceLines, @@ -143,8 +150,11 @@ export const ChartLine = ({ : false, hide: !showXAxis, interval: 'preserveStartEnd' as const, + tickMargin: showXAxis ? (XAxisProps?.tickMargin ?? 4) : 0, + height: showXAxis ? (XAxisProps?.height ?? 24) : 0, axisLine: { stroke: CHART_COLORS.AXIS }, tickLine: { stroke: CHART_COLORS.AXIS }, + ...XAxisProps, } const yAxisConfig = {