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 && ( <> @@ -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) => { + + + 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'),