mirror of
https://github.com/supabase/supabase.git
synced 2026-10-14 05:39:11 +03:00
connect
This commit is contained in:
1 parent
0199a270e0
commit
d3c2e144ea
5 files changed
+261
-31
No files matched your search
@@ -1,8 +1,8 @@
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { useParams } from 'common'
|
||||
import { ExternalLink, Plug } from 'lucide-react'
|
||||
import { parseAsBoolean, useQueryState } from 'nuqs'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { parseAsBoolean, parseAsString, useQueryState } from 'nuqs'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import { DatabaseConnectionString } from 'components/interfaces/Connect/DatabaseConnectionString'
|
||||
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
|
||||
@@ -55,6 +55,16 @@ export const Connect = () => {
|
||||
parseAsBoolean.withDefault(false)
|
||||
)
|
||||
|
||||
const [connectTab, setConnectTab] = useQueryState(
|
||||
'connectTab',
|
||||
parseAsString.withDefault('direct').withOptions({ clearOnDefault: true })
|
||||
)
|
||||
|
||||
const [prefFramework] = useQueryState(
|
||||
'framework',
|
||||
parseAsString.withDefault('').withOptions({ clearOnDefault: true })
|
||||
)
|
||||
|
||||
const [connectionObject, setConnectionObject] = useState<ConnectionType[]>(frameworks)
|
||||
const [selectedParent, setSelectedParent] = useState(connectionObject[0].key) // aka nextjs
|
||||
const [selectedChild, setSelectedChild] = useState(
|
||||
@@ -185,6 +195,33 @@ export const Connect = () => {
|
||||
selectedGrandchild,
|
||||
})
|
||||
|
||||
// Sync initial tab and framework from query params
|
||||
useEffect(() => {
|
||||
if (connectTab === 'frameworks') {
|
||||
setConnectionObject(frameworks)
|
||||
handleConnectionTypeChange(frameworks)
|
||||
if (prefFramework) {
|
||||
const exists = frameworks.find((f) => f.key === prefFramework)
|
||||
if (exists) {
|
||||
setSelectedParent(prefFramework)
|
||||
// Reset children/grandchildren based on the new parent
|
||||
const firstChild = exists.children?.[0]?.key ?? ''
|
||||
setSelectedChild(firstChild)
|
||||
const firstGrand = exists.children?.[0]?.children?.[0]?.key ?? ''
|
||||
setSelectedGrandchild(firstGrand)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (connectTab === 'mobiles') {
|
||||
setConnectionObject(MOBILES)
|
||||
handleConnectionTypeChange(MOBILES)
|
||||
}
|
||||
if (connectTab === 'orms') {
|
||||
setConnectionObject(ORMS)
|
||||
handleConnectionTypeChange(ORMS)
|
||||
}
|
||||
}, [connectTab, prefFramework])
|
||||
|
||||
if (!isActiveHealthy) {
|
||||
return (
|
||||
<ButtonTooltip
|
||||
@@ -219,7 +256,13 @@ export const Connect = () => {
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Tabs_Shadcn_ defaultValue="direct" onValueChange={(value) => handleConnectionType(value)}>
|
||||
<Tabs_Shadcn_
|
||||
value={connectTab}
|
||||
onValueChange={(value) => {
|
||||
handleConnectionType(value)
|
||||
setConnectTab(value)
|
||||
}}
|
||||
>
|
||||
<TabsList_Shadcn_ className={cn('flex overflow-x-scroll gap-x-4', DIALOG_PADDING_X)}>
|
||||
{connectionTypes.map((type) => (
|
||||
<TabsTrigger_Shadcn_ key={type.key} value={type.key} className="px-0">
|
||||
|
||||
@@ -154,7 +154,7 @@ export const AdvisorWidget = () => {
|
||||
}}
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-16 flex flex-col justify-end flex-1 overflow-auto">
|
||||
<CardContent className="p-6 pt-16 flex flex-col justify-end flex-1 overflow-auto">
|
||||
{lint.detail ? getFirstSentence(lint.detail) : lint.title}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
Button,
|
||||
CommandEmpty_Shadcn_,
|
||||
CommandGroup_Shadcn_,
|
||||
CommandInput_Shadcn_,
|
||||
CommandItem_Shadcn_,
|
||||
CommandList_Shadcn_,
|
||||
Command_Shadcn_,
|
||||
PopoverContent_Shadcn_,
|
||||
PopoverTrigger_Shadcn_,
|
||||
Popover_Shadcn_,
|
||||
cn,
|
||||
} from 'ui'
|
||||
import { Box, Check, ChevronDown } from 'lucide-react'
|
||||
import { ConnectionType } from 'components/interfaces/Connect/Connect.constants'
|
||||
import { ConnectionIcon } from 'components/interfaces/Connect/ConnectionIcon'
|
||||
|
||||
interface FrameworkSelectorProps {
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
items: ConnectionType[]
|
||||
label?: string
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const FrameworkSelector = ({
|
||||
value,
|
||||
onChange,
|
||||
items,
|
||||
label = 'Framework',
|
||||
className,
|
||||
}: FrameworkSelectorProps) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const selectedItem = items.find((item) => item.key === value)
|
||||
|
||||
function handleSelect(key: string) {
|
||||
onChange(key)
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover_Shadcn_ open={open} onOpenChange={setOpen} modal={false}>
|
||||
<div className={cn('flex', className)}>
|
||||
<PopoverTrigger_Shadcn_ asChild>
|
||||
<Button
|
||||
size="tiny"
|
||||
type="default"
|
||||
className="gap-0 rounded-l-none"
|
||||
iconRight={<ChevronDown strokeWidth={1.5} />}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{selectedItem?.icon ? <ConnectionIcon icon={selectedItem.icon} /> : <Box size={12} />}
|
||||
{selectedItem?.label}
|
||||
</div>
|
||||
</Button>
|
||||
</PopoverTrigger_Shadcn_>
|
||||
</div>
|
||||
<PopoverContent_Shadcn_ className="p-0 max-w-48" side="bottom" align="start">
|
||||
<Command_Shadcn_>
|
||||
<CommandInput_Shadcn_ placeholder="Search..." />
|
||||
<CommandList_Shadcn_>
|
||||
<CommandEmpty_Shadcn_>No results found.</CommandEmpty_Shadcn_>
|
||||
<CommandGroup_Shadcn_>
|
||||
{items.map((item) => (
|
||||
<CommandItem_Shadcn_
|
||||
key={item.key}
|
||||
value={item.key}
|
||||
onSelect={() => handleSelect(item.key)}
|
||||
className="flex gap-2 items-center"
|
||||
>
|
||||
{item.icon ? <ConnectionIcon icon={item.icon} /> : <Box size={12} />}
|
||||
{item.label}
|
||||
<Check
|
||||
size={15}
|
||||
className={cn('ml-auto', item.key === value ? 'opacity-100' : 'opacity-0')}
|
||||
/>
|
||||
</CommandItem_Shadcn_>
|
||||
))}
|
||||
</CommandGroup_Shadcn_>
|
||||
</CommandList_Shadcn_>
|
||||
</Command_Shadcn_>
|
||||
</PopoverContent_Shadcn_>
|
||||
</Popover_Shadcn_>
|
||||
)
|
||||
}
|
||||
|
||||
export default FrameworkSelector
|
||||
@@ -9,6 +9,7 @@ export type GettingStartedAction = {
|
||||
onClick?: () => void
|
||||
variant?: React.ComponentProps<typeof Button>['type']
|
||||
icon?: React.ReactNode
|
||||
component?: React.ReactNode
|
||||
}
|
||||
|
||||
export type GettingStartedStep = {
|
||||
@@ -17,6 +18,7 @@ export type GettingStartedStep = {
|
||||
icon?: React.ReactNode
|
||||
title: string
|
||||
description: string
|
||||
image?: React.ReactNode
|
||||
actions: GettingStartedAction[]
|
||||
}
|
||||
|
||||
@@ -36,7 +38,7 @@ export function GettingStarted({ steps, className }: GettingStartedProps) {
|
||||
</div>
|
||||
<Row columns={[3, 2, 1]} className="items-stretch">
|
||||
{steps.map((step) => (
|
||||
<Card key={step.key} className="overflow-hidden h-full">
|
||||
<Card key={step.key} className="overflow-hidden h-full flex flex-col">
|
||||
<CardHeader className="flex flex-row space-y-0 justify-between items-center border-b-0">
|
||||
<div className="flex flex-row items-center gap-3">
|
||||
{step.icon && <div>{step.icon}</div>}
|
||||
@@ -49,10 +51,14 @@ export function GettingStarted({ steps, className }: GettingStartedProps) {
|
||||
{step.status}
|
||||
</Badge>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 pt-16">
|
||||
<CardContent className="p-6 pt-16 flex-1 flex flex-col justify-end">
|
||||
{step.image && <div className="w-full">{step.image}</div>}
|
||||
<p className="text-base text-foreground">{step.description}</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<div className="flex flex-wrap gap-2 mt-4">
|
||||
{step.actions.map((action, i) => {
|
||||
if (action.component) {
|
||||
return <span key={`${step.key}-action-${i}`}>{action.component}</span>
|
||||
}
|
||||
const content = (
|
||||
<Button
|
||||
key={`${step.key}-action-${i}`}
|
||||
|
||||
@@ -2,11 +2,22 @@ import { useParams } from 'common'
|
||||
import { useTablesQuery } from 'data/tables/tables-query'
|
||||
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
|
||||
import GettingStarted, { GettingStartedStep } from './GettingStarted'
|
||||
import { Code, Database, File, Table, User } from 'lucide-react'
|
||||
import { Code, Database, File, Table, User, Upload, UserPlus, BarChart3 } from 'lucide-react'
|
||||
import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useState } from 'react'
|
||||
import FrameworkSelector from './FrameworkSelector'
|
||||
import { FRAMEWORKS } from 'components/interfaces/Connect/Connect.constants'
|
||||
import { AiIconAnimation } from 'ui'
|
||||
|
||||
export default function GettingStartedSection() {
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
const { ref } = useParams()
|
||||
const aiSnap = useAiAssistantStateSnapshot()
|
||||
const router = useRouter()
|
||||
|
||||
// Local state for framework selector preview
|
||||
const [selectedFramework, setSelectedFramework] = useState<string>(FRAMEWORKS[0]?.key ?? 'nextjs')
|
||||
|
||||
const { data: tablesData } = useTablesQuery({
|
||||
projectRef: project?.ref,
|
||||
@@ -25,10 +36,21 @@ export default function GettingStartedSection() {
|
||||
title: 'Design your database',
|
||||
icon: <Database strokeWidth={1} className="text-foreground-muted" size={16} />,
|
||||
description:
|
||||
'Start building your app by creating tables with the table editor, sql editor or describe what you want with the assistant',
|
||||
'Start building your app by creating tables with the table editor, SQL editor, or describe what you want with the assistant.',
|
||||
actions: [
|
||||
{ label: 'View tables', href: `/project/${ref}/editor`, variant: 'default' },
|
||||
{ label: 'Do it for me', href: `/project/${ref}/sql/templates`, variant: 'default' },
|
||||
{
|
||||
label: 'Do it for me',
|
||||
variant: 'default',
|
||||
icon: <AiIconAnimation size={14} />,
|
||||
onClick: () =>
|
||||
aiSnap.newChat({
|
||||
name: 'Design my database',
|
||||
open: true,
|
||||
initialInput:
|
||||
'I want to design my database schema. Please propose tables, relationships, and SQL to create them for my app. Ask clarifying questions if needed.',
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -37,32 +59,31 @@ export default function GettingStartedSection() {
|
||||
title: 'Add some data',
|
||||
icon: <Table strokeWidth={1} className="text-foreground-muted" size={16} />,
|
||||
description:
|
||||
'Use the table editor to create some sample data, or ask the Assistant to generate some data for you.',
|
||||
'Use the table editor to create sample data, or ask the Assistant to generate it for you.',
|
||||
actions: [
|
||||
{ label: 'Table editor', href: `/project/${ref}/editor`, variant: 'default' },
|
||||
{ label: 'Run SQL', href: `/project/${ref}/sql/new`, variant: 'default' },
|
||||
{
|
||||
label: 'Do it for me',
|
||||
variant: 'default',
|
||||
icon: <AiIconAnimation size={14} />,
|
||||
onClick: () =>
|
||||
aiSnap.newChat({
|
||||
name: 'Generate sample data',
|
||||
open: true,
|
||||
initialInput:
|
||||
'Generate SQL INSERT statements to add realistic sample data to my existing tables. Use safe defaults and avoid overwriting data.',
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'implement-auth',
|
||||
key: 'setup-auth',
|
||||
status: 'incomplete',
|
||||
title: 'Implement auth',
|
||||
title: 'Set up authentication',
|
||||
icon: <User strokeWidth={1} className="text-foreground-muted" size={16} />,
|
||||
description: 'Set up authentication and manage users using Supabase Auth.',
|
||||
description: 'Configure authentication providers and manage settings for Supabase Auth.',
|
||||
actions: [
|
||||
{ label: 'Users', href: `/project/${ref}/auth/users`, variant: 'default' },
|
||||
{ label: 'Auth settings', href: `/project/${ref}/auth/settings`, variant: 'default' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'implement-storage',
|
||||
status: 'incomplete',
|
||||
title: 'Implement storage',
|
||||
icon: <File strokeWidth={1} className="text-foreground-muted" size={16} />,
|
||||
description: 'Create buckets and manage files with Supabase Storage.',
|
||||
actions: [
|
||||
{ label: 'Buckets', href: `/project/${ref}/storage/buckets`, variant: 'default' },
|
||||
{ label: 'Policies', href: `/project/${ref}/storage/policies`, variant: 'default' },
|
||||
{ label: 'Configure', href: `/project/${ref}/auth/providers`, variant: 'default' },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -71,12 +92,83 @@ export default function GettingStartedSection() {
|
||||
title: 'Connect your app',
|
||||
icon: <Code strokeWidth={1} className="text-foreground-muted" size={16} />,
|
||||
description:
|
||||
'Install a client library and connect to your Supabase project to start querying your data',
|
||||
'Install a client library and connect to your Supabase project to start querying your data.',
|
||||
actions: [
|
||||
{ label: 'Client libraries', href: `/project/${ref}/settings/api`, variant: 'default' },
|
||||
{ label: 'Quickstarts', href: `/project/${ref}/sql/quickstarts`, variant: 'default' },
|
||||
{
|
||||
label: 'Framework selector',
|
||||
component: (
|
||||
<FrameworkSelector
|
||||
value={selectedFramework}
|
||||
onChange={setSelectedFramework}
|
||||
items={FRAMEWORKS}
|
||||
label="Framework"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Connect',
|
||||
variant: 'default',
|
||||
onClick: () =>
|
||||
router.push(
|
||||
{
|
||||
pathname: router.pathname,
|
||||
query: {
|
||||
...router.query,
|
||||
showConnect: true,
|
||||
connectTab: 'frameworks',
|
||||
framework: selectedFramework,
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
{ shallow: true }
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'signup-first-user',
|
||||
status: 'incomplete',
|
||||
title: 'Sign up your first user',
|
||||
icon: <UserPlus strokeWidth={1} className="text-foreground-muted" size={16} />,
|
||||
description: 'Create your first user account to test authentication flows.',
|
||||
actions: [
|
||||
{
|
||||
label: 'Read docs',
|
||||
href: 'https://supabase.com/docs/guides/auth',
|
||||
variant: 'default',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'upload-file',
|
||||
status: 'incomplete',
|
||||
title: 'Upload a file',
|
||||
icon: <Upload strokeWidth={1} className="text-foreground-muted" size={16} />,
|
||||
description: 'Create a bucket and upload your first file with Supabase Storage.',
|
||||
actions: [
|
||||
{ label: 'Buckets', href: `/project/${ref}/storage/buckets`, variant: 'default' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'create-edge-function',
|
||||
status: 'incomplete',
|
||||
title: 'Create your first edge function',
|
||||
icon: <Code strokeWidth={1} className="text-foreground-muted" size={16} />,
|
||||
description:
|
||||
'Build and deploy an edge function to run server-side logic close to your users.',
|
||||
actions: [
|
||||
{ label: 'Create function', href: `/project/${ref}/functions/new`, variant: 'default' },
|
||||
{ label: 'View functions', href: `/project/${ref}/functions`, variant: 'default' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'monitor-progress',
|
||||
status: 'incomplete',
|
||||
title: 'Monitor your progress',
|
||||
icon: <BarChart3 strokeWidth={1} className="text-foreground-muted" size={16} />,
|
||||
description: 'Create custom reports to track API, database, storage, and auth activity.',
|
||||
actions: [{ label: 'Reports', href: `/project/${ref}/reports`, variant: 'default' }],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user