mirror of
https://github.com/XRPLF/xrpl-dev-portal.git
synced 2025-11-25 22:25:52 +00:00
Recreate branch from base, add react-query-params, fix permalinks, fix sidebar use correct params library and upgrade redocly. Fix command text not working with permalink and move more modal logic out of main component. Moved more connection selection logic to connection modal Removed many `data-*` attributes previously used by bootstrap modal css Created a shared modal component which removed 38 lines. WS Tool: Fix Link import fix UL error toggle CurlModal to show/hide on button clicks resolve error: <div> cannot appear as a descendant of <p> remove <span> WS tool: sidebar fixes
364 lines
12 KiB
TypeScript
364 lines
12 KiB
TypeScript
import { useEffect, useState, useRef } from 'react';
|
|
import { useLocation } from "react-router-dom";
|
|
import { useTranslate } from "@portal/hooks";
|
|
import {
|
|
JsonParam,
|
|
StringParam,
|
|
useQueryParams,
|
|
withDefault,
|
|
QueryParamProvider
|
|
} from "use-query-params"
|
|
import { ReactRouter6Adapter } from 'use-query-params/adapters/react-router-6';
|
|
|
|
import { PermalinkButton } from './components/websocket-api/permalink-modal';
|
|
import { CurlButton } from './components/websocket-api/curl-modal';
|
|
import { ConnectionModal } from "./components/websocket-api/connection-modal";
|
|
|
|
import { RightSideBar } from "./components/websocket-api/right-sidebar";
|
|
import { slugify } from "./components/websocket-api/slugify";
|
|
import { JsonEditor } from '../../shared/editor/json-editor';
|
|
import { CommandGroup, CommandMethod } from './components/websocket-api/types';
|
|
|
|
import commandList from "./components/websocket-api/data/command-list.json";
|
|
import connections from "./components/websocket-api/data/connections.json";
|
|
import { Loader } from './components/Loader';
|
|
|
|
export function WebsocketApiTool() {
|
|
|
|
const [params, setParams] = useQueryParams({
|
|
server: withDefault(StringParam, null),
|
|
req: withDefault(JsonParam, null)
|
|
})
|
|
|
|
const { hash: slug } = useLocation();
|
|
const { translate } = useTranslate();
|
|
const [isConnectionModalVisible, setIsConnectionModalVisible] =
|
|
useState(false);
|
|
const [selectedConnection, setSelectedConnection] = useState((params.server) ? connections.find((connection) => { return connection?.ws_url === params.server }) : connections[0]); const [connected, setConnected] = useState(false);
|
|
const [connectionError, setConnectionError] = useState(false);
|
|
const [keepLast, setKeepLast] = useState(50);
|
|
const [streamPaused, setStreamPaused] = useState(false);
|
|
const streamPausedRef = useRef(streamPaused);
|
|
const [wsLoading, setWsLoading] = useState(false);
|
|
const [sendLoading, setSendLoading] = useState(false);
|
|
|
|
const getInitialMethod = (): CommandMethod => {
|
|
for (const group of (commandList as CommandGroup[])) {
|
|
for (const method of group.methods) {
|
|
if (slug.slice(1) === slugify(method.name) || params.req?.command == method.body.command) {
|
|
return method;
|
|
}
|
|
}
|
|
}
|
|
return commandList[0].methods[0] as CommandMethod;
|
|
};
|
|
|
|
const setMethod = (method: CommandMethod) => {
|
|
setCurrentMethod(method)
|
|
setCurrentBody(JSON.stringify(method.body, null, 2))
|
|
}
|
|
|
|
const [currentMethod, setCurrentMethod] = useState<CommandMethod>(getInitialMethod);
|
|
const [currentBody, setCurrentBody] = useState(
|
|
JSON.stringify(params.req || currentMethod.body, null, 2)
|
|
);
|
|
streamPausedRef.current = streamPaused;
|
|
|
|
const handleCurrentBodyChange = (value: any) => {
|
|
setCurrentBody(value);
|
|
};
|
|
|
|
const handleKeepLastChange = (event) => {
|
|
const newValue = event.target.value;
|
|
setKeepLast(newValue);
|
|
};
|
|
|
|
const openConnectionModal = () => {
|
|
setIsConnectionModalVisible(true);
|
|
};
|
|
|
|
const closeConnectionModal = () => {
|
|
setIsConnectionModalVisible(false);
|
|
};
|
|
|
|
const [ws, setWs] = useState(null);
|
|
const [responses, setResponses] = useState([]);
|
|
|
|
useEffect(() => {
|
|
if (ws && ws.readyState < 2) {
|
|
ws.close();
|
|
}
|
|
const newWs = new WebSocket(selectedConnection.ws_url);
|
|
setWs(newWs);
|
|
setWsLoading(true);
|
|
newWs.onopen = function handleOpen(event) {
|
|
setConnected(true);
|
|
setConnectionError(false);
|
|
setWsLoading(false);
|
|
};
|
|
|
|
newWs.onclose = function handleClose(event) {
|
|
if (event.wasClean) {
|
|
setConnected(false);
|
|
setWsLoading(false);
|
|
} else {
|
|
console.debug(
|
|
"socket close event discarded (new socket status already provided):",
|
|
event
|
|
);
|
|
}
|
|
};
|
|
|
|
newWs.onerror = function handleError(event) {
|
|
setConnectionError(true);
|
|
setWsLoading(false);
|
|
console.error("socket error:", event);
|
|
};
|
|
|
|
newWs.onmessage = function handleMessage(event) {
|
|
const message = event.data;
|
|
let data;
|
|
try {
|
|
data = JSON.parse(message);
|
|
} catch (error) {
|
|
console.error("Error parsing validation message", error);
|
|
return;
|
|
}
|
|
if (data.type === "response") {
|
|
setSendLoading(false);
|
|
}
|
|
if (data.type === "response" || !streamPausedRef.current) {
|
|
setResponses((prevResponses) =>
|
|
[JSON.stringify(data, null, 2)].concat(prevResponses)
|
|
);
|
|
}
|
|
};
|
|
|
|
return () => {
|
|
newWs.close();
|
|
};
|
|
}, [selectedConnection.ws_url]);
|
|
|
|
useEffect(() => {
|
|
if (responses.length > keepLast) {
|
|
setResponses(responses.slice(0, keepLast));
|
|
}
|
|
}, [responses, keepLast]);
|
|
|
|
const sendWebSocketMessage = (messageBody) => {
|
|
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
alert("Can't send request: Must be connected first!");
|
|
return;
|
|
}
|
|
try {
|
|
JSON.parse(messageBody); // we only need the text version, but test JSON syntax
|
|
} catch (e) {
|
|
alert("Invalid request JSON");
|
|
return;
|
|
}
|
|
|
|
setSendLoading(true);
|
|
|
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
ws.send(messageBody);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="container-fluid" role="document" id="main_content_wrapper">
|
|
<div className="row">
|
|
<aside
|
|
className="right-sidebar col-lg-3 order-lg-4"
|
|
role="complementary"
|
|
>
|
|
<RightSideBar
|
|
commandList={commandList}
|
|
currentMethod={currentMethod}
|
|
setCurrentMethod={setMethod}
|
|
/>
|
|
</aside>
|
|
<main
|
|
className="main col-lg-9"
|
|
role="main"
|
|
id="main_content_body"
|
|
>
|
|
<section
|
|
className="container-fluid pt-3 p-md-3 websocket-tool"
|
|
id="wstool-1"
|
|
>
|
|
<h1>{translate("WebSocket Tool")}</h1>
|
|
<div className="api-method-description-wrapper">
|
|
<h3>
|
|
<a
|
|
href={`${currentMethod.name.split(" ")[0]}.html`}
|
|
className="selected_command"
|
|
>
|
|
{currentMethod.name}
|
|
</a>
|
|
</h3>
|
|
{currentMethod.description && (
|
|
<p
|
|
className="blurb"
|
|
dangerouslySetInnerHTML={{
|
|
__html: currentMethod.description,
|
|
}}
|
|
/>
|
|
)}
|
|
{currentMethod.link && (
|
|
<a
|
|
className="btn btn-outline-secondary api-readmore"
|
|
href={currentMethod.link}
|
|
>
|
|
{translate("Read more")}
|
|
</a>
|
|
)}
|
|
</div>
|
|
|
|
<div className="api-input-area pt-4">
|
|
<h4>{translate("Request")}</h4>
|
|
<div className="request-body">
|
|
<JsonEditor
|
|
value={currentBody}
|
|
onChange={handleCurrentBodyChange}
|
|
/>
|
|
</div>
|
|
<div
|
|
className="btn-toolbar justify-content-between pt-4"
|
|
role="toolbar"
|
|
>
|
|
<div className="btn-group mr-3" role="group">
|
|
<button
|
|
className="btn btn-outline-secondary send-request"
|
|
onClick={() => sendWebSocketMessage(currentBody)}
|
|
>
|
|
{translate("Send request")}
|
|
</button>
|
|
{sendLoading && (
|
|
<div className="input-group loader send-loader">
|
|
<span className="input-group-append">
|
|
<Loader />
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="btn-group request-options" role="group">
|
|
<button
|
|
className={`btn connection ${
|
|
connected ? "btn-success" : "btn-outline-secondary"
|
|
} ${connectionError ?? "btn-danger"}`}
|
|
onClick={openConnectionModal}
|
|
data-toggle="modal"
|
|
data-target="#wstool-1-connection-settings"
|
|
>
|
|
{`${selectedConnection.shortname}${
|
|
connected ? " (Connected)" : " (Not Connected)"
|
|
}${connectionError ? " (Failed to Connect)" : ""}`}
|
|
</button>
|
|
{isConnectionModalVisible && (
|
|
<ConnectionModal
|
|
selectedConnection={selectedConnection}
|
|
setSelectedConnection={setSelectedConnection}
|
|
closeConnectionModal={closeConnectionModal}
|
|
connections={connections}
|
|
/>
|
|
)}
|
|
{wsLoading && (
|
|
<div className="input-group loader connect-loader">
|
|
<span className="input-group-append">
|
|
<Loader />
|
|
</span>
|
|
</div>
|
|
)}
|
|
<PermalinkButton
|
|
currentBody={currentBody}
|
|
selectedConnection={selectedConnection}
|
|
/>
|
|
{!currentMethod.ws_only &&
|
|
(<CurlButton currentBody={currentBody} selectedConnection={selectedConnection}/>)
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="api-response-area pt-4">
|
|
<h4>{translate("Responses")}</h4>
|
|
|
|
<div
|
|
className="btn-toolbar justify-content-between response-options"
|
|
role="toolbar"
|
|
>
|
|
<div className="input-group">
|
|
<div className="input-group-prepend">
|
|
<div
|
|
className="input-group-text"
|
|
id="wstool-1-keep-last-label"
|
|
>
|
|
{translate("Keep last:")}
|
|
</div>
|
|
</div>
|
|
<input
|
|
type="number"
|
|
value={keepLast}
|
|
min="1"
|
|
aria-label="Number of responses to keep at once"
|
|
aria-describedby="wstool-1-keep-last-label"
|
|
className="form-control keep-last"
|
|
onChange={handleKeepLastChange}
|
|
/>
|
|
</div>
|
|
|
|
<div className="btn-group" role="group">
|
|
{!streamPaused && (
|
|
<button
|
|
className="btn btn-outline-secondary stream-pause"
|
|
title="Pause Subscriptions"
|
|
onClick={() => setStreamPaused(true)}
|
|
>
|
|
<i className="fa fa-pause"></i>
|
|
</button>
|
|
)}
|
|
{streamPaused && (
|
|
<button
|
|
className="btn btn-outline-secondary stream-unpause"
|
|
title="Unpause Subscriptions"
|
|
onClick={() => setStreamPaused(false)}
|
|
>
|
|
<i className="fa fa-play"></i>
|
|
</button>
|
|
)}
|
|
<button
|
|
className="btn btn-outline-secondary wipe-responses"
|
|
title="Delete All Responses"
|
|
onClick={() => setResponses([])}
|
|
>
|
|
<i className="fa fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="response-body-wrapper">
|
|
{responses.map((response, i) => (
|
|
<div className="response-metadata" key={response.id + '_' + i}>
|
|
<span className="timestamp">
|
|
{new Date().toISOString()}
|
|
</span>
|
|
<div className="response-json">
|
|
<JsonEditor value={response} />
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function Page() {
|
|
return <QueryParamProvider adapter={ReactRouter6Adapter}>
|
|
<WebsocketApiTool />
|
|
</QueryParamProvider>
|
|
}
|