preview tab works now

This commit is contained in:
Jonathan Summers-Muir committed 2024-11-14 00:01:46 +08:00
1 parent 7ecd0ff90d
commit 6aafe41eee
5 files changed
+529 -91

No files matched your search

@@ -22,10 +22,10 @@ export const generateToolRoutes = (ref?: string, project?: Project): Route[] =>
return [
{
key: 'editor',
key: 'explorer',
label: 'Explorer',
icon: <PackageSearch size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />,
link: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/editor`),
link: ref && (isProjectBuilding ? buildingUrl : `/project/${ref}/explorer`),
linkElement: <EditorIndexPageLink projectRef={ref} />,
},
// {
@@ -68,9 +68,8 @@ export const EmptyStatePanel = ({
)}
{aiButtonText && (
<Button
type="default"
icon={<Sparkles className="w-4 h-4 text-white" />}
className="!bg-[#6366f1] hover:!bg-[#6366f1]/90 text-white border border-[#6366f1]"
type="primary"
icon={<Sparkles className="w-4 h-4" />}
onClick={onAiButtonClick}
>
{aiButtonText}
@@ -0,0 +1,102 @@
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { useTablesQuery } from 'data/tables/tables-query'
import { useSchemasQuery } from './schemas-query'
/**
* Hook to determine if Postgres is being used in a project.
* Postgres is considered "in use" if either:
* 1. Custom schemas exist (schemas other than 'public')
* 2. The public schema contains tables or views
*/
const DEFAULT_SCHEMAS = [
'public',
'graphql',
'graphql_public',
'vault',
'pgsodium_masks',
'pgsodium',
'auth',
'storage',
'realtime',
'extensions',
'pgbouncer',
]
export const usePostgresInUse = () => {
const { project } = useProjectContext()
// Query for all schemas in the project
const {
data: schemas,
isLoading: isLoadingSchemas,
isError: isSchemasError,
error: schemasError,
isSuccess: isSchemasSuccess,
refetch: refetchSchemas,
} = useSchemasQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
// Query for tables specifically in the public schema
const {
data: tables,
isLoading: isLoadingTables,
isError: isTablesError,
error: tablesError,
isSuccess: isTablesSuccess,
refetch: refetchTables,
} = useTablesQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
schema: 'public',
})
console.log('Schemas:', schemas)
console.log('Tables:', tables)
// Debug what we're getting
console.log('Tables query result:', {
isLoadingTables,
tableCount: tables?.length,
firstFewTables: tables?.slice(0, 3),
})
// Check if any schemas exist other than 'public'
const hasCustomSchemas = (schemas ?? []).some((schema) => !DEFAULT_SCHEMAS.includes(schema.name))
// Check if the public schema has any tables or views
const hasPublicSchemaObjects = (tables ?? []).length > 0
return {
// Computed values indicating Postgres usage
isInUse: hasCustomSchemas || hasPublicSchemaObjects,
hasCustomSchemas,
hasPublicSchemaObjects,
// Combined states from both queries
isLoading: isLoadingSchemas || isLoadingTables,
isError: isSchemasError || isTablesError,
error: schemasError || tablesError,
isSuccess: isSchemasSuccess && isTablesSuccess,
// Individual query states
schemas: {
data: schemas,
isLoading: isLoadingSchemas,
isError: isSchemasError,
error: schemasError,
isSuccess: isSchemasSuccess,
refetch: refetchSchemas,
},
tables: {
data: tables,
isLoading: isLoadingTables,
isError: isTablesError,
error: tablesError,
isSuccess: isTablesSuccess,
refetch: refetchTables,
},
}
}
@@ -0,0 +1,150 @@
import { EmptyStatePanel } from 'components/layouts/explorer/empty-state-panel'
import { ExplorerLayout } from 'components/layouts/explorer/layout'
import { ProjectContextFromParamsProvider } from 'components/layouts/ProjectLayout/ProjectContext'
import { Sparkles } from 'lucide-react'
import type { NextPageWithLayout } from 'types'
import { Separator } from 'ui'
import { motion, Variants } from 'framer-motion'
const ExplorerPage: NextPageWithLayout = () => {
const EXPLORER_PANELS = [
{
title: `Create a table`,
description: `Write and execute SQL queries in an interactive editor with syntax highlighting and autocompletion.`,
buttonText: 'Open SQL Editor',
aiButtonText: 'Generate SQL',
},
{
title: `Write SQL`,
description: `Browse and manage your database with an intuitive interface. Create and modify tables, manage relationships, and edit data directly.`,
buttonText: 'Open Table Editor',
aiButtonText: 'Generate Schema',
},
]
const containerVariants: Variants = {
hidden: { opacity: 0, y: 20 },
show: {
opacity: 1,
y: 0,
transition: {
type: 'spring',
stiffness: 600,
damping: 50,
staggerChildren: 0.08,
delayChildren: 0.03,
},
},
}
const itemVariants: Variants = {
hidden: { opacity: 0, scale: 0.96, y: 15 },
show: {
opacity: 1,
scale: 1,
y: 0,
transition: {
type: 'spring',
stiffness: 600,
damping: 50,
staggerChildren: 0.08,
delayChildren: 0.03,
},
},
}
return (
<>
<motion.div
className="flex flex-col items-center justify-center h-full w-full max-w-3xl mx-auto px-5 gap-12"
initial="hidden"
animate="show"
variants={containerVariants}
>
<motion.div
className="w-full bg-surface-200 dark:bg-surface-100 rounded-lg overflow-hidden relative border border-muted"
variants={itemVariants}
>
<div className="absolute inset-0 bg-gradient-to-r from-background-surface-200 via-background-surface-200 to-transparent"></div>
{[...Array(5)].map((_, index) => (
<div
key={index}
className="absolute inset-y-0 right-0 max-h-[300px]"
style={{
width: '70%',
background: `linear-gradient(135deg,
rgba(229, 231, 235, ${0.3 - index * 0.05}) 0%,
rgba(209, 213, 219, ${0.3 - index * 0.05}) 50%,
transparent 100%)`,
transform: `skew(-12deg) translateX(${index * 15}%)`,
maskImage: 'linear-gradient(to bottom, black 0%, black 50%, transparent 100%)',
WebkitMaskImage: 'linear-gradient(to bottom, black 0%, black 40%, transparent 85%)',
boxShadow: `-1px 0 2px rgba(0, 0, 0, 0.03)`,
}}
></div>
))}
<div className="relative z-10 p-8 text-gray-700 h-full flex flex-col gap-5">
<div className="flex flex-col gap-5">
<div className="flex items-center gap-4">
<div className="inline-flex items-center justify-center w-8 h-8 rounded-lg bg-[#6366f1]/10">
<Sparkles size={14} className="text-[#6366f1]" />
</div>
<h2 className="text-base text-foreground">AI Assistant</h2>
</div>
<p className="text-sm text-foreground-light max-w-md">
This panel features geometric shapes on the right side, each with a distinct shade
and reduced opacity, creating a subtle fade-out effect.
</p>
</div>
<p className="text-xs text-foreground-lighter max-w-md">
The shapes now have a more delicate gradation, transitioning smoothly from slightly
darker to lighter and more transparent.
</p>
</div>
</motion.div>
<Separator />
<motion.div className="flex gap-16" variants={itemVariants}>
{EXPLORER_PANELS.map((data, index) => (
<motion.div
key={data.title}
variants={itemVariants}
transition={{ delay: 0.2 + index * 0.1 }}
>
<EmptyStatePanel {...data} />
</motion.div>
))}
</motion.div>
<motion.div
className="w-full rounded-lg overflow-hidden relative border border-muted"
variants={itemVariants}
>
<div className="absolute inset-0 bg-gradient-to-r from-background-surface-200 via-background-surface-200 to-transparent"></div>
<div className="relative z-10 p-8 text-gray-700 h-full flex flex-col gap-5">
<div className="flex flex-col gap-5">
<div className="flex items-center gap-4">
<div className="inline-flex items-center justify-center w-8 h-8 rounded-lg bg-[#6366f1]/10">
<Sparkles size={14} className="text-[#6366f1]" />
</div>
<h2 className="text-base text-foreground">Migrate to Supabase</h2>
</div>
<p className="text-sm text-foreground-light max-w-md">
This panel features geometric shapes on the right side, each with a distinct shade
and reduced opacity, creating a subtle fade-out effect.
</p>
</div>
</div>
</motion.div>
</motion.div>
</>
)
}
ExplorerPage.getLayout = (page) => (
<ProjectContextFromParamsProvider>
<ExplorerLayout hideTabs>{page}</ExplorerLayout>
</ProjectContextFromParamsProvider>
)
export default ExplorerPage
@@ -1,27 +1,93 @@
import { EmptyStatePanel } from 'components/layouts/explorer/empty-state-panel'
import { ExplorerLayout } from 'components/layouts/explorer/layout'
import { ProjectContextFromParamsProvider } from 'components/layouts/ProjectLayout/ProjectContext'
import { Sparkles } from 'lucide-react'
import { Sparkles, Box, X, Move, BaggageClaim, BookOpen } from 'lucide-react'
import type { NextPageWithLayout } from 'types'
import { Separator } from 'ui'
import { motion, Variants } from 'framer-motion'
import { Button, Separator, Skeleton } from 'ui'
import { motion, Variants, LayoutGroup, AnimatePresence } from 'framer-motion'
import { useEffect, useState, useRef } from 'react'
import { AssistantChatForm } from 'ui-patterns/AssistantChat'
import { usePostgresInUse } from 'data/database/postgres-in-use-query'
interface AssistantChatDemoProps {
value: string
setValueState: (value: string) => void
onSubmit: () => void
}
function AssistantChatDemo({ value, setValueState, onSubmit }: AssistantChatDemoProps) {
const [loading, setLoading] = useState(false)
const inputRef = useRef<HTMLTextAreaElement>(null)
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus()
}
}, [value])
return (
<AssistantChatForm
textAreaRef={inputRef}
placeholder="Ask me anything..."
icon={<Box strokeWidth={1.5} size={24} className="text-foreground-muted" />}
value={value}
loading={loading}
disabled={loading}
onValueChange={(e) => setValueState(e.target.value)}
onSubmit={async (event) => {
event.preventDefault()
setLoading(true)
setTimeout(() => {
setLoading(false)
onSubmit()
}, 1500)
}}
/>
)
}
const ExplorerPage: NextPageWithLayout = () => {
const EXPLORER_PANELS = [
const [isLoading, setIsLoading] = useState(true)
const [input, setInput] = useState('')
const [showRightPanel, setShowRightPanel] = useState(false)
const inputRef = useRef<HTMLTextAreaElement>(null)
const { isInUse: isPostgresInUse, isLoading: isLoadingCheckingPostgres } = usePostgresInUse()
// Panels to show when Postgres is not in use yet
const EMPTY_POSTGRES_PANELS = [
{
title: `Create a table`,
description: `Write and execute SQL queries in an interactive editor with syntax highlighting and autocompletion.`,
buttonText: 'Open SQL Editor',
aiButtonText: 'Generate SQL',
title: `Create your first table`,
description: `Create tables in your Postgres database to store and organize your data.`,
buttonText: 'Create Table',
aiButtonText: 'Ask AI',
},
{
title: `Write SQL`,
description: `Browse and manage your database with an intuitive interface. Create and modify tables, manage relationships, and edit data directly.`,
buttonText: 'Open Table Editor',
aiButtonText: 'Generate Schema',
title: `Write your first SQL query`,
description: `Write SQL to execute on your Postgres database.`,
buttonText: 'Create your first Query',
aiButtonText: 'Ask AI',
},
]
// Panels to show when Postgres is already in use
const POSTGRES_IN_USE_PANELS = [
{
title: 'Query your data',
description: 'Write and execute SQL queries to explore and analyze your Postgres database.',
buttonText: 'Create query',
aiButtonText: 'Ask AI',
},
{
title: 'Create table',
description: 'Add new tables to your database schema.',
buttonText: 'Open Table Editor',
aiButtonText: 'Ask AI',
},
]
const panels = isPostgresInUse ? POSTGRES_IN_USE_PANELS : EMPTY_POSTGRES_PANELS
const containerVariants: Variants = {
hidden: { opacity: 0, y: 20 },
show: {
@@ -53,91 +119,212 @@ const ExplorerPage: NextPageWithLayout = () => {
},
}
useEffect(() => {
const timer = setTimeout(() => setIsLoading(false), 800)
return () => clearTimeout(timer)
}, [])
return (
<>
<LayoutGroup>
<motion.div
className="flex flex-col items-center justify-center h-full w-full max-w-3xl mx-auto px-5 gap-12"
className="flex h-full w-full"
initial="hidden"
animate="show"
variants={containerVariants}
>
<motion.div
className="w-full bg-surface-200 dark:bg-surface-100 rounded-lg overflow-hidden relative border border-muted"
variants={itemVariants}
>
<div className="absolute inset-0 bg-gradient-to-r from-background-surface-200 via-background-surface-200 to-transparent"></div>
{[...Array(5)].map((_, index) => (
<div
key={index}
className="absolute inset-y-0 right-0 max-h-[300px]"
style={{
width: '70%',
background: `linear-gradient(135deg,
rgba(229, 231, 235, ${0.3 - index * 0.05}) 0%,
rgba(209, 213, 219, ${0.3 - index * 0.05}) 50%,
transparent 100%)`,
transform: `skew(-12deg) translateX(${index * 15}%)`,
maskImage: 'linear-gradient(to bottom, black 0%, black 50%, transparent 100%)',
WebkitMaskImage: 'linear-gradient(to bottom, black 0%, black 40%, transparent 85%)',
boxShadow: `-1px 0 2px rgba(0, 0, 0, 0.03)`,
}}
></div>
))}
<div className="relative z-10 p-8 text-gray-700 h-full flex flex-col gap-5">
<div className="flex flex-col gap-5">
<div className="flex items-center gap-4">
<div className="inline-flex items-center justify-center w-8 h-8 rounded-lg bg-[#6366f1]/10">
<Sparkles size={14} className="text-[#6366f1]" />
</div>
<h2 className="text-base text-foreground">AI Assistant</h2>
</div>
<p className="text-sm text-foreground-light max-w-md">
This panel features geometric shapes on the right side, each with a distinct shade
and reduced opacity, creating a subtle fade-out effect.
</p>
</div>
<p className="text-xs text-foreground-lighter max-w-md">
The shapes now have a more delicate gradation, transitioning smoothly from slightly
darker to lighter and more transparent.
</p>
</div>
</motion.div>
<Separator />
<motion.div className="flex gap-16" variants={itemVariants}>
{EXPLORER_PANELS.map((data, index) => (
<motion.div
key={data.title}
variants={itemVariants}
transition={{ delay: 0.2 + index * 0.1 }}
>
<EmptyStatePanel {...data} />
<motion.div className="flex-1 flex flex-col items-center justify-center max-w-3xl mx-auto px-5 gap-12">
<motion.div className="grid grid-cols-2 gap-16 w-full" variants={itemVariants}>
{isLoadingCheckingPostgres ? (
<>
{[...Array(2)].map((_, i) => (
<motion.div
key={i}
variants={itemVariants}
transition={{ delay: 0.2 + i * 0.1 }}
className="w-full flex flex-col gap-4"
>
<div className="flex items-center gap-4">
<Skeleton className="w-8 h-8 rounded-lg" />
<Skeleton className="h-4 w-24 rounded" />
</div>
<div className="space-y-2">
<Skeleton className="h-3 w-full rounded" />
<Skeleton className="h-3 w-3/4 rounded" />
</div>
<div className="flex gap-2 mt-auto">
<Skeleton className="h-8 w-24 rounded" />
<Skeleton className="h-8 w-32 rounded" />
</div>
</motion.div>
))}
</>
) : (
panels.map((data, index) => (
<motion.div
key={data.title}
variants={itemVariants}
transition={{ delay: 0.2 + index * 0.1 }}
>
<EmptyStatePanel {...data} />
</motion.div>
))
)}
</motion.div>
{/* Migration section - only show if Postgres is not in use */}
{isLoadingCheckingPostgres ? (
<motion.div className="w-full" variants={itemVariants}>
<Skeleton className="w-full h-[160px] rounded-lg" />
</motion.div>
))}
) : (
!isPostgresInUse && (
<motion.div
className="w-full rounded-lg overflow-hidden relative border border-muted"
variants={itemVariants}
>
<div className="absolute inset-0 bg-gradient-to-r from-background-surface-200 via-background-surface-200 to-transparent"></div>
<div className="relative z-10 px-5 py-4 h-full flex gap-5 items-center justify-between">
<div className="flex flex-col gap-3">
<div className="flex gap-4">
<div className="inline-flex items-center justify-center w-7 h-7 rounded-lg bg-surface-300 border">
<BaggageClaim
size={14}
className="text-foreground-light"
strokeWidth={1.5}
/>
</div>
<div className="flex flex-col gap-1">
<h2 className="text-base text-foreground">
Migrate your Database to Postgres
</h2>
<p className="text-xs text-foreground-light max-w-md">
Already have a database? Migrate any database to Supabase to access
backups, RESTful auto APIs, Authentication and more.
</p>
</div>
</div>
</div>
<Button type="default" className="rounded-full" iconRight={<BookOpen />}>
Migrate to Postgres
</Button>
</div>
</motion.div>
)
)}
{!showRightPanel && <Separator />}
<AnimatePresence>
{!showRightPanel && (
<motion.div
variants={itemVariants}
className="w-full"
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.2 }}
>
<motion.div className="flex flex-col gap-3">
<motion.p
className="text-sm text-foreground-light"
initial={{ opacity: 0 }}
animate={{ opacity: showRightPanel ? 0 : 1 }}
transition={{ delay: 0.3 }}
>
Try these example prompts:
</motion.p>
{isLoading ? (
<>
{[...Array(4)].map((_, i) => (
<motion.div
key={i}
className="h-6 bg-foreground/10 rounded animate-pulse"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.2 * i }}
/>
))}
</>
) : (
[
'Create a table to store user profiles with basic information',
'Help me write a query to find the most active users in the last 30 days',
'Show me how to implement row level security for a multi-tenant application',
'Generate a function to calculate the distance between two geographic points',
].map((prompt, i) => (
<motion.div
key={prompt}
className="text-sm text-foreground-light hover:text-foreground cursor-pointer"
initial={{ opacity: 0, x: -20 }}
animate={{
opacity: showRightPanel ? 0 : 1,
x: showRightPanel ? -20 : 0,
}}
transition={{
delay: showRightPanel ? 0 : 0.4 + i * 0.1,
duration: 0.2,
}}
onClick={() => {
setInput(prompt)
inputRef.current?.focus()
}}
>
"{prompt}"
</motion.div>
))
)}
</motion.div>
</motion.div>
)}
</AnimatePresence>
<motion.div
variants={itemVariants}
className="w-full"
layout
layoutId="chat-input"
key="chat-input"
>
<AssistantChatDemo
value={input}
setValueState={setInput}
onSubmit={() => setShowRightPanel(true)}
/>
</motion.div>
</motion.div>
<motion.div
className="w-full rounded-lg overflow-hidden relative border border-muted"
variants={itemVariants}
>
<div className="absolute inset-0 bg-gradient-to-r from-background-surface-200 via-background-surface-200 to-transparent"></div>
<div className="relative z-10 p-8 text-gray-700 h-full flex flex-col gap-5">
<div className="flex flex-col gap-5">
<div className="flex items-center gap-4">
<div className="inline-flex items-center justify-center w-8 h-8 rounded-lg bg-[#6366f1]/10">
<Sparkles size={14} className="text-[#6366f1]" />
</div>
<h2 className="text-base text-foreground">Migrate to Supabase</h2>
</div>
<p className="text-sm text-foreground-light max-w-md">
This panel features geometric shapes on the right side, each with a distinct shade
and reduced opacity, creating a subtle fade-out effect.
</p>
{showRightPanel && (
<motion.div
className="w-[400px] border-l border-muted h-full bg-surface-100 flex flex-col"
initial={{ x: 400, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
exit={{ x: 400, opacity: 0 }}
>
<div className="flex justify-between items-center p-4 border-b border-muted">
<h3 className="text-sm font-medium">AI Assistant</h3>
<button
onClick={() => {
setShowRightPanel(false)
setInput('')
}}
className="p-1 hover:bg-surface-200 rounded-md"
>
<X size={16} className="text-foreground-light" />
</button>
</div>
</div>
</motion.div>
<div className="flex-1">{/* Chat content will go here */}</div>
<motion.div
layout
layoutId="chat-input"
key="chat-input"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="p-4 border-t border-muted"
>
<AssistantChatDemo value={input} setValueState={setInput} onSubmit={() => {}} />
</motion.div>
</motion.div>
)}
</motion.div>
</>
</LayoutGroup>
)
}