From a7671019a8f60b63dc59fdd49582c9268bd382a5 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Thu, 6 Aug 2026 12:11:35 +0700 Subject: [PATCH] Scaffold the explorer layout (#48740) ## Context Resolves FE-4074 Just adds scaffolding for the explorer UI - no data fetching yet. Initializes the page + side nav, based off Saxon's POC in `poc/explorer-prototype` image ## Summary by CodeRabbit ## New Features - Added a project Explorer page accessible from `/project/:ref/explorer`. - Added navigation for browsing notebooks and chats. - Added search fields, back navigation, animated transitions, and empty states for Explorer sections. - Added a conditional Explorer link to the SQL Editor menu when enabled. ## Documentation - Marked the Explorer route migration as complete in the migration checklist. --- apps/studio/TANSTACK_MIGRATION.md | 1 + .../interfaces/Explorer/ExplorerPage.tsx | 20 +++++ .../ExplorerLayout.constants.tsx | 88 +++++++++++++++++++ .../layouts/ExplorerLayout/ExplorerLayout.tsx | 48 ++++++++++ .../ExplorerLayout/ExplorerNavChats.tsx | 17 ++++ .../ExplorerLayout/ExplorerNavHome.tsx | 55 ++++++++++++ .../ExplorerLayout/ExplorerNavNotebooks.tsx | 22 +++++ .../layouts/Navigation/ProductMenuBar.tsx | 24 +++-- .../layouts/ProjectLayout/index.test.tsx | 2 +- .../layouts/ProjectLayout/index.tsx | 2 +- .../pages/project/[ref]/explorer/index.tsx | 16 ++++ apps/studio/routeTree.gen.ts | 21 +++++ apps/studio/routes/project/$ref/explorer.tsx | 16 ++++ 13 files changed, 325 insertions(+), 7 deletions(-) create mode 100644 apps/studio/components/interfaces/Explorer/ExplorerPage.tsx create mode 100644 apps/studio/components/layouts/ExplorerLayout/ExplorerLayout.constants.tsx create mode 100644 apps/studio/components/layouts/ExplorerLayout/ExplorerLayout.tsx create mode 100644 apps/studio/components/layouts/ExplorerLayout/ExplorerNavChats.tsx create mode 100644 apps/studio/components/layouts/ExplorerLayout/ExplorerNavHome.tsx create mode 100644 apps/studio/components/layouts/ExplorerLayout/ExplorerNavNotebooks.tsx create mode 100644 apps/studio/pages/project/[ref]/explorer/index.tsx create mode 100644 apps/studio/routes/project/$ref/explorer.tsx diff --git a/apps/studio/TANSTACK_MIGRATION.md b/apps/studio/TANSTACK_MIGRATION.md index a30d05471c9..536450f6635 100644 --- a/apps/studio/TANSTACK_MIGRATION.md +++ b/apps/studio/TANSTACK_MIGRATION.md @@ -156,6 +156,7 @@ These are the layout-only TanStack files. Most hold a single product layout comp - [x] A `routes/project/$ref/index.tsx` ← `pages/project/[ref]/index.tsx` (route wraps in `ProjectLayoutWithAuth` itself — see shell delta above) - [x] `routes/project/$ref/merge.tsx` ← `pages/project/[ref]/merge.tsx` (leaf wraps body in `ProjectLayoutWithAuth`; parent `project/$ref.tsx` shell provides DefaultLayout) +- [x] A `routes/project/$ref/explorer.tsx` ← `pages/project/[ref]/explorer/index.tsx` (new placeholder page; leaf wraps body in `ProjectLayoutWithAuth`; parent `project/$ref.tsx` shell provides DefaultLayout) ### Project shell — `/api/*` diff --git a/apps/studio/components/interfaces/Explorer/ExplorerPage.tsx b/apps/studio/components/interfaces/Explorer/ExplorerPage.tsx new file mode 100644 index 00000000000..afa62fcd98f --- /dev/null +++ b/apps/studio/components/interfaces/Explorer/ExplorerPage.tsx @@ -0,0 +1,20 @@ +import { Compass } from 'lucide-react' +import { EmptyStatePresentational } from 'ui-patterns/EmptyStatePresentational' +import { PageContainer } from 'ui-patterns/PageContainer' +import { PageSection, PageSectionContent } from 'ui-patterns/PageSection' + +export const ExplorerPage = () => { + return ( + + + + + + + + ) +} diff --git a/apps/studio/components/layouts/ExplorerLayout/ExplorerLayout.constants.tsx b/apps/studio/components/layouts/ExplorerLayout/ExplorerLayout.constants.tsx new file mode 100644 index 00000000000..0a076fcaa79 --- /dev/null +++ b/apps/studio/components/layouts/ExplorerLayout/ExplorerLayout.constants.tsx @@ -0,0 +1,88 @@ +import { motion } from 'framer-motion' +import { ChevronLeft, MessageSquare, NotebookText } from 'lucide-react' +import { type ComponentType, type PropsWithChildren } from 'react' +import { Button, cn } from 'ui' +import { InnerSideBarFilters, InnerSideBarFilterSearchInput } from 'ui-patterns/InnerSideMenu' + +export type ExplorerResourceType = 'notebook' | 'chat' + +export const LEVEL_OFFSET = 8 +export const LEVEL_TRANSITION = { duration: 0.09, ease: 'easeOut' } as const + +export const EXPLORER_SECTIONS: Array<{ + type: ExplorerResourceType + label: string + icon: ComponentType<{ size?: number; className?: string }> + searchPlaceholder: string +}> = [ + { + type: 'notebook', + label: 'Notebooks', + icon: NotebookText, + searchPlaceholder: 'Search notebooks', + }, + { type: 'chat', label: 'Chats', icon: MessageSquare, searchPlaceholder: 'Search chats' }, +] + +export const rowClassName = (isActive: boolean) => + cn( + 'group relative flex h-7 w-full items-center gap-2 rounded-md pl-3 pr-2 text-sm', + isActive + ? 'bg-selection text-foreground' + : 'text-foreground-light hover:bg-surface-200 hover:text-foreground' + ) + +export const ExplorerNavResourceWrapper = ({ + type, + label, + className, + children, + search, + setSearch, + onBack, +}: PropsWithChildren<{ + type: ExplorerResourceType + label?: string + className?: string + search?: string + setSearch: (value: string) => void + onBack: () => void +}>) => { + const searchPlaceholder = EXPLORER_SECTIONS.find((x) => x.type === type)?.searchPlaceholder + + return ( + +
+
+ {children} +
+ ) +} diff --git a/apps/studio/components/layouts/ExplorerLayout/ExplorerLayout.tsx b/apps/studio/components/layouts/ExplorerLayout/ExplorerLayout.tsx new file mode 100644 index 00000000000..d084a7aeb62 --- /dev/null +++ b/apps/studio/components/layouts/ExplorerLayout/ExplorerLayout.tsx @@ -0,0 +1,48 @@ +import { AnimatePresence } from 'framer-motion' +import { ComponentProps, ReactNode, useState } from 'react' + +import { ProjectLayoutWithAuth } from '../ProjectLayout' +import { type ExplorerResourceType } from './ExplorerLayout.constants' +import { ExplorerNavChats } from './ExplorerNavChats' +import { ExplorerNavHome } from './ExplorerNavHome' +import { ExplorerNavNotebooks } from './ExplorerNavNotebooks' + +export interface ExplorerLayoutProps extends ComponentProps { + children: ReactNode + title?: string +} + +export const ExplorerLayout = ({ browserTitle, children, title }: ExplorerLayoutProps) => { + const [section, setSection] = useState() + + // [Joshen] Temporary, to hook up with tabs store + const activeTabLabel = 'Active Tab Label' + + const mergedBrowserTitle = { + ...browserTitle, + section: title ?? browserTitle?.section, + entity: browserTitle?.entity ?? activeTabLabel, + } + + return ( + + + {section === undefined && } + {section === 'notebook' && ( + setSection(undefined)} /> + )} + {section === 'chat' && ( + setSection(undefined)} /> + )} + + + } + > + {children} + + ) +} diff --git a/apps/studio/components/layouts/ExplorerLayout/ExplorerNavChats.tsx b/apps/studio/components/layouts/ExplorerLayout/ExplorerNavChats.tsx new file mode 100644 index 00000000000..7bef8979324 --- /dev/null +++ b/apps/studio/components/layouts/ExplorerLayout/ExplorerNavChats.tsx @@ -0,0 +1,17 @@ +import { useState } from 'react' + +import { ExplorerNavResourceWrapper } from './ExplorerLayout.constants' + +export const ExplorerNavChats = ({ onBack }: { onBack: () => void }) => { + const [search, setSearch] = useState('') + + // [Joshen] Eventually will have data fetching for notebooks via useAiAssistantState + + return ( + +
+

No chats created yet

+
+
+ ) +} diff --git a/apps/studio/components/layouts/ExplorerLayout/ExplorerNavHome.tsx b/apps/studio/components/layouts/ExplorerLayout/ExplorerNavHome.tsx new file mode 100644 index 00000000000..da7ce2a25a3 --- /dev/null +++ b/apps/studio/components/layouts/ExplorerLayout/ExplorerNavHome.tsx @@ -0,0 +1,55 @@ +import { motion } from 'framer-motion' +import { ChevronRight } from 'lucide-react' + +import { + EXPLORER_SECTIONS, + ExplorerResourceType, + LEVEL_OFFSET, + LEVEL_TRANSITION, + rowClassName, +} from './ExplorerLayout.constants' + +export const ExplorerNavHome = ({ + onSelectSection, +}: { + onSelectSection: (section: ExplorerResourceType) => void +}) => { + return ( + + + +
+

+ Recent +

+

Nothing edited yet

+
+
+ ) +} diff --git a/apps/studio/components/layouts/ExplorerLayout/ExplorerNavNotebooks.tsx b/apps/studio/components/layouts/ExplorerLayout/ExplorerNavNotebooks.tsx new file mode 100644 index 00000000000..d465190d848 --- /dev/null +++ b/apps/studio/components/layouts/ExplorerLayout/ExplorerNavNotebooks.tsx @@ -0,0 +1,22 @@ +import { useState } from 'react' + +import { ExplorerNavResourceWrapper } from './ExplorerLayout.constants' + +export const ExplorerNavNotebooks = ({ onBack }: { onBack: () => void }) => { + const [search, setSearch] = useState('') + + // [Joshen] Eventually will have data fetching for notebooks via RQ + + return ( + +
+

No notebooks created yet

+
+
+ ) +} diff --git a/apps/studio/components/layouts/Navigation/ProductMenuBar.tsx b/apps/studio/components/layouts/Navigation/ProductMenuBar.tsx index 668052b3978..f36c6d8c4d9 100644 --- a/apps/studio/components/layouts/Navigation/ProductMenuBar.tsx +++ b/apps/studio/components/layouts/Navigation/ProductMenuBar.tsx @@ -1,12 +1,23 @@ +import { useFlag, useParams } from 'common' +import Link from 'next/link' import { PropsWithChildren } from 'react' -import { cn } from 'ui' +import { Button, cn } from 'ui' interface ProductMenuBarProps { title: string className?: string } -const ProductMenuBar = ({ title, children, className }: PropsWithChildren) => { +export const ProductMenuBar = ({ + title, + children, + className, +}: PropsWithChildren) => { + // [Joshen] Temporary entry point into explorer + const { ref } = useParams() + const isExplorerEnabled = useFlag('explorer') + const showExplorerCTA = isExplorerEnabled && title === 'SQL Editor' + return (
-
+

{title}

+ {showExplorerCTA && ( + + )}
{children}
) } - -export default ProductMenuBar diff --git a/apps/studio/components/layouts/ProjectLayout/index.test.tsx b/apps/studio/components/layouts/ProjectLayout/index.test.tsx index bfd02e291fe..d7bd6257232 100644 --- a/apps/studio/components/layouts/ProjectLayout/index.test.tsx +++ b/apps/studio/components/layouts/ProjectLayout/index.test.tsx @@ -138,7 +138,7 @@ vi.mock('./PausedState/ProjectPausedState', () => ({ ProjectPausedState: () => n vi.mock('./PauseFailedState', () => ({ PauseFailedState: () => null })) vi.mock('./PausingState', () => ({ PausingState: () => null })) vi.mock('./ProductMenuBar', () => ({ - default: ({ children }: { children: ReactNode }) => <>{children}, + ProductMenuBar: ({ children }: { children: ReactNode }) => <>{children}, })) vi.mock('./ResizingState', () => ({ ResizingState: () => null })) vi.mock('./RestartingState', () => ({ default: () => null })) diff --git a/apps/studio/components/layouts/ProjectLayout/index.tsx b/apps/studio/components/layouts/ProjectLayout/index.tsx index 21e7aad58b3..d9d487962d3 100644 --- a/apps/studio/components/layouts/ProjectLayout/index.tsx +++ b/apps/studio/components/layouts/ProjectLayout/index.tsx @@ -27,7 +27,7 @@ import { import { useEditorType } from '../editors/EditorsLayout.hooks' import { useMainScrollContainer, useSetMainScrollContainer } from '../MainScrollContainerContext' import { useMobileSheet } from '../Navigation/NavigationBar/MobileSheetContext' -import ProductMenuBar from '../Navigation/ProductMenuBar' +import { ProductMenuBar } from '../Navigation/ProductMenuBar' import BuildingState from './BuildingState' import ConnectingState from './ConnectingState' import { getSectionKeyFromPathname, MobileMenuContent } from './LayoutHeader/MobileMenuContent' diff --git a/apps/studio/pages/project/[ref]/explorer/index.tsx b/apps/studio/pages/project/[ref]/explorer/index.tsx new file mode 100644 index 00000000000..82ae81843a3 --- /dev/null +++ b/apps/studio/pages/project/[ref]/explorer/index.tsx @@ -0,0 +1,16 @@ +import { ExplorerPage } from '@/components/interfaces/Explorer/ExplorerPage' +import { DefaultLayout } from '@/components/layouts/DefaultLayout' +import { ExplorerLayout } from '@/components/layouts/ExplorerLayout/ExplorerLayout' +import type { NextPageWithLayout } from '@/types' + +const ProjectExplorerPage: NextPageWithLayout = () => { + return +} + +ProjectExplorerPage.getLayout = (page) => ( + + {page} + +) + +export default ProjectExplorerPage diff --git a/apps/studio/routeTree.gen.ts b/apps/studio/routeTree.gen.ts index 3e6b5c721d8..e7f21b42556 100644 --- a/apps/studio/routeTree.gen.ts +++ b/apps/studio/routeTree.gen.ts @@ -63,6 +63,7 @@ import { Route as ProjectRefMergeRouteImport } from './routes/project/$ref/merge import { Route as ProjectRefLogsRouteImport } from './routes/project/$ref/logs' import { Route as ProjectRefIntegrationsRouteImport } from './routes/project/$ref/integrations' import { Route as ProjectRefFunctionsRouteImport } from './routes/project/$ref/functions' +import { Route as ProjectRefExplorerRouteImport } from './routes/project/$ref/explorer' import { Route as ProjectRefEditorRouteImport } from './routes/project/$ref/editor' import { Route as ProjectRefDatabaseRouteImport } from './routes/project/$ref/database' import { Route as ProjectRefBranchesRouteImport } from './routes/project/$ref/branches' @@ -588,6 +589,11 @@ const ProjectRefFunctionsRoute = ProjectRefFunctionsRouteImport.update({ path: '/functions', getParentRoute: () => ProjectRefRoute, } as any) +const ProjectRefExplorerRoute = ProjectRefExplorerRouteImport.update({ + id: '/explorer', + path: '/explorer', + getParentRoute: () => ProjectRefRoute, +} as any) const ProjectRefEditorRoute = ProjectRefEditorRouteImport.update({ id: '/editor', path: '/editor', @@ -2085,6 +2091,7 @@ export interface FileRoutesByFullPath { '/project/$ref/branches': typeof ProjectRefBranchesRouteWithChildren '/project/$ref/database': typeof ProjectRefDatabaseRouteWithChildren '/project/$ref/editor': typeof ProjectRefEditorRouteWithChildren + '/project/$ref/explorer': typeof ProjectRefExplorerRoute '/project/$ref/functions': typeof ProjectRefFunctionsRouteWithChildren '/project/$ref/integrations': typeof ProjectRefIntegrationsRouteWithChildren '/project/$ref/logs': typeof ProjectRefLogsRouteWithChildren @@ -2387,6 +2394,7 @@ export interface FileRoutesByTo { '/project/$ref/advisors': typeof ProjectRefAdvisorsRouteWithChildren '/project/$ref/auth': typeof ProjectRefAuthRouteWithChildren '/project/$ref/database': typeof ProjectRefDatabaseRouteWithChildren + '/project/$ref/explorer': typeof ProjectRefExplorerRoute '/project/$ref/merge': typeof ProjectRefMergeRoute '/project/$ref/realtime': typeof ProjectRefRealtimeRouteWithChildren '/project/$ref/settings': typeof ProjectRefSettingsRouteWithChildren @@ -2688,6 +2696,7 @@ export interface FileRoutesById { '/project/$ref/branches': typeof ProjectRefBranchesRouteWithChildren '/project/$ref/database': typeof ProjectRefDatabaseRouteWithChildren '/project/$ref/editor': typeof ProjectRefEditorRouteWithChildren + '/project/$ref/explorer': typeof ProjectRefExplorerRoute '/project/$ref/functions': typeof ProjectRefFunctionsRouteWithChildren '/project/$ref/integrations': typeof ProjectRefIntegrationsRouteWithChildren '/project/$ref/logs': typeof ProjectRefLogsRouteWithChildren @@ -2996,6 +3005,7 @@ export interface FileRouteTypes { | '/project/$ref/branches' | '/project/$ref/database' | '/project/$ref/editor' + | '/project/$ref/explorer' | '/project/$ref/functions' | '/project/$ref/integrations' | '/project/$ref/logs' @@ -3298,6 +3308,7 @@ export interface FileRouteTypes { | '/project/$ref/advisors' | '/project/$ref/auth' | '/project/$ref/database' + | '/project/$ref/explorer' | '/project/$ref/merge' | '/project/$ref/realtime' | '/project/$ref/settings' @@ -3598,6 +3609,7 @@ export interface FileRouteTypes { | '/project/$ref/branches' | '/project/$ref/database' | '/project/$ref/editor' + | '/project/$ref/explorer' | '/project/$ref/functions' | '/project/$ref/integrations' | '/project/$ref/logs' @@ -4350,6 +4362,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof ProjectRefFunctionsRouteImport parentRoute: typeof ProjectRefRoute } + '/project/$ref/explorer': { + id: '/project/$ref/explorer' + path: '/explorer' + fullPath: '/project/$ref/explorer' + preLoaderRoute: typeof ProjectRefExplorerRouteImport + parentRoute: typeof ProjectRefRoute + } '/project/$ref/editor': { id: '/project/$ref/editor' path: '/editor' @@ -6716,6 +6735,7 @@ interface ProjectRefRouteChildren { ProjectRefBranchesRoute: typeof ProjectRefBranchesRouteWithChildren ProjectRefDatabaseRoute: typeof ProjectRefDatabaseRouteWithChildren ProjectRefEditorRoute: typeof ProjectRefEditorRouteWithChildren + ProjectRefExplorerRoute: typeof ProjectRefExplorerRoute ProjectRefFunctionsRoute: typeof ProjectRefFunctionsRouteWithChildren ProjectRefIntegrationsRoute: typeof ProjectRefIntegrationsRouteWithChildren ProjectRefLogsRoute: typeof ProjectRefLogsRouteWithChildren @@ -6735,6 +6755,7 @@ const ProjectRefRouteChildren: ProjectRefRouteChildren = { ProjectRefBranchesRoute: ProjectRefBranchesRouteWithChildren, ProjectRefDatabaseRoute: ProjectRefDatabaseRouteWithChildren, ProjectRefEditorRoute: ProjectRefEditorRouteWithChildren, + ProjectRefExplorerRoute: ProjectRefExplorerRoute, ProjectRefFunctionsRoute: ProjectRefFunctionsRouteWithChildren, ProjectRefIntegrationsRoute: ProjectRefIntegrationsRouteWithChildren, ProjectRefLogsRoute: ProjectRefLogsRouteWithChildren, diff --git a/apps/studio/routes/project/$ref/explorer.tsx b/apps/studio/routes/project/$ref/explorer.tsx new file mode 100644 index 00000000000..a67032bc417 --- /dev/null +++ b/apps/studio/routes/project/$ref/explorer.tsx @@ -0,0 +1,16 @@ +import { createFileRoute } from '@tanstack/react-router' + +import { ExplorerPage } from '@/components/interfaces/Explorer/ExplorerPage' +import { ExplorerLayout } from '@/components/layouts/ExplorerLayout/ExplorerLayout' + +export const Route = createFileRoute('/project/$ref/explorer')({ + component: ProjectExplorerRoute, +}) + +function ProjectExplorerRoute() { + return ( + + + + ) +}