[FE-1712] chore: use /available-regions endpoint for project creation (#37371)

* chore: use /available-regions endpoint for project creation

* fix types

* make postgres version selection work

* Small nit refactors

* Feature flag changes + fix icons for flags in various places

* Fix creating projects when selecting a smart region

* Fix sending region selection to create project endpoint

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
Alaister YoungandJoshen Lim authored and GitHub committed 2025-08-19 16:36:59 +08:00
1 parent a0f86b3010
commit 29e107af4a
35 files changed
+268 -101

No files matched your search

@@ -20,7 +20,7 @@ const STATIC_NODES = [
label: 'Primary Database',
region: 'East US (Ohio)',
provider: 'AWS',
regionIcon: 'EAST_US',
regionIcon: 'us-east-1',
},
position: { x: 825, y: 0 },
},
@@ -30,7 +30,7 @@ const STATIC_NODES = [
data: {
label: 'Iceberg',
details: '3 tables',
regionIcon: 'WEST_US',
regionIcon: 'us-west-1',
},
position: { x: 875, y: 110 },
},
@@ -40,7 +40,7 @@ const STATIC_NODES = [
data: {
label: 'BigQuery',
details: '5 tables',
regionIcon: 'WEST_US',
regionIcon: 'us-west-1',
},
position: { x: 875, y: 200 },
},
@@ -151,11 +151,10 @@ const ProjectUsage = () => {
if (disabled) {
const retentionDuration = LOG_RETENTION[plan?.id ?? 'free']
return (
<Tooltip>
<Tooltip key={i.key}>
<TooltipTrigger asChild>
<DropdownMenuRadioItem
disabled
key={i.key}
value={i.key}
className="!pointer-events-auto"
>
@@ -16,6 +16,7 @@ import {
Select_Shadcn_,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { smartRegionToExactRegion } from './ProjectCreation.utils'
interface PostgresVersionDetails {
postgresEngine: PostgresEngine | undefined
@@ -64,6 +65,8 @@ export const PostgresVersionSelector = ({
}: PostgresVersionSelectorProps) => {
const { data: project } = useSelectedProjectQuery()
const dbRegionExact = smartRegionToExactRegion(dbRegion)
const {
data: createVersions,
isLoading: isLoadingProjectCreateVersions,
@@ -71,7 +74,7 @@ export const PostgresVersionSelector = ({
} = useProjectCreationPostgresVersionsQuery(
{
cloudProvider,
dbRegion,
dbRegion: dbRegionExact,
organizationSlug,
},
{ enabled: type === 'create' }
@@ -1,6 +1,16 @@
import type { CloudProvider, Region } from 'shared-data'
import { AWS_REGIONS, FLY_REGIONS } from 'shared-data'
const smartRegionToExactRegionMap = new Map([
['Americas', 'East US (North Virginia)'],
['Europe', 'Central EU (Frankfurt)'],
['APAC', 'Southeast Asia (Singapore)'],
])
export function smartRegionToExactRegion(smartOrExactRegion: string) {
return smartRegionToExactRegionMap.get(smartOrExactRegion) ?? smartOrExactRegion
}
export function getAvailableRegions(cloudProvider: CloudProvider): Region {
switch (cloudProvider) {
case 'AWS':
@@ -1,13 +1,18 @@
import { useRouter } from 'next/router'
import { ControllerRenderProps, UseFormReturn } from 'react-hook-form'
import { useParams } from 'common'
import { useDefaultRegionQuery } from 'data/misc/get-default-region-query'
import { PROVIDERS } from 'lib/constants'
import { useOrganizationAvailableRegionsQuery } from 'data/organizations/organization-available-regions-query'
import { useFlag } from 'hooks/ui/useFlag'
import { BASE_PATH, PROVIDERS } from 'lib/constants'
import type { CloudProvider } from 'shared-data'
import {
Badge,
SelectContent_Shadcn_,
SelectGroup_Shadcn_,
SelectItem_Shadcn_,
SelectLabel_Shadcn_,
SelectSeparator_Shadcn_,
SelectTrigger_Shadcn_,
SelectValue_Shadcn_,
Select_Shadcn_,
@@ -31,17 +36,50 @@ export const RegionSelector = ({
field,
layout = 'horizontal',
}: RegionSelectorProps) => {
const router = useRouter()
const { slug } = useParams()
const smartRegionEnabled = useFlag('enableSmartRegion')
const { isLoading: isLoadingDefaultRegion } = useDefaultRegionQuery(
{ cloudProvider },
{ enabled: !smartRegionEnabled }
)
const { data: availableRegionsData, isLoading: isLoadingAvailableRegions } =
useOrganizationAvailableRegionsQuery(
{
slug,
cloudProvider,
},
{ enabled: smartRegionEnabled }
)
const smartRegions = availableRegionsData?.all.smartGroup ?? []
const allRegions = availableRegionsData?.all.specific ?? []
const recommendedSmartRegions = new Set(
[availableRegionsData?.recommendations.smartGroup.code].filter(Boolean)
)
const recommendedSpecificRegions = new Set(
availableRegionsData?.recommendations.specific.map((region) => region.code)
)
const availableRegions = getAvailableRegions(PROVIDERS[cloudProvider].id)
const regionsArray = Object.entries(availableRegions).map(([key, value]) => {
return {
code: value.code,
name: value.displayName,
provider: cloudProvider,
}
})
const regionOptions = smartRegionEnabled ? allRegions : regionsArray
const isLoading = smartRegionEnabled ? isLoadingAvailableRegions : isLoadingDefaultRegion
const showNonProdFields =
process.env.NEXT_PUBLIC_ENVIRONMENT === 'local' ||
process.env.NEXT_PUBLIC_ENVIRONMENT === 'staging'
const availableRegions = getAvailableRegions(PROVIDERS[cloudProvider].id)
const regionsArray = Object.entries(availableRegions)
const { isLoading: isLoadingDefaultRegion } = useDefaultRegionQuery({ cloudProvider })
return (
<FormItemLayout
layout={layout}
@@ -57,27 +95,78 @@ export const RegionSelector = ({
</>
}
>
<Select_Shadcn_
value={field.value}
onValueChange={field.onChange}
disabled={isLoadingDefaultRegion}
>
<SelectTrigger_Shadcn_>
<SelectValue_Shadcn_ placeholder="Select a region for your project.." />
<Select_Shadcn_ value={field.value} onValueChange={field.onChange} disabled={isLoading}>
<SelectTrigger_Shadcn_ className="[&>:nth-child(1)]:w-full [&>:nth-child(1)]:flex [&>:nth-child(1)]:items-start">
<SelectValue_Shadcn_
placeholder={
isLoading ? 'Loading available regions...' : 'Select a region for your project..'
}
/>
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
{smartRegionEnabled && (
<>
<SelectGroup_Shadcn_>
<SelectLabel_Shadcn_>Smart Region Selection</SelectLabel_Shadcn_>
{smartRegions.map((value) => {
return (
<SelectItem_Shadcn_
key={value.code}
value={value.name}
className="w-full [&>:nth-child(2)]:w-full"
>
<div className="flex flex-row items-center justify-between w-full">
<div className="flex items-center gap-x-3">
<img
alt="region icon"
className="w-5 rounded-sm"
src={`${BASE_PATH}/img/regions/${value.code}.svg`}
/>
<span className="text-foreground">{value.name}</span>
</div>
<div>
{recommendedSmartRegions.has(value.code) && (
<Badge variant="success" className="mr-1">
Recommended
</Badge>
)}
</div>
</div>
</SelectItem_Shadcn_>
)
})}
</SelectGroup_Shadcn_>
<SelectSeparator_Shadcn_ />
</>
)}
<SelectGroup_Shadcn_>
{regionsArray.map(([key, value]) => {
const label = value.displayName as string
<SelectLabel_Shadcn_>All Regions</SelectLabel_Shadcn_>
{regionOptions.map((value) => {
return (
<SelectItem_Shadcn_ key={key} value={label}>
<div className="flex items-center gap-3">
<img
alt="region icon"
className="w-5 rounded-sm"
src={`${router.basePath}/img/regions/${key}.svg`}
/>
<span className="text-foreground">{label}</span>
<SelectItem_Shadcn_
key={value.code}
value={value.name}
className="w-full [&>:nth-child(2)]:w-full"
>
<div className="flex flex-row items-center justify-between w-full">
<div className="flex items-center gap-x-3">
<img
alt="region icon"
className="w-5 rounded-sm"
src={`${BASE_PATH}/img/regions/${value.code}.svg`}
/>
<span className="text-foreground">{value.name}</span>
</div>
<div>
{recommendedSpecificRegions.has(value.code) && (
<Badge variant="success" className="mr-1">
Recommended
</Badge>
)}
</div>
</div>
</SelectItem_Shadcn_>
)
@@ -443,7 +443,7 @@ const DeployNewReplicaPanel = ({
<img
alt="region icon"
className="w-5 rounded-sm"
src={`${BASE_PATH}/img/regions/${region.key}.svg`}
src={`${BASE_PATH}/img/regions/${region.region}.svg`}
/>
)}
>
@@ -141,7 +141,7 @@ export const PrimaryNode = ({ data }: NodeProps<PrimaryNodeData>) => {
<img
alt="region icon"
className="w-8 rounded-sm mt-0.5"
src={`${BASE_PATH}/img/regions/${region.key}.svg`}
src={`${BASE_PATH}/img/regions/${region.region}.svg`}
/>
</div>
{numReplicas > 0 && (
@@ -58,7 +58,7 @@ const MapView = ({
const [tooltip, setTooltip] = useState<{
x: number
y: number
region: { key: string; country?: string; name?: string }
region: { key: string; country?: string; name?: string; region?: string }
}>()
const { can: canManageReplicas } = useAsyncCheckProjectPermissions(
PermissionAction.CREATE,
@@ -165,6 +165,7 @@ const MapView = ({
region: {
key: region.key,
country: region.name,
region: region.region,
name: hasNoDatabases
? undefined
: hasPrimary
@@ -213,17 +214,17 @@ const MapView = ({
<Marker coordinates={[tooltip.x - 47, tooltip.y - 5]}>
<foreignObject width={220} height={66.25}>
<div className="bg-studio/50 rounded border">
<div className="px-3 py-2 flex flex-col gap-y-1">
<div className="px-3 py-2 flex flex-col">
<div className="flex items-center gap-x-2">
<img
alt="region icon"
className="w-4 rounded-sm"
src={`${BASE_PATH}/img/regions/${tooltip.region.key}.svg`}
src={`${BASE_PATH}/img/regions/${tooltip.region.region}.svg`}
/>
<p className="text-[11px]">{tooltip.region.country}</p>
<p className="text-[10px]">{tooltip.region.country}</p>
</div>
<p
className={`text-[11px] ${
className={`text-[10px] ${
tooltip.region.name === undefined ? 'text-foreground-light' : ''
}`}
>
@@ -250,7 +251,7 @@ const MapView = ({
<img
alt="region icon"
className="w-10 rounded-sm"
src={`${BASE_PATH}/img/regions/${selectedRegion.key}.svg`}
src={`${BASE_PATH}/img/regions/${selectedRegion.region}.svg`}
/>
</div>
@@ -1,8 +1,8 @@
import Link from 'next/link'
import { useState } from 'react'
import { components } from 'api-types'
import { getAddons } from 'components/interfaces/Billing/Subscription/Subscription.utils'
import { InfraInstanceSize } from 'components/interfaces/DiskManagement/DiskManagement.types'
import { useOrgSubscriptionQuery } from 'data/subscriptions/org-subscription-query'
import { useProjectAddonsQuery } from 'data/subscriptions/project-addons-query'
import { ProjectAddonVariantMeta } from 'data/subscriptions/types'
@@ -11,7 +11,6 @@ import { INSTANCE_MICRO_SPECS } from 'lib/constants'
import { Button, HoverCard, HoverCardContent, HoverCardTrigger, Separator } from 'ui'
import { ComputeBadge } from 'ui-patterns/ComputeBadge'
import ShimmeringLoader from './ShimmeringLoader'
import { InfraInstanceSize } from 'components/interfaces/DiskManagement/DiskManagement.types'
const Row = ({ label, stat }: { label: string; stat: React.ReactNode | string }) => {
return (
@@ -81,9 +80,9 @@ export const ComputeBadgeWrapper = ({ project }: ComputeBadgeWrapperProps) => {
return (
<HoverCard onOpenChange={() => setOpenState(!open)} openDelay={280}>
<HoverCardTrigger asChild className="group" onClick={(e) => e.stopPropagation()}>
<Link href={`/project/${project?.ref}/settings/compute-and-disk`}>
<div>
<ComputeBadge infraComputeSize={project.infra_compute_size} />
</Link>
</div>
</HoverCardTrigger>
<HoverCardContent
side="bottom"
+2
View File
@@ -20,4 +20,6 @@ export const organizationKeys = {
['organizations', slug, 'validate-token', token] as const,
projectClaim: (slug: string, token: string) =>
['organizations', slug, 'project-claim', token] as const,
availableRegions: (slug: string | undefined, cloudProvider: string) =>
['organizations', slug, 'available-regions', cloudProvider] as const,
}
@@ -0,0 +1,51 @@
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import { get, handleError } from 'data/fetchers'
import type { ResponseError } from 'types'
import { organizationKeys } from './keys'
export type OrganizationAvailableRegionsVariables = {
slug?: string
cloudProvider: 'AWS' | 'FLY' | 'AWS_K8S'
}
export async function getOrganizationAvailableRegions(
{ slug, cloudProvider }: OrganizationAvailableRegionsVariables,
signal?: AbortSignal
) {
if (!slug) throw new Error('slug is required')
const { data, error } = await get(`/platform/projects/available-regions`, {
params: {
query: {
cloud_provider: cloudProvider,
organization_slug: slug,
},
},
signal,
})
if (error) handleError(error)
return data
}
export type OrganizationAvailableRegionsData = Awaited<
ReturnType<typeof getOrganizationAvailableRegions>
>
export type OrganizationAvailableRegionsError = ResponseError
export const useOrganizationAvailableRegionsQuery = <TData = OrganizationAvailableRegionsData>(
{ slug, cloudProvider }: OrganizationAvailableRegionsVariables,
{
enabled = true,
...options
}: UseQueryOptions<
OrganizationAvailableRegionsData,
OrganizationAvailableRegionsError,
TData
> = {}
) =>
useQuery<OrganizationAvailableRegionsData, OrganizationAvailableRegionsError, TData>(
organizationKeys.availableRegions(slug, cloudProvider),
({ signal }) => getOrganizationAvailableRegions({ slug, cloudProvider }, signal),
{ enabled: enabled && typeof slug !== 'undefined', ...options }
)
@@ -26,7 +26,8 @@ export type ProjectCreateVariables = {
name: string
organizationSlug: string
dbPass: string
dbRegion: string
dbRegion?: string
regionSelection?: CreateProjectBody['region_selection']
dbSql?: string
dbPricingTierId?: string
cloudProvider?: string
@@ -44,6 +45,7 @@ export async function createProject({
organizationSlug,
dbPass,
dbRegion,
regionSelection,
dbSql,
cloudProvider = PROVIDERS.AWS.id,
authSiteUrl,
@@ -60,6 +62,7 @@ export async function createProject({
name,
db_pass: dbPass,
db_region: dbRegion,
region_selection: regionSelection,
db_sql: dbSql,
auth_site_url: authSiteUrl,
...(customSupabaseRequest !== undefined && {
@@ -1,6 +1,5 @@
import { useParams } from 'common'
import { debounce } from 'lodash'
import { useRouter } from 'next/router'
import { ChangeEvent, useRef, useState } from 'react'
import { toast } from 'sonner'
import { Alert, Button, Checkbox, Input, Listbox } from 'ui'
@@ -17,7 +16,7 @@ import { useVercelProjectsQuery } from 'data/integrations/integrations-vercel-pr
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { useProjectCreateMutation } from 'data/projects/project-create-mutation'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { PROVIDERS } from 'lib/constants'
import { BASE_PATH, PROVIDERS } from 'lib/constants'
import { getInitialMigrationSQLFromGitHubRepo } from 'lib/integration-utils'
import passwordStrength from 'lib/password-strength'
import { generateStrongPassword } from 'lib/project'
@@ -54,7 +53,6 @@ VercelIntegration.getLayout = (page) => (
)
const CreateProject = () => {
const router = useRouter()
const { data: selectedOrganization } = useSelectedOrganizationQuery()
const [projectName, setProjectName] = useState('')
const [dbPass, setDbPass] = useState('')
@@ -258,11 +256,11 @@ const CreateProject = () => {
key={option}
label={label}
value={label}
addOnBefore={({ active, selected }: any) => (
addOnBefore={() => (
<img
alt="region icon"
className="w-5 rounded-sm"
src={`${router.basePath}/img/regions/${Object.keys(AWS_REGIONS)[i]}.svg`}
src={`${BASE_PATH}/img/regions/${Object.values(AWS_REGIONS)[i].code}.svg`}
/>
)}
>
+45 -14
View File
@@ -23,6 +23,7 @@ import {
PostgresVersionSelector,
} from 'components/interfaces/ProjectCreation/PostgresVersionSelector'
import { SPECIAL_CHARS_REGEX } from 'components/interfaces/ProjectCreation/ProjectCreation.constants'
import { smartRegionToExactRegion } from 'components/interfaces/ProjectCreation/ProjectCreation.utils'
import { RegionSelector } from 'components/interfaces/ProjectCreation/RegionSelector'
import { SecurityOptions } from 'components/interfaces/ProjectCreation/SecurityOptions'
import { SpecialSymbolsCallout } from 'components/interfaces/ProjectCreation/SpecialSymbolsCallout'
@@ -38,6 +39,7 @@ import { useOverdueInvoicesQuery } from 'data/invoices/invoices-overdue-query'
import { useDefaultRegionQuery } from 'data/misc/get-default-region-query'
import { useAuthorizedAppsQuery } from 'data/oauth/authorized-apps-query'
import { useFreeProjectLimitCheckQuery } from 'data/organizations/free-project-limit-check-query'
import { useOrganizationAvailableRegionsQuery } from 'data/organizations/organization-available-regions-query'
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
import { DesiredInstanceSize, instanceSizeSpecs } from 'data/projects/new-project.constants'
import {
@@ -150,6 +152,7 @@ const Wizard: NextPageWithLayout = () => {
const { mutate: sendEvent } = useSendEventMutation()
const smartRegionEnabled = useFlag('enableSmartRegion')
const projectCreationDisabled = useFlag('disableProjectCreationAndUpdate')
const showPostgresVersionSelector = useFlag('showPostgresVersionSelector')
const cloudProviderEnabled = useFlag('enableFlyCloudProvider')
@@ -212,23 +215,18 @@ const Wizard: NextPageWithLayout = () => {
components['schemas']['ProjectInfo'][] | undefined
>(undefined)
useEffect(() => {
// Only set once to ensure compute credits dont change while project is being created
if (allProjectsFromApi && !allProjects) {
setAllProjects(allProjectsFromApi)
}
}, [allProjectsFromApi, allProjects, setAllProjects])
const organizationProjects =
allProjects?.filter(
(project) =>
project.organization_id === currentOrg?.id && project.status !== PROJECT_STATUS.INACTIVE
) ?? []
const { data: defaultRegion, error: defaultRegionError } = useDefaultRegionQuery(
const { data: _defaultRegion, error: defaultRegionError } = useDefaultRegionQuery(
{
cloudProvider: PROVIDERS[DEFAULT_PROVIDER].id,
},
{
enabled: !smartRegionEnabled,
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchInterval: false,
@@ -237,6 +235,26 @@ const Wizard: NextPageWithLayout = () => {
}
)
const { data: availableRegionsData, error: availableRegionsError } =
useOrganizationAvailableRegionsQuery(
{
slug: slug,
cloudProvider: PROVIDERS[DEFAULT_PROVIDER].id,
},
{
enabled: smartRegionEnabled,
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchInterval: false,
refetchOnReconnect: false,
}
)
const regionError = smartRegionEnabled ? availableRegionsError : defaultRegionError
const defaultRegion = smartRegionEnabled
? availableRegionsData?.recommendations.specific[0]?.name
: _defaultRegion
const isAdmin = useCheckPermissions(PermissionAction.CREATE, 'projects')
const isInvalidSlug = isOrganizationsSuccess && currentOrg === undefined
@@ -299,11 +317,12 @@ const Wizard: NextPageWithLayout = () => {
})
const { instanceSize, cloudProvider, dbRegion, organization } = form.watch()
const dbRegionExact = smartRegionToExactRegion(dbRegion)
const availableOrioleVersion = useAvailableOrioleImageVersion(
{
cloudProvider: cloudProvider as CloudProvider,
dbRegion,
dbRegion: smartRegionEnabled ? dbRegionExact : dbRegion,
organizationSlug: organization,
},
{ enabled: currentOrg !== null && !isManagedByVercel }
@@ -371,12 +390,16 @@ const Wizard: NextPageWithLayout = () => {
const { postgresEngine, releaseChannel } =
extractPostgresVersionDetails(postgresVersionSelection)
const { smartGroup = [], specific = [] } = availableRegionsData?.all ?? {}
const selectedRegion = smartRegionEnabled
? smartGroup.find((x) => x.name === dbRegion) ?? specific.find((x) => x.name === dbRegion)
: undefined
const data: ProjectCreateVariables = {
cloudProvider: cloudProvider,
dbPass,
cloudProvider,
organizationSlug: currentOrg.slug,
name: projectName,
dbPass: dbPass,
dbRegion: dbRegion,
// gets ignored due to org billing subscription anyway
dbPricingTierId: 'tier_free',
// only set the compute size on pro+ plans. Free plans always use micro (nano in the future) size.
@@ -385,6 +408,7 @@ const Wizard: NextPageWithLayout = () => {
dataApiUseApiSchema: !dataApi ? false : useApiSchema,
postgresEngine: useOrioleDb ? availableOrioleVersion?.postgres_engine : postgresEngine,
releaseChannel: useOrioleDb ? availableOrioleVersion?.release_channel : releaseChannel,
...(smartRegionEnabled ? { regionSelection: selectedRegion } : { dbRegion }),
}
if (postgresVersion) {
@@ -402,6 +426,13 @@ const Wizard: NextPageWithLayout = () => {
createProject(data)
}
useEffect(() => {
// Only set once to ensure compute credits dont change while project is being created
if (allProjectsFromApi && !allProjects) {
setAllProjects(allProjectsFromApi)
}
}, [allProjectsFromApi, allProjects, setAllProjects])
useEffect(() => {
// Handle no org: redirect to new org route
if (isEmptyOrganizations) {
@@ -423,10 +454,10 @@ const Wizard: NextPageWithLayout = () => {
}, [defaultRegion])
useEffect(() => {
if (defaultRegionError) {
if (regionError) {
form.setValue('dbRegion', PROVIDERS[DEFAULT_PROVIDER].default_region.displayName)
}
}, [defaultRegionError])
}, [regionError])
const availableComputeCredits = organizationProjects.length === 0 ? 10 : 0
+16 -21
View File
@@ -166,13 +166,12 @@ const Home: NextPageWithLayout = () => {
>
Tables
</Link>
<p className="text-2xl tabular-nums">
{isLoadingTables ? (
<ShimmeringLoader className="w-full h-[32px] w-6 p-0" />
) : (
tablesCount
)}
</p>
{isLoadingTables ? (
<ShimmeringLoader className="w-full h-[32px] w-6 p-0" />
) : (
<p className="text-2xl tabular-nums">{tablesCount}</p>
)}
</div>
{IS_PLATFORM && (
@@ -183,13 +182,11 @@ const Home: NextPageWithLayout = () => {
>
Functions
</Link>
<p className="text-2xl tabular-nums">
{isLoadingFunctions ? (
<ShimmeringLoader className="w-full h-[32px] w-6 p-0" />
) : (
functionsCount
)}
</p>
{isLoadingFunctions ? (
<ShimmeringLoader className="w-full h-[32px] w-6 p-0" />
) : (
<p className="text-2xl tabular-nums">{functionsCount}</p>
)}
</div>
)}
@@ -201,13 +198,11 @@ const Home: NextPageWithLayout = () => {
>
Replicas
</Link>
<p className="text-2xl tabular-nums">
{isLoadingReplicas ? (
<ShimmeringLoader className="w-full h-[32px] w-6 p-0" />
) : (
replicasCount
)}
</p>
{isLoadingReplicas ? (
<ShimmeringLoader className="w-full h-[32px] w-6 p-0" />
) : (
<p className="text-2xl tabular-nums">{replicasCount}</p>
)}
</div>
)}
</div>
@@ -1,17 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" id="flag-icons-za" viewBox="0 0 640 480">
<defs>
<clipPath id="za-a">
<path fill-opacity=".7" d="M-71.9 0h682.7v512H-71.9z"/>
</clipPath>
</defs>
<g clip-path="url(#za-a)" transform="translate(67.4) scale(.93748)">
<g fill-rule="evenodd" stroke-width="1pt">
<path d="M-71.9 407.8V104.4L154 256.1-72 407.8z"/>
<path fill="#00c" d="m82.2 512.1 253.6-170.6H696V512H82.2z"/>
<path fill="red" d="M66 0h630v170.8H335.7S69.3-1.7 66 0z"/>
<path fill="#fc0" d="M-71.9 64v40.4L154 256-72 407.8v40.3l284.5-192L-72 64z"/>
<path fill="#093" d="M-71.9 64V0h95l301.2 204h371.8v104.2H324.3L23 512h-94.9v-63.9l284.4-192L-71.8 64z"/>
<path fill="#fff" d="M23 0h59.2l253.6 170.7H696V204H324.3L23 .1zm0 512.1h59.2l253.6-170.6H696v-33.2H324.3L23 512z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 856 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="640" height="480" fill="none"><path fill="#313131" d="M0 0h640v480H0z"/><g clip-path="url(#a)"><path fill="#2E8BC0" d="M320 400c88.366 0 160-71.634 160-160S408.366 80 320 80s-160 71.634-160 160 71.634 160 160 160Z"/><path fill="#3ECF8E" d="M427.822 291.093c-9.04-3.466-7.431-2.204-13.004-14.053-5.574-11.84-8.951-15.653-21.485-17.04-12.542-1.378-12.071-.4-15.555-6.667-3.485-6.275-17.618 1.698-26.667 5.191-9.075 3.485-6.835 11.076-9.627 10.365a42.945 42.945 0 0 1-16.826-8.062c-6.969-5.583 5.182-9.938 8.675-13.414 3.494-3.493-6.577-1.591-10.071-3.697-3.493-2.08-9.742 4.186-15.315-1.405-5.583-5.573-2.089-13.938-2.783-20.907-.702-6.977 9.049-8.364 15.325-9.76 6.267-1.386 11.138.009 11.138 8.365 0 8.364 7.671 11.155 5.582 1.387-2.089-9.769 2.089-12.543 4.889-17.423 2.782-4.88 7.662-11.857 13.929-22.311 6.275-10.453 10.462-5.582 18.124-4.835 7.671.755 9.76-.738 15.333-.036 5.556.702 0-6.969 2.098-14.631 2.08-7.662 9.751-6.276 17.431-4.178 7.663 2.089-7.68-9.769-10.462-16.035-2.8-6.276-10.462 0-15.324-10.454-4.916-10.453-11.769-17.217-18.045-18.151-6.275-.942 2 13.28-2.88 20.942-4.898 7.663-12.4 6.463-20.089 6.463-7.653 0-14.764-6.463-9.182-16.223 5.565-9.751 16.205-8.746 21.778-17.111 5.582-8.355-19.822-7.59-28.889-10.373-9.067-2.791-19.947-2.764-29.004 4.204-9.067 6.96-28.454 9.138-36.125 11.92-7.662 2.783-9.04-4.622-10.427-10.204-.622-2.48-3.448-3.262-7.431-3.173a160.711 160.711 0 0 0-50.364 43.742c2.107 7.173 12.311-1.68 14.311-3.68 2.089-2.089 12.542.702 13.929 8.364 1.395 7.663.702 16.045-2.08 23.014-2.791 6.96 6.969 16.72 12.542 26.48 5.582 9.769 11.849 9.76 14.64 9.76 2.782 0 18.818 6.275 18.116 13.937-.694 7.672 14.64 14.632 20.915 19.512 6.276 4.897 10.24 9.706 18.382 11.111 8.152 1.404 17.68 14.729 21.858 16.133 3.209 1.067 8.64 6.196 11.92 7.787-4.311 5.457-12.658 16.666-12.658 21.831 0 6.986 9.112 17.964 12.596 20.746 3.476 2.774 13.484 4.889 15.556 11.849 2.097 6.969-1.458 30.427-7.716 36.694-6.293 6.266-8.578 21.084-8.578 21.084s2.214 4.809 9.627 0c8.195-5.307 21.92-20.418 27.404-23.707a184.585 184.585 0 0 0 30.374-22.96c9.76-9.057 6.222-15.404 11.111-24.444 4.88-9.058 11.191-8.649 19.546-16.293 8.374-7.68 2.525-6.16-6.542-9.654Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M160 80h320v320H160z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="640" height="480" fill="none"><path fill="#313131" d="M0 0h640v480H0z"/><g clip-path="url(#a)"><path fill="#2E8BC0" d="M320 400c88.366 0 160-71.634 160-160S408.366 80 320 80s-160 71.634-160 160 71.634 160 160 160Z"/><path fill="#3ECF8E" d="M184.996 305.556c-.552-8.889-.552-10.552-.552-21.112 0-10.56 4.996-8.888 10-13.884 5.005-4.996 3.894-5.556 12.223-11.031 8.329-5.476 3.893-11.742 3.333-15.076-.56-3.333-5.556-.56-13.893.543-8.338 1.102-5.547-2.774-10.551-4.996-5.005-2.222-7.218-5.556-10.56-15.556-3.343-10-3.894-12.782-2.774-21.111s5.005-.56 5.556 8.329c.551 8.889 8.338 5.556 8.338 11.111 0 5.556 11.111 11.663 13.884 13.334 2.773 1.671 10.56-8.338 13.333-11.111 2.774-2.774 6.116-6.667 7.218-8.889 1.102-2.223 15.005-3.894 17.778-3.894 2.773 0 9.44 8.338 9.44 12.223 0 3.884 3.333 14.444 6.115 20.551 2.783 6.106 8.889 7.217 14.445 11.662 5.555 4.444 8.338.551 8.338.551s-2.223-9.44-2.223-17.227c0-7.786 6.667-14.444 6.667-14.444s10.56 7.778 11.111 10c.551 2.222 8.889 10 9.44 13.884.551 3.885 4.996 8.889 13.182 10 8.187 1.111 2.374-9.44 5.147-16.666 2.773-7.227 6.667-8.338 11.662-9.44 4.996-1.103 8.889-5.556 12.223-10 3.333-4.445 9.44-10.56 8.888-15.556-.551-4.995-2.222-8.338-4.444-14.444-2.222-6.107 6.667-8.338 10.56-15.556 3.893-7.218 0 0 8.889-2.222s4.995-2.222 6.667-5.556c1.671-3.333 2.773-6.666 1.111-12.782-1.663-6.115-7.778 0-13.885 0-6.106 0 2.231-8.302 2.782-14.969.552-6.666 8.338-4.995 13.885-5.555 5.547-.56 7.218 7.217 8.889 8.889 1.671 1.671 18.889-11.112 23.333-17.227 4.445-6.116-3.884-4.436-1.662-7.013 2.222-2.578-13.333-3.103-19.449-4.089-6.116-.987-21.662-1.671-27.769-5.44-6.107-3.769-29.44-.925-35.555 2.106-6.116 3.031-10-.257-12.783-4.444-2.782-4.187-14.444-2.089-17.777-4.445-3.334-2.355-6.667 3.885-11.112 5.556-4.444 1.671-6.115 2.222-11.662-1.111-5.546-3.333 1.662-7.227-6.115-10-5.209-1.858-11.449-.773-21.156-.987C194.684 125.262 160 178.551 160 240c0 30.4 8.631 58.711 23.351 82.907 2.738-4.605 2.053-10.818 1.645-17.351Zm128.888-10.552c3.894.552 15.005 0 6.116-7.217-8.889-7.218-13.884-1.671-12.782-10 1.102-8.329-5.556-8.338-5.556-8.338 0 6.115-4.444 12.782 0 18.889 4.445 6.106 8.338 6.106 12.222 6.666Z"/><path fill="#3ECF8E" d="M370.56 202.222c-8.889-7.218-2.222-4.995-1.111-13.333 1.111-8.338-5.556-8.338-5.556-8.338 0 6.116-4.444 12.782 0 18.889 4.445 6.107-8.889 11.111-4.995 11.662 3.893.551 20.551-1.662 11.662-8.88Zm-34.489 86.667c5.511 6.115 3.378 0 10.596-2.773 7.217-2.774-6.116-8.889-10.56-12.223-4.445-3.333-8.863-3.457-12.747 3.894-4.409 8.329 7.2 4.986 12.711 11.102ZM401.111 300c-2.773-3.333-8.889-4.996-15.555-4.844-6.667.16-6.116-7.378-12.783-6.827-6.666.551-11.431-4.48-14.444-6.036-6.551-3.377-2.222 4.365 0 12.854 2.222 8.489 10.56 2.062 18.329 6.506 7.769 4.445 8.338-1.671 15.555.551 7.218 2.223 10 7.218 16.925 6.667 6.924-.551-5.245-5.538-8.027-8.871Zm-14.444 24.444c-2.774-5.555-10.898-10.56-14.231-13.377-3.334-2.818-8.552-3.769-12.996-2.134-4.444 1.636-7.218 0-9.44-4.4-2.222-4.4-6.116-2.862-9.44-2.311-3.324.551-16.667 6.116-24.444 10-7.778 3.885-11.316 7.262-16.418 12.222-7.982 7.769-3.582 2.774 0 7.778 3.582 4.996-3.929 20.551-4.48 29.44-.551 8.889 14.24-2.773 19.795-3.893 5.556-1.12 3.92-4.445 8.365-7.778 4.444-3.333 7.333-4.115 12.213.329 4.88 4.444 11.271 8.56 11.271 13.556 0 4.995 17.591 13.333 24.258 10 6.667-3.334 10.56-10 16.667-15.556 6.106-5.556 3.893-16.107 5.555-22.773 1.662-6.667-3.902-5.547-6.675-11.103Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M160 80h320v320H160z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="640" height="480" fill="none"><path fill="#313131" d="M0 0h640v480H0z"/><g clip-path="url(#a)"><path fill="#2E8BC0" d="M320 400c88.366 0 160-71.634 160-160S408.366 80 320 80s-160 71.634-160 160 71.634 160 160 160Z"/><path fill="#3ECF8E" d="M388.587 95.609c-9.085 5.049-16.64 13.582-26.916 10.498-16.667-4.996-38.889-16.107-53.333-2.223-14.445 13.885-17.778 26.667 0 26.116 17.778-.551 30-21.671 38.889-12.782 8.889 8.889 6.657 16.115-14.445 18.889-21.102 2.773-43.893 6.115-56.115 6.115-12.223 0-15.005 7.218-6.667 15.005 8.338 7.786-5.004 8.329-18.889 16.106-13.884 7.778 3.333 11.111 15.005 17.778 11.671 6.667 20.551-1.671 25.555-12.782 5.005-11.111 26.498-24.445 35.467-22.773 8.978 1.671 8.978 6.115 7.306 13.884-1.671 7.769 6.667 5.556 7.218-3.333.551-8.889 10.56-15.556 18.329-16.107 7.769-.551 14.445 10.56 5.556 16.107-8.889 5.546-17.778 10-6.667 12.782 11.111 2.782 18.889 17.227 6.116 23.333-12.774 6.107-34.996 10-44.996 4.996-10-5.005-32.782-12.222-38.889-8.338-6.107 3.884-9.44 7.911-16.667 10.622-7.217 2.711-36.666 16.045-37.226 33.271-.56 17.227-1.102 36.676 10.56 37.227 11.662.551 40-7.218 48.889-14.444 8.889-7.227 21.111-5.556 24.995 2.773 3.885 8.329 1.111 13.333-2.773 26.667-3.885 13.333 2.542 20 8.773 31.662 6.231 11.662 11.227 18.329 11.227 26.666 0 8.338 8.889 16.667 22.222 2.774 13.333-13.894 25.556-41.111 31.111-51.111 5.556-10 10-32.223 16.667-36.667 6.667-4.444 17.227-15.004 9.44-13.333-7.787 1.671-23.333-.551-27.218-11.663-3.884-11.111-20.551-32.222-12.782-34.444 7.769-2.222 16.667 12.356 20 19.236 3.333 6.88 7.778 15.208 14.444 17.431 6.667 2.222 21.111-14.871 25.556-17.431 4.444-2.569 1.111-13.12-7.778-12.009-8.889 1.111-20.551 0-20.551-5.547s11.111-12.782 20-11.111c8.889 1.671 15.556 4.444 21.111 11.111 5.556 6.667 16.667 18.889 21.111 26.667 4.445 7.777 7.778 8.889 10-4.996 1.476-9.227 3.44-14.302 5.245-19.751-9.005-51.813-42.845-94.96-88.88-116.871Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M160 80h320v320H160z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB