SPB Git forge
38commits 1branches 0releases
338.7 MBsize
maindefault branch
yesterdaylast push
HTML 53.9% TypeScript 44.5% JavaScript 0.6% SQL 0.5%
12.3 KB · 282 lines tsx
Raw Blame History
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