From 697709af5813a23ed62da58e9ac2f2a2801e1807 Mon Sep 17 00:00:00 2001 From: Saxon Fletcher Date: Thu, 11 Sep 2025 10:05:03 +1000 Subject: [PATCH 1/2] Home New: Getting Started (#38574) * getting started row * refine * fix: minor type and lint issues * connect --------- Co-authored-by: Charis Lam <26616127+charislam@users.noreply.github.com> --- .../GettingStarted/FrameworkSelector.tsx | 85 +++ .../HomeNew/GettingStarted/GettingStarted.tsx | 68 +++ .../GettingStarted/GettingStartedSection.tsx | 560 ++++++++++++++++++ .../components/interfaces/HomeNew/Home.tsx | 16 +- 4 files changed, 728 insertions(+), 1 deletion(-) create mode 100644 apps/studio/components/interfaces/HomeNew/GettingStarted/FrameworkSelector.tsx create mode 100644 apps/studio/components/interfaces/HomeNew/GettingStarted/GettingStarted.tsx create mode 100644 apps/studio/components/interfaces/HomeNew/GettingStarted/GettingStartedSection.tsx diff --git a/apps/studio/components/interfaces/HomeNew/GettingStarted/FrameworkSelector.tsx b/apps/studio/components/interfaces/HomeNew/GettingStarted/FrameworkSelector.tsx new file mode 100644 index 0000000000000..5be91506db440 --- /dev/null +++ b/apps/studio/components/interfaces/HomeNew/GettingStarted/FrameworkSelector.tsx @@ -0,0 +1,85 @@ +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[] + className?: string +} + +export const FrameworkSelector = ({ + value, + onChange, + items, + className, +}: FrameworkSelectorProps) => { + const [open, setOpen] = useState(false) + + const selectedItem = items.find((item) => item.key === value) + + function handleSelect(key: string) { + onChange(key) + setOpen(false) + } + + return ( + +
+ + + +
+ + + + + No results found. + + {items.map((item) => ( + handleSelect(item.key)} + className="flex gap-2 items-center" + > + {item.icon ? : } + {item.label} + + + ))} + + + + +
+ ) +} diff --git a/apps/studio/components/interfaces/HomeNew/GettingStarted/GettingStarted.tsx b/apps/studio/components/interfaces/HomeNew/GettingStarted/GettingStarted.tsx new file mode 100644 index 0000000000000..7763014676ab1 --- /dev/null +++ b/apps/studio/components/interfaces/HomeNew/GettingStarted/GettingStarted.tsx @@ -0,0 +1,68 @@ +import Link from 'next/link' +import { cn, Button, Card, CardContent, CardHeader, CardTitle, Badge } from 'ui' +import { Row } from 'ui-patterns' +import { GettingStartedStep } from './GettingStartedSection' + +export interface GettingStartedProps { + steps: GettingStartedStep[] +} + +export function GettingStarted({ steps }: GettingStartedProps) { + return ( + + {steps.map((step, index) => ( + + +
+ {step.icon &&
{step.icon}
} + + {index + 1}. {step.title} + +
+ + {step.status} + +
+ + {step.image &&
{step.image}
} +

{step.description}

+
+ {step.actions.map((action, i) => { + if (action.component) { + return
{action.component}
+ } + if (action.href) { + return ( + + ) + } + return ( + + ) + })} +
+
+
+ ))} +
+ ) +} diff --git a/apps/studio/components/interfaces/HomeNew/GettingStarted/GettingStartedSection.tsx b/apps/studio/components/interfaces/HomeNew/GettingStarted/GettingStartedSection.tsx new file mode 100644 index 0000000000000..9d91f50447d79 --- /dev/null +++ b/apps/studio/components/interfaces/HomeNew/GettingStarted/GettingStartedSection.tsx @@ -0,0 +1,560 @@ +import { useParams } from 'common' +import { useTablesQuery } from 'data/tables/tables-query' +import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' +import { GettingStarted } from './GettingStarted' +import { + Code, + Database, + Table, + User, + Upload, + UserPlus, + BarChart3, + Shield, + Table2, + GitBranch, +} from 'lucide-react' +import { useBranchesQuery } from 'data/branches/branches-query' +import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state' +import { useRouter } from 'next/router' +import { useCallback, useMemo, useState } from 'react' +import { FrameworkSelector } from './FrameworkSelector' +import { FRAMEWORKS } from 'components/interfaces/Connect/Connect.constants' +import { + AiIconAnimation, + Button, + Card, + CardContent, + CodeBlock, + ToggleGroup, + ToggleGroupItem, +} from 'ui' +import { BASE_PATH } from 'lib/constants' + +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?: React.ReactNode + actions: GettingStartedAction[] +} + +export type GettingStartedState = 'empty' | 'code' | 'no-code' | 'hidden' + +export function GettingStartedSection({ + value, + onChange, +}: { + value: GettingStartedState + onChange: (v: GettingStartedState) => void +}) { + const { data: project } = useSelectedProjectQuery() + const { ref } = useParams() + const aiSnap = useAiAssistantStateSnapshot() + const router = useRouter() + + // Local state for framework selector preview + const [selectedFramework, setSelectedFramework] = useState(FRAMEWORKS[0]?.key ?? 'nextjs') + const workflow: 'no-code' | 'code' | null = value === 'code' || value === 'no-code' ? value : 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 + + // Helpers + const openAiChat = useCallback( + (name: string, initialInput: string) => aiSnap.newChat({ name, open: true, initialInput }), + [aiSnap] + ) + + const openConnect = useCallback(() => { + router.push( + { + pathname: router.pathname, + query: { + ...router.query, + showConnect: true, + connectTab: 'frameworks', + framework: selectedFramework, + }, + }, + undefined, + { shallow: true } + ) + }, [router, selectedFramework]) + + const connectActions: GettingStartedAction[] = useMemo( + () => [ + { + label: 'Framework selector', + component: ( + + ), + }, + { + label: 'Connect', + variant: 'default', + onClick: openConnect, + }, + ], + [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 to manage your project locally, handle migrations, and seed data.', + 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: , + description: 'Next, create a schema file that defines the structure of your database.', + 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: 'incomplete', + title: 'Seed your database with data', + icon: , + description: 'Now, create a seed file to populate your database with initial data.', + 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: 'incomplete', + title: 'Secure your data with RLS policies', + icon: , + description: + "Let's secure your data by enabling Row Level Security and defining access policies in a migration file.", + 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.", + 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. Connect your app using one of our client libraries.', + 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.', + actions: [ + { + label: 'Read docs', + href: 'https://supabase.com/docs/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.', + 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.', + 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.", + 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 to automatically manage branches.', + actions: [ + { + label: 'Connect to GitHub', + href: `/project/${ref}/settings/integrations`, + variant: 'default', + }, + ], + }, + ], + [tablesCount, ref, openAiChat, connectActions, hasNonDefaultBranch] + ) + + const noCodeSteps: GettingStartedStep[] = useMemo( + () => [ + { + key: 'design-db', + status: tablesCount > 0 ? '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 AI Assistant.", + 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.", + 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 and creating policies.", + 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.", + 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. Let's connect your application to Supabase.", + 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.', + actions: [ + { + label: 'Read docs', + href: 'https://supabase.com/docs/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.", + 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 for server-side logic.", + 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.", + 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.', + actions: [ + { label: 'Create a branch', href: `/project/${ref}/branches`, variant: 'default' }, + ], + }, + ], + [tablesCount, ref, openAiChat, connectActions, hasNonDefaultBranch] + ) + + const steps = workflow === 'code' ? codeSteps : workflow === 'no-code' ? noCodeSteps : [] + + return ( +
+
+

Getting started

+
+ v && onChange(v as 'no-code' | 'code')} + > + + + + + + + + +
+
+ + {steps.length === 0 ? ( + +
+ Supabase Grafana + Supabase Grafana +
+
+ +
+

+ Choose a preferred workflow +

+

+ With Supabase, you have the flexibility to adopt a workflow that works for you. You + can do everything via the dashboard, or manage your entire project within your own + codebase. +

+
+
+ + +
+
+ + ) : ( + + )} +
+ ) +} diff --git a/apps/studio/components/interfaces/HomeNew/Home.tsx b/apps/studio/components/interfaces/HomeNew/Home.tsx index d2cc0c2908719..fc8e75fa97cfc 100644 --- a/apps/studio/components/interfaces/HomeNew/Home.tsx +++ b/apps/studio/components/interfaces/HomeNew/Home.tsx @@ -17,6 +17,10 @@ import { import { PROJECT_STATUS } from 'lib/constants' import { useAppStateSnapshot } from 'state/app-state' import { AdvisorSection } from './AdvisorSection' +import { + GettingStartedSection, + type GettingStartedState, +} from './GettingStarted/GettingStartedSection' export const HomeV2 = () => { const { ref, enableBranching } = useParams() @@ -49,7 +53,7 @@ export const HomeV2 = () => { ['getting-started', 'usage', 'advisor', 'custom-report'] ) - const [gettingStartedState] = useLocalStorage<'empty' | 'code' | 'no-code' | 'hidden'>( + const [gettingStartedState, setGettingStartedState] = useLocalStorage( `home-getting-started-${project?.ref || 'default'}`, 'empty' ) @@ -102,6 +106,16 @@ export const HomeV2 = () => { strategy={verticalListSortingStrategy} > {sectionOrder.map((id) => { + if (id === 'getting-started') { + return gettingStartedState === 'hidden' ? null : ( + + + + ) + } if (id === 'advisor') { return ( From 3afbbe5986e5cd626d6f48f92152081592c8ca85 Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Wed, 10 Sep 2025 23:40:02 -0400 Subject: [PATCH 2/2] fix: client side exception in cron jobs details (#38609) The cron jobs details detail will throw a client side exception while cron jobs are in running status. This is because the types that we've defined for cron job details expect both return_message and end_time to be strings, but they will be null until the cron job is finished. Fixed the types and added some defensive checks to fix the bug. Also made some minor tweaks to the table. Figured that end time and duration can also be shown for failed jobs, doesn't only have to be succeeded ones. --- .../Integrations/CronJobs/PreviousRunsTab.tsx | 81 ++++++++++--------- .../database-cron-jobs-runs-infinite-query.ts | 4 +- 2 files changed, 47 insertions(+), 38 deletions(-) diff --git a/apps/studio/components/interfaces/Integrations/CronJobs/PreviousRunsTab.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/PreviousRunsTab.tsx index 2490a348133c8..9f2e2c492d5b7 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/PreviousRunsTab.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/PreviousRunsTab.tsx @@ -33,30 +33,34 @@ const cronJobColumns = [ minWidth: 200, value: (row: CronJobRun) => (
- - - - {row.return_message} - - - -

Message

- code]:m-0 [&>code>span]:flex [&>code>span]:flex-wrap min-h-11', - '[&>code]:text-xs' - )} - /> -
-
+ {row.return_message ? ( + + + + {row.return_message} + + + +

Message

+ code]:m-0 [&>code>span]:flex [&>code>span]:flex-wrap min-h-11', + '[&>code]:text-xs' + )} + /> +
+
+ ) : ( + - + )}
), }, @@ -78,7 +82,9 @@ const cronJobColumns = [ name: 'End Time', minWidth: 120, value: (row: CronJobRun) => ( -
{row.status === 'succeeded' ? formatDate(row.end_time) : '-'}
+
+ {row.end_time ? formatDate(row.end_time) : '-'} +
), }, @@ -89,7 +95,7 @@ const cronJobColumns = [ value: (row: CronJobRun) => (
- {row.status === 'succeeded' ? calculateDuration(row.start_time, row.end_time) : ''} + {row.start_time && row.end_time ? calculateDuration(row.start_time, row.end_time) : ''}
), @@ -119,16 +125,19 @@ const columns = cronJobColumns.map((col) => { const value = col.value(props.row) if (['start_time', 'end_time'].includes(col.id)) { - const formattedValue = dayjs((props.row as any)[(col as any).id]).valueOf() - return ( -
- -
- ) + const rawValue = (props.row as any)[(col as any).id] + if (rawValue) { + const formattedValue = dayjs(rawValue).valueOf() + return ( +
+ +
+ ) + } } return value diff --git a/apps/studio/data/database-cron-jobs/database-cron-jobs-runs-infinite-query.ts b/apps/studio/data/database-cron-jobs/database-cron-jobs-runs-infinite-query.ts index d7fb6aee2af0f..1556bb1a0faa7 100644 --- a/apps/studio/data/database-cron-jobs/database-cron-jobs-runs-infinite-query.ts +++ b/apps/studio/data/database-cron-jobs/database-cron-jobs-runs-infinite-query.ts @@ -20,9 +20,9 @@ export type CronJobRun = { command: string // statuses https://github.com/citusdata/pg_cron/blob/f5d111117ddc0f4d83a1bad34d61b857681b6720/include/job_metadata.h#L20 status: 'starting' | 'running' | 'sending' | 'connecting' | 'succeeded' | 'failed' - return_message: string + return_message: string | null start_time: string - end_time: string + end_time: string | null } export const CRON_JOB_RUNS_PAGE_SIZE = 30