├── .gitignore
├── LICENSE
├── README.md
├── __tests__
├── __config__
│ └── jest.setup.js
├── components
│ └── Input.test.js
└── unit
│ ├── autoQuery.test.js
│ ├── checkObject.test.js
│ └── createTree.test.js
├── babel.config.js
├── client
├── App.jsx
├── components
│ ├── Input.jsx
│ ├── Output.jsx
│ ├── TopBar.jsx
│ └── Tree.jsx
├── index.js
└── stylesheets
│ ├── CodeMirror-default-overwrites.scss
│ ├── body.scss
│ ├── bottom-bar.scss
│ ├── custom-colors.scss
│ ├── custom-themes
│ └── custom-nord.scss
│ ├── endpoint-input.scss
│ ├── error-message.scss
│ ├── get-schema-btn.scss
│ ├── input-container.scss
│ ├── input-instance.scss
│ ├── io-container.scss
│ ├── logo.scss
│ ├── main-container.scss
│ ├── output-container.scss
│ ├── send-btn.scss
│ ├── styles.scss
│ ├── top-bar.scss
│ ├── tree.scss
│ └── widget-btn.scss
├── helpers
├── autoQuery.js
├── createTree.js
├── customHeader.js
├── customToggle.js
└── validateObject.js
├── images
├── Logo-Dark.png
├── PractiQL-local-storage.gif
├── PractiQL-logodark.png
├── PractiQL-logolite.png
├── PractiQL-mq1.gif
├── PractiQL-mq2.gif
├── PractiQL-schemaTree.gif
└── logo-lite.png
├── index.html
├── package.json
├── server
└── server.js
└── webpack.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 |
78 | # Next.js build output
79 | .next
80 |
81 | # Nuxt.js build / generate output
82 | .nuxt
83 | dist
84 |
85 | # Gatsby files
86 | .cache/
87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
88 | # https://nextjs.org/blog/next-9-1#public-directory-support
89 | # public
90 |
91 | # vuepress build output
92 | .vuepress/dist
93 |
94 | # Serverless directories
95 | .serverless/
96 |
97 | # FuseBox cache
98 | .fusebox/
99 |
100 | # DynamoDB Local files
101 | .dynamodb/
102 |
103 | # TernJS port file
104 | .tern-port
105 |
106 | package-lock.json
107 |
108 | build
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 OSLabs Beta
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | #
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | **PractiQL** is an open-source, browser-based, GraphQL IDE. Users can send a single query to a GraphQL API or send out multiple queries simultaneously. Users also have the ability to click nodes on a treelist representing the target schema and have queries automatically generated for an intuitive experience designed to improve developer workflow.
12 |
13 |
14 | # **Features**
15 |
16 | ## Multiple Queries Simultaneously
17 | PractiQL provides users with the ability to create and send out multiple queries at once without the need for GraphQL aliases. Results are returned in separate code blocks, including results for queries to the same field, for an intuitive and expedited experience with little mental overhead.
18 | Send a bunch of queries. Get a bunch of responses. It just works. (NOTE: For internal API's, please enable CORS.)
19 |
20 |
21 |
22 |
23 |
24 | ## Write a Bunch of Queries. Send the Ones You Want
25 | Users also have the option to send out different combinations of multiple queries. It’s as easy as highlighting the queries you want to send and sending them.
26 |
27 |
28 |
29 |
30 |
31 |
32 | ## Schema Trees
33 | PractiQL can render a tree list model of a target GraphQL API for an easy-to-navigate representation of the schema. We call the models schema trees, and they’re a quick and organized way to view and interact with entire GraphQL schemas at once.
34 |
35 |
36 | ## Automatically Generated Queries
37 | Users also have the ability to generate queries by clicking on the nodes of the schema tree instead of typing those queries out manually. The result is an extremely intuitive experience and improved developer workflow that’s especially useful when testing new GraphQL APIs.
38 | No more guessing. No more relying on auto-completed fields. Generate queries as you’re browsing the schema.
39 |
40 |
41 |
42 |
43 |
44 | ## Saved Queries
45 | Navigated away from PractiQL? No problem. Queries are saved in local storage and populate automatically when you return.
46 |
47 |
48 |
49 |
50 | # **Contributors**
51 |
52 |
53 | [Anthony Cruz](linkedin.com/in/anthonycruz2) [@anthonycruz1](https://github.com/anthonycruz1)
54 |
55 | [Les C.](linkedin.com/in/leschae) [@lesc999](https://github.com/lesc999)
56 |
57 | [Rob Caporino](https://www.linkedin.com/in/rob-a-caporino/) [@rcaporino](https://github.com/rcaporino)
58 |
59 | [David Nadler](https://www.linkedin.com/in/davenads/) [@davenads](https://github.com/Davenads)
60 |
61 |
62 |
--------------------------------------------------------------------------------
/__tests__/__config__/jest.setup.js:
--------------------------------------------------------------------------------
1 | const Enzyme = require('enzyme');
2 | const Adapter = require('enzyme-adapter-react-16');
3 |
4 | Enzyme.configure({ adapter: new Adapter() });
5 |
--------------------------------------------------------------------------------
/__tests__/components/Input.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Input from '../../client/components/Input.jsx';
3 | import { shallow } from 'enzyme';
4 |
5 | describe('...', () => {
6 | const inputComponent = shallow();
7 | it('...renders div element as the container', () => {
8 | expect(inputComponent.type()).toBe('div');
9 | });
10 | it('...div has class "input-container-outer"', () => {
11 | expect(inputComponent.hasClass('input-container-outer')).toBeTruthy();
12 | });
13 | it('...renders React component ', () => {
14 | expect(inputComponent.text()).toBe('');
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/__tests__/unit/autoQuery.test.js:
--------------------------------------------------------------------------------
1 | import Input from '../../client/components/Input';
2 |
3 | const autoQuery = require('../../helpers/autoQuery');
4 |
5 | test('autoQuery() returns error if array is not passed.', () => {
6 | expect(autoQuery(7)).toEqual('');
7 | });
8 | test('autoQuery() returns string representing a query when an array is passed', () => {
9 | expect(autoQuery(['parent', 'child'])).toEqual(
10 | '{\n parent{\n child\n }\n}'
11 | );
12 | });
13 |
--------------------------------------------------------------------------------
/__tests__/unit/checkObject.test.js:
--------------------------------------------------------------------------------
1 | const validateObject = require('../../helpers/validateObject');
2 |
3 | test('validateObject() returns error if array is passed.', () => {
4 | expect(validateObject([])).toBeFalsy();
5 | });
6 | test('validateObject() returns error if number is passed.', () => {
7 | expect(validateObject(7)).toBeFalsy();
8 | });
9 | test('validateObject() returns error if string is passed.', () => {
10 | expect(validateObject('test')).toBeFalsy();
11 | });
12 | test('validateObject() returns error if map is passed.', () => {
13 | expect(validateObject(new Map())).toBeFalsy();
14 | });
15 | test('validateObject() returns error if set is passed.', () => {
16 | expect(validateObject(new Set())).toBeFalsy();
17 | });
18 | test('validateObject() returns true if empty object is passed.', () => {
19 | expect(validateObject({})).toBeTruthy();
20 | });
21 | test('validateObject() returns true if object is passed.', () => {
22 | expect(validateObject({ a: 1, b: 2, c: 3 })).toBeTruthy();
23 | });
24 |
--------------------------------------------------------------------------------
/__tests__/unit/createTree.test.js:
--------------------------------------------------------------------------------
1 | const createTree = require('../../helpers/createTree');
2 |
3 | test('createTree() returns custom error object when number is passed.', () => {
4 | expect(createTree(7)).toStrictEqual({
5 | error: 'Sorry, something went wrong',
6 | });
7 | });
8 | test('createTree() returns custom error object when string is passed.', () => {
9 | expect(createTree('string')).toStrictEqual({
10 | error: 'Sorry, something went wrong',
11 | });
12 | });
13 | test('createTree() returns custom error object when array is passed.', () => {
14 | expect(createTree([])).toStrictEqual({
15 | error: 'Sorry, something went wrong',
16 | });
17 | });
18 | test('createTree() returns custom error object when non-schema object is passed.', () => {
19 | expect(createTree({})).toStrictEqual({
20 | error: 'Sorry, something went wrong',
21 | });
22 | });
23 | test('createTree() returns custom error object when schema._queryType.name does not exist.', () => {
24 | expect(
25 | createTree({ _queryType: { testProperty: 'this is not a name' } })
26 | ).toStrictEqual({
27 | error: 'Sorry, something went wrong',
28 | });
29 | });
30 | test('createTree() returns custom error object when schema._queryType.name is not a string.', () => {
31 | expect(createTree({ _queryType: { name: 7 } })).toStrictEqual({
32 | error: 'Sorry, something went wrong',
33 | });
34 | });
35 | test('createTree() returns custom error object when schema._queryType.fields does not exist.', () => {
36 | expect(createTree({ _queryType: 'test' })).toStrictEqual({
37 | error: 'Sorry, something went wrong',
38 | });
39 | });
40 |
41 | test('createTree() returns tree object when schema object is passed.', () => {
42 | expect(
43 | createTree({
44 | _queryType: { name: 'Test Schema', _fields: {} },
45 | })
46 | ).toStrictEqual({
47 | name: 'Test Schema',
48 | children: [],
49 | toggled: true,
50 | });
51 | });
52 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | // needed for tests to run
2 | module.exports = {
3 | presets: [
4 | ['@babel/preset-env', { targets: { node: 'current' } }],
5 | '@babel/preset-react',
6 | ],
7 | };
8 |
--------------------------------------------------------------------------------
/client/App.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { getIntrospectionQuery, buildClientSchema } from 'graphql';
3 | import Output from './components/Output';
4 | import TopBar from './components/TopBar';
5 | import Input from './components/Input';
6 | import 'codemirror/lib/codemirror.css';
7 | import 'codemirror/theme/nord.css';
8 | import 'codemirror/addon/hint/show-hint.css';
9 | import 'codemirror/addon/hint/show-hint';
10 | import 'codemirror/addon/lint/lint';
11 | import 'codemirror/addon/edit/matchbrackets.js';
12 | import 'codemirror/addon/edit/closebrackets.js';
13 | import 'codemirror/addon/fold/foldgutter';
14 | import 'codemirror/addon/fold/brace-fold';
15 | import 'codemirror/addon/fold/foldgutter.css';
16 | import 'codemirror-graphql/hint';
17 | import 'codemirror-graphql/lint';
18 | import 'codemirror-graphql/mode';
19 | import 'codemirror/mode/javascript/javascript';
20 | import 'codemirror/addon/scroll/simplescrollbars.css';
21 | import 'codemirror/addon/scroll/simplescrollbars';
22 | import 'codemirror-graphql/results/mode';
23 | import Tree from './components/Tree.jsx';
24 | import createTree from '../helpers/createTree.js';
25 | export default function App(props) {
26 | const { theme, endpoint } = props;
27 | const [editor, setEditor] = useState('');
28 | const [input, setInput] = useState(
29 | localStorage.getItem('PractiQL') || ''
30 | );
31 | const [myTheme, setMyTheme] = useState(theme);
32 | const [querySubjects, setQuerySubjects] = useState([]);
33 | const [results, setResults] = useState(false);
34 | const [schema, setSchema] = useState('');
35 | const [selection, setSelection] = useState('');
36 | const [sideBarWidth, setSideBarWidth] = useState({
37 | width: '0rem',
38 | padding: '0.5rem 0',
39 | });
40 | const [stateEndpoint, setStateEndpoint] = useState(endpoint);
41 | const [treeObj, setTreeObj] = useState({});
42 |
43 | // Sends introspection query to endpoint and sets results as schema
44 | useEffect(() => {
45 | fetch(stateEndpoint, {
46 | method: 'POST',
47 | headers: {
48 | Accept: 'application/json',
49 | 'Content-Type': 'application/json',
50 | },
51 | body: JSON.stringify({
52 | query: getIntrospectionQuery(),
53 | }),
54 | })
55 | .then((res) => res.json())
56 | .then((schemaJSON) => {
57 | setSchema(buildClientSchema(schemaJSON.data));
58 | });
59 | }, [stateEndpoint]);
60 |
61 | // Uses schema to build tree
62 | useEffect(() => {
63 | if (schema) {
64 | setTreeObj(createTree(schema));
65 | }
66 | }, [schema]);
67 |
68 | // Sets new endpoints
69 | const handleBtnClick = (newEndpoint) => {
70 | // Sets new endpoint
71 | setStateEndpoint(newEndpoint);
72 | setQuerySubjects([]);
73 | // if sidebar is open, closes sidebar and removes tree from state
74 | if (sideBarWidth.width !== '0rem') {
75 | handleCloseSideBar();
76 | }
77 | };
78 |
79 | // Generates autoQueries
80 | const handleAutoQuery = (query) => {
81 | // If current input is empty, return query, else return query one line under current input
82 | const newInput = input === '' ? query : input + '\n' + query;
83 | setInput(newInput);
84 | };
85 |
86 | // Constructs new tree diagram and opens side bar
87 | const handleSchemaRequest = () => {
88 | const widthToSet = sideBarWidth.width === '0rem' ? '20rem' : '0rem';
89 | const paddingToSet =
90 | sideBarWidth.padding === '0.5rem 0' ? '0.5rem' : '0.5rem 0';
91 | setSideBarWidth({ width: widthToSet, padding: paddingToSet });
92 | setTreeObj(createTree(schema));
93 | };
94 |
95 | // Close sidebar
96 | const handleCloseSideBar = () => {
97 | setSideBarWidth({ width: '0rem', padding: '0.5rem 0' });
98 | setTreeObj({});
99 | // closes bottom bar
100 | const bottomBar = document.getElementById('bottom-bar');
101 | bottomBar.style.height = '0';
102 | bottomBar.style.padding = '1rem 0.75rem 0 0.75rem';
103 | };
104 |
105 | // Sets new editor for keyboard shortcuts
106 | const setNewEditor = (newEditor) => {
107 | setEditor(newEditor);
108 | };
109 |
110 | // Expands bottom bar when mouse enters
111 | const handleBottomBarExpand = () => {
112 | const bottomBar = document.getElementById('bottom-bar');
113 | bottomBar.style.removeProperty = 'height';
114 | bottomBar.style.padding = '1rem 0.75rem 2rem 0.75rem';
115 | };
116 |
117 | // Collapses bottom bar when mouse leaves and if side bar is not open
118 | const handleBottomBarCollapse = () => {
119 | // if sidebar is open, bottom bar stays expanded
120 | if (sideBarWidth.width !== '0rem') return;
121 | const bottomBar = document.getElementById('bottom-bar');
122 | bottomBar.style.height = '0';
123 | bottomBar.style.padding = '1rem 0.75rem 0 0.75rem';
124 | };
125 |
126 | // Save queries in the LocalStorage
127 | const handleSnapshot = () => {
128 | //LocalStorage
129 | localStorage.setItem('PractiQL', input.trim());
130 | };
131 |
132 | return (
133 |
134 |
135 |
136 |
145 |
146 |
147 |
148 |
157 |
163 |
164 | 🜉
165 |
166 |
170 | Schema
171 |
172 |
173 | Docs
174 |
175 |
179 | Save Snapshot
180 |
181 |
182 |
183 |
184 |
185 |
191 |
192 |
193 |
194 |
195 |
196 | X
197 |
198 |
199 |
Schema
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 | );
208 | }
209 |
--------------------------------------------------------------------------------
/client/components/Input.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Controlled as ControlledEditor } from 'react-codemirror2';
3 | import { ValidationContext, SDLValidationContext } from 'graphql';
4 |
5 | export default function Input(props) {
6 | const {
7 | autoQuery,
8 | value,
9 | onChange,
10 | selection,
11 | onSelectionChange,
12 | schema,
13 | theme,
14 | } = props;
15 |
16 | function handleChange(editor, data, value) {
17 | onChange(value);
18 | }
19 |
20 | function handleSelection(sel) {
21 | if (onSelectionChange) {
22 | onSelectionChange(sel);
23 | }
24 | }
25 |
26 | function handlePress(editor, keyEvent) {
27 | if (!editor.state.completionActive && keyEvent.keyCode != 13) {
28 | editor.showHint({ completeSingle: false });
29 | }
30 | }
31 |
32 | return (
33 |
34 | {
40 | handleSelection(editor.getSelection());
41 | }}
42 | editorDidMount={(editor) => {
43 | editor.display.wrapper.className =
44 | editor.display.wrapper.className + ' input-instance';
45 | props.setNewEditor(editor);
46 | }}
47 | options={{
48 | foldGutter: true,
49 | gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
50 | matchBrackets: true,
51 | autoCloseBrackets: true,
52 | lineWrapping: true,
53 | indentUnit: 2,
54 | tabSize: 2,
55 | //currently is not linting need to look into it, might need options
56 | mode: 'graphql',
57 | // lint: {
58 | // schema: schema,
59 | // },
60 | showHint: true,
61 | hintOptions: {
62 | schema: schema,
63 | },
64 | lineNumbers: true,
65 | theme: theme,
66 | extraKeys: { 'Ctrl-Space': 'autocomplete' },
67 | }}
68 | />
69 |
70 | );
71 | }
72 |
--------------------------------------------------------------------------------
/client/components/Output.jsx:
--------------------------------------------------------------------------------
1 | import CodeMirror, { overlayMode } from 'codemirror';
2 | import React, { useState, useEffect, useCallback } from 'react';
3 | import { Controlled as ControlledEditor } from 'react-codemirror2';
4 |
5 | export default function Output(props) {
6 | const [editorToGrab, setEditor] = useState(null);
7 | const [value, setValue] = useState('');
8 | const {
9 | displayName,
10 | language,
11 | results,
12 | onChange,
13 | theme,
14 | numOfQueries,
15 | } = props;
16 |
17 | useEffect(() => {
18 | // Returns results folded
19 | // How: copies the results into a headless CodeMirror instance. This headless instance is used to count lines and identify where to fold code
20 | // If only one query was sent, won't collapse code. Returns code expanded.
21 | if (results && Object.keys(results).length === 1) return;
22 |
23 | if (editorToGrab) {
24 | let count = 1;
25 | let lastLine = 0;
26 | for (let key in results) {
27 | let instance = new CodeMirror(document.createElement('div'), {
28 | value: JSON.stringify(results[key], null, 2),
29 | });
30 |
31 | if (count === 1) {
32 | editorToGrab.foldCode(1);
33 | } else {
34 | editorToGrab.foldCode(lastLine + 1);
35 | }
36 | count++;
37 | lastLine += instance.lineCount();
38 | }
39 | }
40 | }, [results]);
41 |
42 | return (
43 | <>
44 | {
49 | setEditor(editor);
50 | }}
51 | on
52 | options={{
53 | mode: 'javascript',
54 | foldGutter: true,
55 | gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
56 | readOnly: true,
57 | lineNumbers: true,
58 | theme: theme,
59 | scrollbarStyle: null,
60 | }}
61 | >
62 | >
63 | );
64 | }
65 |
--------------------------------------------------------------------------------
/client/components/TopBar.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | // https://countries.trevorblades.com/
4 | export default function TopBar(props) {
5 | const { endpoint, input, selection, setResults, setQuerySubjects } = props;
6 |
7 | const handleClick = () => {
8 | //LocalStorage
9 | localStorage.setItem('PractiQL', input.trim());
10 |
11 | const sel = selection ? selection.trim() : input.trim();
12 |
13 | const isMutation = sel.includes('mutation');
14 |
15 | let myQuery;
16 | if (isMutation) {
17 | myQuery = 'mutation {\r\n';
18 | } else {
19 | myQuery = 'query myquery {\r\n';
20 | }
21 | const arrItems = matchRecursiveRegExp(sel, '{', '}');
22 |
23 | let querySubjects = [];
24 |
25 |
26 | for (let i = 0; i < arrItems.length; i++) {
27 | const x = arrItems[i];
28 | // IS THIS A MERGED QUERY?
29 | if (x.includes(',')) {
30 | const items = x.split(',');
31 |
32 | for (let i = 0; i < items.length; i++) {
33 | // DOES THIS item HAVE AN ALIAS?
34 | if (items[i].includes(':')) {
35 | querySubjects.push(
36 | items[i].substring(0, items[i].indexOf(':')).trim()
37 | );
38 |
39 | myQuery += items[i] + (i < items.length - 1 ? ',\r\n' : '\r\n');
40 | } else {
41 | // ADD ALIAS TO RETURN MULTIPLE RESULTSETS
42 | const alias =
43 | items[i].substring(0, items[i].indexOf('{')).trim() +
44 | '_' +
45 | i.toString();
46 | querySubjects.push(alias);
47 |
48 | myQuery +=
49 | alias +
50 | ' : ' +
51 | items[i] +
52 | (i < items.length - 1 ? ',\r\n' : '\r\n');
53 | }
54 | }
55 | } else {
56 | // DOES THIS item HAVE AN ALIAS?
57 | let test = x.substring(0, x.indexOf(':')).trim();
58 | if (test.indexOf('(') > 0) {
59 | if (x.trimStart().startsWith('__')) {
60 | querySubjects.push(x.substring(0, x.indexOf('{')).trim());
61 | } else {
62 | querySubjects.push(x.substring(0, x.indexOf('(')).trim());
63 | }
64 | myQuery += arrItems[i].trim();
65 | } else {
66 | let alias;
67 | const query = x.substring(0, x.indexOf('{')).trim();
68 | const repeat = querySubjects.includes(query);
69 | if (repeat) {
70 | // CREATE AN ALIAS
71 | alias = query + '_' + i.toString();
72 | }
73 |
74 | querySubjects.push(repeat ? alias : query);
75 | myQuery +=
76 | (repeat ? alias + ' : ' : '') +
77 | arrItems[i].trim() +
78 | (i < arrItems.length - 1 ? ',\r\n' : '\r\n');
79 | }
80 | }
81 | }
82 |
83 | myQuery += '}';
84 |
85 | fetch(endpoint, {
86 | method: 'POST',
87 | headers: { 'Content-Type': 'application/json' },
88 | body: JSON.stringify({
89 | query: myQuery,
90 | }),
91 | })
92 | .then((res) => res.json())
93 | .then((data) => {
94 | if (data.errors) {
95 | setResults(data.errors);
96 | return;
97 | }
98 |
99 | // SET STATE - results
100 | setResults(data.data);
101 | setQuerySubjects(querySubjects);
102 | })
103 | .catch((err) => {
104 | console.log(err);
105 | });
106 | };
107 |
108 | function matchRecursiveRegExp(str, left, right) {
109 | const x = new RegExp(left + '|' + right, 'g');
110 | const l = new RegExp(left);
111 | let a = [];
112 | let t, s, m;
113 |
114 | t = 0;
115 |
116 | while ((m = x.exec(str))) {
117 | if (l.test(m[0])) {
118 | if (!t++) {
119 | s = x.lastIndex;
120 | }
121 | } else if (t) {
122 | if (!--t) {
123 | a.push(str.slice(s, m.index));
124 | }
125 | }
126 | }
127 | return a;
128 | }
129 |
130 | function handleBtnClick() {
131 | // passes value of input to props.handleBtnClick
132 | const inputValue = document.getElementById('endpoint-input').value;
133 | props.handleBtnClick(inputValue);
134 |
135 | let count = 6;
136 | let toggle = false;
137 | const color = inputValue ? '#a9d0c6' : 'red';
138 |
139 | const intervalID = setInterval(() => {
140 | const endpointIcon = document.getElementById('endpoint-input-icon');
141 | if (!toggle) endpointIcon.style.color = color;
142 | else endpointIcon.style.color = 'gray';
143 | toggle = !toggle;
144 | count--;
145 | if (count === 0) clearInterval(intervalID);
146 | }, 175);
147 | }
148 |
149 | // Sets keyboard shortcut for sending queries
150 | if (props.editor !== '') {
151 | const editor = props.editor;
152 | const keyMap = {
153 | 'Ctrl-Enter': handleClick,
154 | };
155 | editor.addKeyMap(keyMap);
156 | }
157 |
158 | // Sets enter shortcut for endpoint input
159 | function handleChange(e) {
160 | if (e.charCode === 13) handleBtnClick();
161 | }
162 | return (
163 |
164 | {/*
PractiQL */}
165 |
166 |
172 |
175 |
176 |
181 | ↻
182 |
183 |
190 |
191 |
192 | );
193 | }
194 |
--------------------------------------------------------------------------------
/client/components/Tree.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Treebeard, decorators } from 'react-treebeard';
3 | import autoQuery from '../../helpers/autoQuery';
4 | import customHeader from '../../helpers/customHeader';
5 | import customToggle from '../../helpers/customToggle';
6 |
7 |
8 | export default function TreeExample(props) {
9 | const { tree } = props;
10 | const [cursor, setCursor] = useState(false);
11 | const [data, setData] = useState(tree);
12 |
13 |
14 | decorators.Header = customHeader;
15 | decorators.Toggle = customToggle;
16 |
17 | const onToggle = (node, toggled) => {
18 | if (cursor) {
19 | cursor.active = false;
20 | }
21 |
22 | // Checks if clicked node in tree diagram is a scalar value.
23 | if (node.scalar) {
24 | // If true, generates query for input instance.
25 | const queryToAdd = autoQuery(node.autoQueryChain);
26 | props.handleAutoQuery(queryToAdd);
27 | }
28 |
29 | node.active = true;
30 | if (node.children) {
31 | node.toggled = toggled;
32 | }
33 | setCursor(node);
34 | setData(Object.assign({}, data));
35 | };
36 |
37 | if (tree.error)
38 | return (
39 | <>
40 | {tree.error}
41 | >
42 | );
43 | else return ;
44 | }
45 |
--------------------------------------------------------------------------------
/client/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from 'react-dom';
3 | import App from './App';
4 | import styles from './stylesheets/styles.scss';
5 |
6 | render(
7 | ,
8 | document.getElementById('root')
9 | );
10 |
--------------------------------------------------------------------------------
/client/stylesheets/CodeMirror-default-overwrites.scss:
--------------------------------------------------------------------------------
1 | .CodeMirror {
2 | // max-height: 150px;
3 | transition: height 185ms, max-height 185ms;
4 | color: #141823;
5 | font-family: 'Consolas', 'Inconsolata', 'Droid Sans Mono', 'Monaco', monospace;
6 | font-size: 13px;
7 | height: 100%;
8 | left: 0;
9 | position: absolute;
10 | top: 0;
11 | width: 100%;
12 | }
13 |
14 | .CodeMirror-overlayscroll-vertical div {
15 | background-color: rgba(65, 65, 65, 0.15);
16 | }
17 |
--------------------------------------------------------------------------------
/client/stylesheets/body.scss:
--------------------------------------------------------------------------------
1 | body {
2 | // font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
3 | // Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serifs;
4 | font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI',
5 | 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans',
6 | 'Helvetica Neue', sans-serif;
7 | margin: 0;
8 | // overflow: hidden;
9 | // background-color: $nord-primary;
10 | }
11 |
--------------------------------------------------------------------------------
/client/stylesheets/bottom-bar.scss:
--------------------------------------------------------------------------------
1 | .bottom-bar {
2 | color: gray;
3 | font-size: 0.95rem;
4 | background-color: $nord-black;
5 | height: 0;
6 | padding: 1rem 0.75rem 0rem 0.75rem;
7 | transition: padding 200ms;
8 | }
9 |
10 | .bottom-bar-options {
11 | margin-left: 3rem;
12 | }
13 |
14 | .bottom-bar-options:first-of-type {
15 | margin-left: -1.5rem;
16 | }
17 |
18 | .bottom-bar-schema {
19 | cursor: pointer;
20 | }
21 |
22 | .bottom-bar-schema:hover {
23 | color: $nord-green;
24 | }
25 |
26 | .bottom-bar-snapshot {
27 | cursor: pointer;
28 | }
29 |
30 | .bottom-bar-snapshot:hover {
31 | color: $nord-green;
32 | }
33 |
34 | .bottom-bar-toggle-icon-wrapper {
35 | display: inline-flex;
36 | height: 1.4rem;
37 | width: 1.3rem;
38 | padding: 0rem 0.3rem 0rem 0.3rem;
39 | color: gray;
40 | position: relative;
41 | // top should be negative bottom-bar padding and icon height, e.g. padding = .75rem, icon height = 1rem, then icon top should be -1.75rem
42 | top: -2.15rem;
43 | background-color: $nord-black;
44 | z-index: 4;
45 | justify-content: center;
46 | border-top-left-radius: 3px;
47 | border-top-right-radius: 3px;
48 | cursor: pointer;
49 | }
50 |
51 | .bottom-bar-toggle-icon {
52 | // icon is flipped upside down, so style accordingly
53 | transform: rotate(180deg);
54 | display: flex;
55 | align-items: center;
56 | font-size: 1.5rem;
57 | padding-bottom: 0.5rem;
58 | color: $nord-green;
59 | }
60 |
61 | .bottom-bar-unavailable {
62 | color: rgb(48, 48, 48);
63 | }
64 |
--------------------------------------------------------------------------------
/client/stylesheets/custom-colors.scss:
--------------------------------------------------------------------------------
1 | $secondary-color: rgba(135, 250, 231, 0.849);
2 | $nord-primary: #2e3440;
3 | $text-black: rgb(70, 70, 70);
4 | $nord-black: rgb(32, 32, 32);
5 | // $nord-green: hsl(164, 29%, 64%);
6 | $nord-green: #a9d0c6;
7 | $treebeard-blue: #21252b;
8 |
--------------------------------------------------------------------------------
/client/stylesheets/custom-themes/custom-nord.scss:
--------------------------------------------------------------------------------
1 | /* To use the complete custom nord theme:
2 | 1. import 'codemirror/theme/nord.css' into input and output components
3 | 2. Set theme option to 'nord' for each component
4 | 3. Import './custom-themes/custom-nord' at the end of styles.scss.
5 | */
6 | .top-bar--nord {
7 | background: none;
8 | background: linear-gradient($nord-primary 45%, $nord-black);
9 | border-bottom: 1px solid black;
10 | }
11 |
12 | .logo--nord {
13 | font-size: 1.5rem;
14 | color: $nord-green;
15 | }
16 |
17 | .output-container-outer--nord {
18 | border-left-color: $nord-black;
19 | background-color: $nord-primary;
20 | }
21 |
22 | .output-container-inner--nord {
23 | border-color: $nord-black;
24 | }
25 |
26 | .widget-btn--nord {
27 | color: rgba(15, 15, 15);
28 | background-color: rgba(185, 185, 185, 0.15);
29 | }
30 |
--------------------------------------------------------------------------------
/client/stylesheets/endpoint-input.scss:
--------------------------------------------------------------------------------
1 | .endpoint-input-wrapper {
2 | display: flex;
3 | align-items: center;
4 | width: 100%;
5 | border: 1px solid #202020;
6 | border-radius: 3px;
7 | }
8 |
9 | .endpoint-input {
10 | color: gray;
11 | background-color: #2e3440;
12 | font-size: 1.025rem;
13 | width: 100%;
14 | outline: none;
15 | padding: 0.3rem;
16 | border: 0;
17 | border-left: 1px solid #202020;
18 | border-radius: 3px;
19 | border-top-left-radius: 0;
20 | border-bottom-left-radius: 0;
21 | height: 100%;
22 | }
23 |
24 | .endpoint-input-icon {
25 | color: gray;
26 | font-size: 1.1rem;
27 | padding: 0 0.5rem;
28 | cursor: pointer;
29 | transition: color 200ms;
30 | }
31 |
32 | .endpoint-input-icon:hover {
33 | color: $nord-green;
34 | }
35 |
--------------------------------------------------------------------------------
/client/stylesheets/error-message.scss:
--------------------------------------------------------------------------------
1 | .error-message {
2 | color: rgb(206, 75, 75);
3 | }
4 |
--------------------------------------------------------------------------------
/client/stylesheets/get-schema-btn.scss:
--------------------------------------------------------------------------------
1 | .get-schema-btn {
2 | background-color: purple;
3 | outline: none;
4 | }
5 |
--------------------------------------------------------------------------------
/client/stylesheets/input-container.scss:
--------------------------------------------------------------------------------
1 | .input-container-wrapper {
2 | // border: 5px solid purple;
3 | display: -webkit-box;
4 | display: -ms-flexbox;
5 | display: flex;
6 | -webkit-box-orient: vertical;
7 | -webkit-box-direction: normal;
8 | -ms-flex-direction: column;
9 | flex-direction: column;
10 | -webkit-box-flex: 1;
11 | -ms-flex: 1;
12 | flex: 1 1 0%;
13 | }
14 |
15 | .input-container-outer {
16 | display: -webkit-box;
17 | display: -ms-flexbox;
18 | display: flex;
19 | -webkit-box-orient: vertical;
20 | -webkit-box-direction: normal;
21 | -ms-flex-direction: column;
22 | flex-direction: column;
23 | -webkit-box-flex: 1;
24 | -ms-flex: 1;
25 | flex: 1 1 0%;
26 | // border: 5px solid brown;
27 | }
28 |
29 | .input-container-inner {
30 | -webkit-box-flex: 1;
31 | -ms-flex: 1;
32 | flex: 1;
33 | position: relative;
34 | // background-color: $nord-primary;
35 | // border: 5px solid pink;
36 | // overflow-y: hidden;
37 | }
38 |
--------------------------------------------------------------------------------
/client/stylesheets/input-instance.scss:
--------------------------------------------------------------------------------
1 | .input-instance {
2 | // box-sizing: border-box;
3 | // border: 5px solid purple;
4 | // height: 85vh;
5 | // max-height: 85vh;
6 | // align-self: stretch;
7 | // overflow-y: auto;
8 | }
9 |
--------------------------------------------------------------------------------
/client/stylesheets/io-container.scss:
--------------------------------------------------------------------------------
1 | .io-container {
2 | // flex: 1;
3 | display: -webkit-box;
4 | display: -ms-flexbox;
5 | display: flex;
6 | -webkit-box-orient: horizontal;
7 | -webkit-box-direction: normal;
8 | -ms-flex-direction: row;
9 | flex-direction: row;
10 | -webkit-box-flex: 1;
11 | -ms-flex: 1;
12 | flex: 1;
13 | // border: 5px solid orange;
14 | }
15 |
--------------------------------------------------------------------------------
/client/stylesheets/logo.scss:
--------------------------------------------------------------------------------
1 | .logo {
2 | color: $text-black;
3 | margin-left: 0.5rem;
4 | }
5 |
--------------------------------------------------------------------------------
/client/stylesheets/main-container.scss:
--------------------------------------------------------------------------------
1 | .main-container {
2 | display: -webkit-box;
3 | display: -ms-flexbox;
4 | display: flex;
5 | -webkit-box-orient: horizontal;
6 | -webkit-box-direction: normal;
7 | -ms-flex-direction: row;
8 | flex-direction: row;
9 | height: 100%;
10 | margin: 0;
11 | overflow: hidden;
12 | width: 100%;
13 | // border: 5px solid green;
14 | }
15 |
--------------------------------------------------------------------------------
/client/stylesheets/output-container.scss:
--------------------------------------------------------------------------------
1 | .output-container-outer {
2 | border-left: 0.1rem solid rgb(238, 238, 238);
3 | // flex-grow: 1;
4 | // max-width: 50%;
5 | display: -webkit-box;
6 | display: -ms-flexbox;
7 | display: flex;
8 | -webkit-box-orient: vertical;
9 | -webkit-box-direction: normal;
10 | -ms-flex-direction: column;
11 | flex-direction: column;
12 | -webkit-box-flex: 1;
13 | -ms-flex: 1;
14 | flex: 1;
15 | position: relative;
16 | }
17 |
18 | .output-container-inner {
19 | border: 1px solid rgb(235, 235, 235);
20 | // overflow-y: hidden;
21 | -webkit-box-flex: 1;
22 | -ms-flex: 1;
23 | flex: 1;
24 | height: 100%;
25 | position: relative;
26 | }
27 |
28 | .output-container-inner:first-of-type {
29 | border-top: 0;
30 | }
31 |
--------------------------------------------------------------------------------
/client/stylesheets/send-btn.scss:
--------------------------------------------------------------------------------
1 | .send-btn {
2 | cursor: pointer;
3 | color: $nord-green;
4 | padding: 0.3rem;
5 | width: 5rem;
6 | margin: 0.5rem;
7 | border: 1px solid #202020;
8 | border-radius: 3px;
9 | font-size: 1.025rem;
10 | background-color: rgba(0, 0, 0, 0.03);
11 | transition: font-size 200ms, background-color 200ms;
12 | }
13 |
14 | .send-btn:hover {
15 | color: lighten($nord-green, 10%);
16 | background-color: lighten(rgba(212, 212, 212, 0.08), 45%);
17 | }
18 |
19 | .send-btn:active {
20 | color: black;
21 | background-color: darken(rgba(212, 212, 212, 0.06), 50%);
22 | font-size: 1rem;
23 | box-shadow: 0;
24 | }
25 |
26 | .send-btn:focus {
27 | outline: none;
28 | }
29 |
--------------------------------------------------------------------------------
/client/stylesheets/styles.scss:
--------------------------------------------------------------------------------
1 | @import 'custom-colors';
2 | @import 'bottom-bar';
3 | @import 'body';
4 | @import 'CodeMirror-default-overwrites';
5 | @import 'endpoint-input';
6 | @import 'top-bar';
7 | @import 'main-container';
8 | @import 'io-container';
9 | @import 'input-container';
10 | @import 'output-container';
11 | @import 'input-instance';
12 | @import 'send-btn';
13 | @import 'widget-btn';
14 | @import 'logo';
15 | @import 'get-schema-btn';
16 | @import 'error-message';
17 | @import './custom-themes/custom-nord';
18 | @import 'tree';
19 |
20 | body,
21 | html {
22 | margin: 0;
23 | }
24 |
25 | #root {
26 | height: 100vh;
27 | }
28 |
29 | .content-wrap {
30 | display: -webkit-box;
31 | display: -ms-flexbox;
32 | display: flex;
33 | -webkit-box-orient: vertical;
34 | -webkit-box-direction: normal;
35 | -ms-flex-direction: column;
36 | flex-direction: column;
37 | -webkit-box-flex: 1;
38 | -ms-flex: 1;
39 | flex: 1;
40 | }
41 |
42 | .rd3t-label {
43 | }
44 |
45 | tspan {
46 | color: red !important;
47 | }
48 |
49 | text {
50 | max-width: 10px;
51 | }
52 |
53 | #hidden {
54 | display: none;
55 | }
56 |
--------------------------------------------------------------------------------
/client/stylesheets/top-bar.scss:
--------------------------------------------------------------------------------
1 | .top-bar-wrap {
2 | display: -webkit-box;
3 | display: -ms-flexbox;
4 | display: flex;
5 | -webkit-box-orient: horizontal;
6 | -webkit-box-direction: normal;
7 | -ms-flex-direction: row;
8 | flex-direction: row;
9 | }
10 |
11 | .top-bar {
12 | // flex-grow: 1;
13 | // box-sizing: border-box;
14 | // min-width: 100%;
15 | // background: linear-gradient(rgb(204, 204, 204), rgb(238, 238, 238));
16 | // border-bottom: 1px solid rgb(217, 217, 217);
17 | // border: 5px solid aqua;
18 | -webkit-box-align: center;
19 | -ms-flex-align: center;
20 | align-items: center;
21 | background: -webkit-linear-gradient(#f7f7f7, #e2e2e2);
22 | background: linear-gradient(#f7f7f7, #e2e2e2);
23 | border-bottom: 1px solid #d0d0d0;
24 | cursor: default;
25 | display: -webkit-box;
26 | display: -ms-flexbox;
27 | display: flex;
28 | -webkit-box-orient: horizontal;
29 | -webkit-box-direction: normal;
30 | -ms-flex-direction: row;
31 | flex-direction: row;
32 | -webkit-box-flex: 1;
33 | -ms-flex: 1;
34 | flex: 1;
35 | height: 34px;
36 | padding: 7px 14px 6px;
37 | -webkit-user-select: none;
38 | -moz-user-select: none;
39 | -ms-user-select: none;
40 | user-select: none;
41 | }
42 |
--------------------------------------------------------------------------------
/client/stylesheets/tree.scss:
--------------------------------------------------------------------------------
1 | .outer-tree-wrap {
2 | height: 100%;
3 | transition: width 350ms, padding 350ms;
4 | background-color: $treebeard-blue;
5 | // background-color: $nord-black;
6 | padding: 0.5rem;
7 | width: 20rem;
8 | }
9 |
10 | .inner-tree-wrap {
11 | width: 100%;
12 | height: 100%;
13 | position: relative;
14 | }
15 |
16 | .close-tree-wrapper {
17 | background-color: #21252b;
18 | // background-color: $nord-black;
19 | text-align: end;
20 | }
21 |
22 | .sidebar-schema,
23 | .sidebar-schema-description {
24 | font-size: 2rem;
25 | color: lightgray;
26 | text-align: start;
27 | border-bottom: 1px solid gray;
28 | margin-bottom: 0.5rem;
29 | }
30 |
31 | .sidebar-schema-description {
32 | font-size: 0.9rem;
33 | font-weight: lighter;
34 | margin-bottom: 1rem;
35 | }
36 |
37 | .close-tree-btn {
38 | font-size: 0.95rem;
39 | line-height: 1;
40 | color: #9da5ab;
41 | cursor: pointer;
42 | padding-right: 0.5rem;
43 | }
44 |
45 | .css-f91fgu {
46 | height: 100%;
47 | left: 0;
48 | position: absolute;
49 | top: 0;
50 | width: 100%;
51 | overflow: hidden;
52 | overflow-y: auto;
53 | }
54 |
55 | .css-f91fgu ul,
56 | .css-f91fgu li {
57 | background-color: $treebeard-blue;
58 | }
59 |
--------------------------------------------------------------------------------
/client/stylesheets/widget-btn.scss:
--------------------------------------------------------------------------------
1 | .widget-btn {
2 | position: absolute;
3 | color: rgba(65, 65, 65, 0.4);
4 | right: 1.1rem;
5 | margin-top: 0.3rem;
6 | z-index: 100;
7 | background-color: rgba(75, 75, 75, 0.065);
8 | border-radius: 0.3rem;
9 | border: 0;
10 | outline: none;
11 | cursor: pointer;
12 | transition: all 235ms;
13 | }
14 |
15 | .widget-btn:hover {
16 | color: darken(rgb(100, 100, 100), 45%);
17 | background-color: rgba(135, 250, 231, 0.849);
18 | }
19 |
--------------------------------------------------------------------------------
/helpers/autoQuery.js:
--------------------------------------------------------------------------------
1 | function autoQuery(autoQueryChain) {
2 | // Uses autoQueryChain array to generate a query for input instance. Returns a string.
3 | if (!Array.isArray(autoQueryChain)) {
4 | console.log(new Error('Must pass an array to autoQuery'));
5 | return '';
6 | }
7 | let spaceCount = 2;
8 | const query = autoQueryChain.reduce((acc, el, index) => {
9 | let spaces = ' '.repeat(spaceCount);
10 | acc += `{
11 | ${spaces}${el}`;
12 |
13 | if (index === autoQueryChain.length - 1) {
14 | for (let i = 0; i <= index; i++) {
15 | spaces = ' '.repeat(spaceCount - 2);
16 | acc += `
17 | ${spaces}}`;
18 | spaceCount -= 2;
19 | }
20 | }
21 |
22 | spaceCount += 2;
23 | return acc;
24 | }, '');
25 | return query;
26 | }
27 |
28 | module.exports = autoQuery;
29 |
--------------------------------------------------------------------------------
/helpers/createTree.js:
--------------------------------------------------------------------------------
1 | function createTree(schema) {
2 | // createTree() takes a GraphQL schema object and creates a tree for a Treebeard component
3 |
4 | // creates custom error object and error message
5 | const errorObject = { error: 'Sorry, something went wrong' };
6 | const createTreeError = new Error('Schema not valid object');
7 |
8 | // Validates schema argument
9 | if (
10 | !(schema !== null && typeof schema === 'object') ||
11 | !schema._queryType ||
12 | !schema._queryType.name ||
13 | !(typeof schema._queryType.name !== 'String') ||
14 | !schema._queryType._fields
15 | ) {
16 | console.log(createTreeError);
17 | return errorObject;
18 | }
19 |
20 | if(!schema) return {name: 'no schema found'};
21 |
22 | const myTree = [
23 | {
24 | name: schema._queryType.name,
25 | children: [],
26 | type: schema._queryType
27 | }
28 | ];
29 |
30 |
31 | if(schema._mutationType) {
32 | myTree.push({name: schema._mutationType.name, children: [], type: schema._mutationType});
33 | }
34 |
35 | myTree.forEach(child => {
36 | createTopLevel(child);
37 | })
38 |
39 |
40 | function createTopLevel(topChild) {
41 | mainFields = Object.values(topChild.type._fields);
42 | mainFields.forEach((field) => {
43 | const typeDef = {};
44 | const attributes = {};
45 | const children = [];
46 |
47 | for (let i = 0; i < field.args.length; i++) {
48 | attributes[field.args[i].name] = findType(field.args[i].type).name;
49 | }
50 |
51 | const innerChildren = Object.values(findSubFields(field.type));
52 |
53 | for (let i = 0; i < innerChildren.length; i++) {
54 | children.push(getChildren(innerChildren[i], field.name));
55 | }
56 |
57 | typeDef.name = field.name;
58 | typeDef.autoQueryChain = [field.name];
59 | typeDef.attributes = attributes;
60 | typeDef.children = children;
61 |
62 | topChild.children.push(typeDef);
63 | });
64 | }
65 |
66 | function findType(type) {
67 | if (type.name) return { name: type.name, description: type.description };
68 | return findType(type.ofType);
69 | }
70 |
71 | function findSubFields(type) {
72 | if (type.name) return type._fields;
73 | return findSubFields(type.ofType);
74 | }
75 |
76 | function getChildren(child, parentName) {
77 | const typeDef = {};
78 | const attributes = {};
79 | const children = [];
80 |
81 | for (let i = 0; i < child.args.length; i++) {
82 | attributes[child.args[i].name] = findType(child.args[i].type).name;
83 | }
84 |
85 | const innerChild = findType(child.type);
86 | children.push({
87 | name: innerChild.name,
88 | autoQueryChain: [parentName, child.name],
89 | scalar: true,
90 | attributes: { description: innerChild.description },
91 | });
92 |
93 | typeDef.name = child.name;
94 | typeDef.autoQueryChain = [parentName, child.name];
95 | typeDef.attributes = attributes;
96 | typeDef.children = children;
97 |
98 | return typeDef;
99 | }
100 | return myTree;
101 | }
102 |
103 | module.exports = createTree;
104 |
--------------------------------------------------------------------------------
/helpers/customHeader.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | const customHeader = (props) => {
4 | const [nodeStyle, setNodeStyle] = useState({ base: props.style.base });
5 | const [hover, setHover] = useState(false);
6 | const [position, setPosition] = useState('absolute')
7 |
8 | let argString = ''
9 | const args = props.node.attributes ? Object.entries(props.node.attributes) : '';
10 | if(typeof args === 'object') {
11 | args.forEach(arg => {
12 | if(arg[0] === 'description') argString = arg[1];
13 | else argString += `(${arg[0]} : ${arg[1]})`;
14 | })
15 | }
16 |
17 | const onMouseOver = node => {
18 | if (node) {
19 | setNodeStyle(() => ({
20 | base: { ...props.style.base, ...{ color: "#a8cfc5" } }
21 | }));
22 | // setHover(true);
23 | Object.keys(node.attributes).length === 0 ? setHover(false) : setHover(true);
24 | if(node.attributes.description) setPosition('relative');
25 | }
26 | };
27 |
28 | const onMouseLeave = node => {
29 | if (node) {
30 | setNodeStyle(() => ({ base: props.style.base }));
31 | setHover(false);
32 | }
33 | };
34 |
35 | return (
36 |
37 |
onMouseOver(props.node)} onMouseLeave={() => onMouseLeave(props.node)}>
38 | {`${props.node.name} `}
39 |
40 |
41 | )
42 | }
43 |
44 | export default customHeader;
--------------------------------------------------------------------------------
/helpers/customToggle.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const customToggle = (props) => {
4 | return (
5 |
8 | )
9 | }
10 |
11 | export default customToggle;
--------------------------------------------------------------------------------
/helpers/validateObject.js:
--------------------------------------------------------------------------------
1 | function validateObject(value) {
2 | // validates value is object. To be used with createTree.js and any other function needing an object
3 | return (
4 | !Array.isArray(value) &&
5 | !(value instanceof Map) &&
6 | !(value instanceof Set) &&
7 | value !== null &&
8 | typeof value === 'object'
9 | );
10 | }
11 |
12 | module.exports = validateObject;
13 |
--------------------------------------------------------------------------------
/images/Logo-Dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/PractiQL/28821749619376cabc79be2d530df6039ce6300b/images/Logo-Dark.png
--------------------------------------------------------------------------------
/images/PractiQL-local-storage.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/PractiQL/28821749619376cabc79be2d530df6039ce6300b/images/PractiQL-local-storage.gif
--------------------------------------------------------------------------------
/images/PractiQL-logodark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/PractiQL/28821749619376cabc79be2d530df6039ce6300b/images/PractiQL-logodark.png
--------------------------------------------------------------------------------
/images/PractiQL-logolite.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/PractiQL/28821749619376cabc79be2d530df6039ce6300b/images/PractiQL-logolite.png
--------------------------------------------------------------------------------
/images/PractiQL-mq1.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/PractiQL/28821749619376cabc79be2d530df6039ce6300b/images/PractiQL-mq1.gif
--------------------------------------------------------------------------------
/images/PractiQL-mq2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/PractiQL/28821749619376cabc79be2d530df6039ce6300b/images/PractiQL-mq2.gif
--------------------------------------------------------------------------------
/images/PractiQL-schemaTree.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/PractiQL/28821749619376cabc79be2d530df6039ce6300b/images/PractiQL-schemaTree.gif
--------------------------------------------------------------------------------
/images/logo-lite.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/PractiQL/28821749619376cabc79be2d530df6039ce6300b/images/logo-lite.png
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | PractiQL
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "practiql",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "webpack.config.js",
6 | "scripts": {
7 | "build": "NODE_ENV=production webpack",
8 | "dev": "NODE_ENV=development webpack serve --open & nodemon server/server.js",
9 | "start": "node server/server.js",
10 | "test": "jest",
11 | "test-vs": "jest --verbose --silent",
12 | "windows": "concurrently \"cross-env NODE_ENV=development nodemon server/server.js\" \"NODE_ENV=development webpack serve --open\""
13 | },
14 | "jest": {
15 | "setupFiles": [
16 | "/__tests__/__config__/jest.setup.js"
17 | ],
18 | "testPathIgnorePatterns": [
19 | "node_modules",
20 | "/__tests__/__config__/"
21 | ]
22 | },
23 | "repository": {
24 | "type": "git",
25 | "url": "git+https://github.com/oslabs-beta/PractiQL.git"
26 | },
27 | "keywords": [],
28 | "author": "",
29 | "license": "ISC",
30 | "bugs": {
31 | "url": "https://github.com/oslabs-beta/PractiQL/issues"
32 | },
33 | "homepage": "https://github.com/oslabs-beta/PractiQL#readme",
34 | "dependencies": {
35 | "codemirror": "^5.60.0",
36 | "codemirror-graphql": "^1.0.0",
37 | "express": "^4.17.1",
38 | "graphql": "^15.5.0",
39 | "react": "^17.0.1",
40 | "react-codemirror2": "^7.2.1",
41 | "react-d3-tree": "^2.0.1",
42 | "react-dom": "^17.0.1",
43 | "react-treebeard": "^3.2.4"
44 | },
45 | "devDependencies": {
46 | "@babel/core": "^7.13.8",
47 | "@babel/preset-env": "^7.13.9",
48 | "@babel/preset-react": "^7.12.13",
49 | "babel-jest": "^26.6.3",
50 | "babel-loader": "^8.2.2",
51 | "css-loader": "^5.2.0",
52 | "enzyme": "^3.11.0",
53 | "enzyme-adapter-react-16": "^1.15.6",
54 | "jest": "^26.6.3",
55 | "nodemon": "^2.0.7",
56 | "sass": "^1.32.8",
57 | "sass-loader": "^11.0.1",
58 | "style-loader": "^2.0.0",
59 | "webpack": "^5.24.3",
60 | "webpack-cli": "^4.5.0",
61 | "webpack-dev-server": "^3.11.2"
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/server/server.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const path = require('path');
3 | const app = express();
4 |
5 | app.use(express.json());
6 |
7 | app.use('/build', express.static(path.resolve(__dirname, '../build')));
8 |
9 | app.get('/', (req, res) => {
10 | return res.status(200).sendFile(path.resolve(__dirname, '../index.html'));
11 | });
12 |
13 | app.listen(3000, () => {
14 | console.log('Server listening on port 3000');
15 | });
16 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | mode: process.env.NODE_ENV,
5 | entry: './client/index.js',
6 | output: {
7 | path: path.resolve(__dirname, 'build'),
8 | filename: 'bundle.js',
9 | },
10 | module: {
11 | rules: [
12 | {
13 | test: /.(jsx|js)$/,
14 | exclude: /(node_modules)/,
15 | use: {
16 | loader: 'babel-loader',
17 | options: {
18 | presets: ['@babel/preset-env', '@babel/preset-react'],
19 | },
20 | },
21 | },
22 | {
23 | test: /\.(sa|sc|c)ss$/,
24 | use: ['style-loader', 'css-loader', 'sass-loader'],
25 | },
26 | ],
27 | },
28 | devtool: 'eval-source-map',
29 | devServer: {
30 | publicPath: '/build/',
31 | proxy: {
32 | '/': 'http://localhost:3000',
33 | },
34 | },
35 | resolve: {
36 | extensions: ['.js', '.jsx'],
37 | },
38 | };
39 |
--------------------------------------------------------------------------------