ui integration and rest api includes
This commit is contained in:
@@ -1,12 +1,15 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import EndpointCard from './EndpointCard'
|
||||
import { getTopicIcon } from '../lib/icons'
|
||||
import { BASE_URL } from '../data/topics'
|
||||
|
||||
// Strip the documented BASE_URL so fetches are same-origin and get
|
||||
// proxied by Vite (dev) or Express (prod), which injects the secret header.
|
||||
function toProxyPath(fullUrl) {
|
||||
if (fullUrl.startsWith(BASE_URL)) return fullUrl.slice(BASE_URL.length)
|
||||
import { LEGACY_BASE_URL } from '../data/topics'
|
||||
|
||||
function toProxyPath(fullUrl, baseUrl) {
|
||||
// Only proxy the legacy Hasura API (to inject the admin secret via server)
|
||||
if (baseUrl === LEGACY_BASE_URL && fullUrl.startsWith(baseUrl)) {
|
||||
return fullUrl.slice(baseUrl.length)
|
||||
}
|
||||
// Let the browser hit the new REST API directly (it supports CORS)
|
||||
return fullUrl
|
||||
}
|
||||
|
||||
@@ -21,8 +24,6 @@ export default function TopicView({ topic, searchQuery }) {
|
||||
)
|
||||
: topic.endpoints
|
||||
|
||||
// Only one endpoint's response is visible at a time. Sending on a new
|
||||
// endpoint replaces the previous one (and cancels its in-flight fetch).
|
||||
const [active, setActive] = useState(null)
|
||||
const abortRef = useRef(null)
|
||||
|
||||
@@ -41,12 +42,28 @@ export default function TopicView({ topic, searchQuery }) {
|
||||
|
||||
setActive({ name: endpoint.name, result: null, loading: true })
|
||||
const start = Date.now()
|
||||
|
||||
try {
|
||||
const res = await fetch(toProxyPath(composedUrl), {
|
||||
const fetchOptions = {
|
||||
method: endpoint.method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
signal: controller.signal
|
||||
})
|
||||
}
|
||||
|
||||
if (['POST', 'PUT', 'PATCH'].includes(endpoint.method) && endpoint.body) {
|
||||
if (typeof endpoint.body === 'string') {
|
||||
try {
|
||||
// Re-stringify in case it's a badly formatted JSON string, but mostly it's just raw string
|
||||
fetchOptions.body = JSON.stringify(JSON.parse(endpoint.body))
|
||||
} catch {
|
||||
fetchOptions.body = endpoint.body
|
||||
}
|
||||
} else {
|
||||
fetchOptions.body = JSON.stringify(endpoint.body)
|
||||
}
|
||||
}
|
||||
|
||||
const res = await fetch(toProxyPath(composedUrl, topic.baseUrl), fetchOptions)
|
||||
const ms = Date.now() - start
|
||||
const text = await res.text()
|
||||
let body
|
||||
@@ -71,23 +88,26 @@ export default function TopicView({ topic, searchQuery }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl">
|
||||
<div className="mb-8 flex items-start gap-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-brand-500 to-indigo-600 flex items-center justify-center shadow-glow flex-shrink-0">
|
||||
<Icon className="text-white w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-slate-900 tracking-tight">{topic.name}</h1>
|
||||
<p className="text-slate-600 mt-2 leading-relaxed">{topic.description}</p>
|
||||
<div className="mt-3 text-xs text-slate-400 uppercase tracking-wide">
|
||||
{filtered.length} endpoint{filtered.length === 1 ? '' : 's'}
|
||||
</div>
|
||||
<div className="pb-24">
|
||||
<div className="mb-12">
|
||||
<h1 className="text-4xl lg:text-5xl font-bold text-slate-900 mb-4 tracking-tight">{topic.name}</h1>
|
||||
<p className="text-lg text-slate-600 max-w-3xl leading-relaxed">{topic.description}</p>
|
||||
<div className="mt-4 flex items-center gap-3">
|
||||
<span className={`inline-flex items-center gap-1.5 text-[11px] font-bold uppercase tracking-widest px-3 py-1 rounded-full border ${
|
||||
topic.type === 'legacy'
|
||||
? 'bg-brand-50 text-brand-600 border-brand-100'
|
||||
: 'bg-indigo-50 text-indigo-600 border-indigo-100'
|
||||
}`}>
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-current"></span>
|
||||
{topic.type === 'legacy' ? 'Hasura API' : 'REST API'}
|
||||
</span>
|
||||
<span className="text-sm text-slate-400">{filtered.length} endpoint{filtered.length !== 1 ? 's' : ''}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{filtered.length === 0 ? (
|
||||
<div className="text-center py-16 text-slate-400 text-sm">
|
||||
<div className="text-center py-20 text-slate-400 text-sm bg-white rounded-2xl border border-slate-200/60">
|
||||
No endpoints match "{searchQuery}".
|
||||
</div>
|
||||
) : (
|
||||
@@ -95,8 +115,9 @@ export default function TopicView({ topic, searchQuery }) {
|
||||
const isActive = active?.name === e.name
|
||||
return (
|
||||
<EndpointCard
|
||||
key={`${topic.id}/${e.name}`}
|
||||
key={`${topic.uniqueId}/${e.name}`}
|
||||
endpoint={e}
|
||||
baseUrl={topic.baseUrl}
|
||||
onSend={handleSend}
|
||||
result={isActive ? active.result : null}
|
||||
loading={isActive ? active.loading : false}
|
||||
|
||||
Reference in New Issue
Block a user