Fix admin SQL count parsing for local SpacetimeDB
This commit is contained in:
214
apps/admin-web/src/pages/AdminDebugHttpPage.tsx
Normal file
214
apps/admin-web/src/pages/AdminDebugHttpPage.tsx
Normal file
@@ -0,0 +1,214 @@
|
||||
import {Plus, Send, Trash2} from 'lucide-react';
|
||||
import {FormEvent, useMemo, useState} from 'react';
|
||||
|
||||
import {debugAdminHttp} from '../api/adminApiClient';
|
||||
import type {
|
||||
AdminDebugHeaderInput,
|
||||
AdminDebugHttpMethod,
|
||||
AdminDebugHttpResponse,
|
||||
} from '../api/adminApiTypes';
|
||||
import {formatUnknownJson, handlePageError} from './pageUtils';
|
||||
|
||||
interface AdminDebugHttpPageProps {
|
||||
token: string;
|
||||
onUnauthorized: (message?: string) => void;
|
||||
}
|
||||
|
||||
const httpMethods: AdminDebugHttpMethod[] = [
|
||||
'GET',
|
||||
'POST',
|
||||
'PUT',
|
||||
'PATCH',
|
||||
'DELETE',
|
||||
];
|
||||
|
||||
export function AdminDebugHttpPage({
|
||||
token,
|
||||
onUnauthorized,
|
||||
}: AdminDebugHttpPageProps) {
|
||||
const [method, setMethod] = useState<AdminDebugHttpMethod>('GET');
|
||||
const [path, setPath] = useState('/healthz');
|
||||
const [body, setBody] = useState('');
|
||||
const [headers, setHeaders] = useState<AdminDebugHeaderInput[]>([]);
|
||||
const [result, setResult] = useState<AdminDebugHttpResponse | null>(null);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const jsonPreview = useMemo(
|
||||
() => formatUnknownJson(result?.bodyJson),
|
||||
[result?.bodyJson],
|
||||
);
|
||||
|
||||
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
if (isSubmitting) {
|
||||
return;
|
||||
}
|
||||
|
||||
setErrorMessage('');
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const response = await debugAdminHttp(token, {
|
||||
method,
|
||||
path: path.trim(),
|
||||
headers: headers.filter((header) => header.name.trim()),
|
||||
body,
|
||||
});
|
||||
setResult(response);
|
||||
} catch (error: unknown) {
|
||||
handlePageError(error, onUnauthorized, setErrorMessage);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="admin-page">
|
||||
<div className="admin-page-heading">
|
||||
<div>
|
||||
<h2>API 调试</h2>
|
||||
<p>受控同源请求</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="admin-two-column">
|
||||
<form className="admin-panel admin-form" onSubmit={handleSubmit}>
|
||||
<div className="admin-form-row">
|
||||
<label className="admin-field admin-field-compact">
|
||||
<span>Method</span>
|
||||
<select
|
||||
value={method}
|
||||
onChange={(event) =>
|
||||
setMethod(event.target.value as AdminDebugHttpMethod)
|
||||
}
|
||||
>
|
||||
{httpMethods.map((item) => (
|
||||
<option key={item} value={item}>
|
||||
{item}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="admin-field admin-field-fill">
|
||||
<span>Path</span>
|
||||
<input
|
||||
value={path}
|
||||
onChange={(event) => setPath(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<section className="admin-subsection">
|
||||
<div className="admin-subsection-heading">
|
||||
<span>Headers</span>
|
||||
<button
|
||||
className="admin-ghost-button"
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setHeaders((current) => [
|
||||
...current,
|
||||
{name: '', value: ''},
|
||||
])
|
||||
}
|
||||
>
|
||||
<Plus size={16} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="admin-header-editor">
|
||||
{headers.map((header, index) => (
|
||||
<div className="admin-header-row" key={index}>
|
||||
<input
|
||||
value={header.name}
|
||||
onChange={(event) =>
|
||||
setHeaders((current) =>
|
||||
current.map((item, itemIndex) =>
|
||||
itemIndex === index
|
||||
? {...item, name: event.target.value}
|
||||
: item,
|
||||
),
|
||||
)
|
||||
}
|
||||
/>
|
||||
<input
|
||||
value={header.value}
|
||||
onChange={(event) =>
|
||||
setHeaders((current) =>
|
||||
current.map((item, itemIndex) =>
|
||||
itemIndex === index
|
||||
? {...item, value: event.target.value}
|
||||
: item,
|
||||
),
|
||||
)
|
||||
}
|
||||
/>
|
||||
<button
|
||||
className="admin-ghost-button"
|
||||
title="移除"
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setHeaders((current) =>
|
||||
current.filter((_, itemIndex) => itemIndex !== index),
|
||||
)
|
||||
}
|
||||
>
|
||||
<Trash2 size={16} aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<label className="admin-field">
|
||||
<span>Body</span>
|
||||
<textarea
|
||||
rows={9}
|
||||
value={body}
|
||||
onChange={(event) => setBody(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
|
||||
{errorMessage ? (
|
||||
<div className="admin-alert" role="status">
|
||||
{errorMessage}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<button
|
||||
className="admin-primary-button"
|
||||
disabled={isSubmitting || !path.trim().startsWith('/')}
|
||||
type="submit"
|
||||
>
|
||||
<Send size={17} aria-hidden="true" />
|
||||
<span>{isSubmitting ? '发送中' : '发送'}</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<section className="admin-panel admin-result-panel">
|
||||
<div className="admin-panel-heading">
|
||||
<h3>结果</h3>
|
||||
<span>{result ? `${result.status} ${result.statusText}` : '-'}</span>
|
||||
</div>
|
||||
{result ? (
|
||||
<>
|
||||
<dl className="admin-info-list">
|
||||
<div>
|
||||
<dt>Status</dt>
|
||||
<dd>{result.status}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Headers</dt>
|
||||
<dd>{result.headers.length}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<pre className="admin-code-block">
|
||||
{jsonPreview || result.bodyText || '(empty)'}
|
||||
</pre>
|
||||
</>
|
||||
) : (
|
||||
<div className="admin-empty-state">暂无结果</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user