mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
## What kind of change does this PR introduce? Mechanical cleanup on top of the Button default-variant change (#50160). ## What is the current behavior? Many callsites still pass `variant="default"` even though that is now the component default. ## What is the new behavior? Removes redundant static `variant="default"` from legacy `Button` and `ButtonTooltip` callsites. Keeps explicit defaults where they document the API: - `button-default.tsx` and `button-sizes.tsx` demos - `DocsButton`, which pins neutral styling at the wrapper boundary ## To test Studio: - [Auth → Rate Limits](https://studio-staging-2s957kwc4-supabase.vercel.app/dashboard/project/_/auth/rate-limits): dirty the form so Cancel appears; Cancel stays neutral, Save stays green - [Project Settings → API Keys](https://studio-staging-2s957kwc4-supabase.vercel.app/dashboard/project/_/settings/api-keys): `DocsButton` in the header actions stays neutral Design system: - [Design system → Button](https://design-system-git-dnywh-dc924ac1-supabase.vercel.app/design-system/docs/components/button): `button-default` / `button-sizes` still show explicit default styling; Primary (green) is restricted to the Primary section (and `asChild`) WWW: - [www → Brand assets](https://zone-www-dot-com-git-dnywh-dc924ac1-supabase.vercel.app/brand-assets): Download logo kit / Download button kit stay neutral
182 lines
6.1 KiB
TypeScript
182 lines
6.1 KiB
TypeScript
import { ChevronDown } from 'lucide-react'
|
|
import Link from 'next/link'
|
|
import { parseAsString, useQueryState } from 'nuqs'
|
|
import { useMemo, useState } from 'react'
|
|
import { Badge, Button, Card, CardHeader, CardTitle, Input } from 'ui'
|
|
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
|
|
|
|
import { ButtonTooltip } from './ButtonTooltip'
|
|
import { useFreeProjectLimitCheckQuery } from '@/data/organizations/free-project-limit-check-query'
|
|
import { useOrganizationsQuery } from '@/data/organizations/organizations-query'
|
|
import type { Organization } from '@/types'
|
|
|
|
export interface ProjectClaimChooseOrgProps {
|
|
onSelect: (orgSlug: string) => void
|
|
maxOrgsToShow?: number
|
|
canCreateNewOrg: boolean
|
|
}
|
|
|
|
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 (
|
|
<Card
|
|
key={org.id}
|
|
className="hover:bg-surface-200 rounded-none first:rounded-t-lg last:rounded-b-lg -mb-px"
|
|
>
|
|
<CardHeader className="flex flex-row justify-between border-none space-y-0 space-x-2">
|
|
<CardTitle className="flex items-center gap-2 min-w-0 flex-1">
|
|
<span className="truncate min-w-0" title={org.name}>
|
|
{org.name}
|
|
</span>
|
|
<Badge className="shrink-0">{org.plan?.name}</Badge>
|
|
</CardTitle>
|
|
<ButtonTooltip
|
|
tooltip={{
|
|
content: {
|
|
text:
|
|
isSuccess && freePlanWithExceedingLimits ? (
|
|
<div className="space-y-3 w-96 p-2">
|
|
<p className="text-sm leading-normal">
|
|
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:
|
|
</p>
|
|
<ul className="pl-5 list-disc">
|
|
{membersExceededLimit.map((member, idx: number) => (
|
|
<li key={`member-${idx}`}>
|
|
{member.username || member.primary_email} (Limit:{' '}
|
|
{member.free_project_limit} free projects)
|
|
</li>
|
|
))}
|
|
</ul>
|
|
<p className="text-sm leading-normal">
|
|
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.
|
|
</p>
|
|
</div>
|
|
) : undefined,
|
|
},
|
|
}}
|
|
size="small"
|
|
onClick={() => {
|
|
onSelect(org.slug)
|
|
}}
|
|
className="shrink-0"
|
|
disabled={isSuccess && freePlanWithExceedingLimits}
|
|
>
|
|
Choose
|
|
</ButtonTooltip>
|
|
</CardHeader>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
export function OrganizationSelector({
|
|
onSelect,
|
|
maxOrgsToShow = 5,
|
|
canCreateNewOrg,
|
|
}: ProjectClaimChooseOrgProps) {
|
|
const {
|
|
data: organizations = [],
|
|
isPending: isLoadingOrgs,
|
|
isSuccess: isSuccessOrgs,
|
|
isError: isErrorOrgs,
|
|
} = useOrganizationsQuery()
|
|
|
|
const [search, setSearch] = useQueryState(
|
|
'org',
|
|
parseAsString.withDefault('').withOptions({ clearOnDefault: true })
|
|
)
|
|
const [showAll, setShowAll] = useState(false)
|
|
|
|
const filteredOrgs = useMemo(() => {
|
|
if (!search) {
|
|
return showAll ? organizations : organizations.slice(0, maxOrgsToShow)
|
|
}
|
|
return organizations.filter((org) => org.name.toLowerCase().includes(search.toLowerCase()))
|
|
}, [organizations, search, showAll, maxOrgsToShow])
|
|
|
|
const searchParams = new URLSearchParams(location.search)
|
|
let pathname = location.pathname
|
|
const basePath = process.env.NEXT_PUBLIC_BASE_PATH
|
|
if (basePath) {
|
|
pathname = pathname.replace(basePath, '')
|
|
}
|
|
|
|
searchParams.set('returnTo', pathname)
|
|
|
|
const onSelectOrg = (orgSlug: string) => {
|
|
onSelect(orgSlug)
|
|
setSearch('')
|
|
}
|
|
|
|
return (
|
|
<div className="w-full flex flex-col gap-y-4">
|
|
{isLoadingOrgs ? (
|
|
<ShimmeringLoader />
|
|
) : isErrorOrgs ? (
|
|
<div>Error</div>
|
|
) : isSuccessOrgs && organizations.length === 0 ? (
|
|
<span className="text-sm text-foreground-light">
|
|
It seems you don't have any organizations yet.
|
|
</span>
|
|
) : (
|
|
<>
|
|
<Input
|
|
type="text"
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
placeholder="Search..."
|
|
/>
|
|
<div>
|
|
{filteredOrgs.length === 0 && (
|
|
<div className="text-center text-foreground-light py-6">No organizations found.</div>
|
|
)}
|
|
{filteredOrgs.map((org) => (
|
|
<OrganizationCard key={org.id} org={org} onSelect={onSelectOrg} />
|
|
))}
|
|
{organizations.length > maxOrgsToShow && !showAll && !search && (
|
|
<div className="flex justify-center py-2">
|
|
<Button
|
|
icon={<ChevronDown className="w-4 h-4" />}
|
|
size="tiny"
|
|
onClick={() => {
|
|
setSearch('')
|
|
setShowAll(true)
|
|
}}
|
|
>
|
|
Show all organizations
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
{canCreateNewOrg && (
|
|
<Card className="flex items-center justify-between border-dashed pr-6">
|
|
<CardHeader className="border-none">
|
|
<CardTitle>Need a new organization?</CardTitle>
|
|
</CardHeader>
|
|
<Button size="small" className="" asChild>
|
|
<Link href={`/new?${searchParams.toString()}`}>New Organization</Link>
|
|
</Button>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|