SPB Git forge
38commits 1branches 0releases
338.7 MBsize
maindefault branch
6 h agolast push
HTML 53.9% TypeScript 44.5% JavaScript 0.6% SQL 0.5%
6.8 KB · 148 lines tsx
Raw Blame History
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&apos; 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