diff --git a/apps/studio/components/interfaces/Reports/ReportBlock/ReportBlock.tsx b/apps/studio/components/interfaces/Reports/ReportBlock/ReportBlock.tsx index c9a7e9f7000..4cb76fc040c 100644 --- a/apps/studio/components/interfaces/Reports/ReportBlock/ReportBlock.tsx +++ b/apps/studio/components/interfaces/Reports/ReportBlock/ReportBlock.tsx @@ -1,5 +1,5 @@ -import { useCallback, useState } from 'react' import { X } from 'lucide-react' +import { useCallback, useState } from 'react' import { toast } from 'sonner' import { useParams } from 'common' @@ -9,13 +9,13 @@ import { DEFAULT_CHART_CONFIG, QueryBlock } from 'components/ui/QueryBlock/Query import { AnalyticsInterval } from 'data/analytics/constants' import { useContentIdQuery } from 'data/content/content-id-query' import { usePrimaryDatabase } from 'data/read-replicas/replicas-query' -import { useExecuteSqlMutation, type QueryResponseError } from 'data/sql/execute-sql-mutation' -import { useChanged } from 'hooks/misc/useChanged' +import { useExecuteSqlMutation } from 'data/sql/execute-sql-mutation' +import { useChangedSync } from 'hooks/misc/useChanged' import { useDatabaseSelectorStateSnapshot } from 'state/database-selector' -import { Dashboards, SqlSnippets } from 'types' +import type { Dashboards, SqlSnippets } from 'types' +import { DEPRECATED_REPORTS } from '../Reports.constants' import { ChartBlock } from './ChartBlock' import { DeprecatedChartBlock } from './DeprecatedChartBlock' -import { DEPRECATED_REPORTS } from '../Reports.constants' interface ReportBlockProps { item: Dashboards.Chart @@ -131,8 +131,8 @@ export const ReportBlock = ({ [projectRef, readOnlyConnectionString, postgresConnectionString, executeSql] ) - const sqlHasChanged = useChanged(sql) - const isRefreshingChanged = useChanged(isRefreshing) + const sqlHasChanged = useChangedSync(sql) + const isRefreshingChanged = useChangedSync(isRefreshing) if (sqlHasChanged || (isRefreshingChanged && isRefreshing)) { runQuery('select', sql) } diff --git a/apps/studio/components/ui/AIAssistantPanel/DisplayBlockRenderer.tsx b/apps/studio/components/ui/AIAssistantPanel/DisplayBlockRenderer.tsx index f59f767fd9d..2582c436f2a 100644 --- a/apps/studio/components/ui/AIAssistantPanel/DisplayBlockRenderer.tsx +++ b/apps/studio/components/ui/AIAssistantPanel/DisplayBlockRenderer.tsx @@ -1,19 +1,19 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { useRouter } from 'next/router' -import { DragEvent, PropsWithChildren, useEffect, useRef, useState } from 'react' +import { type DragEvent, type PropsWithChildren, useRef, useState } from 'react' import { useParams } from 'common' import { ChartConfig } from 'components/interfaces/SQLEditor/UtilityPanel/ChartConfig' import { usePrimaryDatabase } from 'data/read-replicas/replicas-query' import { useExecuteSqlMutation } from 'data/sql/execute-sql-mutation' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' +import { useChangedSync } from 'hooks/misc/useChanged' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useProfile } from 'lib/profile' - -import { ConfirmFooter } from './ConfirmFooter' import { DEFAULT_CHART_CONFIG, QueryBlock } from '../QueryBlock/QueryBlock' import { identifyQueryType } from './AIAssistant.utils' +import { ConfirmFooter } from './ConfirmFooter' interface DisplayBlockRendererProps { messageId: string @@ -51,6 +51,15 @@ export const DisplayBlockRenderer = ({ onChartConfigChange, onQueryRun, }: PropsWithChildren) => { + const savedInitialArgs = useRef(initialArgs) + const savedInitialResults = useRef(initialResults) + const savedInitialConfig = useRef({ + ...DEFAULT_CHART_CONFIG, + view: initialArgs.view === 'chart' ? 'chart' : 'table', + xKey: initialArgs.xAxis ?? '', + yKey: initialArgs.yAxis ?? '', + }) + const router = useRouter() const { ref } = useParams() const { profile } = useProfile() @@ -86,57 +95,31 @@ export const DisplayBlockRenderer = ({ const readOnlyConnectionString = primaryDatabase?.connection_string_read_only const postgresConnectionString = primaryDatabase?.connectionString - const lastQueryType = useRef<'select' | 'mutation'>('select') - - useEffect(() => { - const initialConfig: ChartConfig = { - ...DEFAULT_CHART_CONFIG, - view: initialArgs.view === 'chart' ? 'chart' : 'table', - xKey: initialArgs.xAxis ?? '', - yKey: initialArgs.yAxis ?? '', - } - setChartConfig(initialConfig) - onChartConfigChange?.(initialConfig) - setIsWriteQuery(initialArgs.isWriteQuery || false) - setRows(Array.isArray(initialResults) ? initialResults : undefined) - lastQueryType.current = initialArgs.isWriteQuery ? 'mutation' : 'select' - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [toolCallId]) - - useEffect(() => { - const normalized = Array.isArray(initialResults) ? initialResults : undefined - if (!normalized || normalized === rows) return - setRows(normalized) - }, [initialResults, rows]) - const { mutate: executeSql, error: executeSqlError, isLoading: executeSqlLoading, } = useExecuteSqlMutation({ - onSuccess: (data) => { - setRows(Array.isArray(data.result) ? data.result : undefined) - setIsWriteQuery(lastQueryType.current === 'mutation' || initialArgs.isWriteQuery || false) - onResults?.({ - messageId, - results: Array.isArray(data.result) ? data.result : undefined, - }) - }, - onError: (error) => { - const lowerMessage = error.message.toLowerCase() - const isReadOnlyError = - lowerMessage.includes('read-only transaction') || - lowerMessage.includes('permission denied') || - lowerMessage.includes('must be owner') - - if (lastQueryType.current === 'select' && isReadOnlyError) { - setIsWriteQuery(true) - } - - onError?.({ messageId, errorText: error.message }) + onError: () => { + // Suppress toast because error message is displayed inline }, }) + const toolCallIdChanged = useChangedSync(toolCallId) + if (toolCallIdChanged) { + setChartConfig(savedInitialConfig.current) + onChartConfigChange?.(savedInitialConfig.current) + setIsWriteQuery(savedInitialArgs.current.isWriteQuery || false) + setRows(Array.isArray(savedInitialResults.current) ? savedInitialResults.current : undefined) + } + + const initialResultsChanged = useChangedSync(initialResults) + if (initialResultsChanged) { + const normalized = Array.isArray(initialResults) ? initialResults : undefined + if (!normalized || normalized === rows) return + setRows(normalized) + } + const handleRunQuery = (queryType: 'select' | 'mutation') => { if (!sqlQuery) return @@ -169,11 +152,35 @@ export const DisplayBlockRenderer = ({ return } - lastQueryType.current = queryType if (queryType === 'mutation') { setIsWriteQuery(true) } - executeSql({ projectRef: ref, connectionString, sql: sqlQuery }) + executeSql( + { projectRef: ref, connectionString, sql: sqlQuery }, + { + onSuccess: (data) => { + setRows(Array.isArray(data.result) ? data.result : undefined) + setIsWriteQuery(queryType === 'mutation' || initialArgs.isWriteQuery || false) + onResults?.({ + messageId, + results: Array.isArray(data.result) ? data.result : undefined, + }) + }, + onError: (error) => { + const lowerMessage = error.message.toLowerCase() + const isReadOnlyError = + lowerMessage.includes('read-only transaction') || + lowerMessage.includes('permission denied') || + lowerMessage.includes('must be owner') + + if (queryType === 'select' && isReadOnlyError) { + setIsWriteQuery(true) + } + + onError?.({ messageId, errorText: error.message }) + }, + } + ) } const handleExecute = (queryType: 'select' | 'mutation') => { diff --git a/apps/studio/hooks/misc/useChanged.ts b/apps/studio/hooks/misc/useChanged.ts index afeeb0f3198..63431717f1e 100644 --- a/apps/studio/hooks/misc/useChanged.ts +++ b/apps/studio/hooks/misc/useChanged.ts @@ -10,3 +10,11 @@ export function useChanged(value: T): boolean { return changed } + +export function useChangedSync(value: T): boolean { + const prev = useRef() + const changed = prev.current !== value + prev.current = value + + return changed +}