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 ( 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