Files
Saxon FletcherandClaude Opus 5 252f69e451 chore(studio): refine Explorer sidebar, onboarding, and notebooks (#50555)
## 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>
2026-09-18 14:38:29 +08:00

153 lines
5.5 KiB
TypeScript

import { AlignLeft } from 'lucide-react'
import { forwardRef, useState } from 'react'
import { KeyboardShortcut } from 'ui'
import { type Snapshot } from 'valtio'
import { AddCellDropdown } from '../AddCellDropdown'
import { ExplorerToolbarAction } from '../ExplorerToolbar'
import { MoveCellDropdownContent } from '../MoveCellDropdownContent'
import { QueryEditor, type QueryEditorHandle } from '../QueryEditor'
import { type QueryDisplay, type QueryResult } from '../types'
import {
changeCellSource,
cloneChartConfig,
cloneQueryCell,
getCellDisplay,
setCellRowLimit,
setCellSql,
shouldInvalidateResultOnSourceChange,
toQueryModel,
} from './QueryCell.utils'
import { SortableSection } from '@/components/ui/SortableSection'
import {
isQueryCell,
type QueryCell as QueryCellSchema,
} from '@/data/content/notebooks/notebook-schema'
import { type QuerySourceBinding } from '@/data/query-sources/query-source-registry'
import { useCurrentNotebook, useNotebooksStateSnapshot } from '@/state/notebooks/notebooks-state'
import { useLocalRoleImpersonationState } from '@/state/role-impersonation-state'
import { hotkeyToKeys } from '@/state/shortcuts/formatShortcut'
import { SHORTCUT_DEFINITIONS, SHORTCUT_IDS } from '@/state/shortcuts/registry'
const PRETTIFY_SHORTCUT_KEYS = hotkeyToKeys(
SHORTCUT_DEFINITIONS[SHORTCUT_IDS.SQL_EDITOR_FORMAT].sequence[0]
)
interface QueryCellProps {
cell: Snapshot<QueryCellSchema>
onEdit?: () => void
onPrettifyQuery?: () => void
}
/** Notebook adapter around the shared QueryEditor. */
export const QueryCell = forwardRef<QueryEditorHandle, QueryCellProps>(function QueryCell(
{ cell, onEdit, onPrettifyQuery },
ref
) {
const snap = useNotebooksStateSnapshot()
const currentNotebook = useCurrentNotebook()
const [sql, setSql] = useState<string>(cell.unchecked_sql)
const [result, setResult] = useState<QueryResult>()
const roleImpersonationState = useLocalRoleImpersonationState()
const title = cell.title ?? 'Untitled query'
const showQuery =
snap.cellLocalState.get(cell._id)?.showQuery ?? currentNotebook?.status === 'new'
/**
* Applies an update to this cell. The updater runs against the cell as the store holds
* it rather than the snapshot this component rendered with, so a concurrent edit isn't
* clobbered; `isQueryCell` keeps the per-backend helpers off a markdown cell that
* somehow shares the id.
*/
const updateQueryCell = (updater: (candidate: Snapshot<QueryCellSchema>) => QueryCellSchema) => {
const notebookId = currentNotebook?.notebook.id
if (!notebookId) return
onEdit?.()
snap.updateCell({
id: notebookId,
cellId: cell._id,
updater: (candidate) => {
if (!isQueryCell(candidate)) return candidate
return updater(candidate)
},
})
}
const handleSourceChange = (source: QuerySourceBinding) => {
// The query text carries over (see `changeCellSource`), so the editor's buffer stays
// valid — but a result run against the old source (backend or time range) does not.
if (shouldInvalidateResultOnSourceChange(cell, source)) setResult(undefined)
updateQueryCell((candidate) => changeCellSource(candidate, source))
}
const handleTitleChange = (value: string) => {
const nextTitle = value.trim()
if (!nextTitle) return
updateQueryCell((candidate) => ({ ...cloneQueryCell(candidate), title: nextTitle }))
}
// Running a cell re-commits its current SQL (see QueryEditor's handleRunQuery) even when
// nothing changed — skip the store write so that doesn't spuriously mark the notebook
// unsaved.
const handleSqlCommit = (value: string) => {
if (value === cell.unchecked_sql) return
updateQueryCell((candidate) => setCellSql(candidate, value))
}
const handleDisplayChange = (display: QueryDisplay) =>
updateQueryCell((candidate) => ({
...cloneQueryCell(candidate),
view: display.view,
chart: cloneChartConfig(display.chart),
}))
const handleRowLimitChange = (rowLimit: number) =>
updateQueryCell((candidate) => setCellRowLimit(candidate, rowLimit))
return (
<SortableSection
id={cell._id}
sectionWidth="48rem"
actions={<AddCellDropdown cellId={cell._id} />}
gripDropdownContent={<MoveCellDropdownContent cellId={cell._id} />}
gripClassName="mt-2 sm:opacity-0 group-hover:opacity-100 has-[[data-state=open]]:opacity-100 transition"
>
<QueryEditor
ref={ref}
id={cell._id}
variant="embedded"
className="min-h-0"
title={title}
query={toQueryModel(cell, sql)}
result={result}
showQuery={showQuery}
onShowQueryChange={(showQuery) => snap.setQueryVisibility({ cellId: cell._id, showQuery })}
roleImpersonationState={roleImpersonationState}
display={getCellDisplay(cell)}
onTitleChange={handleTitleChange}
onSqlChange={setSql}
onSqlCommit={handleSqlCommit}
onSourceChange={handleSourceChange}
onResultChange={setResult}
onRowLimitChange={handleRowLimitChange}
onDisplayChange={handleDisplayChange}
toolbarActions={
<ExplorerToolbarAction
icon={<AlignLeft size={16} strokeWidth={2} />}
tooltip={
<div className="flex items-center gap-2.5">
<span>Prettify SQL</span>
<KeyboardShortcut keys={PRETTIFY_SHORTCUT_KEYS} />
</div>
}
onClick={onPrettifyQuery}
/>
}
/>
</SortableSection>
)
})