Skip to content

Commit 7c678b9

Browse files
saltcodivasilovjoshenlim
authored
Update new project flow to add a warning before redirecting (supabase#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 <[email protected]> Co-authored-by: Joshen Lim <[email protected]>
1 parent 7e5850b commit 7c678b9

File tree

6 files changed

+149
-77
lines changed

6 files changed

+149
-77
lines changed

apps/studio/components/interfaces/Organization/NewProject/NotOrganizationOwnerWarning.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,30 @@
11
import InformationBox from 'components/ui/InformationBox'
22
import { AlertCircle } from 'lucide-react'
33

4+
interface NotOrganizationOwnerWarningProps {
5+
slug?: string
6+
}
7+
48
// [Joshen] This can just use NoPermission component i think
5-
const NotOrganizationOwnerWarning = () => {
9+
const NotOrganizationOwnerWarning = ({ slug }: NotOrganizationOwnerWarningProps) => {
610
return (
711
<div className="mt-4">
812
<InformationBox
9-
icon={<AlertCircle className="text-white" size="20" strokeWidth={1.5} />}
13+
icon={<AlertCircle size="20" strokeWidth={1.5} />}
1014
defaultVisibility={true}
1115
hideCollapse
1216
title="You do not have permission to create a project"
1317
description={
1418
<div className="space-y-3">
1519
<p className="text-sm leading-normal">
16-
Contact your organization owner or administrator to create a new project.
20+
{slug ? (
21+
<>
22+
Contact the owner or administrator to create a new project in the{' '}
23+
<code>{slug}</code> organization.
24+
</>
25+
) : (
26+
<>Contact the owner or administrator to create a new project.</>
27+
)}
1728
</p>
1829
</div>
1930
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import AlertError from 'components/ui/AlertError'
2+
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
3+
import { Skeleton } from 'ui'
4+
import { Admonition } from 'ui-patterns/admonition'
5+
import { OrganizationCard } from './OrganizationCard'
6+
7+
export const OrgNotFound = ({ slug }: { slug?: string }) => {
8+
const {
9+
data: organizations,
10+
isSuccess: isOrganizationsSuccess,
11+
isLoading: isOrganizationsLoading,
12+
isError: isOrganizationsError,
13+
error: organizationsError,
14+
} = useOrganizationsQuery()
15+
16+
return (
17+
<>
18+
<Admonition type="danger">
19+
The selected organization does not exist or you don't have permission to access it.{' '}
20+
{slug ? (
21+
<>
22+
Contact the owner or administrator to create a new project in the <code>{slug}</code>{' '}
23+
organization.
24+
</>
25+
) : (
26+
<>Contact the owner or administrator to create a new project.</>
27+
)}
28+
</Admonition>
29+
30+
<h3 className="text-sm">Select an organization to create your new project from</h3>
31+
32+
<div className="grid gap-2 grid-cols-2">
33+
{isOrganizationsLoading && (
34+
<>
35+
<Skeleton className="h-[62px] rounded-md" />
36+
<Skeleton className="h-[62px] rounded-md" />
37+
<Skeleton className="h-[62px] rounded-md" />
38+
</>
39+
)}
40+
{isOrganizationsError && (
41+
<AlertError error={organizationsError} subject="Failed to load organizations" />
42+
)}
43+
{isOrganizationsSuccess &&
44+
organizations?.map((org) => <OrganizationCard key={org.slug} organization={org} />)}
45+
</div>
46+
</>
47+
)
48+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Boxes } from 'lucide-react'
2+
import Link from 'next/link'
3+
4+
import { ActionCard } from 'components/ui/ActionCard'
5+
import { useProjectsQuery } from 'data/projects/projects-query'
6+
import { Organization } from 'types'
7+
8+
export const OrganizationCard = ({ organization }: { organization: Organization }) => {
9+
const { data: allProjects = [] } = useProjectsQuery()
10+
const numProjects = allProjects.filter((x) => x.organization_slug === organization.slug).length
11+
12+
return (
13+
<Link href={`/new/${organization.slug}`}>
14+
<ActionCard
15+
bgColor="bg border"
16+
className="[&>div]:items-center"
17+
icon={<Boxes size={18} strokeWidth={1} className="text-foreground" />}
18+
title={organization.name}
19+
description={`${organization.plan.name} Plan${numProjects > 0 ? `${' '}${' '}${numProjects} project${numProjects > 1 ? 's' : ''}` : ''}`}
20+
/>
21+
</Link>
22+
)
23+
}

apps/studio/components/layouts/AppLayout/OrganizationDropdown.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,22 @@ export const OrganizationDropdown = () => {
4545

4646
return (
4747
<>
48-
<Link href={`/org/${slug}`} className="flex items-center gap-2 flex-shrink-0 text-sm">
48+
<Link
49+
href={slug ? `/org/${slug}` : '/organizations'}
50+
className="flex items-center gap-2 flex-shrink-0 text-sm"
51+
>
4952
<Boxes size={14} strokeWidth={1.5} className="text-foreground-lighter" />
50-
<span className="text-foreground max-w-32 lg:max-w-none truncate hidden md:block">
51-
{orgName}
53+
<span
54+
className={cn(
55+
'max-w-32 lg:max-w-none truncate hidden md:block',
56+
!!selectedOrganization ? 'text-foreground' : 'text-foreground-lighter'
57+
)}
58+
>
59+
{orgName ?? 'Select an organization'}
5260
</span>
53-
<Badge variant="default">{selectedOrganization?.plan.name}</Badge>
61+
{!!selectedOrganization && (
62+
<Badge variant="default">{selectedOrganization?.plan.name}</Badge>
63+
)}
5464
</Link>
5565
<Popover_Shadcn_ open={open} onOpenChange={setOpen} modal={false}>
5666
<PopoverTrigger_Shadcn_ asChild>

apps/studio/pages/new/[slug].tsx

Lines changed: 47 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
FreeProjectLimitWarning,
1717
NotOrganizationOwnerWarning,
1818
} from 'components/interfaces/Organization/NewProject'
19+
import { OrgNotFound } from 'components/interfaces/Organization/OrgNotFound'
1920
import { AdvancedConfiguration } from 'components/interfaces/ProjectCreation/AdvancedConfiguration'
2021
import {
2122
extractPostgresVersionDetails,
@@ -140,7 +141,6 @@ const Wizard: NextPageWithLayout = () => {
140141
const { slug, projectName } = useParams()
141142
const currentOrg = useSelectedOrganization()
142143
const isFreePlan = currentOrg?.plan?.id === 'free'
143-
144144
const [lastVisitedOrganization] = useLocalStorageQuery(
145145
LOCAL_STORAGE_KEYS.LAST_VISITED_ORGANIZATION,
146146
''
@@ -233,8 +233,11 @@ const Wizard: NextPageWithLayout = () => {
233233
)
234234

235235
const isAdmin = useCheckPermissions(PermissionAction.CREATE, 'projects')
236+
236237
const isInvalidSlug = isOrganizationsSuccess && currentOrg === undefined
238+
const orgNotFound = isOrganizationsSuccess && (organizations?.length ?? 0) > 0 && isInvalidSlug
237239
const isEmptyOrganizations = (organizations?.length ?? 0) <= 0 && isOrganizationsSuccess
240+
238241
const hasMembersExceedingFreeTierLimit = (membersExceededLimit || []).length > 0
239242

240243
const showNonProdFields = process.env.NEXT_PUBLIC_ENVIRONMENT !== 'prod'
@@ -408,14 +411,6 @@ const Wizard: NextPageWithLayout = () => {
408411
if (projectName) form.setValue('projectName', projectName || '')
409412
}, [slug])
410413

411-
useEffect(() => {
412-
// Redirect to first org if the slug doesn't match an org slug
413-
// this is mainly to capture the /new/new-project url, which is redirected from database.new
414-
if (isInvalidSlug && isOrganizationsSuccess && (organizations?.length ?? 0) > 0) {
415-
router.push(`/new/${organizations?.[0].slug}`)
416-
}
417-
}, [isInvalidSlug, isOrganizationsSuccess, organizations])
418-
419414
useEffect(() => {
420415
if (form.getValues('dbRegion') === undefined && defaultRegion) {
421416
form.setValue('dbRegion', defaultRegion)
@@ -537,7 +532,6 @@ const Wizard: NextPageWithLayout = () => {
537532
Total Monthly Compute Costs
538533
{/**
539534
* API currently doesnt output replica information on the projects list endpoint. Until then, we cannot correctly calculate the costs including RRs.
540-
*
541535
* Will be adjusted in the future [kevin]
542536
*/}
543537
{organizationProjects.length > 0 && (
@@ -588,45 +582,49 @@ const Wizard: NextPageWithLayout = () => {
588582
) : (
589583
<div className="divide-y divide-border-muted">
590584
<Panel.Content className={['space-y-4'].join(' ')}>
591-
<FormField_Shadcn_
592-
control={form.control}
593-
name="organization"
594-
render={({ field }) => (
595-
<FormItemLayout label="Organization" layout="horizontal">
596-
{(organizations?.length ?? 0) > 0 && (
597-
<Select_Shadcn_
598-
onValueChange={(slug) => {
599-
field.onChange(slug)
600-
router.push(`/new/${slug}`)
601-
}}
602-
value={field.value}
603-
defaultValue={field.value}
604-
>
605-
<FormControl_Shadcn_>
606-
<SelectTrigger_Shadcn_>
607-
<SelectValue_Shadcn_ placeholder="Select an organization" />
608-
</SelectTrigger_Shadcn_>
609-
</FormControl_Shadcn_>
610-
<SelectContent_Shadcn_>
611-
<SelectGroup_Shadcn_>
612-
{organizations?.map((x) => (
613-
<SelectItem_Shadcn_
614-
key={x.id}
615-
value={x.slug}
616-
className="flex justify-between"
617-
>
618-
<span className="mr-2">{x.name}</span>
619-
<Badge>{x.plan.name}</Badge>
620-
</SelectItem_Shadcn_>
621-
))}
622-
</SelectGroup_Shadcn_>
623-
</SelectContent_Shadcn_>
624-
</Select_Shadcn_>
625-
)}
626-
</FormItemLayout>
627-
)}
628-
/>
629-
{!isAdmin && <NotOrganizationOwnerWarning />}
585+
{isAdmin && !isInvalidSlug && (
586+
<FormField_Shadcn_
587+
control={form.control}
588+
name="organization"
589+
render={({ field }) => (
590+
<FormItemLayout label="Organization" layout="horizontal">
591+
{(organizations?.length ?? 0) > 0 && (
592+
<Select_Shadcn_
593+
onValueChange={(slug) => {
594+
field.onChange(slug)
595+
router.push(`/new/${slug}`)
596+
}}
597+
value={field.value}
598+
defaultValue={field.value}
599+
>
600+
<FormControl_Shadcn_>
601+
<SelectTrigger_Shadcn_>
602+
<SelectValue_Shadcn_ placeholder="Select an organization" />
603+
</SelectTrigger_Shadcn_>
604+
</FormControl_Shadcn_>
605+
<SelectContent_Shadcn_>
606+
<SelectGroup_Shadcn_>
607+
{organizations?.map((x) => (
608+
<SelectItem_Shadcn_
609+
key={x.id}
610+
value={x.slug}
611+
className="flex justify-between"
612+
>
613+
<span className="mr-2">{x.name}</span>
614+
<Badge>{x.plan.name}</Badge>
615+
</SelectItem_Shadcn_>
616+
))}
617+
</SelectGroup_Shadcn_>
618+
</SelectContent_Shadcn_>
619+
</Select_Shadcn_>
620+
)}
621+
</FormItemLayout>
622+
)}
623+
/>
624+
)}
625+
626+
{!isAdmin && !orgNotFound && <NotOrganizationOwnerWarning slug={slug} />}
627+
{orgNotFound && <OrgNotFound slug={slug} />}
630628
</Panel.Content>
631629

632630
{canCreateProject && (

apps/studio/pages/organizations.tsx

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
1-
import { Boxes, Search } from 'lucide-react'
1+
import { Search } from 'lucide-react'
22
import Link from 'next/link'
33
import { useRouter } from 'next/router'
44
import { useEffect, useState } from 'react'
55

66
import { useParams } from 'common'
7+
import { OrganizationCard } from 'components/interfaces/Organization/OrganizationCard'
78
import AppLayout from 'components/layouts/AppLayout/AppLayout'
89
import DefaultLayout from 'components/layouts/DefaultLayout'
910
import { ScaffoldContainerLegacy, ScaffoldTitle } from 'components/layouts/Scaffold'
10-
import { ActionCard } from 'components/ui/ActionCard'
1111
import AlertError from 'components/ui/AlertError'
1212
import NoSearchResults from 'components/ui/NoSearchResults'
1313
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
14-
import { useProjectsQuery } from 'data/projects/projects-query'
1514
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
1615
import { withAuth } from 'hooks/misc/withAuth'
1716
import { NextPageWithLayout } from 'types'
@@ -31,7 +30,6 @@ const OrganizationsPage: NextPageWithLayout = () => {
3130
const { error: orgNotFoundError, org: orgSlug } = useParams()
3231
const orgNotFound = orgNotFoundError === 'org_not_found'
3332

34-
const { data: projects = [] } = useProjectsQuery()
3533
const { data: organizations = [], error, isLoading, isError, isSuccess } = useOrganizationsQuery()
3634

3735
const organizationCreationEnabled = useIsFeatureEnabled('organizations:create')
@@ -105,23 +103,7 @@ const OrganizationsPage: NextPageWithLayout = () => {
105103
)}
106104
{isError && <AlertError error={error} subject="Failed to load organizations" />}
107105
{isSuccess &&
108-
filteredOrganizations.map((organization) => {
109-
const numProjects = projects.filter(
110-
(x) => x.organization_slug === organization.slug
111-
).length
112-
113-
return (
114-
<ActionCard
115-
bgColor="bg border"
116-
className="[&>div]:items-center"
117-
key={organization.id}
118-
icon={<Boxes size={18} strokeWidth={1} className="text-foreground" />}
119-
title={organization.name}
120-
description={`${organization.plan.name} Plan${numProjects > 0 ? `${' '}${' '}${numProjects} project${numProjects > 1 ? 's' : ''}` : ''}`}
121-
onClick={() => router.push(`/org/${organization.slug}`)}
122-
/>
123-
)
124-
})}
106+
filteredOrganizations.map((org) => <OrganizationCard key={org.id} organization={org} />)}
125107
</div>
126108
</ScaffoldContainerLegacy>
127109
)

0 commit comments

Comments
 (0)