diff --git a/apps/studio/components/interfaces/Organization/ProjectClaim/choose-org.tsx b/apps/studio/components/interfaces/Organization/ProjectClaim/choose-org.tsx index fd8ff8217f56e..74a0d46081cbd 100644 --- a/apps/studio/components/interfaces/Organization/ProjectClaim/choose-org.tsx +++ b/apps/studio/components/interfaces/Organization/ProjectClaim/choose-org.tsx @@ -1,9 +1,8 @@ import { OrganizationSelector } from 'components/ui/org-selector' -import { Organization } from 'types' import { ProjectClaimLayout } from './layout' export interface ProjectClaimChooseOrgProps { - onChoose: (org: Organization) => void + onChoose: (orgSlug: string) => void } const MAX_ORGS_TO_SHOW = 5 diff --git a/apps/studio/components/interfaces/Organization/ProjectClaim/confirm.tsx b/apps/studio/components/interfaces/Organization/ProjectClaim/confirm.tsx index 12ec20e15df1c..dc9f83dfef30d 100644 --- a/apps/studio/components/interfaces/Organization/ProjectClaim/confirm.tsx +++ b/apps/studio/components/interfaces/Organization/ProjectClaim/confirm.tsx @@ -41,13 +41,14 @@ export const ProjectClaimConfirm = ({ const queryClient = useQueryClient() const { mutateAsync: approveRequest, isLoading: isApproving } = - useApiAuthorizationApproveMutation() + useApiAuthorizationApproveMutation({ onError: () => {} }) const { mutateAsync: claimProject, isLoading: isClaiming } = useOrganizationProjectClaimMutation() const onClaimProject = async () => { try { const response = await approveRequest({ id: auth_id!, slug: selectedOrganization.slug }) + await claimProject({ slug: selectedOrganization.slug, token: claimToken!, diff --git a/apps/studio/components/ui/org-selector.tsx b/apps/studio/components/ui/org-selector.tsx index 94fc1c6ad07cc..17c0e31513d9f 100644 --- a/apps/studio/components/ui/org-selector.tsx +++ b/apps/studio/components/ui/org-selector.tsx @@ -3,16 +3,87 @@ import Link from 'next/link' import { useMemo, useState } from 'react' import ShimmeringLoader from 'components/ui/ShimmeringLoader' +import { useFreeProjectLimitCheckQuery } from 'data/organizations/free-project-limit-check-query' import { useOrganizationsQuery } from 'data/organizations/organizations-query' import { parseAsString, useQueryState } from 'nuqs' import { Organization } from 'types' import { Badge, Button, Card, CardHeader, CardTitle, Input_Shadcn_ } from 'ui' +import { ButtonTooltip } from './ButtonTooltip' export interface ProjectClaimChooseOrgProps { - onSelect: (org: Organization) => void + onSelect: (orgSlug: string) => void maxOrgsToShow?: number } +const OrganizationCard = ({ + org, + onSelect, +}: { + org: Organization + onSelect: (orgSlug: string) => void +}) => { + const isFreePlan = org.plan?.id === 'free' + const { data: membersExceededLimit, isSuccess } = useFreeProjectLimitCheckQuery( + { slug: org.slug }, + { enabled: isFreePlan } + ) + const hasMembersExceedingFreeTierLimit = (membersExceededLimit || []).length > 0 + const freePlanWithExceedingLimits = isFreePlan && hasMembersExceedingFreeTierLimit + + return ( + + + + + {org.name} + + {org.plan?.name} + + +

+ The following members have reached their maximum limits for the number of + active free plan projects within organizations where they are an administrator + or owner: +

+
    + {membersExceededLimit.map((member, idx: number) => ( +
  • + {member.username || member.primary_email} (Limit:{' '} + {member.free_project_limit} free projects) +
  • + ))} +
+

+ These members will need to either delete, pause, or upgrade one or more of + these projects before you're able to create a free project within this + organization. +

+ + ) : undefined, + }, + }} + size="small" + onClick={() => { + onSelect(org.slug) + }} + className="shrink-0" + disabled={isSuccess && freePlanWithExceedingLimits} + > + Choose +
+
+
+ ) +} + export function OrganizationSelector({ onSelect, maxOrgsToShow = 5 }: ProjectClaimChooseOrgProps) { const { data: organizations = [], @@ -43,6 +114,11 @@ export function OrganizationSelector({ onSelect, maxOrgsToShow = 5 }: ProjectCla searchParams.set('returnTo', pathname) + const onSelectOrg = (orgSlug: string) => { + onSelect(orgSlug) + setSearch('') + } + return (
{isLoadingOrgs ? ( @@ -66,29 +142,7 @@ export function OrganizationSelector({ onSelect, maxOrgsToShow = 5 }: ProjectCla
No organizations found.
)} {filteredOrgs.map((org) => ( - - - - - {org.name} - - {org.plan?.name} - - - - + ))} {organizations.length > maxOrgsToShow && !showAll && !search && (
diff --git a/apps/studio/pages/claim-project.tsx b/apps/studio/pages/claim-project.tsx index 521f3959f23ae..e3a496a4e7cf1 100644 --- a/apps/studio/pages/claim-project.tsx +++ b/apps/studio/pages/claim-project.tsx @@ -13,7 +13,7 @@ import { useOrganizationsQuery } from 'data/organizations/organizations-query' import { useCustomContent } from 'hooks/custom-content/useCustomContent' import { withAuth } from 'hooks/misc/withAuth' import type { NextPageWithLayout } from 'types' -import { Admonition } from 'ui-patterns' +import { Admonition } from 'ui-patterns/admonition' const ClaimProjectPageLayout = ({ children }: PropsWithChildren) => { const { appTitle } = useCustomContent(['app:title']) @@ -92,8 +92,8 @@ const ClaimProjectPage: NextPageWithLayout = () => { if (step === 'choose-org' || !selectedOrganization) { return ( { - setSelectedOrgSlug(org.slug) + onChoose={(orgSlug) => { + setSelectedOrgSlug(orgSlug) setStep('benefits') }} />