From 997d2c365ad3ca26d78a1c78a27c48b10c76ec8e Mon Sep 17 00:00:00 2001 From: Vaibhav <117663341+7ttp@users.noreply.github.com> Date: Tue, 10 Feb 2026 23:04:35 +0530 Subject: [PATCH 1/4] fix: CSV drag and drop (#42656) ## Problem Drag-and-drop CSV import functionality is broken in the Table Editor when tables are empty. The `pointer-events-none` CSS class on the empty state overlay blocks all pointer events, including drag events, preventing users from dropping CSV files onto empty tables. ## Solution Move drag event handlers (`onDragOver`, `onDragLeave`, `onDrop`) from the inner overlay div to the parent container div. This allows: - Drag events to be captured by the parent container - The overlay to retain `pointer-events-none` for proper horizontal scrolling - (as intended in #42618) - Interactive elements inside to use `pointer-events-auto` This follows the existing pattern used in `FileExplorerColumn.tsx` where drag handlers are on the parent container while visual overlays have `pointer-events-none`. ## Related - Closes https://github.com/supabase/supabase/issues/42655 - Extends https://github.com/supabase/supabase/pull/42618 ## Summary by CodeRabbit * **Bug Fixes** * Improved drag-and-drop event detection scope in the grid component. --- apps/studio/components/grid/components/grid/Grid.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/studio/components/grid/components/grid/Grid.tsx b/apps/studio/components/grid/components/grid/Grid.tsx index 1159273f2049e..d57ac84322e40 100644 --- a/apps/studio/components/grid/components/grid/Grid.tsx +++ b/apps/studio/components/grid/components/grid/Grid.tsx @@ -232,6 +232,9 @@ export const Grid = memo(
{/* Render no rows fallback outside of the DataGrid */} {(rows ?? []).length === 0 && ( @@ -241,9 +244,6 @@ export const Grid = memo( isTableEmpty && isDraggedOver && 'border-2 border-dashed', isValidFileDraggedOver ? 'border-brand-600' : 'border-destructive-600' )} - onDragOver={onDragOver} - onDragLeave={onDragOver} - onDrop={onFileDrop} > {isLoading && !isDisabled && ( From 37748b5078cc1af392faebe1a331d13caf7c081e Mon Sep 17 00:00:00 2001 From: Ali Waseem Date: Tue, 10 Feb 2026 13:39:06 -0700 Subject: [PATCH 2/4] chore: add E2E tests for simulating a drag and drop CSV (#42658) ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Chore: test drag and drop on table editor follow up for https://github.com/supabase/supabase/issues/42655 ## Summary by CodeRabbit * **Tests** * Added comprehensive end-to-end test for CSV drag-and-drop import functionality that validates the complete import workflow: drag-drop detection on empty tables, import dialog display with accurate row counts, and successful data import to the grid. --- .../components/grid/components/grid/Grid.tsx | 1 + .../features/files/table-editor-drag-drop.csv | 4 ++ e2e/studio/features/table-editor.spec.ts | 62 +++++++++++++++++++ 3 files changed, 67 insertions(+) create mode 100644 e2e/studio/features/files/table-editor-drag-drop.csv diff --git a/apps/studio/components/grid/components/grid/Grid.tsx b/apps/studio/components/grid/components/grid/Grid.tsx index d57ac84322e40..0ee375115afdf 100644 --- a/apps/studio/components/grid/components/grid/Grid.tsx +++ b/apps/studio/components/grid/components/grid/Grid.tsx @@ -230,6 +230,7 @@ export const Grid = memo( return (
{ await deleteTable(page, ref, sourceTableName) await deleteTable(page, ref, targetTableName) }) + + test('CSV drag and drop imports data on empty table', async ({ page, ref }) => { + const tableName = 'pw_table_csv_drag_drop' + + await dropTable(tableName) + await dbCreateTable(tableName, columnName) + + const loadPromise = waitForTableToLoad(page, ref) + await page.goto(toUrl(`/project/${ref}/editor?schema=public`)) + await loadPromise + + await page.getByRole('button', { name: `View ${tableName}`, exact: true }).click() + await page.waitForURL(/\/editor\/\d+\?schema=public$/) + + await expect( + page.getByText('or drag and drop a CSV file here'), + 'Empty table should show drag and drop hint' + ).toBeVisible() + + const csvFilePath = path.join(import.meta.dirname, 'files', 'table-editor-drag-drop.csv') + const csvBuffer = fs.readFileSync(csvFilePath) + + // Synthesize a DataTransfer with the CSV file to simulate a browser file drag-and-drop + const dataTransfer = await page.evaluateHandle((csvBase64: string) => { + const dt = new DataTransfer() + const bytes = Uint8Array.from(atob(csvBase64), (c) => c.charCodeAt(0)) + const file = new File([bytes], 'table-editor-drag-drop.csv', { type: 'text/csv' }) + dt.items.add(file) + return dt + }, csvBuffer.toString('base64')) + + const gridContainer = page.getByTestId('table-editor-grid-container') + + await gridContainer.dispatchEvent('dragover', { dataTransfer }) + await expect( + page.getByText('Drop your CSV file here'), + 'Drag feedback should show when CSV is dragged over' + ).toBeVisible() + + await gridContainer.dispatchEvent('drop', { dataTransfer }) + + await expect( + page.getByText('A total of 3 rows will be'), + 'Import dialog should show correct row count from CSV' + ).toBeVisible({ timeout: 10_000 }) + + const waitForCsvInsert = createApiResponseWaiter(page, 'pg-meta', ref, 'query?key=', { + method: 'POST', + }) + await page.getByRole('button', { name: 'Import data' }).click() + await waitForCsvInsert + await waitForGridDataToLoad(page, ref) + + await expect( + page.getByText('3 records'), + 'Table should show 3 records after drag and drop import' + ).toBeVisible() + await expect(page.getByRole('gridcell', { name: 'drag drop value 1' })).toBeVisible() + + await dropTable(tableName) + }) }) From c39747f8b2bf451fc46091d999d7e2e9798402ca Mon Sep 17 00:00:00 2001 From: Ali Waseem Date: Tue, 10 Feb 2026 13:41:43 -0700 Subject: [PATCH 3/4] feat: added copy prompt button for AI assistant for your own agent (#42624) ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Any instance where we want to ask the AI assistant, we create a copy prompt button for your agent ## Demo https://github.com/user-attachments/assets/c6afe319-ad36-49b7-a244-a8bf04c809a1 ## Summary by CodeRabbit * **New Features** * Introduced a new dropdown-style AI assistant trigger across explain, debug, and lint features with improved interaction flow. * Added copy-to-clipboard functionality for AI prompts with visual feedback confirmation. * Enhanced AI assistant integration across query performance, SQL editor, and lint detail interfaces for consistent experience. --- .../ExplainVisualizer.Header.tsx | 50 +++++--- .../interfaces/Home/AdvisorWidget.tsx | 52 +++++--- .../interfaces/HomeNew/AdvisorSection.tsx | 52 ++++---- .../interfaces/Linter/LintDetail.tsx | 23 ++-- .../QueryPerformance/QueryDetail.tsx | 45 +++---- .../interfaces/SQLEditor/SQLEditor.tsx | 23 +++- .../SQLEditor/UtilityPanel/UtilityPanel.tsx | 11 +- .../UtilityPanel/UtilityTabResults.tsx | 27 ++-- .../components/ui/AiAssistantDropdown.tsx | 121 ++++++++++++++++++ packages/common/telemetry-constants.ts | 26 ++++ 10 files changed, 316 insertions(+), 114 deletions(-) create mode 100644 apps/studio/components/ui/AiAssistantDropdown.tsx diff --git a/apps/studio/components/interfaces/ExplainVisualizer/ExplainVisualizer.Header.tsx b/apps/studio/components/interfaces/ExplainVisualizer/ExplainVisualizer.Header.tsx index b0ab2c0f9fc06..24b8d8fc2da3b 100644 --- a/apps/studio/components/interfaces/ExplainVisualizer/ExplainVisualizer.Header.tsx +++ b/apps/studio/components/interfaces/ExplainVisualizer/ExplainVisualizer.Header.tsx @@ -1,14 +1,13 @@ -import { ArrowUp, Eye, Code, HelpCircle } from 'lucide-react' - -import { useFlag } from 'common' -import { AiIconAnimation, Button } from 'ui' import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider' +import { AiAssistantDropdown } from 'components/ui/AiAssistantDropdown' +import { Code, Eye, HelpCircle } from 'lucide-react' import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state' -import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2' import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state' +import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2' +import { Button, Tooltip, TooltipContent, TooltipTrigger } from 'ui' + import { buildExplainPrompt } from './ExplainVisualizer.ai' import type { QueryPlanRow } from './ExplainVisualizer.types' -import { Tooltip, TooltipContent, TooltipTrigger } from 'ui' export interface ExplainSummary { totalTime: number @@ -31,28 +30,41 @@ export function ExplainHeader({ mode, onToggleMode, summary, id, rows }: Explain const { openSidebar } = useSidebarManagerSnapshot() const aiSnap = useAiAssistantStateSnapshot() - const handleExplainWithAI = () => { - if (!id) return + const getPromptData = () => { + if (!id) return null const snippet = snapV2.snippets[id]?.snippet - if (!snippet?.content?.sql) return + if (!snippet?.content?.sql) return null - const { query, prompt } = buildExplainPrompt({ + return buildExplainPrompt({ sql: snippet.content.sql, explainPlanRows: (rows as QueryPlanRow[]) ?? [], }) + } + + const handleExplainWithAI = () => { + const promptData = getPromptData() + if (!promptData) return openSidebar(SIDEBAR_KEYS.AI_ASSISTANT) aiSnap.newChat({ sqlSnippets: [ { label: 'Query', - content: query, + content: promptData.query, }, ], - initialMessage: prompt, + initialMessage: promptData.prompt, }) } + const buildPromptForCopy = () => { + const promptData = getPromptData() + if (!promptData) return '' + + // Combine SQL and prompt into a single copyable text + return `${promptData.prompt}\n\nSQL Query:\n\`\`\`sql\n${promptData.query}\n\`\`\`` + } + const hasSummaryStats = isVisual && summary && (summary.totalTime > 0 || (summary.hasSeqScan && !summary.hasIndexScan)) @@ -111,14 +123,14 @@ export function ExplainHeader({ mode, onToggleMode, summary, id, rows }: Explain
{id && rows && ( - + type="default" + /> )} - } +
{ e.stopPropagation() e.preventDefault() - openSidebar(SIDEBAR_KEYS.AI_ASSISTANT) - snap.newChat({ - name: 'Summarize lint', - initialInput: createLintSummaryPrompt(lint), - }) - }} - tooltip={{ - content: { side: 'bottom', text: 'Help me fix this issue' }, }} - /> + className="opacity-0 group-hover:opacity-100" + > + createLintSummaryPrompt(lint)} + onOpenAssistant={() => { + openSidebar(SIDEBAR_KEYS.AI_ASSISTANT) + snap.newChat({ + name: 'Summarize lint', + initialInput: createLintSummaryPrompt(lint), + }) + track('advisor_assistant_button_clicked', { + origin: 'homepage', + advisorCategory: lint.categories[0], + advisorType: lint.name, + advisorLevel: lint.level, + }) + }} + telemetrySource="advisor_widget" + type="text" + className="px-1 w-7" + /> +
) diff --git a/apps/studio/components/interfaces/HomeNew/AdvisorSection.tsx b/apps/studio/components/interfaces/HomeNew/AdvisorSection.tsx index 0eff936d443b3..4d686b16c62f2 100644 --- a/apps/studio/components/interfaces/HomeNew/AdvisorSection.tsx +++ b/apps/studio/components/interfaces/HomeNew/AdvisorSection.tsx @@ -1,19 +1,19 @@ -import { BarChart, Shield } from 'lucide-react' -import { useCallback, useMemo } from 'react' - import { useParams } from 'common' import { LINTER_LEVELS } from 'components/interfaces/Linter/Linter.constants' import { createLintSummaryPrompt } from 'components/interfaces/Linter/Linter.utils' import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider' -import { ButtonTooltip } from 'components/ui/ButtonTooltip' +import { AiAssistantDropdown } from 'components/ui/AiAssistantDropdown' import { Lint, useProjectLintsQuery } from 'data/lint/lint-query' import { useTrack } from 'lib/telemetry/track' +import { BarChart, Shield } from 'lucide-react' +import { useCallback, useMemo } from 'react' import { useAdvisorStateSnapshot } from 'state/advisor-state' import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state' import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state' import { AiIconAnimation, Button, Card, CardContent, CardHeader, CardTitle } from 'ui' import { Row } from 'ui-patterns' import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' + import { Markdown } from '../Markdown' export const AdvisorSection = ({ showEmptyState = false }: { showEmptyState?: boolean }) => { @@ -114,29 +114,35 @@ export const AdvisorSection = ({ showEmptyState = false }: { showEmptyState?: bo )} {lint.categories[0]}
- } +
{ e.stopPropagation() e.preventDefault() - openSidebar(SIDEBAR_KEYS.AI_ASSISTANT) - snap.newChat({ - name: 'Summarize lint', - initialInput: createLintSummaryPrompt(lint), - }) - track('advisor_assistant_button_clicked', { - origin: 'homepage', - advisorCategory: lint.categories[0], - advisorType: lint.name, - advisorLevel: lint.level, - }) }} - tooltip={{ - content: { side: 'bottom', text: 'Help me fix this issue' }, - }} - /> + > + createLintSummaryPrompt(lint)} + onOpenAssistant={() => { + openSidebar(SIDEBAR_KEYS.AI_ASSISTANT) + snap.newChat({ + name: 'Summarize lint', + initialInput: createLintSummaryPrompt(lint), + }) + track('advisor_assistant_button_clicked', { + origin: 'homepage', + advisorCategory: lint.categories[0], + advisorType: lint.name, + advisorLevel: lint.level, + }) + }} + telemetrySource="advisor_section" + type="text" + className="w-7 h-7" + /> +

{lint.title}

diff --git a/apps/studio/components/interfaces/Linter/LintDetail.tsx b/apps/studio/components/interfaces/Linter/LintDetail.tsx index 3db030282567b..b2c3981077359 100644 --- a/apps/studio/components/interfaces/Linter/LintDetail.tsx +++ b/apps/studio/components/interfaces/Linter/LintDetail.tsx @@ -1,14 +1,15 @@ -import Link from 'next/link' - import { createLintSummaryPrompt, lintInfoMap } from 'components/interfaces/Linter/Linter.utils' import { SIDEBAR_KEYS } from 'components/layouts/ProjectLayout/LayoutSidebar/LayoutSidebarProvider' +import { AiAssistantDropdown } from 'components/ui/AiAssistantDropdown' import { Lint } from 'data/lint/lint-query' import { DOCS_URL } from 'lib/constants' import { useTrack } from 'lib/telemetry/track' import { ExternalLink } from 'lucide-react' +import Link from 'next/link' import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state' import { useSidebarManagerSnapshot } from 'state/sidebar-manager-state' -import { AiIconAnimation, Button } from 'ui' +import { Button } from 'ui' + import { Markdown } from '../Markdown' import { EntityTypeIcon, LintCTA, LintEntity } from './Linter.utils' @@ -39,6 +40,10 @@ const LintDetail = ({ lint, projectRef, onAskAssistant }: LintDetailProps) => { }) } + const buildPromptForCopy = () => { + return createLintSummaryPrompt(lint) + } + return (

Entity

@@ -58,12 +63,12 @@ const LintDetail = ({ lint, projectRef, onAskAssistant }: LintDetailProps) => {

Resolve

- + + type="default" + /> )}
{ editor.onDidScrollChange((e) => (scrollTopRef.current = e.scrollTop)) } + const buildDebugPrompt = useCallback(() => { + const snippet = snapV2.snippets[id] + const result = snapV2.results[id]?.[0] + const sql = (snippet?.snippet.content?.sql ?? '').replace(sqlAiDisclaimerComment, '').trim() + const errorMessage = result?.error?.message ?? 'Unknown error' + const prompt = `Help me to debug the attached sql snippet which gives the following error: \n\n${errorMessage}` + + return `${prompt}\n\nSQL Query:\n\`\`\`sql\n${sql}\n\`\`\`` + }, [id, snapV2.results, snapV2.snippets]) + const onDebug = useCallback(async () => { try { const snippet = snapV2.snippets[id] @@ -904,6 +914,7 @@ export const SQLEditor = () => { executeQuery={executeQuery} executeExplainQuery={executeExplainQuery} onDebug={onDebug} + buildDebugPrompt={buildDebugPrompt} activeTab={activeUtilityTab} onActiveTabChange={setActiveUtilityTab} /> diff --git a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityPanel.tsx b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityPanel.tsx index 68f98ad63a782..59abfe43110a3 100644 --- a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityPanel.tsx +++ b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityPanel.tsx @@ -1,17 +1,17 @@ -import { toast } from 'sonner' - import { useFlag, useParams } from 'common' import { DownloadResultsButton } from 'components/ui/DownloadResultsButton' import { useContentUpsertMutation } from 'data/content/content-upsert-mutation' import { Snippet } from 'data/content/sql-folders-query' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' +import { toast } from 'sonner' import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2' -import { TabsContent_Shadcn_, TabsList_Shadcn_, TabsTrigger_Shadcn_, Tabs_Shadcn_ } from 'ui' +import { Tabs_Shadcn_, TabsContent_Shadcn_, TabsList_Shadcn_, TabsTrigger_Shadcn_ } from 'ui' + import { ChartConfig } from './ChartConfig' import UtilityActions from './UtilityActions' -import UtilityTabResults from './UtilityTabResults' import { UtilityTabExplain } from './UtilityTabExplain' +import UtilityTabResults from './UtilityTabResults' export type UtilityPanelProps = { id: string @@ -24,6 +24,7 @@ export type UtilityPanelProps = { executeQuery: () => void executeExplainQuery: () => void onDebug: () => void + buildDebugPrompt: () => string activeTab?: string onActiveTabChange?: (tab: string) => void } @@ -48,6 +49,7 @@ const UtilityPanel = ({ executeQuery, executeExplainQuery, onDebug, + buildDebugPrompt, activeTab = 'results', onActiveTabChange, }: UtilityPanelProps) => { @@ -188,6 +190,7 @@ const UtilityPanel = ({ isExecuting={isExecuting} isDisabled={isDisabled} onDebug={onDebug} + buildDebugPrompt={buildDebugPrompt} isDebugging={isDebugging} /> diff --git a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityTabResults.tsx b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityTabResults.tsx index 31cd91d7926b9..d92981b565d1f 100644 --- a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityTabResults.tsx +++ b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityTabResults.tsx @@ -1,18 +1,19 @@ -import { ExternalLink, Loader2 } from 'lucide-react' -import { parseAsBoolean, useQueryState } from 'nuqs' -import { forwardRef } from 'react' - import { useParams } from 'common' import { subscriptionHasHipaaAddon } from 'components/interfaces/Billing/Subscription/Subscription.utils' +import { AiAssistantDropdown } from 'components/ui/AiAssistantDropdown' import CopyButton from 'components/ui/CopyButton' import { InlineLink, InlineLinkClassName } from 'components/ui/InlineLink' import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { DOCS_URL } from 'lib/constants' +import { ExternalLink, Loader2 } from 'lucide-react' +import { parseAsBoolean, useQueryState } from 'nuqs' +import { forwardRef } from 'react' import { useDatabaseSelectorStateSnapshot } from 'state/database-selector' import { useSqlEditorV2StateSnapshot } from 'state/sql-editor-v2' -import { AiIconAnimation, Button, cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui' +import { Button, cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui' + import Results from './Results' export type UtilityTabResultsProps = { @@ -20,11 +21,12 @@ export type UtilityTabResultsProps = { isExecuting?: boolean isDisabled?: boolean onDebug: () => void + buildDebugPrompt: () => string isDebugging?: boolean } const UtilityTabResults = forwardRef( - ({ id, isExecuting, isDisabled, isDebugging, onDebug }) => { + ({ id, isExecuting, isDisabled, isDebugging, onDebug, buildDebugPrompt }) => { const { ref } = useParams() const state = useDatabaseSelectorStateSnapshot() const { data: organization } = useSelectedOrganizationQuery() @@ -155,13 +157,14 @@ const UtilityTabResults = forwardRef( )} {!hasHipaaAddon && ( - + loading={isDebugging} + /> )}
diff --git a/apps/studio/components/ui/AiAssistantDropdown.tsx b/apps/studio/components/ui/AiAssistantDropdown.tsx new file mode 100644 index 0000000000000..9a44649f4f8bc --- /dev/null +++ b/apps/studio/components/ui/AiAssistantDropdown.tsx @@ -0,0 +1,121 @@ +import { AiPromptCopiedEvent } from 'common/telemetry-constants' +import { useTrack } from 'lib/telemetry/track' +import { Check, ChevronDown, Copy } from 'lucide-react' +import { ComponentProps, useEffect, useState } from 'react' +import { + AiIconAnimation, + Button, + cn, + copyToClipboard, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + Tooltip, + TooltipContent, + TooltipTrigger, +} from 'ui' + +type TelemetrySource = AiPromptCopiedEvent['properties']['source'] + +export interface AiAssistantDropdownProps { + buildPrompt: () => string + label: string + iconOnly?: boolean + onOpenAssistant: () => void + telemetrySource?: TelemetrySource + size?: ComponentProps['size'] + type?: ComponentProps['type'] + disabled?: boolean + loading?: boolean + className?: string + tooltip?: string +} + +export function AiAssistantDropdown({ + buildPrompt, + label, + iconOnly = false, + onOpenAssistant, + telemetrySource, + size = 'tiny', + type = 'default', + disabled = false, + loading = false, + className, + tooltip, +}: AiAssistantDropdownProps) { + const track = useTrack() + const [showCopied, setShowCopied] = useState(false) + const [isOpen, setIsOpen] = useState(false) + + useEffect(() => { + if (!showCopied) return + const timer = setTimeout(() => setShowCopied(false), 2000) + return () => clearTimeout(timer) + }, [showCopied]) + + const handleCopyPrompt = () => { + const prompt = buildPrompt() + copyToClipboard(prompt) + setShowCopied(true) + setIsOpen(false) + + if (telemetrySource) { + track('ai_prompt_copied', { source: telemetrySource }) + } + } + + const handleOpenAssistant = () => { + onOpenAssistant() + } + + const buttonContent = ( +
+ {/* Main button */} + + + {/* Dropdown trigger */} + + +
+ ) + + // Wrap in tooltip for icon-only mode + if (iconOnly && tooltip) { + return ( + + +
{buttonContent}
+
+ {tooltip} +
+ ) + } + + return buttonContent +} diff --git a/packages/common/telemetry-constants.ts b/packages/common/telemetry-constants.ts index edd2cbb46b284..a87758c7847a9 100644 --- a/packages/common/telemetry-constants.ts +++ b/packages/common/telemetry-constants.ts @@ -2488,6 +2488,31 @@ export interface QueryPerformanceAIExplanationButtonClickedEvent { groups: TelemetryGroups } +/** + * User copied an AI prompt to clipboard instead of using the built-in assistant. + * This allows users to paste the prompt into external AI tools (Cursor, Claude, etc.) + * + * @group Events + * @source studio + */ +export interface AiPromptCopiedEvent { + action: 'ai_prompt_copied' + properties: { + /** + * Source/location where the prompt was copied from + */ + source: + | 'explain_visualizer' + | 'query_performance' + | 'sql_debug' + | 'lint_detail' + | 'advisor_section' + | 'advisor_widget' + | 'branch_review' + } + groups: TelemetryGroups +} + /** * User opened the request upgrade modal (for users without billing permissions). * @@ -2788,6 +2813,7 @@ export type TelemetryEvent = | AdvisorDetailOpenedEvent | AdvisorAssistantButtonClickedEvent | QueryPerformanceAIExplanationButtonClickedEvent + | AiPromptCopiedEvent | RequestUpgradeModalOpenedEvent | RequestUpgradeSubmittedEvent | DashboardErrorCreatedEvent From 6b805289c7701d47f1dac0df3402cf481804220d Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Tue, 10 Feb 2026 16:18:53 -0500 Subject: [PATCH 4/4] chore(studio): remove feature flag for dataApiExposedBadge (#42563) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cleanup / chore — removing a feature flag that has been at 100% rollout with no issues. ## What is the current behavior? The `dataApiExposedBadge` feature flag is checked at runtime via ConfigCat, even though it has been set to 100% for a while with no issues. ## What is the new behavior? The feature flag check is removed and the gated behavior is now unconditionally enabled. A reminder has been set to delete the flag from ConfigCat in a month. ## Summary by CodeRabbit **Refactor** * Simplified API access status indicators in the studio editor, ensuring security-related tooltips now display consistently based on actual access conditions. * Updated component interfaces and consolidated internal hook dependencies for improved code maintainability and organization. --- .../TableEditorLayout/EntityListItem.tsx | 55 ++++++++++--------- .../misc/useDataApiGrantTogglesEnabled.ts | 6 +- e2e/studio/features/table-editor.spec.ts | 19 ++++--- 3 files changed, 40 insertions(+), 40 deletions(-) diff --git a/apps/studio/components/layouts/TableEditorLayout/EntityListItem.tsx b/apps/studio/components/layouts/TableEditorLayout/EntityListItem.tsx index 86007254e064c..1e33bfeb2844c 100644 --- a/apps/studio/components/layouts/TableEditorLayout/EntityListItem.tsx +++ b/apps/studio/components/layouts/TableEditorLayout/EntityListItem.tsx @@ -1,31 +1,13 @@ -import { useFlag, useParams } from 'common' -import { buildTableEditorUrl } from 'components/grid/SupabaseGrid.utils' -import { useTableFilter } from 'components/grid/hooks/useTableFilter' -import { getEntityLintDetails } from 'components/interfaces/TableGridEditor/TableEntity.utils' -import { EntityTypeIcon } from 'components/ui/EntityTypeIcon' -import { InlineLink } from 'components/ui/InlineLink' -import { getTableDefinition } from 'data/database/table-definition-query' -import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants' -import { Entity } from 'data/entity-types/entity-types-infinite-query' -import { useProjectLintsQuery } from 'data/lint/lint-query' -import { EditorTablePageLink } from 'data/prefetchers/project.$ref.editor.$id' -import type { TableApiAccessData, TableApiAccessMap } from 'data/privileges/table-api-access-query' -import { useTableRowsCountQuery } from 'data/table-rows/table-rows-count-query' -import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState' -import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' +import { useParams } from 'common' import { Copy, Download, Edit, Globe, Lock, MoreVertical, Trash } from 'lucide-react' import Link from 'next/link' import { type CSSProperties } from 'react' import { toast } from 'sonner' -import { - type RoleImpersonationState, - useRoleImpersonationStateSnapshot, -} from 'state/role-impersonation-state' -import { useTableEditorStateSnapshot } from 'state/table-editor' -import { createTabId, useTabsStateSnapshot } from 'state/tabs' import { Badge, Button, + cn, + copyToClipboard, DropdownMenu, DropdownMenuContent, DropdownMenuItem, @@ -38,12 +20,33 @@ import { TooltipContent, TooltipTrigger, TreeViewItemVariant, - cn, - copyToClipboard, } from 'ui' import { useExportAllRowsAsCsv, useExportAllRowsAsSql } from './ExportAllRows' +import { useTableFilter } from '@/components/grid/hooks/useTableFilter' +import { buildTableEditorUrl } from '@/components/grid/SupabaseGrid.utils' +import { getEntityLintDetails } from '@/components/interfaces/TableGridEditor/TableEntity.utils' +import { EntityTypeIcon } from '@/components/ui/EntityTypeIcon' +import { InlineLink } from '@/components/ui/InlineLink' +import { getTableDefinition } from '@/data/database/table-definition-query' +import { ENTITY_TYPE } from '@/data/entity-types/entity-type-constants' +import { Entity } from '@/data/entity-types/entity-types-infinite-query' +import { useProjectLintsQuery } from '@/data/lint/lint-query' +import { EditorTablePageLink } from '@/data/prefetchers/project.$ref.editor.$id' +import type { + TableApiAccessData, + TableApiAccessMap, +} from '@/data/privileges/table-api-access-query' +import { useTableRowsCountQuery } from '@/data/table-rows/table-rows-count-query' +import { useQuerySchemaState } from '@/hooks/misc/useSchemaQueryState' +import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { formatSql } from '@/lib/formatSql' +import { + useRoleImpersonationStateSnapshot, + type RoleImpersonationState, +} from '@/state/role-impersonation-state' +import { useTableEditorStateSnapshot } from '@/state/table-editor' +import { createTabId, useTabsStateSnapshot } from '@/state/tabs' export interface EntityListItemProps { id: number | string @@ -79,7 +82,6 @@ export const EntityListItem = ({ const tabs = useTabsStateSnapshot() const isPreview = tabs.previewTabId === tabId - const isOpened = Object.values(tabs.tabsMap).some((tab) => tab.metadata?.tableId === entity.id) const isActive = Number(id) === entity.id const canEdit = isActive && !isLocked @@ -405,7 +407,6 @@ const EntityTooltipTrigger = ({ apiAccessData?: TableApiAccessData }) => { const { ref } = useParams() - const isDataApiExposedBadgeEnabled = useFlag('dataApiExposedBadge') let tooltipContent = null const accessWarning = 'Data is publicly accessible via API' @@ -476,7 +477,7 @@ const EntityTooltipTrigger = ({ entity.type === ENTITY_TYPE.TABLE && apiAccessData?.apiAccessType === 'access' && tableHasRlsEnabledNoPolicyLint - if (isDataApiExposedBadgeEnabled && isRlsEnabledNoPolicies) { + if (isRlsEnabledNoPolicies) { return ( @@ -492,7 +493,7 @@ const EntityTooltipTrigger = ({ const isApiExposedWithRlsAndPolicies = apiAccessData?.apiAccessType === 'access' && !tableHasRlsEnabledNoPolicyLint - if (isDataApiExposedBadgeEnabled && isApiExposedWithRlsAndPolicies) { + if (isApiExposedWithRlsAndPolicies) { return ( diff --git a/apps/studio/hooks/misc/useDataApiGrantTogglesEnabled.ts b/apps/studio/hooks/misc/useDataApiGrantTogglesEnabled.ts index e79771a31181a..fdd1f92e80709 100644 --- a/apps/studio/hooks/misc/useDataApiGrantTogglesEnabled.ts +++ b/apps/studio/hooks/misc/useDataApiGrantTogglesEnabled.ts @@ -1,6 +1,5 @@ -import { useFlag } from 'common' -import { IS_TEST_ENV } from 'lib/constants' import { usePHFlag } from '../ui/useFlag' +import { IS_TEST_ENV } from '@/lib/constants' /** * Determine whether a user has access to Data API grant toggles. @@ -12,7 +11,6 @@ import { usePHFlag } from '../ui/useFlag' * without requiring the feature flag infrastructure. */ export const useDataApiGrantTogglesEnabled = (): boolean => { - const isDataApiBadgesEnabled = useFlag('dataApiExposedBadge') const isTableEditorApiAccessEnabled = usePHFlag('tableEditorApiAccessToggle') // In test environment, enable the feature for E2E testing @@ -20,5 +18,5 @@ export const useDataApiGrantTogglesEnabled = (): boolean => { return true } - return isDataApiBadgesEnabled && !!isTableEditorApiAccessEnabled + return !!isTableEditorApiAccessEnabled } diff --git a/e2e/studio/features/table-editor.spec.ts b/e2e/studio/features/table-editor.spec.ts index 61933ed6b5613..7cc237fc4fdcf 100644 --- a/e2e/studio/features/table-editor.spec.ts +++ b/e2e/studio/features/table-editor.spec.ts @@ -1,8 +1,9 @@ -import { expect, Page } from '@playwright/test' import fs from 'fs' import path from 'path' -import { createTable as dbCreateTable, dropTable } from '../utils/db/index.js' +import { expect, Page } from '@playwright/test' + import { env } from '../env.config.js' +import { createTable as dbCreateTable, dropTable } from '../utils/db/index.js' import { releaseFileOnceCleanup, withFileOnceSetup } from '../utils/once-per-file.js' import { resetLocalStorage } from '../utils/reset-local-storage.js' import { test } from '../utils/test.js' @@ -165,7 +166,7 @@ testRunner('table editor', () => { await page .getByRole('button', { name: `View ${tableNameActions}`, exact: true }) .getByRole('button') - .nth(1) + .nth(2) .click() await page.getByRole('menuitem', { name: 'Copy name' }).click() await page.waitForTimeout(500) @@ -176,7 +177,7 @@ testRunner('table editor', () => { await page .getByRole('button', { name: `View ${tableNameActions}`, exact: true }) .getByRole('button') - .nth(1) + .nth(2) .click() await page.getByRole('menuitem', { name: 'Copy table schema' }).click() await waitForApiResponse(page, 'pg-meta', ref, 'query?key=table-definition-') // wait for endpoint to generate schema @@ -193,7 +194,7 @@ testRunner('table editor', () => { await page .getByRole('button', { name: `View ${tableNameActions}`, exact: true }) .getByRole('button') - .nth(1) + .nth(2) .click() await page.getByRole('menuitem', { name: 'Duplicate table' }).click() await page.getByRole('button', { name: 'Save' }).click() @@ -367,7 +368,7 @@ testRunner('table editor', () => { await page .getByRole('button', { name: `View ${tableNameGridEditor}`, exact: true }) .getByRole('button') - .nth(1) + .nth(2) .click() await page.getByRole('menuitem', { name: 'Edit table' }).click() await page.getByTestId('table-name-input').fill(tableNameUpdated) @@ -386,7 +387,7 @@ testRunner('table editor', () => { await page .getByRole('button', { name: `View ${tableNameUpdated}`, exact: true }) .getByRole('button') - .nth(1) + .nth(2) .click() // Open nested export submenu via keyboard (more stable than hover in headless) const exportDataItemCsv = page.getByRole('menuitem', { name: 'Export data' }) @@ -425,7 +426,7 @@ testRunner('table editor', () => { await page .getByRole('button', { name: `View ${tableNameUpdated}`, exact: true }) .getByRole('button') - .nth(1) + .nth(2) .click() // Open nested export submenu via keyboard (more stable than hover in headless) const exportDataItemSql = page.getByRole('menuitem', { name: 'Export data' }) @@ -459,7 +460,7 @@ testRunner('table editor', () => { await page .getByRole('button', { name: `View ${tableNameUpdated}`, exact: true }) .getByRole('button') - .nth(1) + .nth(2) .click() const exportDataItemCli = page.getByRole('menuitem', { name: 'Export data' })