From 7c678b9db8f4bf30fe7c66be086395c7cc624844 Mon Sep 17 00:00:00 2001 From: Terry Sutton Date: Tue, 17 Jun 2025 05:25:05 -0230 Subject: [PATCH] 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 Co-authored-by: Joshen Lim --- .../NotOrganizationOwnerWarning.tsx | 17 +++- .../interfaces/Organization/OrgNotFound.tsx | 48 ++++++++++ .../Organization/OrganizationCard.tsx | 23 +++++ .../AppLayout/OrganizationDropdown.tsx | 18 +++- apps/studio/pages/new/[slug].tsx | 96 +++++++++---------- apps/studio/pages/organizations.tsx | 24 +---- 6 files changed, 149 insertions(+), 77 deletions(-) create mode 100644 apps/studio/components/interfaces/Organization/OrgNotFound.tsx create mode 100644 apps/studio/components/interfaces/Organization/OrganizationCard.tsx diff --git a/apps/studio/components/interfaces/Organization/NewProject/NotOrganizationOwnerWarning.tsx b/apps/studio/components/interfaces/Organization/NewProject/NotOrganizationOwnerWarning.tsx index 5a2cf43acc..a74dcafae7 100644 --- a/apps/studio/components/interfaces/Organization/NewProject/NotOrganizationOwnerWarning.tsx +++ b/apps/studio/components/interfaces/Organization/NewProject/NotOrganizationOwnerWarning.tsx @@ -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 (
} + icon={} defaultVisibility={true} hideCollapse title="You do not have permission to create a project" description={

- Contact your organization owner or administrator to create a new project. + {slug ? ( + <> + Contact the owner or administrator to create a new project in the{' '} + {slug} organization. + + ) : ( + <>Contact the owner or administrator to create a new project. + )}

} diff --git a/apps/studio/components/interfaces/Organization/OrgNotFound.tsx b/apps/studio/components/interfaces/Organization/OrgNotFound.tsx new file mode 100644 index 0000000000..7aa3241555 --- /dev/null +++ b/apps/studio/components/interfaces/Organization/OrgNotFound.tsx @@ -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 ( + <> + + 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 {slug}{' '} + organization. + + ) : ( + <>Contact the owner or administrator to create a new project. + )} + + +

Select an organization to create your new project from

+ +
+ {isOrganizationsLoading && ( + <> + + + + + )} + {isOrganizationsError && ( + + )} + {isOrganizationsSuccess && + organizations?.map((org) => )} +
+ + ) +} diff --git a/apps/studio/components/interfaces/Organization/OrganizationCard.tsx b/apps/studio/components/interfaces/Organization/OrganizationCard.tsx new file mode 100644 index 0000000000..ba0e439cee --- /dev/null +++ b/apps/studio/components/interfaces/Organization/OrganizationCard.tsx @@ -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 ( + + } + title={organization.name} + description={`${organization.plan.name} Plan${numProjects > 0 ? `${' '}•${' '}${numProjects} project${numProjects > 1 ? 's' : ''}` : ''}`} + /> + + ) +} diff --git a/apps/studio/components/layouts/AppLayout/OrganizationDropdown.tsx b/apps/studio/components/layouts/AppLayout/OrganizationDropdown.tsx index b6131c9910..62ca07f041 100644 --- a/apps/studio/components/layouts/AppLayout/OrganizationDropdown.tsx +++ b/apps/studio/components/layouts/AppLayout/OrganizationDropdown.tsx @@ -45,12 +45,22 @@ export const OrganizationDropdown = () => { return ( <> - + - - {orgName} + - {selectedOrganization?.plan.name} + {!!selectedOrganization && ( + {selectedOrganization?.plan.name} + )} diff --git a/apps/studio/pages/new/[slug].tsx b/apps/studio/pages/new/[slug].tsx index a989aa3c47..44b45fd77b 100644 --- a/apps/studio/pages/new/[slug].tsx +++ b/apps/studio/pages/new/[slug].tsx @@ -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 = () => { ) : (
- ( - - {(organizations?.length ?? 0) > 0 && ( - { - field.onChange(slug) - router.push(`/new/${slug}`) - }} - value={field.value} - defaultValue={field.value} - > - - - - - - - - {organizations?.map((x) => ( - - {x.name} - {x.plan.name} - - ))} - - - - )} - - )} - /> - {!isAdmin && } + {isAdmin && !isInvalidSlug && ( + ( + + {(organizations?.length ?? 0) > 0 && ( + { + field.onChange(slug) + router.push(`/new/${slug}`) + }} + value={field.value} + defaultValue={field.value} + > + + + + + + + + {organizations?.map((x) => ( + + {x.name} + {x.plan.name} + + ))} + + + + )} + + )} + /> + )} + + {!isAdmin && !orgNotFound && } + {orgNotFound && } {canCreateProject && ( diff --git a/apps/studio/pages/organizations.tsx b/apps/studio/pages/organizations.tsx index e95006b29b..9d2e6fd3c2 100644 --- a/apps/studio/pages/organizations.tsx +++ b/apps/studio/pages/organizations.tsx @@ -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 && } {isSuccess && - filteredOrganizations.map((organization) => { - const numProjects = projects.filter( - (x) => x.organization_slug === organization.slug - ).length - - return ( - } - title={organization.name} - description={`${organization.plan.name} Plan${numProjects > 0 ? `${' '}•${' '}${numProjects} project${numProjects > 1 ? 's' : ''}` : ''}`} - onClick={() => router.push(`/org/${organization.slug}`)} - /> - ) - })} + filteredOrganizations.map((org) => )}
)