mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
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` <img width="1389" height="500" alt="image" src="https://github.com/user-attachments/assets/8f293992-97d9-403e-91d6-2e104cd20eb5" /> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## 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. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
1 parent
ec24d69369
commit
a7671019a8
13 files changed
+325
-7
No files matched your search
@@ -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/*`
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<PageContainer size="default">
|
||||
<PageSection>
|
||||
<PageSectionContent>
|
||||
<EmptyStatePresentational
|
||||
icon={Compass}
|
||||
title="Explorer is coming soon"
|
||||
description="This page is a placeholder for the Explorer feature."
|
||||
/>
|
||||
</PageSectionContent>
|
||||
</PageSection>
|
||||
</PageContainer>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<motion.div
|
||||
role="group"
|
||||
aria-label={label}
|
||||
initial={{ opacity: 0, x: LEVEL_OFFSET }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: LEVEL_OFFSET }}
|
||||
transition={LEVEL_TRANSITION}
|
||||
className={cn('absolute inset-0 flex flex-col', className)}
|
||||
>
|
||||
<div className="flex items-center gap-1 p-3 pb-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="tiny"
|
||||
aria-label="Back to all resources"
|
||||
onClick={onBack}
|
||||
className="size-7 shrink-0 px-0"
|
||||
icon={<ChevronLeft size={16} />}
|
||||
/>
|
||||
<span id="explorer-sidebar-search-label" className="sr-only">
|
||||
{searchPlaceholder}
|
||||
</span>
|
||||
<InnerSideBarFilters className="w-full gap-0 p-0">
|
||||
<InnerSideBarFilterSearchInput
|
||||
name="explorer-sidebar-search"
|
||||
value={search}
|
||||
placeholder={searchPlaceholder}
|
||||
aria-labelledby="explorer-sidebar-search-label"
|
||||
onChange={(event) => setSearch(event.target.value)}
|
||||
/>
|
||||
</InnerSideBarFilters>
|
||||
</div>
|
||||
{children}
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -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<typeof ProjectLayoutWithAuth> {
|
||||
children: ReactNode
|
||||
title?: string
|
||||
}
|
||||
|
||||
export const ExplorerLayout = ({ browserTitle, children, title }: ExplorerLayoutProps) => {
|
||||
const [section, setSection] = useState<ExplorerResourceType>()
|
||||
|
||||
// [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 (
|
||||
<ProjectLayoutWithAuth
|
||||
product="Explorer"
|
||||
browserTitle={mergedBrowserTitle}
|
||||
productMenu={
|
||||
<div className="relative h-full overflow-hidden">
|
||||
<AnimatePresence mode="wait">
|
||||
{section === undefined && <ExplorerNavHome key="home" onSelectSection={setSection} />}
|
||||
{section === 'notebook' && (
|
||||
<ExplorerNavNotebooks key="notebooks" onBack={() => setSection(undefined)} />
|
||||
)}
|
||||
{section === 'chat' && (
|
||||
<ExplorerNavChats key="chats" onBack={() => setSection(undefined)} />
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</ProjectLayoutWithAuth>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<ExplorerNavResourceWrapper type="chat" search={search} setSearch={setSearch} onBack={onBack}>
|
||||
<div className="flex flex-1 flex-col gap-px overflow-y-auto px-3 pb-3">
|
||||
<p className="px-2 py-2 text-xs text-foreground-lighter">No chats created yet</p>
|
||||
</div>
|
||||
</ExplorerNavResourceWrapper>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<motion.div
|
||||
key="root"
|
||||
role="group"
|
||||
aria-label="All resources"
|
||||
initial={{ opacity: 0, x: -LEVEL_OFFSET }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -LEVEL_OFFSET }}
|
||||
transition={LEVEL_TRANSITION}
|
||||
className="absolute inset-0 flex flex-col gap-4 overflow-y-auto p-3"
|
||||
>
|
||||
<nav className="flex flex-col gap-px">
|
||||
{EXPLORER_SECTIONS.map(({ type, label, icon: Icon }) => {
|
||||
return (
|
||||
<button
|
||||
key={type}
|
||||
type="button"
|
||||
tabIndex={0}
|
||||
className={rowClassName(false)}
|
||||
onClick={() => onSelectSection(type)}
|
||||
>
|
||||
<Icon size={14} className="shrink-0" />
|
||||
<span className="flex-1 text-left">{label}</span>
|
||||
<span className="text-xs text-foreground-lighter">{/* Length will be here */}</span>
|
||||
<ChevronRight size={14} className="shrink-0 text-foreground-muted" />
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<section className="flex flex-col gap-px">
|
||||
<h3 className="mb-2 px-3 font-mono text-sm font-normal uppercase text-foreground-lighter">
|
||||
Recent
|
||||
</h3>
|
||||
<p className="px-3 text-xs text-foreground-lighter">Nothing edited yet</p>
|
||||
</section>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<ExplorerNavResourceWrapper
|
||||
type="notebook"
|
||||
search={search}
|
||||
setSearch={setSearch}
|
||||
onBack={onBack}
|
||||
>
|
||||
<div className="flex flex-1 flex-col gap-px overflow-y-auto px-3 pb-3">
|
||||
<p className="px-2 py-2 text-xs text-foreground-lighter">No notebooks created yet</p>
|
||||
</div>
|
||||
</ExplorerNavResourceWrapper>
|
||||
)
|
||||
}
|
||||
@@ -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<ProductMenuBarProps>) => {
|
||||
export const ProductMenuBar = ({
|
||||
title,
|
||||
children,
|
||||
className,
|
||||
}: PropsWithChildren<ProductMenuBarProps>) => {
|
||||
// [Joshen] Temporary entry point into explorer
|
||||
const { ref } = useParams()
|
||||
const isExplorerEnabled = useFlag('explorer')
|
||||
const showExplorerCTA = isExplorerEnabled && title === 'SQL Editor'
|
||||
|
||||
return (
|
||||
<div
|
||||
/**
|
||||
@@ -18,12 +29,15 @@ const ProductMenuBar = ({ title, children, className }: PropsWithChildren<Produc
|
||||
'hide-scrollbar bg-dash-sidebar border-default'
|
||||
)}
|
||||
>
|
||||
<div className="border-default flex min-h-(--header-height) items-center border-b px-6">
|
||||
<div className="border-default flex min-h-(--header-height) items-center border-b px-6 justify-between">
|
||||
<h4 className="text-lg">{title}</h4>
|
||||
{showExplorerCTA && (
|
||||
<Button asChild variant="default">
|
||||
<Link href={`/project/${ref}/explorer`}>Explorer</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className={cn('grow overflow-y-auto', className)}>{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProductMenuBar
|
||||
@@ -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 }))
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 <ExplorerPage />
|
||||
}
|
||||
|
||||
ProjectExplorerPage.getLayout = (page) => (
|
||||
<DefaultLayout>
|
||||
<ExplorerLayout>{page}</ExplorerLayout>
|
||||
</DefaultLayout>
|
||||
)
|
||||
|
||||
export default ProjectExplorerPage
|
||||
@@ -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,
|
||||
|
||||
@@ -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 (
|
||||
<ExplorerLayout>
|
||||
<ExplorerPage />
|
||||
</ExplorerLayout>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user