mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 01:45:10 +03:00
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:
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,
|
||||
}
|
||||
Reference in new issue
Block a user