From c111fbab71ea8fba9cbacaf97b06393202298a88 Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Fri, 10 Oct 2025 10:30:24 +0800 Subject: [PATCH 1/6] Opt to silently swallow 404 error when creating new snippet (#39389) * Opt to silently swallow 404 error when creating new snippet * Update comment * Simplify comment --- apps/studio/pages/project/[ref]/sql/[id].tsx | 23 ++++++++++++++++---- apps/studio/state/sql-editor-v2.ts | 4 +++- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/apps/studio/pages/project/[ref]/sql/[id].tsx b/apps/studio/pages/project/[ref]/sql/[id].tsx index b7c6c7fca393b..307a973517b53 100644 --- a/apps/studio/pages/project/[ref]/sql/[id].tsx +++ b/apps/studio/pages/project/[ref]/sql/[id].tsx @@ -2,6 +2,7 @@ import Link from 'next/link' import { useRouter } from 'next/router' import { useEffect } from 'react' +import { usePrevious } from '@uidotdev/usehooks' import { useParams } from 'common/hooks/useParams' import { SQLEditor } from 'components/interfaces/SQLEditor/SQLEditor' import DefaultLayout from 'components/layouts/DefaultLayout' @@ -22,6 +23,7 @@ import { Admonition } from 'ui-patterns' const SqlEditor: NextPageWithLayout = () => { const router = useRouter() const { id, ref, content, skip } = useParams() + const previousRoute = usePrevious(id) const { data: project } = useSelectedProjectQuery() const editor = useEditorType() @@ -34,8 +36,6 @@ const SqlEditor: NextPageWithLayout = () => { const tabId = !!id ? tabs.openTabs.find((x) => x.endsWith(id)) : undefined - // [Refactor] There's an unnecessary request getting triggered when we start typing while on /new - // the URL ID gets updated and we attempt to fetch content for a snippet that's not been created yet // [Joshen] May need to investigate separately, but occasionally addSnippet doesnt exist in // the snapV2 valtio store for some reason hence why the added typeof check here const canFetchContentBasedOnId = Boolean( @@ -53,6 +53,14 @@ const SqlEditor: NextPageWithLayout = () => { isError && error.code === 404 && error.message.includes('Content not found') const invalidId = isError && error.code === 400 && error.message.includes('Invalid uuid') + // [Joshen] Atm we suspect that replication lag is causing this to happen whereby a newly created snippet + // shows the "Unable to find snippet" error which blocks the whole UI + // Am opting to silently swallow this error, since the saves are still going through and we're scoping this behaviour + // behaviour down to a very specific use case too with all these conditionals + // More details: https://github.com/supabase/supabase/pull/39389 + const snippetMissingImmediatelyAfterCreating = + !!snippet && snippetMissing && previousRoute === 'new' && 'isNotSavedInDatabaseYet' in snippet + useEffect(() => { if (ref && data && project) { // [Joshen] Check if snippet belongs to the current project @@ -63,6 +71,7 @@ const SqlEditor: NextPageWithLayout = () => { router.push(`/project/${ref}/sql/new`) } } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ref, data, project]) // Load the last visited snippet when landing on /new @@ -95,9 +104,10 @@ const SqlEditor: NextPageWithLayout = () => { name: snippet?.name, }, }) + // eslint-disable-next-line react-hooks/exhaustive-deps }, [router.isReady, id]) - if (snippetMissing || invalidId) { + if ((snippetMissing || invalidId) && !snippetMissingImmediatelyAfterCreating) { return (
@@ -122,7 +132,12 @@ const SqlEditor: NextPageWithLayout = () => { Close tab ) : ( - )} 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' From 8e9d3de3bca09088c5ba524678aa6c99c6467c6a Mon Sep 17 00:00:00 2001 From: Joshen Lim Date: Fri, 10 Oct 2025 10:56:28 +0800 Subject: [PATCH 2/6] chore/projects pagination part 06 (#39351) * CmdK OrgProjectSwitcher to swap useProjectsQuery with useProjectsInfiniteQuery * Remove usage of useProjectsQuery in ProjectDropdown * Remove usage of useProjectsQuery in NotificationsPopover * Remove usage of useProjectsQuery in NotificationsFilter * Remove usage of useProjectsQuery from LoadingState * Clean * Remove usage of getProjects from org-ai-details and fix missing key props in AIOnboarding * Remove useAutoProjectsPrefetch from org/[slug]/index * Fix TS + clean up * Clean * Remove usage of useProjectsQuery in NewOrgForm * Remove usage of useProjectsQuery in SupportForm -> AIAssistantOption * Remove usage of useProjectsQuery in PlanUpdateSidePanel * Remove usage of useProjectsQuery in NoProjectsOnPaidPlan * Remove useProjectsQuery in IntegrationPanels * Remove useProjectsQuery from IntegrationPanels 2 * Remove useProjectsQuery from IntegrationConnection * Remove useProjectsQuery from ProjectLinker, SidePanelVercelProjectLinker, and choose-project * Clean up * Rename a state to be in the same style. * Remove button ref, it was used to match the width. Use sameWidthAsTrigger. * Remove console.log. * Remove hardcoded width. --------- Co-authored-by: Ivan Vasilov --- .../VercelGithub/ProjectLinker.tsx | 221 +++++++++--------- .../SidePanelVercelProjectLinker.tsx | 18 +- .../[slug]/marketplace/choose-project.tsx | 36 +-- 3 files changed, 118 insertions(+), 157 deletions(-) diff --git a/apps/studio/components/interfaces/Integrations/VercelGithub/ProjectLinker.tsx b/apps/studio/components/interfaces/Integrations/VercelGithub/ProjectLinker.tsx index 8a3569fa3d46c..84ce0b010284a 100644 --- a/apps/studio/components/interfaces/Integrations/VercelGithub/ProjectLinker.tsx +++ b/apps/studio/components/interfaces/Integrations/VercelGithub/ProjectLinker.tsx @@ -1,18 +1,24 @@ -import { ChevronDown, PlusIcon } from 'lucide-react' +import { Check, ChevronDown, Plus, PlusIcon } from 'lucide-react' import { useRouter } from 'next/router' -import { ReactNode, useEffect, useRef, useState } from 'react' +import { ReactNode, useEffect, useState } from 'react' import { toast } from 'sonner' +import { OrganizationProjectSelector } from 'components/ui/OrganizationProjectSelector' import ShimmerLine from 'components/ui/ShimmerLine' import { IntegrationConnectionsCreateVariables, IntegrationProjectConnection, } from 'data/integrations/integrations.types' +import { useOrgProjectsInfiniteQuery } from 'data/projects/org-projects-infinite-query' +import { useProjectDetailQuery } from 'data/projects/project-detail-query' +import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { BASE_PATH } from 'lib/constants' import { openInstallGitHubIntegrationWindow } from 'lib/github' import { EMPTY_ARR } from 'lib/void' +import Link from 'next/link' import { + Badge, Button, CommandEmpty_Shadcn_, CommandGroup_Shadcn_, @@ -39,9 +45,9 @@ export interface ForeignProject { } export interface ProjectLinkerProps { + slug?: string organizationIntegrationId?: string foreignProjects: ForeignProject[] - supabaseProjects: Project[] onCreateConnections: (variables: IntegrationConnectionsCreateVariables) => void installedConnections?: IntegrationProjectConnection[] isLoading?: boolean @@ -50,7 +56,6 @@ export interface ProjectLinkerProps { choosePrompt?: string onSkip?: () => void loadingForeignProjects?: boolean - loadingSupabaseProjects?: boolean showNoEntitiesState?: boolean defaultSupabaseProjectRef?: string @@ -59,9 +64,9 @@ export interface ProjectLinkerProps { } const ProjectLinker = ({ + slug, organizationIntegrationId, foreignProjects, - supabaseProjects, onCreateConnections: _onCreateConnections, installedConnections = EMPTY_ARR, isLoading, @@ -70,7 +75,6 @@ const ProjectLinker = ({ choosePrompt = 'Choose a project', onSkip, loadingForeignProjects, - loadingSupabaseProjects, showNoEntitiesState = true, defaultSupabaseProjectRef, @@ -78,24 +82,28 @@ const ProjectLinker = ({ mode, }: ProjectLinkerProps) => { const router = useRouter() - const [supabaseProjectsComboBoxOpen, setSupabaseProjectsComboboxOpen] = useState(false) - const [foreignProjectsComboBoxOpen, setForeignProjectsComboboxOpen] = useState(false) - const supabaseProjectsComboBoxRef = useRef(null) - const foreignProjectsComboBoxRef = useRef(null) - - const { data: selectedOrganization } = useSelectedOrganizationQuery() + const projectCreationEnabled = useIsFeatureEnabled('projects:create') + const [openProjectsDropdown, setOpenProjectsDropdown] = useState(false) + const [openForeignProjectsComboBox, setOpenForeignProjectsComboBox] = useState(false) + const [foreignProjectId, setForeignProjectId] = useState( + defaultForeignProjectId + ) const [supabaseProjectRef, setSupabaseProjectRef] = useState( defaultSupabaseProjectRef ) + + const { data: selectedOrganization } = useSelectedOrganizationQuery() + const { data: orgProjects, isLoading: loadingSupabaseProjects } = useOrgProjectsInfiniteQuery({ + slug, + }) + const numProjects = orgProjects?.pages[0].pagination.count ?? 0 + useEffect(() => { if (defaultSupabaseProjectRef !== undefined && supabaseProjectRef === undefined) setSupabaseProjectRef(defaultSupabaseProjectRef) }, [defaultSupabaseProjectRef, supabaseProjectRef]) - const [foreignProjectId, setForeignProjectId] = useState( - defaultForeignProjectId - ) useEffect(() => { if (defaultForeignProjectId !== undefined && foreignProjectId === undefined) setForeignProjectId(defaultForeignProjectId) @@ -104,9 +112,7 @@ const ProjectLinker = ({ // create a flat array of foreign project ids. ie, ["prj_MlkO6AiLG5ofS9ojKrkS3PhhlY3f", ..] const flatInstalledConnectionsIds = new Set(installedConnections.map((x) => x.foreign_project_id)) - const selectedSupabaseProject = supabaseProjectRef - ? supabaseProjects.find((x) => x.ref?.toLowerCase() === supabaseProjectRef?.toLowerCase()) - : undefined + const { data: selectedSupabaseProject } = useProjectDetailQuery({ ref: supabaseProjectRef }) const selectedForeignProject = foreignProjectId ? foreignProjects.find((x) => x.id?.toLowerCase() === foreignProjectId?.toLowerCase()) @@ -158,7 +164,7 @@ const ProjectLinker = ({ ) } - const noSupabaseProjects = supabaseProjects.length === 0 + const noSupabaseProjects = numProjects === 0 const noForeignProjects = foreignProjects.length === 0 const missingEntity = noSupabaseProjects ? 'Supabase' : mode const oppositeMissingEntity = noSupabaseProjects ? mode : 'Supabase' @@ -171,7 +177,7 @@ const ProjectLinker = ({ style={{ backgroundPosition: '10px 10px' }} /> - {loadingForeignProjects || loadingSupabaseProjects ? ( + {loadingForeignProjects ? (

Loading projects

@@ -193,94 +199,94 @@ const ProjectLinker = ({ Supabase
- - - - - - - - - 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/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/Integrations/CronJobs/CronJobTableCell.tsx b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobTableCell.tsx index b7017e8684a20..15b5ddaad32f2 100644 --- a/apps/studio/components/interfaces/Integrations/CronJobs/CronJobTableCell.tsx +++ b/apps/studio/components/interfaces/Integrations/CronJobs/CronJobTableCell.tsx @@ -1,6 +1,6 @@ import parser from 'cron-parser' import dayjs from 'dayjs' -import { Clipboard, Edit, MoreVertical, Play, Trash } from 'lucide-react' +import { Copy, Edit, MoreVertical, Play, Trash } from 'lucide-react' import { parseAsString, useQueryState } from 'nuqs' import { useState } from 'react' import { toast } from 'sonner' @@ -295,7 +295,7 @@ export const CronJobTableCell = ({ onFocusCapture={(e) => e.stopPropagation()} onSelect={() => copyToClipboard(formattedValue)} > - + Copy {col.name.toLowerCase()} diff --git a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/Results.tsx b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/Results.tsx index 32a96d761077b..222fb6ab9314c 100644 --- a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/Results.tsx +++ b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/Results.tsx @@ -1,4 +1,4 @@ -import { Clipboard, Expand } from 'lucide-react' +import { Copy, Expand } from 'lucide-react' import { useState } from 'react' import DataGrid, { CalculatedColumn } from 'react-data-grid' @@ -53,7 +53,7 @@ const Results = ({ rows }: { rows: readonly any[] }) => { }} onFocusCapture={(e) => e.stopPropagation()} > - + Copy cell content { onSelect={() => setExpandCell(true)} onFocusCapture={(e) => e.stopPropagation()} > - + View cell content diff --git a/apps/studio/components/interfaces/Settings/Logs/LogSelection.tsx b/apps/studio/components/interfaces/Settings/Logs/LogSelection.tsx index ffd19f2e298ca..2dcb41b37c715 100644 --- a/apps/studio/components/interfaces/Settings/Logs/LogSelection.tsx +++ b/apps/studio/components/interfaces/Settings/Logs/LogSelection.tsx @@ -1,4 +1,4 @@ -import { Check, Clipboard, MousePointerClick, X } from 'lucide-react' +import { Check, Copy, MousePointerClick, X } from 'lucide-react' import { useEffect, useState } from 'react' import { ButtonTooltip } from 'components/ui/ButtonTooltip' @@ -104,20 +104,16 @@ const LogSelection = ({ log, onClose, queryType, isLoading, error }: LogSelectio type="text" tooltip={{ content: { + side: 'left', text: isLoading ? 'Loading log...' : 'Copy as JSON', }, }} + icon={showCopied ? : } onClick={() => { setShowCopied(true) copyToClipboard(JSON.stringify(log, null, 2)) }} - > - {showCopied ? ( - - ) : ( - - )} - + />
} - arrow={} + arrow={} > setView(STORAGE_VIEWS.COLUMNS)}> As columns @@ -90,11 +90,11 @@ export const ColumnContextMenu = ({ id = '' }: ColumnContextMenuProps) => { - + Sort by } - arrow={} + arrow={} > setSortBy(STORAGE_SORT_BY.NAME)}> Name @@ -112,11 +112,11 @@ export const ColumnContextMenu = ({ id = '' }: ColumnContextMenuProps) => { - + Sort by order } - arrow={} + arrow={} > setSortByOrder(STORAGE_SORT_BY_ORDER.ASC)}> Ascending diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRow.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRow.tsx index 467af3341c71c..4634275532aa7 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRow.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/FileExplorerRow.tsx @@ -2,7 +2,7 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { find, isEmpty, isEqual } from 'lodash' import { AlertCircle, - Clipboard, + Copy, Download, Edit, File, @@ -183,7 +183,7 @@ export const FileExplorerRow: ItemRenderer = }, { name: 'Copy path to folder', - icon: , + icon: , onClick: () => copyPathToFolder(openedFolders, itemWithColumnIndex), }, ...(canUpdateFiles @@ -204,14 +204,14 @@ export const FileExplorerRow: ItemRenderer = ? [ { name: 'Get URL', - icon: , + icon: , onClick: () => onCopyUrl(itemWithColumnIndex.name), }, ] : [ { name: 'Get URL', - icon: , + icon: , children: [ { name: 'Expire in 1 week', diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/FolderContextMenu.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/FolderContextMenu.tsx index c91c498c0bbb1..b52a18bb28256 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/FolderContextMenu.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/FolderContextMenu.tsx @@ -1,5 +1,5 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import { Clipboard, Download, Edit, Trash2 } from 'lucide-react' +import { Copy, Download, Edit, Trash2 } from 'lucide-react' import { Item, Menu, Separator } from 'react-contexify' import 'react-contexify/dist/ReactContexify.css' @@ -20,22 +20,22 @@ export const FolderContextMenu = ({ id = '' }: FolderContextMenuProps) => { {canUpdateFiles && ( setSelectedItemToRename(props.item)}> - + Rename )} downloadFolder(props.item)}> - + Download copyPathToFolder(openedFolders, props.item)}> - + Copy path to folder {canUpdateFiles && [ , setSelectedItemsToDelete([props.item])}> - + Delete , ]} diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/ItemContextMenu.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/ItemContextMenu.tsx index 359a35878c7f3..62ebcaa51a34f 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/ItemContextMenu.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/ItemContextMenu.tsx @@ -1,5 +1,5 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import { ChevronRight, Clipboard, Download, Edit, Move, Trash2 } from 'lucide-react' +import { ChevronRight, Copy, Download, Edit, Move, Trash2 } from 'lucide-react' import { Item, Menu, Separator, Submenu } from 'react-contexify' import 'react-contexify/dist/ReactContexify.css' @@ -51,18 +51,18 @@ export const ItemContextMenu = ({ id = '' }: ItemContextMenuProps) => { {isPublic ? ( onHandleClick('copy', props.item)}> - + Get URL ) : ( - + Get URL } - arrow={} + arrow={} > onHandleClick('copy', props.item, URL_EXPIRY_DURATION.WEEK)} @@ -86,20 +86,20 @@ export const ItemContextMenu = ({ id = '' }: ItemContextMenuProps) => { )} {canUpdateFiles && [ onHandleClick('rename', props.item)}> - + Rename , onHandleClick('move', props.item)}> - + Move , onHandleClick('download', props.item)}> - + Download , , setSelectedItemsToDelete([props.item])}> - + Delete , ]} diff --git a/apps/studio/components/interfaces/Storage/StorageExplorer/PreviewPane.tsx b/apps/studio/components/interfaces/Storage/StorageExplorer/PreviewPane.tsx index d4cdb53dab5ec..8305d8f739f50 100644 --- a/apps/studio/components/interfaces/Storage/StorageExplorer/PreviewPane.tsx +++ b/apps/studio/components/interfaces/Storage/StorageExplorer/PreviewPane.tsx @@ -1,7 +1,7 @@ import { Transition } from '@headlessui/react' import { PermissionAction } from '@supabase/shared-types/out/constants' import { isEmpty } from 'lodash' -import { AlertCircle, ChevronDown, Clipboard, Download, Loader, Trash2, X } from 'lucide-react' +import { AlertCircle, ChevronDown, Copy, Download, Loader, Trash2, X } from 'lucide-react' import SVG from 'react-inlinesvg' import { useParams } from 'common' @@ -202,7 +202,7 @@ export const PreviewPane = () => {
- } - 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 )} - {showArrows && canScrollRight && ( + {showArrows && canScrollRight && hasContentToScroll && (