Skip to content

Commit b0d2bd8

Browse files
authored
Merge pull request #3309 from ava-labs/fumadocs-openapi/customize
`OpenAPI` bump fumadocs + customize API page
2 parents 68cafed + 2344a56 commit b0d2bd8

15 files changed

+493
-150
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ content/docs/tooling/cli-commands.mdx
222222
# Generated OpenAPI specs (fetched during build)
223223
public/openapi/glacier.json
224224
public/openapi/popsicle.json
225+
public/openapi/speakeasy.yaml
225226

226227
# Generated OpenAPI documentation (auto-generated during build)
227228
content/docs/api-reference/data-api/**/*.mdx

app/docs/[...slug]/page.tsx

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
1-
import { APIStorageManager } from "@/components/content-design/api-storage-manager";
21
import Mermaid from "@/components/content-design/mermaid";
32
import { AutoTypeTable } from "@/components/content-design/type-table";
43
import YouTube from "@/components/content-design/youtube";
54
import { BackToTop } from "@/components/ui/back-to-top";
65
import { Feedback } from "@/components/ui/feedback";
76
import { SidebarActions } from "@/components/ui/sidebar-actions";
8-
import { cChainApi, dataApi, metricsApi, pChainApi } from "@/lib/openapi";
7+
import { CChainAPIPage, DataAPIPage, MetricsAPIPage, PChainAPIPage, XChainAPIPage } from "@/components/api/api-pages";
98
import { documentation } from "@/lib/source";
109
import { createMetadata } from "@/utils/metadata";
11-
import { APIPage } from "fumadocs-openapi/ui";
1210
import { Popup, PopupContent, PopupTrigger } from "fumadocs-twoslash/ui";
1311
import { Accordion, Accordions } from "fumadocs-ui/components/accordion";
1412
import { Callout } from "fumadocs-ui/components/callout";
@@ -127,29 +125,19 @@ export default async function Page(props: {
127125
const isMetricsApi = document.includes('popsicle.json');
128126
const isPChainApi = document.includes('platformvm.yaml');
129127
const isCChainApi = document.includes('coreth.yaml');
128+
const isXChainApi = document.includes('xchain.yaml');
130129

131-
let apiInstance;
132-
let storageKey;
133130
if (isPChainApi) {
134-
apiInstance = pChainApi;
135-
storageKey = 'apiBaseUrl-pchain';
131+
return <PChainAPIPage {...props} />;
136132
} else if (isCChainApi) {
137-
apiInstance = cChainApi;
138-
storageKey = 'apiBaseUrl-cchain';
133+
return <CChainAPIPage {...props} />;
134+
} else if (isXChainApi) {
135+
return <XChainAPIPage {...props} />;
139136
} else if (isMetricsApi) {
140-
apiInstance = metricsApi;
141-
storageKey = 'apiBaseUrl-metrics';
137+
return <MetricsAPIPage {...props} />;
142138
} else {
143-
apiInstance = dataApi;
144-
storageKey = 'apiBaseUrl-data';
139+
return <DataAPIPage {...props} />;
145140
}
146-
147-
return (
148-
<>
149-
<APIStorageManager storageKey={storageKey} />
150-
<APIPage {...apiInstance.getAPIPageProps(props)} />
151-
</>
152-
);
153141
},
154142
blockquote: Callout as unknown as FC<ComponentProps<"blockquote">>,
155143
}}

app/global.css

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1208,3 +1208,33 @@ body[data-hide-sidebar-dropdown] #nd-sidebar-mobile button:has(> div > p.text-sm
12081208
margin-top: 0 !important;
12091209
}
12101210

1211+
/* API Playground Send button customization - make it blue (All APIs) */
1212+
.pchain-api-playground form.not-prose button[type="submit"].bg-fd-primary,
1213+
.cchain-api-playground form.not-prose button[type="submit"].bg-fd-primary,
1214+
.xchain-api-playground form.not-prose button[type="submit"].bg-fd-primary,
1215+
.data-api-playground form.not-prose button[type="submit"].bg-fd-primary,
1216+
.metrics-api-playground form.not-prose button[type="submit"].bg-fd-primary,
1217+
.webhooks-api-playground form.not-prose button[type="submit"].bg-fd-primary {
1218+
background-color: #3b82f6 !important; /* Blue-500 */
1219+
color: white !important;
1220+
}
1221+
1222+
.pchain-api-playground form.not-prose button[type="submit"].bg-fd-primary:hover,
1223+
.cchain-api-playground form.not-prose button[type="submit"].bg-fd-primary:hover,
1224+
.xchain-api-playground form.not-prose button[type="submit"].bg-fd-primary:hover,
1225+
.data-api-playground form.not-prose button[type="submit"].bg-fd-primary:hover,
1226+
.metrics-api-playground form.not-prose button[type="submit"].bg-fd-primary:hover,
1227+
.webhooks-api-playground form.not-prose button[type="submit"].bg-fd-primary:hover {
1228+
background-color: #2563eb !important; /* Blue-600 on hover */
1229+
}
1230+
1231+
.pchain-api-playground form.not-prose button[type="submit"].bg-fd-primary:disabled,
1232+
.cchain-api-playground form.not-prose button[type="submit"].bg-fd-primary:disabled,
1233+
.xchain-api-playground form.not-prose button[type="submit"].bg-fd-primary:disabled,
1234+
.data-api-playground form.not-prose button[type="submit"].bg-fd-primary:disabled,
1235+
.metrics-api-playground form.not-prose button[type="submit"].bg-fd-primary:disabled,
1236+
.webhooks-api-playground form.not-prose button[type="submit"].bg-fd-primary:disabled {
1237+
background-color: #93c5fd !important; /* Blue-300 when disabled */
1238+
opacity: 0.5;
1239+
}
1240+

components/api/api-pages.tsx

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { dataApi, metricsApi, pChainApi, cChainApi, xChainApi } from '@/lib/openapi';
2+
import { createAPIPage } from 'fumadocs-openapi/ui';
3+
import dataApiClient from './data-api-page.client';
4+
import metricsApiClient from './metrics-api-page.client';
5+
import pchainApiClient from './pchain-api-page.client';
6+
import cchainApiClient from './cchain-api-page.client';
7+
import xchainApiClient from './xchain-api-page.client';
8+
9+
const DataAPIPageBase = createAPIPage(dataApi, {
10+
client: dataApiClient,
11+
});
12+
13+
export function DataAPIPage(props: any) {
14+
return (
15+
<div className="data-api-playground">
16+
<DataAPIPageBase {...props} />
17+
</div>
18+
);
19+
}
20+
21+
const MetricsAPIPageBase = createAPIPage(metricsApi, {
22+
client: metricsApiClient,
23+
});
24+
25+
export function MetricsAPIPage(props: any) {
26+
return (
27+
<div className="metrics-api-playground">
28+
<MetricsAPIPageBase {...props} />
29+
</div>
30+
);
31+
}
32+
33+
const PChainAPIPageBase = createAPIPage(pChainApi, {
34+
client: pchainApiClient,
35+
});
36+
37+
export function PChainAPIPage(props: any) {
38+
return (
39+
<div className="pchain-api-playground">
40+
<PChainAPIPageBase {...props} />
41+
</div>
42+
);
43+
}
44+
45+
const CChainAPIPageBase = createAPIPage(cChainApi, {
46+
client: cchainApiClient,
47+
});
48+
49+
export function CChainAPIPage(props: any) {
50+
return (
51+
<div className="cchain-api-playground">
52+
<CChainAPIPageBase {...props} />
53+
</div>
54+
);
55+
}
56+
57+
const XChainAPIPageBase = createAPIPage(xChainApi, {
58+
client: xchainApiClient,
59+
});
60+
61+
export function XChainAPIPage(props: any) {
62+
return (
63+
<div className="xchain-api-playground">
64+
<XChainAPIPageBase {...props} />
65+
</div>
66+
);
67+
}
68+
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
'use client';
2+
import { defineClientConfig } from 'fumadocs-openapi/ui/client';
3+
import { BodyFieldWithExpandedParams } from './rpc-api-page.client';
4+
5+
export default defineClientConfig({
6+
storageKeyPrefix: 'fumadocs-openapi-cchain-',
7+
playground: {
8+
renderBodyField: (fieldName, info) => {
9+
return <BodyFieldWithExpandedParams fieldName={fieldName} info={info} />;
10+
},
11+
},
12+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
'use client';
2+
import { defineClientConfig } from 'fumadocs-openapi/ui/client';
3+
4+
export default defineClientConfig({
5+
storageKeyPrefix: 'fumadocs-openapi-data-',
6+
});
7+
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
'use client';
2+
import { defineClientConfig } from 'fumadocs-openapi/ui/client';
3+
4+
export default defineClientConfig({
5+
storageKeyPrefix: 'fumadocs-openapi-metrics-',
6+
});
7+
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
'use client';
2+
import { defineClientConfig } from 'fumadocs-openapi/ui/client';
3+
import { BodyFieldWithExpandedParams } from './rpc-api-page.client';
4+
5+
export default defineClientConfig({
6+
storageKeyPrefix: 'fumadocs-openapi-pchain-',
7+
playground: {
8+
renderBodyField: (fieldName, info) => {
9+
return <BodyFieldWithExpandedParams fieldName={fieldName} info={info} />;
10+
},
11+
},
12+
});
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
'use client';
2+
import { Custom } from 'fumadocs-openapi/playground/client';
3+
import { useState } from 'react';
4+
5+
export function BodyFieldWithExpandedParams({
6+
fieldName,
7+
info
8+
}: {
9+
fieldName: 'body';
10+
info: { schema: any; mediaType: string }
11+
}) {
12+
const { field } = Custom.useController({ name: fieldName });
13+
const [expandedFields, setExpandedFields] = useState<Set<string>>(new Set(['params']));
14+
15+
const toggleField = (key: string) => {
16+
setExpandedFields(prev => {
17+
const newSet = new Set(prev);
18+
if (newSet.has(key)) {
19+
newSet.delete(key);
20+
} else {
21+
newSet.add(key);
22+
}
23+
return newSet;
24+
});
25+
};
26+
27+
const renderSchema = (schema: any, path: string = '', level: number = 0): React.ReactNode => {
28+
if (!schema || typeof schema !== 'object') return null;
29+
30+
const properties = schema.properties || {};
31+
const required = schema.required || [];
32+
33+
return Object.entries(properties).map(([key, propSchema]: [string, any]) => {
34+
const fieldPath = path ? `${path}.${key}` : key;
35+
const isRequired = required.includes(key);
36+
const isObject = propSchema.type === 'object';
37+
const isExpanded = expandedFields.has(key);
38+
39+
return (
40+
<fieldset key={fieldPath} className={`flex flex-col gap-1.5 ${isObject && isExpanded ? 'col-span-full @container' : ''}`}>
41+
<label htmlFor={`body.${fieldPath}`} className="w-full inline-flex items-center gap-0.5">
42+
{isObject && (
43+
<button
44+
type="button"
45+
onClick={() => toggleField(key)}
46+
className="inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors duration-100 disabled:pointer-events-none disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fd-ring hover:bg-fd-accent hover:text-fd-accent-foreground p-1 [&_svg]:size-4 text-fd-muted-foreground -ms-1"
47+
>
48+
<svg
49+
xmlns="http://www.w3.org/2000/svg"
50+
width="24"
51+
height="24"
52+
viewBox="0 0 24 24"
53+
fill="none"
54+
stroke="currentColor"
55+
strokeWidth="2"
56+
strokeLinecap="round"
57+
strokeLinejoin="round"
58+
style={{ transform: isExpanded ? 'rotate(180deg)' : 'rotate(0deg)', transition: 'transform 0.2s' }}
59+
>
60+
<path d="m6 9 6 6 6-6"></path>
61+
</svg>
62+
</button>
63+
)}
64+
<span className="text-xs font-medium text-fd-foreground peer-disabled:cursor-not-allowed peer-disabled:opacity-70 font-mono me-auto">
65+
{key}
66+
{isRequired && <span className="text-red-400/80 mx-1">*</span>}
67+
</span>
68+
<code className="text-xs text-fd-muted-foreground">{propSchema.type || 'any'}</code>
69+
</label>
70+
71+
{!isObject && (
72+
<div className="flex flex-row gap-2">
73+
<input
74+
className="flex h-9 w-full rounded-md border bg-fd-secondary px-2 py-1.5 text-[13px] text-fd-secondary-foreground transition-colors placeholder:text-fd-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-fd-ring disabled:cursor-not-allowed disabled:opacity-50"
75+
id={`body.${fieldPath}`}
76+
placeholder={propSchema.default || "Enter value"}
77+
type={propSchema.type === 'integer' || propSchema.type === 'number' ? 'number' : 'text'}
78+
value={(field.value && typeof field.value === 'object' && key in field.value ? (field.value as Record<string, any>)[key] : propSchema.default) || ''}
79+
onChange={(e) => {
80+
const newValue = propSchema.type === 'integer' || propSchema.type === 'number'
81+
? Number(e.target.value)
82+
: e.target.value;
83+
const currentValue = (field.value && typeof field.value === 'object') ? field.value as Record<string, any> : {};
84+
field.onChange({ ...currentValue, [key]: newValue });
85+
}}
86+
name={`body.${fieldPath}`}
87+
{...(propSchema.type === 'number' || propSchema.type === 'integer' ? { step: propSchema.type === 'integer' ? '1' : 'any' } : {})}
88+
/>
89+
</div>
90+
)}
91+
92+
{isObject && isExpanded && (
93+
<div className="grid grid-cols-1 gap-4 @md:grid-cols-2 rounded-lg border border-fd-primary/20 bg-fd-background/50 p-2 shadow-sm ml-4">
94+
{renderSchema(propSchema, fieldPath, level + 1)}
95+
</div>
96+
)}
97+
</fieldset>
98+
);
99+
});
100+
};
101+
102+
return (
103+
<div className="grid grid-cols-1 gap-4 @md:grid-cols-2 rounded-lg border border-fd-primary/20 bg-fd-background/50 p-2 shadow-sm">
104+
{renderSchema(info.schema)}
105+
</div>
106+
);
107+
}
108+
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
'use client';
2+
import { defineClientConfig } from 'fumadocs-openapi/ui/client';
3+
import { BodyFieldWithExpandedParams } from './rpc-api-page.client';
4+
5+
export default defineClientConfig({
6+
storageKeyPrefix: 'fumadocs-openapi-xchain-',
7+
playground: {
8+
renderBodyField: (fieldName, info) => {
9+
return <BodyFieldWithExpandedParams fieldName={fieldName} info={info} />;
10+
},
11+
},
12+
});

0 commit comments

Comments
 (0)