diff --git a/apps/studio/components/grid/SupabaseGrid.tsx b/apps/studio/components/grid/SupabaseGrid.tsx index b468f085d191d..44eaa5de10fe4 100644 --- a/apps/studio/components/grid/SupabaseGrid.tsx +++ b/apps/studio/components/grid/SupabaseGrid.tsx @@ -16,7 +16,7 @@ import { Shortcuts } from './components/common/Shortcuts' import { Footer } from './components/footer/Footer' import { Grid } from './components/grid/Grid' import { Header, HeaderProps } from './components/header/Header' -import { RowContextMenu } from './components/menu' +import { RowContextMenu } from './components/menu/RowContextMenu' import { GridProps } from './types' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' diff --git a/apps/studio/components/grid/components/menu/RowContextMenu.tsx b/apps/studio/components/grid/components/menu/RowContextMenu.tsx index 4460cc0314019..285bc1ecaa0d3 100644 --- a/apps/studio/components/grid/components/menu/RowContextMenu.tsx +++ b/apps/studio/components/grid/components/menu/RowContextMenu.tsx @@ -1,4 +1,4 @@ -import { Clipboard, Edit, Trash } from 'lucide-react' +import { Copy, Edit, Trash } from 'lucide-react' import { useCallback } from 'react' import { Item, ItemParams, Menu } from 'react-contexify' import { toast } from 'sonner' @@ -14,7 +14,7 @@ export type RowContextMenuProps = { rows: SupaRow[] } -const RowContextMenu = ({ rows }: RowContextMenuProps) => { +export const RowContextMenu = ({ rows }: RowContextMenuProps) => { const tableEditorSnap = useTableEditorStateSnapshot() const snap = useTableEditorTableStateSnapshot() @@ -68,11 +68,11 @@ const RowContextMenu = ({ rows }: RowContextMenuProps) => { return ( - + Copy cell - + Copy row @@ -88,4 +88,3 @@ const RowContextMenu = ({ rows }: RowContextMenuProps) => { ) } -export default RowContextMenu diff --git a/apps/studio/components/grid/components/menu/index.ts b/apps/studio/components/grid/components/menu/index.ts index cd53af00a3334..a09f947f19c04 100644 --- a/apps/studio/components/grid/components/menu/index.ts +++ b/apps/studio/components/grid/components/menu/index.ts @@ -1,2 +1 @@ export { default as ColumnMenu } from './ColumnMenu' -export { default as RowContextMenu } from './RowContextMenu' diff --git a/apps/studio/components/interfaces/Auth/Users/Users.utils.tsx b/apps/studio/components/interfaces/Auth/Users/Users.utils.tsx index ca935717c1d48..e67174d8bef86 100644 --- a/apps/studio/components/interfaces/Auth/Users/Users.utils.tsx +++ b/apps/studio/components/interfaces/Auth/Users/Users.utils.tsx @@ -1,5 +1,5 @@ import dayjs from 'dayjs' -import { Clipboard, Trash, UserIcon } from 'lucide-react' +import { Copy, Trash, UserIcon } from 'lucide-react' import { Column, useRowSelection } from 'react-data-grid' import { User } from 'data/auth/users-infinite-query' @@ -386,7 +386,7 @@ export const formatUserColumns = ({ copyToClipboard(value) }} > - + Copy {col.id === 'id' ? col.name : col.name.toLowerCase()} diff --git a/apps/studio/components/interfaces/Database/Schemas/SchemaGraph.tsx b/apps/studio/components/interfaces/Database/Schemas/SchemaGraph.tsx index 61e1d483f12c1..3880fd9c62c80 100644 --- a/apps/studio/components/interfaces/Database/Schemas/SchemaGraph.tsx +++ b/apps/studio/components/interfaces/Database/Schemas/SchemaGraph.tsx @@ -1,6 +1,6 @@ import type { PostgresSchema } from '@supabase/postgres-meta' import { toPng, toSvg } from 'html-to-image' -import { Check, Clipboard, Download, Loader2 } from 'lucide-react' +import { Check, Copy, Download, Loader2 } from 'lucide-react' import { useTheme } from 'next-themes' import { useEffect, useMemo, useState } from 'react' import ReactFlow, { Background, BackgroundVariant, MiniMap, useReactFlow } from 'reactflow' @@ -207,7 +207,7 @@ export const SchemaGraph = () => {
: } + icon={copied ? : } onClick={() => { if (tables) { copyToClipboard(tablesToSQL(tables)) diff --git a/apps/studio/components/interfaces/Functions/CommandRender.tsx b/apps/studio/components/interfaces/Functions/CommandRender.tsx index 8285ce8b3c2b0..df92d2c8fed91 100644 --- a/apps/studio/components/interfaces/Functions/CommandRender.tsx +++ b/apps/studio/components/interfaces/Functions/CommandRender.tsx @@ -1,4 +1,4 @@ -import { Check, Clipboard } from 'lucide-react' +import { Check, Copy } from 'lucide-react' import { forwardRef, useState } from 'react' import { cn, copyToClipboard } from 'ui' @@ -45,7 +45,7 @@ const Command = ({ item }: any) => { {isCopied ? ( ) : ( - + )} diff --git a/apps/studio/components/interfaces/Functions/EdgeFunctionsListItem.tsx b/apps/studio/components/interfaces/Functions/EdgeFunctionsListItem.tsx index b7a152514bebb..9056d0f55ccb5 100644 --- a/apps/studio/components/interfaces/Functions/EdgeFunctionsListItem.tsx +++ b/apps/studio/components/interfaces/Functions/EdgeFunctionsListItem.tsx @@ -1,5 +1,5 @@ import dayjs from 'dayjs' -import { Check, Clipboard } from 'lucide-react' +import { Check, Copy } from 'lucide-react' import { useRouter } from 'next/router' import { useState } from 'react' @@ -66,7 +66,7 @@ export const EdgeFunctionsListItem = ({ function: item }: EdgeFunctionsListItemP ) : (
- +
)} diff --git a/apps/studio/components/interfaces/HomeNew/AdvisorSection.tsx b/apps/studio/components/interfaces/HomeNew/AdvisorSection.tsx index 65858938c2637..8d2e466e4b091 100644 --- a/apps/studio/components/interfaces/HomeNew/AdvisorSection.tsx +++ b/apps/studio/components/interfaces/HomeNew/AdvisorSection.tsx @@ -2,6 +2,7 @@ import { BarChart, Shield } from 'lucide-react' import { useCallback, useMemo, useState } from 'react' import { useParams } from 'common' +import LintDetail from 'components/interfaces/Linter/LintDetail' import { LINTER_LEVELS } from 'components/interfaces/Linter/Linter.constants' import { createLintSummaryPrompt, @@ -28,7 +29,6 @@ import { } from 'ui' import { Row } from 'ui-patterns' import ShimmeringLoader from 'ui-patterns/ShimmeringLoader' -import LintDetail from 'components/interfaces/Linter/LintDetail' export const AdvisorSection = () => { const { ref: projectRef } = useParams() diff --git a/apps/studio/components/interfaces/HomeNew/CustomReportSection.tsx b/apps/studio/components/interfaces/HomeNew/CustomReportSection.tsx index 8737860d1dd96..ad0e12284ad4a 100644 --- a/apps/studio/components/interfaces/HomeNew/CustomReportSection.tsx +++ b/apps/studio/components/interfaces/HomeNew/CustomReportSection.tsx @@ -50,9 +50,11 @@ export function CustomReportSection() { reportContent ) - useEffect(() => { - if (reportContent) setEditableReport(reportContent) - }, [reportContent]) + const { can: canCreateReport } = useAsyncCheckPermissions( + PermissionAction.CREATE, + 'user_content', + { resource: { type: 'report', owner_id: profile?.id }, subject: { id: profile?.id } } + ) const { can: canUpdateReport } = useAsyncCheckPermissions( PermissionAction.UPDATE, @@ -67,12 +69,6 @@ export function CustomReportSection() { } ) - const { can: canCreateReport } = useAsyncCheckPermissions( - PermissionAction.CREATE, - 'user_content', - { resource: { type: 'report', owner_id: profile?.id }, subject: { id: profile?.id } } - ) - const { mutate: upsertContent } = useContentUpsertMutation() const persistReport = useCallback( @@ -275,10 +271,14 @@ export function CustomReportSection() { const layout = useMemo(() => editableReport?.layout ?? [], [editableReport]) + useEffect(() => { + if (reportContent) setEditableReport(reportContent) + }, [reportContent]) + return (
-

At a glance

+

Reports

{canUpdateReport || canCreateReport ? (
- {(() => { - if (layout.length === 0) { - return ( -
- {canUpdateReport || canCreateReport ? ( - }> - Add your first chart - - } - side="bottom" - align="center" - autoFocus - /> - ) : ( -

No charts set up yet in report

- )} -
- ) - } - return ( - +

Build a custom report

+

+ Keep track of your most important metrics +

+ {canUpdateReport || canCreateReport ? ( + }> + Add your first block + + } + side="bottom" + align="center" + autoFocus + /> + ) : ( +

No charts set up yet in report

+ )} +
+ ) : ( + + String(x.id))} + strategy={rectSortingStrategy} > - String(x.id))} - strategy={rectSortingStrategy} - > - - {layout.map((item) => ( - -
- handleUpdateChart(item.id, config)} - /> -
-
- ))} -
-
-
- ) - })()} + + {layout.map((item) => ( + +
+ handleUpdateChart(item.id, config)} + /> +
+
+ ))} +
+ + + )}
) diff --git a/apps/studio/components/interfaces/HomeNew/GettingStarted/GettingStarted.tsx b/apps/studio/components/interfaces/HomeNew/GettingStarted/GettingStarted.tsx index b07d0819f5685..fbdfe7aa41f0f 100644 --- a/apps/studio/components/interfaces/HomeNew/GettingStarted/GettingStarted.tsx +++ b/apps/studio/components/interfaces/HomeNew/GettingStarted/GettingStarted.tsx @@ -2,10 +2,9 @@ import { Check, ChevronLeft, ChevronRight } from 'lucide-react' import Image from 'next/image' import Link from 'next/link' import { useEffect, useState } from 'react' - +import { cn, Button, Card, CardContent, Badge } from 'ui' +import { GettingStartedStep, GettingStartedAction } from './GettingStarted.types' import { BASE_PATH } from 'lib/constants' -import { Badge, Button, Card, CardContent, cn } from 'ui' -import { GettingStartedAction, GettingStartedStep } from './GettingStartedSection' // Determine action type for tracking const getActionType = (action: GettingStartedAction): 'primary' | 'ai_assist' | 'external_link' => { diff --git a/apps/studio/components/interfaces/HomeNew/GettingStarted/GettingStarted.types.ts b/apps/studio/components/interfaces/HomeNew/GettingStarted/GettingStarted.types.ts new file mode 100644 index 0000000000000..e1a6ca55ea82b --- /dev/null +++ b/apps/studio/components/interfaces/HomeNew/GettingStarted/GettingStarted.types.ts @@ -0,0 +1,24 @@ +import { ComponentProps, ReactNode } from 'react' + +import { Button } from 'ui' + +export type GettingStartedAction = { + label: string + href?: string + variant?: ComponentProps['type'] + icon?: ReactNode + component?: ReactNode + onClick?: () => void +} + +export type GettingStartedStep = { + key: string + status: 'complete' | 'incomplete' + icon?: ReactNode + title: string + description: string + image?: string + actions: GettingStartedAction[] +} + +export type GettingStartedState = 'empty' | 'code' | 'no-code' | 'hidden' diff --git a/apps/studio/components/interfaces/HomeNew/GettingStarted/GettingStarted.utils.tsx b/apps/studio/components/interfaces/HomeNew/GettingStarted/GettingStarted.utils.tsx new file mode 100644 index 0000000000000..a5eb527201034 --- /dev/null +++ b/apps/studio/components/interfaces/HomeNew/GettingStarted/GettingStarted.utils.tsx @@ -0,0 +1,412 @@ +import { + BarChart3, + Code, + Database, + GitBranch, + Shield, + Table, + Upload, + User, + UserPlus, +} from 'lucide-react' + +import { FRAMEWORKS } from 'components/interfaces/Connect/Connect.constants' +import { AiIconAnimation, CodeBlock } from 'ui' +import type { GettingStartedAction, GettingStartedStep } from './GettingStarted.types' +import type { GettingStartedStatuses } from './useGettingStartedProgress' + +type BuildStepsBaseArgs = { + ref: string | undefined + openAiChat: (name: string, initialInput: string) => void + connectActions: GettingStartedAction[] + statuses: GettingStartedStatuses +} + +type BuildCodeStepsArgs = BuildStepsBaseArgs + +type BuildNoCodeStepsArgs = BuildStepsBaseArgs + +export const getCodeWorkflowSteps = ({ + ref, + openAiChat, + connectActions, + statuses, +}: BuildCodeStepsArgs): GettingStartedStep[] => { + const { + hasTables, + hasCliSetup, + hasSampleData, + hasRlsPolicies, + hasConfiguredAuth, + hasAppConnected, + hasFirstUser, + hasStorageObjects, + hasEdgeFunctions, + hasReports, + hasGitHubConnection, + } = statuses + + return [ + { + key: 'install-cli', + status: hasCliSetup ? 'complete' : 'incomplete', + title: 'Install the Supabase CLI', + icon: , + description: + 'To get started, install the Supabase CLI—our command-line toolkit for managing projects locally, handling migrations, and seeding data—using the npm command below to add it to your workspace.', + actions: [ + { + label: 'Install via npm', + component: ( + + npm install supabase --save-dev + + ), + }, + ], + }, + { + key: 'design-db', + status: hasTables ? 'complete' : 'incomplete', + title: 'Design your database schema', + icon: , + description: + 'Next, create a schema file that defines the structure of your database, either following our declarative schema guide or asking the AI assistant to generate one for you.', + actions: [ + { + label: 'Create schema file', + href: 'https://supabase.com/docs/guides/local-development/declarative-database-schemas', + variant: 'default', + }, + { + label: 'Generate it', + variant: 'default', + icon: , + onClick: () => + openAiChat( + 'Design my database', + 'Help me create a schema file for my database. We will be using Supabase declarative schemas which you can learn about by searching docs for declarative schema.' + ), + }, + ], + }, + { + key: 'add-data', + status: hasSampleData ? 'complete' : 'incomplete', + title: 'Seed your database with data', + icon: , + description: + 'Now, create a seed file to populate your database with initial data, using the docs for guidance or letting the AI assistant draft realistic inserts.', + actions: [ + { + label: 'Create a seed file', + href: 'https://supabase.com/docs/guides/local-development/seeding-your-database', + variant: 'default', + }, + { + label: 'Generate data', + variant: 'default', + icon: , + onClick: () => + openAiChat( + 'Generate seed data', + 'Generate SQL INSERT statements for realistic seed data that I can run via the Supabase CLI.' + ), + }, + ], + }, + { + key: 'add-rls-policies', + status: hasRlsPolicies ? 'complete' : 'incomplete', + title: 'Secure your data with RLS policies', + icon: , + description: + "Let's secure your data by enabling Row Level Security (per-row access rules that decide who can read or write specific records) and defining policies in a migration file, either configuring them manually or letting the AI assistant draft policies for your tables.", + actions: [ + { + label: 'Create a migration file', + href: `/project/${ref}/auth/policies`, + variant: 'default', + }, + { + label: 'Create policies for me', + variant: 'default', + icon: , + onClick: () => + openAiChat( + 'Generate RLS policies', + 'Generate RLS policies for my existing tables in the public schema and guide me through the process of adding them as migration files to my codebase ' + ), + }, + ], + }, + { + key: 'setup-auth', + status: hasConfiguredAuth ? 'complete' : 'incomplete', + title: 'Allow user signups', + icon: , + description: + "It's time to configure your authentication providers and settings for Supabase Auth, so jump into the configuration page and tailor the providers you need.", + actions: [{ label: 'Configure', href: `/project/${ref}/auth/providers`, variant: 'default' }], + }, + { + key: 'connect-app', + status: hasAppConnected ? 'complete' : 'incomplete', + title: 'Connect your application', + icon: , + description: + 'Your project is ready; use the framework selector to preview starter code and launch the Connect flow with the client library you prefer.', + actions: connectActions, + }, + { + key: 'signup-first-user', + status: hasFirstUser ? 'complete' : 'incomplete', + title: 'Sign up your first user', + icon: , + description: + 'Test your authentication setup by creating the first user account, following the docs if you need a step-by-step walkthrough.', + actions: [ + { + label: 'Read docs', + href: 'https://supabase.com/docs/guides/auth', + variant: 'default', + }, + ], + }, + { + key: 'upload-file', + status: hasStorageObjects ? 'complete' : 'incomplete', + title: 'Upload a file', + icon: , + description: + 'Integrate file storage by creating a bucket via SQL and uploading a file using our client libraries.', + actions: [ + { + label: 'Create a bucket via SQL', + href: 'https://supabase.com/docs/guides/storage/buckets/creating-buckets?queryGroups=language&language=sql', + variant: 'default', + }, + { + label: 'Upload a file', + href: 'https://supabase.com/docs/guides/storage/uploads/standard-uploads', + variant: 'default', + }, + ], + }, + { + key: 'create-edge-function', + status: hasEdgeFunctions ? 'complete' : 'incomplete', + title: 'Deploy an Edge Function', + icon: , + description: + 'Add server-side logic by creating and deploying your first Edge Function—a lightweight TypeScript or JavaScript function that runs close to your users—then revisit the list to monitor and iterate on it.', + actions: [ + { + label: 'Create and deploy via CLI', + href: `https://supabase.com/docs/guides/functions/quickstart`, + variant: 'default', + }, + { label: 'View functions', href: `/project/${ref}/functions`, variant: 'default' }, + ], + }, + { + key: 'monitor-progress', + status: hasReports ? 'complete' : 'incomplete', + title: "Monitor your project's usage", + icon: , + description: + "Track your project's activity by creating custom reports for API, database, and auth events right from the reports dashboard.", + actions: [{ label: 'Reports', href: `/project/${ref}/reports`, variant: 'default' }], + }, + { + key: 'connect-github', + status: hasGitHubConnection ? 'complete' : 'incomplete', + title: 'Connect to GitHub', + icon: , + description: + 'Link this project to a GitHub repository to keep production in sync and spin up preview branches from pull requests.', + actions: [ + { + label: 'Connect to GitHub', + href: `/project/${ref}/settings/integrations`, + variant: 'default', + }, + ], + }, + ] +} + +export const getNoCodeWorkflowSteps = ({ + ref, + openAiChat, + connectActions, + statuses, +}: BuildNoCodeStepsArgs): GettingStartedStep[] => { + const { + hasTables, + hasSampleData, + hasRlsPolicies, + hasConfiguredAuth, + hasAppConnected, + hasFirstUser, + hasStorageObjects, + hasEdgeFunctions, + hasReports, + hasGitHubConnection, + } = statuses + + return [ + { + key: 'design-db', + status: hasTables ? 'complete' : 'incomplete', + title: 'Create your first table', + icon: , + description: + "To kick off your new project, let's start by creating your very first database table using either the table editor or the AI assistant to shape the structure for you.", + actions: [ + { label: 'Create a table', href: `/project/${ref}/editor`, variant: 'default' }, + { + label: 'Do it for me', + variant: 'default', + icon: , + onClick: () => + openAiChat( + 'Design my database', + 'I want to design my database schema. Please propose tables, relationships, and SQL to create them for my app. Ask clarifying questions if needed.' + ), + }, + ], + }, + { + key: 'add-data', + status: hasSampleData ? 'complete' : 'incomplete', + title: 'Add sample data', + icon:
, + description: + "Next, let's add some sample data that you can play with once you connect your app, either by inserting rows yourself or letting the AI assistant craft realistic examples.", + actions: [ + { label: 'Add data', href: `/project/${ref}/editor`, variant: 'default' }, + { + label: 'Do it for me', + variant: 'default', + icon: , + onClick: () => + openAiChat( + 'Generate sample data', + 'Generate SQL INSERT statements to add realistic sample data to my existing tables. Use safe defaults and avoid overwriting data.' + ), + }, + ], + }, + { + key: 'add-rls-policies', + status: hasRlsPolicies ? 'complete' : 'incomplete', + title: 'Secure your data with Row Level Security', + icon: , + description: + "Now that you have some data, let's secure it by enabling Row Level Security (row-specific access rules that control who can view or modify records) and creating policies yourself or with help from the AI assistant.", + actions: [ + { + label: 'Create a policy', + href: `/project/${ref}/auth/policies`, + variant: 'default', + }, + { + label: 'Do it for me', + variant: 'default', + icon: , + onClick: () => + openAiChat( + 'Generate RLS policies', + 'Generate RLS policies for my existing tables in the public schema. ' + ), + }, + ], + }, + { + key: 'setup-auth', + status: hasConfiguredAuth ? 'complete' : 'incomplete', + title: 'Allow user signups', + icon: , + description: + "It's time to set up authentication so you can start signing up users, configuring providers and settings from the auth dashboard.", + actions: [ + { label: 'Configure auth', href: `/project/${ref}/auth/providers`, variant: 'default' }, + ], + }, + { + key: 'connect-app', + status: hasAppConnected ? 'complete' : 'incomplete', + title: 'Connect your application', + icon: , + description: + 'Your project is ready; use the framework selector to preview starter code and launch the Connect flow to wire up your app.', + actions: connectActions, + }, + { + key: 'signup-first-user', + status: hasFirstUser ? 'complete' : 'incomplete', + title: 'Sign up your first user', + icon: , + description: + 'Test your authentication by signing up your first user, referencing the docs if you need sample flows or troubleshooting tips.', + actions: [ + { + label: 'Read docs', + href: 'https://supabase.com/docs/guides/auth', + variant: 'default', + }, + ], + }, + { + key: 'upload-file', + status: hasStorageObjects ? 'complete' : 'incomplete', + title: 'Upload a file', + icon: , + description: + "Let's add file storage to your app by creating a bucket and uploading your first file from the buckets dashboard.", + actions: [{ label: 'Buckets', href: `/project/${ref}/storage/buckets`, variant: 'default' }], + }, + { + key: 'create-edge-function', + status: hasEdgeFunctions ? 'complete' : 'incomplete', + title: 'Add server-side logic', + icon: , + description: + "Extend your app's functionality by creating an Edge Function—a lightweight serverless function that executes close to your users—for server-side logic directly from the functions page.", + actions: [ + { + label: 'Create a function', + href: `/project/${ref}/functions/new`, + variant: 'default', + }, + ], + }, + { + key: 'monitor-progress', + status: hasReports ? 'complete' : 'incomplete', + title: "Monitor your project's health", + icon: , + description: + "Keep an eye on your project's performance and usage by setting up custom reports from the reports dashboard.", + actions: [{ label: 'Create a report', href: `/project/${ref}/reports`, variant: 'default' }], + }, + { + key: 'connect-github', + status: hasGitHubConnection ? 'complete' : 'incomplete', + title: 'Connect to GitHub', + icon: , + description: + 'Connect your project to GitHub to automatically create preview branches and sync production changes.', + actions: [ + { + label: 'Connect to GitHub', + href: `/project/${ref}/settings/integrations`, + variant: 'default', + }, + ], + }, + ] +} + +export const DEFAULT_FRAMEWORK_KEY = FRAMEWORKS[0]?.key ?? 'nextjs' diff --git a/apps/studio/components/interfaces/HomeNew/GettingStarted/GettingStartedSection.tsx b/apps/studio/components/interfaces/HomeNew/GettingStarted/GettingStartedSection.tsx index 4771b751fb664..ba121daa17079 100644 --- a/apps/studio/components/interfaces/HomeNew/GettingStarted/GettingStartedSection.tsx +++ b/apps/studio/components/interfaces/HomeNew/GettingStarted/GettingStartedSection.tsx @@ -1,99 +1,48 @@ -import { - BarChart3, - Code, - Database, - GitBranch, - Shield, - Table, - Table2, - Upload, - User, - UserPlus, -} from 'lucide-react' +import { Code, Table2 } from 'lucide-react' import { useRouter } from 'next/router' import { useCallback, useMemo, useState } from 'react' import { useParams } from 'common' import { FRAMEWORKS } from 'components/interfaces/Connect/Connect.constants' -import { useBranchesQuery } from 'data/branches/branches-query' -import { useTablesQuery } from 'data/tables/tables-query' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' -import { BASE_PATH, DOCS_URL } from 'lib/constants' +import { BASE_PATH } from 'lib/constants' import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state' -import { - AiIconAnimation, - Button, - Card, - CardContent, - CodeBlock, - ToggleGroup, - ToggleGroupItem, -} from 'ui' +import { Button, Card, CardContent, ToggleGroup, ToggleGroupItem } from 'ui' import { FrameworkSelector } from './FrameworkSelector' import { GettingStarted } from './GettingStarted' +import { + GettingStartedAction, + GettingStartedState, + GettingStartedStep, +} from './GettingStarted.types' +import { + DEFAULT_FRAMEWORK_KEY, + getCodeWorkflowSteps, + getNoCodeWorkflowSteps, +} from './GettingStarted.utils' +import { useGettingStartedProgress } from './useGettingStartedProgress' -export type GettingStartedAction = { - label: string - href?: string - onClick?: () => void - variant?: React.ComponentProps['type'] - icon?: React.ReactNode - component?: React.ReactNode -} - -export type GettingStartedStep = { - key: string - status: 'complete' | 'incomplete' - icon?: React.ReactNode - title: string - description: string - image?: string - actions: GettingStartedAction[] -} - -export type GettingStartedState = 'empty' | 'code' | 'no-code' | 'hidden' - -export function GettingStartedSection({ - value, - onChange, -}: { +interface GettingStartedSectionProps { value: GettingStartedState onChange: (v: GettingStartedState) => void -}) { +} + +export function GettingStartedSection({ value, onChange }: GettingStartedSectionProps) { + const router = useRouter() + const { ref } = useParams() const { data: project } = useSelectedProjectQuery() const { data: organization } = useSelectedOrganizationQuery() - const { ref } = useParams() - const aiSnap = useAiAssistantStateSnapshot() - const router = useRouter() const { mutate: sendEvent } = useSendEventMutation() + const aiSnap = useAiAssistantStateSnapshot() - // Local state for framework selector preview - const [selectedFramework, setSelectedFramework] = useState(FRAMEWORKS[0]?.key ?? 'nextjs') + const [selectedFramework, setSelectedFramework] = useState(DEFAULT_FRAMEWORK_KEY) const workflow: 'no-code' | 'code' | null = value === 'code' || value === 'no-code' ? value : null const [previousWorkflow, setPreviousWorkflow] = useState<'no-code' | 'code' | null>(null) - const { data: tablesData } = useTablesQuery({ - projectRef: project?.ref, - connectionString: project?.connectionString, - schema: 'public', - }) - - const tablesCount = Math.max(0, tablesData?.length ?? 0) - const { data: branchesData } = useBranchesQuery({ - projectRef: project?.parent_project_ref ?? project?.ref, - }) - const isDefaultProject = project?.parent_project_ref === undefined - const hasNonDefaultBranch = - (branchesData ?? []).some((b) => !b.is_default) || isDefaultProject === false + const statuses = useGettingStartedProgress() - const selectedFrameworkMeta = useMemo( - () => FRAMEWORKS.find((item) => item.key === selectedFramework), - [selectedFramework] - ) - - // Helpers const openAiChat = useCallback( (name: string, initialInput: string) => aiSnap.newChat({ name, open: true, initialInput }), [aiSnap] @@ -133,354 +82,29 @@ export function GettingStartedSection({ onClick: openConnect, }, ], - [openConnect, openAiChat, selectedFramework, selectedFrameworkMeta?.label] + [openConnect, selectedFramework] ) const codeSteps: GettingStartedStep[] = useMemo( - () => [ - { - key: 'install-cli', - status: 'incomplete', - title: 'Install the Supabase CLI', - icon: , - description: - 'To get started, install the Supabase CLI—our command-line toolkit for managing projects locally, handling migrations, and seeding data—using the npm command below to add it to your workspace.', - actions: [ - { - label: 'Install via npm', - component: ( - - npm install supabase --save-dev - - ), - }, - ], - }, - { - key: 'design-db', - status: tablesCount > 0 ? 'complete' : 'incomplete', - title: 'Design your database schema', - icon: , - image: `${BASE_PATH}/img/getting-started/declarative-schemas.png`, - description: - 'Next, create a schema file that defines the structure of your database, either following our declarative schema guide or asking the AI assistant to generate one for you.', - actions: [ - { - label: 'Create schema file', - href: `${DOCS_URL}/guides/local-development/declarative-database-schemas`, - variant: 'default', - }, - { - label: 'Generate it', - variant: 'default', - icon: , - onClick: () => - openAiChat( - 'Design my database', - 'Help me create a schema file for my database. We will be using Supabase declarative schemas which you can learn about by searching docs for declarative schema.' - ), - }, - ], - }, - { - key: 'add-data', - status: 'incomplete', - title: 'Seed your database with data', - icon:
, - description: - 'Now, create a seed file to populate your database with initial data, using the docs for guidance or letting the AI assistant draft realistic inserts.', - actions: [ - { - label: 'Create a seed file', - href: `${DOCS_URL}/guides/local-development/seeding-your-database`, - variant: 'default', - }, - { - label: 'Generate data', - variant: 'default', - icon: , - onClick: () => - openAiChat( - 'Generate seed data', - 'Generate SQL INSERT statements for realistic seed data that I can run via the Supabase CLI.' - ), - }, - ], - }, - { - key: 'add-rls-policies', - status: 'incomplete', - title: 'Secure your data with RLS policies', - icon: , - description: - "Let's secure your data by enabling Row Level Security (per-row access rules that decide who can read or write specific records) and defining policies in a migration file, either configuring them manually or letting the AI assistant draft policies for your tables.", - actions: [ - { - label: 'Create a migration file', - href: `/project/${ref}/auth/policies`, - variant: 'default', - }, - { - label: 'Create policies for me', - variant: 'default', - icon: , - onClick: () => - openAiChat( - 'Generate RLS policies', - 'Generate RLS policies for my existing tables in the public schema and guide me through the process of adding them as migration files to my codebase ' - ), - }, - ], - }, - { - key: 'setup-auth', - status: 'incomplete', - title: 'Configure authentication', - icon: , - description: - "It's time to configure your authentication providers and settings for Supabase Auth, so jump into the configuration page and tailor the providers you need.", - actions: [ - { label: 'Configure', href: `/project/${ref}/auth/providers`, variant: 'default' }, - ], - }, - { - key: 'connect-app', - status: 'incomplete', - title: 'Connect your application', - icon: , - description: - 'Your project is ready; use the framework selector to preview starter code and launch the Connect flow with the client library you prefer.', - actions: connectActions, - }, - { - key: 'signup-first-user', - status: 'incomplete', - title: 'Sign up your first user', - icon: , - description: - 'Test your authentication setup by creating the first user account, following the docs if you need a step-by-step walkthrough.', - actions: [ - { - label: 'Read docs', - href: `${DOCS_URL}/guides/auth`, - variant: 'default', - }, - ], - }, - { - key: 'upload-file', - status: 'incomplete', - title: 'Upload a file', - icon: , - description: - 'Integrate file storage by creating a bucket and uploading a file, starting from the buckets dashboard linked below.', - actions: [ - { label: 'Buckets', href: `/project/${ref}/storage/buckets`, variant: 'default' }, - ], - }, - { - key: 'create-edge-function', - status: 'incomplete', - title: 'Deploy an Edge Function', - icon: , - description: - 'Add server-side logic by creating and deploying your first Edge Function—a lightweight TypeScript or JavaScript function that runs close to your users—then revisit the list to monitor and iterate on it.', - actions: [ - { - label: 'Create a 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 project's usage", - icon: , - description: - "Track your project's activity by creating custom reports for API, database, and auth events right from the reports dashboard.", - actions: [{ label: 'Reports', href: `/project/${ref}/reports`, variant: 'default' }], - }, - { - key: 'create-first-branch', - status: hasNonDefaultBranch ? 'complete' : 'incomplete', - title: 'Connect to GitHub', - icon: , - description: - 'Streamline your development workflow by connecting your project to GitHub, using the integrations page to automate branch management.', - actions: [ - { - label: 'Connect to GitHub', - href: `/project/${ref}/settings/integrations`, - variant: 'default', - }, - ], - }, - ], - [tablesCount, ref, openAiChat, connectActions, hasNonDefaultBranch] + () => + getCodeWorkflowSteps({ + ref, + openAiChat, + connectActions, + statuses, + }), + [connectActions, openAiChat, ref, statuses] ) const noCodeSteps: GettingStartedStep[] = useMemo( - () => [ - { - key: 'design-db', - status: tablesCount > 0 ? 'complete' : 'incomplete', - title: 'Create your first table', - icon: , - image: `${BASE_PATH}/img/getting-started/sample.png`, - description: - "To kick off your new project, let's start by creating your very first database table using either the table editor or the AI assistant to shape the structure for you.", - actions: [ - { label: 'Create a table', href: `/project/${ref}/editor`, variant: 'default' }, - { - label: 'Do it for me', - variant: 'default', - icon: , - onClick: () => - openAiChat( - 'Design my database', - 'I want to design my database schema. Please propose tables, relationships, and SQL to create them for my app. Ask clarifying questions if needed.' - ), - }, - ], - }, - { - key: 'add-data', - status: 'incomplete', - title: 'Add sample data', - icon:
, - description: - "Next, let's add some sample data that you can play with once you connect your app, either by inserting rows yourself or letting the AI assistant craft realistic examples.", - actions: [ - { label: 'Add data', href: `/project/${ref}/editor`, variant: 'default' }, - { - label: 'Do it for me', - variant: 'default', - icon: , - onClick: () => - openAiChat( - 'Generate sample data', - 'Generate SQL INSERT statements to add realistic sample data to my existing tables. Use safe defaults and avoid overwriting data.' - ), - }, - ], - }, - { - key: 'add-rls-policies', - status: 'incomplete', - title: 'Secure your data with Row Level Security', - icon: , - description: - "Now that you have some data, let's secure it by enabling Row Level Security (row-specific access rules that control who can view or modify records) and creating policies yourself or with help from the AI assistant.", - actions: [ - { - label: 'Create a policy', - href: `/project/${ref}/auth/policies`, - variant: 'default', - }, - { - label: 'Do it for me', - variant: 'default', - icon: , - onClick: () => - openAiChat( - 'Generate RLS policies', - 'Generate RLS policies for my existing tables in the public schema. ' - ), - }, - ], - }, - { - key: 'setup-auth', - status: 'incomplete', - title: 'Set up authentication', - icon: , - description: - "It's time to set up authentication so you can start signing up users, configuring providers and settings from the auth dashboard.", - actions: [ - { - label: 'Configure auth', - href: `/project/${ref}/auth/providers`, - variant: 'default', - }, - ], - }, - { - key: 'connect-app', - status: 'incomplete', - title: 'Connect your application', - icon: , - description: - 'Your project is ready; use the framework selector to preview starter code and launch the Connect flow to wire up your app.', - actions: connectActions, - }, - { - key: 'signup-first-user', - status: 'incomplete', - title: 'Sign up your first user', - icon: , - description: - 'Test your authentication by signing up your first user, referencing the docs if you need sample flows or troubleshooting tips.', - actions: [ - { - label: 'Read docs', - href: `${DOCS_URL}/guides/auth`, - variant: 'default', - }, - ], - }, - { - key: 'upload-file', - status: 'incomplete', - title: 'Upload a file', - icon: , - description: - "Let's add file storage to your app by creating a bucket and uploading your first file from the buckets dashboard.", - actions: [ - { label: 'Buckets', href: `/project/${ref}/storage/buckets`, variant: 'default' }, - ], - }, - { - key: 'create-edge-function', - status: 'incomplete', - title: 'Add server-side logic', - icon: , - description: - "Extend your app's functionality by creating an Edge Function—a lightweight serverless function that executes close to your users—for server-side logic directly from the functions page.", - actions: [ - { - label: 'Create a function', - href: `/project/${ref}/functions/new`, - variant: 'default', - }, - ], - }, - { - key: 'monitor-progress', - status: 'incomplete', - title: "Monitor your project's health", - icon: , - description: - "Keep an eye on your project's performance and usage by setting up custom reports from the reports dashboard.", - actions: [ - { label: 'Create a report', href: `/project/${ref}/reports`, variant: 'default' }, - ], - }, - { - key: 'create-first-branch', - status: hasNonDefaultBranch ? 'complete' : 'incomplete', - title: 'Create a branch to test changes', - icon: , - description: - 'Safely test changes by creating a preview branch before deploying to production, using the branches view to spin one up.', - actions: [ - { label: 'Create a branch', href: `/project/${ref}/branches`, variant: 'default' }, - ], - }, - ], - [tablesCount, ref, openAiChat, connectActions, hasNonDefaultBranch] + () => + getNoCodeWorkflowSteps({ + ref, + openAiChat, + connectActions, + statuses, + }), + [connectActions, openAiChat, ref, statuses] ) const steps = workflow === 'code' ? codeSteps : workflow === 'no-code' ? noCodeSteps : [] @@ -519,7 +143,7 @@ export function GettingStartedSection({ className="text-xs gap-2 h-auto" > - Code + No-code - No-code + Code - - - - - - No results found. - - {supabaseProjects.map((project, i) => { - return ( - { - if (project.ref) setSupabaseProjectRef(project.ref) - setSupabaseProjectsComboboxOpen(false) - }} - > -
- Supabase -
- {project.name} -
- ) - })} - {supabaseProjects.length === 0 && ( -

- No projects found in this organization -

- )} -
- + {project.ref === supabaseProjectRef && } + + ) + }} + renderTrigger={() => { + return ( + + ) + }} + renderActions={() => { + return ( + projectCreationEnabled && ( router.push(`/new/${selectedOrganization?.slug}`)} - onSelect={() => router.push(`/new/${selectedOrganization?.slug}`)} + className="cursor-pointer w-full" + onSelect={() => { + setOpenProjectsDropdown(false) + router.push(`/new/${selectedOrganization?.slug}`) + }} + onClick={() => setOpenProjectsDropdown(false)} > - - Create a new project + { + setOpenProjectsDropdown(false) + }} + className="w-full flex items-center gap-2" + > + +

Create a new project

+
-
-
-
- + ) + ) + }} + />
@@ -291,12 +297,11 @@ const ProjectLinker = ({
) : ( - )} diff --git a/apps/studio/state/sql-editor-v2.ts b/apps/studio/state/sql-editor-v2.ts index a5d2e75897b5c..afdc7aed40ea8 100644 --- a/apps/studio/state/sql-editor-v2.ts +++ b/apps/studio/state/sql-editor-v2.ts @@ -306,7 +306,9 @@ async function upsertSnippet( } let snippet = sqlEditorState.snippets[id]?.snippet - if (snippet?.content) snippet.isNotSavedInDatabaseYet = false + if (snippet?.content && 'isNotSavedInDatabaseYet' in snippet) { + snippet.isNotSavedInDatabaseYet = false + } sqlEditorState.savingStates[id] = 'IDLE' } catch (error) { sqlEditorState.savingStates[id] = 'UPDATING_FAILED' diff --git a/packages/ui-patterns/src/Row/index.tsx b/packages/ui-patterns/src/Row/index.tsx index 76a8922ec874b..f609c3cd8e9db 100644 --- a/packages/ui-patterns/src/Row/index.tsx +++ b/packages/ui-patterns/src/Row/index.tsx @@ -1,10 +1,10 @@ 'use client' +import { ChevronLeft, ChevronRight } from 'lucide-react' import type React from 'react' +import type { ReactNode } from 'react' import { forwardRef, useEffect, useMemo, useRef, useState } from 'react' import { Button, cn } from 'ui' -import type { ReactNode } from 'react' -import { ChevronLeft, ChevronRight } from 'lucide-react' import { useMeasuredWidth } from './Row.utils' interface RowProps extends React.HTMLAttributes { @@ -40,7 +40,7 @@ export const Row = forwardRef(function Row( return smCols } - const renderColumns = useMemo( + const numberOfColumns = useMemo( () => resolveColumnsForWidth(measuredWidth ?? 0), [measuredWidth, columns] ) @@ -49,7 +49,7 @@ export const Row = forwardRef(function Row( const el = containerRef.current if (!el) return const widthLocal = measuredWidth ?? el.getBoundingClientRect().width - const colsLocal = renderColumns + const colsLocal = numberOfColumns const columnWidth = (widthLocal - (colsLocal - 1) * gap) / colsLocal const scrollAmount = columnWidth + gap setScrollPosition((prev) => { @@ -63,15 +63,17 @@ export const Row = forwardRef(function Row( const maxScroll = useMemo(() => { if (measuredWidth == null) return -1 - const colsLocal = renderColumns + const colsLocal = numberOfColumns const columnWidth = (measuredWidth - (colsLocal - 1) * gap) / colsLocal const totalWidth = childrenArray.length * columnWidth + (childrenArray.length - 1) * gap return Math.max(0, totalWidth - measuredWidth) - }, [measuredWidth, renderColumns, childrenArray.length, gap]) + }, [measuredWidth, numberOfColumns, childrenArray.length, gap]) const canScrollLeft = scrollPosition > 0 const canScrollRight = scrollPosition < maxScroll + const hasContentToScroll = childrenArray.length > numberOfColumns + const rafIdRef = useRef(0 as number) const pendingDeltaRef = useRef(0) @@ -125,7 +127,7 @@ export const Row = forwardRef(function Row( )} - {showArrows && canScrollRight && ( + {showArrows && canScrollRight && hasContentToScroll && (