-
Notifications
You must be signed in to change notification settings - Fork 16
feat(apps/ensadmin): add interactive REST API playground to ENSAdmin #1951
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
f6c98bb
2634a36
df03a48
b02d222
3192877
da1a457
955db50
ba62521
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "ensadmin": patch | ||
|
vercel[bot] marked this conversation as resolved.
|
||
| --- | ||
|
|
||
| Introduced interactive REST API Reference playground (`/api/rest`) powered by Scalar, enabling discovery and testing of all REST APIs published by a connected ENSApi instance. Added `@ensnode/scalar-react` wrapper package. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| "use client"; | ||
|
|
||
| import { CopyButton } from "@namehash/namehash-ui"; | ||
| import { CheckIcon, CopyIcon } from "lucide-react"; | ||
|
|
||
| import { useOpenApiUrl } from "@/hooks/active/use-openapi-url"; | ||
|
|
||
| export default function Actions() { | ||
| const url = useOpenApiUrl(); | ||
|
|
||
| return ( | ||
| <div className="flex w-full max-w-md min-w-0 items-center space-x-2"> | ||
| <span className="font-mono text-xs select-none text-gray-500 truncate min-w-0" title={url}> | ||
| {url} | ||
| </span> | ||
| <CopyButton | ||
| value={url} | ||
| message="URL copied to clipboard!" | ||
| successIcon={<CheckIcon className="h-4 w-4" />} | ||
| icon={<CopyIcon className="h-4 w-4" />} | ||
| showToast={true} | ||
| /> | ||
| </div> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import { BreadcrumbItem, BreadcrumbPage } from "@/components/ui/breadcrumb"; | ||
|
|
||
| export default function Page() { | ||
| return ( | ||
| <BreadcrumbItem> | ||
| <BreadcrumbPage>REST API Reference</BreadcrumbPage> | ||
| </BreadcrumbItem> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| import { LoadingSpinner } from "@/components/loading-spinner"; | ||
|
|
||
| export default function Loading() { | ||
| return ( | ||
| <div className="flex flex-col items-center justify-center h-screen"> | ||
| <LoadingSpinner className="h-16 w-16" /> | ||
| </div> | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| "use client"; | ||
|
|
||
| import { ScalarApiReference } from "@ensnode/scalar-react"; | ||
|
|
||
| import { RequireENSAdminFeature } from "@/components/require-ensadmin-feature"; | ||
| import { useOpenApiUrl } from "@/hooks/active/use-openapi-url"; | ||
| import { useValidatedSelectedConnection } from "@/hooks/active/use-selected-connection"; | ||
|
|
||
| function RestApiPage() { | ||
| const url = useOpenApiUrl(); | ||
| const selectedConnection = useValidatedSelectedConnection(); | ||
|
|
||
| return <ScalarApiReference url={url} serverUrl={selectedConnection.toString()} />; | ||
|
notrab marked this conversation as resolved.
|
||
| } | ||
|
|
||
| export default function Page() { | ||
| return ( | ||
| <RequireENSAdminFeature title="REST API Reference" feature="restApi"> | ||
| <RestApiPage /> | ||
| </RequireENSAdminFeature> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| "use client"; | ||
|
|
||
| import { useMemo } from "react"; | ||
|
|
||
| import { useValidatedSelectedConnection } from "@/hooks/active/use-selected-connection"; | ||
|
|
||
| export function useOpenApiUrl(): string { | ||
| const selectedConnection = useValidatedSelectedConnection(); | ||
| return useMemo( | ||
| () => new URL("/openapi.json", selectedConnection).toString(), | ||
| [selectedConnection], | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| { | ||
| "name": "@ensnode/scalar-react", | ||
| "private": true, | ||
| "version": "0.0.0", | ||
| "type": "module", | ||
| "description": "Scalar API Reference React component for ENSNode", | ||
| "license": "MIT", | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "git+https://github.com/namehash/ensnode.git", | ||
| "directory": "packages/scalar-react" | ||
| }, | ||
| "homepage": "https://github.com/namehash/ensnode/tree/main/packages/scalar-react", | ||
| "exports": { | ||
| ".": "./src/index.ts" | ||
| }, | ||
| "scripts": { | ||
| "lint": "biome check --write .", | ||
| "lint:ci": "biome ci", | ||
| "typecheck": "tsgo --noEmit" | ||
| }, | ||
| "peerDependencies": { | ||
| "react": "^18.0.0 || ^19.0.0", | ||
| "react-dom": "^18.0.0 || ^19.0.0" | ||
| }, | ||
| "dependencies": { | ||
| "@scalar/api-reference-react": "^0.5.2" | ||
| }, | ||
| "devDependencies": { | ||
| "@ensnode/shared-configs": "workspace:*", | ||
| "@types/react": "catalog:", | ||
| "react": "catalog:", | ||
| "react-dom": "catalog:", | ||
| "typescript": "catalog:" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| "use client"; | ||
|
|
||
| import { ApiReferenceReact, type ReferenceProps } from "@scalar/api-reference-react"; | ||
| import { useEffect, useMemo, useState } from "react"; | ||
| import "@scalar/api-reference-react/style.css"; | ||
|
|
||
| interface ScalarApiReferenceProps { | ||
| /** URL to the OpenAPI spec (e.g. `https://api.alpha.ensnode.io/openapi.json`) */ | ||
| url: string; | ||
| /** | ||
| * Overrides the `servers` list in the OpenAPI spec so the playground targets | ||
| * this base URL instead (e.g. the currently active connection). | ||
| */ | ||
| serverUrl?: string; | ||
| } | ||
|
|
||
| const CUSTOM_CSS = ` | ||
| .scalar-api-reference { --scalar-y-offset: 0; } | ||
| .references-layout { | ||
| height: calc(100svh - var(--ensadmin-header-height, 4rem)) !important; | ||
| min-height: 0 !important; | ||
| max-height: calc(100svh - var(--ensadmin-header-height, 4rem)) !important; | ||
| --full-height: calc(100svh - var(--ensadmin-header-height, 4rem)) !important; | ||
| grid-template-rows: var(--scalar-header-height, 0px) 1fr auto !important; | ||
| } | ||
| .references-rendered { | ||
| overflow-y: auto !important; | ||
| min-height: 0 !important; | ||
| } | ||
| .references-navigation-list { | ||
| height: 100% !important; | ||
| } | ||
|
notrab marked this conversation as resolved.
|
||
| .scalar-app, .scalar-api-reference { | ||
| --scalar-font: var(--font-inter), -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; | ||
| } | ||
| .light-mode { | ||
| --scalar-color-1: #121212; | ||
| --scalar-color-2: rgba(0, 0, 0, 0.6); | ||
| --scalar-color-3: rgba(0, 0, 0, 0.4); | ||
| --scalar-color-accent: hsl(222.2, 47.4%, 11.2%); | ||
| --scalar-background-1: #ffffff; | ||
| --scalar-background-2: #f6f5f4; | ||
| --scalar-background-3: #f1ede9; | ||
| --scalar-background-accent: color-mix(in srgb, hsl(222.2, 47.4%, 11.2%) 6%, transparent); | ||
| --scalar-border-color: hsl(214.3, 31.8%, 91.4%); | ||
| } | ||
| .scalar-mcp-layer { display: none !important; } | ||
| .section { padding-inline: 0 !important; } | ||
| .section-container:not(.section-container .section-container) { | ||
| padding-inline: clamp(16px, 4vw, 60px) !important; | ||
| } | ||
| `; | ||
|
|
||
| export function ScalarApiReference({ url, serverUrl }: ScalarApiReferenceProps) { | ||
| const [mounted, setMounted] = useState(false); | ||
| useEffect(() => { | ||
| setMounted(true); | ||
| }, []); | ||
|
|
||
|
Comment on lines
+54
to
+59
|
||
| const configuration = useMemo<NonNullable<ReferenceProps["configuration"]>>( | ||
| () => ({ | ||
| url, | ||
| servers: serverUrl ? [{ url: serverUrl }] : undefined, | ||
| theme: "none", | ||
| hideDownloadButton: true, | ||
| hiddenClients: true, | ||
| defaultOpenAllTags: true, | ||
| forceDarkModeState: "light", | ||
| hideDarkModeToggle: true, | ||
| withDefaultFonts: false, | ||
| hideClientButton: true, | ||
| customCss: CUSTOM_CSS, | ||
| }), | ||
| [url, serverUrl], | ||
| ); | ||
|
|
||
| if (!mounted) return null; | ||
|
|
||
| return <ApiReferenceReact configuration={configuration} />; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { ScalarApiReference } from "./api-reference"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| { | ||
| "extends": "@ensnode/shared-configs/tsconfig.lib.json", | ||
| "compilerOptions": { | ||
| "jsx": "react-jsx", | ||
| "rootDir": ".", | ||
| "types": ["react"] | ||
| }, | ||
| "include": ["./**/*.ts", "./**/*.tsx"], | ||
| "exclude": ["dist", "**/__tests__/**"] | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.