Skip to content

Commit 7e3ee21

Browse files
committed
Handle loading and errors
1 parent 4755be7 commit 7e3ee21

File tree

6 files changed

+286
-1
lines changed

6 files changed

+286
-1
lines changed
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
'use client';
2+
3+
import { use } from 'react';
4+
import { cn, ComparisonTable as Table } from '@theguild/components';
5+
import { functionalTones } from './functional-tones';
6+
import { CheckmarkIcon, XIcon } from './icons';
7+
8+
interface BenchmarkDatum {
9+
name: string;
10+
cases: {
11+
passed: number;
12+
failed: number;
13+
};
14+
suites: {
15+
passed: number;
16+
failed: number;
17+
};
18+
}
19+
20+
const dataJson = fetch('https://the-guild.dev/graphql/hive/federation-gateway-audit/data.json')
21+
.then(
22+
res =>
23+
// we didn't parse this, because we trust @kamilkisiela
24+
res.json() as Promise<BenchmarkDatum[]>,
25+
)
26+
.then(async data => {
27+
await new Promise(resolve => setTimeout(resolve, 10_000));
28+
return data;
29+
});
30+
31+
export function BenchmarkTableBody() {
32+
// we're fetching in client component to get fresh data without redeploy
33+
// if we don't need it THAT fresh, feel free to just await it in the parent component
34+
const data = use(dataJson);
35+
36+
return (
37+
<tbody className="">
38+
{data.map(row => {
39+
const compatibility = (row.cases.passed / (row.cases.passed + row.cases.failed)) * 100;
40+
41+
return (
42+
<Table.Row key={row.name} highlight={row.name === 'Hive Gateway'}>
43+
<Table.Cell
44+
className={cn(
45+
// todo: this is a bug in Components: we diverged from design
46+
row.name === 'Hive Gateway' ? '!bg-green-100' : '',
47+
'pl-5', // yes, the dot cuts in to the left per design
48+
'max-sm:pr-1.5',
49+
)}
50+
>
51+
<div className="flex items-center gap-2.5 whitespace-nowrap">
52+
<div
53+
className="size-3 rounded-full"
54+
style={{
55+
background:
56+
compatibility > 99
57+
? functionalTones.positiveBright
58+
: compatibility > 90
59+
? functionalTones.warning
60+
: functionalTones.criticalBright,
61+
}}
62+
/>
63+
{row.name}
64+
</div>
65+
</Table.Cell>
66+
<Table.Cell className="text-sm text-green-800">{compatibility.toFixed(2)}%</Table.Cell>
67+
<Table.Cell>
68+
<span
69+
className="inline-flex items-center gap-0.5 text-sm"
70+
style={{ color: functionalTones.positiveDark }}
71+
>
72+
<CheckmarkIcon className="size-4" /> {row.cases.passed}
73+
</span>
74+
{row.cases.failed > 0 && (
75+
<span
76+
className="ml-2 inline-flex items-center text-sm"
77+
style={{ color: functionalTones.criticalDark }}
78+
>
79+
<XIcon className="size-4" /> {row.cases.failed}
80+
</span>
81+
)}
82+
</Table.Cell>
83+
<Table.Cell>
84+
<span
85+
className="inline-flex items-center gap-0.5 text-sm"
86+
style={{ color: functionalTones.positiveDark }}
87+
>
88+
<CheckmarkIcon className="size-4" /> {row.suites.passed}
89+
</span>
90+
{row.suites.failed > 0 && (
91+
<span
92+
className="ml-2 inline-flex items-center text-sm"
93+
style={{ color: functionalTones.criticalDark }}
94+
>
95+
<XIcon className="size-4" /> {row.suites.failed}
96+
</span>
97+
)}
98+
</Table.Cell>
99+
</Table.Row>
100+
);
101+
})}
102+
</tbody>
103+
);
104+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* todo: move this to the design system as Tailwind classes
3+
*/
4+
export const functionalTones = {
5+
criticalBright: '#FD3325',
6+
criticalDark: ' #F81202',
7+
warning: '#FE8830',
8+
positiveBright: '#24D551',
9+
positiveDark: '#1BA13D',
10+
};
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// these are different than CheckIcon and CloseIcon we have in the design system
2+
3+
export function CheckmarkIcon(props: React.SVGProps<SVGSVGElement>) {
4+
return (
5+
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" {...props}>
6+
<path d="M6.66668 10.1134L12.7947 3.98608L13.7373 4.92875L6.66668 11.9994L2.42401 7.75675L3.36668 6.81408L6.66668 10.1134Z" />
7+
</svg>
8+
);
9+
}
10+
11+
export function XIcon(props: React.SVGProps<SVGSVGElement>) {
12+
return (
13+
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" {...props}>
14+
<path d="M7.99999 7.05806L11.3 3.75806L12.2427 4.70072L8.94266 8.00072L12.2427 11.3007L11.2993 12.2434L7.99932 8.94339L4.69999 12.2434L3.75732 11.3001L7.05732 8.00006L3.75732 4.70006L4.69999 3.75872L7.99999 7.05806Z" />
15+
</svg>
16+
);
17+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { Suspense } from 'react';
2+
import { CallToAction, cn, Heading, ComparisonTable as Table } from '@theguild/components';
3+
import { BenchmarkTableBody } from './benchmark-table-body';
4+
import { functionalTones } from './functional-tones';
5+
import { CheckmarkIcon, XIcon } from './icons';
6+
7+
export interface FederationCompatibleBenchmarksSectionProps
8+
extends React.HTMLAttributes<HTMLDivElement> {}
9+
10+
export function FederationCompatibleBenchmarksSection({
11+
className,
12+
...rest
13+
}: FederationCompatibleBenchmarksSectionProps) {
14+
return (
15+
<section
16+
className={cn(
17+
'text-green-1000 px-4 py-6 sm:py-12 md:px-6 lg:py-[120px] xl:px-[120px]',
18+
className,
19+
)}
20+
{...rest}
21+
>
22+
<header className="md:text-balance md:text-center">
23+
<Heading as="h1" size="lg">
24+
Federation-Compatible Gateway Benchmarks
25+
</Heading>
26+
<p className="mb-6 mt-4 text-green-800 md:mb-16">
27+
See the results of our open-source audit for Apollo Federation Gateways.
28+
</p>
29+
</header>
30+
<div className="my-6 flex items-start gap-6 max-md:flex-col md:mb-12 md:mt-16">
31+
<p className="text-pretty text-2xl/8 lg:text-[32px]/10">
32+
Learn how Hive Gateway performs against other gateways in&nbsp;terms of correctness and
33+
compliance with the Apollo Federation specification
34+
</p>
35+
<CallToAction
36+
variant="tertiary"
37+
href="https://the-guild.dev/graphql/hive/federation-gateway-audit"
38+
>
39+
Learn about our audit and methodology
40+
</CallToAction>
41+
</div>
42+
<div className="hive-focus nextra-scrollbar border-beige-400 [&_:is(td,th)]:border-beige-400 overflow-x-auto rounded-2xl border [scrollbar-width:auto] max-sm:-mx-8">
43+
<Table className="table w-full border-none max-sm:rounded-none max-sm:text-sm">
44+
<thead>
45+
<Table.Row className="*:text-left">
46+
<Table.Header className="whitespace-pre pl-6">
47+
Gateway
48+
<small className="block text-xs/[18px] text-green-800">Name and variant</small>
49+
</Table.Header>
50+
<Table.Header className="whitespace-pre sm:w-1/4">
51+
Compatibility
52+
<small className="block text-xs/[18px] text-green-800">
53+
Pass rate of test cases
54+
</small>
55+
</Table.Header>
56+
<Table.Header className="whitespace-pre sm:w-1/4">
57+
Test Cases
58+
<small className="block text-xs/[18px] text-green-800">
59+
All available test cases
60+
</small>
61+
</Table.Header>
62+
<Table.Header className="whitespace-pre sm:w-1/4">
63+
Test Suites
64+
<small className="block text-xs/[18px] text-green-800">
65+
Test cases grouped by feature
66+
</small>
67+
</Table.Header>
68+
</Table.Row>
69+
</thead>
70+
<Suspense
71+
fallback={
72+
<tbody aria-busy>
73+
<tr>
74+
<td colSpan={4} className="bg-beige-100 h-[347.5px] animate-pulse cursor-wait" />
75+
</tr>
76+
</tbody>
77+
}
78+
>
79+
<BenchmarkTableBody />
80+
</Suspense>
81+
</Table>
82+
</div>
83+
<BenchmarkLegend />
84+
</section>
85+
);
86+
}
87+
88+
function BenchmarkLegend() {
89+
return (
90+
<div className="mt-6 flex flex-wrap gap-2 whitespace-nowrap text-xs text-green-800 sm:gap-4">
91+
<div className="flex gap-2 max-sm:-mx-1 max-sm:w-full sm:contents">
92+
<div className="flex items-center gap-1">
93+
<CheckmarkIcon className="size-4" style={{ color: functionalTones.positiveDark }} />{' '}
94+
Passed tests
95+
</div>
96+
<div className="flex items-center gap-1">
97+
<XIcon className="size-4" style={{ color: functionalTones.criticalDark }} /> Failed tests
98+
</div>
99+
</div>
100+
<div className="flex items-center gap-2">
101+
<div
102+
className="size-2 rounded-full"
103+
style={{ background: functionalTones.positiveBright }}
104+
/>
105+
Perfect compatibility
106+
</div>
107+
<div className="flex items-center gap-2">
108+
<div className="size-2 rounded-full" style={{ background: functionalTones.warning }} />
109+
75% and higher
110+
</div>
111+
<div className="flex items-center gap-2">
112+
<div className="size-2 rounded-full" style={{ background: functionalTones.criticalDark }} />
113+
Less than 75%
114+
</div>
115+
</div>
116+
);
117+
}

packages/web/docs/src/app/gateway/page.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
HeroLogo,
99
HiveGatewayIcon,
1010
} from '@theguild/components';
11+
import { ErrorBoundary } from '../../components/error-boundary';
1112
import { LandingPageContainer } from '../../components/landing-page-container';
1213
import { metadata as rootMetadata } from '../layout';
1314
import { FederationCompatibleBenchmarksSection } from './federation-compatible-benchmarks';
@@ -55,7 +56,14 @@ export default function HiveGatewayPage() {
5556
</Hero>
5657
<GatewayFeatureTabs className="relative mt-6 sm:mt-[-72px] sm:bg-blue-100" />
5758
<OrchestrateYourWay className="mx-4 mt-6 sm:mx-8" />
58-
<FederationCompatibleBenchmarksSection />
59+
<ErrorBoundary
60+
fallback={
61+
// this section doesn't make sense if data didn't load, so we just unmount
62+
null
63+
}
64+
>
65+
<FederationCompatibleBenchmarksSection />
66+
</ErrorBoundary>
5967
{/* Let's get advanced */}
6068
{/* Cloud-Native Nature */}
6169
<ExploreMainProductCards className="max-lg:mx-4 max-lg:my-8" />
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
'use client';
2+
3+
import { Component } from 'react';
4+
5+
export class ErrorBoundary extends Component<{
6+
fallback: React.ReactNode;
7+
children: React.ReactNode;
8+
}> {
9+
state = { hasError: false };
10+
11+
static getDerivedStateFromError(error: Error) {
12+
console.error(error);
13+
// Update state so the next render will show the fallback UI.
14+
return { hasError: true };
15+
}
16+
17+
componentDidCatch(error: Error, info: { componentStack: string }) {
18+
console.error(error, info);
19+
}
20+
21+
render() {
22+
if (this.state.hasError) {
23+
// You can render any custom fallback UI
24+
return this.props.fallback;
25+
}
26+
27+
return this.props.children;
28+
}
29+
}

0 commit comments

Comments
 (0)