mirror of
https://github.com/supabase/supabase.git
synced 2026-10-08 10:55:06 +03:00
## Summary A round of small Explorer refinements. **Sidebar** - Adds a **Run SQL** row (with a `+` icon) above Notebooks in the Explorer sidebar; opens a new query tab. **Assistant** - Assistant query cells now have the same **Save** dropdown as query tabs (add to an existing notebook or create a new one). It shows only when Explorer is enabled, and not while the query is still streaming. - `SaveQueryDropdown` takes an optional `source`, so logs queries are saved as log cells (keeping their time range) instead of database cells. This also fixes saving logs queries from query tabs. - The "Drafting notebook..." notice (and the notebook loading/status rows) now span the full message width; `delete_notebook` parts use the wide layout like create/update. **Onboarding** - Replaces the single page with a four-step walkthrough: Welcome to Explorer (with a **Preview** badge), Run SQL, Notebooks, and Chat with your project. Each step has an icon, heading, and short description, with step dots and **Skip** / **Back** / **Next** buttons; the last step ends with **Continue to Explorer**. - Removes the "Choose how Explorer opens" choice (still available in Account preferences) and the collapsible "Learn more" section. Skipping or finishing still respects the saved startup preference. - Deletes `ExplorerOnboardingLearnMore`, `ExplorerHomePreference`, and `ExplorerHomePreview`, which were only used by onboarding. **Notebooks** - Query cells use the same max width as markdown cells (`48rem`, was `72rem`). - "Add query cell" / "Add markdown cell" are now **Add query** / **Add markdown** everywhere; the buttons at the bottom of a notebook are larger (34px, 18px icons). ## Test plan - [ ] Explorer sidebar: **Run SQL** opens a new query tab - [ ] Assistant: generate SQL, use **Save** to add it to a new and an existing notebook; repeat with a logs query and confirm a log cell is created - [ ] Assistant: ask for a notebook and confirm the drafting notice is full width - [ ] Clear `hasCompletedOnboarding` in Explorer preferences and step through onboarding (Next / Back / Skip); finishing or skipping respects the startup preference set in Account preferences - [ ] Notebook: query cells line up with markdown cell width; bottom add buttons are larger <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added a **Run SQL** shortcut to Explorer navigation. - Assistant query results can now be saved to notebooks, including log queries. - **Improvements** - Updated Explorer onboarding with guided steps, progress navigation, and visual previews. - Shortened Explorer action labels and refined control sizing. - Reduced notebook query layout width and adjusted assistant notebook displays. - **Changes** - Removed the Explorer startup preference selector and onboarding “Learn more” section. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
200 lines
6.3 KiB
TypeScript
200 lines
6.3 KiB
TypeScript
import { useRef, useState } from 'react'
|
|
|
|
import { identifyQueryType } from './AIAssistant.utils'
|
|
import {
|
|
changeAssistantQuerySource,
|
|
createAssistantQueryModel,
|
|
DEFAULT_ASSISTANT_LOGS_QUERY_TITLE,
|
|
DEFAULT_ASSISTANT_QUERY_TITLE,
|
|
getAssistantQueryDisplay,
|
|
setAssistantQuerySql,
|
|
shouldClearAssistantQueryResult,
|
|
} from './AssistantQueryCell.utils'
|
|
import { Confirm } from './Confirm'
|
|
import { type ConfirmFooterApprovalState } from './Confirm.utils'
|
|
import { useIsExplorerEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
|
|
import { QueryEditor } from '@/components/interfaces/Explorer/QueryEditor'
|
|
import { SaveQueryDropdown } from '@/components/interfaces/Explorer/SaveQueryDropdown'
|
|
import { type QueryDisplay, type QueryResult } from '@/components/interfaces/Explorer/types'
|
|
import {
|
|
type QuerySourceBinding,
|
|
type QuerySourceTag,
|
|
} from '@/data/query-sources/query-source-registry'
|
|
import { useTrack } from '@/lib/telemetry/track'
|
|
import { useAiAssistantState } from '@/state/ai-assistant-state'
|
|
import { useLocalRoleImpersonationState } from '@/state/role-impersonation-state'
|
|
|
|
interface AssistantQueryCellProps {
|
|
id: string
|
|
sql: string
|
|
title?: string
|
|
initialResult?: QueryResult
|
|
source?: QuerySourceBinding
|
|
view?: 'table' | 'chart'
|
|
xAxis?: string
|
|
yAxis?: string
|
|
/** Follow incoming SQL while the assistant is still streaming the query text. */
|
|
isStreaming?: boolean
|
|
confirmState?: ConfirmFooterApprovalState
|
|
onApprove?: () => void
|
|
onDeny?: () => void
|
|
}
|
|
|
|
const DEFAULT_SOURCE: QuerySourceBinding = { _tag: 'database' }
|
|
|
|
const QUERY_OUTCOME_MESSAGES: Record<
|
|
QuerySourceTag,
|
|
{ success: string; error: string; denied: string }
|
|
> = {
|
|
database: {
|
|
success: 'Query executed',
|
|
error: 'Failed to execute SQL',
|
|
denied: 'Skipped query',
|
|
},
|
|
logs: {
|
|
success: 'Query executed',
|
|
error: 'Failed to query logs',
|
|
denied: 'Skipped query',
|
|
},
|
|
}
|
|
|
|
/** Assistant adapter around the shared QueryEditor. Local state only — nothing is persisted. */
|
|
export const AssistantQueryCell = ({
|
|
id,
|
|
sql: initialSql,
|
|
title: initialTitle,
|
|
initialResult,
|
|
source = DEFAULT_SOURCE,
|
|
view,
|
|
xAxis,
|
|
yAxis,
|
|
isStreaming = false,
|
|
confirmState,
|
|
onApprove,
|
|
onDeny,
|
|
}: AssistantQueryCellProps) => {
|
|
const track = useTrack()
|
|
const roleImpersonationState = useLocalRoleImpersonationState()
|
|
const aiAssistantState = useAiAssistantState()
|
|
const isExplorerEnabled = useIsExplorerEnabled()
|
|
|
|
const fallbackTitle =
|
|
initialTitle?.trim() ||
|
|
(source._tag === 'logs' ? DEFAULT_ASSISTANT_LOGS_QUERY_TITLE : DEFAULT_ASSISTANT_QUERY_TITLE)
|
|
|
|
const hasExplicitAxes = Boolean(xAxis || yAxis)
|
|
|
|
const [title, setTitle] = useState(fallbackTitle)
|
|
const [query, setQuery] = useState(() => createAssistantQueryModel(initialSql, source))
|
|
const [showQuery, setShowQuery] = useState(true)
|
|
// undefined uses the tool output; null intentionally clears it after changing source.
|
|
const [resultOverride, setResultOverride] = useState<QueryResult | null>()
|
|
const [localDisplay, setLocalDisplay] = useState<QueryDisplay | undefined>(undefined)
|
|
const previousId = useRef(id)
|
|
|
|
if (previousId.current !== id) {
|
|
previousId.current = id
|
|
setTitle(fallbackTitle)
|
|
setQuery(createAssistantQueryModel(initialSql, source))
|
|
setShowQuery(true)
|
|
setResultOverride(undefined)
|
|
setLocalDisplay(undefined)
|
|
}
|
|
|
|
if (isStreaming && query.uncheckedSql !== initialSql) {
|
|
setQuery((current) => setAssistantQuerySql(current, initialSql))
|
|
}
|
|
|
|
const result = resultOverride === undefined ? initialResult : (resultOverride ?? undefined)
|
|
const display =
|
|
localDisplay ??
|
|
getAssistantQueryDisplay({
|
|
view,
|
|
xAxis,
|
|
yAxis,
|
|
sql: query.uncheckedSql,
|
|
rows: result?.rows,
|
|
})
|
|
|
|
const handleTitleChange = (value: string) => {
|
|
const nextTitle = value.trim()
|
|
if (!nextTitle) return
|
|
setTitle(nextTitle)
|
|
}
|
|
|
|
const handleSourceChange = (nextSource: QuerySourceBinding) => {
|
|
const isBackendChange = nextSource._tag !== query._tag
|
|
if (shouldClearAssistantQueryResult(query, nextSource)) setResultOverride(null)
|
|
if (isBackendChange && !hasExplicitAxes) setLocalDisplay(undefined)
|
|
setQuery((current) => changeAssistantQuerySource(current, nextSource))
|
|
}
|
|
|
|
const handleDisplayChange = (nextDisplay: QueryDisplay) => {
|
|
setLocalDisplay(nextDisplay)
|
|
}
|
|
|
|
const handleResultChange = (nextResult: QueryResult) => {
|
|
setResultOverride(nextResult)
|
|
}
|
|
|
|
const handleRun = () => {
|
|
const sql = query.uncheckedSql
|
|
const mutationType = identifyQueryType(sql)
|
|
track('assistant_suggestion_run_query_clicked', {
|
|
queryType: mutationType ? 'mutation' : 'select',
|
|
...(mutationType ? { mutationType } : {}),
|
|
})
|
|
}
|
|
|
|
const isRunDisabled =
|
|
confirmState === 'approval-requested' || confirmState === 'approval-responded'
|
|
const outcomeMessages = QUERY_OUTCOME_MESSAGES[source._tag]
|
|
|
|
return (
|
|
<Confirm
|
|
fill
|
|
className="w-full max-w-3xl mx-auto"
|
|
state={confirmState}
|
|
message="Assistant wants to run this query"
|
|
cancelLabel="Skip"
|
|
confirmLabel="Run query"
|
|
confirmLabelLoading="Running..."
|
|
successMessage={outcomeMessages.success}
|
|
errorMessage={outcomeMessages.error}
|
|
deniedMessage={outcomeMessages.denied}
|
|
onCancel={onDeny}
|
|
onConfirm={onApprove}
|
|
>
|
|
<QueryEditor
|
|
isReadOnly
|
|
id={id}
|
|
variant="viewport"
|
|
className="h-96"
|
|
title={title}
|
|
query={query}
|
|
result={result}
|
|
showQuery={showQuery}
|
|
onShowQueryChange={setShowQuery}
|
|
roleImpersonationState={roleImpersonationState}
|
|
display={display}
|
|
isRunDisabled={isRunDisabled}
|
|
onTitleChange={handleTitleChange}
|
|
onSqlChange={(sql) => setQuery((current) => setAssistantQuerySql(current, sql))}
|
|
onSourceChange={handleSourceChange}
|
|
onResultChange={handleResultChange}
|
|
onRowLimitChange={(rowLimit) =>
|
|
setQuery((current) => (current._tag === 'database' ? { ...current, rowLimit } : current))
|
|
}
|
|
onDisplayChange={handleDisplayChange}
|
|
onRun={handleRun}
|
|
onDebug={aiAssistantState.setInitialInput}
|
|
toolbarActions={
|
|
isExplorerEnabled && !isStreaming ? (
|
|
<SaveQueryDropdown query={{ title, sql: query.uncheckedSql }} source={query} />
|
|
) : undefined
|
|
}
|
|
/>
|
|
</Confirm>
|
|
)
|
|
}
|