Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4d33c79
feat(sql): add query workspace with iceberg bridge [UX-1259]
c-julin Jun 5, 2026
c6c04a2
feat(sql): query workspace UI updates [UX-1259]
c-julin Jun 9, 2026
04a9642
style(sql): replace off-token colours and ad-hoc classes with design …
c-julin Jun 9, 2026
82da880
style(sql): drop redundant inline styles in favour of classes [UX-1259]
c-julin Jun 9, 2026
144d620
chore(frontend): pin zod to ^4.3.6 [UX-1330]
c-julin Jun 11, 2026
2082b77
style(frontend): apply biome lint fixes [UX-1330]
c-julin Jun 11, 2026
0f23979
chore(frontend): add sql-formatter dependency [UX-1330]
c-julin Jun 11, 2026
9ba6936
refactor(sql): replace hand-rolled SQL helpers with library-backed on…
c-julin Jun 11, 2026
02c2f04
feat(sql): clamp wide result cells behind a full-value popover [UX-1330]
c-julin Jun 11, 2026
d626772
test(sql): cover firstKeyword gate and result cell popover [UX-1330]
c-julin Jun 11, 2026
fdee55d
revert(debug): drop overlay simulation tab from debug dialog [UX-1330]
c-julin Jun 11, 2026
3e29711
feat(sql): derive json and array column kinds with brace/bracket icon…
c-julin Jun 11, 2026
854df64
refactor(sql): rebuild catalog tree on registry components with ARIA …
c-julin Jun 11, 2026
a472911
feat(ui): add fill variant to table container [UX-1330]
c-julin Jun 11, 2026
3ae9418
refactor(sql): rebuild results panel on registry components [UX-1330]
c-julin Jun 11, 2026
2185fe3
refactor(sql): rebuild editor chrome on registry components [UX-1330]
c-julin Jun 11, 2026
7244124
test(sql): cover editor tabs, history popover and run flow [UX-1330]
c-julin Jun 11, 2026
f386706
refactor(sql): drop dead proto re-exports and inline shortPgType [UX-…
c-julin Jun 11, 2026
122edf1
fix(ui): center choicebox radio indicator dot [UX-1330]
c-julin Jun 11, 2026
aca5464
refactor(sql): rebuild add-topic wizard on registry components [UX-1330]
c-julin Jun 11, 2026
d3554bd
test(sql): cover add-topic wizard flow [UX-1330]
c-julin Jun 11, 2026
76ffabb
refactor(sql): render results with react-data-grid [UX-1330]
c-julin Jun 11, 2026
c0ee5f3
revert(ui): drop table fill variant; keep registry table vanilla [UX-…
c-julin Jun 11, 2026
d9adf1d
fix(sql): fill leftover grid width with a 1fr spacer column [UX-1330]
c-julin Jun 11, 2026
645fed8
fix(sql): flex result columns to fill panel width [UX-1330]
c-julin Jun 11, 2026
b9b144c
refactor(sql): migrate SQL editor from Monaco to CodeMirror 6
c-julin Jun 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ fixname.sh
# IDEs
**/.vscode
**/.idea
**/.cursor
**/*.code-workspace

# Helper Scripts
Expand All @@ -17,7 +18,14 @@ requests.txt
.prettierrc

# Local build tools installed via Taskfiles
build
/build
/configs

.cursor
# Go workspace (local dev only)
go.work
go.work.sum

# Claude Code local state
.claude/worktrees/
.claude/settings.local.json
.claude/worktrees/
67 changes: 54 additions & 13 deletions frontend/bun.lock

Large diffs are not rendered by default.

22 changes: 19 additions & 3 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,20 +55,26 @@
"@buf/redpandadata_cloud.connectrpc_query-es": "^2.2.0-20251128173054-b9f9fc6e5a70.1",
"@buf/redpandadata_common.bufbuild_es": "^2.11.0-20260316210807-5d899910f714.1",
"@bufbuild/cel": "^0.4.0",
"@bufbuild/protobuf": "^2.11.0",
"@bufbuild/protobuf": "^2.12.0",
"@bufbuild/protoc-gen-es": "^2.10.0",
"@builder.io/sdk-react": "^4.2.4",
"@chakra-ui/object-utils": "^2.1",
"@chakra-ui/popper": "^2.1",
"@chakra-ui/portal": "^2.1",
"@chakra-ui/react-use-disclosure": "^2.1",
"@chakra-ui/system": "^2.1",
"@codemirror/autocomplete": "^6.20.3",
"@codemirror/lang-sql": "^6.10.0",
"@codemirror/language": "^6.12.3",
"@codemirror/state": "^6.6.0",
"@codemirror/view": "^6.43.1",
"@connectrpc/connect": "^2.1.0",
"@connectrpc/connect-query": "^2.2.0",
"@connectrpc/connect-web": "^2.1.0",
"@emotion/css": "^11.13.5",
"@hookform/resolvers": "^5.2.2",
"@icons-pack/react-simple-icons": "^13.8.0",
"@lezer/highlight": "^1.2.3",
"@milkdown/kit": "^7.18.0",
"@milkdown/react": "^7.18.0",
"@modelcontextprotocol/sdk": "^1.29.0",
Expand All @@ -86,14 +92,15 @@
"@tanstack/react-virtual": "^3.13.12",
"@tanstack/zod-adapter": "^1.167.0",
"@types/prismjs": "^1.26.5",
"@uiw/react-codemirror": "^4.25.10",
"@xyflow/react": "^12.9.2",
"ai": "^6.0.168",
"array-move": "^4.0.0",
"chakra-react-select": "5.0.5",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"date-fns": "^4.3.0",
"dexie": "^4.2.1",
"dotenv": "^17.2.3",
"es-cookie": "^1.5.0",
Expand All @@ -117,12 +124,13 @@
"react": "^18.3.1",
"react-beautiful-dnd": "^13.1.1",
"react-compiler-runtime": "^1.0.0",
"react-data-grid": "7.0.0-beta.47",
"react-day-picker": "^9.14.0",
"react-dom": "^18.3.1",
"react-draggable": "^4.5.0",
"react-dropzone": "^15.0.0",
"react-highlight-words": "^0.21.0",
"react-hook-form": "^7.72.0",
"react-hook-form": "^7.76.1",
"react-markdown": "^10.1.0",
"react-resizable-panels": "^3.0.6",
"react-simple-code-editor": "^0.14.1",
Expand All @@ -132,6 +140,7 @@
"remark-gfm": "^4.0.1",
"shiki": "^3.23.0",
"sonner": "^2.0.7",
"sql-formatter": "^15.8.1",
"stacktrace-js": "^2.0.2",
"streamdown": "^2.5.0",
"tailwind-merge": "^3.5.0",
Expand Down Expand Up @@ -198,6 +207,13 @@
"vitest-browser-react": "^2.2.0"
},
"overrides": {
"@codemirror/autocomplete": "^6.20.3",
"@codemirror/commands": "^6.10.3",
"@codemirror/language": "^6.12.3",
"@codemirror/lint": "^6.9.7",
"@codemirror/search": "^6.7.0",
"@codemirror/state": "^6.6.0",
"@codemirror/view": "^6.43.1",
"dompurify": "^3.4.0",
"prismjs": "^1.30.0",
"baseline-browser-mapping": "2.10.33"
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export const FEATURE_FLAGS = {
enableNewSecurityPage: true,
enableTeamsBridge: false,
enableNewTopicPage: true,
enableSqlInConsole: true,
};

// Cloud-managed tag keys for service account integration
Expand Down
160 changes: 160 additions & 0 deletions frontend/src/components/pages/sql/catalog-tree.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/**
* Copyright 2026 Redpanda Data, Inc.
*
* Use of this software is governed by the Business Source License
* included in the file https://github.com/redpanda-data/redpanda/blob/dev/licenses/bsl.md
*
* As of the Change Date specified in that file, in accordance with
* the Business Source License, use of this software will be governed
* by the Apache License, Version 2.0
*/

import userEvent from '@testing-library/user-event';
import { useDescribeTableQuery, useListTablesQuery, useTopicIcebergQuery } from 'react-query/api/sql';
import { render, screen } from 'test-utils';
import { beforeEach, describe, expect, test, vi } from 'vitest';

import { CatalogTree } from './catalog-tree';
import type { Catalog, TableRef } from './sql-types';

vi.mock('react-query/api/sql', () => ({
useListTablesQuery: vi.fn(),
useDescribeTableQuery: vi.fn(),
useTopicIcebergQuery: vi.fn(),
}));

const tableRef = (name: string, overrides: Partial<TableRef> = {}): TableRef => ({
id: `rp.public.${name}`,
name,
namespaceName: 'public',
catalogName: 'rp',
...overrides,
});

const catalog = (overrides: Partial<Catalog> = {}): Catalog => ({
name: 'rp',
displayLabel: 'Redpanda Catalog',
engine: 'redpanda',
namespaces: [{ id: 'rp.public', name: 'public', tables: [tableRef('orders'), tableRef('users')] }],
...overrides,
});

const noTables = { data: undefined, isLoading: false };

beforeEach(() => {
vi.mocked(useListTablesQuery).mockReturnValue(noTables as never);
vi.mocked(useDescribeTableQuery).mockReturnValue({ data: undefined, isLoading: false } as never);
vi.mocked(useTopicIcebergQuery).mockReturnValue({ isIceberg: false } as never);
});

describe('CatalogTree', () => {
test('renders an ARIA tree with catalogs, namespaces and tables expanded by default', () => {
render(<CatalogTree catalogs={[catalog()]} onQueryTable={vi.fn()} role="viewer" />);

expect(screen.getByRole('tree', { name: 'Catalogs' })).toBeInTheDocument();
const items = screen.getAllByRole('treeitem');
expect(items.map((i) => i.getAttribute('aria-level'))).toEqual(['1', '2', '3', '3']);
expect(screen.getByRole('treeitem', { name: /Redpanda Catalog/ })).toHaveAttribute('aria-expanded', 'true');
expect(screen.getByRole('treeitem', { name: /orders/ })).toBeInTheDocument();
});

test('collapsing a catalog hides its namespaces and tables', async () => {
render(<CatalogTree catalogs={[catalog()]} onQueryTable={vi.fn()} role="viewer" />);

await userEvent.click(screen.getByRole('treeitem', { name: /Redpanda Catalog/ }));

expect(screen.getByRole('treeitem', { name: /Redpanda Catalog/ })).toHaveAttribute('aria-expanded', 'false');
expect(screen.queryByRole('treeitem', { name: /public/ })).toBeNull();
expect(screen.queryByRole('treeitem', { name: /orders/ })).toBeNull();
});

test('search filters tables and shows the matched count', async () => {
render(<CatalogTree catalogs={[catalog()]} onQueryTable={vi.fn()} role="viewer" />);

await userEvent.type(screen.getByPlaceholderText('Search tables'), 'ord');

expect(screen.getByRole('treeitem', { name: /orders/ })).toBeInTheDocument();
expect(screen.queryByRole('treeitem', { name: /users/ })).toBeNull();
expect(screen.getByText('1/2')).toBeInTheDocument();
});

test('clicking the query action calls onQueryTable with the catalog and table', async () => {
const onQueryTable = vi.fn();
render(<CatalogTree catalogs={[catalog()]} onQueryTable={onQueryTable} role="viewer" />);

await userEvent.click(screen.getAllByRole('button', { name: 'Query this table' })[0]);

expect(onQueryTable).toHaveBeenCalledWith(
expect.objectContaining({ name: 'rp' }),
expect.objectContaining({ name: 'orders' })
);
});

test('expanding a table lists its columns with type labels', async () => {
vi.mocked(useDescribeTableQuery).mockReturnValue({
data: {
columns: [
{ name: 'id', type: 'INT8' },
{ name: 'payload', type: 'JSONB' },
{ name: 'tags', type: 'TEXT[]' },
],
},
isLoading: false,
} as never);
render(<CatalogTree catalogs={[catalog()]} onQueryTable={vi.fn()} role="viewer" />);

await userEvent.click(screen.getByRole('treeitem', { name: /orders/ }));

expect(screen.getByText('payload')).toBeInTheDocument();
expect(screen.getByText('jsonb')).toBeInTheDocument();
expect(screen.getByText('text[]')).toBeInTheDocument();
});

test('locked tables are disabled and show no query action', () => {
const locked = catalog({
namespaces: [{ id: 'rp.public', name: 'public', tables: [tableRef('secret', { allowed: false })] }],
});
render(<CatalogTree catalogs={[locked]} onQueryTable={vi.fn()} role="viewer" />);

expect(screen.getByRole('treeitem', { name: /secret/ })).toBeDisabled();
expect(screen.queryByRole('button', { name: 'Query this table' })).toBeNull();
});

test('admin sees the catalog-level add action; viewer does not', () => {
const onAddTable = vi.fn();
const { rerender } = render(
<CatalogTree catalogs={[catalog()]} onAddTable={onAddTable} onQueryTable={vi.fn()} role="admin" />
);
expect(screen.getByRole('button', { name: 'Add a topic to this catalog' })).toBeInTheDocument();

rerender(<CatalogTree catalogs={[catalog()]} onAddTable={onAddTable} onQueryTable={vi.fn()} role="viewer" />);
expect(screen.queryByRole('button', { name: 'Add a topic to this catalog' })).toBeNull();
});

test('namespaces past the page limit paginate with a load-more row', async () => {
const tables = Array.from({ length: 25 }, (_, i) => tableRef(`t${String(i).padStart(2, '0')}`));
const big = catalog({ namespaces: [{ id: 'rp.public', name: 'public', tables }] });
render(<CatalogTree catalogs={[big]} onQueryTable={vi.fn()} role="viewer" />);

expect(screen.getAllByRole('treeitem', { name: /t\d\d/ })).toHaveLength(20);
await userEvent.click(screen.getByRole('button', { name: /Load more · 5 remaining/ }));
expect(screen.getAllByRole('treeitem', { name: /t\d\d/ })).toHaveLength(25);
});

test('arrow keys move focus through visible rows; left collapses', async () => {
render(<CatalogTree catalogs={[catalog()]} onQueryTable={vi.fn()} role="viewer" />);
const catalogRow = screen.getByRole('treeitem', { name: /Redpanda Catalog/ });
const namespaceRow = screen.getByRole('treeitem', { name: /public/ });

catalogRow.focus();
await userEvent.keyboard('{ArrowDown}');
expect(namespaceRow).toHaveFocus();

await userEvent.keyboard('{ArrowUp}');
expect(catalogRow).toHaveFocus();

await userEvent.keyboard('{ArrowLeft}');
expect(catalogRow).toHaveAttribute('aria-expanded', 'false');
expect(screen.queryByRole('treeitem', { name: /public/ })).toBeNull();
});
});
Loading
Loading