Skip to content

Commit

Permalink
Merge pull request #141 from open-rpc/feat/transport-plugins
Browse files Browse the repository at this point in the history
Feat/transport plugins
  • Loading branch information
shanejonas authored Mar 27, 2020
2 parents e8e0209 + 2c5c871 commit 8a99419
Show file tree
Hide file tree
Showing 4 changed files with 431 additions and 53 deletions.
167 changes: 167 additions & 0 deletions src/components/TransportDropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import React, { useState, ChangeEvent } from "react";
import { Button, Menu, MenuItem, Typography, Dialog, Container, Grid, InputBase } from "@material-ui/core";
import { CSSProperties } from "@material-ui/core/styles/withStyles";
import PlusIcon from "@material-ui/icons/Add";
import DropdownArrowIcon from "@material-ui/icons/ArrowDropDown";
import { ITransport } from "../hooks/useTransport";

interface IProps {
transports: ITransport[];
selectedTransport: ITransport;
onChange: (changedTransport: ITransport) => void;
onAddTransport: (addedTransport: ITransport) => void;
style?: CSSProperties;
}

const TransportDropdown: React.FC<IProps> = ({ selectedTransport, transports, onChange, style, onAddTransport }) => {
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const handleMenuItemClick = (transport: ITransport) => {
setAnchorEl(null);
// this forces language change for react + i18n react
onChange(transport);
};

const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [dialogOpen, setDialogOpen] = useState<boolean>(false);

const [selectedCustomTransport, setSelectedCustomTransport] = useState<ITransport | undefined>();
const [customTransportName, setCustomTransportName] = useState<string | undefined>();
const [customTransportUri, setCustomTransportUri] = useState<string | undefined>();

const [dialogMenuAnchorEl, setDialogMenuAnchorEl] = useState<null | HTMLElement>(null);

const handleDialogAnchorClose = () => {
setDialogMenuAnchorEl(null);
};
const handleDialogCustomTransportClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setDialogMenuAnchorEl(event.currentTarget);
};

const handleCustomTransportDialogMenuItemClick = (transport: ITransport) => {
setDialogMenuAnchorEl(null);
setSelectedCustomTransport(transport);
};

const handleSubmitCustomTransport = () => {
setDialogMenuAnchorEl(null);
if (selectedCustomTransport && customTransportName && customTransportUri) {
const t: ITransport = {
type: "plugin",
transport: selectedCustomTransport,
name: customTransportName,
uri: customTransportUri,
};
onAddTransport(t);
setDialogOpen(false);
}
};
return (
<div style={style}>
<Dialog onClose={() => setDialogOpen(false)} aria-labelledby="simple-dialog-title" open={dialogOpen}>
<Container maxWidth="sm">
<Grid
container
justify="space-between"
alignItems="center"
style={{ padding: "30px", paddingTop: "10px", paddingBottom: "10px", marginTop: "10px" }}>
<Typography variant="h6">Custom Transport Plugin</Typography>
<Typography variant="caption" gutterBottom>
Transport plugins are created by implementing the "connect",
"sendData", and "close" methods over JSON-RPC.
</Typography>
<Grid container direction="column" spacing={1}>
<Grid item>
<InputBase placeholder="Plugin Name"
onChange={
(event: ChangeEvent<HTMLInputElement>) => {
setCustomTransportName(event.target.value);
}
}
style={{
display: "block",
background: "rgba(0,0,0,0.1)",
borderRadius: "4px",
padding: "0px 10px",
marginRight: "5px",
}}
/>
</Grid>
<Grid item>
<InputBase placeholder="Plugin URI"
onChange={
(event: ChangeEvent<HTMLInputElement>) => {
setCustomTransportUri(event.target.value);
}
}
style={{
display: "block",
background: "rgba(0,0,0,0.1)",
borderRadius: "4px",
padding: "0px 10px",
marginRight: "5px",
}}
/>
</Grid>
<Grid item>
<Button
variant="outlined"
onClick={handleDialogCustomTransportClick}>
{selectedCustomTransport ? selectedCustomTransport.name : "Select A Transport"}
</Button>
</Grid>
</Grid>
<Menu
id="transport-menu"
anchorEl={dialogMenuAnchorEl}
keepMounted
open={Boolean(dialogMenuAnchorEl)}
onClose={handleDialogAnchorClose}
>
{transports.filter((value) => value.type !== "plugin").map((transport, i) => (
<MenuItem
onClick={() => handleCustomTransportDialogMenuItemClick(transport)}
>{transport.name}</MenuItem>
))}
</Menu>
<Button
style={{ marginTop: "10px", marginBottom: "10px" }}
onClick={handleSubmitCustomTransport}
disabled={!customTransportName || !customTransportUri || !selectedCustomTransport}
variant="contained">
Add Transport
</Button>
</Grid>
</Container>
</Dialog>
<Button
style={{
marginRight: "10px",
marginLeft: "5px",
}}
variant="outlined"
onClick={handleClick} endIcon={<DropdownArrowIcon />}
>{selectedTransport && selectedTransport.name}</Button>
<Menu
id="transport-menu"
anchorEl={anchorEl}
keepMounted
open={Boolean(anchorEl)}
onClose={handleClose}
>
{transports.map((transport, i) => (
<MenuItem onClick={() => handleMenuItemClick(transport)}>{transport.name}</MenuItem>
))}
<MenuItem onClick={() => setDialogOpen(true)}>
<PlusIcon style={{ marginRight: "5px" }} /><Typography variant="caption">Add Transport</Typography>
</MenuItem>
</Menu>
</div>
);
};

export default TransportDropdown;
128 changes: 75 additions & 53 deletions src/containers/Inspector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import JSONRPCRequestEditor from "./JSONRPCRequestEditor";
import * as monaco from "monaco-editor";
import PlayCircle from "@material-ui/icons/PlayCircleFilled";
import CloseIcon from "@material-ui/icons/Close";
import FlashOn from "@material-ui/icons/FlashOn";
import FlashOff from "@material-ui/icons/FlashOff";
import History from "@material-ui/icons/History";
import PlusIcon from "@material-ui/icons/Add";
import CheckCircle from "@material-ui/icons/CheckCircle";
import DocumentIcon from "@material-ui/icons/Description";
import {
IconButton,
AppBar,
Expand All @@ -24,8 +26,6 @@ import {
ListItemText,
Container,
} from "@material-ui/core";
import { HTTPTransport, WebSocketTransport } from "@open-rpc/client-js";
import { Transport } from "@open-rpc/client-js/build/transports/Transport";
import Brightness3Icon from "@material-ui/icons/Brightness3";
import WbSunnyIcon from "@material-ui/icons/WbSunny";
import { JSONRPCError } from "@open-rpc/client-js/build/Error";
Expand All @@ -38,6 +38,23 @@ import { parseOpenRPCDocument } from "@open-rpc/schema-utils-js";
import useMonacoVimMode from "../hooks/useMonacoVimMode";
import { addDiagnostics } from "@etclabscore/monaco-add-json-schema-diagnostics";
import openrpcDocumentToJSONRPCSchemaResult from "../helpers/openrpcDocumentToJSONRPCSchemaResult";
import TransportDropdown from "../components/TransportDropdown";
import useTransport, { ITransport } from "../hooks/useTransport";

const defaultTransports: ITransport[] = [
{
type: "http",
name: "HTTP",
},
{
type: "websocket",
name: "WebSocket",
},
{
type: "postmessage",
name: "PostMessage",
},
];

const errorToJSON = (error: JSONRPCError | any, id: string | number): any => {
const isError = error instanceof Error;
Expand Down Expand Up @@ -81,38 +98,6 @@ interface IProps {
onToggleDarkMode?: () => void;
}

type TUseTransport = (url: string) =>
[Transport | undefined, JSONRPCError | undefined, Dispatch<JSONRPCError | undefined>];

const useTransport: TUseTransport = (url) => {
const [transport, setTransport]: [Transport | undefined, Dispatch<Transport | undefined>] = useState();
const [error, setError]: [JSONRPCError | undefined, Dispatch<JSONRPCError | undefined>] = useState();
useEffect(() => {
let localTransport;
if (url === "" || url === undefined) {
setTransport(undefined);
return;
}
if (url.includes("http://") || url.includes("https://")) {
localTransport = HTTPTransport;
}
if (url.includes("ws://")) {
localTransport = WebSocketTransport;
}
try {
const transportTransport = localTransport || HTTPTransport;
const t = new transportTransport(url);
t.connect().then(() => {
setTransport(t);
});
} catch (e) {
setTransport(undefined);
setError(e);
}
}, [url]);
return [transport, error, setError];
};

function useCounter(defaultValue: number): [number, () => void] {
const [counter, setCounter] = useState(defaultValue);

Expand Down Expand Up @@ -163,9 +148,11 @@ const Inspector: React.FC<IProps> = (props) => {
id,
});
const [results, setResults]: [any, Dispatch<any>] = useState();
const [transportList, setTransportList] = useState(defaultTransports);
const [url, setUrl] = useState(props.url || "");
const [debouncedUrl] = useDebounce(url, 1000);
const [transport, error] = useTransport(debouncedUrl);
const [selectedTransport, setSelectedTransport] = useState(defaultTransports[0]);
const [transport, setTransport, , connected] = useTransport(transportList, debouncedUrl, defaultTransports[0]);
const [historyOpen, setHistoryOpen] = useState(false);
const [requestHistory, setRequestHistory]: [any[], Dispatch<any>] = useState([]);
const [historySelectedIndex, setHistorySelectedIndex] = useState(0);
Expand Down Expand Up @@ -193,6 +180,13 @@ const Inspector: React.FC<IProps> = (props) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id]);

useEffect(() => {
if (selectedTransport !== undefined) {
setTransport(selectedTransport!);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedTransport]);

useEffect(() => {
if (!openrpcDocument) {
return;
Expand Down Expand Up @@ -273,6 +267,8 @@ const Inspector: React.FC<IProps> = (props) => {
};
const refreshOpenRpcDocument = async () => {
if (!transport) {
setOpenRpcDocument(undefined);
setTabOpenRPCDocument(tabIndex, undefined);
return;
}
try {
Expand All @@ -296,6 +292,7 @@ const Inspector: React.FC<IProps> = (props) => {
}
};
useEffect(() => {
setOpenRpcDocument(undefined);
refreshOpenRpcDocument();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [transport, tabIndex]);
Expand Down Expand Up @@ -450,27 +447,52 @@ const Inspector: React.FC<IProps> = (props) => {
src="https://github.com/open-rpc/design/raw/master/icons/open-rpc-logo-noText/open-rpc-logo-noText%20(PNG)/128x128.png" //tslint:disable-line
/>
<Typography variant="h6" color="textSecondary">Inspector</Typography>
<TransportDropdown
transports={transportList}
onAddTransport={(addedTransport: ITransport) => {
setTransportList([
...transportList,
addedTransport,
]);
}}
selectedTransport={selectedTransport}
onChange={(changedTransport) => setSelectedTransport(changedTransport)}
style={{
marginLeft: "10px",
}}
/>
<Tooltip title="Play">
<IconButton onClick={handlePlayButton}>
<PlayCircle />
<PlayCircle fontSize="large" />
</IconButton>
</Tooltip>
<InputBase
startAdornment={openrpcDocument
?
<Tooltip title={
<div style={{ textAlign: "center" }}>
<Typography>OpenRPC Document Detected</Typography>
<Typography variant="caption">
A JSON-RPC endpoint may respond to the rpc.discover method
or a provide a static document.
This adds features like auto completion to the inspector.
startAdornment={
<>
<Tooltip title={connected ? "Connected" : "Not Connected"}>
{connected
? <FlashOn style={{ color: green[500] }} />
: <FlashOff color="error" />
}
</Tooltip>
{
openrpcDocument
?
<Tooltip title={
<div style={{ textAlign: "center" }}>
<Typography>OpenRPC Document Detected</Typography>
<Typography variant="caption">
A JSON-RPC endpoint may respond to the rpc.discover method
or a provide a static document.
This adds features like auto completion to the inspector.
</Typography>
</div>
} onClick={() => window.open("https://spec.open-rpc.org/#service-discovery-method")}>
<CheckCircle style={{ color: green[500], marginRight: "5px", cursor: "pointer" }} scale={0.1} />
</Tooltip>
: null
</div>
} onClick={() => window.open("https://spec.open-rpc.org/#service-discovery-method")}>
<DocumentIcon style={{ color: green[500], marginRight: "5px", cursor: "pointer" }} scale={0.1} />
</Tooltip>
: null
}
</>
}
value={url}
placeholder="Enter a JSON-RPC server URL"
Expand Down Expand Up @@ -528,12 +550,12 @@ const Inspector: React.FC<IProps> = (props) => {
value={JSON.stringify(json, null, 4)}
/>
<>
{(results || error) &&
{results &&
<Button
style={{ position: "absolute", top: "15px", right: "15px", zIndex: 1 }}
onClick={handleClearButton}>
Clear
</Button>
</Button>
}
{results
?
Expand Down
Loading

0 comments on commit 8a99419

Please sign in to comment.