Add Studio ExplorerQuery shell (#48926)

## Summary

Adds the Studio-owned `ExplorerQuery` shell used by notebooks, SQL
snippets, dedicated query tabs, and assistant query blocks within
Explorer.

- provides framed embedded and full-height viewport roots
- provides composable editor, results, and footer layout regions
- keeps result content responsible for its own scrolling while the
results region fills remaining height
- remains presentational: query models, execution, source resolution,
persistence, and result rendering stay external
- expands the project-local Explorer agent skill with composition and
sizing guidance
- adds focused Studio component tests

All files are scoped to `apps/studio`; this PR no longer changes
`ui-patterns` or the design-system app.

## Stack

- Base: #48925
- Next: #48961
- This PR targets `chore/toolbar-component`, so its review diff contains
only the query shell layer.

## Validation

- `pnpm --filter studio exec vitest run
components/interfaces/Explorer/ExplorerQuery/ExplorerQuery.test.tsx
components/interfaces/Explorer/ExplorerToolbar/ExplorerToolbar.test.tsx`
— 6 tests passed
- `pnpm --filter studio typecheck`
- Prettier
- `git diff --check`
This commit is contained in:
Saxon Fletcher authored and GitHub committed 2026-08-12 13:21:13 +10:00
1 parent fb80d8cf86
commit 7c4872ad32
3 files changed
+216

No files matched your search

@@ -40,3 +40,41 @@ Compose the toolbar from slots rather than adding resource-specific props:
- Use `ExplorerToolbarAction` for compact direct actions. Icon-only actions are 28px wide automatically.
- Keep execution, persistence, source selection, and other resource state in the consuming Explorer surface.
- Extend layouts with children and `className`; avoid boolean props for resource-specific variants.
## Explorer query shell
Import the layout regions from:
```tsx
import {
ExplorerQuery,
ExplorerQueryEditor,
ExplorerQueryFooter,
ExplorerQueryResults,
ExplorerQueryViewport,
} from '@/components/interfaces/Explorer/ExplorerQuery'
```
Use `ExplorerQuery` for a framed query embedded in a notebook, chat, or another surface. Give it an explicit height when the surrounding surface constrains the cell:
```tsx
<ExplorerQuery className="h-96">
<ExplorerToolbar>{/* title and actions */}</ExplorerToolbar>
<ExplorerQueryEditor>{/* editable or read-only SQL */}</ExplorerQueryEditor>
<ExplorerQueryResults>{/* idle, loading, error, or result display */}</ExplorerQueryResults>
<ExplorerQueryFooter>{/* row count or surface metadata */}</ExplorerQueryFooter>
</ExplorerQuery>
```
Use `ExplorerQueryViewport` when a query owns the content area of an Explorer tab. Its parent must provide a bounded height and `min-h-0`:
```tsx
<div className="min-h-0 flex-1">
<ExplorerQueryViewport>{/* the same query composition */}</ExplorerQueryViewport>
</div>
```
- `ExplorerQueryResults` is always present and fills the space left by the toolbar, editor, and footer.
- A result renderer that can grow supplies its own `min-h-0 flex-1 overflow-auto` container.
- The shell owns layout only. Query models, source resolution, execution, results, display selection, and saved configuration stay controlled by the consumer.
- Compose approval prompts, confirmation notices, and other surface-specific content as children between the standard regions.
@@ -0,0 +1,91 @@
import { render, screen } from '@testing-library/react'
import { createRef } from 'react'
import { describe, expect, it } from 'vitest'
import { ExplorerToolbar, ExplorerToolbarTitle } from '../ExplorerToolbar'
import {
ExplorerQuery,
ExplorerQueryEditor,
ExplorerQueryFooter,
ExplorerQueryResults,
ExplorerQueryViewport,
} from './index'
describe('ExplorerQuery', () => {
it('composes the toolbar, editor, results, and footer without owning their behavior', () => {
render(
<ExplorerQuery aria-label="Weekly signups query">
<ExplorerToolbar aria-label="Explorer query toolbar">
<ExplorerToolbarTitle>Weekly signups</ExplorerToolbarTitle>
</ExplorerToolbar>
<ExplorerQueryEditor>SQL editor</ExplorerQueryEditor>
<ExplorerQueryResults>Results table</ExplorerQueryResults>
<ExplorerQueryFooter>2 rows</ExplorerQueryFooter>
</ExplorerQuery>
)
const cell = screen.getByLabelText('Weekly signups query')
expect(cell).toHaveAttribute('data-slot', 'explorer-query')
expect(cell).toHaveAttribute('data-variant', 'embedded')
expect(cell).toHaveClass('rounded-md', 'border')
expect(screen.getByRole('toolbar', { name: 'Explorer query toolbar' })).toBeInTheDocument()
expect(cell.querySelector('[data-slot="explorer-query-editor"]')).toHaveTextContent(
'SQL editor'
)
expect(cell.querySelector('[data-slot="explorer-query-results"]')).toHaveTextContent(
'Results table'
)
expect(cell.querySelector('[data-slot="explorer-query-footer"]')).toHaveTextContent('2 rows')
})
it('provides an explicit full-height viewport variant and forwards native props', () => {
const viewportRef = createRef<HTMLDivElement>()
render(
<ExplorerQueryViewport
ref={viewportRef}
aria-label="Explorer query tab"
className="custom-viewport"
data-density="compact"
>
<ExplorerQueryEditor>SQL editor</ExplorerQueryEditor>
<ExplorerQueryResults>Run the query to see results</ExplorerQueryResults>
</ExplorerQueryViewport>
)
const viewport = screen.getByLabelText('Explorer query tab')
expect(viewport).toHaveAttribute('data-variant', 'viewport')
expect(viewport).toHaveAttribute('data-density', 'compact')
expect(viewport).toHaveClass('h-full', 'min-h-0', 'custom-viewport')
expect(viewport).not.toHaveClass('rounded-md')
expect(viewportRef.current).toBe(viewport)
})
it('fills the remaining height from the results region itself, in either root and at any depth', () => {
render(
<>
<ExplorerQuery aria-label="Embedded query">
<ExplorerQueryResults>Results table</ExplorerQueryResults>
</ExplorerQuery>
<ExplorerQueryViewport aria-label="Viewport query">
<div data-testid="wrapper">
<ExplorerQueryResults>Results table</ExplorerQueryResults>
</div>
</ExplorerQueryViewport>
</>
)
// The fill behavior belongs to the region, so it survives both roots and a
// wrapper between them — it is not granted by a parent selector.
const [embedded, wrapped] = Array.from(
document.querySelectorAll('[data-slot="explorer-query-results"]')
)
// min-h-0 over a floor: results must shrink rather than push the footer out
// of a height-constrained frame.
for (const results of [embedded, wrapped]) {
expect(results).toHaveClass('flex-1', 'min-h-0', 'overflow-hidden')
}
expect(wrapped.parentElement).toHaveAttribute('data-testid', 'wrapper')
})
})
@@ -0,0 +1,87 @@
import React from 'react'
import { cn } from 'ui'
const explorerQueryClassName = 'flex flex-col overflow-hidden bg-muted'
export type ExplorerQueryProps = React.ComponentProps<'div'>
/**
* Framed Explorer query shell for notebooks and other embedded surfaces.
* The consuming surface owns the height; results fill whatever the toolbar,
* editor, and footer leave behind. The minimum height keeps the frame usable
* when a surface leaves the height to content.
*/
const ExplorerQuery = ({ className, ...props }: ExplorerQueryProps) => (
<div
data-slot="explorer-query"
data-variant="embedded"
className={cn(explorerQueryClassName, 'min-h-64 rounded-md border shadow-xs', className)}
{...props}
/>
)
ExplorerQuery.displayName = 'ExplorerQuery'
export type ExplorerQueryViewportProps = React.ComponentProps<'div'>
/**
* Full-height Explorer query shell for a dedicated tab or another viewport.
* Fills its container so the surrounding page decides the height.
*/
const ExplorerQueryViewport = ({ className, ...props }: ExplorerQueryViewportProps) => (
<div
data-slot="explorer-query"
data-variant="viewport"
className={cn(explorerQueryClassName, 'h-full min-h-0', className)}
{...props}
/>
)
ExplorerQueryViewport.displayName = 'ExplorerQueryViewport'
export type ExplorerQueryEditorProps = React.ComponentProps<'div'>
/** Region containing the editable or read-only SQL editor supplied by the caller. */
const ExplorerQueryEditor = ({ className, ...props }: ExplorerQueryEditorProps) => (
<div
data-slot="explorer-query-editor"
className={cn('min-h-20 shrink-0 overflow-hidden border-b', className)}
{...props}
/>
)
ExplorerQueryEditor.displayName = 'ExplorerQueryEditor'
export type ExplorerQueryResultsProps = React.ComponentProps<'div'>
/**
* Always-present result region for idle, loading, error, table, or chart content.
* Fills the height left by the toolbar, editor, and footer, and shrinks rather
* than pushing them out of the frame. Content that can overflow supplies its own
* scroll container.
*/
const ExplorerQueryResults = ({ className, ...props }: ExplorerQueryResultsProps) => (
<div
data-slot="explorer-query-results"
className={cn('flex min-h-0 w-full flex-1 flex-col overflow-hidden', className)}
{...props}
/>
)
ExplorerQueryResults.displayName = 'ExplorerQueryResults'
export type ExplorerQueryFooterProps = React.ComponentProps<'div'>
/** Optional metadata or surface-specific content below the results region. */
const ExplorerQueryFooter = ({ className, ...props }: ExplorerQueryFooterProps) => (
<div
data-slot="explorer-query-footer"
className={cn('shrink-0 border-t px-3 py-1 text-xs text-foreground-lighter', className)}
{...props}
/>
)
ExplorerQueryFooter.displayName = 'ExplorerQueryFooter'
export {
ExplorerQuery,
ExplorerQueryEditor,
ExplorerQueryFooter,
ExplorerQueryResults,
ExplorerQueryViewport,
}