Skip to content

Commit c94344d

Browse files
authored
Merge pull request #113 from open-rpc/feat/tabs-and-autocomplete
feat: add tabs and openrpdocument autocomplete
2 parents 933e143 + 07a424a commit c94344d

8 files changed

+409
-58
lines changed

package-lock.json

+5
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
"react-json-view": "^1.19.1",
6464
"react-split-pane": "^0.1.87",
6565
"semantic-release": "^15.13.21",
66-
"use-dark-mode": "^2.3.1"
66+
"use-dark-mode": "^2.3.1",
67+
"use-debounce": "^3.3.0"
6768
}
6869
}

src/containers/App.tsx

+3-13
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,15 @@
11
import React, { useEffect } from "react";
22
import { CssBaseline } from "@material-ui/core";
33
import { MuiThemeProvider } from "@material-ui/core";
4+
45
import { lightTheme, darkTheme } from "../themes/openrpcTheme";
56
import useDarkMode from "use-dark-mode";
67
import Inspector from "./Inspector";
78
import useQueryParams from "../hooks/useQueryParams";
89
import * as monaco from "monaco-editor";
9-
import localStorageMock from "../helpers/localStorageMock";
10-
11-
let localStorageEnabled = true;
12-
try {
13-
window.localStorage.setItem("xyz-test", "true");
14-
} catch (e) {
15-
localStorageEnabled = false;
16-
console.error(e);
17-
}
18-
19-
// mock storageProvider for when localStorage is not available via chrome/brave settings
20-
const darkModeOptions = localStorageEnabled ? undefined : localStorageMock;
2110

2211
const App: React.FC = () => {
23-
const darkMode = useDarkMode(undefined, darkModeOptions);
12+
const darkMode = useDarkMode();
2413
const [query] = useQueryParams();
2514
const theme = darkMode.value ? darkTheme : lightTheme;
2615
useEffect(() => {
@@ -31,6 +20,7 @@ const App: React.FC = () => {
3120
return (
3221
<MuiThemeProvider theme={theme}>
3322
<CssBaseline />
23+
3424
<Inspector
3525
onToggleDarkMode={darkMode.toggle}
3626
darkMode={darkMode.value}

src/containers/Inspector.tsx

+138-11
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@ import React, { useState, useEffect, ChangeEvent, Dispatch, useRef } from "react
22
import SplitPane from "react-split-pane";
33
import JSONRPCRequestEditor from "./JSONRPCRequestEditor";
44
import PlayCircle from "@material-ui/icons/PlayCircleFilled";
5-
import { IconButton, AppBar, Toolbar, Typography, Button, InputBase } from "@material-ui/core";
5+
import CloseIcon from "@material-ui/icons/Close";
6+
import PlusIcon from "@material-ui/icons/Add";
7+
import { IconButton, AppBar, Toolbar, Typography, Button, InputBase, Tab, Tabs } from "@material-ui/core";
68
import { Client, RequestManager, HTTPTransport, WebSocketTransport } from "@open-rpc/client-js";
79
import Brightness3Icon from "@material-ui/icons/Brightness3";
810
import WbSunnyIcon from "@material-ui/icons/WbSunny";
911
import { JSONRPCError } from "@open-rpc/client-js/build/Error";
1012
import { MethodObject } from "@open-rpc/meta-schema";
1113
import MonacoEditor from "@etclabscore/react-monaco-editor";
14+
import useTabs from "../hooks/useTabs";
15+
import { useDebounce } from "use-debounce";
1216

1317
const errorToJSON = (error: JSONRPCError | undefined): any => {
1418
if (!error) {
@@ -48,7 +52,6 @@ const useClient = (url: string): [Client, JSONRPCError | undefined, Dispatch<JSO
4852
setClient(c);
4953
c.onError((e) => {
5054
console.log("onError", e); //tslint:disable-line
51-
setError(e);
5255
});
5356
} catch (e) {
5457
setError(e);
@@ -67,8 +70,29 @@ function useCounter(defaultValue: number): [number, () => void] {
6770
return [counter, incrementCounter];
6871
}
6972

73+
const emptyJSONRPC = {
74+
jsonrpc: "2.0",
75+
method: "",
76+
params: [],
77+
id: "0",
78+
};
79+
7080
const Inspector: React.FC<IProps> = (props) => {
81+
const {
82+
setTabContent,
83+
setTabEditing,
84+
setTabIndex,
85+
tabs,
86+
setTabs,
87+
handleClose,
88+
tabIndex,
89+
setTabOpenRPCDocument,
90+
setTabUrl,
91+
handleLabelChange,
92+
setTabResults,
93+
} = useTabs();
7194
const [id, incrementId] = useCounter(0);
95+
const [openrpcDocument, setOpenRpcDocument] = useState();
7296
const [json, setJson] = useState(props.request || {
7397
jsonrpc: "2.0",
7498
method: "",
@@ -78,7 +102,8 @@ const Inspector: React.FC<IProps> = (props) => {
78102
const editorRef = useRef();
79103
const [results, setResults] = useState();
80104
const [url, setUrl] = useState(props.url || "");
81-
const [client, error, setError] = useClient(url);
105+
const [debouncedUrl] = useDebounce(url, 1000);
106+
const [client, error] = useClient(url);
82107
useEffect(() => {
83108
if (props.openrpcMethodObject) {
84109
setJson({
@@ -92,19 +117,29 @@ const Inspector: React.FC<IProps> = (props) => {
92117
}, []);
93118
useEffect(() => {
94119
if (json) {
95-
setJson({
120+
const jsonResult = {
96121
...json,
97122
jsonrpc: "2.0",
98123
id: id.toString(),
99-
});
124+
};
125+
setJson(jsonResult);
100126
}
101127
// eslint-disable-next-line react-hooks/exhaustive-deps
102128
}, [id]);
103129

130+
useEffect(() => {
131+
if (json) {
132+
setTabContent(tabIndex, json);
133+
}
134+
// eslint-disable-next-line react-hooks/exhaustive-deps
135+
}, [json]);
136+
104137
useEffect(() => {
105138
if (props.url) {
106139
setUrl(props.url);
140+
setTabUrl(tabIndex, props.url);
107141
}
142+
// eslint-disable-next-line react-hooks/exhaustive-deps
108143
}, [props.url]);
109144

110145
const handlePlayButton = async () => {
@@ -113,19 +148,22 @@ const Inspector: React.FC<IProps> = (props) => {
113148
incrementId();
114149
try {
115150
const result = await client.request(json.method, json.params);
116-
setResults({ jsonrpc: "2.0", result, id });
151+
const r = { jsonrpc: "2.0", result, id };
152+
setResults(r);
153+
setTabResults(tabIndex, r);
117154
} catch (e) {
118-
setError(e);
155+
setResults(e);
156+
setTabResults(tabIndex, e);
119157
}
120158
}
121159
};
122-
function handleResponseEditorDidMount(_: any, editor: any) {
160+
function handleResponseEditorDidMount(__: any, editor: any) {
123161
editorRef.current = editor;
124162
}
125163

126164
const clear = () => {
127165
setResults(undefined);
128-
setError(undefined);
166+
setTabResults(tabIndex, undefined);
129167
};
130168

131169
const handleClearButton = () => {
@@ -137,9 +175,85 @@ const Inspector: React.FC<IProps> = (props) => {
137175
props.onToggleDarkMode();
138176
}
139177
};
178+
const refreshOpenRpcDocument = async () => {
179+
if (url) {
180+
try {
181+
const d = await client.request("rpc.discover", []);
182+
setOpenRpcDocument(d);
183+
setTabOpenRPCDocument(tabIndex, d);
184+
} catch (e) {
185+
setOpenRpcDocument(undefined);
186+
setTabOpenRPCDocument(tabIndex, undefined);
187+
}
188+
}
189+
};
190+
191+
useEffect(() => {
192+
refreshOpenRpcDocument();
193+
// eslint-disable-next-line react-hooks/exhaustive-deps
194+
}, [debouncedUrl]);
195+
196+
useEffect(() => {
197+
if (tabs[tabIndex]) {
198+
setJson(tabs[tabIndex].content);
199+
setUrl(tabs[tabIndex].url || "");
200+
setOpenRpcDocument(tabs[tabIndex].openrpcDocument);
201+
setResults(tabs[tabIndex].results);
202+
}
203+
// eslint-disable-next-line react-hooks/exhaustive-deps
204+
}, [tabIndex]);
205+
206+
const handleTabIndexChange = (event: React.ChangeEvent<{}>, newValue: number) => {
207+
setTabIndex(newValue);
208+
};
140209

141210
return (
142211
<>
212+
<div style={{ position: "relative" }}>
213+
<Tabs
214+
style={{ background: "transparent" }}
215+
value={tabIndex}
216+
variant="scrollable"
217+
indicatorColor="primary"
218+
onChange={handleTabIndexChange}
219+
>
220+
{tabs.map((tab, index) => (
221+
<Tab disableRipple style={{
222+
border: "none",
223+
outline: "none",
224+
userSelect: "none",
225+
}} onDoubleClick={() => setTabEditing(tab, true)} label={
226+
<div style={{ userSelect: "none" }}>
227+
{tab.editing
228+
? <InputBase
229+
value={tab.name}
230+
onChange={(ev) => handleLabelChange(ev, tab)}
231+
onBlur={() => setTabEditing(tab, false)}
232+
autoFocus
233+
style={{ maxWidth: "80px", marginRight: "25px" }}
234+
/>
235+
: <Typography style={{ display: "inline", textTransform: "none", marginRight: "25px" }} variant="body1" >{tab.name}</Typography>
236+
}
237+
{tabIndex === index
238+
?
239+
<IconButton onClick={
240+
(ev) => handleClose(ev, index)
241+
} style={{ position: "absolute", right: "10px", top: "25%" }} size="small">
242+
<CloseIcon />
243+
</IconButton>
244+
: null
245+
}
246+
</div>
247+
}></Tab>
248+
))}
249+
<Tab disableRipple style={{ minWidth: "50px" }} label={
250+
<IconButton onClick={() => setTabs([...tabs, { name: "New Tab", content: { ...emptyJSONRPC }, url: "" }])}>
251+
<PlusIcon scale={0.5} />
252+
</IconButton>
253+
}>
254+
</Tab>
255+
</Tabs>
256+
</div>
143257
<AppBar elevation={0} position="static">
144258
<Toolbar>
145259
<img
@@ -156,7 +270,10 @@ const Inspector: React.FC<IProps> = (props) => {
156270
value={url}
157271
placeholder="Enter a JSON-RPC server URL"
158272
onChange={
159-
(event: ChangeEvent<HTMLInputElement>) => setUrl(event.target.value)
273+
(event: ChangeEvent<HTMLInputElement>) => {
274+
setUrl(event.target.value);
275+
setTabUrl(tabIndex, event.target.value);
276+
}
160277
}
161278
fullWidth
162279
style={{ background: "rgba(0,0,0,0.1)", borderRadius: "4px", padding: "0px 10px", marginRight: "5px" }}
@@ -183,8 +300,18 @@ const Inspector: React.FC<IProps> = (props) => {
183300
}}>
184301
<JSONRPCRequestEditor
185302
onChange={(val) => {
186-
setJson(JSON.parse(val));
303+
let jsonResult;
304+
try {
305+
jsonResult = JSON.parse(val);
306+
} catch (e) {
307+
console.error(e);
308+
}
309+
if (jsonResult) {
310+
setJson(jsonResult);
311+
setTabContent(tabIndex, jsonResult);
312+
}
187313
}}
314+
openrpcDocument={openrpcDocument}
188315
openrpcMethodObject={props.openrpcMethodObject}
189316
value={JSON.stringify(json, null, 4)}
190317
/>

src/containers/JSONRPCRequestEditor.tsx

+17-11
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,36 @@
11
import React, { useRef, useEffect } from "react";
22
import MonacoEditor from "@etclabscore/react-monaco-editor";
33
import * as monaco from "monaco-editor";
4-
import { MethodObject, ContentDescriptorObject } from "@open-rpc/meta-schema";
4+
import { MethodObject, ContentDescriptorObject, OpenRPC } from "@open-rpc/meta-schema";
55
import useWindowSize from "@rehooks/window-size";
66
import { addDiagnostics } from "@etclabscore/monaco-add-json-schema-diagnostics";
7+
import openrpcDocumentToJSONRPCSchema from "../helpers/openrpcDocumentToJSONRPCSchema";
78

89
interface IProps {
910
onChange?: (newValue: any) => void;
1011
openrpcMethodObject?: MethodObject;
12+
openrpcDocument?: OpenRPC;
1113
value: any;
1214
}
1315

1416
const JSONRPCRequestEditor: React.FC<IProps> = (props) => {
15-
const editorRef = useRef();
17+
const editorRef = useRef<any>();
1618
const windowSize = useWindowSize();
17-
1819
useEffect(() => {
1920
if (editorRef !== undefined && editorRef.current !== undefined) {
2021
(editorRef.current as any).layout();
2122
}
2223
}, [windowSize]);
2324

24-
function handleEditorDidMount(_: any, editor: any) {
25-
editorRef.current = editor;
25+
useEffect(() => {
26+
if (!editorRef.current) {
27+
return;
28+
}
2629
const modelName = props.openrpcMethodObject ? props.openrpcMethodObject.name : "inspector";
2730
const modelUriString = `inmemory://${modelName}-${Math.random()}.json`;
2831
const modelUri = monaco.Uri.parse(modelUriString);
2932
const model = monaco.editor.createModel(props.value || "", "json", modelUri);
30-
editor.setModel(model);
33+
editorRef.current.setModel(model);
3134
let schema: any = {
3235
type: "object",
3336
properties: {
@@ -87,6 +90,8 @@ const JSONRPCRequestEditor: React.FC<IProps> = (props) => {
8790
},
8891
},
8992
};
93+
} else if (props.openrpcDocument) {
94+
schema = openrpcDocumentToJSONRPCSchema(props.openrpcDocument);
9095
} else {
9196
schema = {
9297
additionalProperties: false,
@@ -102,6 +107,12 @@ const JSONRPCRequestEditor: React.FC<IProps> = (props) => {
102107
};
103108
}
104109
addDiagnostics(modelUri.toString(), schema, monaco);
110+
111+
// eslint-disable-next-line react-hooks/exhaustive-deps
112+
}, [props.openrpcDocument, props.openrpcMethodObject]);
113+
114+
function handleEditorDidMount(_: any, editor: any) {
115+
editorRef.current = editor;
105116
}
106117

107118
const handleChange = (ev: any, value: any) => {
@@ -113,11 +124,6 @@ const JSONRPCRequestEditor: React.FC<IProps> = (props) => {
113124
return (
114125
<MonacoEditor
115126
height="93vh"
116-
options={{
117-
minimap: {
118-
enabled: false,
119-
},
120-
}}
121127
value={props.value}
122128
editorDidMount={handleEditorDidMount}
123129
language="json"

0 commit comments

Comments
 (0)