diff --git a/apps/studio/.claude/skills/explorer/SKILL.md b/apps/studio/.claude/skills/explorer/SKILL.md index 3a62f2dfe19..3a086025fe8 100644 --- a/apps/studio/.claude/skills/explorer/SKILL.md +++ b/apps/studio/.claude/skills/explorer/SKILL.md @@ -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 + + {/* title and actions */} + {/* editable or read-only SQL */} + {/* idle, loading, error, or result display */} + {/* row count or surface metadata */} + +``` + +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 +
+ {/* the same query composition */} +
+``` + +- `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. diff --git a/apps/studio/components/interfaces/Explorer/ExplorerQuery/ExplorerQuery.test.tsx b/apps/studio/components/interfaces/Explorer/ExplorerQuery/ExplorerQuery.test.tsx new file mode 100644 index 00000000000..f01c68a66e9 --- /dev/null +++ b/apps/studio/components/interfaces/Explorer/ExplorerQuery/ExplorerQuery.test.tsx @@ -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( + + + Weekly signups + + SQL editor + Results table + 2 rows + + ) + + 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() + + render( + + SQL editor + Run the query to see results + + ) + + 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( + <> + + Results table + + +
+ Results table +
+
+ + ) + + // 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') + }) +}) diff --git a/apps/studio/components/interfaces/Explorer/ExplorerQuery/index.tsx b/apps/studio/components/interfaces/Explorer/ExplorerQuery/index.tsx new file mode 100644 index 00000000000..c37de6e65b5 --- /dev/null +++ b/apps/studio/components/interfaces/Explorer/ExplorerQuery/index.tsx @@ -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) => ( +
+) +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) => ( +
+) +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) => ( +
+) +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) => ( +
+) +ExplorerQueryResults.displayName = 'ExplorerQueryResults' + +export type ExplorerQueryFooterProps = React.ComponentProps<'div'> + +/** Optional metadata or surface-specific content below the results region. */ +const ExplorerQueryFooter = ({ className, ...props }: ExplorerQueryFooterProps) => ( +
+) +ExplorerQueryFooter.displayName = 'ExplorerQueryFooter' + +export { + ExplorerQuery, + ExplorerQueryEditor, + ExplorerQueryFooter, + ExplorerQueryResults, + ExplorerQueryViewport, +}