diff --git a/apps/studio/components/grid/components/grid/Grid.tsx b/apps/studio/components/grid/components/grid/Grid.tsx
index 1159273f2049e..0ee375115afdf 100644
--- a/apps/studio/components/grid/components/grid/Grid.tsx
+++ b/apps/studio/components/grid/components/grid/Grid.tsx
@@ -230,8 +230,12 @@ export const Grid = memo(
return (
{/* Render no rows fallback outside of the DataGrid */}
{(rows ?? []).length === 0 && (
@@ -241,9 +245,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 && (
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 && (
-
}
- onClick={handleExplainWithAI}
- >
- Explain with AI
-
+ 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
-
}
- onClick={handleAskAssistant}
- >
- Ask Assistant
-
+
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/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 */}
+ }
+ className={cn('rounded-r-none border-r-0', iconOnly && 'px-1.5', className)}
+ >
+ {!iconOnly && label}
+
+
+ {/* Dropdown trigger */}
+
+
+ }
+ />
+
+
+
+ {showCopied ? : }
+ {showCopied ? 'Copied!' : 'Copy prompt'}
+
+
+
+
+ )
+
+ // Wrap in tooltip for icon-only mode
+ if (iconOnly && tooltip) {
+ return (
+
+
+ {buttonContent}
+
+ {tooltip}
+
+ )
+ }
+
+ return buttonContent
+}
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/files/table-editor-drag-drop.csv b/e2e/studio/features/files/table-editor-drag-drop.csv
new file mode 100644
index 0000000000000..cef853d61e76a
--- /dev/null
+++ b/e2e/studio/features/files/table-editor-drag-drop.csv
@@ -0,0 +1,4 @@
+id,pw_column
+1,drag drop value 1
+2,drag drop value 2
+3,drag drop value 3
diff --git a/e2e/studio/features/table-editor.spec.ts b/e2e/studio/features/table-editor.spec.ts
index 662816d456339..7cc237fc4fdcf 100644
--- a/e2e/studio/features/table-editor.spec.ts
+++ b/e2e/studio/features/table-editor.spec.ts
@@ -1,7 +1,9 @@
-import { expect, Page } from '@playwright/test'
import fs from 'fs'
import path from 'path'
+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'
@@ -164,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)
@@ -175,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
@@ -192,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()
@@ -366,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)
@@ -385,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' })
@@ -424,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' })
@@ -458,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' })
@@ -1172,4 +1174,65 @@ testRunner('table editor', () => {
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)
+ })
})
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