├── .babelrc ├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── .npmignore ├── Contribution.md ├── LICENSE ├── README.md ├── main.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── server ├── plugins │ └── tracker.js ├── server.js └── utils │ ├── injectPaths.js │ └── updatePackage.js ├── src ├── assets │ ├── bypass.gif │ ├── history.png │ ├── metrics2.png │ ├── performance.png │ ├── q-logo-blk.png │ ├── q-logo.png │ ├── qleo-lion.png │ ├── qleo-logo.png │ └── schema.png ├── components │ ├── App.jsx │ ├── FileUpload.jsx │ ├── GraphData.jsx │ ├── HistoryItem.jsx │ ├── Navbar.jsx │ ├── QueryInput.jsx │ ├── ResolversPerformance.jsx │ ├── Schema.jsx │ ├── deprecated │ │ └── deprecated.jsx │ ├── resolverDetailsComponent │ │ ├── Dropdown.jsx │ │ └── ResolverDetails.jsx │ └── schemaComponents │ │ ├── FieldItem.jsx │ │ └── Root.jsx ├── containers │ ├── HistoryContainer.jsx │ ├── MainContainer.jsx │ ├── PerformanceContainer.jsx │ └── UploadContainer.jsx ├── css │ └── App.css ├── index.jsx └── utils │ └── links.js ├── tailwind.config.js ├── webpack.build.config.js └── webpack.dev.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-react", 4 | "@babel/preset-env" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = crlf 8 | indent_size = 2 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["**/test", "**/__tests__"], 4 | "env": { 5 | "browser": true, 6 | "node": true, 7 | "es2021": true 8 | }, 9 | "plugins": ["import", "react", "jsx-a11y"], 10 | "extends": [ 11 | "eslint:recommended", 12 | "plugin:import/errors", 13 | "plugin:react/recommended", 14 | "plugin:jsx-a11y/recommended" 15 | ], 16 | "parserOptions": { 17 | "ecmaFeatures": { 18 | "jsx": true 19 | }, 20 | "sourceType": "module" 21 | }, 22 | "rules": { 23 | "indent": ["warn", 2, { "SwitchCase": 1 }], 24 | "no-unused-vars": ["off", { "vars": "local" }], 25 | "prefer-const": "warn", 26 | "quotes": ["warn", "single"], 27 | "semi": ["warn", "always"], 28 | "space-infix-ops": "warn", 29 | "react/prefer-stateless-function": "off", 30 | "react/prop-types": "off", 31 | "react/jsx-key": "warn" 32 | }, 33 | "settings": { 34 | "react": { 35 | "version": "detect" 36 | }, 37 | "import/resolver": { 38 | "node": { 39 | "paths": ["./src"], 40 | "extensions": [".js", ".jsx"] 41 | }, 42 | "webpack": "webpack.dev.config.js" 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build folder and files # 2 | ########################## 3 | builds/ 4 | 5 | # Development folders and files # 6 | ################################# 7 | .tmp/ 8 | dist/ 9 | node_modules/ 10 | *.compiled.* 11 | 12 | # Folder config file # 13 | ###################### 14 | Desktop.ini 15 | 16 | # Folder notes # 17 | ################ 18 | _ignore/ 19 | 20 | # Log files & folders # 21 | ####################### 22 | logs/ 23 | *.log 24 | npm-debug.log* 25 | .npm 26 | 27 | # Packages # 28 | ############ 29 | # it's better to unpack these files and commit the raw source 30 | # git has its own built in compression methods 31 | *.7z 32 | *.dmg 33 | *.gz 34 | *.iso 35 | *.jar 36 | *.rar 37 | *.tar 38 | *.zip 39 | 40 | # Photoshop & Illustrator files # 41 | ################################# 42 | *.ai 43 | *.eps 44 | *.psd 45 | 46 | # Windows & Mac file caches # 47 | ############################# 48 | .DS_Store 49 | Thumbs.db 50 | ehthumbs.db 51 | 52 | # Windows shortcuts # 53 | ##################### 54 | *.lnk 55 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | _ignore/ 2 | docs/ 3 | builds/ 4 | dist/ 5 | .editorconfig 6 | code-of-conduct.md 7 | -------------------------------------------------------------------------------- /Contribution.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | Thank you for your interest and passion in contributing to QLeo. 4 | 5 |
6 | 7 | ## Reporting Bugs 8 | 9 | Bugs are tracked as GitHub issues. Create an issue on our repository and provide the following information: 10 | 11 | - Please provide a clear and descriptive title for your issue. 12 | - Describe the steps you took to handle the problem. If possible, provide specific code or screenshots of the problem. 13 | - Describe the expected behavior and why. 14 | - Details about your configuration and environment. 15 | 16 |
17 | 18 | ## Versioning: 19 | 20 | Any improvements are tracked as Pull Requests. Create a pull request on our repository following the guidelines: 21 | 22 | - Fork QLeo’s repository and create a new branch from Main. 23 | - Make your changes in the new branch. 24 | - Run your code through our linter and ensure it passes. 25 | - Issue a pull request! 26 | 27 | ## License 28 | 29 | By contribution, you agree that your contributions will be licensed under MIT License. 30 | 31 |
32 | 33 | ## Thank You 34 | 35 | Our team will be working closely together to continue to improve QLeo and are willing to receive any feedback and assistance :) 36 | 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 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 | QLeo 5 |

6 | 7 |

A Visual Performance Tracking Tool for GraphQL

8 | 9 |
10 | 11 | ## What is QLeo? 12 | 13 | QLeo is designed to be used in a development environment. We provide a simple and intuitive interface with graphical representations of key metrics. This allows developers to debug and tune performance for inefficient GraphQL requests. 14 | 15 | QLeo is in active development. Please follow to the end of the repository for contribution guidelines. 16 | 17 |
18 | 19 | ## Features 20 | 21 | - Easy Configuration: With just a click of a button, QLeo will start to run and allow users to generate performance metrics according to the uploaded schema model. 22 | - Montior Queries at Resolver-Level: Understand and track GraphQL queries, mutations, response times, and function invocations for individual resolvers. 23 | - Visualize: Compare and reveal potential performance bottlenecks utilizing QLeo's illustration to enhance the efficiency of GraphQL API calls. 24 | - History: Keep record of GraphQL request's performance metrics and graphs in the current session. 25 | 26 | 27 | 28 | 29 |
30 | 31 | ## Installation 32 | 33 | QLeo runs on Electron as a native desktop application. 34 | The instructions for MacOS users (Intel & M1) are as follows: 35 | 36 | 1. Visit the QLeo Official [Website](https://qleo.app) or click [here](https://github.com//oslabs-beta/QLeo/releases/download/v1.0.0/QLeo.zip) to download! 37 | 2. Open the application and give access permissions. 38 | 39 | 40 | 41 | 3. And that’s it! QLeo is all set and ready to start collecting performance data for any GraphQL requests. 42 | 43 |
44 | 45 | ## How to Use 46 | 47 | 1. Open application and upload a schema file. 48 | 2. Navigate to the Dashboard tab - where a schema model, a code editor, and a Metrics directory will be displayed. 49 | 3. The schema model and types will be available to view on the left panel for easy reference. 50 | 51 | 52 | 53 | 4. Write up a GraphQL query and/or mutation in QLeo’s code editor, then press 'Submit'. 54 | 5. QLeo will start to gather and display the data on the Metrics directory on the right panel. 55 | - It will display the total query response time. 56 | - It will also display a breakdown of resolvers: 57 | - Toggle the 'Show Details' to view corresponding performance and metrics. 58 |
59 | 60 |
61 | 62 | 6. Navigate to the Performance tab - View your resolver's performance with graphical representation. 63 |
64 | 65 | 7. All previous requests will be saved in the History tab - Refer back to any of the request's performance metrics by simply selecting the corresponding query that was made. 66 | 67 | 68 | 69 | 70 | 71 | ## Technologies Involved 72 | 73 | - Electron 74 | - Javascript ES6+ 75 | - Apollo GraphQL 76 | - Node.js 77 | - React 78 | - Webpack 79 | - Tailwind CSS 80 | - CodeMirror Code Editor 81 |
82 | 83 | ## Contributing 84 | 85 | Interested in making a contribution to QLeo? Click [here](https://github.com/oslabs-beta/QLeo/blob/main/Contribution.md) for our open-source contribution guidelines. 86 | 87 |
88 | 89 | Visit our [website](https://qleo.app) for more information. 90 | 91 |
92 | 93 | ## Our Team 94 | 95 | * Andrew Talle [Github](https://github.com/ogAndrew) || [LinkedIn](https://www.linkedin.com/in/andrewtalle/) 96 | * Chon Hou Ho [Github](https://github.com/chon-h) || [LinkedIn](https://www.linkedin.com/in/chon-hou-ho/) 97 | * Irine Kang [Github](https://github.com/irinekangg) || [LinkedIn](https://www.linkedin.com/in/irinekang/) 98 | * Jack Fitzgerald [Github](https://github.com/jcf7) || [LinkedIn](https://www.linkedin.com/in/jcf7/) 99 | 100 |
101 | 102 | 103 | ## Contact 104 | 105 | 106 | Email: TeamQLeo@gmail.com 107 | 108 | Twitter: [@QLeo.App](https://twitter.com/QLeo.App) 109 | 110 | Website: [qleo.app](https://qleo.app) 111 | 112 | GitHub: [https://github.com/oslabs-beta/QLeo/](https://github.com/oslabs-beta/QLeo) 113 | 114 |
115 | 116 | 117 | ## License 118 | 119 | Distributed under the MIT License. See [`LICENSE`](https://github.com/oslabs-beta/QLeo/blob/main/LICENSE) for more information. 120 | 121 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { app, BrowserWindow } = require('electron'); 4 | const path = require('path'); 5 | const url = require('url'); 6 | const { exec } = require('child_process'); 7 | const { Server } = require('http'); 8 | const server = require('./server/server'); 9 | 10 | let mainWindow; 11 | 12 | // Keep a reference for dev mode 13 | let dev = false; 14 | 15 | // Broken: 16 | // if (process.defaultApp || /[\\/]electron-prebuilt[\\/]/.test(process.execPath) || /[\\/]electron[\\/]/.test(process.execPath)) { 17 | // dev = true 18 | // } 19 | 20 | if (process.env.NODE_ENV !== undefined && process.env.NODE_ENV === 'development') { 21 | dev = true; 22 | } 23 | 24 | // Temporary fix broken high-dpi scale factor on Windows (125% scaling) 25 | // info: https://github.com/electron/electron/issues/9691 26 | if (process.platform === 'win32') { 27 | app.commandLine.appendSwitch('high-dpi-support', 'true'); 28 | app.commandLine.appendSwitch('force-device-scale-factor', '1'); 29 | } 30 | 31 | function createWindow() { 32 | // Create the browser window. 33 | mainWindow = new BrowserWindow({ 34 | width: 1024, 35 | height: 768, 36 | show: false, 37 | webPreferences: { 38 | nodeIntegration: true, 39 | contextIsolation: false 40 | } 41 | }); 42 | 43 | // and load the index.html of the app. 44 | let indexPath; 45 | 46 | if (dev && process.argv.indexOf('--noDevServer') === -1) { 47 | indexPath = url.format({ 48 | protocol: 'http:', 49 | host: 'localhost:8080', 50 | pathname: 'index.html', 51 | slashes: true 52 | }); 53 | } else { 54 | indexPath = url.format({ 55 | protocol: 'file:', 56 | pathname: path.join(__dirname, 'dist', 'index.html'), 57 | slashes: true 58 | }); 59 | } 60 | 61 | mainWindow.loadURL(indexPath); 62 | 63 | // Don't show until we are ready and loaded 64 | mainWindow.once('ready-to-show', () => { 65 | mainWindow.show(); 66 | 67 | // Open the DevTools automatically if developing 68 | if (dev) { 69 | const { default: installExtension, REACT_DEVELOPER_TOOLS } = require('electron-devtools-installer'); 70 | 71 | installExtension(REACT_DEVELOPER_TOOLS) 72 | .catch(err => console.log('Error loading React DevTools: ', err)); 73 | mainWindow.webContents.openDevTools(); 74 | } 75 | }); 76 | 77 | 78 | // Emitted when the window is closed. 79 | mainWindow.on('closed', function() { 80 | // Dereference the window object, usually you would store windows 81 | // in an array if your app supports multi windows, this is the time 82 | // when you should delete the corresponding element. 83 | mainWindow = null; 84 | }); 85 | } 86 | 87 | // This method will be called when Electron has finished 88 | // initialization and is ready to create browser windows. 89 | // Some APIs can only be used after this event occurs. 90 | app.on('ready', ()=>{ 91 | createWindow(); 92 | }); 93 | 94 | // Quit when all windows are closed. 95 | app.on('window-all-closed', () => { 96 | // On macOS it is common for applications and their menu bar 97 | // to stay active until the user quits explicitly with Cmd + Q 98 | if (process.platform !== 'darwin') { 99 | app.quit(); 100 | server.close(); 101 | } 102 | }); 103 | 104 | app.on('activate', () => { 105 | // On macOS it's common to re-create a window in the app when the 106 | // dock icon is clicked and there are no other windows open. 107 | if (mainWindow === null) { 108 | createWindow(); 109 | } 110 | }); 111 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qleo", 3 | "productName": "QLeo", 4 | "version": "1.0.0", 5 | "description": "Electron app for QLeo graphQL performance tool", 6 | "main": "main.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/oslabs-beta/QLeo" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "bugs": { 14 | "url": "https://github.com/oslabs-beta/QLeo/issues" 15 | }, 16 | "homepage": "https://github.com/oslabs-beta/QLeo", 17 | "keywords": [ 18 | "app", 19 | "electron", 20 | "open", 21 | "open-source", 22 | "react", 23 | "reactjs", 24 | "webpack" 25 | ], 26 | "engines": { 27 | "node": ">=9.0.0", 28 | "npm": ">=5.0.0" 29 | }, 30 | "browserslist": [ 31 | "last 4 versions" 32 | ], 33 | "scripts": { 34 | "prod": "cross-env NODE_ENV=production webpack --mode production --config webpack.build.config.js && electron --noDevServer .", 35 | "start": "cross-env NODE_ENV=development webpack serve --hot --host 0.0.0.0 --config=./webpack.dev.config.js --mode development", 36 | "build": "cross-env NODE_ENV=production webpack --config webpack.build.config.js --mode production", 37 | "package": "npm run build", 38 | "postpackage": "electron-packager ./ --out=./builds", 39 | "lint": "eslint --fix --ext .js,.jsx .", 40 | "server": "nodemon server/server.js", 41 | "package-linux": "electron-packager . qleo --overwrite --asar=true --platform=linux --arch=x64 --prune=true --out=release-builds", 42 | "package-win": "electron-packager . --overwrite --platform=win32 --arch=ia32 --prune=false --out=release-builds --version-string.CompanyName=CE --version-string.FileDescription=CE --version-string.ProductName='QLeo'" 43 | }, 44 | "dependencies": { 45 | "@apollo/client": "^3.5.8", 46 | "@codemirror/theme-one-dark": "^0.19.1", 47 | "@fortawesome/fontawesome-svg-core": "^1.3.0", 48 | "@fortawesome/free-solid-svg-icons": "^6.0.0", 49 | "@fortawesome/react-fontawesome": "^0.1.17", 50 | "@uiw/react-codemirror": "^4.3.3", 51 | "apollo-server-express": "^3.6.3", 52 | "chart.js": "^3.7.0", 53 | "codemirror": "^5.65.1", 54 | "codemirror-graphql": "^1.2.11", 55 | "cors": "^2.8.5", 56 | "express": "^4.17.3", 57 | "graphql": "^15.8.0", 58 | "graphql-tools": "^8.2.0", 59 | "lodash": "^4.17.21", 60 | "react": "^17.0.2", 61 | "react-chartjs-2": "^4.0.1", 62 | "react-dom": "^17.0.2", 63 | "react-router-dom": "^6.2.1" 64 | }, 65 | "devDependencies": { 66 | "@babel/core": "^7.16.5", 67 | "@babel/preset-env": "^7.16.5", 68 | "@babel/preset-react": "^7.16.5", 69 | "autoprefixer": "^10.4.2", 70 | "babel-loader": "^8.2.3", 71 | "cross-env": "^7.0.3", 72 | "css-loader": "^6.5.1", 73 | "electron": "^16.0.8", 74 | "electron-devtools-installer": "^3.2.0", 75 | "electron-packager": "^15.4.0", 76 | "eslint": "^8.8.0", 77 | "eslint-config-airbnb": "^19.0.4", 78 | "eslint-import-resolver-webpack": "^0.13.2", 79 | "file-loader": "^6.2.0", 80 | "html-webpack-plugin": "^5.5.0", 81 | "mini-css-extract-plugin": "^2.4.5", 82 | "nodemon": "^2.0.15", 83 | "postcss-loader": "^6.2.1", 84 | "style-loader": "^3.3.1", 85 | "tailwindcss": "^3.0.19", 86 | "webpack": "^5.65.0", 87 | "webpack-cli": "^4.9.1", 88 | "webpack-dev-server": "^3.11.3" 89 | }, 90 | "resolutions": { 91 | "graphql": "^15.6.1", 92 | "**/graphql": "^15.6.1" 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /server/plugins/tracker.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | 3 | // a customize function that handle array and number for lodash deep merge 4 | function customizer(objValue, srcValue) { 5 | // concat the two array if it is an array 6 | if (_.isArray(objValue)) return objValue.concat(srcValue); 7 | // sum it up if it is a number 8 | else if (typeof objValue === 'number') return objValue + srcValue; 9 | } 10 | 11 | module.exports = { 12 | // When the quest is received 13 | requestDidStart(requestContext) { 14 | // store the start time of the request 15 | const requestStart = Date.now(); 16 | // create a placeholder for the performance data 17 | const performanceData = {}; 18 | 19 | return { 20 | // Extract the performance of each resolver using the even cycle 21 | executionDidStart(){ 22 | return { 23 | willResolveField({info}) { 24 | // store the start time of the resolver 25 | const start = performance.now(); 26 | 27 | return (error, result) => { 28 | // error handler 29 | if (error) return console.log(`It failed with ${error}`); 30 | 31 | // store the end time of the resolver 32 | const end = performance.now(); 33 | 34 | // create a placeholder for the performance of the individual resolver 35 | let itemPerformance = {}; 36 | 37 | // initialize the performance data of resolver 38 | itemPerformance[info.fieldName] = {'time': [], trips: 0}; 39 | itemPerformance[info.fieldName].time.push(end - start); 40 | itemPerformance[info.fieldName].trips += 1; 41 | 42 | // create a placeholder that we can use to traverse over the info object 43 | let current = info.path; 44 | 45 | // create the resulting object for performance that we want to send back to our front end 46 | while (current !== undefined) { 47 | if (current.typename !== undefined){ 48 | const temp = {}; 49 | temp[current.typename] = itemPerformance; 50 | itemPerformance = temp; 51 | } 52 | current = current.prev; 53 | } 54 | 55 | // merge the individual resolver performance object with the exisiting performance object 56 | _.mergeWith(performanceData, itemPerformance, customizer); 57 | }; 58 | }, 59 | }; 60 | }, 61 | // Customize data before sending back to client 62 | willSendResponse (context) { 63 | // record the request completion time 64 | const stop = Date.now(); 65 | // compute the time spent to resolve request 66 | const elapsed = stop - requestStart; 67 | // compute the size of the returned response data in bytes 68 | const size = JSON.stringify(context.response).length * 2; 69 | // store the time spent to resolve request in performance data 70 | performanceData.queryTime = elapsed; 71 | // attach performance data to the response that get sent back to client 72 | context.response.extensions = {performanceData}; 73 | } 74 | }; 75 | }, 76 | }; -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const { ApolloServer } = require('apollo-server-express'); 3 | const plugins = require('./plugins/tracker'); 4 | const { updatePackageJson } = require('./utils/updatePackage'); 5 | const cors = require('cors'); 6 | const { fstat } = require('fs'); 7 | const fs = require('fs'); 8 | const { injectRequire } = require('./utils/injectPaths'); 9 | 10 | // Set port where we launch the server 11 | const PORT = 3000; 12 | 13 | // Create express app 14 | const app = express(); 15 | 16 | // Allow cors 17 | app.use(cors()); 18 | 19 | // Handle parsing request body 20 | app.use(express.json()); 21 | app.use(express.urlencoded({ extended: true })); 22 | 23 | 24 | // Handler for upload request 25 | app.post('/upload', async (req, res) => { 26 | // extract the filepath 27 | const filePath = req.body.pathName; 28 | 29 | // if it is a package.json file 30 | if (filePath.includes('package')) { 31 | try { 32 | // update our own package.json file 33 | updatePackageJson(filePath); 34 | // send a 200 response if succeeds 35 | res.status(200).json('upload endpoint'); 36 | } catch (e) { 37 | // send a 500 response if an error occur 38 | res.status(500).json(e); 39 | } 40 | } 41 | // else if it is a schema file 42 | else { 43 | 44 | try { 45 | 46 | 47 | 48 | const schema = injectRequire(filePath); 49 | 50 | // start the apollo server with the passed in schema 51 | startServer(require(filePath)); 52 | 53 | // revert back the changes that we did earlier 54 | console.log('new server start'); 55 | 56 | 57 | // send a response with 200 if succeeds 58 | res.status(200).json('upload endpoints'); 59 | } catch (e) { 60 | // send a 500 response if an error occur 61 | res.status(500).json(e); 62 | } 63 | } 64 | }); 65 | 66 | // helper function for starting the server 67 | async function startServer(schema) { 68 | // first clean up the previous apollo graphql router if it exists 69 | const routes = app._router.stack; 70 | routes.forEach((route, i) => { 71 | if (route.handle.name === 'router') { 72 | routes.splice(i, 1); 73 | } 74 | }); 75 | 76 | // create a new apollo server with the given schema and the performance plugin we created 77 | const server = new ApolloServer({ 78 | schema, 79 | plugins: [ 80 | plugins, 81 | ], 82 | }); 83 | 84 | // start the apollo server 85 | await server.start(); 86 | 87 | // apply the graphql server a middleware to the express app 88 | server.applyMiddleware( {app, path:'/graphql'} ); 89 | } 90 | 91 | // start the express app on the above port 92 | app.listen(PORT, ()=> { 93 | console.log(`Server listening on port: ${PORT}...`); 94 | }); 95 | 96 | module.export = app; 97 | -------------------------------------------------------------------------------- /server/utils/injectPaths.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | // function closure() { 4 | // const underlyingPath = module.paths.slice(0,3); 5 | // return function customizedRequire(filePath) { 6 | // module.paths = underlyingPath.concat(module.paths); 7 | // console.log(module.paths); 8 | // console.log(filePath); 9 | // if (filePath[0] !== '.' && filePath[0] !== '/') { 10 | // console.log('i am here'); 11 | // const requiredModule = require(filePath); 12 | // return requiredModule; 13 | // } 14 | // console.log('i am there'); 15 | // const requiredModule = require(filePath); 16 | // return requiredModule; 17 | // }; 18 | // } 19 | 20 | // const customizedRequire = closure(); 21 | 22 | // function injectRequire(filePath) { 23 | // if (filePath[0] !== '.' && filePath[0] !== '/') return customizedRequire(filePath); 24 | // console.log(filePath); 25 | // const newPath = require.resolve(filePath); 26 | 27 | // const data = fs.readFileSync(newPath, 'utf8'); 28 | 29 | // const thisPath = module.filename; 30 | // // console.log(thisPath); 31 | // const injection = `const injectRequire = require('${thisPath}');\n`; 32 | // const modifyData = data.replaceAll('require(', 'injectRequire('); 33 | // const newData = injection + modifyData; 34 | // fs.writeFileSync(newPath, newData); 35 | // const requiredModule = customizedRequire(newPath); 36 | // console.log('I am there twice'); 37 | // fs.writeFileSync(newPath, data); 38 | // return requiredModule; 39 | // } 40 | 41 | 42 | const underlyingPath = module.paths.slice(0,3); 43 | 44 | function injectRequire(filePath) { 45 | // if it is not a relative path/absolute path, require and return the module 46 | if (filePath[0] !== '.' && filePath[0] !== '/') { 47 | console.log(filePath, require.resolve(filePath)); 48 | const module = require(filePath); 49 | return module; 50 | } 51 | 52 | // get the absolute file path 53 | const newPath = require.resolve(filePath); 54 | 55 | // read the file and store it's content in data 56 | const data = fs.readFileSync(newPath, 'utf8'); 57 | 58 | // create the injections lines 59 | const createPath = `const underlyingPath = ['${underlyingPath[0]}', '${underlyingPath[1]}', '${underlyingPath[2]}']; \n`; 60 | const addPathToModule = 'module.paths = underlyingPath.concat(module.paths); \n'; 61 | const injectFunction = `const fs = require('fs'); \n${injectRequire} \n`; 62 | const modifyData = data.replaceAll('require(', 'injectRequire('); 63 | 64 | // update the file with the injection lines attach at the beginning 65 | const newData = createPath + addPathToModule + injectFunction + modifyData; 66 | fs.writeFileSync(newPath, newData); 67 | 68 | // require the updated file 69 | const requiredModule = require(newPath); 70 | 71 | // revert the changes made to the file 72 | fs.writeFileSync(newPath, data); 73 | 74 | // return the required file 75 | return requiredModule; 76 | } 77 | 78 | module.exports = { 79 | injectRequire 80 | }; -------------------------------------------------------------------------------- /server/utils/updatePackage.js: -------------------------------------------------------------------------------- 1 | const { exec } = require('child_process'); 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | 5 | // function for updating our npm packages according to the end user's need 6 | function updatePackageJson(filePath) { 7 | try { 8 | // read the passed in package.json file 9 | const data = fs.readFileSync(filePath, 'utf8'); 10 | // get the dependencies and devdependencies object 11 | const { dependencies, devDependencies } = JSON.parse(data); 12 | 13 | // read our own package.json file 14 | const currFile = fs.readFileSync(path.resolve('./package.json'), 'utf-8'); 15 | // turn it into an object 16 | const parsedCurrFile = JSON.parse(currFile); 17 | 18 | // get all the keys of the previously defined objects 19 | const dependenciesArr = Object.keys(dependencies); 20 | const devDependenciesArr = Object.keys(devDependencies); 21 | const currDependencies = Object.keys(parsedCurrFile.dependencies); 22 | const currDevDependencies = Object.keys(parsedCurrFile.devDependencies); 23 | 24 | // get all the npm packges under dependencies that are in user's package.json file but not in ours 25 | const newDependencies = dependenciesArr.filter(item => { 26 | if (!currDependencies.includes(item)) return item; 27 | }); 28 | 29 | // get all the npm packges under devDependencies that are in user's package.json file but not in ours 30 | const newDevDependencies = devDependenciesArr.filter(item => { 31 | if (!currDevDependencies.includes(item)) return item; 32 | }); 33 | 34 | // create the terminal command to install all of the missing dependencies/devDependencies 35 | const npmInstallStr = 'npm install ' + newDependencies.join(' '); 36 | const npmDevInstallStr = 'npm install -D ' + newDevDependencies.join(' '); 37 | 38 | // execute the commands 39 | exec(npmInstallStr); 40 | exec(npmDevInstallStr); 41 | 42 | } catch (err) { 43 | console.error(err); 44 | } 45 | return; 46 | } 47 | 48 | module.exports = { 49 | updatePackageJson 50 | }; -------------------------------------------------------------------------------- /src/assets/bypass.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/QLeo/29d447c885f0c0cd7d28ce67f28681af1c971143/src/assets/bypass.gif -------------------------------------------------------------------------------- /src/assets/history.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/QLeo/29d447c885f0c0cd7d28ce67f28681af1c971143/src/assets/history.png -------------------------------------------------------------------------------- /src/assets/metrics2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/QLeo/29d447c885f0c0cd7d28ce67f28681af1c971143/src/assets/metrics2.png -------------------------------------------------------------------------------- /src/assets/performance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/QLeo/29d447c885f0c0cd7d28ce67f28681af1c971143/src/assets/performance.png -------------------------------------------------------------------------------- /src/assets/q-logo-blk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/QLeo/29d447c885f0c0cd7d28ce67f28681af1c971143/src/assets/q-logo-blk.png -------------------------------------------------------------------------------- /src/assets/q-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/QLeo/29d447c885f0c0cd7d28ce67f28681af1c971143/src/assets/q-logo.png -------------------------------------------------------------------------------- /src/assets/qleo-lion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/QLeo/29d447c885f0c0cd7d28ce67f28681af1c971143/src/assets/qleo-lion.png -------------------------------------------------------------------------------- /src/assets/qleo-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/QLeo/29d447c885f0c0cd7d28ce67f28681af1c971143/src/assets/qleo-logo.png -------------------------------------------------------------------------------- /src/assets/schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/QLeo/29d447c885f0c0cd7d28ce67f28681af1c971143/src/assets/schema.png -------------------------------------------------------------------------------- /src/components/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { 3 | Link, 4 | HashRouter as Router, 5 | Routes, 6 | Route 7 | } from 'react-router-dom'; 8 | import NavBar from './Navbar'; 9 | import MainContainer from '../containers/MainContainer'; 10 | import UploadContainer from '../containers/UploadContainer'; 11 | import PerformanceContainer from '../containers/PerformanceContainer'; 12 | import HistoryContinaer from '../containers/HistoryContainer'; 13 | 14 | import '../css/App.css'; 15 | 16 | function App() { 17 | const [query, setQuery] = useState(''); 18 | const [metrics, setMetrics] = useState({}); 19 | const [schema, setSchema] = useState(false); 20 | const [history, setHistory] = useState([]); 21 | 22 | return ( 23 |
24 | 25 | 26 | 27 | 28 | } /> 29 | } /> 30 | } /> 31 | } /> 32 | 33 | 34 |
35 | ); 36 | } 37 | 38 | export default App; 39 | -------------------------------------------------------------------------------- /src/components/FileUpload.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useRef } from 'react'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | import { faUpload, faCircleCheck } from '@fortawesome/free-solid-svg-icons'; 4 | 5 | function FileUpload({ schema, setSchema }) { 6 | const hiddenFileInput = useRef(null); 7 | 8 | // Handles file upload event and post request to server 9 | function handleUpload(event) { 10 | const pathName = event.target.files[0].path; 11 | 12 | fetch('http://localhost:3000/upload/', { 13 | method: 'POST', 14 | headers: { 15 | 'Content-Type': 'application/json', 16 | }, 17 | body: JSON.stringify({pathName}), 18 | } 19 | ).then(res => res.json()) 20 | .then(data => console.log(data)); 21 | 22 | setSchema(!schema); 23 | } 24 | 25 | // invokes file input tag through a ref 26 | const handleClick = _event => { 27 | hiddenFileInput.current.click(); 28 | }; 29 | 30 | return ( 31 |
32 |
33 |

Upload your schema file

34 | 35 | 44 |
45 |
46 | ); 47 | } 48 | 49 | export default FileUpload; -------------------------------------------------------------------------------- /src/components/GraphData.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Chart, registerables } from 'chart.js'; 3 | import { Bar } from 'react-chartjs-2'; 4 | 5 | Chart.register(...registerables); 6 | 7 | function GraphData({ metrics }) { 8 | const calls = []; 9 | const totalTime = metrics.queryTime; 10 | 11 | let currentKeys = Object.keys(metrics); 12 | let currentObj = metrics; 13 | 14 | if (currentKeys.length > 0) { 15 | const placeholder = []; 16 | 17 | while (currentKeys.length > 0){ 18 | for (let i = 0; i < currentKeys.length; i++){ 19 | if (currentObj[currentKeys[i]].trips) { 20 | 21 | const time = currentObj[currentKeys[i]].time.reduce((avg, current, _index, arr) => (avg + current / arr.length), 0); 22 | 23 | calls.push({ label: currentKeys[i], calls: currentObj[currentKeys[i]].trips, time: time }); 24 | } 25 | 26 | if (typeof currentObj[currentKeys[i]] === 'object') { 27 | placeholder.push(currentObj[currentKeys[i]]); 28 | } 29 | } 30 | 31 | currentObj = placeholder.shift(); 32 | if (currentObj === undefined) break; 33 | currentKeys = Object.keys(currentObj); 34 | } 35 | 36 | const speedData = { 37 | labels: calls.map(item => item.label), 38 | datasets: [{ 39 | label: 'Speed in ms', 40 | backgroundColor: 'rgba(193,199,205, 1)', 41 | borderColor: 'rgba(0,0,0,1)', 42 | borderWidth: 2, 43 | hoverBackgroundColor: 'rgba(202,160,85,1)', 44 | data: calls.map(item => item.time), 45 | }] 46 | }; 47 | 48 | 49 | const callsData = { 50 | labels: calls.map(item => item.label), 51 | datasets: [{ 52 | label: 'Resolver Calls', 53 | backgroundColor: 'rgba(193,199,205, 1)', 54 | borderColor: 'rgba(0,0,0,1)', 55 | borderWidth: 2, 56 | hoverBackgroundColor: 'rgba(202,160,85,1)', 57 | data: calls.map(item => item.calls) 58 | }] 59 | }; 60 | 61 | const totalCalls = calls.reduce((acc, item) => { 62 | acc += item.calls; 63 | return acc; 64 | }, 0); 65 | 66 | return ( 67 |
68 |
69 |

Total execution time(ms): {totalTime}

70 | 84 |
85 |
86 |

Total Resolver invocations: {totalCalls}

87 | 101 |
102 |
103 | ); 104 | }} 105 | 106 | export default GraphData; -------------------------------------------------------------------------------- /src/components/HistoryItem.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | function HistoryItem({metrics, query, setQuery, setMetrics, index}) { 4 | function onclickHandler(){ 5 | setQuery(query); 6 | setMetrics(metrics); 7 | } 8 | 9 | return ( 10 |
11 |
12 | 13 |
14 |
15 | ); 16 | } 17 | 18 | export default HistoryItem; -------------------------------------------------------------------------------- /src/components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link, useLocation } from 'react-router-dom'; 3 | import lion from '../assets/q-logo.png'; 4 | 5 | const navItems = [ 6 | { 7 | title: 'Upload', 8 | path: '/upload', 9 | }, 10 | { 11 | title: 'Dashboard', 12 | path: '/dashboard' 13 | }, 14 | { 15 | title: 'Performance', 16 | path: '/performance' 17 | }, 18 | { 19 | title: 'History', 20 | path: '/history' 21 | } 22 | ]; 23 | 24 | 25 | function NavBar() { 26 | const { pathname } = useLocation(); 27 | 28 | return ( 29 | 50 | ); 51 | } 52 | 53 | export default NavBar; -------------------------------------------------------------------------------- /src/components/QueryInput.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | 3 | 4 | import CodeMirror from '@uiw/react-codemirror'; 5 | // import { ValidationContext, SDLValidationContext } from 'graphql'; 6 | import { oneDark } from '@codemirror/theme-one-dark'; 7 | import 'codemirror/keymap/sublime'; 8 | import 'codemirror/addon/hint/show-hint'; 9 | import 'codemirror/addon/lint/lint'; 10 | import 'codemirror-graphql/hint'; 11 | import 'codemirror-graphql/lint'; 12 | import 'codemirror-graphql/mode'; 13 | require('codemirror/mode/markdown/markdown'); 14 | 15 | function QueryInput({ query, setQuery, setExecRequest, setMetrics}) { 16 | const [input, setInput] = useState(query); 17 | 18 | const reset = () => { 19 | setInput(''); 20 | setQuery(''); 21 | setMetrics({}); 22 | }; 23 | 24 | const handleChange = (value) => { 25 | setInput(value); 26 | }; 27 | 28 | const handleSubmit = (e) =>{ 29 | e.preventDefault(); 30 | setExecRequest(true); 31 | setQuery(input); 32 | }; 33 | 34 | return ( 35 |
36 |
37 |
Query Input
38 |
39 | 42 | 45 |
46 |
47 | 48 | handleChange(value)} 54 | theme={oneDark} 55 | options={{ 56 | lineNumbers: true, 57 | mode: 'graphql', 58 | lint: { 59 | validationRules: [] 60 | }, 61 | smartIndent: true, 62 | lineWrapping: true }} 63 | /> 64 | 65 |
66 | ); 67 | } 68 | 69 | export default QueryInput; -------------------------------------------------------------------------------- /src/components/ResolversPerformance.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { useMutation } from '@apollo/client'; 3 | import { gql, useQuery } from '@apollo/client'; 4 | import Dropdown from './resolverDetailsComponent/Dropdown'; 5 | 6 | 7 | function MutationHook({query, setMetrics, setExecRequest, history, setHistory}){ 8 | const [ mutationFunc , { data, loading , error }] = useMutation(gql`${query}`, { 9 | onCompleted: data => { 10 | setExecRequest(false); 11 | setMetrics(data.extensions.performanceData); 12 | setHistory(history.push({query:query, metrics:data.extensions.performanceData})); 13 | }, 14 | fetchPolicy: 'no-cache' 15 | }); 16 | 17 | useEffect(() => { 18 | mutationFunc(); 19 | }, []); 20 | return null; 21 | } 22 | 23 | function QueryHook({query, setMetrics, setExecRequest, history, setHistory}){ 24 | const { loading, error, data } = useQuery(gql`${query}`, { 25 | onCompleted: (data) => { 26 | setExecRequest(false); 27 | setMetrics(data.extensions.performanceData); 28 | // history.push({query:query, metrics:data.extensions.performanceData}); 29 | // const newArray = history.slice(); 30 | setHistory([...history, {query:query, metrics:data.extensions.performanceData}]); 31 | }, 32 | fetchPolicy: 'no-cache' 33 | }); 34 | return null; 35 | } 36 | 37 | function ResolversPerformance({ query, metrics, execRequest, setMetrics, setExecRequest, history, setHistory }) { 38 | let isQuery = false, isMutation = false; 39 | 40 | if (execRequest && query.trim().substring(0,8) === 'mutation') isMutation = true; 41 | else if (execRequest && query.trim().substring(0,5) === 'query') isQuery = true; 42 | 43 | return ( 44 |
45 |

GraphQL Performance Details

46 | {isQuery ? 47 | : isMutation ? 48 | : <>} 49 |
50 |

Query Response Time

51 |

{metrics.queryTime ? metrics.queryTime + 'ms' : '...'}

52 |
53 |
54 |

Resolver Breakdown

55 |
56 |
57 | 58 |
59 |
60 | ); 61 | } 62 | 63 | export default ResolversPerformance; -------------------------------------------------------------------------------- /src/components/Schema.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { gql, useQuery } from '@apollo/client'; 3 | import QueryItem from './schemaComponents/FieldItem'; 4 | import Root from './schemaComponents/Root'; 5 | 6 | const introQuery = gql` 7 | { 8 | __schema { 9 | queryType{ 10 | fields{ 11 | name 12 | type { 13 | name 14 | kind 15 | ofType{ 16 | name 17 | kind 18 | ofType{ 19 | name 20 | kind 21 | } 22 | } 23 | } 24 | } 25 | } 26 | mutationType { 27 | fields{ 28 | name 29 | args{ 30 | name 31 | type{ 32 | name 33 | kind 34 | ofType{ 35 | name 36 | kind 37 | ofType{ 38 | name 39 | kind 40 | } 41 | } 42 | } 43 | } 44 | type { 45 | name 46 | kind 47 | ofType{ 48 | name 49 | kind 50 | ofType{ 51 | name 52 | kind 53 | } 54 | } 55 | } 56 | } 57 | } 58 | } 59 | } 60 | `; 61 | 62 | function Schema() { 63 | const [isRoot, setIsRoot] = useState(true); 64 | const [prev, setPrev] = useState(null); 65 | const [reqData, setReqData] = useState({}); 66 | const { loading, error, data } = useQuery(introQuery); 67 | 68 | if (loading) { 69 | return

Loading...

; 70 | } 71 | 72 | if (error) { 73 | console.log('There was an error', error); 74 | } 75 | 76 | const schemaTypes = []; 77 | if (Object.keys(reqData).length > 0) { 78 | if (reqData.args !== undefined) { 79 | schemaTypes.push(

Arguments:

); 80 | reqData.args.forEach((el, index) => schemaTypes.push()); 81 | } 82 | schemaTypes.push(

Fields:

); 83 | reqData.fields.forEach((el, index) => schemaTypes.push()); 84 | } 85 | 86 | const backHandler = () => { 87 | if (prev === null) setIsRoot(true); 88 | else { 89 | setReqData(prev.typeDef); 90 | setPrev(prev.prev); 91 | } 92 | }; 93 | 94 | let button; 95 | if (prev) button = `... / ${prev.typeDef.name} / ${reqData.name}`; 96 | else if (isRoot) button = '... / root'; 97 | else button = `... / root / ${reqData.name}`; 98 | 99 | 100 | return ( 101 |
102 |

Schema

103 | 104 | {isRoot && } 105 | {!isRoot && schemaTypes} 106 |
107 | ); 108 | } 109 | 110 | export default Schema; -------------------------------------------------------------------------------- /src/components/deprecated/deprecated.jsx: -------------------------------------------------------------------------------- 1 | // const [ packages, setPackages] = useState(false); 2 | 3 | // if (pathName.includes('package')) { 4 | // setPackages(!packages); 5 | // } else { 6 | // setSchema(!schema); 7 | // } 8 | 9 | 10 | //
11 | //

Step 1

12 | //

Upload your package.json

13 | // 14 | // 23 | //
24 | // -------------------------------------------------------------------------------- /src/components/resolverDetailsComponent/Dropdown.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import ResolverDetails from './ResolverDetails'; 3 | import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons'; 4 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 5 | 6 | const Dropdown = ({ obj, indent }) => { 7 | const [toggle, setToggle] = useState(false); 8 | 9 | const newIndent = indent + 0.75; 10 | 11 | return ( 12 |
13 | {Object.keys(obj).map(key => { 14 | if (typeof obj[key] === 'object'){ 15 | if (obj[key].time !== undefined){ 16 | return ( 17 |
18 | 19 |
20 | ); 21 | } 22 | return ( 23 |
24 |
25 | 29 |
30 | 31 | {toggle && } 32 |
33 | ); 34 | } 35 | })} 36 |
37 | ); 38 | }; 39 | 40 | export default Dropdown; -------------------------------------------------------------------------------- /src/components/resolverDetailsComponent/ResolverDetails.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | const PerformanceDetail = ({time, calls}) => { 4 | return ( 5 |
6 |

Average time: {time.reduce((avg, current, index, arr) => (avg + current / arr.length), 0)}

7 |

Calls: {calls}

8 |
9 | ); 10 | }; 11 | 12 | const ResolverDetails = ({ obj, resolver }) => { 13 | const [toggle, setToggle] = useState(false); 14 | 15 | return ( 16 |
17 |
18 |
{resolver}
19 | 20 |
21 | 22 | {toggle && } 23 |
24 | ); 25 | }; 26 | 27 | export default ResolverDetails; -------------------------------------------------------------------------------- /src/components/schemaComponents/FieldItem.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { gql, useQuery } from '@apollo/client'; 3 | 4 | 5 | function QueryItem({ typeDef, setReqData, setPrev, prev, parent }) { 6 | let isObject = false; 7 | let isNonNull = false; 8 | let isList = false; 9 | let queryKey; 10 | let current = typeDef.type; 11 | while (current !== null ){ 12 | if (current.kind === 'OBJECT') { 13 | isObject = true; 14 | queryKey = current.name; 15 | break; 16 | }else if (current.kind === 'NON_NULL') isNonNull = true; 17 | else if (current.kind === 'LIST') isList = true; 18 | current = current.ofType; 19 | } 20 | 21 | if (isObject){ 22 | let type; 23 | if (isNonNull && isList) type = `[${typeDef.type.ofType.ofType.name}!]`; 24 | else if (isNonNull) type = `${typeDef.type.ofType.ofType.name}!`; 25 | else if (isList) type = `[${typeDef.type.ofType.ofType.name}]`; 26 | else type = `${typeDef.type.name}`; 27 | 28 | const queryItem = `{ 29 | __type(name: "${queryKey}") { 30 | name 31 | fields { 32 | name 33 | type { 34 | name 35 | kind 36 | ofType{ 37 | name 38 | kind 39 | ofType{ 40 | name 41 | kind 42 | } 43 | } 44 | } 45 | } 46 | } 47 | } 48 | `; 49 | 50 | const { loading, error, data } = useQuery(gql`${queryItem}`); 51 | if (loading) { 52 | return

Loading...

; 53 | } 54 | 55 | if (error) { 56 | return

There was an error: {error}

; 57 | } 58 | 59 | const clickHandler = () => { 60 | setPrev({prev: prev, typeDef: parent}); 61 | if (typeDef.args !== undefined){ 62 | return setReqData({args:typeDef.args, ...data['__type'], name:typeDef.name}); 63 | } 64 | setReqData(data['__type']); 65 | }; 66 | 67 | return ( 68 |
69 | {} 70 |
71 | ); 72 | } 73 | let tag; 74 | if (isNonNull) tag =

{`${typeDef.name}: ${typeDef.type.ofType.name}!`}

; 75 | else tag =

{`${typeDef.name}: ${typeDef.type.name}`}

; 76 | 77 | return ( 78 |
79 | {tag} 80 |
81 | ); 82 | } 83 | 84 | export default QueryItem; -------------------------------------------------------------------------------- /src/components/schemaComponents/Root.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | function Root({setIsRoot, setReqData, setPrev, data}) { 4 | 5 | function clickHandler(e) { 6 | e.preventDefault(); 7 | if (e.target.innerText === 'Query'){ 8 | // setPrev({prev:prev, typeDef:data.queryType}); 9 | setReqData({...data.queryType, name:'Query'}); 10 | }else if ((e.target.innerText === 'Mutation')){ 11 | setReqData({...data.mutationType, name:'Mutation'}); 12 | } 13 | setIsRoot(false); 14 | } 15 | 16 | return ( 17 |
18 |
19 | 20 |
21 |
22 | 23 |
24 |
25 | ); 26 | } 27 | 28 | export default Root; -------------------------------------------------------------------------------- /src/containers/HistoryContainer.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import HistoryItem from '../components/HistoryItem'; 3 | 4 | function HistoryContinaer({history, setMetrics, setQuery}) { 5 | const historyItem = []; 6 | if (history.length > 0) history.forEach((el, index) => { 7 | historyItem.push(); 8 | }); 9 | 10 | return ( 11 |
12 | {historyItem} 13 |
14 | ); 15 | } 16 | 17 | export default HistoryContinaer; -------------------------------------------------------------------------------- /src/containers/MainContainer.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import QueryInput from '../components/QueryInput'; 3 | import ResolversPerformance from '../components/ResolversPerformance'; 4 | import Schema from '../components/Schema'; 5 | 6 | function MainContainer({schema, query, metrics, setQuery, setMetrics, history, setHistory}) { 7 | const [execRequest, setExecRequest] = useState(false); 8 | 9 | return ( 10 |
11 | {schema ? 12 | :

Please upload a schema 🧐

} 13 | 19 | 28 |
29 | ); 30 | } 31 | 32 | export default MainContainer; -------------------------------------------------------------------------------- /src/containers/PerformanceContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import GraphData from '../components/GraphData'; 3 | 4 | function PerformanceContainer({ metrics }) { 5 | let hasMetrics = false; 6 | if (Object.keys(metrics).length > 0) hasMetrics = true; 7 | return ( 8 |
9 | { 10 | hasMetrics ? 11 | :

Nothing to see here 👀

12 | } 13 |
14 | ); 15 | } 16 | 17 | export default PerformanceContainer; -------------------------------------------------------------------------------- /src/containers/UploadContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import FileUpload from '../components/FileUpload'; 3 | import lion from '../assets/qleo-logo.png'; 4 | 5 | 6 | function UploadContainer({ schema, setSchema }) { 7 | return ( 8 |
9 |
10 | logo 11 |

Let's get started!

12 |
13 | 14 |
15 | ); 16 | } 17 | 18 | export default UploadContainer; -------------------------------------------------------------------------------- /src/css/App.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | @import url("https://fonts.googleapis.com/css2?family=Rubik&display=swap"); 5 | 6 | * { 7 | margin: 0; 8 | padding: 0; 9 | box-sizing: border-box; 10 | font-family: "Rubik", sans-serif; 11 | overflow: hidden; 12 | } 13 | -------------------------------------------------------------------------------- /src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { 4 | ApolloClient, 5 | InMemoryCache, 6 | ApolloProvider, 7 | } from '@apollo/client'; 8 | import { linksChain } from './utils/links'; 9 | 10 | import App from './components/App'; 11 | 12 | // Create a new apollo client for doing graphql request and get the customized performance data 13 | const client = new ApolloClient({ 14 | cache: new InMemoryCache(), 15 | link: linksChain 16 | }); 17 | 18 | 19 | // Since we are using HtmlWebpackPlugin WITHOUT a template, we should create our own root node in the body element before rendering into it 20 | const root = document.createElement('div'); 21 | 22 | // set an id for root and append it to the DOM document 23 | root.id = 'root'; 24 | document.body.appendChild(root); 25 | 26 | // Now we can render our application into it 27 | render( 28 | 29 | 30 | , 31 | document.getElementById('root') 32 | ); 33 | -------------------------------------------------------------------------------- /src/utils/links.js: -------------------------------------------------------------------------------- 1 | import { ApolloLink, from, HttpLink } from '@apollo/client'; 2 | 3 | const ForwardExtensionsLink = new ApolloLink((operation, forward) => 4 | forward(operation).map(response => { 5 | if (response.data) { 6 | response.data.extensions = response.extensions; 7 | } 8 | return response; 9 | }) 10 | ); 11 | 12 | const httpLink = new HttpLink({ 13 | uri: 'http://localhost:3000/graphql' 14 | }); 15 | 16 | export const linksChain = from([ForwardExtensionsLink, httpLink]); -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: [ 3 | './src/**/*.{js,jsx,ts,tsx}' 4 | ], 5 | mode: 'jit', 6 | theme: { 7 | colors: { 8 | 'primary': '#CAA055', 9 | 'black': '#000000', 10 | 'bg-mirror': '#21272A', 11 | 'bg-gray': '#343A3F', 12 | 'text-primary': '#F2F4F8', 13 | 'text-secondary': '#DDE1E6', 14 | 'link': '#CAA055', 15 | 'link-hover': '#F2D7A5', 16 | 'text-disable': '#697077', 17 | 'btn-gray': '#4d5358', 18 | 'left': '#404040', 19 | 'right': '#CAA055' 20 | }, 21 | }, 22 | }; -------------------------------------------------------------------------------- /webpack.build.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 5 | 6 | // Any directories you will be adding code/files into, need to be added to this array so webpack will pick them up 7 | const defaultInclude = path.resolve(__dirname, 'src'); 8 | 9 | module.exports = { 10 | module: { 11 | rules: [ 12 | { 13 | test: /\.(css|scss)$/, 14 | use: [ 15 | MiniCssExtractPlugin.loader, 16 | 'css-loader', 17 | 'postcss-loader' 18 | ], 19 | include: defaultInclude 20 | }, 21 | { 22 | test: /\.(jsx|js|ts|tsx)?$/, 23 | use: [{ loader: 'babel-loader' }], 24 | include: defaultInclude 25 | }, 26 | { 27 | test: /\.(jpe?g|png|gif)$/, 28 | use: [{ loader: 'file-loader?name=img/[name]__[hash:base64:5].[ext]' }], 29 | include: defaultInclude 30 | }, 31 | { 32 | test: /\.(eot|svg|ttf|woff|woff2)$/, 33 | use: [{ loader: 'file-loader?name=font/[name]__[hash:base64:5].[ext]' }], 34 | include: defaultInclude 35 | } 36 | ] 37 | }, 38 | target: 'electron-renderer', 39 | plugins: [ 40 | new HtmlWebpackPlugin(), 41 | new MiniCssExtractPlugin({ 42 | // Options similar to the same options in webpackOptions.output 43 | // both options are optional 44 | filename: 'bundle.css', 45 | chunkFilename: '[id].css' 46 | }), 47 | new webpack.DefinePlugin({ 48 | 'process.env.NODE_ENV': JSON.stringify('production') 49 | }), 50 | // new MinifyPlugin() 51 | ], 52 | stats: { 53 | colors: true, 54 | children: false, 55 | chunks: false, 56 | modules: false 57 | }, 58 | optimization: { 59 | minimize: true 60 | }, 61 | resolve: { 62 | extensions: ['', '.js', '.jsx', '.ts', '.tsx'], 63 | } 64 | }; 65 | -------------------------------------------------------------------------------- /webpack.dev.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const { spawn } = require('child_process'); 5 | 6 | // Any directories you will be adding code/files into, need to be added to this array so webpack will pick them up 7 | const defaultInclude = path.resolve(__dirname, './src'); 8 | 9 | module.exports = { 10 | module: { 11 | rules: [ 12 | { 13 | test: /\.css$/, 14 | use: [{ loader: 'style-loader' }, { loader: 'css-loader' }, { loader: 'postcss-loader' }], 15 | include: defaultInclude 16 | }, 17 | { 18 | test: /\.(jsx|js|ts|tsx)?$/, 19 | use: [{ loader: 'babel-loader' }], 20 | include: defaultInclude 21 | }, 22 | { 23 | test: /\.(jpe?g|png|gif)$/, 24 | use: [{ loader: 'file-loader?name=img/[name]__[hash:base64:5].[ext]' }], 25 | include: defaultInclude 26 | }, 27 | { 28 | test: /\.(eot|svg|ttf|woff|woff2)$/, 29 | use: [{ loader: 'file-loader?name=font/[name]__[hash:base64:5].[ext]' }], 30 | include: defaultInclude 31 | } 32 | ] 33 | }, 34 | target: 'electron-renderer', 35 | plugins: [ 36 | new HtmlWebpackPlugin(), 37 | new webpack.DefinePlugin({ 38 | 'process.env.NODE_ENV': JSON.stringify('development') 39 | }) 40 | ], 41 | devtool: 'cheap-source-map', 42 | devServer: { 43 | contentBase: path.resolve(__dirname, 'dist'), 44 | stats: { 45 | colors: true, 46 | chunks: false, 47 | children: false 48 | }, 49 | before() { 50 | spawn( 51 | 'electron', 52 | ['.'], 53 | { shell: true, env: process.env, stdio: 'inherit' } 54 | ) 55 | .on('close', code => process.exit(0)) 56 | .on('error', spawnError => console.error(spawnError)); 57 | } 58 | }, 59 | resolve: { 60 | extensions: ['', '.js', '.jsx', '.ts', '.tsx'], 61 | } 62 | }; 63 | --------------------------------------------------------------------------------