feat(studio): add Explorer query tabs (#49038)

<img width="1693" height="1037" alt="image"
src="https://github.com/user-attachments/assets/51fdf618-f06e-45ca-bf30-e1307dfbb372"
/>


## Stack

Depends on #49041. Followed by #49028.

## Summary

- add a dedicated ad-hoc query tab type and route under Explorer
- connect query tabs to the shared `QueryEditor` through a `QueryTab`
lifecycle adapter
- add local query draft/result state and restore query tabs from their
routes
- confirm before closing populated local-only drafts and clean up their
state on close

## To test

1. Open Explorer, select **Run SQL**, enter `select 1`, and run the
query.
2. Rename the query, reload the page, then close the tab and confirm the
discard prompt appears.

## Why

Explorer needs a lightweight place to run SQL without creating a
snippet. This layer adds the query-tab lifecycle on top of the shared
editor foundation.

## Impact

Queries in this layer run against the selected project's primary
database. Drafts are local to the browser and are discarded when their
tabs are closed.

## Validation

- fresh non-incremental Studio TypeScript check
- 22 focused tests across query draft state, tab state, and notebook tab
registration

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added support for creating, opening, editing, and running SQL queries
in Explorer.
  * Added project-scoped persistence for query drafts and results.
  * Added dedicated query routes, query icons, and query tabs.
  * Added unsaved-changes warnings when closing query tabs.
  * Added a pinned Explorer Home tab and “New query” option.
  * Improved notebook tab registration and editor tab organization.

* **Bug Fixes**
  * Improved tab navigation, closing behavior, and layout.

* **Tests**
* Added coverage for query persistence, cleanup, restoration, and tab
navigation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
Saxon Fletcher authored and GitHub committed 2026-08-13 23:03:08 +07:00
1 parent 076a205ac4
commit 75d16f360f
23 files changed
+772 -320

No files matched your search

+22
View File
@@ -229,6 +229,7 @@ import { Route as ProjectRefFunctionsFunctionSlugLogsRouteImport } from './route
import { Route as ProjectRefFunctionsFunctionSlugInvocationsRouteImport } from './routes/project/$ref/functions/$functionSlug/invocations'
import { Route as ProjectRefFunctionsFunctionSlugDetailsRouteImport } from './routes/project/$ref/functions/$functionSlug/details'
import { Route as ProjectRefFunctionsFunctionSlugCodeRouteImport } from './routes/project/$ref/functions/$functionSlug/code'
import { Route as ProjectRefExplorerQueryIdRouteImport } from './routes/project/$ref/explorer/query/$id'
import { Route as ProjectRefExplorerNotebookIdRouteImport } from './routes/project/$ref/explorer/notebook/$id'
import { Route as ProjectRefDatabaseTriggersEventRouteImport } from './routes/project/$ref/database/triggers/event'
import { Route as ProjectRefDatabaseTriggersDataRouteImport } from './routes/project/$ref/database/triggers/data'
@@ -1516,6 +1517,12 @@ const ProjectRefFunctionsFunctionSlugCodeRoute =
path: '/code',
getParentRoute: () => ProjectRefFunctionsFunctionSlugRoute,
} as any)
const ProjectRefExplorerQueryIdRoute =
ProjectRefExplorerQueryIdRouteImport.update({
id: '/query/$id',
path: '/query/$id',
getParentRoute: () => ProjectRefExplorerRoute,
} as any)
const ProjectRefExplorerNotebookIdRoute =
ProjectRefExplorerNotebookIdRouteImport.update({
id: '/notebook/$id',
@@ -2283,6 +2290,7 @@ export interface FileRoutesByFullPath {
'/project/$ref/database/triggers/data': typeof ProjectRefDatabaseTriggersDataRoute
'/project/$ref/database/triggers/event': typeof ProjectRefDatabaseTriggersEventRoute
'/project/$ref/explorer/notebook/$id': typeof ProjectRefExplorerNotebookIdRoute
'/project/$ref/explorer/query/$id': typeof ProjectRefExplorerQueryIdRoute
'/project/$ref/functions/$functionSlug/code': typeof ProjectRefFunctionsFunctionSlugCodeRoute
'/project/$ref/functions/$functionSlug/details': typeof ProjectRefFunctionsFunctionSlugDetailsRoute
'/project/$ref/functions/$functionSlug/invocations': typeof ProjectRefFunctionsFunctionSlugInvocationsRoute
@@ -2580,6 +2588,7 @@ export interface FileRoutesByTo {
'/project/$ref/database/triggers/data': typeof ProjectRefDatabaseTriggersDataRoute
'/project/$ref/database/triggers/event': typeof ProjectRefDatabaseTriggersEventRoute
'/project/$ref/explorer/notebook/$id': typeof ProjectRefExplorerNotebookIdRoute
'/project/$ref/explorer/query/$id': typeof ProjectRefExplorerQueryIdRoute
'/project/$ref/functions/$functionSlug/code': typeof ProjectRefFunctionsFunctionSlugCodeRoute
'/project/$ref/functions/$functionSlug/details': typeof ProjectRefFunctionsFunctionSlugDetailsRoute
'/project/$ref/functions/$functionSlug/invocations': typeof ProjectRefFunctionsFunctionSlugInvocationsRoute
@@ -2893,6 +2902,7 @@ export interface FileRoutesById {
'/project/$ref/database/triggers/data': typeof ProjectRefDatabaseTriggersDataRoute
'/project/$ref/database/triggers/event': typeof ProjectRefDatabaseTriggersEventRoute
'/project/$ref/explorer/notebook/$id': typeof ProjectRefExplorerNotebookIdRoute
'/project/$ref/explorer/query/$id': typeof ProjectRefExplorerQueryIdRoute
'/project/$ref/functions/$functionSlug/code': typeof ProjectRefFunctionsFunctionSlugCodeRoute
'/project/$ref/functions/$functionSlug/details': typeof ProjectRefFunctionsFunctionSlugDetailsRoute
'/project/$ref/functions/$functionSlug/invocations': typeof ProjectRefFunctionsFunctionSlugInvocationsRoute
@@ -3205,6 +3215,7 @@ export interface FileRouteTypes {
| '/project/$ref/database/triggers/data'
| '/project/$ref/database/triggers/event'
| '/project/$ref/explorer/notebook/$id'
| '/project/$ref/explorer/query/$id'
| '/project/$ref/functions/$functionSlug/code'
| '/project/$ref/functions/$functionSlug/details'
| '/project/$ref/functions/$functionSlug/invocations'
@@ -3502,6 +3513,7 @@ export interface FileRouteTypes {
| '/project/$ref/database/triggers/data'
| '/project/$ref/database/triggers/event'
| '/project/$ref/explorer/notebook/$id'
| '/project/$ref/explorer/query/$id'
| '/project/$ref/functions/$functionSlug/code'
| '/project/$ref/functions/$functionSlug/details'
| '/project/$ref/functions/$functionSlug/invocations'
@@ -3814,6 +3826,7 @@ export interface FileRouteTypes {
| '/project/$ref/database/triggers/data'
| '/project/$ref/database/triggers/event'
| '/project/$ref/explorer/notebook/$id'
| '/project/$ref/explorer/query/$id'
| '/project/$ref/functions/$functionSlug/code'
| '/project/$ref/functions/$functionSlug/details'
| '/project/$ref/functions/$functionSlug/invocations'
@@ -5561,6 +5574,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ProjectRefFunctionsFunctionSlugCodeRouteImport
parentRoute: typeof ProjectRefFunctionsFunctionSlugRoute
}
'/project/$ref/explorer/query/$id': {
id: '/project/$ref/explorer/query/$id'
path: '/query/$id'
fullPath: '/project/$ref/explorer/query/$id'
preLoaderRoute: typeof ProjectRefExplorerQueryIdRouteImport
parentRoute: typeof ProjectRefExplorerRoute
}
'/project/$ref/explorer/notebook/$id': {
id: '/project/$ref/explorer/notebook/$id'
path: '/notebook/$id'
@@ -6517,11 +6537,13 @@ const ProjectRefEditorRouteWithChildren =
interface ProjectRefExplorerRouteChildren {
ProjectRefExplorerIndexRoute: typeof ProjectRefExplorerIndexRoute
ProjectRefExplorerNotebookIdRoute: typeof ProjectRefExplorerNotebookIdRoute
ProjectRefExplorerQueryIdRoute: typeof ProjectRefExplorerQueryIdRoute
}
const ProjectRefExplorerRouteChildren: ProjectRefExplorerRouteChildren = {
ProjectRefExplorerIndexRoute: ProjectRefExplorerIndexRoute,
ProjectRefExplorerNotebookIdRoute: ProjectRefExplorerNotebookIdRoute,
ProjectRefExplorerQueryIdRoute: ProjectRefExplorerQueryIdRoute,
}
const ProjectRefExplorerRouteWithChildren =