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:
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user