Files
supabase/apps/studio/components/interfaces/Explorer/ExplorerHomeTab.tsx
Joshen Lim ad30c04e0e Persist last visited explorer tab (#50557)
## Context

Saves the last visited explorer tab via `useDashboardHistory`, such that
landing back on `/explorer` will open the last visited page. Similar
behaviour to Table Editor and SQL Editor

Would also be useful when going between the SQL Editor and Explorer to
bring snippets over

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

* **New Features**
* Explorer now remembers and restores the last visited query, chat, or
notebook tab.
* Automatically returns to the Explorer home screen when a saved tab is
unavailable.
* Displays a loading state while the last visited tab is being restored.

* **Bug Fixes**
  * Closing deleted chat tabs now clears their saved history.

* **Tests**
* Added coverage for Explorer tab restoration, loading states, and stale
history cleanup.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-21 16:34:50 +08:00

144 lines
5.3 KiB
TypeScript

import { useParams } from 'common'
import { Loader2, NotebookText, SquareCode } from 'lucide-react'
import { useEffect, useEffectEvent, useRef, useState } from 'react'
import { cn } from 'ui'
import { isSqlStatement } from './ExplorerHomeTab.utils'
import { ExplorerOnboarding } from './ExplorerOnboarding'
import { useCreateChat, useCreateNotebook, useCreateQuery } from './hooks'
import { NOTEBOOK_TEMPLATES } from './templates'
import { useExplorerPreferences } from '@/components/interfaces/Account/Preferences/useExplorerPreferences'
import { ActionCard } from '@/components/layouts/Tabs/ActionCard'
import { CHAT_TEMPLATES } from '@/components/ui/AIAssistantPanel/AIAssistant.prompts'
import { AssistantAgentHarnessFooter } from '@/components/ui/AIAssistantPanel/AssistantAgentHarnessFooter'
import { AssistantChatForm } from '@/components/ui/AIAssistantPanel/AssistantChatForm'
export const ExplorerHomeTab = () => {
const { home, hasCompletedOnboarding, isReady } = useExplorerPreferences()
if (!isReady) return <ExplorerHomeLoading />
if (!hasCompletedOnboarding) return <ExplorerOnboarding />
if (home === 'query') return <ExplorerHomeQuery />
return <ExplorerHomeContent />
}
export const ExplorerHomeLoading = () => (
<div
role="status"
aria-label="Opening Explorer"
className="flex h-full items-center justify-center bg-surface-100"
>
<Loader2 size={18} className="animate-spin motion-reduce:animate-none text-foreground-muted" />
</div>
)
const ExplorerHomeQuery = () => {
const { ref } = useParams()
const { createQuery, projectRef } = useCreateQuery()
const openedProjectRef = useRef<string | undefined>(undefined)
const openQuery = useEffectEvent(() => {
if (!ref || ref !== projectRef || openedProjectRef.current === ref) return
openedProjectRef.current = ref
createQuery({ replace: true })
})
useEffect(() => openQuery(), [ref, projectRef])
return <ExplorerHomeLoading />
}
const ExplorerHomeContent = () => {
const { createNotebook } = useCreateNotebook()
const { createQuery } = useCreateQuery()
const { createChat } = useCreateChat()
const [value, setValue] = useState<string>('')
const isSqlQuery = isSqlStatement(value)
return (
<div className="flex flex-col h-full">
<div
className={cn(
'flex-grow min-h-0 overflow-y-auto bg-surface-100 h-full',
'flex flex-col items-center px-10'
)}
>
<div className="w-full max-w-2xl my-auto py-10">
<div className="text-center mb-6">
<h1 className="heading-section">Run SQL. Chat with your project. Create a Notebook</h1>
</div>
<AssistantChatForm
loading={false}
className="bg"
placeholder="Explore your data, check project health, create a notebook..."
value={value}
onValueChange={(e) => setValue(e.target.value)}
onSubmit={(message) => createChat({ initialMessage: message })}
secondaryAction={
isSqlQuery
? {
label: 'Run SQL',
onClick: () => createQuery({ sql: value, autoRun: true }),
}
: undefined
}
/>
<AssistantAgentHarnessFooter />
<section className="mt-6">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<ActionCard
icon={<SquareCode className="h-4 w-4 text-foreground" strokeWidth={1.5} />}
title="Run SQL"
description="Write and run an ad-hoc query"
bgColor="bg-blue-500"
onClick={() => createQuery()}
/>
<ActionCard
icon={<NotebookText className="h-4 w-4 text-foreground" strokeWidth={1.5} />}
title="Create a notebook"
description="Combine notes, queries, and results"
bgColor="bg-blue-500"
onClick={() => createNotebook()}
/>
</div>
</section>
<section className="mt-8 flex flex-col gap-y-3">
<h2 className="text-sm font-medium text-foreground">Start with a template</h2>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-3">
{NOTEBOOK_TEMPLATES.map((template) => (
<ActionCard
key={template.title}
icon={<NotebookText className="h-4 w-4 text-foreground" strokeWidth={1.5} />}
title={template.title}
description={template.description}
bgColor="bg-blue-500"
onClick={() =>
createNotebook({ name: template.title, cells: template.buildCells() })
}
/>
))}
{CHAT_TEMPLATES.map((template) => (
<ActionCard
key={template.title}
icon={<template.icon className="h-4 w-4 text-foreground" strokeWidth={1.5} />}
title={template.title}
bgColor="bg-blue-500"
onClick={() =>
createChat({ name: template.title, initialMessage: template.initialMessage })
}
/>
))}
</div>
</section>
</div>
</div>
</div>
)
}