From ec2a060d98dc5e7d729e58435ab394545238cb4a Mon Sep 17 00:00:00 2001 From: shanejonas Date: Tue, 2 Jul 2019 19:43:20 -0700 Subject: [PATCH] fix: refactor to hooks fixes #231 --- package-lock.json | 70 ++++-- package.json | 4 +- src/App.tsx | 374 ++++++++++------------------ src/AppBar/AppBar.tsx | 9 +- src/MonacoJSONEditor.test.tsx | 9 - src/MonacoJSONEditor.tsx | 213 ---------------- src/PlaygroundSplitPane.tsx | 44 ++++ src/SearchBar/SearchBar.tsx | 6 +- src/fetchSchemaFromRpcDiscover.tsx | 2 +- src/fetchUrlSchemaFile.tsx | 2 +- src/hooks/useDefaultEditorValue.tsx | 10 + src/hooks/useMonaco.tsx | 48 ++++ src/hooks/useMonacoModel.tsx | 51 ++++ src/hooks/useParsedSchema.tsx | 25 ++ src/hooks/useQueryParams.ts | 0 src/hooks/useSearchBar.tsx | 44 ++++ src/hooks/useUISchema.tsx | 24 ++ 17 files changed, 433 insertions(+), 502 deletions(-) delete mode 100755 src/MonacoJSONEditor.test.tsx delete mode 100644 src/MonacoJSONEditor.tsx create mode 100644 src/PlaygroundSplitPane.tsx create mode 100644 src/hooks/useDefaultEditorValue.tsx create mode 100644 src/hooks/useMonaco.tsx create mode 100644 src/hooks/useMonacoModel.tsx create mode 100644 src/hooks/useParsedSchema.tsx create mode 100644 src/hooks/useQueryParams.ts create mode 100644 src/hooks/useSearchBar.tsx create mode 100644 src/hooks/useUISchema.tsx diff --git a/package-lock.json b/package-lock.json index 0e244048..addb2ca2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3913,6 +3913,11 @@ "integrity": "sha512-sCZy4SxP9rN2w30Hlmg5dtdRwgYQfYRiLo9usw8X9cxlf+H4FqM1xX7+sNH7NNKVdbXMJWqva7iyy+fxh/V7fA==", "dev": true }, + "@use-it/interval": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@use-it/interval/-/interval-0.1.3.tgz", + "integrity": "sha512-chshdtDZTFoWA9aszBz1Cc04Ca9NBD2JTi/GMjdJ+HGm4q7Vy1v71+2mm22r7Kfb2nYW+lTRsPcEHdB/VFVHsQ==" + }, "@webassemblyjs/ast": { "version": "1.7.11", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.7.11.tgz", @@ -9060,8 +9065,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -9082,14 +9086,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -9102,20 +9104,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -9230,8 +9229,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -9243,7 +9241,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -9258,7 +9255,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -9272,7 +9268,6 @@ "minipass": { "version": "2.3.5", "bundled": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -9370,8 +9365,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -9383,7 +9377,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -9468,8 +9461,7 @@ }, "safe-buffer": { "version": "5.1.2", - "bundled": true, - "optional": true + "bundled": true }, "safer-buffer": { "version": "2.1.2", @@ -9505,7 +9497,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -9525,7 +9516,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -9554,13 +9544,11 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.3", - "bundled": true, - "optional": true + "bundled": true } } }, @@ -19160,6 +19148,26 @@ "integrity": "sha512-A5MOagrPFga4YaKQSWHryl7AXvbQkEqpw4NNYMTNYUNV51bA8ABHgYFpqKx+YFFrw59xMV1qGH1R4AgoNIVgCw==", "dev": true }, + "serialize-query-params": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/serialize-query-params/-/serialize-query-params-0.1.3.tgz", + "integrity": "sha512-TyxcNmHN+enyyjN/s/kSYRK+F3lP7JBvWy7+SrYsdVgnLY0fsfyoCYhCDq1q7bDiwzDO2DPz3uCN6nZfpkQfFA==", + "requires": { + "query-string": "^5.0.0" + }, + "dependencies": { + "query-string": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz", + "integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==", + "requires": { + "decode-uri-component": "^0.2.0", + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + } + } + } + }, "serve-index": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", @@ -20922,6 +20930,14 @@ "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" }, + "use-query-params": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/use-query-params/-/use-query-params-0.3.3.tgz", + "integrity": "sha512-WnuR6ks8XZfKaFJIljskhJuuhtellHkLSnMRIusYBnah+zTqZuPilGATY92I7lf38hazZm34QZ+kDM3KjfaSPg==", + "requires": { + "serialize-query-params": "^0.1.3" + } + }, "util": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", diff --git a/package.json b/package.json index aa6514b9..48a76af4 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@open-rpc/examples": "^1.3.1", "@open-rpc/meta-schema": "^1.3.2", "@open-rpc/schema-utils-js": "^1.11.0", + "@use-it/interval": "^0.1.3", "ajv": "^6.10.0", "classnames": "^2.2.6", "commander": "^2.20.0", @@ -38,7 +39,8 @@ "react-json-view": "^1.19.1", "react-markdown": "^4.0.8", "react-monaco-editor": "^0.25.1", - "react-split-pane": "^0.1.87" + "react-split-pane": "^0.1.87", + "use-query-params": "^0.3.3" }, "scripts": { "lint": "tslint --fix -p .", diff --git a/src/App.tsx b/src/App.tsx index 0855191f..22b0737e 100755 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,13 +1,11 @@ -import React from "react"; +import React, { useState, useRef, useEffect } from "react"; import JSONValidationErrorList from "./JSONValidationErrorList"; -import MonacoJSONEditor from "./MonacoJSONEditor"; -import refParser from "json-schema-ref-parser"; import * as monaco from "monaco-editor"; +import _ from "lodash"; import Documentation from "@open-rpc/docs-react"; -import { debounce, isEmpty } from "lodash"; +import { debounce } from "lodash"; +import useInterval from "@use-it/interval"; import "./App.css"; -import fetchUrlSchemaFile from "./fetchUrlSchemaFile"; -import fetchSchemaFromRpcDiscover from "./fetchSchemaFromRpcDiscover"; import AppBar from "./AppBar/AppBar"; import * as qs from "qs"; import { OpenRPC } from "@open-rpc/meta-schema"; @@ -15,131 +13,18 @@ import { IUISchema } from "./UISchema"; import { SnackBar, ISnackBarNotification, NotificationType } from "./SnackBar/SnackBar"; import { MuiThemeProvider } from "@material-ui/core/styles"; import { lightTheme, darkTheme } from "./themes/openrpcTheme"; -import SplitPane from "react-split-pane"; -import { Paper, CssBaseline } from "@material-ui/core"; - -interface IState { - markers: any[]; - notification: ISnackBarNotification; - defaultValue: string; - parsedSchema: OpenRPC; - reactJsonOptions: any; - uiSchema: IUISchema; -} - -export default class App extends React.Component<{}, IState> { - private debouncedHandleUrlChange: any; - private editorInstance?: monaco.editor.IStandaloneCodeEditor; - - constructor(props: {}) { - super(props); - this.state = { - defaultValue: "", - markers: [], - notification: {} as ISnackBarNotification, - parsedSchema: {} as OpenRPC, - reactJsonOptions: { - theme: "summerfruit:inverted", - collapseStringsAfterLength: 25, - displayDataTypes: false, - displayObjectSize: false, - indentWidth: 2, - name: false, - }, - uiSchema: { - appBar: { - "ui:input": true, - "ui:inputPlaceholder": "Enter OpenRPC Document Url or rpc.discover Endpoint", - /* tslint:disable */ - "ui:logoUrl": "https://github.com/open-rpc/design/raw/master/icons/open-rpc-logo-noText/open-rpc-logo-noText%20(PNG)/128x128.png", - /* tslint:enable */ - "ui:splitView": true, - "ui:darkMode": false, - "ui:title": "OpenRPC Playground", - }, - methods: { - "ui:defaultExpanded": false, - }, - params: { - "ui:defaultExpanded": false, - }, - }, - }; - this.refreshEditorData = this.refreshEditorData.bind(this); - this.setMarkers = debounce(this.setMarkers.bind(this), 300); - this.debouncedHandleUrlChange = debounce(this.dHandleUrlChange.bind(this), 300); - this.handleSnackbarClose = this.handleSnackbarClose.bind(this); - } - - public setNotification = (notification: ISnackBarNotification) => { - this.setState({ notification }); - } - public setErrorNotification = (message: string) => { - this.setNotification({ message, type: NotificationType.error }); - } - - public handleSnackbarClose() { - this.setState({ notification: {} as ISnackBarNotification }); - } - - public dHandleUrlChange = async (jsonOrRPC: string) => { - let newSchema; - if (isEmpty(jsonOrRPC)) { return; } - if (jsonOrRPC.match(/\.json$/)) { - try { - newSchema = await fetchUrlSchemaFile(jsonOrRPC); - } catch (e) { - const msg = `Error fetching schema for: ${jsonOrRPC}`; - console.error(msg, e); - this.setErrorNotification(msg); - return; - } - } else { - try { - const rpcResult = await fetchSchemaFromRpcDiscover(jsonOrRPC); - newSchema = rpcResult.result; - } catch (e) { - const msg = `Error fetching rpc.discover for: ${jsonOrRPC}`; - console.error(msg, e); - this.setErrorNotification(msg); - return; - } - } - monaco.editor.getModels()[0].setValue(JSON.stringify(newSchema, undefined, " ")); - this.refreshEditorData(); - this.setState({ - ...this.state, - defaultValue: newSchema, - }); - } - - public handleUrlChange = (value: any) => this.debouncedHandleUrlChange(value); - - public handleUISchemaAppBarChange = (name: string) => (value: any) => { - let reactJsonOptions = this.state.reactJsonOptions; - if (name === "ui:darkMode") { - monaco.editor.setTheme(value ? "vs-dark" : "vs"); - reactJsonOptions = { - ...this.state.reactJsonOptions, - theme: value ? "summerfruit" : "summerfruit:inverted", - }; - } - - this.setState({ - ...this.state, - reactJsonOptions, - uiSchema: { - ...this.state.uiSchema, - appBar: { - ...this.state.uiSchema.appBar, - [name]: value, - }, - }, - }); - } - - public async componentDidMount() { - const urlParams = qs.parse(window.location.search, { +import { CssBaseline } from "@material-ui/core"; +import PlaygroundSplitPane from "./PlaygroundSplitPane"; +import useMonaco from "./hooks/useMonaco"; +import useMonacoModel from "./hooks/useMonacoModel"; +import useParsedSchema from "./hooks/useParsedSchema"; +import useUISchema from "./hooks/useUISchema"; +import useDefaultEditorValue from "./hooks/useDefaultEditorValue"; +import useSearchBar from "./hooks/useSearchBar"; + +const useQueryParams = () => { + const parse = () => { + return qs.parse(window.location.search, { ignoreQueryPrefix: true, depth: 100, decoder(str) { @@ -156,122 +41,123 @@ export default class App extends React.Component<{}, IState> { return decodeURIComponent(str); }, }); - if (urlParams.schemaUrl) { - this.dHandleUrlChange(urlParams.schemaUrl); + }; + const [query] = useState(parse()); + return [query]; +}; + +const App: React.FC = () => { + const [query] = useQueryParams(); + const [defaultValue, setDefaultValue] = useDefaultEditorValue(); + const [markers, setMarkers] = useState([] as monaco.editor.IMarker[]); + const [searchUrl, { results, error }, setSearchUrl] = useSearchBar(query.schemaUrl); + const [notification, setNotification] = useState(); + + useInterval(() => { + setMarkers(monaco.editor.getModelMarkers({})); + }, 5000); + + useEffect(() => { + if (results) { + setParsedSchema(results); } - if (urlParams.schema) { - monaco.editor.getModels()[0].setValue(JSON.stringify(urlParams.schema, undefined, " ")); - } - if (urlParams.uiSchema) { - this.setState({ - uiSchema: { - appBar: { - ...this.state.uiSchema.appBar, - ...urlParams.uiSchema.appBar || {}, - }, - methods: { - ...this.state.uiSchema.methods, - ...urlParams.uiSchema.methods || {}, - }, - params: { - ...this.state.uiSchema.params, - ...urlParams.uiSchema.params || {}, - }, - }, - }); - } - setTimeout(this.refreshEditorData, 300); - setTimeout(this.refreshEditorData, 2000); - } + }, [results]); - public refeshMarkers() { - setTimeout(() => { - const markers = monaco.editor.getModelMarkers({}); - this.setState({ - markers, + useEffect(() => { + if (error) { + setNotification({ + type: NotificationType.error, + message: error, }); - }, 1000); - } - public async refreshEditorData() { - let parsedSchema; - try { - parsedSchema = await refParser.dereference(JSON.parse(monaco.editor.getModels()[0].getValue())) as OpenRPC; - } catch (e) { - console.error("error parsing schema", e); - } - - if (!parsedSchema) { - this.refeshMarkers(); - return; } - - this.setState({ - ...this.state, - parsedSchema: parsedSchema || this.state.parsedSchema, - }); - - this.refeshMarkers(); - } - public setMarkers() { - this.refreshEditorData(); - } - - public render() { - return ( - - - - {this.getPlayground()} - - - ); - } - - private getSplitPane() { - return ( - this.editorInstance && this.editorInstance.layout()}> -
- - this.editorInstance = editorInstance} - defaultValue={this.state.defaultValue} - onChange={this.setMarkers.bind(this)} /> -
-
- -
-
- ); - } - - private getPlayground = () => { - if (!this.state.uiSchema.appBar["ui:splitView"]) { - return ( -
+ }, [error]); + + const [parsedSchema, setParsedSchema] = useParsedSchema(defaultValue ? JSON.parse(defaultValue) : null); + const [reactJsonOptions, setReactJsonOptions] = useState({ + theme: "summerfruit:inverted", + collapseStringsAfterLength: 25, + displayDataTypes: false, + displayObjectSize: false, + indentWidth: 2, + name: false, + }); + const [UISchema, setUISchemaBySection] = useUISchema({ + appBar: { + "ui:input": true, + "ui:inputPlaceholder": "Enter OpenRPC Document Url or rpc.discover Endpoint", + /* tslint:disable */ + "ui:logoUrl": "https://github.com/open-rpc/design/raw/master/icons/open-rpc-logo-noText/open-rpc-logo-noText%20(PNG)/128x128.png", + /* tslint:enable */ + "ui:splitView": true, + "ui:darkMode": false, + "ui:title": "OpenRPC Playground", + }, + methods: { + "ui:defaultExpanded": false, + }, + params: { + "ui:defaultExpanded": false, + }, + }); + const monacoEl = useRef(null); + const handleMonacoEditorOnChange = (event: monaco.editor.IModelContentChangedEvent, value: string) => { + setParsedSchema(value); + const changes = event.changes[0].range; + setPosition([changes.startLineNumber, changes.startColumn, changes.endLineNumber, changes.endColumn]); + }; + const [editor, updateDimensions] = useMonaco( + monacoEl, + undefined, + _.debounce(handleMonacoEditorOnChange, 500), + [UISchema], + ); + const [model, setPosition] = useMonacoModel( + parsedSchema ? JSON.stringify(parsedSchema, null, 2) : defaultValue, + editor, + ); + + return ( + + + { + setUISchemaBySection({ + value, + key: "ui:splitView", + section: "appBar", + }); + }} + onDarkModeChange={(value: boolean) => { + setUISchemaBySection({ + value, + key: "ui:darkMode", + section: "appBar", + }); + }} + onChangeUrl={_.debounce(setSearchUrl, 500)} /> + + +
+ + } + right={ -
- ); - } else { - return this.getSplitPane(); - } - } - -} + } + /> + setNotification({} as ISnackBarNotification)} + notification={notification as ISnackBarNotification} /> +
+ ); +}; +export default App; diff --git a/src/AppBar/AppBar.tsx b/src/AppBar/AppBar.tsx index ac98ad87..fad95702 100644 --- a/src/AppBar/AppBar.tsx +++ b/src/AppBar/AppBar.tsx @@ -6,7 +6,6 @@ import { Grid, IconButton, Paper, - InputBase, Theme, WithStyles, withStyles, @@ -29,9 +28,10 @@ const styles = (theme: Theme) => ({ interface IProps extends WithStyles { uiSchema?: IUISchema; + searchBarUrl: string | undefined; onChangeUrl?: any; onDarkModeChange?: any; - onSplitViewChange?: any; + onSplitViewChange: (split: boolean) => any; } class ApplicationBar extends Component { @@ -64,7 +64,10 @@ class ApplicationBar extends Component { padding: "0px 10px 0px 10px", width: "100%", }} elevation={0}> - + } diff --git a/src/MonacoJSONEditor.test.tsx b/src/MonacoJSONEditor.test.tsx deleted file mode 100755 index 9fda4a14..00000000 --- a/src/MonacoJSONEditor.test.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from "react"; -import ReactDOM from "react-dom"; -import MonacoJSONEditor from "./MonacoJSONEditor"; - -it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); -}); diff --git a/src/MonacoJSONEditor.tsx b/src/MonacoJSONEditor.tsx deleted file mode 100644 index 7423f947..00000000 --- a/src/MonacoJSONEditor.tsx +++ /dev/null @@ -1,213 +0,0 @@ -import React, { ReactNode } from "react"; -/* tslint:disable */ -let empty = require("json-schema-empty"); -empty = empty.default; -const { initVimMode } = require("monaco-vim"); -/* tslint:enable */ -import * as monaco from "monaco-editor"; -import _ from "lodash"; -import { JSONSchema4 } from "json-schema"; -import schema from "@open-rpc/meta-schema"; -import { IUISchema } from "./UISchema"; -import examplesList from "./examplesList"; - -interface IProps { - defaultValue?: string; - onChangeMarkers?: any; - uiSchema: IUISchema; - onCreate?: any; - onChange?: any; -} - -export default class MonacoJSONEditor extends React.Component { - private monaco: React.RefObject; - private metaSchema: JSONSchema4 | null; - private editorInstance: monaco.editor.IStandaloneCodeEditor | null; - private vimMode: any; - private statusNode: HTMLElement | null; - constructor(props: IProps) { - super(props); - this.monaco = React.createRef(); - this.metaSchema = null; - this.editorInstance = null; - this.addCommands = this.addCommands.bind(this); - this.vimMode = null; - this.statusNode = null; - } - - public async componentDidMount() { - const existingModels = monaco.editor.getModels().length > 0; - let model; - - if (!existingModels) { - /* tslint:disable */ - /* tslint:enable */ - const modelUri = monaco.Uri.parse(`inmemory:/${Math.random()}/model/userSpec.json`); - - this.metaSchema = schema as JSONSchema4; - const defaultV = _.isEmpty(this.props.defaultValue) ? null - : JSON.stringify(this.props.defaultValue, undefined, " "); - const ex = examplesList.find((e) => e.name! === "petstore"); - const defaultSchema = await fetch(ex!.url).then((res) => res.text()); - const localStorageSchema = window.localStorage.getItem("schema"); - let defaultValue = (defaultV || localStorageSchema || defaultSchema).trim(); - - if (defaultValue === "{}" || defaultValue === "") { - defaultValue = defaultSchema; - } - - model = monaco.editor.createModel(defaultValue, "json", modelUri); - monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ - enableSchemaRequest: true, - schemas: [ - { - fileMatch: ["*"], - schema, - uri: modelUri.toString(), - }, - ], - validate: true, - }); - model.updateOptions({ tabSize: 2 }); - } else { - model = monaco.editor.getModels()[0]; - } - - const options = { - language: "json", - options: { - autoIndent: true, - formatOnPaste: true, - formatOnType: true, - }, - theme: this.props.uiSchema.appBar["ui:darkMode"] ? "vs-dark" : "vs", - }; - - if (this.monaco && this.monaco.current) { - this.editorInstance = monaco.editor.create(this.monaco.current, { - ...options, - }); - this.editorInstance.setModel(model); - this.editorInstance.setSelection(new monaco.Selection(4, 13, 4, 13)); - - this.editorInstance.focus(); - window.onresize = () => this.editorInstance && this.editorInstance.layout(); - setTimeout(() => this.editorInstance && this.editorInstance.layout(), 1000); - - this.editorInstance.onDidChangeModelContent(() => { - const changedSchema = this.editorInstance && this.editorInstance.getValue(); - if (changedSchema) { - window.localStorage.setItem("schema", changedSchema); - this.props.onChange(changedSchema); - } - }); - this.props.onCreate(this.editorInstance); - } - - this.addCommands(this.editorInstance); - } - public addCommands(editor: monaco.editor.IStandaloneCodeEditor | null) { - if (!editor) { return; } - - // reset editor to empty schema - - /* tslint:disable */ - // replace schema: - // Press Chord Ctrl-K, Ctrl-R => the action will run if it is enabled - - editor.addAction({ - // An unique identifier of the contributed action. - id: "replace-meta-schema", - - // A label of the action that will be presented to the user. - label: "Replace Meta Schema", - - // An optional array of keybindings for the action. - keybindings: [ - monaco.KeyMod.chord(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KEY_K, monaco.KeyMod.CtrlCmd | monaco.KeyCode.KEY_R), - ], - contextMenuGroupId: "navigation", - contextMenuOrder: 1.5, - // Method that will be executed when the action is triggered. - // @param editor The editor instance is passed in as a convinience - run: (ed) => { - const result = window.prompt("Paste schema to replace current meta schema", "{}"); - if (result) { - const metaSchema = JSON.parse(result); - this.metaSchema = metaSchema; - } - if (result != null) { - const modelUri = monaco.Uri.parse(`inmemory:/${Math.random()}/model/userSpec.json`); - monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ - enableSchemaRequest: true, - schemas: [ - { - fileMatch: ["*"], - schema: this.metaSchema, - uri: modelUri.toString(), - }, - ], - validate: true, - }); - } - }, - }); - - // Vim Mode: - // Press Chord Ctrl-K, Ctrl-V => the action will run if it is enabled - - editor.addAction({ - // An unique identifier of the contributed action. - id: "vim-mode", - - // A label of the action that will be presented to the user. - label: "Vim Mode", - - // An optional array of keybindings for the action. - keybindings: [ - // chord - monaco.KeyMod.chord(monaco.KeyMod.CtrlCmd | monaco.KeyCode.KEY_K, monaco.KeyMod.CtrlCmd | monaco.KeyCode.KEY_V), - ], - contextMenuGroupId: "navigation", - contextMenuOrder: 1.5, - - // Method that will be executed when the action is triggered. - // @param editor The editor instance is passed in as a convinience - run: (ed) => { - this.onVimKeybind(); - }, - }); - /* tslint:enable */ - } - public onChange() { - if (this.props.onChangeMarkers) { - this.props.onChangeMarkers(monaco.editor.getModelMarkers({})); - } - } - public componentWillUnmount() { - if (this.editorInstance) { - this.editorInstance.dispose(); - } - } - public onVimKeybind() { - if (this.vimMode) { - this.vimMode.dispose(); - if (this.statusNode) { - this.statusNode.innerHTML = ""; - } - this.vimMode = null; - return; - } - this.statusNode = document.getElementById("vim-status-bar"); - this.vimMode = initVimMode(this.editorInstance, this.statusNode); - return; - } - public render() { - return ( - <> -
-
- - ); - } -} diff --git a/src/PlaygroundSplitPane.tsx b/src/PlaygroundSplitPane.tsx new file mode 100644 index 00000000..c7d691eb --- /dev/null +++ b/src/PlaygroundSplitPane.tsx @@ -0,0 +1,44 @@ +import SplitPane from "react-split-pane"; +import React, { useState } from "react"; +import { Component } from "react"; + +interface IProps { + onChange?: (size: number) => any; + left: JSX.Element; + right: JSX.Element; + split: boolean; +} + +const PlaygroundSplitPane: React.FC = (props) => { + const handleChange = (size: number) => { + if (props.onChange) { + props.onChange(size); + } + }; + + if (props.split === false) { + return ( +
+ {props.right} +
+ ); + } + + return ( + +
+ {props.left} +
+
+ {props.right} +
+
+ ); +}; + +export default PlaygroundSplitPane; diff --git a/src/SearchBar/SearchBar.tsx b/src/SearchBar/SearchBar.tsx index 1e1082d6..3a981cde 100644 --- a/src/SearchBar/SearchBar.tsx +++ b/src/SearchBar/SearchBar.tsx @@ -27,12 +27,12 @@ const styles = (theme: Theme) => ({ interface IProps extends WithStyles { uiSchema?: IUISchema; + searchBarUrl: string | undefined; onChangeUrl?: any; onDarkModeChange?: any; onSplitViewChange?: any; } - function getSuggestion(query: string | null) { if (!query) { return suggestions; @@ -44,9 +44,9 @@ function getSuggestion(query: string | null) { class SearchBar extends Component { public render() { - const { uiSchema, classes, onSplitViewChange, onDarkModeChange } = this.props; + const { uiSchema, classes, onSplitViewChange, onDarkModeChange, searchBarUrl } = this.props; return ( - + {({ getInputProps, getItemProps, diff --git a/src/fetchSchemaFromRpcDiscover.tsx b/src/fetchSchemaFromRpcDiscover.tsx index 68fafbb9..1a66701d 100644 --- a/src/fetchSchemaFromRpcDiscover.tsx +++ b/src/fetchSchemaFromRpcDiscover.tsx @@ -11,7 +11,7 @@ export default async (url: string) => { headers: {"Content-Type": "application/json"}, method: "POST", }); - return await response.json(); + return await response.text(); } catch (e) { throw new Error(`Unable to call rpc.discover at: ${url}`); } diff --git a/src/fetchUrlSchemaFile.tsx b/src/fetchUrlSchemaFile.tsx index e7a2ace5..957047e8 100644 --- a/src/fetchUrlSchemaFile.tsx +++ b/src/fetchUrlSchemaFile.tsx @@ -1,7 +1,7 @@ export default async (schemaUrl: string) => { try { const response = await fetch(schemaUrl); - return await response.json(); + return await response.text(); } catch (e) { throw new Error(`Unable to download openrpc.json file located at the url: ${schemaUrl}`); } diff --git a/src/hooks/useDefaultEditorValue.tsx b/src/hooks/useDefaultEditorValue.tsx new file mode 100644 index 00000000..a361c79d --- /dev/null +++ b/src/hooks/useDefaultEditorValue.tsx @@ -0,0 +1,10 @@ +import { useState, Dispatch } from "react"; + +function useDefaultEditorValue(): [string | null, Dispatch] { + const [defaultValue, setDefaultValue] = useState(() => { + return window.localStorage.getItem("schema"); + }); + return [defaultValue, setDefaultValue]; +} + +export default useDefaultEditorValue; diff --git a/src/hooks/useMonaco.tsx b/src/hooks/useMonaco.tsx new file mode 100644 index 00000000..0f63d355 --- /dev/null +++ b/src/hooks/useMonaco.tsx @@ -0,0 +1,48 @@ +import React, { useState, useEffect } from "react"; +import * as monaco from "monaco-editor"; + +const useMonaco = ( + monacoRef: React.RefObject, + didMount?: (editor: monaco.editor.IStandaloneCodeEditor) => any, + onChange?: (event: monaco.editor.IModelContentChangedEvent, schema: string) => any, + watchers?: any[], +) => { + const [editor, setEditor] = useState(); + const updateDimensions = () => { + if (editor) { + editor.layout(); + } + }; + useEffect(() => { + let onChangeRef: monaco.IDisposable; + if (monacoRef && monacoRef.current) { + const e = monaco.editor.create(monacoRef.current); + setEditor(e); + if (didMount) { + didMount(editor); + } + onChangeRef = e.onDidChangeModelContent((event: monaco.editor.IModelContentChangedEvent) => { + if (onChange) { + const timerLabel = "monaco editor getValue"; + console.time(timerLabel); + const v = e.getValue(); + console.timeEnd(timerLabel); + onChange(event, v); + } + }); + window.addEventListener("resize", updateDimensions); + } + return () => { + window.removeEventListener("resize", updateDimensions); + if (editor) { + editor.dispose(); + } + if (onChangeRef) { + onChangeRef.dispose(); + } + }; + }, [...watchers || []]); + return [editor, updateDimensions]; +}; + +export default useMonaco; diff --git a/src/hooks/useMonacoModel.tsx b/src/hooks/useMonacoModel.tsx new file mode 100644 index 00000000..2193be92 --- /dev/null +++ b/src/hooks/useMonacoModel.tsx @@ -0,0 +1,51 @@ +import { useState, useEffect } from "react"; +import * as monaco from "monaco-editor"; +import schema from "@open-rpc/meta-schema"; + +const useMonacoModel = ( + defaultValue: string | undefined | null, + editor: monaco.editor.IStandaloneCodeEditor, +) => { + const [model, setModel] = useState(); + const [position, setPosition] = useState([4, 13, 4, 13]); + useEffect(() => { + // const existingModel = monaco.editor.getModels()[0]; + // if (!model && existingModel && editor) { + // editor.setModel(existingModel); + // setModel(existingModel); + // return; + // } + if (editor) { + const modelUri = monaco.Uri.parse(`inmemory:/${Math.random()}/model/userSpec.json`); + const m = monaco.editor.createModel(defaultValue || "", "json", modelUri); + monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ + enableSchemaRequest: true, + schemas: [ + { + fileMatch: ["*"], + schema, + uri: modelUri.toString(), + }, + ], + validate: true, + }); + m.updateOptions({ tabSize: 2 }); + setModel(m); + editor.setModel(m); + const [selectionStartLineNumber, selectionStartColumn, positionLineNumber, positionColumn] = position; + editor.setSelection( + new monaco.Selection(selectionStartLineNumber, selectionStartColumn, positionLineNumber, positionColumn) + ); + editor.focus(); + } + + return () => { + if (model) { + model.dispose(); + } + }; + }, [editor, defaultValue]); + return [model, setPosition]; +}; + +export default useMonacoModel; diff --git a/src/hooks/useParsedSchema.tsx b/src/hooks/useParsedSchema.tsx new file mode 100644 index 00000000..27611dab --- /dev/null +++ b/src/hooks/useParsedSchema.tsx @@ -0,0 +1,25 @@ +import { useState } from "react"; +import _ from "lodash"; +import refParser from "json-schema-ref-parser"; + +const useParsedSchema = (defaultValue: object | any) => { + const [parsedSchema, setParsedSchema] = useState(defaultValue); + const validateAndSetSchema = (schema: string) => { + let maybeSchema; + try { + maybeSchema = JSON.parse(schema); + } catch (e) { + // + } + if (!maybeSchema) { + return; + } + refParser.dereference(maybeSchema).then((dereferencedSchema) => { + setParsedSchema(dereferencedSchema); + _.defer(() => window.localStorage.setItem("schema", schema)); + }); + }; + return [parsedSchema, validateAndSetSchema]; +}; + +export default useParsedSchema; diff --git a/src/hooks/useQueryParams.ts b/src/hooks/useQueryParams.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/hooks/useSearchBar.tsx b/src/hooks/useSearchBar.tsx new file mode 100644 index 00000000..8c42634b --- /dev/null +++ b/src/hooks/useSearchBar.tsx @@ -0,0 +1,44 @@ +import { useState, useEffect, Dispatch } from "react"; +import { isEmpty } from "lodash"; +import fetchUrlSchemaFile from "../fetchUrlSchemaFile"; +import fetchSchemaFromRpcDiscover from "../fetchSchemaFromRpcDiscover"; + +interface ISearchBarResponse { + results: any; + error: string | undefined; +} + +const useSearchBar = (defaultValue: string | undefined): [string | undefined, ISearchBarResponse, Dispatch] => { + const [searchUrl, setSearchUrl] = useState(defaultValue); + const [results, setResults] = useState(); + const [error, setError] = useState(); + useEffect(() => { + if (!searchUrl) { + return; + } + if (isEmpty(searchUrl)) { + return; + } + if (searchUrl.match(/\.json$/)) { + fetchUrlSchemaFile(searchUrl) + .then(setResults) + .catch((e) => { + const msg = `Error fetching schema for: ${searchUrl}`; + console.error(msg, e); + setError(msg); + }); + } + else { + fetchSchemaFromRpcDiscover(searchUrl) + .then(setResults) + .catch((e) => { + const msg = `Error fetching rpc.discover for: ${searchUrl}`; + console.error(msg, e); + setError(msg); + }); + } + }, [searchUrl]); + return [searchUrl, { results, error }, setSearchUrl]; +}; + +export default useSearchBar; diff --git a/src/hooks/useUISchema.tsx b/src/hooks/useUISchema.tsx new file mode 100644 index 00000000..48aa9252 --- /dev/null +++ b/src/hooks/useUISchema.tsx @@ -0,0 +1,24 @@ +import { useState } from "react"; +import { IUISchema } from "../UISchema"; + +type SetSectionType = ({ section, key, value }: { + section: string; + key: string; + value: any; +}) => any; + +const useUISchema = (defaultValue: IUISchema): [IUISchema, SetSectionType] => { + const [UISchema, setUISchema] = useState(defaultValue); + const setUISchemaBySection: SetSectionType = ({ section, key, value }) => { + setUISchema({ + ...UISchema, + [section]: { + ...UISchema.appBar, + [key]: value, + }, + }); + }; + return [UISchema, setUISchemaBySection]; +}; + +export default useUISchema;