diff --git a/apps/studio/components/grid/components/footer/Footer.tsx b/apps/studio/components/grid/components/footer/Footer.tsx
index 98d023d30c1..e2482f54bb1 100644
--- a/apps/studio/components/grid/components/footer/Footer.tsx
+++ b/apps/studio/components/grid/components/footer/Footer.tsx
@@ -4,6 +4,7 @@ import { useParams } from 'common'
import TwoOptionToggle from 'components/ui/TwoOptionToggle'
import { useUrlState } from 'hooks'
import RefreshButton from '../header/RefreshButton'
+import { GridFooter } from 'components/ui/GridFooter'
export interface FooterProps {
isLoading?: boolean
@@ -26,8 +27,8 @@ const Footer = ({ isLoading, isRefetching }: FooterProps) => {
}
return (
-
- {selectedView === 'data' &&
}
+
+ {selectedView === 'data' && }
{selectedTable && selectedView === 'data' && (
@@ -42,7 +43,7 @@ const Footer = ({ isLoading, isRefetching }: FooterProps) => {
onClickOption={setSelectedView}
/>
-
+
)
}
diff --git a/apps/studio/components/grid/components/footer/pagination/Pagination.tsx b/apps/studio/components/grid/components/footer/pagination/Pagination.tsx
index 5df372be147..f9d391e2c76 100644
--- a/apps/studio/components/grid/components/footer/pagination/Pagination.tsx
+++ b/apps/studio/components/grid/components/footer/pagination/Pagination.tsx
@@ -1,15 +1,16 @@
+import { ArrowLeft, ArrowRight } from 'lucide-react'
import { useEffect, useState } from 'react'
import { formatFilterURLParams } from 'components/grid/SupabaseGrid.utils'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
-import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { useTableRowsCountQuery } from 'data/table-rows/table-rows-count-query'
import { useUrlState } from 'hooks'
import { useRoleImpersonationStateSnapshot } from 'state/role-impersonation-state'
-import { Button, IconArrowLeft, IconArrowRight, IconLoader, InputNumber, Modal } from 'ui'
+import { useTableEditorStateSnapshot } from 'state/table-editor'
+import { Button, InputNumber } from 'ui'
+import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { useDispatch, useTrackedState } from '../../../store'
import { DropdownControl } from '../../common'
-import { useTableEditorStateSnapshot } from 'state/table-editor'
const rowsPerPageOptions = [
{ value: 100, label: '100 rows' },
@@ -17,11 +18,7 @@ const rowsPerPageOptions = [
{ value: 1000, label: '1000 rows' },
]
-export interface PaginationProps {
- isLoading?: boolean
-}
-
-const Pagination = ({ isLoading: isLoadingRows = false }: PaginationProps) => {
+const Pagination = () => {
const state = useTrackedState()
const dispatch = useDispatch()
@@ -144,11 +141,11 @@ const Pagination = ({ isLoading: isLoadingRows = false }: PaginationProps) => {
{isSuccess && (
<>
}
+ icon={}
type="outline"
+ className="px-1.5"
disabled={page <= 1 || isLoading}
onClick={onPreviousPage}
- style={{ padding: '3px 10px' }}
/>
Page
@@ -165,13 +162,13 @@ const Pagination = ({ isLoading: isLoadingRows = false }: PaginationProps) => {
min={1}
/>
- {`of ${totalPages}`}
+ of {totalPages}
}
+ icon={}
type="outline"
+ className="px-1.5"
disabled={page >= maxPages || isLoading}
onClick={onNextPage}
- style={{ padding: '3px 10px' }}
/>
{
// Customers on HIPAA plans should not have access to Supabase AI
const hasHipaaAddon = subscriptionHasHipaaAddon(subscription)
- const [isAiOpen, setIsAiOpen] = useLocalStorageQuery('supabase_sql-editor-ai-open', true)
+ const [isAiOpen, setIsAiOpen] = useLocalStorageQuery(LOCAL_STORAGE_KEYS.SQL_EDITOR_AI_OPEN, true)
const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false)
const selectedOrganization = useSelectedOrganization()
@@ -169,13 +171,13 @@ const SQLEditor = () => {
}, [chatMessages])
const { mutate: execute, isLoading: isExecuting } = useExecuteSqlMutation({
- onSuccess(data) {
- if (id) snap.addResult(id, data.result)
+ onSuccess(data, vars) {
+ if (id) snap.addResult(id, data.result, vars.autoLimit)
// Refetching instead of invalidating since invalidate doesn't work with `enabled` flag
refetchEntityDefinitions()
},
- onError(error: any) {
+ onError(error: any, vars) {
if (id) {
if (error.position && monacoRef.current) {
const editor = editorRef.current
@@ -208,7 +210,7 @@ const SQLEditor = () => {
}
}
- snap.addResultError(id, error)
+ snap.addResultError(id, error, vars.autoLimit)
}
},
})
@@ -312,13 +314,17 @@ const SQLEditor = () => {
return toast.error('Unable to run query: Connection string is missing')
}
+ const { appendAutoLimit } = checkIfAppendLimitRequired(sql, snap.limit)
+ const formattedSql = suffixWithLimit(sql, snap.limit)
+
execute({
projectRef: project.ref,
connectionString: connectionString,
- sql: wrapWithRoleImpersonation(sql, {
+ sql: wrapWithRoleImpersonation(formattedSql, {
projectRef: project.ref,
role: impersonatedRole,
}),
+ autoLimit: appendAutoLimit ? snap.limit : undefined,
isRoleImpersonationEnabled: isRoleImpersonationEnabled(impersonatedRole),
handleError: (error) => {
throw error
diff --git a/apps/studio/components/interfaces/SQLEditor/SQLEditor.utils.test.ts b/apps/studio/components/interfaces/SQLEditor/SQLEditor.utils.test.ts
new file mode 100644
index 00000000000..5b6e1862c0a
--- /dev/null
+++ b/apps/studio/components/interfaces/SQLEditor/SQLEditor.utils.test.ts
@@ -0,0 +1,93 @@
+import { checkIfAppendLimitRequired, suffixWithLimit } from './SQLEditor.utils'
+
+describe('SQLEditor.utils.ts:checkIfAppendLimitRequired', () => {
+ test('Should return false if limit passed is <= 0', () => {
+ const sql = 'select * from countries;'
+ const limit = -1
+ const { appendAutoLimit } = checkIfAppendLimitRequired(sql, limit)
+ expect(appendAutoLimit).toBe(false)
+ })
+ test('Should return true if limit passed is > 0', () => {
+ const sql = 'select * from countries;'
+ const limit = 100
+ const { appendAutoLimit } = checkIfAppendLimitRequired(sql, limit)
+ expect(appendAutoLimit).toBe(true)
+ })
+ test('Should return false if query already has a limit', () => {
+ const sql = 'select * from countries limit 10;'
+ const limit = 100
+ const { appendAutoLimit } = checkIfAppendLimitRequired(sql, limit)
+ expect(appendAutoLimit).toBe(false)
+ })
+ test('Should return false if query already has a limit, even if no value provided for limit', () => {
+ const sql = 'select * from countries limit'
+ const limit = 100
+ const { appendAutoLimit } = checkIfAppendLimitRequired(sql, limit)
+ expect(appendAutoLimit).toBe(false)
+ })
+ test('Should return false if query is not a select statement', () => {
+ const sql = 'create table test (id int8 primary key, name varchar);'
+ const limit = 100
+ const { appendAutoLimit } = checkIfAppendLimitRequired(sql, limit)
+ expect(appendAutoLimit).toBe(false)
+ })
+ test('Should return false if there are multiple queries I', () => {
+ const sql1 = `
+select * from countries;
+select * from cities;
+`.trim()
+ const limit = 100
+ const { appendAutoLimit } = checkIfAppendLimitRequired(sql1, limit)
+ expect(appendAutoLimit).toBe(false)
+ })
+ test('Should return false if there are multiple queries II', () => {
+ const sql1 = `
+select * from countries;
+select * from cities
+`.trim()
+ const limit = 100
+ const { appendAutoLimit } = checkIfAppendLimitRequired(sql1, limit)
+ expect(appendAutoLimit).toBe(false)
+ })
+ // [Joshen] Opting to just avoid appending in this case to prevent making the logic overly complex atm
+ test('Should return false if query has with a comment I', () => {
+ const sql = `
+-- This is a comment
+select * from cities
+`.trim()
+ const limit = 100
+ const { appendAutoLimit } = checkIfAppendLimitRequired(sql, limit)
+ expect(appendAutoLimit).toBe(false)
+ })
+ test('Should return false if query has with a comment II', () => {
+ const sql = `
+select * from cities
+-- This is a comment
+`.trim()
+ const limit = 100
+ const { appendAutoLimit } = checkIfAppendLimitRequired(sql, limit)
+ expect(appendAutoLimit).toBe(false)
+ })
+})
+
+// [Joshen] These will just need to test the cases when appendAutoLimit returns true then
+describe('SQLEditor.utils.ts:suffixWithLimit', () => {
+ test('Should add the limit param properly if query ends without a semi colon', () => {
+ const sql = 'select * from countries'
+ const limit = 100
+ const formattedSql = suffixWithLimit(sql, limit)
+ expect(formattedSql).toBe('select * from countries limit 100;')
+ })
+ test('Should add the limit param properly if query ends with a semi colon', () => {
+ const sql = 'select * from countries;'
+ const limit = 100
+ const formattedSql = suffixWithLimit(sql, limit)
+ expect(formattedSql).toBe('select * from countries limit 100;')
+ })
+ test('Should add the limit param properly if query ends with multiple semi colon', () => {
+ const sql = 'select * from countries;;;;;;;'
+ const limit = 100
+ const formattedSql = suffixWithLimit(sql, limit)
+ expect(formattedSql).toBe('select * from countries limit 100;')
+ })
+})
diff --git a/apps/studio/components/interfaces/SQLEditor/SQLEditor.utils.ts b/apps/studio/components/interfaces/SQLEditor/SQLEditor.utils.ts
index d76787eb063..fe5e5a74047 100644
--- a/apps/studio/components/interfaces/SQLEditor/SQLEditor.utils.ts
+++ b/apps/studio/components/interfaces/SQLEditor/SQLEditor.utils.ts
@@ -109,3 +109,43 @@ export const compareAsNewSnippet = (sqlDiff: ContentDiff) => {
modified: sqlDiff.modified,
}
}
+
+// [Joshen] Just FYI as well the checks here on whether to append limit is quite restricted
+// This is to prevent dashboard from accidentally appending limit to the end of a query
+// thats not supposed to have any, since there's too many cases to cover.
+// We can however look into making this logic better in the future
+// i.e It's harder to append the limit param, than just leaving the query as it is
+// Otherwise we'd need a full on parser to do this properly
+export const checkIfAppendLimitRequired = (sql: string, limit: number = 0) => {
+ // Remove lines and whitespaces to use for checking
+ const cleanedSql = sql.trim().replaceAll('\n', ' ').replaceAll(/\s+/g, ' ')
+
+ // Check how many queries
+ const regMatch = cleanedSql.matchAll(/[a-zA-Z]*[0-9]*[;]+/g)
+ const queries = new Array(...regMatch)
+ const indexSemiColon = cleanedSql.lastIndexOf(';')
+ const hasComments = cleanedSql.includes('--')
+ const hasMultipleQueries =
+ queries.length > 1 || (indexSemiColon > 0 && indexSemiColon !== cleanedSql.length - 1)
+
+ // Check if need to auto limit rows
+ const appendAutoLimit =
+ limit > 0 &&
+ !hasComments &&
+ !hasMultipleQueries &&
+ cleanedSql.toLowerCase().startsWith('select') &&
+ !cleanedSql.endsWith('limit') &&
+ !cleanedSql.endsWith('limit;') &&
+ !cleanedSql.match('limit [0-9]*[;]?$')
+ return { cleanedSql, appendAutoLimit }
+}
+
+export const suffixWithLimit = (sql: string, limit: number = 0) => {
+ const { cleanedSql, appendAutoLimit } = checkIfAppendLimitRequired(sql, limit)
+ const formattedSql = appendAutoLimit
+ ? cleanedSql.endsWith(';')
+ ? sql.replace(/[;]+$/, ` limit ${limit};`)
+ : `${sql} limit ${limit};`
+ : sql
+ return formattedSql
+}
diff --git a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/FavoriteButton.tsx b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/FavoriteButton.tsx
deleted file mode 100644
index 53b373ffa91..00000000000
--- a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/FavoriteButton.tsx
+++ /dev/null
@@ -1,109 +0,0 @@
-import {
- Button,
- IconHeart,
- TooltipContent_Shadcn_,
- TooltipTrigger_Shadcn_,
- Tooltip_Shadcn_,
-} from 'ui'
-import { useQueryClient } from '@tanstack/react-query'
-import { contentKeys } from 'data/content/keys'
-import type { Content, ContentData } from 'data/content/content-query'
-import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
-import { useSqlEditorStateSnapshot } from 'state/sql-editor'
-
-export type FavoriteButtonProps = { id: string }
-const FavoriteButton = ({ id }: FavoriteButtonProps) => {
- const client = useQueryClient()
- const { project } = useProjectContext()
- const snap = useSqlEditorStateSnapshot()
-
- const snippet = snap.snippets[id]
- const isFavorite = snippet !== undefined ? snippet.snippet.content.favorite : false
-
- async function addFavorite() {
- snap.addFavorite(id)
-
- client.setQueryData(
- contentKeys.list(project?.ref),
- (oldData: ContentData | undefined) => {
- if (!oldData) {
- return
- }
-
- return {
- ...oldData,
- content: oldData.content.map((content: Content) => {
- if (content.type === 'sql' && content.id === id) {
- return {
- ...content,
- content: {
- ...content.content,
- favorite: true,
- },
- }
- }
-
- return content
- }),
- }
- }
- )
- }
-
- async function removeFavorite() {
- snap.removeFavorite(id)
-
- client.setQueryData(
- contentKeys.list(project?.ref),
- (oldData: ContentData | undefined) => {
- if (!oldData) {
- return
- }
-
- return {
- ...oldData,
- content: oldData.content.map((content: Content) => {
- if (content.type === 'sql' && content.id === id) {
- return {
- ...content,
- content: {
- ...content.content,
- favorite: false,
- },
- }
- }
-
- return content
- }),
- }
- }
- )
- }
-
- return (
-
-
- {isFavorite ? (
- }
- />
- ) : (
- }
- />
- )}
-
- Add to favorites
-
- )
-}
-
-export default FavoriteButton
diff --git a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/Results.tsx b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/Results.tsx
index 1186c766129..3e1e1577da1 100644
--- a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/Results.tsx
+++ b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/Results.tsx
@@ -1,17 +1,22 @@
-import { useKeyboardShortcuts } from 'hooks'
-import { copyToClipboard } from 'lib/helpers'
+import { Clipboard } from 'lucide-react'
import { useState } from 'react'
import { Item, Menu, useContextMenu } from 'react-contexify'
import DataGrid, { CalculatedColumn } from 'react-data-grid'
import { createPortal } from 'react-dom'
-import { IconClipboard } from 'ui'
+
+import { useKeyboardShortcuts } from 'hooks'
+import { copyToClipboard } from 'lib/helpers'
+import { useSqlEditorStateSnapshot } from 'state/sql-editor'
+import { GridFooter } from 'components/ui/GridFooter'
const Results = ({ id, rows }: { id: string; rows: readonly any[] }) => {
const SQL_CONTEXT_EDITOR_ID = 'sql-context-menu-' + id
-
const [cellPosition, setCellPosition] = useState(undefined)
- function onCopyCell() {
+ const snap = useSqlEditorStateSnapshot()
+ const results = snap.results[id]?.[0]
+
+ const onCopyCell = () => {
if (cellPosition) {
const { rowIdx, column } = cellPosition
const colKey = column.key
@@ -109,16 +114,24 @@ const Results = ({ id, rows }: { id: string; rows: readonly any[] }) => {
rowClass={() => '[&>.rdg-cell]:items-center'}
onSelectedCellChange={onSelectedCellChange}
/>
+
{typeof window !== 'undefined' &&
createPortal(
,
document.body
)}
+
+
+
+ {rows.length} row{rows.length > 1 ? 's' : ''}
+ {results.autoLimit !== undefined && ` (auto limit ${results.autoLimit} rows)`}
+
+
>
)
}
diff --git a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/ResultsDropdown.tsx b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/ResultsDropdown.tsx
index 7cd4448d153..67f8e018742 100644
--- a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/ResultsDropdown.tsx
+++ b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/ResultsDropdown.tsx
@@ -9,6 +9,7 @@ import { useTelemetryProps } from 'common'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { copyToClipboard } from 'lib/helpers'
import Telemetry from 'lib/telemetry'
+import { ChevronDownIcon, Clipboard, Download } from 'lucide-react'
import { useSqlEditorStateSnapshot } from 'state/sql-editor'
import {
Button,
@@ -16,9 +17,6 @@ import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
- IconChevronDown,
- IconClipboard,
- IconDownload,
} from 'ui'
export type ResultsDropdownProps = {
@@ -122,7 +120,7 @@ const ResultsDropdown = ({ id }: ResultsDropdownProps) => {
return (
- }>
+ }>
Export
@@ -137,15 +135,15 @@ const ResultsDropdown = ({ id }: ResultsDropdownProps) => {
-
+
Download CSV
-
+
Copy as markdown
-
+
Copy as JSON
diff --git a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/SavingIndicator.tsx b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/SavingIndicator.tsx
index bde673d3087..17f13f6e770 100644
--- a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/SavingIndicator.tsx
+++ b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/SavingIndicator.tsx
@@ -1,9 +1,10 @@
import * as Tooltip from '@radix-ui/react-tooltip'
import { useUser } from 'common'
import { usePrevious } from 'hooks'
+import { AlertCircle, Check, Loader2, RefreshCcw } from 'lucide-react'
import { useEffect, useState } from 'react'
import { useSqlEditorStateSnapshot } from 'state/sql-editor'
-import { Button, IconAlertCircle, IconCheck, IconLoader, IconRefreshCcw } from 'ui'
+import { Button, IconCheck, IconRefreshCcw } from 'ui'
import ReadOnlyBadge from './ReadOnlyBadge'
export type SavingIndicatorProps = { id: string }
@@ -44,7 +45,7 @@ const SavingIndicator = ({ id }: SavingIndicatorProps) => {
}
+ icon={}
onClick={retry}
>
Retry
@@ -53,7 +54,7 @@ const SavingIndicator = ({ id }: SavingIndicatorProps) => {
{showSavedText ? (
-
+
@@ -70,7 +71,7 @@ const SavingIndicator = ({ id }: SavingIndicatorProps) => {
) : isSnippetOwner && savingState === 'UPDATING' ? (
-
+
@@ -88,7 +89,7 @@ const SavingIndicator = ({ id }: SavingIndicatorProps) => {
isSnippetOwner ? (
-
+
diff --git a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityActions.tsx b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityActions.tsx
index 4165c8dff64..f0d3769580c 100644
--- a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityActions.tsx
+++ b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityActions.tsx
@@ -1,8 +1,22 @@
-import { AlignLeft, Check, Command, CornerDownLeft, Keyboard, Loader2 } from 'lucide-react'
+import { useQueryClient } from '@tanstack/react-query'
+import {
+ AlignLeft,
+ Check,
+ ChevronDown,
+ Command,
+ CornerDownLeft,
+ Heart,
+ Keyboard,
+ Loader2,
+ MoreVertical,
+} from 'lucide-react'
import toast from 'react-hot-toast'
import { RoleImpersonationPopover } from 'components/interfaces/RoleImpersonationSelector'
+import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import DatabaseSelector from 'components/ui/DatabaseSelector'
+import { Content, ContentData } from 'data/content/content-query'
+import { contentKeys } from 'data/content/keys'
import { useLocalStorageQuery } from 'hooks'
import { IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'lib/constants'
import { detectOS } from 'lib/helpers'
@@ -12,14 +26,24 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+ DropdownMenuSeparator,
DropdownMenuTrigger,
TooltipContent_Shadcn_,
TooltipTrigger_Shadcn_,
Tooltip_Shadcn_,
+ cn,
} from 'ui'
-import FavoriteButton from './FavoriteButton'
import SavingIndicator from './SavingIndicator'
+const ROWS_PER_PAGE_OPTIONS = [
+ { value: -1, label: 'No limit' },
+ { value: 100, label: '100 rows' },
+ { value: 500, label: '500 rows' },
+ { value: 1000, label: '1,000 rows' },
+]
+
export type UtilityActionsProps = {
id: string
isExecuting?: boolean
@@ -38,13 +62,72 @@ const UtilityActions = ({
executeQuery,
}: UtilityActionsProps) => {
const os = detectOS()
+ const client = useQueryClient()
+ const { project } = useProjectContext()
const snap = useSqlEditorStateSnapshot()
+ const [isAiOpen] = useLocalStorageQuery(LOCAL_STORAGE_KEYS.SQL_EDITOR_AI_OPEN, true)
const [intellisenseEnabled, setIntellisenseEnabled] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.SQL_EDITOR_INTELLISENSE,
true
)
+ const snippet = snap.snippets[id]
+ const isFavorite = snippet !== undefined ? snippet.snippet.content.favorite : false
+
+ const toggleIntellisense = () => {
+ setIntellisenseEnabled(!intellisenseEnabled)
+ toast.success(
+ `Successfully ${intellisenseEnabled ? 'disabled' : 'enabled'} intellisense. ${intellisenseEnabled ? 'Please refresh your browser for changes to take place.' : ''}`
+ )
+ }
+
+ const addFavorite = async () => {
+ snap.addFavorite(id)
+ client.setQueryData(
+ contentKeys.list(project?.ref),
+ (oldData: ContentData | undefined) => {
+ if (!oldData) return
+
+ return {
+ ...oldData,
+ content: oldData.content.map((content: Content) => {
+ if (content.type === 'sql' && content.id === id) {
+ return {
+ ...content,
+ content: { ...content.content, favorite: true },
+ }
+ }
+ return content
+ }),
+ }
+ }
+ )
+ }
+
+ const removeFavorite = async () => {
+ snap.removeFavorite(id)
+ client.setQueryData(
+ contentKeys.list(project?.ref),
+ (oldData: ContentData | undefined) => {
+ if (!oldData) return
+
+ return {
+ ...oldData,
+ content: oldData.content.map((content: Content) => {
+ if (content.type === 'sql' && content.id === id) {
+ return {
+ ...content,
+ content: { ...content.content, favorite: false },
+ }
+ }
+ return content
+ }),
+ }
+ }
+ )
+ }
+
return (
{IS_PLATFORM &&
}
@@ -52,42 +135,121 @@ const UtilityActions = ({
}
+ type="default"
+ className={cn('px-1', isAiOpen ? 'block 2xl:hidden' : 'hidden')}
+ icon={}
/>
+
+
+
+ Intellisense enabled
+
+ {intellisenseEnabled && }
+
+
{
- setIntellisenseEnabled(!intellisenseEnabled)
- toast.success(
- `Successfully ${intellisenseEnabled ? 'disabled' : 'enabled'} intellisense. ${intellisenseEnabled ? 'Please refresh your browser for changes to take place.' : ''}`
- )
+ if (isFavorite) removeFavorite()
+ else addFavorite()
}}
>
- Intellisense enabled
- {intellisenseEnabled && }
+
+ {isFavorite ? 'Remove from' : 'Add to'} favorites
+
+
+
+ Prettify SQL
- {IS_PLATFORM &&
}
+
+
+
+ }
+ />
+
+
+
+ Intellisense enabled
+ {intellisenseEnabled && }
+
+
+
-
-
-
- Prettify SQL
-
+ {IS_PLATFORM && (
+
+
+ {isFavorite ? (
+ }
+ />
+ ) : (
+ }
+ />
+ )}
+
+
+ {isFavorite ? 'Remove from' : 'Add to'} favorites
+
+
+ )}
-
+
+
+ }
+ />
+
+ Prettify SQL
+
+
+
+
+
+ }>
+ {ROWS_PER_PAGE_OPTIONS.find((opt) => opt.value === snap.limit)?.label}
+
+
+
+ snap.setLimit(Number(val))}
+ >
+ {ROWS_PER_PAGE_OPTIONS.map((option) => (
+
+ {option.label}
+
+ ))}
+
+
+
+
+
snap.resetResult(id)} />
diff --git a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityPanel.tsx b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityPanel.tsx
index 308400594a8..7c044980ec6 100644
--- a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityPanel.tsx
+++ b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityPanel.tsx
@@ -1,7 +1,7 @@
import { useQueryClient } from '@tanstack/react-query'
-import { useParams } from 'common'
import toast from 'react-hot-toast'
+import { useParams } from 'common'
import { useContentUpsertMutation } from 'data/content/content-upsert-mutation'
import { contentKeys } from 'data/content/keys'
import { useSqlEditorStateSnapshot } from 'state/sql-editor'
@@ -111,28 +111,23 @@ const UtilityPanel = ({
return (
-
+
- Results{' '}
- {!isExecuting &&
- (result?.rows ?? []).length > 0 &&
- `(${result.rows.length.toLocaleString()})`}
+ Results
- Chart
+ Chart
+ {result?.rows && }
-
- {result && result.rows && }
-
-
+
@@ -96,15 +91,19 @@ const UtilityTabResults = ({
))
) : (
- <>
- Error: {result.error?.message}
- {readReplicaError && (
-
- Note: Read replicas are for read only queries. Run write queries on the
- primary database instead.
-
- )}
- >
+ Error: {result.error?.message}
+ )}
+ {result.autoLimit && (
+
+ Note: A limit of {result.autoLimit} was applied to your query. If this was the
+ cause of a syntax error, try selecting "No limit" instead and re-run the query.
+
+ )}
+ {readReplicaError && (
+
+ Note: Read replicas are for read only queries. Run write queries on the primary
+ database instead.
+
)}
)}
diff --git a/apps/studio/components/ui/GridFooter.tsx b/apps/studio/components/ui/GridFooter.tsx
new file mode 100644
index 00000000000..b46168e957f
--- /dev/null
+++ b/apps/studio/components/ui/GridFooter.tsx
@@ -0,0 +1,12 @@
+import { PropsWithChildren } from 'react'
+import { cn } from 'ui'
+
+export const GridFooter = ({ children, className }: PropsWithChildren<{ className?: string }>) => {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/apps/studio/data/sql/execute-sql-query.ts b/apps/studio/data/sql/execute-sql-query.ts
index 0d5bdbb4322..ad6798e5131 100644
--- a/apps/studio/data/sql/execute-sql-query.ts
+++ b/apps/studio/data/sql/execute-sql-query.ts
@@ -15,6 +15,7 @@ export type ExecuteSqlVariables = {
queryKey?: QueryKey
handleError?: (error: ResponseError) => { result: any }
isRoleImpersonationEnabled?: boolean
+ autoLimit?: number
}
export async function executeSql(
diff --git a/apps/studio/lib/constants/index.ts b/apps/studio/lib/constants/index.ts
index b22e7e9b354..a43d862e2a8 100644
--- a/apps/studio/lib/constants/index.ts
+++ b/apps/studio/lib/constants/index.ts
@@ -40,6 +40,7 @@ export const LOCAL_STORAGE_KEYS = {
SQL_EDITOR_INTELLISENSE: 'supabase_sql-editor-intellisense-enabled',
SQL_EDITOR_SPLIT_SIZE: 'supabase_sql-editor-split-size',
SQL_EDITOR_AI_SCHEMA: 'supabase_sql-editor-ai-schema-enabled',
+ SQL_EDITOR_AI_OPEN: 'supabase_sql-editor-ai-open',
LOG_EXPLORER_SPLIT_SIZE: 'supabase_log-explorer-split-size',
GRAPHIQL_RLS_BYPASS_WARNING: 'graphiql-rls-bypass-warning-dismissed',
CLS_DIFF_WARNING: 'cls-diff-warning-dismissed',
diff --git a/apps/studio/state/sql-editor.ts b/apps/studio/state/sql-editor.ts
index f7b7924e2c4..b26fd1da4e4 100644
--- a/apps/studio/state/sql-editor.ts
+++ b/apps/studio/state/sql-editor.ts
@@ -21,6 +21,7 @@ export const sqlEditorState = proxy({
[key: string]: {
rows: any[]
error?: any
+ autoLimit?: number
}[]
},
// Project ref as the key, ids of each snippet as the order
@@ -30,12 +31,14 @@ export const sqlEditorState = proxy({
loaded: {} as {
[key: string]: boolean
},
+ limit: 100,
needsSaving: proxySet([]),
savingStates: {} as {
[key: string]: 'IDLE' | 'UPDATING' | 'UPDATING_FAILED'
},
+ setLimit: (value: number) => (sqlEditorState.limit = value),
orderSnippets: (snippets: SqlSnippet[]) => {
return (
snippets
@@ -151,14 +154,14 @@ export const sqlEditorState = proxy({
sqlEditorState.results[id] = []
}
},
- addResult: (id: string, results: any[]) => {
+ addResult: (id: string, results: any[], autoLimit?: number) => {
if (sqlEditorState.results[id]) {
- sqlEditorState.results[id].unshift({ rows: results })
+ sqlEditorState.results[id].unshift({ rows: results, autoLimit })
}
},
- addResultError: (id: string, error: any) => {
+ addResultError: (id: string, error: any, autoLimit?: number) => {
if (sqlEditorState.results[id]) {
- sqlEditorState.results[id].unshift({ rows: [], error })
+ sqlEditorState.results[id].unshift({ rows: [], error, autoLimit })
}
},
addFavorite: (id: string) => {
diff --git a/apps/studio/vitest.config.mts b/apps/studio/vitest.config.mts
index 683e302d6e0..c255ef58ca0 100644
--- a/apps/studio/vitest.config.mts
+++ b/apps/studio/vitest.config.mts
@@ -25,7 +25,10 @@ export default defineConfig({
test: {
globals: true,
environment: 'jsdom', // TODO(kamil): This should be set per test via header in .tsx files only
- include: [resolve(dirname, './tests/**/*.test.{ts,tsx}')],
+ include: [
+ resolve(dirname, './tests/**/*.test.{ts,tsx}'),
+ resolve(dirname, './components/**/*.test.{ts,tsx}'),
+ ],
restoreMocks: true,
setupFiles: [
resolve(dirname, './tests/vitestSetup.ts'),