spb/datacenterindex
Public
HTML 53.9%
TypeScript 44.5%
JavaScript 0.6%
SQL 0.5%
1import type { CloudRegionSummary } from "@dci/core";2import type { Metadata } from "next";3import Link from "next/link";4import { SectionNav } from "@/components/entity/section-nav";5import { Badge } from "@/components/ui/badge";6import { Button } from "@/components/ui/button";7import { Page, PageHeader, Section } from "@/components/ui/card";8import { CountryFlag } from "@/components/ui/country-flag";9import { FilterChips, type Chip } from "@/components/ui/filter-chips";10import { Metric, MetricCell, MetricRow } from "@/components/ui/metric";11import { EmptyState, OfflineNotice } from "@/components/ui/states";12import { RowLink, TBody, THead, Table, Td, Th, Tr } from "@/components/ui/table";13import { api } from "@/lib/api";14import { countryName, fmtDate, fmtInt } from "@/lib/format";15import { routes } from "@/lib/routes";16import { pageMetadata } from "@/lib/seo";17import { compareBy, hrefWith, parseSort, sortTh, spGet, type SearchParams, type SortState } from "@/lib/sort";18import { CLOUD_STATUSES, CLOUD_STATUS_LABEL, CloudStatusBadge, type CloudStatus } from "../cloud-format";1920export const revalidate = 120;2122export const metadata: Metadata = pageMetadata({23 title: "Cloud regions",24 description: "Public cloud regions by provider — code, location, availability zones, launch date, status and sovereignty, each linked to the provider's published source.",25 path: routes.cloudRegions(),26});2728const SORT_KEYS = ["provider", "code", "name", "city", "country", "azs", "launched", "status"] as const;29type SortKey = (typeof SORT_KEYS)[number];3031const BASE = routes.cloudRegions();3233const sel = "h-9 rounded-sm border border-hair-2 bg-surface px-2 text-[13px] text-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent";3435function RegionTable({ items, showProvider, sp, sort }: { items: CloudRegionSummary[]; showProvider: boolean; sp: SearchParams; sort: SortState<SortKey> | null }) {36 const th = (key: SortKey) => (sort ? sortTh(BASE, sp, sort, key) : {});37 return (38 <Table dense mobileCollapse={{ total: items.length, label: "regions" }}>39 <THead>40 <tr>41 {showProvider && <Th {...th("provider")}>Provider</Th>}42 <Th {...th("code")}>Code</Th>43 <Th {...th("name")}>Region</Th>44 <Th className="hidden sm:table-cell" {...th("city")}>45 City46 </Th>47 <Th {...th("country")}>Country</Th>48 <Th numeric {...th("azs")}>49 AZs50 </Th>51 <Th className="hidden lg:table-cell" {...th("launched")}>52 Launched53 </Th>54 <Th {...th("status")}>Status</Th>55 <Th className="hidden lg:table-cell">Sovereign</Th>56 </tr>57 </THead>58 <TBody>59 {items.map((r) => (60 <Tr key={r.id}>61 {showProvider && (62 <Td label="Provider">63 <Link href={routes.operator(r.provider.slug)} className="text-ink-2 hover:text-ink hover:underline">64 {r.provider.name}65 </Link>66 </Td>67 )}68 <Td primary>69 <RowLink href={routes.cloudRegion(r.slug)} className="figure text-[12.5px]">70 {r.code}71 </RowLink>72 <div className="text-[11.5px] text-ink-3 sm:hidden">73 {r.name}74 {r.city ? ` · ${r.city}` : ""}75 </div>76 </Td>77 <Td label="Region" className="hidden max-w-[220px] truncate text-ink-2 sm:table-cell">78 {r.name}79 </Td>80 <Td label="City" className="hidden max-w-[140px] truncate text-ink-3 sm:table-cell">81 {r.city ?? "—"}82 </Td>83 <Td label="Country">84 <CountryFlag iso2={r.countryIso2} withName className="max-w-[120px] text-ink-2 lg:max-w-[160px]" />85 </Td>86 <Td label="AZs" numeric>87 {r.availabilityZones ?? <span className="text-ink-4">—</span>}88 </Td>89 <Td label="Launched" className="figure hidden text-[12px] text-ink-3 lg:table-cell">90 {fmtDate(r.launchedOn)}91 </Td>92 <Td label="Status">93 <CloudStatusBadge status={r.status} />94 </Td>95 <Td label="Sovereign" className="hidden lg:table-cell">96 {r.isSovereign ? (97 <Badge tone="accent" size="xs">98 Sovereign99 </Badge>100 ) : (101 <span className="text-ink-4">—</span>102 )}103 </Td>104 </Tr>105 ))}106 </TBody>107 </Table>108 );109}110111export default async function CloudRegionsPage({ searchParams }: { searchParams: Promise<SearchParams> }) {112 const sp = await searchParams;113 const provider = spGet(sp, "provider");114 const country = spGet(sp, "country")?.toUpperCase();115 const statusRaw = spGet(sp, "status");116 const status = (CLOUD_STATUSES as string[]).includes(statusRaw ?? "") ? (statusRaw as CloudStatus) : undefined;117 const sorted = spGet(sp, "sort") !== undefined;118119 // Unfiltered list for facets + metrics; filtered list for the table (the API filters server-side).120 const [all, filtered] = await Promise.all([api.cloudRegions({ per_page: 400 }), provider || country || status ? api.cloudRegions({ per_page: 400, provider, country, status }) : null]);121 const ok = all.items.length > 0;122 const items = filtered ? filtered.items : all.items;123124 const providers = Array.from(new Map(all.items.map((r) => [r.provider.slug, { slug: r.provider.slug, name: r.provider.name, n: 0 }])).values());125 for (const r of all.items) providers.find((p) => p.slug === r.provider.slug)!.n++;126 providers.sort((a, b) => b.n - a.n || a.name.localeCompare(b.name));127 const countries = Array.from(new Set(all.items.map((r) => r.countryIso2).filter((x): x is string => !!x))).sort((a, b) => countryName(a).localeCompare(countryName(b)));128 const operational = all.items.filter((r) => r.status === "operational").length;129 const announced = all.items.filter((r) => r.status === "announced").length;130131 const chips: Chip[] = [];132 if (provider) chips.push({ key: "provider", label: "Provider", value: providers.find((p) => p.slug === provider)?.name ?? provider, removeHref: hrefWith(BASE, sp, { provider: undefined }) });133 if (country) chips.push({ key: "country", label: "Country", value: countryName(country), removeHref: hrefWith(BASE, sp, { country: undefined }) });134 if (status) chips.push({ key: "status", label: "Status", value: CLOUD_STATUS_LABEL[status], removeHref: hrefWith(BASE, sp, { status: undefined }) });135136 const sort = sorted ? parseSort<SortKey>(sp, SORT_KEYS, { key: "provider", order: "asc" }) : null;137 let flat: CloudRegionSummary[] = items;138 if (sort) {139 const get = (r: CloudRegionSummary): string | number | null => {140 switch (sort.key) {141 case "provider":142 return r.provider.name;143 case "code":144 return r.code;145 case "name":146 return r.name;147 case "city":148 return r.city;149 case "country":150 return r.countryIso2 ? countryName(r.countryIso2) : null;151 case "azs":152 return r.availabilityZones;153 case "launched":154 return r.launchedOn;155 case "status":156 return r.status;157 }158 };159 flat = [...items].sort(compareBy(get, sort.order));160 }161 const groups = sorted162 ? []163 : providers164 .map((p) => ({ ...p, items: items.filter((r) => r.provider.slug === p.slug).sort((a, b) => a.code.localeCompare(b.code)) }))165 .filter((g) => g.items.length);166167 return (168 <Page>169 <PageHeader170 eyebrow="Index"171 title="Cloud regions"172 description={ok ? `${fmtInt(all.total)} public cloud regions across ${providers.length} providers — code, location, availability zones, launch date, status and sovereignty, each linked to the provider's published source.` : "Public cloud regions by provider: code, location, availability zones, launch date, status and sovereignty."}173 aside={174 <Button href={routes.map("cr=1")} size="sm">175 Show on map176 </Button>177 }178 />179 {!ok && <OfflineNotice detail={all.total === 0 ? "no cloud regions returned" : undefined} className="mb-3" />}180 <p className="-mt-3 mb-4 text-[12px] text-ink-3">Coordinates are city-level (the published location), never the physical data center.</p>181182 {ok && (183 <MetricRow cols={5} className="mb-5">184 <MetricCell>185 <Metric value={fmtInt(all.total)} label="Regions" size="sm" />186 </MetricCell>187 <MetricCell>188 <Metric value={fmtInt(providers.length)} label="Providers" size="sm" />189 </MetricCell>190 <MetricCell>191 <Metric value={fmtInt(countries.length)} label="Countries" size="sm" />192 </MetricCell>193 <MetricCell>194 <Metric value={fmtInt(operational)} label="Operational" size="sm" />195 </MetricCell>196 <MetricCell>197 <Metric value={fmtInt(announced)} label="Announced" size="sm" />198 </MetricCell>199 </MetricRow>200 )}201202 <form method="get" action={BASE} className="flex flex-wrap items-center gap-2">203 <label className="sr-only" htmlFor="cr-provider">204 Provider205 </label>206 <select id="cr-provider" name="provider" defaultValue={provider ?? ""} className={`${sel} max-w-[46vw] sm:max-w-none`}>207 <option value="">All providers</option>208 {providers.map((p) => (209 <option key={p.slug} value={p.slug}>210 {p.name} ({p.n})211 </option>212 ))}213 </select>214 <label className="sr-only" htmlFor="cr-country">215 Country216 </label>217 <select id="cr-country" name="country" defaultValue={country ?? ""} className={`${sel} max-w-[46vw] sm:max-w-none`}>218 <option value="">All countries</option>219 {countries.map((c) => (220 <option key={c} value={c}>221 {countryName(c)}222 </option>223 ))}224 </select>225 <label className="sr-only" htmlFor="cr-status">226 Status227 </label>228 <select id="cr-status" name="status" defaultValue={status ?? ""} className={sel}>229 <option value="">Any status</option>230 {CLOUD_STATUSES.map((s) => (231 <option key={s} value={s}>232 {CLOUD_STATUS_LABEL[s]}233 </option>234 ))}235 </select>236 {sort && <input type="hidden" name="sort" value={sort.key} />}237 {sort && <input type="hidden" name="order" value={sort.order} />}238 <Button type="submit" size="sm" variant="primary">239 Apply240 </Button>241 <Link href={sorted ? hrefWith(BASE, sp, { sort: undefined, order: undefined }) : hrefWith(BASE, sp, { sort: "provider", order: "asc" })} className="ml-auto text-[12.5px] text-ink-3 hover:text-ink">242 {sorted ? "Group by provider" : "Flat sortable table"}243 </Link>244 </form>245 <FilterChips chips={chips} resetHref={BASE} className="mt-2" />246247 {ok && items.length === 0 && <EmptyState className="mt-4" title="No cloud region matches these filters" description="Loosen a filter or clear them all." />}248249 {!sorted && groups.length > 0 && <SectionNav items={groups.map((g) => ({ id: g.slug, label: g.name, count: g.items.length }))} className="mt-3" />}250251 {sorted ? (252 <div className="mt-4">253 <RegionTable items={flat} showProvider sp={sp} sort={sort} />254 </div>255 ) : (256 <div className="mt-2 flex flex-col gap-8">257 {groups.map((g) => (258 <Section259 key={g.slug}260 id={g.slug}261 title={262 <Link href={routes.operator(g.slug)} className="hover:text-accent hover:underline">263 {g.name}264 </Link>265 }266 description={`${g.items.length} ${g.items.length === 1 ? "region" : "regions"} · ${g.items.filter((r) => r.status === "operational").length} operational`}267 className="scroll-mt-28"268 aside={269 <Link href={hrefWith(BASE, sp, { provider: g.slug })} className="text-[12px] text-ink-3 hover:text-ink">270 Only {g.name}271 </Link>272 }273 >274 <RegionTable items={g.items} showProvider={false} sp={sp} sort={null} />275 </Section>276 ))}277 </div>278 )}279 </Page>280 );281}282