-
-
Notifications
You must be signed in to change notification settings - Fork 182
feat(playground): add default execution plugins for HTTP and Webhook operations #1300
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
Changes from all commits
7fe00bc
917086e
c569e1e
48c7dc1
abe1769
008e1b4
1b05390
00ec54f
e13d1f0
6508c82
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,101 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| import React from 'react'; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| interface ExecutionResultProps { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| error: string | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| response: any; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+3
to
+5
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Define the execution response type. The repository enforces Proposed type contract+interface ExecutionResponse {
+ status: number;
+ statusText: string;
+ headers: Record<string, string>;
+ data: unknown;
+}
+
interface ExecutionResultProps {
error: string | null;
- response: any;
+ response: ExecutionResponse | null;
errorMessage?: string;
}
export const useExecution = () => {
- const [response, setResponse] = React.useState<any>(null);
+ const [response, setResponse] = React.useState<ExecutionResponse | null>(null);
const [error, setError] = React.useState<string | null>(null);
const [loading, setLoading] = React.useState(false);
const parseAndSetResponse = async (res: Response) => {
const data = await res.text();
- let parsedData: any = data;
+ let parsedData: unknown = data;📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
| errorMessage?: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| export const ExecutionResult: React.FC<ExecutionResultProps> = ({ error, response, errorMessage = 'Request Failed' }) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| {error && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="mt-4 p-4 bg-red-50 border border-red-200 rounded-md"> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <h5 className="text-sm font-medium text-red-800">{errorMessage}</h5> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <p className="mt-1 text-sm text-red-700">{error}</p> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| {response && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="mt-6 space-y-4"> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="flex items-center space-x-2"> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| response.status >= 200 && response.status < 300 | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ? 'bg-green-100 text-green-800' | ||||||||||||||||||||||||||||||||||||||||||||||||||
| : 'bg-red-100 text-red-800' | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }`}> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| {response.status} {response.statusText} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="border rounded-md overflow-hidden"> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="bg-gray-50 px-4 py-2 border-b"> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <h5 className="text-sm font-medium text-gray-700">Response Body</h5> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="p-4 bg-gray-900 overflow-x-auto"> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <pre className="text-sm text-gray-100 font-mono"> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| {typeof response.data === 'string' ? response.data : JSON.stringify(response.data, null, 2)} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </pre> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| {response.headers && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <details className="group"> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <summary className="text-sm font-medium text-gray-700 cursor-pointer hover:text-gray-900 flex items-center"> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <svg className="w-4 h-4 mr-1 transition-transform group-open:rotate-90" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </svg> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| Response Headers | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </summary> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="mt-2 p-4 bg-gray-50 border rounded-md overflow-x-auto"> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| <pre className="text-xs text-gray-600 font-mono"> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| {JSON.stringify(response.headers, null, 2)} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </pre> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </details> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||||||||
| </> | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| export const useExecution = () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const [response, setResponse] = React.useState<any>(null); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const [error, setError] = React.useState<string | null>(null); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const [loading, setLoading] = React.useState(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const parseAndSetResponse = async (res: Response) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const data = await res.text(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| let parsedData: any = data; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| parsedData = JSON.parse(data); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| console.debug('Failed to parse response as JSON', e); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| setResponse({ | ||||||||||||||||||||||||||||||||||||||||||||||||||
| status: res.status, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| statusText: res.statusText, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: Object.fromEntries(res.headers.entries()), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| data: parsedData, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const executeRequest = async (fetchPromise: Promise<Response>, defaultErrorMsg: string) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| setLoading(true); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| setError(null); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| setResponse(null); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const res = await fetchPromise; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| await parseAndSetResponse(res); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+85
to
+92
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Add cancellation and a request timeout.
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (err: any) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| setError(err.message || defaultErrorMsg); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| setLoading(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| return { response, error, loading, executeRequest, setError }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| import React from 'react'; | ||
| import { AsyncApiPlugin, PluginAPI, PluginSlot, PluginContext } from '@asyncapi/react-component'; | ||
| import { ExecutionResult, useExecution } from './ExecutionResult'; | ||
|
|
||
| const HttpExecutionComponent: React.FC<{ context: PluginContext }> = ({ context }) => { | ||
| const { schema } = context; | ||
| const schemaObj = schema as Record<string, any>; | ||
| const operation = schemaObj?.operation; | ||
| const channel = schemaObj?.channel; | ||
|
|
||
| const { response, error, loading, executeRequest } = useExecution(); | ||
|
|
||
| const hasHttpBinding = channel?.bindings()?.has('http') || operation?.bindings()?.has('http'); | ||
| const isWebhook = typeof operation?.isWebhook === 'function' ? operation.isWebhook() : false; | ||
|
|
||
| if (!hasHttpBinding || isWebhook) { | ||
| return null; | ||
| } | ||
|
|
||
| const handleExecute = async () => { | ||
| const servers = typeof channel?.servers === 'function' && channel.servers() ? channel.servers().all() : []; | ||
| const serverUrl = servers.length > 0 ? servers[0].url() : globalThis.location.origin; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Do not use the Playground origin when the operation has no server. If 🤖 Prompt for AI AgentsSource: Path instructions |
||
| const address = channel?.address() || ''; | ||
|
|
||
|
Comment on lines
+22
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Resolve server variables and channel parameters before calling 🤖 Prompt for AI Agents |
||
| const baseUrl = serverUrl.replace(/\/$/, ''); | ||
| const path = address.startsWith('/') ? address : `/${address}`; | ||
| const fullUrl = `${baseUrl}${path}`; | ||
|
|
||
| const operationBinding = operation?.bindings()?.get('http'); | ||
| const channelBinding = channel?.bindings()?.get('http'); | ||
| const method = operationBinding?.method || channelBinding?.method || 'GET'; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -euo pipefail
fd -a '^(package\.json|pnpm-lock\.yaml|yarn\.lock|package-lock\.json)$' . |
xargs rg -n -C 3 '`@asyncapi/parser`|`@asyncapi/react-component`'
rg -n -C 5 --type=ts --type=tsx \
"\.bindings\(\)\?*\.get\(['\"]http['\"]\)|\.method\(\)|\.method\b" \
library playgroundRepository: asyncapi/asyncapi-react Length of output: 8196 🤖 get_repo_knowledge executed:
Length of output: 9348 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- HttpPlugin.tsx ---'
cat -n playground/components/plugins/HttpPlugin.tsx | sed -n '1,90p'
printf '%s\n' '--- parser dependency declarations ---'
rg -n -C 4 '"`@asyncapi/parser`"|`@asyncapi/parser`' library/package.json package-lock.json
printf '%s\n' '--- binding and method usages ---'
rg -n -C 4 --glob '*.ts' --glob '*.tsx' \
'bindings\(\)|\.method\(\)|\.method\b|operationBinding|channelBinding' \
library playgroundRepository: asyncapi/asyncapi-react Length of output: 13708 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- parser package source or declarations, if present ---'
fd -a -i '(`@asyncapi`|parser)' . | head -200
printf '%s\n' '--- lockfile parser entry ---'
sed -n '1640,1675p' package-lock.json
printf '%s\n' '--- package metadata and exports, if available ---'
for f in node_modules/@asyncapi/parser/package.json \
node_modules/@asyncapi/parser/dist/models/v3/Message.d.ts \
node_modules/@asyncapi/parser/dist/models/v3/HTTPBinding.d.ts \
node_modules/@asyncapi/parser/dist/models/v3/OperationBinding.d.ts; do
if [ -f "$f" ]; then
echo "--- $f"
sed -n '1,220p' "$f"
fi
doneRepository: asyncapi/asyncapi-react Length of output: 1874 🌐 Web query:
💡 Result: In the Citations:
🏁 Script executed: #!/bin/bash
set -euo pipefail
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
curl -fsSL https://registry.npmjs.org/@asyncapi/parser/-/parser-3.6.3.tgz -o "$tmpdir/parser.tgz"
tar -xzf "$tmpdir/parser.tgz" -C "$tmpdir"
printf '%s\n' '--- HTTP binding declarations and implementations ---'
rg -n -C 5 --glob '*.{js,d.ts,ts}' \
'class HTTPBinding|HTTPBinding|method\(' "$tmpdir/package" | head -240Repository: asyncapi/asyncapi-react Length of output: 215 🏁 Script executed: #!/bin/bash
set -euo pipefail
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
curl -fsSL https://registry.npmjs.org/@asyncapi/parser/-/parser-3.6.3.tgz -o "$tmpdir/parser.tgz"
printf '%s\n' '--- package paths containing binding or model ---'
tar -tzf "$tmpdir/parser.tgz" | grep -Ei 'binding|model|index|api' | head -240
tar -xzf "$tmpdir/parser.tgz" -C "$tmpdir"
printf '%s\n' '--- package top-level files ---'
find "$tmpdir/package" -maxdepth 3 -type f | sort | head -240
printf '%s\n' '--- method references ---'
rg -n -C 4 'method' "$tmpdir/package" | head -240Repository: asyncapi/asyncapi-react Length of output: 8784 🌐 Web query:
💡 Result: There is no standard "HTTPBinding method property parser API." The term likely refers to the handling of protocol-specific bindings—specifically HTTP bindings—within the context of the AsyncAPI Parser-JS library [1][2]. In the AsyncAPI specification and its JavaScript parser (parser-js), "bindings" are used to define protocol-specific information (such as HTTP, Kafka, or AMQP) [3][4]. The parser provides methods to access these bindings as part of the document model [3][5]. Key technical points regarding this functionality include: 1. Binding Access: The parser includes a Citations:
🏁 Script executed: #!/bin/bash
set -euo pipefail
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
curl -fsSL https://registry.npmjs.org/@asyncapi/parser/-/parser-3.6.3.tgz -o "$tmpdir/parser.tgz"
tar -xzf "$tmpdir/parser.tgz" -C "$tmpdir"
for f in \
"$tmpdir/package/cjs/models/binding.js" \
"$tmpdir/package/cjs/models/v3/binding.js" \
"$tmpdir/package/cjs/models/bindings.js" \
"$tmpdir/package/cjs/models/v3/mixins.js"; do
if [ -f "$f" ]; then
echo "--- ${f#$tmpdir/package/}"
cat -n "$f" | sed -n '1,260p'
fi
done
printf '%s\n' '--- HTTP method handling in parser package ---'
rg -n -C 8 --no-heading 'http|method' \
"$tmpdir/package/cjs/models" "$tmpdir/package/esm/models" \
| grep -Ei 'binding|http|method' | head -240 || trueRepository: asyncapi/asyncapi-react Length of output: 6511 🏁 Script executed: #!/bin/bash
set -euo pipefail
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
curl -fsSL https://registry.npmjs.org/@asyncapi/parser/-/parser-3.6.3.tgz -o "$tmpdir/parser.tgz"
tar -xzf "$tmpdir/parser.tgz" -C "$tmpdir"
for f in \
"$tmpdir/package/cjs/models/base.js" \
"$tmpdir/package/cjs/models/v3/bindings.js" \
"$tmpdir/package/cjs/models/collection.js" \
"$tmpdir/package/cjs/models/v3/operation.js" \
"$tmpdir/package/cjs/models/v3/channel.js"; do
if [ -f "$f" ]; then
echo "--- ${f#$tmpdir/package/}"
cat -n "$f" | sed -n '1,260p'
fi
doneRepository: asyncapi/asyncapi-react Length of output: 10535 Read the HTTP method from the binding value
🤖 Prompt for AI AgentsSource: Path instructions |
||
|
|
||
| await executeRequest( | ||
| fetch(fullUrl, { | ||
| method: method.toUpperCase(), | ||
| headers: { | ||
| 'Accept': 'application/json, text/plain, */*', | ||
| }, | ||
| }), | ||
|
Comment on lines
+34
to
+39
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift Include the operation payload for methods that require a body. POST, PUT, and PATCH operations currently send an empty body. Operations with required message payloads will fail or execute with incorrect data. Generate or accept a schema-based payload. Set the applicable 🤖 Prompt for AI AgentsSource: Path instructions |
||
| 'Network failure. Ensure CORS is configured correctly and the server is reachable.' | ||
| ); | ||
| }; | ||
|
|
||
| return ( | ||
| <div className="mt-4 p-6 border rounded-lg bg-white shadow-sm"> | ||
| <div className="flex items-center justify-between mb-4"> | ||
| <div> | ||
| <h4 className="text-lg font-semibold text-gray-800">HTTP Execution</h4> | ||
| <p className="text-sm text-gray-500">Fire a real HTTP request using the operation's server bindings.</p> | ||
| </div> | ||
| <button | ||
| onClick={handleExecute} | ||
| disabled={loading} | ||
| className="px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded shadow hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 transition-colors" | ||
| > | ||
| {loading ? 'Executing...' : 'Send Request'} | ||
| </button> | ||
| </div> | ||
|
|
||
| <ExecutionResult error={error} response={response} errorMessage="Request Failed" /> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export const httpPlugin: AsyncApiPlugin = { | ||
| name: 'default-http-plugin', | ||
| version: '1.0.0', | ||
| description: 'Playground default plugin for HTTP execution', | ||
| install(api: PluginAPI) { | ||
| api.registerComponent(PluginSlot.OPERATION, HttpExecutionComponent); | ||
| } | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| import React, { useState } from 'react'; | ||
| import { AsyncApiPlugin, PluginAPI, PluginSlot, PluginContext } from '@asyncapi/react-component'; | ||
| import { ExecutionResult, useExecution } from './ExecutionResult'; | ||
|
|
||
| const WebhookExecutionComponent: React.FC<{ context: PluginContext }> = ({ context }) => { | ||
| const { schema } = context; | ||
| const schemaObj = schema as Record<string, any>; | ||
| const operation = schemaObj?.operation; | ||
|
|
||
| const [endpoint, setEndpoint] = useState(''); | ||
| const { response, error, loading, executeRequest, setError } = useExecution(); | ||
|
|
||
| const isWebhook = typeof operation?.isWebhook === 'function' ? operation.isWebhook() : false; | ||
|
|
||
| if (!isWebhook) { | ||
| return null; | ||
| } | ||
|
|
||
| // Extract expected payload from the first message | ||
| const messages = operation.messages ? operation.messages().all() : []; | ||
| const firstMessage = messages.length > 0 ? messages[0] : null; | ||
| const payloadSchema = firstMessage?.payload ? firstMessage.payload().json() : null; | ||
|
Comment on lines
+20
to
+22
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift Let the user select the webhook message. An operation can contain multiple messages. This code always sends the first message and cannot simulate the other valid payloads. Add a message selector or register one execution action for each message. 🤖 Prompt for AI AgentsSource: Path instructions |
||
|
|
||
| // A simple placeholder payload generation based on type | ||
| const generateMockPayload = (schema: any) => { | ||
| if (!schema) return {}; | ||
| if (schema.example) return schema.example; | ||
| if (schema.examples && schema.examples.length > 0) return schema.examples[0]; | ||
| if (schema.type === 'object' && schema.properties) { | ||
| const obj: any = {}; | ||
| Object.keys(schema.properties).forEach(k => { | ||
| const propType = schema.properties[k].type; | ||
| if (propType === 'string') { | ||
| obj[k] = 'string'; | ||
| } else if (propType === 'integer') { | ||
| obj[k] = 1; | ||
| } else if (propType === 'boolean') { | ||
| obj[k] = true; | ||
| } else { | ||
| obj[k] = null; | ||
|
Comment on lines
+27
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift Generate a payload that conforms to the schema. The generator converts nested objects, arrays, numbers, enums, and other valid property types to Use property-existence checks for examples. Generate values recursively for supported JSON Schema constructs. 🤖 Prompt for AI AgentsSource: Path instructions |
||
| } | ||
| }); | ||
| return obj; | ||
| } | ||
| return { mock: "payload" }; | ||
| }; | ||
|
|
||
| const expectedPayload = generateMockPayload(payloadSchema); | ||
| const payloadString = JSON.stringify(expectedPayload, null, 2); | ||
|
|
||
| const handleSimulate = async () => { | ||
| if (!endpoint) { | ||
| setError("Please provide an endpoint URL to simulate delivery."); | ||
| return; | ||
| } | ||
| await executeRequest( | ||
| fetch(endpoint, { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| 'Accept': 'application/json, text/plain, */*', | ||
| }, | ||
| body: payloadString, | ||
| }), | ||
| 'Simulation failed. Ensure the endpoint is reachable and configured for CORS.' | ||
| ); | ||
| }; | ||
|
|
||
| return ( | ||
| <div className="mt-4 p-6 border rounded-lg bg-white shadow-sm border-purple-100"> | ||
| <div className="mb-4"> | ||
| <h4 className="text-lg font-semibold text-purple-800 flex items-center"> | ||
| <svg className="w-5 h-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | ||
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" /> | ||
| </svg> | ||
| Webhook Simulation | ||
| </h4> | ||
| <p className="text-sm text-gray-500">Simulate delivery of the expected incoming payload to a configured endpoint.</p> | ||
| </div> | ||
|
|
||
| <div className="space-y-4"> | ||
| <div> | ||
| <label htmlFor="webhook-endpoint" className="block text-sm font-medium text-gray-700 mb-1">Target Endpoint</label> | ||
| <input | ||
| id="webhook-endpoint" | ||
| type="url" | ||
| value={endpoint} | ||
| onChange={(e) => setEndpoint(e.target.value)} | ||
| placeholder="https://your-server.com/webhook" | ||
| className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500 text-sm" | ||
| /> | ||
| </div> | ||
|
|
||
| <div> | ||
| <label htmlFor="expected-payload" className="block text-sm font-medium text-gray-700 mb-1">Expected Payload</label> | ||
| <pre id="expected-payload" className="p-3 bg-gray-50 border rounded-md text-xs font-mono text-gray-800 overflow-x-auto"> | ||
| {payloadString} | ||
| </pre> | ||
| </div> | ||
|
|
||
| <div className="flex justify-end"> | ||
| <button | ||
| onClick={handleSimulate} | ||
| disabled={loading || !endpoint} | ||
| className="px-4 py-2 bg-purple-600 text-white text-sm font-medium rounded shadow hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-2 disabled:opacity-50 transition-colors" | ||
| > | ||
| {loading ? 'Simulating...' : 'Simulate Delivery'} | ||
| </button> | ||
| </div> | ||
| </div> | ||
|
|
||
| <ExecutionResult error={error} response={response} errorMessage="Simulation Failed" /> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export const webhookPlugin: AsyncApiPlugin = { | ||
| name: 'default-webhook-plugin', | ||
| version: '1.0.0', | ||
| description: 'Playground default plugin for Webhook simulation', | ||
| install(api: PluginAPI) { | ||
| api.registerComponent(PluginSlot.OPERATION, WebhookExecutionComponent); | ||
| } | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: asyncapi/asyncapi-react
Length of output: 259
🤖 get_repo_knowledge executed:
get_repo_knowledge asyncapi/asyncapi-react /tmp/coderabbit-repo-knowledge/asyncapi-asyncapi-react-0831e4b3/conventionsLength of output: 8384
🏁 Script executed:
Repository: asyncapi/asyncapi-react
Length of output: 490
🏁 Script executed:
Repository: asyncapi/asyncapi-react
Length of output: 34315
🏁 Script executed:
Repository: asyncapi/asyncapi-react
Length of output: 19666
🏁 Script executed:
Repository: asyncapi/asyncapi-react
Length of output: 40995
🏁 Script executed:
Repository: asyncapi/asyncapi-react
Length of output: 42026
🏁 Script executed:
Repository: asyncapi/asyncapi-react
Length of output: 6684
Prevent redundant plugin update renders.
Playground.render()creates a new plugin array on every render.AsyncApiStandalone.componentDidUpdatedetects the new array identity and callsupdatePlugins. Although plugin names prevent duplicate installation,updatePluginsalways callssetState({}), causing an unnecessary additional render.Keep the array stable at module scope:
Suggested stabilization
📝 Committable suggestion
🤖 Prompt for AI Agents
Source: Path instructions