Update new project flow to add a warning before redirecting (#36349)

* Update new project flow to add a warning before redirecting

* Simplify the logic for picking an org.

* Small UI tweaks

* Readd the alert about a not found org.

* Link to all orgs if slug is undefined

* Nit

---------

Co-authored-by: Ivan Vasilov <vasilov.ivan@gmail.com>
Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
Terry Sutton
2025-06-17 05:25:05 -02:30
committed by GitHub
parent 7e5850bc70
commit 7c678b9db8
6 changed files with 149 additions and 77 deletions

View File

@@ -1,19 +1,30 @@
import InformationBox from 'components/ui/InformationBox'
import { AlertCircle } from 'lucide-react'
interface NotOrganizationOwnerWarningProps {
slug?: string
}
// [Joshen] This can just use NoPermission component i think
const NotOrganizationOwnerWarning = () => {
const NotOrganizationOwnerWarning = ({ slug }: NotOrganizationOwnerWarningProps) => {
return (
<div className="mt-4">
<InformationBox
icon={<AlertCircle className="text-white" size="20" strokeWidth={1.5} />}
icon={<AlertCircle size="20" strokeWidth={1.5} />}
defaultVisibility={true}
hideCollapse
title="You do not have permission to create a project"
description={
<div className="space-y-3">
<p className="text-sm leading-normal">
Contact your organization owner or administrator to create a new project.
{slug ? (
<>
Contact the owner or administrator to create a new project in the{' '}
<code>{slug}</code> organization.
</>
) : (
<>Contact the owner or administrator to create a new project.</>
)}
</p>
</div>
}

View File

@@ -0,0 +1,48 @@
import AlertError from 'components/ui/AlertError'
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { Skeleton } from 'ui'
import { Admonition } from 'ui-patterns/admonition'
import { OrganizationCard } from './OrganizationCard'
export const OrgNotFound = ({ slug }: { slug?: string }) => {
const {
data: organizations,
isSuccess: isOrganizationsSuccess,
isLoading: isOrganizationsLoading,
isError: isOrganizationsError,
error: organizationsError,
} = useOrganizationsQuery()
return (
<>
<Admonition type="danger">
The selected organization does not exist or you don't have permission to access it.{' '}
{slug ? (
<>
Contact the owner or administrator to create a new project in the <code>{slug}</code>{' '}
organization.
</>
) : (
<>Contact the owner or administrator to create a new project.</>
)}
</Admonition>
<h3 className="text-sm">Select an organization to create your new project from</h3>
<div className="grid gap-2 grid-cols-2">
{isOrganizationsLoading && (
<>
<Skeleton className="h-[62px] rounded-md" />
<Skeleton className="h-[62px] rounded-md" />
<Skeleton className="h-[62px] rounded-md" />
</>
)}
{isOrganizationsError && (
<AlertError error={organizationsError} subject="Failed to load organizations" />
)}
{isOrganizationsSuccess &&
organizations?.map((org) => <OrganizationCard key={org.slug} organization={org} />)}
</div>
</>
)
}

View File

@@ -0,0 +1,23 @@
import { Boxes } from 'lucide-react'
import Link from 'next/link'
import { ActionCard } from 'components/ui/ActionCard'
import { useProjectsQuery } from 'data/projects/projects-query'
import { Organization } from 'types'
export const OrganizationCard = ({ organization }: { organization: Organization }) => {
const { data: allProjects = [] } = useProjectsQuery()
const numProjects = allProjects.filter((x) => x.organization_slug === organization.slug).length
return (
<Link href={`/new/${organization.slug}`}>
<ActionCard
bgColor="bg border"
className="[&>div]:items-center"
icon={<Boxes size={18} strokeWidth={1} className="text-foreground" />}
title={organization.name}
description={`${organization.plan.name} Plan${numProjects > 0 ? `${' '}${' '}${numProjects} project${numProjects > 1 ? 's' : ''}` : ''}`}
/>
</Link>
)
}

View File

@@ -45,12 +45,22 @@ export const OrganizationDropdown = () => {
return (
<>
<Link href={`/org/${slug}`} className="flex items-center gap-2 flex-shrink-0 text-sm">
<Link
href={slug ? `/org/${slug}` : '/organizations'}
className="flex items-center gap-2 flex-shrink-0 text-sm"
>
<Boxes size={14} strokeWidth={1.5} className="text-foreground-lighter" />
<span className="text-foreground max-w-32 lg:max-w-none truncate hidden md:block">
{orgName}
<span
className={cn(
'max-w-32 lg:max-w-none truncate hidden md:block',
!!selectedOrganization ? 'text-foreground' : 'text-foreground-lighter'
)}
>
{orgName ?? 'Select an organization'}
</span>
<Badge variant="default">{selectedOrganization?.plan.name}</Badge>
{!!selectedOrganization && (
<Badge variant="default">{selectedOrganization?.plan.name}</Badge>
)}
</Link>
<Popover_Shadcn_ open={open} onOpenChange={setOpen} modal={false}>
<PopoverTrigger_Shadcn_ asChild>

View File

@@ -16,6 +16,7 @@ import {
FreeProjectLimitWarning,
NotOrganizationOwnerWarning,
} from 'components/interfaces/Organization/NewProject'
import { OrgNotFound } from 'components/interfaces/Organization/OrgNotFound'
import { AdvancedConfiguration } from 'components/interfaces/ProjectCreation/AdvancedConfiguration'
import {
extractPostgresVersionDetails,
@@ -140,7 +141,6 @@ const Wizard: NextPageWithLayout = () => {
const { slug, projectName } = useParams()
const currentOrg = useSelectedOrganization()
const isFreePlan = currentOrg?.plan?.id === 'free'
const [lastVisitedOrganization] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.LAST_VISITED_ORGANIZATION,
''
@@ -233,8 +233,11 @@ const Wizard: NextPageWithLayout = () => {
)
const isAdmin = useCheckPermissions(PermissionAction.CREATE, 'projects')
const isInvalidSlug = isOrganizationsSuccess && currentOrg === undefined
const orgNotFound = isOrganizationsSuccess && (organizations?.length ?? 0) > 0 && isInvalidSlug
const isEmptyOrganizations = (organizations?.length ?? 0) <= 0 && isOrganizationsSuccess
const hasMembersExceedingFreeTierLimit = (membersExceededLimit || []).length > 0
const showNonProdFields = process.env.NEXT_PUBLIC_ENVIRONMENT !== 'prod'
@@ -408,14 +411,6 @@ const Wizard: NextPageWithLayout = () => {
if (projectName) form.setValue('projectName', projectName || '')
}, [slug])
useEffect(() => {
// Redirect to first org if the slug doesn't match an org slug
// this is mainly to capture the /new/new-project url, which is redirected from database.new
if (isInvalidSlug && isOrganizationsSuccess && (organizations?.length ?? 0) > 0) {
router.push(`/new/${organizations?.[0].slug}`)
}
}, [isInvalidSlug, isOrganizationsSuccess, organizations])
useEffect(() => {
if (form.getValues('dbRegion') === undefined && defaultRegion) {
form.setValue('dbRegion', defaultRegion)
@@ -537,7 +532,6 @@ const Wizard: NextPageWithLayout = () => {
Total Monthly Compute Costs
{/**
* API currently doesnt output replica information on the projects list endpoint. Until then, we cannot correctly calculate the costs including RRs.
*
* Will be adjusted in the future [kevin]
*/}
{organizationProjects.length > 0 && (
@@ -588,45 +582,49 @@ const Wizard: NextPageWithLayout = () => {
) : (
<div className="divide-y divide-border-muted">
<Panel.Content className={['space-y-4'].join(' ')}>
<FormField_Shadcn_
control={form.control}
name="organization"
render={({ field }) => (
<FormItemLayout label="Organization" layout="horizontal">
{(organizations?.length ?? 0) > 0 && (
<Select_Shadcn_
onValueChange={(slug) => {
field.onChange(slug)
router.push(`/new/${slug}`)
}}
value={field.value}
defaultValue={field.value}
>
<FormControl_Shadcn_>
<SelectTrigger_Shadcn_>
<SelectValue_Shadcn_ placeholder="Select an organization" />
</SelectTrigger_Shadcn_>
</FormControl_Shadcn_>
<SelectContent_Shadcn_>
<SelectGroup_Shadcn_>
{organizations?.map((x) => (
<SelectItem_Shadcn_
key={x.id}
value={x.slug}
className="flex justify-between"
>
<span className="mr-2">{x.name}</span>
<Badge>{x.plan.name}</Badge>
</SelectItem_Shadcn_>
))}
</SelectGroup_Shadcn_>
</SelectContent_Shadcn_>
</Select_Shadcn_>
)}
</FormItemLayout>
)}
/>
{!isAdmin && <NotOrganizationOwnerWarning />}
{isAdmin && !isInvalidSlug && (
<FormField_Shadcn_
control={form.control}
name="organization"
render={({ field }) => (
<FormItemLayout label="Organization" layout="horizontal">
{(organizations?.length ?? 0) > 0 && (
<Select_Shadcn_
onValueChange={(slug) => {
field.onChange(slug)
router.push(`/new/${slug}`)
}}
value={field.value}
defaultValue={field.value}
>
<FormControl_Shadcn_>
<SelectTrigger_Shadcn_>
<SelectValue_Shadcn_ placeholder="Select an organization" />
</SelectTrigger_Shadcn_>
</FormControl_Shadcn_>
<SelectContent_Shadcn_>
<SelectGroup_Shadcn_>
{organizations?.map((x) => (
<SelectItem_Shadcn_
key={x.id}
value={x.slug}
className="flex justify-between"
>
<span className="mr-2">{x.name}</span>
<Badge>{x.plan.name}</Badge>
</SelectItem_Shadcn_>
))}
</SelectGroup_Shadcn_>
</SelectContent_Shadcn_>
</Select_Shadcn_>
)}
</FormItemLayout>
)}
/>
)}
{!isAdmin && !orgNotFound && <NotOrganizationOwnerWarning slug={slug} />}
{orgNotFound && <OrgNotFound slug={slug} />}
</Panel.Content>
{canCreateProject && (

View File

@@ -1,17 +1,16 @@
import { Boxes, Search } from 'lucide-react'
import { Search } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import { useParams } from 'common'
import { OrganizationCard } from 'components/interfaces/Organization/OrganizationCard'
import AppLayout from 'components/layouts/AppLayout/AppLayout'
import DefaultLayout from 'components/layouts/DefaultLayout'
import { ScaffoldContainerLegacy, ScaffoldTitle } from 'components/layouts/Scaffold'
import { ActionCard } from 'components/ui/ActionCard'
import AlertError from 'components/ui/AlertError'
import NoSearchResults from 'components/ui/NoSearchResults'
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { useProjectsQuery } from 'data/projects/projects-query'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { withAuth } from 'hooks/misc/withAuth'
import { NextPageWithLayout } from 'types'
@@ -31,7 +30,6 @@ const OrganizationsPage: NextPageWithLayout = () => {
const { error: orgNotFoundError, org: orgSlug } = useParams()
const orgNotFound = orgNotFoundError === 'org_not_found'
const { data: projects = [] } = useProjectsQuery()
const { data: organizations = [], error, isLoading, isError, isSuccess } = useOrganizationsQuery()
const organizationCreationEnabled = useIsFeatureEnabled('organizations:create')
@@ -105,23 +103,7 @@ const OrganizationsPage: NextPageWithLayout = () => {
)}
{isError && <AlertError error={error} subject="Failed to load organizations" />}
{isSuccess &&
filteredOrganizations.map((organization) => {
const numProjects = projects.filter(
(x) => x.organization_slug === organization.slug
).length
return (
<ActionCard
bgColor="bg border"
className="[&>div]:items-center"
key={organization.id}
icon={<Boxes size={18} strokeWidth={1} className="text-foreground" />}
title={organization.name}
description={`${organization.plan.name} Plan${numProjects > 0 ? `${' '}${' '}${numProjects} project${numProjects > 1 ? 's' : ''}` : ''}`}
onClick={() => router.push(`/org/${organization.slug}`)}
/>
)
})}
filteredOrganizations.map((org) => <OrganizationCard key={org.id} organization={org} />)}
</div>
</ScaffoldContainerLegacy>
)