spb/datacenterindex
Public
HTML 53.9%
TypeScript 44.5%
JavaScript 0.6%
SQL 0.5%
1import type { Metadata } from "next";2import Link from "next/link";3import { ExtLink } from "@/components/entity/kv";4import { Button } from "@/components/ui/button";5import { Page, PageHeader } from "@/components/ui/card";6import { CountryFlag } from "@/components/ui/country-flag";7import { FilterChips, type Chip } from "@/components/ui/filter-chips";8import { Pagination } from "@/components/ui/pagination";9import { EmptyState, OfflineNotice } from "@/components/ui/states";10import { RowLink, TBody, THead, Table, Td, Th, Tr } from "@/components/ui/table";11import { api } from "@/lib/api";12import { countryName, fmtInt } from "@/lib/format";13import { routes } from "@/lib/routes";14import { pageMetadata } from "@/lib/seo";15import { hrefWith, spGet, spInt, type SearchParams } from "@/lib/sort";1617export const revalidate = 120;1819export const metadata: Metadata = pageMetadata({20 title: "Internet exchanges",21 description: "Internet exchange points (IXPs) with connected networks and the data centers where they are present.",22 path: routes.ixps(),23});2425const BASE = routes.ixps();26const PER_PAGE = 100;27const input = "h-9 rounded-sm border border-hair-2 bg-surface px-2 text-[13px] text-ink placeholder:text-ink-4 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent";2829export default async function IxpsPage({ searchParams }: { searchParams: Promise<SearchParams> }) {30 const sp = await searchParams;31 const q = spGet(sp, "q");32 const country = spGet(sp, "country")?.toUpperCase();33 const page = spInt(sp, "page", 1);34 const [res, dash, countries] = await Promise.all([api.ixps({ q, country, page, per_page: PER_PAGE }), api.dashboard(), api.countries()]);35 const ok = res.total > 0 || res.items.length > 0 || (!q && !country && dash.ok);36 const total = res.total;37 const statIxps = dash.ok ? dash.data.stats.ixps : null;38 const countryOptions = countries.items.filter((c) => c.facilityCount > 0).sort((a, b) => a.name.localeCompare(b.name));3940 const chips: Chip[] = [];41 if (q) chips.push({ key: "q", label: "Search", value: q, removeHref: hrefWith(BASE, sp, { q: undefined }) });42 if (country) chips.push({ key: "country", label: "Country", value: countryName(country), removeHref: hrefWith(BASE, sp, { country: undefined }) });4344 return (45 <Page>46 <PageHeader47 eyebrow="Index"48 title="Internet exchanges"49 description={total > 0 ? `${fmtInt(total)} internet exchange points indexed with their connected networks and the facilities where they are present.` : "Internet exchange points (IXPs), the networks they connect and the data centers where they are present."}50 aside={51 <Button href={routes.datacenters()} size="sm">52 Data centers53 </Button>54 }55 />56 {!ok && <OfflineNotice className="mb-3" />}5758 <form method="get" action={BASE} className="flex flex-wrap items-center gap-2">59 <label className="sr-only" htmlFor="ixp-q">60 Search61 </label>62 <input id="ixp-q" name="q" type="search" defaultValue={q ?? ""} placeholder="Name, e.g. DE-CIX" className={`${input} w-full sm:w-64`} />63 <label className="sr-only" htmlFor="ixp-country">64 Country65 </label>66 <select id="ixp-country" name="country" defaultValue={country ?? ""} className={`${input} max-w-[60vw]`}>67 <option value="">All countries</option>68 {countryOptions.map((c) => (69 <option key={c.iso2} value={c.iso2}>70 {c.name}71 </option>72 ))}73 </select>74 <Button type="submit" size="sm" variant="primary">75 Apply76 </Button>77 </form>78 <FilterChips chips={chips} resetHref={BASE} className="mt-2" />7980 <div className="mt-4">81 {res.items.length === 0 ? (82 q || country ? (83 <EmptyState title="No internet exchange matches these filters" description="Loosen a filter or clear them all." />84 ) : (85 <EmptyState86 title="No internet exchange indexed yet"87 description={88 <>89 PeeringDB IXP data is being ingested; facilities' IXP presence appears on facility pages as soon as it is observed.90 {statIxps !== null && statIxps > 0 && <> The index currently counts {fmtInt(statIxps)} exchanges.</>}91 </>92 }93 action={94 <Link href={routes.datacenters()} className="text-[13px] text-ink underline decoration-hair-2 hover:text-accent">95 Browse data centers96 </Link>97 }98 />99 )100 ) : (101 <>102 <Table dense>103 <THead>104 <tr>105 <Th>Exchange</Th>106 <Th className="hidden sm:table-cell">City</Th>107 <Th>Country</Th>108 <Th numeric>Networks</Th>109 <Th numeric>Facilities</Th>110 <Th className="hidden md:table-cell">Website</Th>111 </tr>112 </THead>113 <TBody>114 {res.items.map((x) => (115 <Tr key={x.id}>116 <Td primary>117 <RowLink href={routes.ixp(x.slug)}>{x.name}</RowLink>118 {x.nameLong && x.nameLong !== x.name && <div className="text-[11.5px] text-ink-3">{x.nameLong}</div>}119 {x.city && <div className="text-[11.5px] text-ink-3 sm:hidden">{x.city}</div>}120 </Td>121 <Td label="City" className="hidden text-ink-3 sm:table-cell">122 {x.city ?? "—"}123 </Td>124 <Td label="Country">125 <CountryFlag iso2={x.countryIso2} withName className="max-w-[160px] text-ink-2" />126 </Td>127 <Td label="Networks" numeric lead>128 {x.networkCount === null ? <span className="text-ink-4">—</span> : fmtInt(x.networkCount)}129 </Td>130 <Td label="Facilities" numeric>131 {fmtInt(x.facilityCount)}132 </Td>133 <Td label="Website" className="hidden text-[12.5px] md:table-cell">134 {x.website ? <ExtLink href={x.website} className="text-ink-2 no-underline hover:underline" /> : <span className="text-ink-4">—</span>}135 </Td>136 </Tr>137 ))}138 </TBody>139 </Table>140 <Pagination page={page} perPage={PER_PAGE} total={total} hrefFor={(p) => hrefWith(BASE, sp, { page: p > 1 ? p : undefined })} label="exchanges" className="mt-3" />141 </>142 )}143 </div>144 <p className="mt-8 text-[12px] text-ink-4">Exchange and network data: PeeringDB (attribution requested) and exchange operators. Network counts are as published by the source at the last observation.</p>145 </Page>146 );147}148