├── .DS_Store ├── .babelrc ├── .env.development ├── .env.production ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .prettierrc.json ├── README.md ├── __tests__ ├── contextReducer.js ├── enzyme.js ├── fetchErrorCheck.js └── puppeteer.js ├── assets ├── Logo-full-size.png ├── Logo-full.png ├── enter-endpoint.gif ├── enter-query.gif ├── errors.gif ├── history.gif ├── hypnos-icon.png ├── hypnos-logo-large.png └── tabs.gif ├── client ├── App.jsx ├── Components │ ├── APIKeyModal.jsx │ ├── EndpointField.jsx │ ├── Header.jsx │ ├── HistoryDisplay.jsx │ ├── MiniComponents │ │ ├── ErrorDisplay.jsx │ │ ├── HistoryListItem.jsx │ │ ├── NullResultCheck.jsx │ │ ├── TabsDeleteButton.jsx │ │ └── URLResultCheck.jsx │ ├── QueryInput.jsx │ └── QueryOutputDisplay.jsx ├── Constants │ ├── actionTypes.js │ ├── defaultEndpoint.js │ ├── errors │ │ ├── errorDispatchObjects.js │ │ ├── errorResponseStrings.js │ │ └── errorStrings.js │ └── inlineComponentStyle.js ├── Containers │ ├── QueriesContainer.jsx │ └── TabsManager.jsx ├── Context.js ├── StyleSheets │ ├── App.scss │ ├── TabPanel.scss │ ├── _APIKeyModal.scss │ ├── _buttons.scss │ ├── _endpointField.scss │ ├── _header.scss │ ├── _historyDisplay.scss │ ├── _queriesContainer.scss │ ├── _queryInput.scss │ ├── _queryOutputDisplay.scss │ ├── _spinner.scss │ ├── external │ │ ├── CodeMirror.css │ │ └── react-tabs.css │ └── variables.scss ├── db.js ├── index.js └── utils │ ├── queryInput │ ├── addQueryToDB.js │ └── fetchErrorCheck.js │ └── queryOutputDisplay │ ├── jsonFormatter.js │ ├── nullChecker.js │ └── nullResultChecker.jsx ├── dev └── Code Snippets │ ├── DEV_QueryInput.jsx │ └── DEV_handleQueryFetch.js ├── electron.js ├── index.html ├── package-lock.json └── package.json /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/hypnos/0c131c66582f3f444b02bdbe2df234560b589127/.DS_Store -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-react", 4 | [ 5 | "@babel/preset-env", 6 | { 7 | "targets": { 8 | "node": "current" 9 | } 10 | } 11 | ] 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | IS_DEV=1 -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | IS_DEV=0 -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true 5 | }, 6 | "extends": ["airbnb", "plugin:prettier/recommended"], 7 | "parser": "babel-eslint", 8 | "globals": { 9 | "Atomics": "readonly", 10 | "SharedArrayBuffer": "readonly" 11 | }, 12 | "parserOptions": { 13 | "ecmaFeatures": { 14 | "jsx": true 15 | }, 16 | "ecmaVersion": 2018, 17 | "sourceType": "module" 18 | }, 19 | "plugins": ["react", "prettier"], 20 | "rules": { 21 | "prettier/prettier": ["error"] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | dist/ 4 | package-lock.json 5 | .cache/ 6 | .vscode/ 7 | .DS_Store -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) 2 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 3 |

4 | 5 |

6 | 7 | ## HYPNOS 8 | 9 | Welcome to __Hypnos(beta)__, a streamlined desktop application to sandbox GraphQL calls to RESTful APIs using the Apollo-link-rest package from Apollo Client, as well as giving detailed, helpful error messages. 10 | 11 | 12 | 13 | 14 | 15 | *\* We recently added tabs to handle multiple entries!* 16 | 17 | ## Getting Started 18 | 19 | __Requirements__ 20 | 21 | All you need to do is download your respective OS package, install, and go! 22 | 23 | 24 | __How to Install__ 25 | 26 | Download for Mac or Windows (Linux coming soon!) 27 | 28 | __Mac__: [hypnos-0.0.2.dmg](https://github.com/oslabs-beta/hypnos/releases/download/0.0.2/Hypnos-0.0.2-mac.dmg) 29 | 30 | __Windows Zipped Config__: [hypnos-0.0.2.zip](https://github.com/oslabs-beta/hypnos/releases/download/0.0.2/Hypnos-0.0.2-win.zip) 31 | 32 | Install the app to your applications folder. 33 | 34 | ## How to Use 35 | 36 | __Enter your API endpoint__ 37 | 38 | Hypnos supports calls to both open APIs and APIs requiring a key. Future versions will include support for APIs requiring Basic Auth and Oauth2 39 | 40 |

41 | 42 |

43 | 44 | 45 | __Enter your Apollo-link-rest query__ 46 | 47 | A sample query is provided. Further documentation on Apollo-link-rest calls can be found in the Apollo GraphQL documents here: 48 | 49 |

50 | 51 |

52 | 53 | 54 | __Hypnos will return the GraphQL response or meaningful errors__ 55 | 56 | If there are any errors in query formatting, they will be displayed on the right 57 | 58 |

59 | 60 |

61 | 62 | 63 | ## Features 64 | 65 | __Hypnos History__ 66 | 67 | Hypnos will store your local query history, persisting queries from your sessions. You can delete your entire history, or you can delete individual queries. You also have the option of repopulating the current tab with a previous query. 68 | 69 |

70 | 71 |

72 | 73 | __Tabs__ 74 | 75 | Want to submit another query, but don't want to submit your current query just yet? Just open up a new tab! Hypnos will save the endpoint and query, and you can come back to it later. 76 | 77 |

78 | 79 |

80 | 81 | __Authenticated APIs__ 82 | 83 | A majority of APIs require a key in order to gain access to their endpoints, but have no fear: Hypnos supports API keys. Type in your endpoint and query as you would normally, but before you submit the query, go to __Submit API Key__. Clicking on it will open a new window in which you can enter the key for your headers and the API key given to you by the API itself. Once you submit the keys, you can go ahead and submit your query. 84 | 85 | 86 | 87 | ## Resources 88 | 89 | Built with Electron and React 90 | 91 | 92 | __Authors__: [Dillon Garrett](https://github.com/dillon-garrett), [Sophie Nye](https://github.com/SophieNye), [Will Robinson](https://github.com/wrobinson91) 93 | -------------------------------------------------------------------------------- /__tests__/contextReducer.js: -------------------------------------------------------------------------------- 1 | import { reducer, initialState } from '../client/Context'; 2 | import * as types from '../client/Constants/actionTypes'; 3 | import * as errorResponse from '../client/Constants/errors/errorResponseStrings'; 4 | 5 | describe('React Context reducer tests', () => { 6 | // console.log('reducer is: ', reducer); 7 | let state; 8 | let newState; 9 | 10 | beforeEach(() => { 11 | state = { ...initialState }; 12 | }); 13 | 14 | describe('default state', () => { 15 | it("reducer should return same state when there's no type property in action parameter", () => { 16 | // strict equality in this instance 17 | expect(reducer(state, {})).toBe(state); 18 | }); 19 | it('reducer should return same state when given unknown action type', () => { 20 | // strict equality in this instance 21 | expect(reducer(state, { type: 'test', payload: 'test' })).toBe(state); 22 | }); 23 | }); 24 | 25 | describe('action type tests', () => { 26 | // mock/lite version of payload from running a succesful query 27 | const mockPayload = { 28 | newAPIKey: '', 29 | newEndpoint: 'https://api.tvmaze.com/', 30 | newHeadersKey: '', 31 | query: { 32 | definitions: [{}], 33 | kind: 'Document', 34 | loc: { 35 | end: 237, 36 | source: {}, 37 | start: 0 38 | } 39 | }, 40 | queryResultObject: 'show', 41 | ranQueryTab: '1', 42 | type: types.RUN_QUERY 43 | }; 44 | 45 | describe('RUN_QUERY case', () => { 46 | beforeAll(() => { 47 | // receive new state after reducer has been run 48 | newState = reducer(state, mockPayload); 49 | }); 50 | 51 | it('queryGQLError, endpointFromDB, historyTextValue should always be empty strings', () => { 52 | expect(newState.queryGQLError).toBe(''); 53 | expect(newState.endpointFromDB).toBe(''); 54 | expect(newState.historyTextValue).toBe(''); 55 | }); 56 | 57 | it("expect queryResultObject to be 'show'", () => { 58 | expect(newState.queryResultObject).toBe('show'); 59 | }); 60 | 61 | it('expect headersKey to be empty string', () => { 62 | expect(newState.headersKey).toBe(''); 63 | }); 64 | 65 | it('expect apiKey to be empty string', () => { 66 | expect(newState.apiKey).toBe(''); 67 | }); 68 | 69 | it('expect query property to contain shallow copy of action query & tab from which query was run', () => { 70 | // newState = reducer(state, mockPayload); 71 | expect(newState.query.query).toEqual(mockPayload.query); 72 | expect(newState.query.ranQueryTab).toBe(mockPayload.ranQueryTab); 73 | }); 74 | 75 | it('expect endpointHistory at index 1 to be changed to what is in payload', () => { 76 | expect(newState.endpointHistory[mockPayload.ranQueryTab]).toBe(mockPayload.newEndpoint); 77 | }); 78 | }); 79 | 80 | describe('RESET_STATE case', () => { 81 | beforeAll(() => { 82 | // run a query on tab '1' 83 | newState = reducer(state, mockPayload); 84 | // run a new query from a new tab 85 | newState = reducer(newState, { ...mockPayload, ranQueryTab: '2' }); 86 | // reset on the secondary tab 87 | newState = reducer(newState, { type: types.RESET_STATE, currentTab: '2' }); 88 | }); 89 | 90 | it('state outside of endpointHistory for everything but current tab should match initial state version', () => { 91 | expect(newState.query).toEqual(initialState.query); 92 | expect(newState.queryResultObject).toBe(initialState.queryResultObject); 93 | expect(newState.queryGQLError).toBe(initialState.queryGQLError); 94 | expect(newState.endpoint).toBe(initialState.endpoint); 95 | expect(newState.historyTextValue).toBe(initialState.historyTextValue); 96 | expect(newState.historyIdx).toBe(initialState.historyIdx); 97 | expect(newState.headersKey).toBe(initialState.headersKey); 98 | expect(newState.apiKey).toBe(initialState.apiKey); 99 | expect(newState.endpointFromDB).toBe(initialState.endpointFromDB); 100 | // this is the only data point that should NOT match the initial state 101 | expect(newState.endpointHistory[mockPayload.ranQueryTab]).toBe(mockPayload.newEndpoint); 102 | expect(newState.endpointHistory['2']).toBe(initialState.endpoint); 103 | }); 104 | }); 105 | 106 | describe('GQL_ERROR case', () => { 107 | // when a query is not constrcucted correctly 108 | beforeAll(() => { 109 | // run one successful query 110 | newState = reducer(state, mockPayload); 111 | // query construction errored out 112 | newState = reducer(newState, { 113 | type: types.GQL_ERROR, 114 | // picked one specific error -- dispatch format is the same for any of these cases 115 | gqlError: errorResponse.queryMethodError 116 | }); 117 | }); 118 | it('query object should have errored version', () => { 119 | expect(newState.query).toEqual({ 120 | query: '', 121 | ranQueryTab: -1 122 | }); 123 | }); 124 | it('expect queryResultObject and historyTextValue to be empty strings', () => { 125 | expect(newState.queryResultObject).toBe(''); 126 | expect(newState.historyTextValue).toBe(''); 127 | }); 128 | it('expect queryGQLError to match error message from payload', () => { 129 | expect(newState.queryGQLError).toBe(errorResponse.queryMethodError); 130 | }); 131 | }); 132 | }); 133 | }); 134 | -------------------------------------------------------------------------------- /__tests__/enzyme.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { configure, shallow, mount } from 'enzyme'; 3 | import Adapter from 'enzyme-adapter-react-16'; 4 | import toJson from 'enzyme-to-json'; 5 | import renderer from 'react-test-renderer'; 6 | import Dexie from 'dexie'; 7 | import Modal from 'react-modal'; 8 | import indexedDB from 'fake-indexeddb'; 9 | 10 | import HistoryDisplay from '../client/Components/HistoryDisplay'; 11 | import App from '../client/App'; 12 | 13 | configure({ adapter: new Adapter() }); 14 | 15 | const db = new Dexie('testHistoryDB', { indexedDB }); 16 | db.version(1).stores({ 17 | history: '++id, query, endpoint' 18 | }); 19 | 20 | describe('First React component test with Enzyme', () => { 21 | it('renders without crashing', () => { 22 | shallow(); 23 | }); 24 | }); 25 | 26 | describe('React unit tests', () => { 27 | beforeAll(() => { 28 | db.history.put({ 29 | query: 'query1', 30 | endpoint: 'endpoint1' 31 | }); 32 | db.history.put({ 33 | query: 'query2', 34 | endpoint: 'endpoint2' 35 | }); 36 | db.history.put({ 37 | query: 'query3', 38 | endpoint: 'endpoint3' 39 | }); 40 | }); 41 | 42 | describe('HistoryDisplay renders', () => { 43 | let wrapper; 44 | 45 | // beforeAll(() => { 46 | // wrapper = shallow(); 47 | // }); 48 | 49 | it('database returns an array of queries', () => { 50 | console.log('in db test'); 51 | let array = [1, 2, 3]; 52 | db.history.toArray().then(queries => { 53 | array = queries; 54 | }); 55 | expect(array).toHaveLength(3); 56 | }); 57 | }); 58 | 59 | afterAll(() => { 60 | db.history.clear(); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /__tests__/fetchErrorCheck.js: -------------------------------------------------------------------------------- 1 | import fetchErrorCheck from '../client/utils/queryInput/fetchErrorCheck'; 2 | import * as errorMessages from '../client/Constants/errors/errorStrings'; 3 | 4 | const mockDispatch = jest.fn(); 5 | const mockErrorObject = { 6 | locations: [{}], 7 | message: '', 8 | extensions: undefined, 9 | nodes: undefined, 10 | positions: [32], 11 | source: ``, 12 | stack: '' 13 | }; 14 | 15 | describe('fetch error check for syntactically incorrect queries', () => { 16 | beforeEach(() => { 17 | mockDispatch.mockClear(); 18 | mockErrorObject.message = ''; 19 | }); 20 | 21 | it('invoking fetchErrorCheck should invoke dispatch once', () => { 22 | mockErrorObject.message = errorMessages.queryMethodError; 23 | fetchErrorCheck(mockErrorObject, mockDispatch); 24 | expect(mockDispatch.mock.calls.length).toBe(1); 25 | }); 26 | 27 | it('generic error not covered should throw error, and dispatch should be fired once', () => { 28 | mockErrorObject.message = errorMessages.queryMethodError; 29 | try { 30 | fetchErrorCheck(mockErrorObject, mockDispatch); 31 | } catch (e) { 32 | expect(e).toBe(typeof Error); 33 | } finally { 34 | expect(mockDispatch.mock.calls.length).toBe(1); 35 | } 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /__tests__/puppeteer.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require('puppeteer'); 2 | 3 | const APP = `http://localhost:${process.env.PORT || 3000}/`; 4 | 5 | describe('front end renders', () => { 6 | let browser; 7 | let page; 8 | 9 | beforeAll(async () => { 10 | browser = await puppeteer.launch({ 11 | args: ['--no-sandbox', '--disable-setuid-sandbox'] 12 | }); 13 | page = await browser.newPage(); 14 | }); 15 | 16 | afterAll(() => { 17 | browser.close(); 18 | }); 19 | 20 | describe('Initial display', () => { 21 | it('queries container loads successfully', async () => { 22 | // We navigate to the page at the beginning of each case so we have a 23 | // fresh start 24 | await page.goto(APP); 25 | await page.waitForSelector('#queries-container'); 26 | const queriesContainer = await page.$eval('#queries-container', el => !!el); 27 | expect(queriesContainer).toBe(true); 28 | }); 29 | 30 | it('query input loads successfully', async () => { 31 | await page.goto(APP); 32 | await page.waitForSelector('#query-input'); 33 | const queriesInput = await page.$eval('#query-input', el => !!el); 34 | expect(queriesInput).toBe(true); 35 | }); 36 | 37 | it('query output loads successfully', async () => { 38 | await page.goto(APP); 39 | await page.waitForSelector('#query-output'); 40 | const queriesOutput = await page.$eval('#query-output', el => !!el); 41 | expect(queriesOutput).toBe(true); 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /assets/Logo-full-size.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/hypnos/0c131c66582f3f444b02bdbe2df234560b589127/assets/Logo-full-size.png -------------------------------------------------------------------------------- /assets/Logo-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/hypnos/0c131c66582f3f444b02bdbe2df234560b589127/assets/Logo-full.png -------------------------------------------------------------------------------- /assets/enter-endpoint.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/hypnos/0c131c66582f3f444b02bdbe2df234560b589127/assets/enter-endpoint.gif -------------------------------------------------------------------------------- /assets/enter-query.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/hypnos/0c131c66582f3f444b02bdbe2df234560b589127/assets/enter-query.gif -------------------------------------------------------------------------------- /assets/errors.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/hypnos/0c131c66582f3f444b02bdbe2df234560b589127/assets/errors.gif -------------------------------------------------------------------------------- /assets/history.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/hypnos/0c131c66582f3f444b02bdbe2df234560b589127/assets/history.gif -------------------------------------------------------------------------------- /assets/hypnos-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/hypnos/0c131c66582f3f444b02bdbe2df234560b589127/assets/hypnos-icon.png -------------------------------------------------------------------------------- /assets/hypnos-logo-large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/hypnos/0c131c66582f3f444b02bdbe2df234560b589127/assets/hypnos-logo-large.png -------------------------------------------------------------------------------- /assets/tabs.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/hypnos/0c131c66582f3f444b02bdbe2df234560b589127/assets/tabs.gif -------------------------------------------------------------------------------- /client/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // not sure we need the below 3 | // import './StyleSheets/App.scss'; 4 | 5 | // import { 6 | // Tab, Tabs, TabList, TabPanel, 7 | // } from 'react-tabs'; 8 | // not sure we need the below 9 | // import 'react-tabs/style/react-tabs.css'; 10 | 11 | import { RestLink } from 'apollo-link-rest'; 12 | import { ApolloClient } from 'apollo-client'; 13 | import { InMemoryCache } from 'apollo-cache-inmemory'; 14 | import { ApolloProvider } from 'react-apollo'; 15 | import { onError } from 'apollo-link-error'; 16 | import { ApolloLink } from 'apollo-link'; 17 | // import { createHttpLink } from 'apollo-link-http'; 18 | 19 | import Header from './Components/Header'; 20 | // history display now rendered inside Tabs Manager 21 | // import HistoryDisplay from './Components/HistoryDisplay'; 22 | import TabsManager from './Containers/TabsManager'; 23 | // import DeleteButton from './Components/MiniComponents/TabsDeleteButton'; 24 | // import QueriesContainer from './Containers/QueriesContainer'; 25 | import { StateProvider, useStateValue } from './Context'; 26 | 27 | import * as errorResponse from './Constants/errors/errorResponseStrings'; 28 | 29 | // import * as errorDispatchObj from './Constants/errors/errorDispatchObjects'; 30 | // using a proxy to get around CORS. We do not need a server. 31 | 32 | const proxy = Number(process.env.IS_DEV) === 1 ? 'https://cors-anywhere.herokuapp.com/' : ''; 33 | 34 | const App = () => { 35 | const [{ endpoint, apiKey, headersKey }] = useStateValue(); 36 | 37 | // instantiated errorLink 38 | // const httpLink = createHttpLink({ uri: proxy + endpoint }); 39 | 40 | const headersOptions = { 41 | 'Content-Type': 'application/json' 42 | // 'Access-Control-Allow-Origin': '*', 43 | }; 44 | 45 | if (apiKey !== '' && headersKey !== '') { 46 | // console.log('apiKey: ', apiKey); 47 | // console.log('headersKey: ', headersKey); 48 | headersOptions[headersKey] = apiKey; 49 | // console.log('headersOptions ', headersOptions); 50 | } 51 | 52 | const restLink = new RestLink({ 53 | // might be able to use custom fetch here for error checking? 54 | uri: proxy + endpoint, 55 | fetchOptions: { 56 | mode: 'no-cors' 57 | }, 58 | headers: headersOptions, 59 | // onError: ({ networkError, graphQLErrors }) => { 60 | // console.log('graphQLErrors', graphQLErrors); 61 | // console.log('networkError', networkError); 62 | // }, 63 | customFetch: (uri, fetchOptions) => 64 | // console.log('in custom fetch. fetchOptions: ', fetchOptions); 65 | new Promise((resolve, reject) => { 66 | fetch(uri, fetchOptions) 67 | .then(res => { 68 | // const clone = res.clone(); 69 | // console.log('in first then lock, custom fetch: ', res); 70 | if (res.status === 404) { 71 | // dispatch inside of here seems to break it 72 | // dispatch(errorDispatchObj.endpointPath404Error); 73 | reject(new Error(errorResponse.endpointPath404Error)); 74 | } 75 | // console.log('clone.json: ', clone.json()); 76 | else return resolve(res); 77 | }) 78 | // .then((data) => { 79 | // console.log('data in 2nd then block: ', data); 80 | // return resolve(data); 81 | // }) 82 | .catch(e => { 83 | // console.log('error in custom fetch'); 84 | reject('error in custom fetch: ', e); 85 | }); 86 | }) 87 | // credentials: 'include', 88 | }); 89 | 90 | // error link, which isn't actually being triggered at all 91 | const errorLink = onError(({ graphQLErrors, networkError, operation, response, forward }) => { 92 | // operation and response are other props in the onError obj 93 | // console.log('operation in errorLink: ', operation); 94 | // console.log('response in errorLink: ', response); 95 | if (graphQLErrors) { 96 | graphQLErrors.map(({ message, locations, path }) => 97 | console.log(`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`) 98 | ); 99 | } 100 | if (networkError) console.log('Network Error: ', networkError); 101 | 102 | // forward(operation); 103 | }); 104 | 105 | // const httpLink = createHttpLink(proxy + endpoint); 106 | 107 | const client = new ApolloClient({ 108 | // added errorLink here 109 | link: ApolloLink.from([errorLink, restLink]), 110 | cache: new InMemoryCache() 111 | // fetchPolicy: 'cache-first', 112 | 113 | // handling errors on default 114 | // defaultOptions: { 115 | // watchQuery: { 116 | // fetchPolicy: 'cache-and-network', 117 | // errorPolicy: 'all', 118 | // }, 119 | // query: { 120 | // fetchPolicy: 'cache-and-network', 121 | // errorPolicy: 'all', 122 | // }, 123 | // }, 124 | // onError: ({ networkError, graphQLErrors }) => { 125 | // console.log('graphQLErrors', graphQLErrors); 126 | // console.log('networkError', networkError); 127 | // }, 128 | }); 129 | 130 | // history display moved to render inside of TabsManager 131 | // QC instances render inside tabs manager 132 | return ( 133 |
134 | 135 |
136 | 137 | 138 |
139 | ); 140 | }; 141 | 142 | const statefulApp = () => ( 143 | 144 | 145 | 146 | ); 147 | 148 | export default statefulApp; 149 | -------------------------------------------------------------------------------- /client/Components/APIKeyModal.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Modal from 'react-modal'; 3 | import { apiKeyModalStyle as styleObj } from '../Constants/inlineComponentStyle'; 4 | 5 | const APIModal = props => { 6 | const { modalOptions, setModalOptions } = props; 7 | 8 | const [apiTextValue, setApiTextValue] = useState(''); 9 | const [headerValue, setHeaderValue] = useState(''); 10 | 11 | const openModal = () => { 12 | setModalOptions({ 13 | ...modalOptions, 14 | isModalOpen: true 15 | }); 16 | }; 17 | 18 | const closeModal = () => { 19 | // console.log('close modal happened'); 20 | setModalOptions({ 21 | ...modalOptions, 22 | newHeadersKey: headerValue.trim(), 23 | newAPIKey: apiTextValue.trim(), 24 | isModalOpen: false 25 | }); 26 | }; 27 | 28 | return ( 29 |
30 | 35 | closeModal()} 39 | style={styleObj} 40 | contentLabel="API Key" 41 | > 42 | 43 |
44 |
45 | 56 | 67 | 70 |
71 |
72 |
73 | ); 74 | }; 75 | 76 | // this is supposed to be down here, but it looks weird. commented out for now 77 | // Modal.setAppElement('#root'); 78 | 79 | export default APIModal; 80 | -------------------------------------------------------------------------------- /client/Components/EndpointField.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useStateValue } from '../Context'; 3 | import APIModal from './APIKeyModal'; 4 | 5 | import defaultEndpoint from '../Constants/defaultEndpoint'; 6 | 7 | const EndpointField = props => { 8 | // streamlined to not use local state from queryInput component 9 | const { setNewAPIEndpoint, stateTabReference, modalOptions, setModalOptions } = props; 10 | // 8/12: deleted endpoint from useStateValue below 11 | const [{ endpointHistory }] = useStateValue(); 12 | 13 | return ( 14 |
15 | { 19 | // have to assign value from text area instead of local state, since state setter 20 | // and dispatch are async 21 | 22 | const newUrl = e.target.value; 23 | setNewAPIEndpoint(newUrl); 24 | }} 25 | /> 26 | 27 |
28 | ); 29 | }; 30 | 31 | export default EndpointField; 32 | -------------------------------------------------------------------------------- /client/Components/Header.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const header = () => ( 4 | 7 | ); 8 | 9 | export default header; 10 | -------------------------------------------------------------------------------- /client/Components/HistoryDisplay.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import db from '../db'; 3 | import { useStateValue } from '../Context'; 4 | import HistoryListItem from './MiniComponents/HistoryListItem'; 5 | import * as types from '../Constants/actionTypes'; 6 | 7 | const HistoryDisplay = props => { 8 | const { currentTabID } = props; 9 | 10 | // edited query to have nested prop also called query. added query tab reference 11 | const [ 12 | { 13 | query: { query }, 14 | queryGQLError 15 | }, 16 | dispatch 17 | ] = useStateValue(); 18 | const [localQH, setLocalQH] = useState([]); 19 | 20 | useEffect(() => { 21 | db.history 22 | .toArray() 23 | .then(queries => { 24 | // console.log('retrieved from DB', queries); 25 | // displaying queries history in descending chronological order 26 | setLocalQH(queries.reverse()); 27 | }) 28 | .catch(e => console.log('Error fetching from DB: ', e)); 29 | }, [query, queryGQLError]); 30 | 31 | const clearHistory = () => { 32 | db.history 33 | .clear() 34 | .then(() => { 35 | // console.log('Database cleared.'); 36 | setLocalQH([]); 37 | }) 38 | .catch(e => { 39 | throw new Error('Error in clearing database: ', e); 40 | }); 41 | }; 42 | 43 | const onEdit = id => { 44 | event.preventDefault(); 45 | db.history 46 | .get(id) 47 | .then(foundQuery => { 48 | // console.log('Query in onEdit ', foundQuery); 49 | dispatch({ 50 | type: types.EDIT_QUERY_FROM_DB, 51 | historyTextValue: foundQuery.query, 52 | endpoint: foundQuery.endpoint, 53 | currentTabID 54 | }); 55 | return foundQuery; 56 | }) 57 | .then(foundQuery => { 58 | const inputField = document.querySelector( 59 | `#endpoint-field[input-field-tab-id ="${currentTabID}"] input` 60 | ); 61 | inputField.value = foundQuery.endpoint; 62 | }) 63 | .catch(e => console.log('Error searching DB.')); 64 | }; 65 | 66 | const onDelete = queryId => { 67 | event.preventDefault(); 68 | // console.log('running onDelete'); 69 | db.history 70 | .delete(queryId) 71 | // .then(() => console.log('deleted ', queryId)) 72 | .then(() => { 73 | setLocalQH(localQH.filter(queryItem => queryItem.id !== queryId)); 74 | }) 75 | .catch(e => console.log('Error deleting from DB :', e)); 76 | }; 77 | 78 | return ( 79 |
80 |
History
81 |
82 | 85 |
86 |
    87 | {localQH.map((pastQueries, idx) => ( 88 | 96 | ))} 97 |
98 |
99 | ); 100 | }; 101 | 102 | export default HistoryDisplay; 103 | -------------------------------------------------------------------------------- /client/Components/MiniComponents/ErrorDisplay.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const ErrorDisplay = props => { 4 | const { errorMessage, extraSpace } = props; 5 | return ( 6 | <> 7 |

{errorMessage}

8 | {extraSpace && ( 9 | <> 10 |

11 | 12 | )} 13 | 14 | ); 15 | }; 16 | 17 | export default ErrorDisplay; 18 | -------------------------------------------------------------------------------- /client/Components/MiniComponents/HistoryListItem.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/button-has-type */ 2 | import React, { useState } from 'react'; 3 | 4 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 5 | import { faTrash, faPen } from '@fortawesome/free-solid-svg-icons'; 6 | 7 | const HistoryListItem = props => { 8 | const [isHovering, toggleHover] = useState(false); 9 | const { id, queryText, endpoint, onDelete, onEdit } = props; 10 | 11 | const pathRegex = queryText.match(/(?<=path:\W*\")\S*(?=\")/gi); 12 | const path = pathRegex ? pathRegex[0] : 'invalid'; 13 | // console.log('rendering a list item'); 14 | return ( 15 | <> 16 |
  • toggleHover(true)} 21 | onMouseLeave={() => toggleHover(false)} 22 | > 23 |

    24 | Endpoint: {endpoint} 25 |

    26 |

    27 | Path: {path} 28 |

    29 | {isHovering && ( 30 | 31 | 42 | 53 | 54 | )} 55 |
    56 |
    57 |
  • 58 | 59 | ); 60 | }; 61 | 62 | export default HistoryListItem; 63 | -------------------------------------------------------------------------------- /client/Components/MiniComponents/NullResultCheck.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import nullResultChecker from '../../utils/queryOutputDisplay/nullResultChecker'; 3 | 4 | const NullResultCheck = props => { 5 | const { isHovering, toggleHover, result } = props; 6 | 7 | return ( 8 |
    9 | 25 |
    26 | ); 27 | }; 28 | 29 | export default NullResultCheck; 30 | -------------------------------------------------------------------------------- /client/Components/MiniComponents/TabsDeleteButton.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | tabsDeleteButtonStyle, 4 | tabsDeleteButtonMainStyle 5 | } from '../../Constants/inlineComponentStyle'; 6 | 7 | const DeleteButton = props => { 8 | const { tabId, deleteTab, isHidden } = props; 9 | 10 | return ( 11 | 19 | ); 20 | }; 21 | 22 | export default DeleteButton; 23 | -------------------------------------------------------------------------------- /client/Components/MiniComponents/URLResultCheck.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ErrorDisplay from './ErrorDisplay'; 3 | 4 | const URLResultCheck = props => { 5 | const { result } = props; 6 | return ( 7 |
    8 | 12 |
      13 | {Object.keys(result).reduce((acc, curVal, idx) => { 14 | if (typeof result[curVal] === 'string' && result[curVal].includes('http')) { 15 | acc.push(
    • {curVal}
    • ); 16 | } 17 | return acc; 18 | }, [])} 19 |
    20 |
    21 | ); 22 | }; 23 | 24 | export default URLResultCheck; 25 | -------------------------------------------------------------------------------- /client/Components/QueryInput.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Controlled as CodeMirror } from 'react-codemirror2'; 3 | import gql from 'graphql-tag'; 4 | import { useStateValue } from '../Context'; 5 | import EndpointField from './EndpointField'; 6 | // import db from '../db'; 7 | import * as types from '../Constants/actionTypes'; 8 | 9 | // // import Code Mirror styling all at once 10 | // NOTE: THIS IS NOW BEING IMPORTED IN INDEX.HTML FILE 11 | // import '../StyleSheets/external/CodeMirror.css'; 12 | 13 | import fetchErrorCheck from '../utils/queryInput/fetchErrorCheck'; 14 | import addQueryToDB from '../utils/queryInput/addQueryToDB'; 15 | // import handleQueryFetch from '../utils/queryInput/handleQueryFetch'; 16 | 17 | import defaultEndpoint from '../Constants/defaultEndpoint'; 18 | 19 | // from addons folder of codemirror 20 | require('codemirror/addon/display/autorefresh'); 21 | 22 | // SHOULD MAKE NOTE: API key should be supplied in endpoint field 23 | // using a proxy to get around CORS. We do not need a server. 24 | 25 | // wrote example query so it can be used as a placeholder in textarea 26 | const exampleQuery = `# Example query: 27 | query ditto { 28 | pokemon @rest(type: "Pokemon", path: "ditto/") { 29 | name 30 | abilities 31 | } 32 | }`; 33 | 34 | const QueryInput = props => { 35 | const { stateTabReference } = props; 36 | 37 | // deleted endpoint from useStateValue below 38 | const [ 39 | { historyTextValue, isModalOpen, endpointHistory, historyIdx }, 40 | dispatch 41 | ] = useStateValue(); 42 | const [textValue, setTextValue] = useState(exampleQuery); 43 | 44 | const [modalOptions, setModalOptions] = useState({ 45 | isModalOpen: false, 46 | newHeadersKey: '', 47 | newAPIKey: '' 48 | }); 49 | // if edit button has been clicked, then historyTextValue exists in state. reassigned to fill out 50 | // code mirror text area 51 | 52 | const [newAPIEndpoint, setNewAPIEndpoint] = useState(''); 53 | 54 | if ( 55 | historyTextValue !== '' && 56 | textValue !== historyTextValue && 57 | historyIdx === stateTabReference 58 | ) { 59 | // if a user has asked for an old query, repopulate 60 | 61 | setTextValue(historyTextValue); 62 | setNewAPIEndpoint(endpointHistory[stateTabReference]); 63 | 64 | dispatch({ 65 | type: types.RESET_GET_QUERY 66 | }); 67 | // once history is assigned down here, reset it in context 68 | // dispatch({ 69 | // type: types.RESET_GET_QUERY, 70 | // }); 71 | } 72 | 73 | const handleSubmit = () => { 74 | event.preventDefault(); 75 | // old way 76 | // const urlToSend = newAPIEndpoint || endpoint; 77 | // new way 78 | // console.log('new api endpoint: ', newAPIEndpoint); 79 | // console.log('history endpoint: ', endpointHistory[stateTabReference]); 80 | // console.log('new api endpoint: ', defaultEndpoint); 81 | const urlToSend = newAPIEndpoint || endpointHistory[stateTabReference] || defaultEndpoint; 82 | 83 | // console.log('url being sent: ', urlToSend); 84 | try { 85 | gql([`${textValue}`]); 86 | } catch (err) { 87 | // console.log('could not make tag: ', err); 88 | // NEED CATCH FOR NO PATH STRING AT ALL 89 | // 'Syntax Error: Unexpected )' 90 | 91 | // NEED 404 CHECK -- PULL FROM HANDLE QUERY FETCH? 92 | fetchErrorCheck(err, dispatch); 93 | return; 94 | } 95 | 96 | // console.log('regex test: ', textValue.match(/(?<=\{\W)(.*?)(?=\@)/g)); 97 | const regexResult = textValue.match(/(?<=\{\W)(.*?)(?=\@)/g); 98 | Promise.all([ 99 | addQueryToDB(textValue, urlToSend), 100 | dispatch({ 101 | type: types.RUN_QUERY, 102 | // decontructed using of gql tag to make query object. need to pass in a stringliteral. 103 | query: gql([`${textValue}`]), 104 | // pulls of key for where data will be in result obj 105 | queryResultObject: regexResult 106 | ? textValue.match(/(?<=\{\W)(.*?)(?=\@)/g)[0].trim() 107 | : 'null', 108 | newEndpoint: urlToSend, 109 | ranQueryTab: stateTabReference, 110 | newHeadersKey: modalOptions.newHeadersKey, 111 | newAPIKey: modalOptions.newAPIKey 112 | }), 113 | setModalOptions({ 114 | ...modalOptions, 115 | newHeadersKey: '', 116 | newAPIKey: '' 117 | }) 118 | ]) 119 | 120 | // .then(() => console.log('DB entry added and dispatch successful.')) 121 | .catch(e => console.log('Error in DB add/dispatch chain: ', e)); 122 | }; 123 | 124 | return ( 125 | <> 126 | 132 |
    133 |
    handleSubmit()} 137 | > 138 | { 143 | // console.log('on before change hit'); 144 | setTextValue(value); 145 | }} 146 | onChange={(editor, data, value) => { 147 | // console.log('on change hit'); 148 | setTextValue(value); 149 | }} 150 | options={{ 151 | lineNumbers: true, 152 | tabSize: 2, 153 | lineWrapping: true, 154 | autoRefresh: true, 155 | mode: 'javascript' 156 | }} 157 | /> 158 |
    159 | {/* NOTE: THIS IS PRESENTLY OK INSIDE THE FORM */} 160 | {/* reset state button */} 161 | { 167 | dispatch({ 168 | type: types.RESET_STATE, 169 | currentTab: stateTabReference 170 | }); 171 | // after reseting state, reset endpoint field to empty string. in state, 172 | // it will be POKEAPI 173 | 174 | // vanilla DOM manipulation was the best way to change the input field value 175 | // only resets current tab's endpoint field 176 | const inputField = document.querySelector( 177 | `#endpoint-field[input-field-tab-id ="${stateTabReference}"] input` 178 | ); 179 | inputField.value = ''; 180 | // reset textValue field to exampleQuery 181 | setTextValue(exampleQuery); 182 | // reset api endpoint in local state to blank string 183 | setNewAPIEndpoint(''); 184 | }} 185 | /> 186 | {/* submit query button */} 187 | 188 |
    189 | 190 |
    191 | 192 | ); 193 | }; 194 | 195 | export default QueryInput; 196 | -------------------------------------------------------------------------------- /client/Components/QueryOutputDisplay.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useStateValue } from '../Context'; 3 | import { jsonFormatter } from '../utils/queryOutputDisplay/jsonFormatter'; 4 | import nullChecker from '../utils/queryOutputDisplay/nullChecker'; 5 | // import nullResultChecker from '../utils/queryOutputDisplay/nullResultChecker'; 6 | 7 | import ErrorDisplay from './MiniComponents/ErrorDisplay'; 8 | import URLResultCheck from './MiniComponents/URLResultCheck'; 9 | import NullResultCheck from './MiniComponents/NullResultCheck'; 10 | 11 | const QueryOutputDisplay = props => { 12 | // ! TODO: MOVE ERROR CHECKING INTO A DIFFERENT FILE BECAUSE THIS IS A LOT 13 | const [{ queryResultObject, queryGQLError }] = useStateValue(); 14 | const [isHovering, toggleHover] = useState(false); 15 | // pull props off from GQL query running 16 | const { loading, error } = props; 17 | 18 | // if the current tab matches the tab from which the query was run, show contents. if not, make invisible 19 | // const styleObj = { visibility: ranQueryTab === stateTabReference ? 'visible' : 'hidden' }; 20 | 21 | // result is assigned either the successful query data or an error string 22 | const result = props[queryResultObject] ? props[queryResultObject] : queryGQLError; 23 | 24 | // checking if __typeName on the result object exists. If it doesn't, we send an error message 25 | if (loading === false) { 26 | // console.log(result); 27 | // if result comes back as an array - checks 0th index, will not work for nested result arrays 28 | if (Array.isArray(result)) { 29 | if (!result[0].__typename) { 30 | return ( 31 | 32 | ); 33 | } 34 | // if result comes back as a flat object 35 | } else if (!Object.keys(result).includes('__typename')) { 36 | return ( 37 | 38 | ); 39 | } 40 | } 41 | 42 | // checking to see if there are any null values on the results object 43 | // if so, means that the query field was improperly named or doesn't exist 44 | 45 | // ! NOTE: NEEDS TO ACCOUNT FOR ARRAYS, AS WELL AS NULL RESULT CHECKER 46 | // ! BUT DOES IT? 47 | const testNull = nullChecker(result); 48 | 49 | // checking if there are any values from our result that look like a url (surface level only) 50 | let urlAsPropCheck = false; 51 | // result will either be an object or string (error message) 52 | if (typeof result === 'object') { 53 | // ensures curVal is not null and that it is a string. a URL-like response 54 | // will only be a string 55 | // NOT NESTED 56 | urlAsPropCheck = Object.values(result).reduce((acc, curVal) => { 57 | if (curVal !== null && typeof curVal === 'string') return curVal.includes('http') || acc; 58 | return acc; 59 | }, false); 60 | } 61 | 62 | // if there are any values from our result that look like a url, make an array of LIs 63 | // now rendered inside return 64 | 65 | // loading and error cases do not have query-output IDs 66 | // loading and error come from GraphQL query result 67 | 68 | // ! TEST: for local state of result. if it's an empty string, query hasn't been run 69 | if (loading) { 70 | return ( 71 |
    72 |
    73 |
    74 | ); 75 | } 76 | 77 | // need to figure out how to deal with this one -- 7/30 at 11:00 am 78 | // if (error.message === 'Network error: forward is not a function') 79 | 80 | // any error from a graphql query that's not already accounted for is rendered here 81 | if (error) { 82 | if (error.message === 'Network error: forward is not a function') { 83 | return ( 84 | 89 | ); 90 | } 91 | return ; 92 | } 93 | 94 | return ( 95 | <> 96 | <> 97 | {testNull && ( 98 | 99 | )} 100 | 101 | <>{urlAsPropCheck && } 102 |
    103 |
    104 |           {jsonFormatter(result)}
    105 |         
    106 |
    107 |
    108 |
    109 | 110 | ); 111 | }; 112 | 113 | export default QueryOutputDisplay; 114 | -------------------------------------------------------------------------------- /client/Constants/actionTypes.js: -------------------------------------------------------------------------------- 1 | export const RUN_QUERY = 'RUN_QUERY'; 2 | // export const SUBMIT_ENDPOINT = 'SUBMIT_ENDPOINT'; 3 | export const RESET_STATE = 'RESET_STATE'; 4 | export const GQL_ERROR = 'GQL_ERROR'; 5 | export const EDIT_QUERY_FROM_DB = 'EDIT_QUERY_FROM_DB'; 6 | export const RESET_GET_QUERY = 'RESET_GET_QUERY'; 7 | -------------------------------------------------------------------------------- /client/Constants/defaultEndpoint.js: -------------------------------------------------------------------------------- 1 | const defaultEndpoint = 'https://pokeapi.co/api/v2/pokemon/'; 2 | export default defaultEndpoint; 3 | -------------------------------------------------------------------------------- /client/Constants/errors/errorDispatchObjects.js: -------------------------------------------------------------------------------- 1 | import { GQL_ERROR } from '../actionTypes'; 2 | import * as errorResponse from './errorResponseStrings'; 3 | 4 | class ErrorDispatch { 5 | constructor(errorMessage) { 6 | this.type = GQL_ERROR; 7 | this.gqlError = errorMessage; 8 | } 9 | } 10 | 11 | export const queryMethodError = new ErrorDispatch(errorResponse.queryMethodError); 12 | export const multipleQueriesError = new ErrorDispatch(errorResponse.multipleQueriesError); 13 | export const varBeforeRestError = new ErrorDispatch(errorResponse.varBeforeRestError); 14 | export const curlyBracketError = new ErrorDispatch(errorResponse.curlyBracketError); 15 | export const queryFieldBlankError = new ErrorDispatch(errorResponse.queryFieldBlankError); 16 | export const typeSyntaxError = new ErrorDispatch(errorResponse.typeSyntaxError); 17 | export const noRestCallError = new ErrorDispatch(errorResponse.noRestCallError); 18 | export const noPathOrTypeError = new ErrorDispatch(errorResponse.noPathOrTypeError); 19 | export const endpointPath404Error = new ErrorDispatch(errorResponse.endpointPath404Error); 20 | export const singleQuotesError = new ErrorDispatch(errorResponse.singleQuotesError); 21 | export const badFieldError = new ErrorDispatch(errorResponse.badFieldError); 22 | export const unterminatedStringError = new ErrorDispatch(errorResponse.unterminatedStringError); 23 | export const genericError = new ErrorDispatch(errorResponse.genericError); 24 | 25 | // BELOW: Old way to make error dispatches, before constructor 26 | 27 | // export const queryMethodError = { 28 | // type: GQL_ERROR, 29 | // gqlError: errorResponse.queryMethodError, 30 | // }; 31 | 32 | // export const multipleQueriesError = { 33 | // type: GQL_ERROR, 34 | // gqlError: errorResponse.multipleQueriesError, 35 | // }; 36 | 37 | // export const varBeforeRestError = { 38 | // type: GQL_ERROR, 39 | // gqlError: errorResponse.varBeforeRestError, 40 | // }; 41 | 42 | // export const curlyBracketError = { 43 | // type: GQL_ERROR, 44 | // gqlError: errorResponse.curlyBracketError, 45 | // }; 46 | 47 | // export const queryFieldBlankError = { 48 | // type: GQL_ERROR, 49 | // gqlError: errorResponse.queryFieldBlankError, 50 | // }; 51 | 52 | // export const typeSyntaxError = { 53 | // type: GQL_ERROR, 54 | // gqlError: errorResponse.typeSyntaxError, 55 | // }; 56 | 57 | // export const noRestCallError = { 58 | // type: GQL_ERROR, 59 | // gqlError: errorResponse.noRestCallError, 60 | // }; 61 | 62 | // export const noPathOrTypeError = { 63 | // type: GQL_ERROR, 64 | // gqlError: errorResponse.noPathOrTypeError, 65 | // }; 66 | 67 | // export const endpointPath404Error = { 68 | // type: GQL_ERROR, 69 | // gqlError: errorResponse.endpointPath404Error, 70 | // }; 71 | 72 | // export const singleQuotesError = { 73 | // type: GQL_ERROR, 74 | // gqlError: errorResponse.singleQuotesError, 75 | // }; 76 | 77 | // export const badFieldError = { 78 | // type: GQL_ERROR, 79 | // gqlError: errorResponse.badFieldError, 80 | // }; 81 | 82 | // export const unterminatedStringError = { 83 | // type: GQL_ERROR, 84 | // gqlError: errorResponse.unterminatedStringError, 85 | // }; 86 | -------------------------------------------------------------------------------- /client/Constants/errors/errorResponseStrings.js: -------------------------------------------------------------------------------- 1 | export const queryMethodError = 2 | 'Query method is invalid. Please double check your query on line 1.'; 3 | export const multipleQueriesError = 4 | 'Currently attempting to run multiple queries, but only one query, subscription, or mutation may be run at one time.'; 5 | export const varBeforeRestError = 6 | 'Variable before "@rest" cannot be blank. Please click reset and check line 3 of the example for reference.'; 7 | export const curlyBracketError = 'Query must be wrapped in curly brackets.'; 8 | export const queryFieldBlankError = 9 | 'Query fields cannot be blank. Please click "Reset" and check line 4 of the example for reference.'; 10 | export const typeSyntaxError = 'Inside @rest, "type" must be followed by a colon (e.g. type:).'; 11 | export const noRestCallError = 'Query must have an @rest call.'; 12 | export const noPathOrTypeError = 13 | "@rest must have a 'path' and 'type' property. Please click reset to check the example for reference."; 14 | export const endpointPath404Error = 'Endpoint is invalid. Please double check your endpoint.'; 15 | export const singleQuotesError = 'Please use double quotes (" ") instead of single quotes (\' \').'; 16 | export const badFieldError = 17 | 'One or more of your query fields might be written incorrectly. Please double check them.'; 18 | export const unterminatedStringError = 19 | 'An open string has not been closed with double quotes(" "). Please double check your query.'; 20 | export const genericError = 21 | 'There was an uncaught error in your GraphQL syntax. Please double check your query.'; 22 | -------------------------------------------------------------------------------- /client/Constants/errors/errorStrings.js: -------------------------------------------------------------------------------- 1 | export const queryMethodError = 'Syntax Error: Unexpected Name'; 2 | export const multipleQueriesError = 'react-apollo'; 3 | export const varBeforeRestError = 'Syntax Error: Expected Name, found @'; 4 | export const curlyBracketError1 = 'Syntax Error: Expected Name, found '; 5 | export const curlyBracketError2 = 'Syntax Error: Expected {'; 6 | export const curlyBracketError3 = 'Syntax Error: Unexpected }'; 7 | export const queryFieldBlankError = 'Syntax Error: Expected Name, found }'; 8 | export const typeSyntaxError = 'Syntax Error: Expected :'; 9 | export const noRestCallError = "Cannot read property '0' of null"; 10 | export const badArgumentOrFieldError = 'Syntax Error: Expected Name'; 11 | export const singleQuotesError = 12 | 'Syntax Error: Unexpected single quote character (\'), did you mean to use a double quote (")?'; 13 | export const unterminatedStringError = 'Syntax Error: Unterminated string.'; 14 | -------------------------------------------------------------------------------- /client/Constants/inlineComponentStyle.js: -------------------------------------------------------------------------------- 1 | export const tabsDeleteButtonStyle = { 2 | borderStyle: 'none', 3 | paddingLeft: '5px', 4 | fontSize: '10px', 5 | backgroundColor: '#f7f9fb', 6 | // isHidden must be handled at component level 7 | visibility: 'visible' 8 | }; 9 | 10 | export const tabsDeleteButtonMainStyle = { 11 | ...tabsDeleteButtonStyle, 12 | visibility: 'hidden' 13 | }; 14 | 15 | export const apiKeyModalStyle = { 16 | content: { 17 | top: '50%', 18 | left: '50%', 19 | right: 'auto', 20 | bottom: 'auto', 21 | marginRight: '-50%', 22 | transform: 'translate(-50%, -50%)', 23 | backgroundColor: '#31708b', 24 | // width: '100px', 25 | borderRadius: '5px' 26 | } 27 | }; 28 | 29 | export const tabStyle = { 30 | fontFamily: 'Helvetica, sans-serif', 31 | fontSize: '12px', 32 | backgroundColor: '#f7f9fb' 33 | }; 34 | 35 | export const tabStyleMain = { 36 | ...tabStyle 37 | // height: '13px', 38 | // marginBottom used to be needed. now it's not 39 | // marginBottom: '1px', 40 | }; 41 | 42 | export const addButtonStyle = { 43 | fontSize: '25px', 44 | borderStyle: 'none', 45 | paddingLeft: '5px', 46 | paddingBottom: '-6px', 47 | backgroundColor: '#f7f9fb', 48 | outline: 'none', 49 | alignSelf: 'center' 50 | }; 51 | -------------------------------------------------------------------------------- /client/Containers/QueriesContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { graphql } from 'react-apollo'; 3 | import { useStateValue } from '../Context'; 4 | // NOTE: moved endpoint field to inside query 5 | import QueryOutputDisplay from '../Components/QueryOutputDisplay'; 6 | import QueryInput from '../Components/QueryInput'; 7 | import { multipleQueriesError as multipleQueriesDispatchObj } from '../Constants/errors/errorDispatchObjects'; 8 | 9 | // MOVED modal to inside endpoint field 10 | // import APIModal from '../Components/APIKeyModal'; 11 | // Modal.setAppElement('#root') 12 | 13 | const QueriesContainer = props => { 14 | const { stateTabReference } = props; 15 | 16 | const [ 17 | { 18 | query: { query, ranQueryTab }, 19 | queryResultObject, 20 | queryGQLError 21 | }, 22 | dispatch 23 | ] = useStateValue(); 24 | 25 | // error thrown because it evals before anything is in query 26 | let OutputOfQuery; 27 | if (query !== '') { 28 | // console.log('inside of QC, after if block'); 29 | // if something is in query, assign QoQ to output of query 30 | // had to pass on props with the props object. it "parses" bigass object 31 | // before it's passed on. one thing needed for dynamism: the name of the prop 32 | // on the data object. e.g. query ditto { !!!POKEMON } 33 | 34 | // if query.definitions is an array with the number of queries. It should not be greater than 1 35 | if (query.definitions.length > 1) { 36 | // GraphQL can only run one query at a time, so even though this if statement block is to check for error, we need to send only one query to GQL so that the app doesn't break 37 | 38 | query.definitions = [query.definitions[0]]; 39 | 40 | // seems to work now with new tabs, with dispatch moved out of props 41 | dispatch(multipleQueriesDispatchObj); 42 | 43 | OutputOfQuery = graphql(query, { 44 | onError: e => { 45 | // not working 46 | console.log('Too many queries being run.', e); 47 | }, 48 | props: ({ data }) => { 49 | // dispatch moved to before query being run 50 | 51 | if (data.loading) { 52 | return { 53 | stateTabReference, 54 | loading: data.loading 55 | }; 56 | } 57 | return { 58 | stateTabReference, 59 | error: 60 | 'Currently attempting to run multiple queries, but only one query, subscription, or mutation may be run at one time' 61 | }; 62 | } 63 | })(QueryOutputDisplay); 64 | } else { 65 | OutputOfQuery = graphql(query, { 66 | // options: { 67 | // errorPolicy: 'true', 68 | // }, 69 | props: ({ data }) => { 70 | if (data.loading) { 71 | return { 72 | stateTabReference, 73 | loading: data.loading 74 | }; 75 | } 76 | if (data.error) { 77 | // console.log('error inside QC: ', data.error); 78 | return { 79 | stateTabReference, 80 | error: data.error 81 | }; 82 | } 83 | // if query successful, instantiate result obj. 84 | const resultObj = { 85 | stateTabReference, 86 | loading: false 87 | }; 88 | // separately assign queryResultVar to output obj 89 | resultObj[queryResultObject] = data[queryResultObject]; 90 | return resultObj; 91 | } 92 | // render QOD with props from GraphQL query 93 | })(QueryOutputDisplay); 94 | // console.log(query, 'this is query after QOD') 95 | } 96 | } 97 | 98 | // NOTE: moved endpoint field to inside query 99 | // NOTE: ERRORS ARE MOSTLY BEING RENDERED HERE, NOT INSIDE QUERY OUTPUT DISPLAY. 100 | // ERRORS RENDERED INSIDE OF QOD ARE UNCAUGHT GQL ERRORS 101 | 102 | // too out this conditional from first item under article 103 | 104 | return ( 105 |
    106 | 107 | 108 |
    109 | {query !== '' && stateTabReference === ranQueryTab && } 110 | {queryGQLError !== '' &&

    {queryGQLError}

    } 111 |
    112 |
    113 | ); 114 | }; 115 | 116 | export default QueriesContainer; 117 | -------------------------------------------------------------------------------- /client/Containers/TabsManager.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'; 3 | import QueriesContainer from './QueriesContainer'; 4 | import DeleteButton from '../Components/MiniComponents/TabsDeleteButton'; 5 | import HistoryDisplay from '../Components/HistoryDisplay'; 6 | import { useStateValue } from '../Context'; 7 | import defaultEndpoint from '../Constants/defaultEndpoint'; 8 | 9 | import { tabStyle, tabStyleMain, addButtonStyle } from '../Constants/inlineComponentStyle'; 10 | 11 | // import 'react-tabs/style/react-tabs.css'; 12 | 13 | const TabsManager = () => { 14 | // rendering tabs inside render method, based on tabsListLabels, just nums in an array 15 | const [{ endpointHistory }] = useStateValue(); 16 | 17 | const [queriesTabs, setQueriesTabs] = useState({ 18 | tabsListLabels: [0] 19 | }); 20 | const [currentTab, setCurrentTab] = useState({ tabIndex: 0 }); 21 | 22 | const deleteTab = tabId => { 23 | // delete tabs by checking tabId, which is passed as a prop upon creation of tab 24 | // let tabIdx; 25 | setQueriesTabs({ 26 | tabsListLabels: queriesTabs.tabsListLabels.filter((el, idx) => el !== tabId) 27 | }); 28 | }; 29 | 30 | const addNewTab = () => { 31 | // push new item (just a num) to tabsListLabels 32 | const newTabsListLabels = queriesTabs.tabsListLabels.slice(0); 33 | 34 | // adds +1 to whateve the final item is in the list 35 | // console.log('getting obj keys, sorted desc', Object.keys(endpointHistory).sort((a, b) => b - a)); 36 | const lastItemInHistory = Number(Object.keys(endpointHistory).sort((a, b) => b - a)[0]) + 1; 37 | const lastItemLocal = newTabsListLabels[newTabsListLabels.length - 1] + 1; 38 | const newLabel = lastItemLocal >= lastItemInHistory ? lastItemLocal : lastItemInHistory; 39 | // const newLabel = newTabsListLabels[newTabsListLabels.length - 1] + 1; 40 | newTabsListLabels.push(newLabel); 41 | // console.log('new tabs: ', newTabsListLabels); 42 | 43 | setQueriesTabs({ 44 | tabsListLabels: newTabsListLabels 45 | }); 46 | }; 47 | 48 | // NEW: history displayed rendered inside of tabs manager now. same location in DOM 49 | return ( 50 | <> 51 | 52 | <> 53 | { 57 | // console.log('last tab: ', lastIndex); 58 | // console.log('new tab: ', tabIndex); 59 | // console.log('unique ids: ', queriesTabs); 60 | 61 | // tabIdToSave is the unique value given by dev. tabIndex is managed by tabs itself 62 | // not being used currently but might be needed in future 63 | // const tabIdToSave = queriesTabs.tabsListLabels[lastIndex]; 64 | // console.log(tabIdToSave); 65 | setCurrentTab({ tabIndex }); 66 | }} 67 | > 68 | 69 | {queriesTabs.tabsListLabels.map((el, idx) => 70 | idx !== 0 ? ( 71 | 72 | {endpointHistory[el] ? endpointHistory[el] : defaultEndpoint} 73 | 79 | 80 | ) : ( 81 | 82 | {endpointHistory[el]} 83 | {}} 89 | isHidden 90 | /> 91 | 92 | ) 93 | )} 94 | {/* {} */} 95 | 98 | 99 | {/* {queriesTabs.queriesContainers} */} 100 | {queriesTabs.tabsListLabels.map((el, idx) => ( 101 | 102 | 103 | 104 | ))} 105 | 106 | 107 | 108 | ); 109 | }; 110 | 111 | export default TabsManager; 112 | -------------------------------------------------------------------------------- /client/Context.js: -------------------------------------------------------------------------------- 1 | /* 2 | Context to be used throughout the application 3 | Allows hooks to be utilized 4 | 5 | ************************** */ 6 | 7 | import React, { createContext, useContext, useReducer } from 'react'; 8 | import * as types from './Constants/actionTypes'; 9 | 10 | export const StateContext = createContext(); 11 | 12 | export const StateProvider = ({ children }) => ( 13 | 14 | {children} 15 | 16 | ); 17 | 18 | export const useStateValue = () => useContext(StateContext); 19 | 20 | // const initialEndpointHistory = { 21 | // endpoint: 'https://pokeapi.co/api/v2/pokemon/', 22 | // headers: { 23 | // headersKey: '', 24 | // apiKey: '', 25 | // }, 26 | // }; 27 | 28 | export const initialState = { 29 | query: { 30 | // MADE QUERY AN OBJ WITH QUERY PROP. ADDED RAN QUERYTAB ON IT TO KNOW WHERE QUERY CAME FROM 31 | query: '', 32 | ranQueryTab: -1 33 | }, 34 | queryResultObject: '', 35 | queryGQLError: '', 36 | // we should probably only need one of these, b/w url and endpoint 37 | endpoint: 'https://pokeapi.co/api/v2/pokemon/', 38 | // need to instantiate url or else query without a user input will not run 39 | // queries stored in db 40 | historyTextValue: '', 41 | historyIdx: 0, 42 | headersKey: '', 43 | apiKey: '', 44 | endpointFromDB: '', 45 | endpointHistory: { 46 | 0: 'https://pokeapi.co/api/v2/pokemon/' 47 | } 48 | }; 49 | 50 | export const reducer = (state, action) => { 51 | switch (action.type) { 52 | // ! NOT BEING USED ANYMORE 53 | // case types.SUBMIT_ENDPOINT: 54 | // return { 55 | // ...state, 56 | // // if user changes endpoint, want to make sure query is valid 57 | // endpoint: action.submitEndpoint, 58 | // query: { 59 | // query: '', 60 | // ranQueryTab: -1 61 | // }, 62 | // queryResultObject: '', 63 | // historyTextValue: '' 64 | // }; 65 | case types.RUN_QUERY: 66 | // when query is run, on button press, endpoint is assigned the dynamically changing url 67 | // console.log('action object, in run query: ', action); 68 | return { 69 | ...state, 70 | // if a query is run, that means no 404 happened 71 | queryGQLError: '', 72 | endpointFromDB: '', 73 | queryResultObject: action.queryResultObject, 74 | query: { 75 | query: { ...action.query }, 76 | ranQueryTab: action.ranQueryTab 77 | }, 78 | // sets endpoint history, for other tabs being able to run their old queries 79 | headersKey: 80 | action.newEndpoint === state.endpoint && action.newHeadersKey === '' 81 | ? state.headersKey 82 | : action.newHeadersKey, 83 | apiKey: 84 | action.newEndpoint === state.endpoint && action.newAPIKey === '' 85 | ? state.apiKey 86 | : action.newAPIKey, 87 | endpointHistory: { 88 | ...state.endpointHistory, 89 | [action.ranQueryTab]: action.newEndpoint ? action.newEndpoint : state.endpoint 90 | }, 91 | endpoint: action.newEndpoint ? action.newEndpoint : state.endpoint, 92 | historyTextValue: '' 93 | }; 94 | // needs to send whatever was in intial state at the very beginning of the app 95 | case types.RESET_STATE: 96 | return { 97 | ...initialState, 98 | // this might not be needed below 99 | endpointHistory: { 100 | ...state.endpointHistory, 101 | [action.currentTab]: 'https://pokeapi.co/api/v2/pokemon/' 102 | } 103 | }; 104 | case types.GQL_ERROR: 105 | // console.log('gql error fired: ', action); 106 | return { 107 | ...state, 108 | // on a 404, reset query. no query is actually run 109 | query: { 110 | query: '', 111 | ranQueryTab: -1 112 | }, 113 | queryResultObject: '', 114 | queryGQLError: action.gqlError, 115 | historyTextValue: '' 116 | }; 117 | case types.EDIT_QUERY_FROM_DB: 118 | return { 119 | ...initialState, 120 | historyTextValue: action.historyTextValue, 121 | historyIdx: action.currentTabID, 122 | endpoint: action.endpoint, 123 | endpointHistory: { 124 | ...state.endpointHistory, 125 | [action.currentTabID]: action.endpoint 126 | } 127 | }; 128 | case types.RESET_GET_QUERY: 129 | // console.log('running reset get query'); 130 | return { 131 | ...state, 132 | historyTextValue: '', 133 | // reset to a number that will never exist 134 | historyIdx: -1 135 | }; 136 | default: 137 | return state; 138 | } 139 | }; 140 | -------------------------------------------------------------------------------- /client/StyleSheets/App.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | @import '_queryInput.scss'; 3 | @import '_queriesContainer.scss'; 4 | @import '_queryInput.scss'; 5 | @import '_queryOutputDisplay.scss'; 6 | @import '_endpointField.scss'; 7 | @import '_header.scss'; 8 | @import '_buttons.scss'; 9 | @import '_spinner.scss'; 10 | @import '_historyDisplay.scss'; 11 | @import '_APIKeyModal.scss'; 12 | 13 | * { 14 | margin: 0; 15 | padding: 0; 16 | } 17 | 18 | body { 19 | font: $font-stack; 20 | } 21 | 22 | #app { 23 | min-width: 100vw; 24 | min-height: 100vh; 25 | display: grid; 26 | grid-template-rows: 45px 35px 1fr; 27 | grid-template-columns: 15% 1fr; 28 | grid-template-areas: 29 | 'header header' 30 | 'historyDisplay reactTabs' 31 | 'historyDisplay reactTabs'; 32 | font: $font-stack; 33 | background-color: $background-color; 34 | // NOT SURE WHAT ELSE THE BELOW MIGHT CAUSE, BUT THIS FIXES WHITESPACE ON BOTTOM 35 | overflow: hidden; 36 | } 37 | 38 | %input-fields { 39 | border-left: 1px solid $highlight-color; 40 | } 41 | 42 | // @-webkit-keyframes colorchange 43 | // { 44 | // 0% {background: #0D5C63;} 45 | // // 25% {background: #247B7B;} 46 | // 50% {background: #78CDD7;} 47 | // // 75% {background: #247B7B;} 48 | // 100% {background: #0D5C63;} 49 | // } 50 | 51 | // @-webkit-keyframes colorchange 52 | // { 53 | // 0% {background: #E5446D;} 54 | // 25% {background: #FF6B6B;} 55 | // 50% {background: #FF8966;} 56 | // 75% {background: #FF6B6B;} 57 | // 100% {background: #E5446D;} 58 | // } 59 | 60 | %headers { 61 | background-color: $dark-color; 62 | // -webkit-animation: colorchange 20s; 63 | // animation: colorchange 20s; 64 | // animation-iteration-count: infinite; 65 | } 66 | -------------------------------------------------------------------------------- /client/StyleSheets/TabPanel.scss: -------------------------------------------------------------------------------- 1 | .react-tab__tabs-panel { 2 | display: grid; 3 | grid-template-columns: 1fr; 4 | grid-template-rows: 1fr; 5 | grid-template-areas: 'queryContainer'; 6 | } 7 | 8 | .react-tabs { 9 | grid-area: reactTabs; 10 | } 11 | 12 | .react-tabs__tab--selected { 13 | background-color: $loading-color; 14 | } 15 | 16 | #tabs-list { 17 | display: grid; 18 | } 19 | 20 | #add-tab-button:focus { 21 | outline: 0; 22 | .react-tabs__tab { 23 | font-family: Helvetica, sans-serif; 24 | bottom: 1px; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /client/StyleSheets/_APIKeyModal.scss: -------------------------------------------------------------------------------- 1 | #API-key-modal { 2 | display: grid; 3 | grid-area: modal; 4 | width: 100%; 5 | justify-content: stretch; 6 | 7 | button { 8 | background-color: $loading-color; 9 | font-family: system-ui; 10 | font-size: 13px; 11 | font-weight: bold; 12 | color: white; 13 | box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); 14 | 15 | &:focus { 16 | outline: 0; 17 | } 18 | } 19 | 20 | button:hover { 21 | background-color: $loading-color-hover; 22 | } 23 | } 24 | 25 | #modal-instructions { 26 | font-family: system-ui; 27 | color: white; 28 | } 29 | 30 | #api-key-input { 31 | font-family: system-ui; 32 | } 33 | 34 | #headers-key-input { 35 | font-family: system-ui; 36 | } 37 | 38 | #api-key-form { 39 | background-color: $dark-color; 40 | 41 | label { 42 | font-family: system-ui; 43 | color: white; 44 | padding-bottom: 5px; 45 | } 46 | 47 | input { 48 | height: 30px; 49 | margin: 0px 5px; 50 | margin-right: 20px; 51 | } 52 | 53 | button { 54 | color: $dark-color; 55 | background-color: white; 56 | height: 35px; 57 | width: 50px; 58 | margin-top: 10px; 59 | font-size: 13px; 60 | // color: $highlight-color; 61 | font-weight: bold; 62 | font-family: system-ui; 63 | } 64 | 65 | button:hover { 66 | background-color: #e5e5e5; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /client/StyleSheets/_buttons.scss: -------------------------------------------------------------------------------- 1 | %button { 2 | height: 40px; 3 | justify-self: stretch; 4 | border: $dark-color; 5 | text-align: center; 6 | outline: none; 7 | font-size: 13px; 8 | color: $highlight-color; 9 | font-weight: bold; 10 | font-family: Helvetica, sans-serif; 11 | // background-color: $loading-color; 12 | } 13 | 14 | %button-hover { 15 | color: $highlight-color; 16 | background-color: $secondary-color; 17 | } 18 | 19 | %button-active { 20 | color: $highlight-color; 21 | background-color: $dark-color; 22 | } 23 | 24 | #buttons { 25 | justify-self: stretch; 26 | display: grid; 27 | grid-template-columns: 1fr 1fr; 28 | box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); 29 | } 30 | 31 | #submit-button { 32 | @extend %button; 33 | background-color: $loading-color; 34 | } 35 | 36 | #reset-button { 37 | @extend %button; 38 | background-color: $dark-color; 39 | } 40 | 41 | #submit-button:hover { 42 | // @extend %button-hover; 43 | background-color: $loading-color-hover; 44 | } 45 | 46 | #reset-button:hover { 47 | @extend %button-hover; 48 | } 49 | 50 | #submit-button:active { 51 | // @extend %button-active; 52 | background-color: $loading-color; 53 | } 54 | 55 | #reset-button:active { 56 | @extend %button-active; 57 | } 58 | 59 | #open-modal { 60 | @extend %button; 61 | background-color: $loading-color; 62 | height: 50px; 63 | } 64 | 65 | #open-modal:hover { 66 | background-color: $loading-color-hover; 67 | } 68 | 69 | #open-modal:active { 70 | @extend %button-active; 71 | } 72 | 73 | #close-modal { 74 | @extend %button; 75 | background-color: $dark-color; 76 | height: 17px; 77 | width: 80px; 78 | margin-left: 10px; 79 | } 80 | 81 | #close-modal:hover { 82 | @extend %button-hover; 83 | } 84 | 85 | #close-modal:active { 86 | @extend %button-active; 87 | } 88 | -------------------------------------------------------------------------------- /client/StyleSheets/_endpointField.scss: -------------------------------------------------------------------------------- 1 | #endpoint-field { 2 | grid-area: endpointField; 3 | width: 100%; 4 | justify-self: center; 5 | display: grid; 6 | grid-template-columns: 1fr 100px; 7 | grid-template-areas: 'endpointInput modal'; 8 | 9 | input { 10 | grid-area: endpointInput; 11 | box-sizing: border-box; 12 | width: 100%; 13 | height: 50px; 14 | resize: none; 15 | @extend %input-fields; 16 | font-size: 1.1rem; 17 | background-color: white; 18 | text-align: left; 19 | padding-left: 10px; 20 | outline: none; 21 | box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); 22 | border-top: solid $dark-color; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /client/StyleSheets/_header.scss: -------------------------------------------------------------------------------- 1 | #header { 2 | grid-area: header; 3 | display: grid; 4 | justify-self: stretch; 5 | text-align: right; 6 | align-items: center; 7 | padding-right: 20px; 8 | color: white; 9 | // @extend %headers; 10 | background-color: $dark-color; 11 | font-family: 'Special Elite', Helvetica, cursive; 12 | 13 | h1 { 14 | place-self: center stretch; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /client/StyleSheets/_historyDisplay.scss: -------------------------------------------------------------------------------- 1 | #history-display { 2 | grid-area: historyDisplay; 3 | overflow: scroll; 4 | // will adds 5 | overflow-x: hidden; 6 | // W: box-sizing doesn't seem needed 7 | // box-sizing: border-box; 8 | // end of will adds 9 | display: grid; 10 | grid-template-columns: minmax(0, 1fr); 11 | grid-template-rows: 37px 20px 1fr; 12 | grid-template-areas: 13 | 'historyHeader' 14 | 'clearHistory' 15 | 'historyList'; 16 | height: 88vh; 17 | // width: 90%; 18 | font-family: system-ui; 19 | background-color: #f7f7f7; 20 | resize: none; 21 | // margin-left: 10px; 22 | // padding: 20px 10px; 23 | box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); 24 | 25 | ul { 26 | list-style-type: none; 27 | font-size: 13px; 28 | // margin-bottom: 43px; 29 | 30 | li { 31 | border-bottom: 1px solid $dark-color; 32 | padding-top: 5px; 33 | 34 | // Added in to word wrap endpoints 35 | word-break: break-word; 36 | 37 | span { 38 | font-weight: 700; 39 | } 40 | } 41 | } 42 | 43 | .history-delete { 44 | svg { 45 | color: $dark-color; 46 | } 47 | } 48 | 49 | .history-edit { 50 | svg { 51 | color: $loading-color; 52 | } 53 | } 54 | 55 | #history-header { 56 | display: grid; 57 | grid-area: historyHeader; 58 | background-color: $loading-color; 59 | color: $highlight-color; 60 | // height: 5vh; 61 | padding-left: 10px; 62 | // box-shadow: 0 2px 4px 0 rgba(0,0,0,0.2); 63 | align-items: center; 64 | font-weight: bold; 65 | width: 100%; 66 | } 67 | 68 | #history-list { 69 | // border-top-color: black; 70 | grid-area: historyList; 71 | padding: 20px 10px; 72 | // max-height: 90%; 73 | // margin-bottom: 50px; 74 | } 75 | 76 | #clear-history { 77 | // display: grid; 78 | // grid-area: clearHistory; 79 | // border-style: none; 80 | // background-color: $loading-color; 81 | // color: $highlight-color; 82 | align-items: left; 83 | // padding-left: 10px; 84 | height: 33px; 85 | border-bottom: 1px solid $loading-color; 86 | 87 | // border-bottom-color: black; 88 | } 89 | 90 | #clear-history-button { 91 | // border-style: none; 92 | height: 33px; 93 | width: 100px; 94 | background-color: $background-color; 95 | font-size: 13px; 96 | border-right: 1px solid $loading-color; 97 | border-top: none; 98 | border-bottom: none; 99 | } 100 | 101 | #clear-history-button:hover { 102 | background-color: #d3d3d3; 103 | } 104 | 105 | button:focus { 106 | outline: 0; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /client/StyleSheets/_queriesContainer.scss: -------------------------------------------------------------------------------- 1 | #queries-container { 2 | grid-area: queryContainer; 3 | width: 100%; 4 | padding: 20px; 5 | box-sizing: border-box; 6 | // margin-bottom: 50px; 7 | height: 90%; 8 | justify-self: center; 9 | display: grid; 10 | // gives fixed size for first column. intermediary column will shrink 11 | // as needed. output will be as large as needed 12 | grid-template-columns: 1fr 30px 1fr; 13 | grid-template-rows: auto 15px auto auto auto; 14 | grid-template-areas: 15 | 'endpointField endpointField endpointField' 16 | '. . .' 17 | 'queryInput . queryOutput' 18 | 'queryInput . queryOutput' 19 | 'queryInput . queryOutput'; 20 | background-color: $background-color; 21 | } 22 | -------------------------------------------------------------------------------- /client/StyleSheets/_queryInput.scss: -------------------------------------------------------------------------------- 1 | #query-input { 2 | grid-area: queryInput; 3 | display: grid; 4 | grid-template-columns: 1fr; 5 | grid-template-rows: 1fr 40px; 6 | grid-template-areas: 7 | 'codeMirror' 8 | 'buttons'; 9 | height: 70vh; 10 | background-color: $background-color; 11 | resize: none; 12 | box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); 13 | 14 | #code-mirror { 15 | grid-area: codeMirror; 16 | } 17 | 18 | #buttons { 19 | grid-area: buttons; 20 | } 21 | 22 | .react-codemirror2 { 23 | max-height: 64.5vh; 24 | border-radius: 3px; 25 | overflow-y: scroll; 26 | overflow-x: scroll; 27 | 28 | .CodeMirror-scroll { 29 | // div inside of react-codemirror2 that dynamically changes based on code lines 30 | height: 100%; 31 | min-height: 72vh; 32 | // move this max width somewhere else 33 | max-width: 38vw; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /client/StyleSheets/_queryOutputDisplay.scss: -------------------------------------------------------------------------------- 1 | #query-output { 2 | // id was already assigned to container. target h4 inside it 3 | grid-area: queryOutput; 4 | display: grid; 5 | box-sizing: border-box; 6 | color: $dark-color; 7 | padding: 20px 20px; 8 | height: 70vh; 9 | box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); 10 | border-radius: 3px; 11 | background-color: white; 12 | outline: none; 13 | font-family: system-ui; 14 | overflow-y: auto; 15 | overflow-x: auto; 16 | 17 | #tooltip { 18 | position: absolute; 19 | background-color: $dark-color; 20 | color: white; 21 | } 22 | 23 | aside span { 24 | font-weight: 700; 25 | } 26 | 27 | ul { 28 | list-style: none; 29 | } 30 | 31 | .lds-circle { 32 | place-self: center; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /client/StyleSheets/_spinner.scss: -------------------------------------------------------------------------------- 1 | .lds-circle { 2 | display: inline-block; 3 | transform: translateZ(1px); 4 | } 5 | .lds-circle > div { 6 | display: inline-block; 7 | width: 51px; 8 | height: 51px; 9 | margin: 6px; 10 | border-radius: 50%; 11 | background: $loading-color; 12 | animation: lds-circle 2.4s cubic-bezier(0, 0.2, 0.8, 1) infinite; 13 | } 14 | @keyframes lds-circle { 15 | 0%, 16 | 100% { 17 | animation-timing-function: cubic-bezier(0.5, 0, 1, 0.5); 18 | } 19 | 0% { 20 | transform: rotateY(0deg); 21 | } 22 | 50% { 23 | transform: rotateY(1800deg); 24 | animation-timing-function: cubic-bezier(0, 0.5, 0.5, 1); 25 | } 26 | 100% { 27 | transform: rotateY(3600deg); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /client/StyleSheets/external/CodeMirror.css: -------------------------------------------------------------------------------- 1 | /* @import '../../../node_modules/codemirror/lib/codemirror.css'; */ 2 | @import '../../../node_modules/codemirror/theme/material.css'; 3 | 4 | /* BASICS */ 5 | /*This is the Codemirro CSS copied over and changed */ 6 | .CodeMirror { 7 | /* Set height, width, borders, and global font properties here */ 8 | font-family: monospace; 9 | height: 100%; 10 | color: black; 11 | direction: ltr; 12 | border-radius: 3px; 13 | } 14 | 15 | /* PADDING */ 16 | 17 | .CodeMirror-lines { 18 | padding: 4px 0; /* Vertical padding around content */ 19 | } 20 | .CodeMirror pre { 21 | padding: 0 4px; /* Horizontal padding of content */ 22 | } 23 | 24 | .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { 25 | background-color: white; /* The little square between H and V scrollbars */ 26 | } 27 | 28 | /* GUTTER */ 29 | 30 | .CodeMirror-gutters { 31 | border-right: 1px solid #ddd; 32 | background-color: #f7f7f7; 33 | white-space: nowrap; 34 | } 35 | .CodeMirror-linenumbers {} 36 | .CodeMirror-linenumber { 37 | padding: 0 3px 0 5px; 38 | min-width: 20px; 39 | text-align: right; 40 | color: #999; 41 | white-space: nowrap; 42 | } 43 | 44 | .CodeMirror-guttermarker { color: black; } 45 | .CodeMirror-guttermarker-subtle { color: #999; } 46 | 47 | /* CURSOR */ 48 | 49 | .CodeMirror-cursor { 50 | border-left: 1px solid black; 51 | border-right: none; 52 | width: 0; 53 | } 54 | /* Shown when moving in bi-directional text */ 55 | .CodeMirror div.CodeMirror-secondarycursor { 56 | border-left: 1px solid silver; 57 | } 58 | .cm-fat-cursor .CodeMirror-cursor { 59 | width: auto; 60 | border: 0 !important; 61 | background: #7e7; 62 | } 63 | .cm-fat-cursor div.CodeMirror-cursors { 64 | z-index: 1; 65 | } 66 | .cm-fat-cursor-mark { 67 | background-color: rgba(20, 255, 20, 0.5); 68 | -webkit-animation: blink 1.06s steps(1) infinite; 69 | -moz-animation: blink 1.06s steps(1) infinite; 70 | animation: blink 1.06s steps(1) infinite; 71 | } 72 | .cm-animate-fat-cursor { 73 | width: auto; 74 | border: 0; 75 | -webkit-animation: blink 1.06s steps(1) infinite; 76 | -moz-animation: blink 1.06s steps(1) infinite; 77 | animation: blink 1.06s steps(1) infinite; 78 | background-color: #7e7; 79 | } 80 | @-moz-keyframes blink { 81 | 0% {} 82 | 50% { background-color: transparent; } 83 | 100% {} 84 | } 85 | @-webkit-keyframes blink { 86 | 0% {} 87 | 50% { background-color: transparent; } 88 | 100% {} 89 | } 90 | @keyframes blink { 91 | 0% {} 92 | 50% { background-color: transparent; } 93 | 100% {} 94 | } 95 | 96 | /* Can style cursor different in overwrite (non-insert) mode */ 97 | .CodeMirror-overwrite .CodeMirror-cursor {} 98 | 99 | .cm-tab { display: inline-block; text-decoration: inherit; } 100 | 101 | .CodeMirror-rulers { 102 | position: absolute; 103 | left: 0; right: 0; top: -50px; bottom: -20px; 104 | overflow: hidden; 105 | } 106 | .CodeMirror-ruler { 107 | border-left: 1px solid #ccc; 108 | top: 0; bottom: 0; 109 | position: absolute; 110 | } 111 | 112 | /* DEFAULT THEME */ 113 | 114 | .cm-s-default .cm-header {color: blue;} 115 | .cm-s-default .cm-quote {color: #090;} 116 | .cm-negative {color: #d44;} 117 | .cm-positive {color: #292;} 118 | .cm-header, .cm-strong {font-weight: bold;} 119 | .cm-em {font-style: italic;} 120 | .cm-link {text-decoration: underline;} 121 | .cm-strikethrough {text-decoration: line-through;} 122 | 123 | .cm-s-default .cm-keyword {color: #708;} 124 | .cm-s-default .cm-atom {color: #219;} 125 | .cm-s-default .cm-number {color: #164;} 126 | .cm-s-default .cm-def {color: #00f;} 127 | .cm-s-default .cm-variable, 128 | .cm-s-default .cm-punctuation, 129 | .cm-s-default .cm-property, 130 | .cm-s-default .cm-operator {} 131 | .cm-s-default .cm-variable-2 {color: #05a;} 132 | .cm-s-default .cm-variable-3, .cm-s-default .cm-type {color: #085;} 133 | .cm-s-default .cm-comment {color: #a50;} 134 | .cm-s-default .cm-string {color: #a11;} 135 | .cm-s-default .cm-string-2 {color: #f50;} 136 | .cm-s-default .cm-meta {color: #555;} 137 | .cm-s-default .cm-qualifier {color: #555;} 138 | .cm-s-default .cm-builtin {color: #30a;} 139 | .cm-s-default .cm-bracket {color: #997;} 140 | .cm-s-default .cm-tag {color: #170;} 141 | .cm-s-default .cm-attribute {color: #00c;} 142 | .cm-s-default .cm-hr {color: #999;} 143 | .cm-s-default .cm-link {color: #00c;} 144 | 145 | .cm-s-default .cm-error {color: #f00;} 146 | .cm-invalidchar {color: #f00;} 147 | 148 | .CodeMirror-composing { border-bottom: 2px solid; } 149 | 150 | /* Default styles for common addons */ 151 | 152 | div.CodeMirror span.CodeMirror-matchingbracket {color: #0b0;} 153 | div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #a22;} 154 | .CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); } 155 | .CodeMirror-activeline-background {background: #e8f2ff;} 156 | 157 | /* STOP */ 158 | 159 | /* The rest of this file contains styles related to the mechanics of 160 | the editor. You probably shouldn't touch them. */ 161 | 162 | .CodeMirror { 163 | position: relative; 164 | overflow: hidden; 165 | background: white; 166 | } 167 | 168 | .CodeMirror-scroll { 169 | overflow: scroll !important; /* Things will break if this is overridden */ 170 | /* 30px is the magic margin used to hide the element's real scrollbars */ 171 | /* See overflow: hidden in .CodeMirror */ 172 | margin-bottom: -30px; margin-right: -30px; 173 | padding-bottom: 30px; 174 | height: 100%; 175 | outline: none; /* Prevent dragging from highlighting the element */ 176 | position: relative; 177 | } 178 | .CodeMirror-sizer { 179 | position: relative; 180 | border-right: 30px solid transparent; 181 | } 182 | 183 | /* The fake, visible scrollbars. Used to force redraw during scrolling 184 | before actual scrolling happens, thus preventing shaking and 185 | flickering artifacts. */ 186 | .CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { 187 | position: absolute; 188 | z-index: 6; 189 | display: none; 190 | } 191 | .CodeMirror-vscrollbar { 192 | right: 0; top: 0; 193 | overflow-x: hidden; 194 | overflow-y: scroll; 195 | } 196 | .CodeMirror-hscrollbar { 197 | bottom: 0; left: 0; 198 | overflow-y: hidden; 199 | overflow-x: scroll; 200 | } 201 | .CodeMirror-scrollbar-filler { 202 | right: 0; bottom: 0; 203 | } 204 | .CodeMirror-gutter-filler { 205 | left: 0; bottom: 0; 206 | } 207 | 208 | .CodeMirror-gutters { 209 | position: absolute; left: 0; top: 0; 210 | min-height: 100%; 211 | z-index: 3; 212 | } 213 | .CodeMirror-gutter { 214 | white-space: normal; 215 | height: 100%; 216 | display: inline-block; 217 | vertical-align: top; 218 | margin-bottom: -30px; 219 | } 220 | .CodeMirror-gutter-wrapper { 221 | position: absolute; 222 | z-index: 4; 223 | background: none !important; 224 | border: none !important; 225 | } 226 | .CodeMirror-gutter-background { 227 | position: absolute; 228 | top: 0; bottom: 0; 229 | z-index: 4; 230 | } 231 | .CodeMirror-gutter-elt { 232 | position: absolute; 233 | cursor: default; 234 | z-index: 4; 235 | } 236 | .CodeMirror-gutter-wrapper ::selection { background-color: transparent } 237 | .CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent } 238 | 239 | .CodeMirror-lines { 240 | cursor: text; 241 | min-height: 1px; /* prevents collapsing before first draw */ 242 | } 243 | .CodeMirror pre { 244 | /* Reset some styles that the rest of the page might have set */ 245 | -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; 246 | border-width: 0; 247 | background: transparent; 248 | font-family: inherit; 249 | font-size: inherit; 250 | margin: 0; 251 | white-space: pre; 252 | word-wrap: normal; 253 | line-height: inherit; 254 | color: inherit; 255 | z-index: 2; 256 | position: relative; 257 | overflow: visible; 258 | -webkit-tap-highlight-color: transparent; 259 | -webkit-font-variant-ligatures: contextual; 260 | font-variant-ligatures: contextual; 261 | } 262 | .CodeMirror-wrap pre { 263 | word-wrap: break-word; 264 | white-space: pre-wrap; 265 | word-break: normal; 266 | } 267 | 268 | .CodeMirror-linebackground { 269 | position: absolute; 270 | left: 0; right: 0; top: 0; bottom: 0; 271 | z-index: 0; 272 | } 273 | 274 | .CodeMirror-linewidget { 275 | position: relative; 276 | z-index: 2; 277 | padding: 0.1px; /* Force widget margins to stay inside of the container */ 278 | } 279 | 280 | .CodeMirror-widget {} 281 | 282 | .CodeMirror-rtl pre { direction: rtl; } 283 | 284 | .CodeMirror-code { 285 | outline: none; 286 | } 287 | 288 | /* Force content-box sizing for the elements where we expect it */ 289 | .CodeMirror-scroll, 290 | .CodeMirror-sizer, 291 | .CodeMirror-gutter, 292 | .CodeMirror-gutters, 293 | .CodeMirror-linenumber { 294 | -moz-box-sizing: content-box; 295 | box-sizing: content-box; 296 | } 297 | 298 | .CodeMirror-measure { 299 | position: absolute; 300 | width: 100%; 301 | height: 0; 302 | overflow: hidden; 303 | visibility: hidden; 304 | } 305 | 306 | .CodeMirror-cursor { 307 | position: absolute; 308 | pointer-events: none; 309 | } 310 | .CodeMirror-measure pre { position: static; } 311 | 312 | div.CodeMirror-cursors { 313 | visibility: hidden; 314 | position: relative; 315 | z-index: 3; 316 | } 317 | div.CodeMirror-dragcursors { 318 | visibility: visible; 319 | } 320 | 321 | .CodeMirror-focused div.CodeMirror-cursors { 322 | visibility: visible; 323 | } 324 | 325 | .CodeMirror-selected { background: #d9d9d9; } 326 | .CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; } 327 | .CodeMirror-crosshair { cursor: crosshair; } 328 | .CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; } 329 | .CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; } 330 | 331 | .cm-searching { 332 | background-color: #ffa; 333 | background-color: rgba(255, 255, 0, .4); 334 | } 335 | 336 | /* Used to force a border model for a node */ 337 | .cm-force-border { padding-right: .1px; } 338 | 339 | @media print { 340 | /* Hide the cursor when printing */ 341 | .CodeMirror div.CodeMirror-cursors { 342 | visibility: hidden; 343 | } 344 | } 345 | 346 | /* See issue #2901 */ 347 | .cm-tab-wrap-hack:after { content: ''; } 348 | 349 | /* Help users use markselection to safely style text background */ 350 | span.CodeMirror-selectedtext { background: none; } 351 | -------------------------------------------------------------------------------- /client/StyleSheets/external/react-tabs.css: -------------------------------------------------------------------------------- 1 | .react-tabs { 2 | -webkit-tap-highlight-color: transparent; 3 | /* font-family: system-ui; */ 4 | } 5 | 6 | .react-tabs button:focus { 7 | outline: 0; 8 | } 9 | 10 | .react-tabs button { 11 | border-style: none; 12 | font-size: '25px'; 13 | padding-left: '5px'; 14 | } 15 | 16 | .react-tabs__tab-list { 17 | /* border-bottom: 1px solid #aaa; */ 18 | margin: 0 0 10px; 19 | padding: 0; 20 | } 21 | 22 | .react-tabs__tab { 23 | display: inline-block; 24 | border: 1px solid #aaa; 25 | border-bottom: none; 26 | border-radius: 5px 5px 0 0; 27 | /* bottom: -1px; */ 28 | position: relative; 29 | list-style: none; 30 | padding: 5px 11px; 31 | cursor: pointer; 32 | /* outline: 0; */ 33 | } 34 | 35 | .react-tabs__tab--selected { 36 | background: #fff; 37 | border-color: #aaa; 38 | color: black; 39 | border-radius: 5px 5px 0 0; 40 | border-bottom: 6px solid #ff6b6b; 41 | outline: 0; 42 | background-color: #5085a5; 43 | } 44 | 45 | .react-tabs__tab--disabled { 46 | color: GrayText; 47 | cursor: default; 48 | } 49 | 50 | .react-tabs__tab:focus { 51 | /* box-shadow: 0 0 5px hsl(208, 99%, 50%); */ 52 | /* border-color: hsl(208, 99%, 50%); */ 53 | outline: none; 54 | } 55 | 56 | .react-tabs__tab:focus:after { 57 | content: ""; 58 | position: absolute; 59 | height: 5px; 60 | left: -4px; 61 | right: -4px; 62 | /* bottom: -5px; */ 63 | /* background: #ff6b6b; */ 64 | outline: none; 65 | 66 | } 67 | 68 | .react-tabs__tab-panel { 69 | display: none; 70 | } 71 | 72 | .react-tabs__tab-panel--selected { 73 | display: block; 74 | /* outline: 0 */ 75 | } 76 | -------------------------------------------------------------------------------- /client/StyleSheets/variables.scss: -------------------------------------------------------------------------------- 1 | $dark-color: #31708b; 2 | $secondary-color: #5085a5; 3 | $background-color: #f7f9fb; 4 | $primary-color: #8fc1e3; 5 | $highlight-color: #f7f9fb; 6 | $loading-color: #ff6b6b; 7 | $loading-color-hover: rgba(255, 107, 107, 0.9); 8 | $font-stack: Helvetica, sans-serif, $dark-color; 9 | -------------------------------------------------------------------------------- /client/db.js: -------------------------------------------------------------------------------- 1 | import Dexie from 'dexie'; 2 | 3 | const db = new Dexie('historyDb'); 4 | db.version(1).stores({ 5 | history: '++id, query, endpoint' 6 | }); 7 | 8 | export default db; 9 | -------------------------------------------------------------------------------- /client/index.js: -------------------------------------------------------------------------------- 1 | import { render } from 'react-dom'; 2 | import React from 'react'; 3 | import App from './App'; 4 | 5 | render(, document.getElementById('root')); 6 | 7 | if (module.hot) { 8 | module.hot.accept(); 9 | } 10 | -------------------------------------------------------------------------------- /client/utils/queryInput/addQueryToDB.js: -------------------------------------------------------------------------------- 1 | import db from '../../db'; 2 | 3 | const addQueryToDB = (textValue, urlToSend) => 4 | new Promise((resolve, reject) => { 5 | // console.log('running addQueryToDB'); 6 | db.history 7 | .put({ 8 | query: textValue, 9 | endpoint: urlToSend 10 | }) 11 | .then(() => { 12 | // console.log('Sent to database.'); 13 | resolve('Sent to database.'); 14 | }) 15 | .catch(e => { 16 | // console.log('Error adding query to database. ', e); 17 | reject(new Error('Error addding query to database.')); 18 | }); 19 | }); 20 | 21 | export default addQueryToDB; 22 | -------------------------------------------------------------------------------- /client/utils/queryInput/fetchErrorCheck.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ERROR CHECKING FOR FETCH REQ IN QUERY INPUT. IMPORTED TO HANDLE QUERY FETCH 3 | * 4 | */ 5 | 6 | import * as errorMsg from '../../Constants/errors/errorStrings'; 7 | import * as dispatchObj from '../../Constants/errors/errorDispatchObjects'; 8 | 9 | const fetchErrorCheck = (error, dispatch) => { 10 | // console.log('error coming in: ', error); 11 | // console.log('error stack coming in: ', error.stack); 12 | // if Gql query does not start with 'query' 13 | // console.log('inside fetch error check: ', error); 14 | if (error.message.slice(0, errorMsg.queryMethodError.length) === errorMsg.queryMethodError) { 15 | dispatch(dispatchObj.queryMethodError); 16 | // throw new Error(errorReponse.queryMethodError); 17 | } else if ( 18 | error.message.slice(0, errorMsg.multipleQueriesError.length) === errorMsg.multipleQueriesError 19 | ) { 20 | dispatch(dispatchObj.multipleQueriesError); 21 | // throw new Error(errorReponse.multipleQueriesError); 22 | // if the variable before @rest does not exist 23 | // ! TODO: this doesn't look like it's firing. 24 | } else if (error.message === errorMsg.varBeforeRestError) { 25 | dispatch(dispatchObj.varBeforeRestError); 26 | // throw new Error(errorReponse.varBeforeRestError); 27 | // if query does not have proper curly brackets 28 | // ! TODO: this didn't look like it fired for inner right bracket being closed. Or final right bracket 29 | } else if ( 30 | error.message === errorMsg.curlyBracketError1 || 31 | error.message.slice(0, errorMsg.curlyBracketError2.length) === errorMsg.curlyBracketError2 || 32 | error.message.slice(0, errorMsg.curlyBracketError3.length) === errorMsg.curlyBracketError3 33 | ) { 34 | dispatch(dispatchObj.curlyBracketError); 35 | // throw new Error(errorReponse.curlyBracketError); 36 | // if the query fields are blank 37 | // ! TODO: this doesn't look like it's firing 38 | } else if (error.message === errorMsg.queryFieldBlankError) { 39 | dispatch(dispatchObj.queryFieldBlankError); 40 | // throw new Error(errorReponse.queryFieldBlankError); 41 | } else if (error.message.slice(0, errorMsg.typeSyntaxError.length) === errorMsg.typeSyntaxError) { 42 | dispatch(dispatchObj.typeSyntaxError); 43 | // throw new Error(errorReponse.typeSyntaxError); 44 | // TypeError: Cannot read property '0' of null 45 | } else if (error.message === errorMsg.noRestCallError) { 46 | dispatch(dispatchObj.noRestCallError); 47 | // throw new Error(errorReponse.noRestCallError); 48 | } else if ( 49 | error.message.slice(0, errorMsg.badArgumentOrFieldError.length) === 50 | errorMsg.badArgumentOrFieldError 51 | ) { 52 | // two known cases for this error: either it's an invalid type/path argument 53 | // ! NOTE: THE STACK CHECKING METHOD DOES NOT WORK ON BUILD, AS THE STACK TRACE IS DIFFERENT 54 | if (error.stack.slice(0, 300).includes('parseArgument')) 55 | dispatch(dispatchObj.noPathOrTypeError); 56 | // or a field has quotes around it 57 | else if (error.stack.slice(0, 300).includes('parseField')) dispatch(dispatchObj.badFieldError); 58 | else dispatch(dispatchObj.genericError); 59 | // throw new Error(errorReponse.badArgumentOrFieldError); 60 | } else if (error.message === errorMsg.singleQuotesError) { 61 | dispatch(dispatchObj.singleQuotesError); 62 | } else if (error.message === errorMsg.unterminatedStringError) { 63 | dispatch(dispatchObj.unterminatedStringError); 64 | } else { 65 | // console.log('Error in fetch: ', error); 66 | // ADDED GENERIC ERROR CATCH SO SOMETHING WOULD ALWAYS SHOW WITH A SYNTAX ERROR 67 | dispatch(dispatchObj.genericError); 68 | throw new Error('Error in fetch: ', error); 69 | } 70 | }; 71 | 72 | export default fetchErrorCheck; 73 | -------------------------------------------------------------------------------- /client/utils/queryOutputDisplay/jsonFormatter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * JSON FORMATTER FOR RESULT OF GQL QUERY. IMPORTED INTO QUERY OUTPUT DISPLAY 3 | * 4 | */ 5 | 6 | export const jsonFormatter = obj => { 7 | const strObj = JSON.stringify(obj); 8 | let resultStr = ''; 9 | const tab = ' '; 10 | let nestLevel = 0; 11 | for (const char of strObj) { 12 | if (char === '{' || char === '[') { 13 | resultStr += char; 14 | resultStr += '\n'; 15 | nestLevel += 1; 16 | resultStr += tab.repeat(nestLevel); 17 | } else if (char === ',') { 18 | resultStr += char; 19 | resultStr += '\n'; 20 | resultStr += tab.repeat(nestLevel); 21 | } else if (char === ':') { 22 | resultStr += `${char} `; 23 | } else if (char === '}' || char === ']') { 24 | resultStr += '\n'; 25 | nestLevel -= 1; 26 | resultStr += tab.repeat(nestLevel); 27 | resultStr += char; 28 | } else { 29 | resultStr += char; 30 | } 31 | } 32 | return resultStr; 33 | }; 34 | -------------------------------------------------------------------------------- /client/utils/queryOutputDisplay/nullChecker.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-syntax */ 2 | const nullChecker = object => { 3 | // pull keys off object passed in 4 | const objKeys = Object.keys(object); 5 | let nullCheck; 6 | // loop over object key by key 7 | for (const key of objKeys) { 8 | // if the value at the current key in the object is null, return true 9 | if (object[key] === null) return true; 10 | // in the case of an array of objects - iterate through array and recursively call nullChecker on all objects 11 | if (Array.isArray(object[key])) { 12 | for (const el of object[key]) { 13 | if (el === null) return true; 14 | if (!Array.isArray(el) && typeof el === 'object') { 15 | // if value at current key is an object, recurse and assign boolean result to a variable 16 | nullCheck = nullChecker(el); 17 | // check again for true 18 | if (nullCheck === true) return true; 19 | } 20 | } 21 | } 22 | if (!Array.isArray(object[key]) && typeof object[key] === 'object') { 23 | // if value at current key is an object, recurse and assign boolean result to a variable 24 | nullCheck = nullChecker(object[key]); 25 | // check again for true 26 | if (nullCheck === true) return true; 27 | } 28 | } 29 | 30 | // if made it out of the loop, nothing is null 31 | return false; 32 | }; 33 | 34 | const objectTest = { 35 | name: 'Will', 36 | // testNull: { 37 | // value: 12, 38 | // moreNull: null, 39 | // superNull: { 40 | // pong: null, 41 | // bed: 'yes', 42 | // }, 43 | // }, 44 | // flatNull: null, 45 | // age: 90, 46 | test: { 47 | once: { 48 | array: [null] 49 | } 50 | } 51 | }; 52 | 53 | const officeCheck = { 54 | id: 526, 55 | name: 'The Office', 56 | seasons: [ 57 | { 58 | number: 1, 59 | image: null, 60 | summary: null, 61 | __typename: 'Season' 62 | }, 63 | { 64 | number: 2, 65 | image: null, 66 | summary: null, 67 | __typename: 'Season' 68 | }, 69 | { 70 | number: 3, 71 | image: null, 72 | summary: null, 73 | __typename: 'Season' 74 | }, 75 | { 76 | number: 4, 77 | image: null, 78 | summary: null, 79 | __typename: 'Season' 80 | }, 81 | { 82 | number: 5, 83 | image: null, 84 | summary: null, 85 | __typename: 'Season' 86 | }, 87 | { 88 | number: 6, 89 | image: null, 90 | summary: null, 91 | __typename: 'Season' 92 | }, 93 | { 94 | number: 7, 95 | image: null, 96 | summary: null, 97 | __typename: 'Season' 98 | }, 99 | { 100 | number: 8, 101 | image: null, 102 | summary: null, 103 | __typename: 'Season' 104 | }, 105 | { 106 | number: 9, 107 | image: null, 108 | summary: null, 109 | __typename: 'Season' 110 | } 111 | ], 112 | __typename: 'Show' 113 | }; 114 | 115 | // should be boolean 116 | // console.log(nullChecker(officeCheck)); 117 | 118 | export default nullChecker; 119 | -------------------------------------------------------------------------------- /client/utils/queryOutputDisplay/nullResultChecker.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-syntax */ 2 | import React from 'react'; 3 | 4 | const nullResultCheck = (object, errorPath = '') => { 5 | // instantiate output variable 6 | const nullVals = []; 7 | let nullPath; 8 | // pull keys off object passed in 9 | const objKeys = Object.keys(object); 10 | // loop over object key by key 11 | for (const key of objKeys) { 12 | // if the value at the current key in the object is null, add to result array 13 | if (object[key] === null) nullVals.push(
  • {errorPath + key}
  • ); 14 | else if (Array.isArray(object[key])) { 15 | for (let i = 0; i < object[key].length; i += 1) { 16 | if (object[key][i] === null) 17 | nullVals.push( 18 |
  • {`${errorPath + key}at index ${i}`}
  • 19 | ); 20 | else if (!Array.isArray(object[key][i]) && typeof object[key][i] === 'object') { 21 | nullPath = nullResultCheck(object[key][i], `${errorPath + key} at index ${i} => `); 22 | // ensure nullPath is not an empty string, in that there's a new nullpath to add 23 | if (nullPath !== '') { 24 | // nullVals is always an array. so if path is an array of strings, flatten vals out 25 | // before pushing 26 | nullPath.forEach((curNullPath, idx) => { 27 | nullVals.push(
  • {curNullPath}
  • ); 28 | }); 29 | // nullVals.push(...nullPath); 30 | } 31 | } 32 | } 33 | } 34 | // if value at current key is an object, recurse and assign to a variable 35 | else if (!Array.isArray(object[key]) && typeof object[key] === 'object') { 36 | nullPath = nullResultCheck(object[key], `${errorPath + key} => `); 37 | // ensure nullPath is not an empty string, in that there's a new nullpath to add 38 | if (nullPath !== '') { 39 | // nullVals is always an array. so if path is an array of strings, flatten vals out 40 | // before pushing 41 | nullPath.forEach((curNullPath, idx) => { 42 | nullVals.push(
  • {curNullPath}
  • ); 43 | }); 44 | // nullVals.push(...nullPath); 45 | } 46 | } 47 | } 48 | 49 | // return flat array of string(s) 50 | return nullVals; 51 | }; 52 | 53 | const objectTest = { 54 | name: 'Will', 55 | testNull: { 56 | value: 12, 57 | moreNull: null, 58 | superNull: { 59 | pong: null, 60 | bed: 'yes' 61 | } 62 | }, 63 | flatNull: null, 64 | age: 90 65 | }; 66 | 67 | // should be array of strings 68 | // console.log(nullResultCheck(objectTest)); 69 | 70 | export default nullResultCheck; 71 | -------------------------------------------------------------------------------- /dev/Code Snippets/DEV_QueryInput.jsx: -------------------------------------------------------------------------------- 1 | { /* NOTE: IN CASE THERE ARE RESET/IMMEDIATELY QUERY ISSUES, USE BUTTON OUTSIDE FORM */ } 2 | { /* */ } 23 | 24 | 25 | // ///// 26 | 27 | // ! NOTE: USE RUN QUERY DISPATCH HERE TO TEST FOR NESTED QUERIES 28 | // dispatch({ 29 | // type: types.RUN_QUERY, 30 | // // decontructed using of gql tag to make query object. need to pass in a stringliteral. 31 | // query: gql([`${textValue}`]), 32 | // // pulls of key for where data will be in result obj 33 | // queryResultObject: textValue.match(/(?<=\{\W)(.*?)(?=\@)/g)[0].trim(), 34 | // newEndpoint: urlToSend, 35 | // }); 36 | // // reset local api endpoint 37 | // setNewAPIEndpoint(''); 38 | // return; 39 | // ! END OF NESTED TEST 40 | 41 | // ! TO DELETE: TEST METHOD TO SEE IF FRONTEND CONNECTS TO SERVER 42 | // const serverCheck = () => { 43 | // event.preventDefault(); 44 | 45 | // // this goes directly to dev server 46 | // // works with localhost:3030. need to have server SERVE up app 47 | // fetch('/api') 48 | // .then(response => response.json()) 49 | // .then((data) => { 50 | // console.log('response: ', data.msg); 51 | // }) 52 | // .catch(e => console.log('error in server test: ', e)); 53 | // }; 54 | -------------------------------------------------------------------------------- /dev/Code Snippets/DEV_handleQueryFetch.js: -------------------------------------------------------------------------------- 1 | import gql from 'graphql-tag'; 2 | import fetchErrorCheck from './fetchErrorCheck'; 3 | import * as types from '../../Constants/actionTypes'; 4 | 5 | const proxy = Number(process.env.IS_DEV) === 1 ? 'https://cors-anywhere.herokuapp.com/' : ''; 6 | 7 | // MIGHT rework to invoke inside of apollo custom fetch. 8 | 9 | const handleQueryFetch = (textValue, urlToSend, dispatch, setNewAPIEndpoint) => { 10 | // prevent refresh 11 | event.preventDefault(); 12 | 13 | // console.log('Running handleQueryFetch'); 14 | // const urlToSend = newAPIEndpoint || endpoint; 15 | 16 | // // send textValue to Dexie db 17 | // db.history.put({ 18 | // query: textValue, 19 | // endpoint: urlToSend, 20 | // }) 21 | // .then(() => console.log('Sent to database.')) 22 | // .catch(e => console.log('Error adding query to database.')); 23 | 24 | 25 | // ! NOTE: Nested test dispatch added to codeSnippets 26 | 27 | // make initial fetch to api, to ensure endpoint is valid. proxy to get around CORS 28 | 29 | // added w/ new promise format 30 | return new Promise((resolve, reject) => { 31 | // * NOTE, 8/6: THROWING ERRORS BEFORE CATCH SEEMS TO HAVE BETTER ERROR MESSAGING IN CONSOLE LOGS 32 | // * REJECTING IN CATCH SEEMS TO HAVE BETTER HANDLING 33 | 34 | fetch(proxy + urlToSend, { 35 | // mode: 'no-cors', 36 | headers: { 37 | 'Access-Control-Allow-Origin': '*', 38 | 'Content-Type': 'application/json', 39 | }, 40 | }) 41 | .then((response) => { 42 | // catch all for when textValue is null 43 | // console.log('in first then block for fetch'); 44 | 45 | // execute regex filtering on the path param 46 | const pathRegex = textValue.match(/(?<=path:\W*\")\S*(?=\")/gi); 47 | // SHOULD ADD CHECKS FOR 400, 401, 403, maybe more 48 | // 422 => happened in one instance with an API key but no attached query 49 | if (response.status === 404) { 50 | dispatch({ 51 | // send off error message for endpoint 404 52 | type: types.GQL_ERROR, 53 | gqlError: 'Endpoint is invalid. Please double check your endpoint.', 54 | }); 55 | // throwing error stops promise chain 56 | throw new Error('Endpoint is invalid. Please double check your endpoint.'); 57 | } else if (pathRegex === null) { 58 | // if regex is null, then there's no path 59 | dispatch({ 60 | // dispatch path error 61 | type: types.GQL_ERROR, 62 | // changed this dispatch message to match error thrown below 63 | gqlError: 'Path is invalid. Please double check your path.', 64 | // gqlError: '@rest must have a \'path\' and \'type\' property. Please click reset to check the example for reference.', 65 | }); 66 | // throwing error/reject stops promise chain 67 | throw new Error('Path is invalid. Please double check your path.'); 68 | } else { 69 | // if regex is NOT null, there was a path. fetch is now made to endpoint + path 70 | const path = textValue.match(/(?<=path:\W*\")\S*(?=\")/gi)[0].trim(); 71 | // ! NEED: check if there's a param in the path 72 | // return fetch, which creates a promise 73 | return fetch(proxy + urlToSend + path, { 74 | headers: { 75 | 'Access-Control-Allow-Origin': '*', 76 | 'Content-Type': 'application/json', 77 | }, 78 | }); 79 | } 80 | }) 81 | // for checking if the path is correct 82 | .then((response) => { 83 | // console.log('in second then block of fetch'); 84 | if (response.status === 404) { 85 | dispatch({ 86 | type: types.GQL_ERROR, 87 | gqlError: 'Path is invalid. Please double check your path.', 88 | }); 89 | throw new Error('Path is invalid. Please double check your path.'); 90 | } else return response.json(); 91 | }) 92 | .then((data) => { 93 | // console.log('in third then of fetch, before run query dispatch'); 94 | // if get request is successful, parse it here. fire dispatch to run query 95 | dispatch({ 96 | type: types.RUN_QUERY, 97 | // decontructed using of gql tag to make query object. need to pass in a stringliteral. 98 | query: gql([`${textValue}`]), 99 | // pulls of key for where data will be in result obj 100 | queryResultObject: textValue.match(/(?<=\{\W)(.*?)(?=\@)/g)[0].trim(), 101 | newEndpoint: urlToSend, 102 | }); 103 | // reset local api endpoint 104 | setNewAPIEndpoint(''); 105 | resolve('Fetch chain successful.'); 106 | }) 107 | .catch((error) => { 108 | // moved error checking to other file for code clarity 109 | fetchErrorCheck(error, dispatch, reject); 110 | // added w/new promise format 111 | }); 112 | }); 113 | }; 114 | 115 | export default handleQueryFetch; 116 | -------------------------------------------------------------------------------- /electron.js: -------------------------------------------------------------------------------- 1 | const electron = require('electron'); 2 | // not presently using clipboard 3 | const { clipboard } = require('electron'); 4 | const url = require('url'); 5 | const path = require('path'); 6 | 7 | // to determine whether a dev environ is being un 8 | const isDev = require('electron-is-dev'); 9 | // const isDev= false; 10 | const { app, BrowserWindow, Menu } = electron; 11 | 12 | let mainWindow; 13 | 14 | // listen for app to be ready 15 | // NOTE: LOCAL HOST CHANGED TO 8080 SO EXPRESS CAN SERVE UP ELECTORN 16 | app.on('ready', () => { 17 | // create new window 18 | mainWindow = new BrowserWindow({ width: 1170, height: 800 }); 19 | // load html into the window 20 | console.log('Dev environment on: ', isDev); 21 | mainWindow.loadURL( 22 | url.format({ 23 | // ssiwtched back to 3000 from 8080 because no longer using server 24 | pathname: isDev ? '//localhost:3000' : path.join(__dirname, './build/index.html'), 25 | protocol: isDev ? 'http:' : 'file:', 26 | slashes: true 27 | }) 28 | ); 29 | 30 | // build menu from template 31 | if (isDev) mainMenuTemplate.push(devToolsMenu); 32 | const mainMenu = Menu.buildFromTemplate(mainMenuTemplate); 33 | // insert Menu 34 | Menu.setApplicationMenu(mainMenu); 35 | }); 36 | 37 | // create menu template 38 | const mainMenuTemplate = [ 39 | { 40 | label: 'File', 41 | submenu: [ 42 | { 43 | label: 'Quit', 44 | click() { 45 | app.quit(); 46 | }, 47 | accelerator: 'CommandOrControl+Q' 48 | } 49 | ] 50 | }, 51 | { 52 | label: 'Edit', 53 | submenu: [ 54 | { label: 'Undo', accelerator: 'CmdOrCtrl+Z', selector: 'undo:' }, 55 | { label: 'Redo', accelerator: 'Shift+CmdOrCtrl+Z', selector: 'redo:' }, 56 | { type: 'separator' }, 57 | { label: 'Cut', accelerator: 'CmdOrCtrl+X', selector: 'cut:' }, 58 | { label: 'Copy', accelerator: 'CmdOrCtrl+C', selector: 'copy:' }, 59 | { label: 'Paste', accelerator: 'CmdOrCtrl+V', selector: 'paste:' }, 60 | { label: 'Select All', accelerator: 'CmdOrCtrl+A', selector: 'selectAll:' } 61 | ] 62 | } 63 | ]; 64 | 65 | const devToolsMenu = { 66 | label: 'View', 67 | submenu: [ 68 | { 69 | // should be available only in dev environment 70 | open: false, 71 | label: 'DevTools', 72 | click() { 73 | if (!this.open) { 74 | mainWindow.webContents.openDevTools(); 75 | this.open = true; 76 | } else { 77 | mainWindow.webContents.closeDevTools(); 78 | this.open = false; 79 | } 80 | }, 81 | accelerator: 'f12' 82 | } 83 | ] 84 | }; 85 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Hypnos 7 | 8 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
    19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hypnos", 3 | "version": "0.0.2", 4 | "description": "An app to test GraphQL calls on RESTful APIs, via Apollo.", 5 | "main": "electron.js", 6 | "homepage": "./", 7 | "scripts": { 8 | "start-electron": "electron .", 9 | "start-electron-prod": "ELECTRON_IS_DEV=0 npm run start-electron", 10 | "electron-build": "electron-builder -mwl", 11 | "clean-build": "rm -rf build/ .cache dist/", 12 | "react-start": "NODE_ENV=development parcel -p 3000 index.html --out-dir build", 13 | "react-build": "NODE_ENV=production parcel build index.html --out-dir build --public-url ./", 14 | "start": "npm run react-start & wait-on http://localhost:3000 && ELECTRON_IS_DEV=1 npm run start-electron", 15 | "build": "npm run clean-build && npm run react-build && ELECTRON_IS_DEV=0 npm run electron-build", 16 | "test": "jest --verbose" 17 | }, 18 | "keywords": [ 19 | "Apollo", 20 | "Apollo-Link-Rest", 21 | "Electron", 22 | "Jest", 23 | "Enzyme", 24 | "Puppeteer", 25 | "Testing", 26 | "Dexie", 27 | "IndexDB", 28 | "React", 29 | "Parcel", 30 | "GraphQL", 31 | "REST API", 32 | "API", 33 | "Query", 34 | "Mutation" 35 | ], 36 | "author": { 37 | "name": "Sophie Nye", 38 | "email": "sophie.nye@gmail.com" 39 | }, 40 | "contributors": [ 41 | "Dillon Garrett