feat: Request and Responses Panes, CodeMirror View

This commit is contained in:
Anoop M D 2021-12-04 17:00:03 +05:30
parent f6732e66a0
commit 8b586bdfae
27 changed files with 10300 additions and 3290 deletions

5104
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,8 +5,6 @@
"packages/grafnode-components",
"packages/grafnode-www"
],
"dependencies": {
},
"devDependencies": {
"@babel/core": "^7.16.0",
"@babel/preset-env": "^7.16.4",
@ -17,6 +15,7 @@
"html-loader": "^3.0.1",
"html-webpack-plugin": "^5.5.0",
"lerna": "^4.0.0",
"mini-css-extract-plugin": "^2.4.5",
"style-loader": "^3.3.1",
"webpack": "^5.64.4",
"webpack-cli": "^4.9.1"

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,7 @@
"description": "",
"main": "dist/index.js",
"scripts": {
"watch:build": "webpack --watch --mode development",
"build": "webpack --mode production",
"test": "echo \"Error: no test specified\" && exit 1"
},
@ -24,6 +25,7 @@
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/react-fontawesome": "^0.1.16",
"classnames": "^2.3.1",
"codemirror": "^5.64.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-tabs": "^3.2.3",

View File

@ -0,0 +1,10 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
div.CodeMirror {
border: solid 1px #e1e1e1;
}
`;
export default StyledWrapper;

View File

@ -0,0 +1,46 @@
import React, { useState, useEffect, useRef } from 'react';
import StyledWrapper from './StyledWrapper';
import * as CodeMirror from 'codemirror';
const QueryEditor = ({query, onChange, width}) => {
const [cmEditor, setCmEditor] = useState(null);
const editor = useRef();
useEffect(() => {
if (editor.current && !cmEditor) {
const _cmEditor = CodeMirror.fromTextArea(editor.current, {
value: '',
lineNumbers: true
});
_cmEditor.setValue(query || 'query { }');
setCmEditor(_cmEditor);
}
return () => {
if(cmEditor) {
cmEditor.toTextArea();
}
}
}, [editor.current, cmEditor]);
return (
<StyledWrapper>
<div className="mt-4">
<textarea
id="operation"
style={{
width: `${width}px`,
height: '400px'
}}
ref={editor}
className="cm-editor"
>
</textarea>
</div>
</StyledWrapper>
);
};
export default QueryEditor;

View File

@ -0,0 +1,26 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
div.CodeMirror {
border: solid 1px #e1e1e1;
}
div.overlay{
position: absolute;
top: 0;
right: 0;
left: 0;
bottom: 0;
z-index: 9;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
text-align: center;
background: rgb(243 243 243 / 78%);
}
`;
export default StyledWrapper;

View File

@ -0,0 +1,66 @@
import React, { useState, useRef, useEffect } from 'react';
import { IconRefresh } from '@tabler/icons';
import StopWatch from '../StopWatch';
import * as CodeMirror from 'codemirror';
import StyledWrapper from './StyledWrapper';
const QueryResult = ({data, isLoading, width}) => {
const [cmEditor, setCmEditor] = useState(null);
const editor = useRef();
useEffect(() => {
if (editor.current && !cmEditor) {
const _cmEditor = CodeMirror.fromTextArea(editor.current, {
value: '',
lineNumbers: true
});
setCmEditor(_cmEditor);
}
if(editor.current && cmEditor && data && !isLoading) {
cmEditor.setValue(JSON.stringify(data, null, 2));
}
return () => {
if(cmEditor) {
cmEditor.toTextArea();
setCmEditor(null);
}
}
}, [editor.current, cmEditor, data]);
return (
<StyledWrapper className="mt-4" style={{position: 'relative'}}>
{isLoading && (
<div className="overlay">
<div style={{marginBottom: 15, fontSize: 26}}>
<div style={{display: 'inline-block', fontSize: 24, marginLeft: 5, marginRight: 5}}>
<StopWatch/>
</div>
</div>
<IconRefresh size={24} className="animate-spin"/>
<button
className="mt-4 uppercase bg-gray-200 active:bg-blueGray-600 text-xs px-4 py-2 rounded shadow hover:shadow-md outline-none focus:outline-none mr-1 mb-1 ease-linear transition-all duration-150" type="button"
>
Cancel Request
</button>
</div>
)}
<div>
<textarea
id="operation"
style={{
width: `${width}px`,
height: '400px'
}}
ref={editor}
className="cm-editor"
>
</textarea>
</div>
</StyledWrapper>
);
};
export default QueryResult;

View File

@ -0,0 +1,26 @@
import styled from 'styled-components';
const Wrapper = styled.div`
height: 2.3rem;
div.method-selector {
border: solid 1px #cfcfcf;
border-right: none;
}
div.input-container {
border: solid 1px #cfcfcf;
input {
outline: none;
box-shadow: none;
&:focus {
outline: none !important;
box-shadow: none !important;
}
}
}
`;
export default Wrapper;

View File

@ -0,0 +1,107 @@
import React, { useRef, forwardRef } from 'react';
import PropTypes from 'prop-types';
import { faCaretDown } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import Dropdown from '../Dropdown';
import StyledWrapper from './StyledWrapper';
const QueryUrl = ({value, onChange, handleRun}) => {
const dropdownTippyRef = useRef();
const viewProfile = () => {};
const Icon = forwardRef((props, ref) => {
return (
<div ref={ref} className="user-icon items-center justify-center pl-3 py-2 select-none">
GET <FontAwesomeIcon className="ml-4 mr-1 text-gray-500" icon={faCaretDown} style={{fontSize: 13}}/>
</div>
);
});
const onDropdownCreate = (ref) => dropdownTippyRef.current = ref;
return (
<StyledWrapper className="mt-3 flex items-center">
<div className="flex items-center cursor-pointer user-action-dropdown h-full method-selector pr-3">
<Dropdown onCreate={onDropdownCreate} icon={<Icon />} placement='bottom-start'>
<div>
<div className="dropdown-item" onClick={() => {
dropdownTippyRef.current.hide();
viewProfile();
}}>
GET
</div>
</div>
<div>
<div className="dropdown-item" onClick={() => {
dropdownTippyRef.current.hide();
viewProfile();
}}>
POST
</div>
</div>
<div>
<div className="dropdown-item" onClick={() => {
dropdownTippyRef.current.hide();
viewProfile();
}}>
PUT
</div>
</div>
<div>
<div className="dropdown-item" onClick={() => {
dropdownTippyRef.current.hide();
viewProfile();
}}>
DELETE
</div>
</div>
<div>
<div className="dropdown-item" onClick={() => {
dropdownTippyRef.current.hide();
viewProfile();
}}>
PATCH
</div>
</div>
<div>
<div className="dropdown-item" onClick={() => {
dropdownTippyRef.current.hide();
viewProfile();
}}>
OPTIONS
</div>
</div>
<div>
<div className="dropdown-item" onClick={() => {
dropdownTippyRef.current.hide();
viewProfile();
}}>
HEAD
</div>
</div>
</Dropdown>
</div>
<div className="flex items-center flex-grow input-container h-full">
<input
className="px-3 w-full"
type="text" value={value} onChange={(event) => onChange(event.target.value)}
/>
</div>
<button
style={{backgroundColor: '#8e44ad'}}
className="flex items-center h-full text-white active:bg-blue-600 font-bold text-xs px-4 py-2 ml-2 uppercase rounded shadow hover:shadow-md outline-none focus:outline-none ease-linear transition-all duration-150"
onClick={handleRun}
>
<span style={{marginLeft: 5}}>Send</span>
</button>
</StyledWrapper>
)
};
QueryUrl.propTypes = {
value: PropTypes.string,
onChange: PropTypes.func.isRequired,
handleRun: PropTypes.func.isRequired
};
export default QueryUrl;

View File

@ -0,0 +1,55 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.react-tabs__tab-list {
border-bottom: none !important;
padding-top: 0;
padding-left: 0 !important;
display: flex;
align-items: center;
margin: 0;
.react-tabs__tab {
font-size: 14px;
padding: 6px 0px;
border: none;
user-select: none;
border-bottom: solid 2px transparent;
margin-right: 20px;
color: rgb(117 117 117);
outline: none !important;
&:focus, &:active, &:focus-within, &:focus-visible, &:target {
outline: none !important;
box-shadow: none !important;
}
&:after {
display: none !important;
}
}
}
.react-tabs__tab--selected {
border: none;
color: #322e2c !important;
border-bottom: solid 2px #8e44ad !important;
border-color: #8e44ad !important;
background: inherit;
outline: none !important;
box-shadow: none !important;
&:focus, &:active, &:focus-within, &:focus-visible, &:target {
border: none;
outline: none !important;
box-shadow: none !important;
border-bottom: solid 2px #8e44ad !important;
border-color: #8e44ad !important;
background: inherit;
outline: none !important;
box-shadow: none !important;
}
}
`;
export default StyledWrapper;

View File

@ -0,0 +1,29 @@
import React from 'react';
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
import StyledWrapper from './StyledWrapper';
import QueryEditor from '../QueryEditor';
const RequestPane = ({leftPaneWidth, query, onQueryChange}) => {
return (
<StyledWrapper className="">
<Tabs className='react-tabs mt-1 flex flex-grow flex-col' forceRenderTabPanel>
<TabList>
<Tab tabIndex="-1">Query</Tab>
<Tab tabIndex="-1">Headers</Tab>
</TabList>
<TabPanel>
<QueryEditor
width={leftPaneWidth}
query={query}
onChange={onQueryChange}
/>
</TabPanel>
<TabPanel>
Headers
</TabPanel>
</Tabs>
</StyledWrapper>
)
};
export default RequestPane;

View File

@ -0,0 +1,6 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
`;
export default StyledWrapper;

View File

@ -0,0 +1,141 @@
import React, { useState, useEffect } from 'react';
import find from 'lodash/find';
import { rawRequest, gql } from 'graphql-request';
import QueryUrl from '../QueryUrl';
import RequestPane from '../RequestPane';
import ResponsePane from '../ResponsePane';
import {
flattenItems,
findItem
} from '../../utils';
import StyledWrapper from './StyledWrapper';
const RequestTabPanel = ({collections, activeRequestTabId, requestTabs}) => {
if(typeof window == 'undefined') {
return <div></div>;
}
let asideWidth = 200;
let [data, setData] = useState({});
let [url, setUrl] = useState('https://api.spacex.land/graphql');
let [query, setQuery] = useState('');
let [isLoading, setIsLoading] = useState(false);
const [leftPaneWidth, setLeftPaneWidth] = useState(500);
const [rightPaneWidth, setRightPaneWidth] = useState(window.innerWidth - 700 - asideWidth);
const [dragging, setDragging] = useState(false);
const handleMouseMove = (e) => {
e.preventDefault();
if(dragging) {
setLeftPaneWidth(e.clientX - asideWidth );
setRightPaneWidth(window.innerWidth - (e.clientX));
}
};
const handleMouseUp = (e) => {
e.preventDefault();
setDragging(false);
};
const handleMouseDown = (e) => {
e.preventDefault();
setDragging(true);
};
useEffect(() => {
document.addEventListener('mouseup', handleMouseUp);
document.addEventListener('mousemove', handleMouseMove);
return () => {
document.removeEventListener('mouseup', handleMouseUp);
document.removeEventListener('mousemove', handleMouseMove);
};
}, [dragging, leftPaneWidth]);
const onUrlChange = (value) => {
setUrl(value);
};
const onQueryChange = (value) => {
setQuery(value);
};
if(!activeRequestTabId) {
return (
<div className="pb-4 px-4">No request selected</div>
);
}
const focusedTab = find(requestTabs, (rt) => rt.id === activeRequestTabId);
if(!focusedTab || !focusedTab.id) {
return (
<div className="pb-4 px-4">An error occured!</div>
);
}
const collection = find(collections, (c) => c.id === focusedTab.collectionId);
const flattenedItems = flattenItems(collection.items);
const item = findItem(flattenedItems, activeRequestTabId);
const runQuery = async () => {
const query = gql`${item.request.body.graphql.query}`;
setIsLoading(true);
const { data, errors, extensions, headers, status } = await rawRequest(item.request.url, query);
setData(data);
setIsLoading(false);
console.log(data);
console.log(headers);
// request(item.request.url, gql`${item.request.body.graphql.query}`)
// .then((data, stuff) => {
// console.log(data);
// console.log(stuff);
// setData(data);
// setIsLoading(false);
// })
// .catch((err) => {
// setIsLoading(false);
// console.log(err);
// });
};
return (
<StyledWrapper>
<div
className="pb-4 px-4"
style={{
borderBottom: 'solid 1px #e1e1e1'
}}
>
<div className="pt-2 text-gray-600">{item.name}</div>
<QueryUrl
value = {url}
onChange={onUrlChange}
handleRun={runQuery}
/>
</div>
<section className="main">
<section className="request-pane px-4">
<div style={{width: `${leftPaneWidth}px`}}>
<RequestPane
leftPaneWidth={leftPaneWidth}
query={item.request.body.graphql.query}
onQueryChange={onQueryChange}
/>
</div>
</section>
<div className="drag-request" onMouseDown={handleMouseDown}>
</div>
<section className="response-pane px-4 flex-grow">
<ResponsePane
rightPaneWidth={rightPaneWidth}
data={data}
isLoading={isLoading}
/>
</section>
</section>
</StyledWrapper>
)
};
export default RequestTabPanel;

View File

@ -0,0 +1,26 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
div.tabs {
div.tab {
padding: 6px 0px;
border: none;
border-bottom: solid 2px transparent;
margin-right: 20px;
color: rgb(117 117 117);
cursor: pointer;
&:focus, &:active, &:focus-within, &:focus-visible, &:target {
outline: none !important;
box-shadow: none !important;
}
&.active {
color: #322e2c !important;
border-bottom: solid 2px #8e44ad !important;
}
}
}
`;
export default StyledWrapper;

View File

@ -0,0 +1,49 @@
import React, { useState } from 'react';
import classnames from 'classnames';
import QueryResult from '../QueryResult';
import StyledWrapper from './StyledWrapper';
const ResponsePane = ({rightPaneWidth, data, isLoading}) => {
const [selectedTab, setSelectedTab] = useState('response');
const getTabClassname = (tabName) => {
return classnames(`tab select-none ${tabName}`, {
'active': tabName === selectedTab
});
};
const getTabPanel = (tab) => {
switch(tab) {
case 'response': {
return (
<QueryResult
width={rightPaneWidth}
data={data}
isLoading={isLoading}
/>
);
}
case 'headers': {
return <div>Headers</div>;
}
default: {
return <div>404 | Not found</div>;
}
}
}
return (
<StyledWrapper className="">
<div className="flex items-center tabs mt-1" role="tablist">
<div className={getTabClassname('response')} role="tab" onClick={() => setSelectedTab('response')}>Response</div>
<div className={getTabClassname('headers')} role="tab" onClick={() => setSelectedTab('headers')}>Headers</div>
</div>
<section>
{getTabPanel(selectedTab)}
</section>
</StyledWrapper>
)
};
export default ResponsePane;

View File

@ -0,0 +1,17 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.spinner {
display: inline-block;
width: 2rem;
height: 2rem;
vertical-align: text-bottom;
border: 0.25em solid currentColor;
border-right-color: transparent;
border-radius: 50%;
animation: spinner-border .75s linear infinite;
}
`;
export default StyledWrapper;

View File

@ -0,0 +1,18 @@
import React from 'react';
import StyledWrapper from './StyledWrapper';
// Todo: Size, Color config support
const Spinner = ({size, color, children}) => {
return (
<StyledWrapper>
<div className="animate-spin"></div>
{children &&
<div>
{children}
</div>
}
</StyledWrapper>
)
};
export default Spinner;

View File

@ -0,0 +1,30 @@
import React, { useState, useEffect } from 'react';
const StopWatch = () => {
const [milliseconds, setMilliseconds] = useState(0);
const tickInterval = 200;
const tick = () => {
setMilliseconds(milliseconds + tickInterval);
};
useEffect(() => {
let timerID = setInterval(() => tick(), tickInterval);
return () => {
clearInterval(timerID);
};
});
if(milliseconds < 1000) {
return 'Loading...';
}
let seconds = milliseconds/1000;
return (
<span>
{seconds.toFixed(1)}s
</span>
)
};
export default StopWatch;

View File

@ -1,9 +1,11 @@
import Navbar from './components/Navbar';
import Sidebar from './components/Sidebar';
import RequestTabs from './components/RequestTabs';
import RequestTabPanel from './components/RequestTabPanel';
export {
Navbar,
Sidebar,
RequestTabs
RequestTabs,
RequestTabPanel
};

View File

@ -0,0 +1,32 @@
import each from 'lodash/each';
import find from 'lodash/find';
export const flattenItems = (items = []) => {
const flattenedItems = [];
const flatten = (itms, flattened) => {
each(itms, (i) => {
flattened.push(i);
if(i.items && i.items.length) {
flatten(i.items, flattened);
}
})
}
flatten(items, flattenedItems);
return flattenedItems;
};
export const findItem = (items = [], itemId) => {
return find(items, (i) => i.id === itemId);
};
export const isItemARequest = (item) => {
return item.hasOwnProperty('request');
};
export const itemIsOpenedInTabs = (item, tabs) => {
return find(tabs, (t) => t.id === item.id);
};

View File

@ -1,8 +1,10 @@
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
entry: "./src/index.js",
output: {
publicPath: '',
globalObject: 'this',
filename: "index.js",
path: path.resolve(__dirname, "dist"),
@ -17,15 +19,27 @@ module.exports = {
use: {
loader: "babel-loader"
}
},
{
test: /\.css$/,
use: [ MiniCssExtractPlugin.loader, 'css-loader' ]
}
]
},
externals: {
'react': 'react',
'lodash': 'lodash',
'styled-components': 'styled-components',
'@tabler/icon': '@tabler/icon',
'@tippyjs/react': '@tippyjs/react',
'@tabler/icons': '@tabler/icons',
'@fortawesome/free-solid-svg-icons': '@fortawesome/free-solid-svg-icons',
'@fortawesome/react-fontawesome': '@fortawesome/react-fontawesome',
'classnames': 'classnames'
}
'classnames': 'classnames',
'react-tabs': 'react-tabs',
'codemirror': 'codemirror',
'graphql-request': 'graphql-request'
},
plugins: [
new MiniCssExtractPlugin()
]
};

File diff suppressed because it is too large Load Diff

View File

@ -27,11 +27,15 @@
"@tabler/icons": "^1.46.0",
"@tippyjs/react": "^4.2.6",
"classnames": "^2.3.1",
"codemirror": "^5.64.0",
"graphql-request": "^3.7.0",
"immer": "^9.0.7",
"lodash": "^4.17.21",
"nanoid": "^3.1.30",
"next": "12.0.4",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-tabs": "^3.2.3",
"styled-components": "^5.3.3",
"tailwindcss": "^2.2.19"
},

View File

@ -1,14 +1,22 @@
import React from 'react';
import dynamic from 'next/dynamic'
import {
Navbar,
RequestTabs,
Sidebar
} from '@grafnode/components';
import actions from 'providers/Store/actions';
import { useStore } from 'providers/Store';
import { useStore } from '../../providers/Store/index';
import StyledWrapper from './StyledWrapper';
const RequestTabPanel = dynamic(import('@grafnode/components').then(mod => mod.RequestTabPanel), { ssr: false });
export default function Main() {
// disable ssr
if(typeof window === 'undefined') {
return null;
}
const [state, dispatch] = useStore();
const {
@ -17,8 +25,6 @@ export default function Main() {
activeRequestTabId
} = state;
console.log(actions);
return (
<div>
<Navbar />
@ -36,6 +42,11 @@ export default function Main() {
dispatch={dispatch}
activeRequestTabId={activeRequestTabId}
/>
<RequestTabPanel
collections={collections}
requestTabs={requestTabs}
activeRequestTabId={activeRequestTabId}
/>
</section>
</StyledWrapper>
</div>

View File

@ -2,6 +2,8 @@ import { StoreProvider } from 'providers/Store';
import '../styles/globals.css'
import 'tailwindcss/dist/tailwind.min.css';
import 'react-tabs/style/react-tabs.css';
import 'codemirror/lib/codemirror.css';
function MyApp({ Component, pageProps }) {
return (