├── .gitignore ├── .vscode └── settings.json ├── dump.rdb ├── client ├── assets │ ├── VisP.png │ ├── logo.png │ ├── landingP.png │ ├── novaCircle.png │ └── novaFullLogo.jsx ├── index.jsx ├── Components │ ├── FieldDesc.jsx │ ├── ScrollToTop.jsx │ ├── 404.jsx │ ├── LandingSections │ │ ├── LoadingModal.jsx │ │ ├── Footer.jsx │ │ ├── Description.jsx │ │ ├── Level.jsx │ │ ├── Nav.jsx │ │ ├── Examples.jsx │ │ └── Contact.jsx │ ├── NoSession.jsx │ ├── Overlay.jsx │ ├── SlideIn.jsx │ ├── TopMenu.jsx │ ├── Graph.jsx │ ├── Setting.jsx │ ├── Sidebar.jsx │ ├── Visualizer.jsx │ ├── Field.jsx │ └── Landing.jsx ├── App.jsx ├── styles.css └── dthreeHelpers │ ├── graphFunctions.js │ └── graphSetup.js ├── .eslintrc.js ├── .dockerignore ├── Dockerfile ├── Dockerfile-dev ├── docker-compose-prod.yml ├── docker-compose-dev-hot.yml ├── server ├── schemaController.js ├── cacheController.js ├── linkedList │ └── linkedList.js ├── server.js ├── IntroQuery.js └── GraphBuild.js ├── index.html ├── LICENSE ├── README.md ├── webpack.config.js ├── __tests__ └── linkedList.test.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | dist/ 4 | .DS_Store -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "git.ignoreLimitWarning": true 3 | } -------------------------------------------------------------------------------- /dump.rdb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nova-introspection/Nova/HEAD/dump.rdb -------------------------------------------------------------------------------- /client/assets/VisP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nova-introspection/Nova/HEAD/client/assets/VisP.png -------------------------------------------------------------------------------- /client/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nova-introspection/Nova/HEAD/client/assets/logo.png -------------------------------------------------------------------------------- /client/assets/landingP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nova-introspection/Nova/HEAD/client/assets/landingP.png -------------------------------------------------------------------------------- /client/assets/novaCircle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nova-introspection/Nova/HEAD/client/assets/novaCircle.png -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['airbnb'], 3 | env: { 4 | browser: true, 5 | node: true 6 | } 7 | } -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | Dockerfile* 4 | docker-compose* 5 | .dockerignore 6 | .git 7 | .gitignore 8 | README.md 9 | LICENSE 10 | .vscode -------------------------------------------------------------------------------- /client/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | // import 'semantic-ui-css/semantic.min.css'; 4 | import App from './App'; 5 | 6 | if (module.hot) { 7 | module.hot.accept(); 8 | } 9 | 10 | render(, document.querySelector('#root')); 11 | -------------------------------------------------------------------------------- /client/Components/FieldDesc.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const FieldDesc = (props) => { 4 | const { description } = props; 5 | return ( 6 |
7 | {description || 'Description not available.'} 8 |
9 | ); 10 | }; 11 | 12 | export default FieldDesc; 13 | -------------------------------------------------------------------------------- /client/Components/ScrollToTop.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { withRouter } from 'react-router-dom'; 3 | 4 | const ScrollToTop = ({ history }) => { 5 | useEffect(() => { 6 | const unlisten = history.listen(() => window.scrollTo(0, 0)); 7 | return () => { unlisten(); } 8 | }, []); 9 | return (null); 10 | } 11 | 12 | export default withRouter(ScrollToTop); -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | #Use dockerhub image for Node 2 | FROM node:10 3 | 4 | #Create App Directory 5 | WORKDIR /usr/src/app 6 | 7 | #INSTALL APP DEPENDENCIES 8 | COPY . /usr/src/app/ 9 | RUN npm install -g webpack 10 | RUN npm install 11 | RUN npm run build 12 | #Expose port from server file 13 | EXPOSE 3000 14 | EXPOSE 80 15 | 16 | #Execute server file 17 | CMD REDIS_URI=redis://redis:6379 node server/server.js -------------------------------------------------------------------------------- /Dockerfile-dev: -------------------------------------------------------------------------------- 1 | #Use dockerhub image for Node 2 | FROM node:10 3 | 4 | #Install Webpack globally 5 | RUN npm install -g webpack 6 | 7 | #Create App Directory 8 | WORKDIR /usr/src/app 9 | 10 | ENV NODE_ENV=development 11 | 12 | 13 | #INSTALL APP DEPENDENCIES 14 | COPY package*.json /usr/src/app/ 15 | RUN npm install compression-webpack-plugin 16 | RUN npm install 17 | 18 | #EXPOSE PORTS 19 | EXPOSE 3000 -------------------------------------------------------------------------------- /client/Components/404.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Logo from '../assets/novaFullLogo'; 3 | 4 | const notFound = () => { 5 | return ( 6 |
7 | 8 |

9 | The Page You Are Looking For Does Not Exist 10 |

11 |
12 | ) 13 | } 14 | 15 | export default notFound; -------------------------------------------------------------------------------- /client/Components/LandingSections/LoadingModal.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const LoadingModal = (props) => ( 4 |
5 |
6 |
7 | 8 |
9 |
10 | ) 11 | 12 | export default LoadingModal; -------------------------------------------------------------------------------- /docker-compose-prod.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | app: 4 | image: "novaintrospection/nova-prod" 5 | container_name: "nova-prod" 6 | ports: 7 | - "3000:3000" 8 | - "80:3000" 9 | 10 | volumes: 11 | - ./:/usr/src/app 12 | - node_modules:/usr/src/app/node_modules 13 | depends_on: 14 | - redis 15 | redis: 16 | image: "redis:alpine" 17 | container_name: "redis" 18 | command: ["redis-server"] 19 | volumes: 20 | node_modules: {} -------------------------------------------------------------------------------- /docker-compose-dev-hot.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | dev: 4 | image: "novaintrospection/nova-dev" 5 | container_name: "nova-dev-hot" 6 | ports: 7 | - "8080:8080" 8 | - "3000:3000" 9 | volumes: 10 | - ./:/usr/src/app 11 | - node_modules:/usr/src/app/node_modules 12 | command: npm run dev:hot 13 | depends_on: 14 | - redis 15 | redis: 16 | image: "redis:alpine" 17 | container_name: "redis" 18 | command: ["redis-server"] 19 | volumes: 20 | node_modules: {} -------------------------------------------------------------------------------- /client/Components/LandingSections/Footer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { setLinks } from './Nav.jsx'; 3 | 4 | const Footer = (props) => { 5 | return ( 6 | 15 | ) 16 | } 17 | 18 | export default Footer; -------------------------------------------------------------------------------- /client/Components/NoSession.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Logo from '../assets/novaFullLogo.jsx'; 3 | import { withRouter } from 'react-router-dom'; 4 | 5 | const NoSession = (props) => { 6 | const { history } = props; 7 | return ( 8 |
9 | 10 |

11 | Your session has ended. Please return to home page. 12 |

13 | 14 |
15 | ) 16 | } 17 | 18 | export default withRouter(NoSession); -------------------------------------------------------------------------------- /client/Components/Overlay.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TopMenu from './TopMenu'; 3 | import Sidebar from './Sidebar'; 4 | 5 | const Overlay = (props) => { 6 | const { 7 | toggleSidebar, 8 | colorChange, 9 | visible, 10 | changeType, 11 | currentType, 12 | root, 13 | data, 14 | } = props; 15 | 16 | return ( 17 |
18 | 19 | 26 |
27 | ); 28 | }; 29 | 30 | export default Overlay; 31 | -------------------------------------------------------------------------------- /server/schemaController.js: -------------------------------------------------------------------------------- 1 | const fetch = require('node-fetch'); 2 | const { JSON_STRING } = require('./IntroQuery'); 3 | const { SchemaGraph } = require('./GraphBuild'); 4 | 5 | const getSchema = (req, res, next) => { 6 | const { uri } = req.body; 7 | fetch(uri, { 8 | method: 'POST', 9 | mode: 'cors', 10 | headers: { 11 | 'Content-Type': 'application/json', 12 | }, 13 | body: JSON.stringify(JSON_STRING), 14 | }) 15 | .then(response => response.json()) 16 | .then(({ data }) => { 17 | res.locals = new SchemaGraph(data.__schema.types); 18 | next(); 19 | }) 20 | .catch((err) => { 21 | res.sendStatus(400); 22 | }); 23 | }; 24 | 25 | module.exports = { getSchema }; 26 | -------------------------------------------------------------------------------- /client/Components/LandingSections/Description.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Level from './Level.jsx'; 3 | 4 | const Description = (props) => { 5 | return ( 6 |
7 |
8 |
9 |

Nova

10 |

GraphQL Schema Visualizer

11 |

Enter any valid GraphQL endpoint to see a D3 rendered graph of the schema.


12 |

Reach out to us and let us know what you think!

13 |
14 |
15 | 16 |
17 |
18 |
19 | ) 20 | } 21 | 22 | export default Description; -------------------------------------------------------------------------------- /client/Components/SlideIn.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Transition } from 'react-transition-group'; 3 | 4 | const duration = 3000; 5 | 6 | const defaultStyle = { 7 | transition: `opacity ${duration}ms ease-in-out`, 8 | opacity: 0, 9 | } 10 | 11 | const transitionStyles = { 12 | entering: { opacity: 1 }, 13 | entered: { opacity: 1 }, 14 | exiting: { opacity: 0 }, 15 | exited: { opacity: 0 }, 16 | }; 17 | 18 | const SlideIn = ({ in: inProp }) => { 19 | // console.log(props); 20 | // const { description } = props; 21 | console.log('Im in', inProp); 22 | return ( 23 | 24 | {state => { 25 | console.log(state) 26 | return ( 27 |
33 | Im in 34 |
) 35 | }} 36 |
37 | ); 38 | }; 39 | 40 | export default SlideIn; 41 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 17 | 18 | 19 | 20 | Nova 21 | 22 | 23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /client/Components/LandingSections/Level.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Level = (props) => { 4 | const stackPic = []; 5 | const stackName = ['React', 'D3', 'Node', 'Redis']; 6 | stackPic[0] = `https://cdn-images-1.medium.com/max/1600/1*14P21AcmS45PUg6g7zyyQA.png`; 7 | stackPic[1] = `https://raw.githubusercontent.com/d3/d3-logo/master/d3.png`; 8 | stackPic[2] = `https://cdn2.iconfinder.com/data/icons/nodejs-1/512/nodejs-512.png`; 9 | stackPic[3] = `https://cdn4.iconfinder.com/data/icons/redis-2/1451/Untitled-2-512.png`; 10 | const width = 50; 11 | const tech = stackName.map((item, i) => ( 12 |
13 |
14 |

{item}

15 |

{item}

16 |
17 |
18 | )) 19 | return ( 20 |
21 | 24 |
25 | ) 26 | } 27 | 28 | export default Level; -------------------------------------------------------------------------------- /server/cacheController.js: -------------------------------------------------------------------------------- 1 | const redis = require('redis'); 2 | const parseUrl = require('url-parse'); 3 | 4 | const redisClient = redis.createClient(process.env.REDIS_URI || 'redis://localhost:6379'); 5 | const { promisify } = require('util'); 6 | 7 | const getAsync = promisify(redisClient.get).bind(redisClient); 8 | 9 | const EXPIRE_TIME = 300; 10 | redisClient.on('connect', () => { 11 | console.log('Redis client connected.'); 12 | }); 13 | 14 | const checkCache = async (req, res, next) => { 15 | const { uri } = req.body; 16 | const result = await getAsync(uri); 17 | if (!result) { 18 | next(); 19 | } else { 20 | res 21 | .status(200) 22 | .type('application/json') 23 | .send(JSON.parse(result)); 24 | } 25 | }; 26 | 27 | const cacheSchema = (req, res, next) => { 28 | const { uri } = req.body; 29 | const { hostname } = parseUrl(uri); 30 | if (hostname !== 'localhost' || hostname !== '127.0.0.1') { 31 | redisClient.set(uri, JSON.stringify(res.locals), 'EX', EXPIRE_TIME); 32 | next(); 33 | } 34 | }; 35 | module.exports = { checkCache, cacheSchema }; 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 nova-introspection 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 | -------------------------------------------------------------------------------- /client/Components/TopMenu.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Menu, Icon } from 'semantic-ui-react'; 3 | import Setting from './Setting'; 4 | 5 | const TopMenu = (props) => { 6 | const { toggleSidebar, colorChange } = props; 7 | const [settingsActive, useSettings] = useState(false); 8 | 9 | function toggleSettings() { 10 | useSettings(!settingsActive); 11 | } 12 | const active = (settingsActive) ? 'activeColor' : ''; 13 | return ( 14 | 31 | ); 32 | }; 33 | 34 | export default TopMenu; -------------------------------------------------------------------------------- /server/linkedList/linkedList.js: -------------------------------------------------------------------------------- 1 | function LinkedList() { 2 | this.head = null; 3 | this.tail = null; 4 | this.size = 0; 5 | } 6 | 7 | function Node(val) { 8 | this.value = val; 9 | this.next = null; 10 | } 11 | 12 | LinkedList.prototype.enqueue = function(val) { 13 | const node = new Node(val); 14 | if(!this.head) { 15 | this.head = node; 16 | this.tail = node; 17 | } else { 18 | this.tail.next = node; 19 | this.tail = this.tail.next; 20 | } this.size++; 21 | } 22 | 23 | LinkedList.prototype.dequeue = function() { 24 | if(!this.head) return null; 25 | const val = this.head.value; 26 | if(this.head === this.tail) { 27 | this.head = null; 28 | this.tail = null; 29 | } else { this.head = this.head.next; } 30 | this.size--; 31 | return val; 32 | } 33 | 34 | LinkedList.prototype.isEmpty = function() { return this.size === 0 ? true : false; } 35 | LinkedList.prototype.printList = function() { 36 | let curr = this.head; 37 | let tail = this.tail; 38 | while(curr) { 39 | console.log(curr.value, 'tail:', tail.value, curr === tail); 40 | curr = curr.next; 41 | } console.log('size:', this.size); 42 | } 43 | 44 | module.exports = LinkedList; -------------------------------------------------------------------------------- /client/Components/Graph.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import graphSetup from '../dthreeHelpers/graphSetup'; 3 | 4 | const Graph = (props) => { 5 | const { handleClick, data } = props; 6 | const [width, changeWidth] = useState(window.innerWidth); 7 | const [height, changeHeight] = useState(window.innerHeight); 8 | data.nodes.forEach(item => { item.radius = item.name.length * 4.4 + 25; }); 9 | useEffect(() => { graphSetup.setup(data, handleClick); }, data); 10 | useEffect(() => { 11 | const wind = () => { 12 | changeWidth(window.innerWidth); 13 | changeHeight(window.innerHeight); 14 | }; window.addEventListener('resize', wind); 15 | return () => window.removeEventListener('resize', wind); 16 | }, [width, height]); 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | }; 30 | 31 | export default Graph; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nova 2 | ## GraphQL Schema visualizer for developers 3 | 4 | ## No Longer Used 5 | ## Moved to [/nova-introspection/novaql](https://github.com/nova-introspection/novaql) 6 | 7 | Front Page | Visualizer Page 8 | :------------------------------:|:-------------------------: 9 | ![](client/assets/landingP.png) |![](client/assets/VisP.png) 10 | 11 | ### How to use 12 | Head over to [novaql.com](http://novaql.com) 13 | Just enter a GraphQL endpoint! Nova will create an interactive visualization that represents the schema. 14 | 15 | 16 | ### Built with 17 | - React 18 | - React Router 19 | - D3.js 20 | - Nodejs 21 | - Express 22 | - Babel 23 | - Webpack 24 | 25 | ### Contributing 26 | Nova is currently in beta release. We encourage you to submit issues for any bugs or ideas for enhancements. Also feel free to fork this repo and submit pull requests to contribute as well. 27 | 28 | ### Authors 29 | - [Jovan Kelly](https://github.com/kellyjovan) 30 | - [Bo Peng](https://github.com/bopeng95) 31 | - [Bryan Costa](https://github.com/bryanAcosta) 32 | - [Brayton Holman](https://github.com/frontleft) 33 | 34 | ### License 35 | This project is licensed under the MIT License - see the LICENSE.md file for details 36 | 37 | 38 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const express = require('express'); 3 | const bodyParser = require('body-parser'); 4 | const cors = require('cors'); 5 | const { getSchema } = require('./schemaController'); 6 | const { checkCache, cacheSchema } = require('./cacheController'); 7 | 8 | const app = express(); 9 | const PORT = 3000; 10 | 11 | app.use(bodyParser.json()); 12 | app.use(bodyParser.urlencoded({ extended: true })); 13 | app.use(cors()); 14 | 15 | app.get('/', (req, res) => { 16 | res.sendFile(path.resolve(__dirname, '../index.html')); 17 | }); 18 | 19 | app.get('/dist/bundle.js', (req, res) => { 20 | res.set({ 21 | 'Content-Encoding': 'gzip', 22 | 'Content-Type': 'application/javascript', 23 | }); 24 | res.sendFile(path.resolve(__dirname, '../dist/bundle.js.gz')); 25 | }); 26 | 27 | app.get('/client/styles.css', (req, res) => { 28 | res.sendFile(path.resolve(__dirname, '../client/styles.css')); 29 | }); 30 | 31 | app.get('/*', (req, res) => { 32 | res.sendFile(path.resolve(__dirname, '../index.html')); 33 | }); 34 | 35 | app.post('/api/schema', checkCache, getSchema, cacheSchema, (req, res, next) => { 36 | res 37 | .status(200) 38 | .type('application/json') 39 | .send(res.locals); 40 | }); 41 | 42 | app.listen(PORT, (err) => { 43 | console.log(`Listening on port ${PORT}....`); 44 | }); 45 | -------------------------------------------------------------------------------- /client/Components/LandingSections/Nav.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { WhiteLogo } from '../../assets/novaFullLogo.jsx'; 3 | 4 | 5 | export const setLinks = (name = '', styles = {}) => { 6 | const links = ['fa-envelope','fa-product-hunt', 'fa-twitter', 'fa-github']; 7 | const link = ['mailto:nova.introspection@gmail.com', 'https://www.producthunt.com/posts/nova-5', 'https://twitter.com/nova_introspect', 'https://github.com/nova-introspection/Nova']; 8 | return links.map((item, i) => { 9 | const icon = i === 0 ? 'fas' : 'fab'; 10 | return ( 11 |
12 | 13 |
14 | ) 15 | }); 16 | } 17 | 18 | const Nav = (props) => { 19 | return ( 20 | 35 | ) 36 | } 37 | export default Nav; -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const CompressionPlugin = require('compression-webpack-plugin'); 4 | 5 | module.exports = env => ({ 6 | mode: env.NODE_ENV, 7 | entry: path.resolve(__dirname, './client/index.jsx'), 8 | output: { 9 | path: path.resolve(__dirname, './dist'), 10 | filename: 'bundle.js', 11 | }, 12 | module: { 13 | rules: [ 14 | { 15 | test: /(.jsx|.js)?/, 16 | use: { 17 | loader: 'babel-loader', 18 | query: { 19 | presets: [ 20 | '@babel/preset-env', 21 | '@babel/preset-react', 22 | ], 23 | }, 24 | }, 25 | }, 26 | { 27 | test: /(.css | .scss)$/, 28 | use: ['style-loader', 'css-loader', 'style-loader'], 29 | }, 30 | ], 31 | }, 32 | resolve: { 33 | extensions: ['.js', '.jsx'], 34 | }, 35 | plugins: [ 36 | new webpack.HotModuleReplacementPlugin(), 37 | new CompressionPlugin(), 38 | ], 39 | devServer: { 40 | host: '0.0.0.0', 41 | port: 8080, 42 | hot: true, 43 | publicPath: '/dist/', 44 | historyApiFallback: true, 45 | inline: true, 46 | headers: { 'Access-Control-Allow-Origin': '*' }, 47 | proxy: { 48 | '/api/**': { 49 | target: 'http://localhost:3000/', 50 | secure: false, 51 | }, 52 | }, 53 | }, 54 | }); 55 | -------------------------------------------------------------------------------- /client/Components/LandingSections/Examples.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import LoadingModal from './LoadingModal.jsx'; 3 | 4 | const Examples = (props) => { 5 | const [active, changeActive] = useState(''); 6 | useEffect(() => changeActive(''), []); 7 | const list = ['Pokemon', 'Countries', 'Swapi', 'Catalysis Hub']; 8 | const colors = ['is-primary', 'is-warning', 'is-info', 'is-light', 'is-dark']; 9 | const demoLinks = []; 10 | demoLinks[0] = 'https://pokeapi-graphiql.herokuapp.com/'; 11 | demoLinks[1] = 'https://countries.trevorblades.com/'; 12 | demoLinks[2] = 'https://swapi.apis.guru'; 13 | demoLinks[3] = 'http://api.catalysis-hub.org/graphql'; 14 | const display = list.map((item, i) => { 15 | return ( 16 |
17 |
{ 20 | changeActive('is-active'); 21 | props.handleUrlClick(demoLinks[i], props.history, {button: true}) 22 | }} 23 | > 24 |

{item}

25 |
26 | 27 |
28 | ) 29 | }); 30 | return ( 31 |
32 |

Demo

33 |
34 | {display} 35 |
36 |
37 | ) 38 | } 39 | 40 | export default Examples; -------------------------------------------------------------------------------- /server/IntroQuery.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | JSON_STRING: { 3 | operationName: 'IntrospectionQuery', 4 | variables: {}, 5 | query: `query IntrospectionQuery { 6 | __schema { 7 | types { 8 | ...FullType 9 | } 10 | } 11 | } 12 | fragment FullType on __Type { 13 | kind 14 | name 15 | description 16 | fields { 17 | name 18 | description 19 | args { 20 | ...InputValue 21 | } 22 | type { 23 | ...TypeRef 24 | } 25 | } 26 | inputFields { 27 | ...InputValue 28 | } 29 | enumValues { 30 | name 31 | description 32 | } 33 | possibleTypes { 34 | ...TypeRef 35 | } 36 | } 37 | fragment InputValue on __InputValue { 38 | name 39 | description 40 | type { 41 | ...TypeRef 42 | } 43 | defaultValue 44 | } 45 | fragment TypeRef on __Type { 46 | kind 47 | name 48 | ofType { 49 | kind 50 | name 51 | ofType { 52 | kind 53 | name 54 | ofType { 55 | kind 56 | name 57 | ofType { 58 | kind 59 | name 60 | ofType { 61 | kind 62 | name 63 | ofType { 64 | kind 65 | name 66 | ofType { 67 | kind 68 | name 69 | } 70 | } 71 | } 72 | } 73 | } 74 | } 75 | } 76 | }` 77 | } 78 | }; 79 | -------------------------------------------------------------------------------- /client/Components/LandingSections/Contact.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { setLinks } from './Nav.jsx'; 3 | 4 | const Contact = (props) => { 5 | const types = ['text', 'email']; 6 | const title = ['Name', 'Email']; 7 | const icon = ['fas fa-user', 'fas fa-envelope']; 8 | const label = types.map((item, i) => ( 9 |
10 |
11 | 12 | 13 | 14 | 15 |
16 |
17 | )) 18 | return ( 19 |
20 |

Let us know if you have any questions!

21 |
22 |
23 | {label} 24 |
25 |
26 | 27 | 28 |
29 |
30 | 31 |
32 |
33 |
34 | 37 |
38 |
39 |
40 |
41 | ) 42 | } 43 | 44 | export default Contact; -------------------------------------------------------------------------------- /client/Components/Setting.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import graphSetup from '../dthreeHelpers/graphSetup'; 3 | import { withRouter } from 'react-router-dom'; 4 | import { color } from '../dthreeHelpers/graphFunctions'; 5 | 6 | const styling = { 7 | color: 'hsl(0, 0%, 86%)', 8 | fontSize: '12px' 9 | } 10 | 11 | const Setting = (props) => { 12 | const { active, history } = props; 13 | const display = (active) ? 'is-active' : 'none'; 14 | const levels = [0,1,2,3,4,5,6,7,8].map((num, i) => { 15 | const des = (i === 0) ? ' - Root' : ` - Level ${num} out`; 16 | return ( 17 |
18 |
{des}
19 |
20 | ); 21 | }); 22 | return ( 23 |
24 | 42 |
43 | ) 44 | }; 45 | 46 | export default withRouter(Setting); -------------------------------------------------------------------------------- /client/Components/Sidebar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { 4 | Menu, 5 | Segment, 6 | Sidebar, 7 | } from 'semantic-ui-react'; 8 | import Field from './Field'; 9 | 10 | const styles = { 11 | sidebar: { 12 | height: '100%', 13 | background: 'transparent', 14 | }, 15 | }; 16 | 17 | const MySidebar = (props) => { 18 | const { visible, type, changeType, root, data } = props; 19 | let fields; 20 | if (type.fields) { 21 | fields = type.fields.map(currentField => ( 22 | 23 | changeType(currentField.type.name)} 28 | /> 29 | )); 30 | } else { 31 | fields = []; 32 | } 33 | 34 | return ( 35 | 49 |
50 |
changeType(root)} style={{height: '80px', color: '#FFF', fontSize: '19px', padding: '40px 0px', cursor: 'pointer' }}> 51 | { type.name } 52 |
53 |
54 | Fields 55 |
56 |
57 | { fields } 58 |
59 |
60 |
61 | ); 62 | }; 63 | 64 | export default MySidebar; 65 | -------------------------------------------------------------------------------- /client/Components/Visualizer.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { withRouter } from 'react-router-dom'; 3 | import Overlay from './Overlay'; 4 | import Graph from './Graph'; 5 | import NoSession from './NoSession'; 6 | 7 | const Visualizer = (props) => { 8 | if (sessionStorage.getItem('schema') === null) { return ; } 9 | const { schemaGraph, location } = props; // can be updated to props destructuring 10 | const rootName = schemaGraph.nodes[0].name; 11 | const { nodes } = schemaGraph; 12 | const [sidebarActive, useSidebar] = useState(false); 13 | const [currentType, useCurrentType] = useState(nodes[0]); 14 | 15 | useEffect(() => { 16 | window.scrollTo(0, 0); 17 | const body = document.querySelector('body'); 18 | body.classList.add('hid'); 19 | return () => body.classList.remove('hid'); 20 | }, location.pathname); 21 | 22 | function toggleSidebar() { 23 | useSidebar(!sidebarActive); 24 | } 25 | 26 | function changeType(typeName) { 27 | for (let i = 0; i < nodes.length; i += 1) { 28 | if (nodes[i].name === typeName) { 29 | useCurrentType(nodes[i]); 30 | useSidebar(true); 31 | break; 32 | } 33 | } 34 | } 35 | 36 | const styles = { 37 | background: '#212121', 38 | width: '100vw', 39 | height: '100vh', 40 | display: 'flex', 41 | }; 42 | 43 | const active = (sidebarActive) ? 'activeColor' : ''; 44 | return ( 45 |
46 | 55 | 56 |
57 | ); 58 | }; 59 | 60 | export default withRouter(Visualizer); 61 | -------------------------------------------------------------------------------- /__tests__/linkedList.test.js: -------------------------------------------------------------------------------- 1 | const LinkedList = require('../server/linkedList/linkedList'); 2 | 3 | describe('Testing LinkedList implementation', () => { 4 | let ll = null; 5 | beforeEach(() => { 6 | ll = new LinkedList(); 7 | ll.enqueue(1); 8 | ll.enqueue(2); 9 | }); 10 | describe('Enqueue Prototype', () => { 11 | test('Appends to list correctly', () => { 12 | expect(ll.head.value).toBe(1); 13 | expect(ll.head.next.value).toBe(2); 14 | }); 15 | }); 16 | describe('Dequeue Prototype', () => { 17 | test('Removes from list correctly', () => { 18 | expect(ll.dequeue()).toBe(1); 19 | expect(ll.head.value).toBe(2); 20 | }); 21 | test('Dequeue returns null when size is already at 0', () => { 22 | expect(ll.dequeue()).toBe(1); 23 | expect(ll.dequeue()).toBe(2); 24 | expect(ll.dequeue()).toEqual(null); 25 | }); 26 | }); 27 | describe('Size of LinkedList', () => { 28 | test('Size changes correctly as list grows and shrinks', () => { 29 | expect(ll.size).toBe(2); 30 | ll.enqueue(3); 31 | expect(ll.size).toBe(3); 32 | ll.dequeue(); 33 | expect(ll.size).toBe(2); 34 | }); 35 | test('Size does not go below 0', () => { 36 | ll.dequeue(); 37 | expect(ll.size).toBe(1); 38 | ll.dequeue(); 39 | expect(ll.size).toBe(0); 40 | ll.dequeue(); 41 | expect(ll.size).toBe(0); 42 | ll.enqueue(1); 43 | expect(ll.size).toBe(1); 44 | }); 45 | }); 46 | describe('isEmpty Prototype', () => { 47 | test('Returns false when there are items', () => { 48 | expect(ll.isEmpty()).toBe(false); 49 | }); 50 | test('Returns true when there are no items', () => { 51 | ll.dequeue(); 52 | ll.dequeue(); 53 | expect(ll.isEmpty()).toBe(true); 54 | }); 55 | }); 56 | afterEach(() => { ll = null; }); 57 | }); -------------------------------------------------------------------------------- /client/Components/Field.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Menu } from 'semantic-ui-react'; 3 | import f from '../dthreeHelpers/graphSetup'; 4 | 5 | const styles = { 6 | container: { 7 | textAlign: 'left', 8 | minHeight: '1.5em', 9 | fontSize: '16px', 10 | }, 11 | field: { 12 | borderBottom: 'solid 1px #2a2a2a', 13 | padding: '10px', 14 | cursor: 'pointer', 15 | }, 16 | description: { 17 | borderBottom: 'solid 1px #2a2a2a', 18 | padding: '10px 10px 10px 25px', 19 | background: 'hsl(0, 0%, 17%)', 20 | }, 21 | }; 22 | 23 | const Field = (props) => { 24 | const { field, handleClick, data } = props; 25 | const [showDesc, useDesc] = useState(false); 26 | let fieldType = field.type.name; 27 | 28 | if (field.isList) fieldType = `[${fieldType}]`; 29 | if (field.isRequired) fieldType = `${fieldType}!`; 30 | 31 | function toggleDesc() { 32 | useDesc(!showDesc); 33 | } 34 | 35 | function activeIcon() { 36 | if (showDesc) return 'fa-caret-up'; 37 | return 'fa-caret-down'; 38 | } 39 | 40 | function typeColor() { 41 | switch (field.type.name) { 42 | case 'String': 43 | return 'string'; 44 | case 'Int': 45 | return 'int'; 46 | case 'Boolean': 47 | return 'bool'; 48 | default: 49 | return 'type'; 50 | } 51 | } 52 | 53 | return ( 54 |
55 |
56 |
59 | {field.name}:

{f.fade(field.type.name); handleClick(field.type.name)} } 61 | className={typeColor()} 62 | > 63 | {fieldType} 64 | 65 | 66 |
67 |
68 | {showDesc && (
69 | {field.description || 'Description not available.'} 70 |
)} 71 |
72 | ); 73 | }; 74 | 75 | export default Field; 76 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Nova", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "jest --verbose", 8 | "dev": "webpack-dev-server --env.NODE_ENV=development", 9 | "dev:hot": "NODE_ENV=development REDIS_URI=redis://redis:6379 node ./server/server.js & webpack-dev-server --env.NODE_ENV=development --hot --inline --progress --colors --watch --content-base ./", 10 | "docker-dev:hot": "docker-compose -f docker-compose-dev-hot.yml up", 11 | "build": "webpack --env.NODE_ENV=production", 12 | "prestart": "npm run build", 13 | "start": "REDIS_URI=redis://redis:6379 node server/server.js" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/nova-introspection/Nova.git" 18 | }, 19 | "keywords": [], 20 | "author": "", 21 | "license": "ISC", 22 | "bugs": { 23 | "url": "https://github.com/nova-introspection/Nova/issues" 24 | }, 25 | "homepage": "https://github.com/nova-introspection/Nova#readme", 26 | "devDependencies": { 27 | "@babel/core": "^7.4.3", 28 | "@babel/preset-env": "^7.4.3", 29 | "@babel/preset-react": "^7.0.0", 30 | "babel-loader": "^8.0.5", 31 | "eslint": "^5.16.0", 32 | "eslint-config-airbnb": "^17.1.0", 33 | "eslint-plugin-import": "^2.17.1", 34 | "eslint-plugin-jsx-a11y": "^6.2.1", 35 | "eslint-plugin-react": "^7.12.4", 36 | "jest": "^24.7.1", 37 | "webpack": "^4.30.0", 38 | "webpack-cli": "^3.3.0", 39 | "webpack-dev-server": "^3.3.1" 40 | }, 41 | "dependencies": { 42 | "body-parser": "^1.18.3", 43 | "compression-webpack-plugin": "^2.0.0", 44 | "cors": "^2.8.5", 45 | "css-loader": "^2.1.1", 46 | "d3": "^5.9.2", 47 | "express": "^4.16.4", 48 | "node-fetch": "^2.3.0", 49 | "normalize-url": "^4.3.0", 50 | "react": "^16.8.6", 51 | "react-dom": "^16.8.6", 52 | "react-router-dom": "^5.0.0", 53 | "redis": "^2.8.0", 54 | "semantic-ui-css": "^2.4.1", 55 | "semantic-ui-react": "^0.86.0", 56 | "style-loader": "^0.23.1", 57 | "url-parse": "^1.4.7" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /client/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; 3 | import normalizeUrl from 'normalize-url'; 4 | import Landing from './Components/Landing'; 5 | import Visualizer from './Components/Visualizer'; 6 | import NotFound from './Components/404'; 7 | 8 | const SERVER_URI = '/api/schema'; 9 | 10 | const App = () => { 11 | const [schema, setSchema] = useState(JSON.parse(sessionStorage.getItem('schema')) || null); 12 | const [invalidSchema, setInvalidSchema] = useState(false); 13 | const [loadingState, setLoadingState] = useState(false); 14 | const handleUrlClick = (url, history, e = { key: false, button: false }) => { 15 | if (e.key === 'Enter' || e.button === true) { 16 | const normalized = normalizeUrl(url, {forceHttps: true}); 17 | setInvalidSchema(false); 18 | setLoadingState(true); 19 | const postBody = { uri: normalized }; 20 | fetch(SERVER_URI, { 21 | method: 'POST', 22 | mode: 'cors', 23 | headers: { 24 | 'Content-Type': 'application/json', 25 | }, 26 | body: JSON.stringify(postBody), 27 | }) 28 | .then(res => res.json()) 29 | .then((data) => { 30 | setSchema(data); 31 | return data; 32 | }) 33 | .then((data) => { 34 | sessionStorage.setItem('schema', JSON.stringify(data)); 35 | setLoadingState(false); 36 | history.push('/visualizer'); 37 | }) 38 | .catch(err => setInvalidSchema(true)); 39 | } 40 | }; 41 | 42 | return ( 43 | 44 |
45 | 46 | ( 50 | 56 | )} 57 | /> 58 | } 61 | /> 62 | 63 | 64 |
65 |
66 | ); 67 | }; 68 | 69 | export default App; 70 | -------------------------------------------------------------------------------- /client/Components/Landing.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Nav from './LandingSections/Nav.jsx'; 3 | import Examples from './LandingSections/Examples.jsx'; 4 | import Description from './LandingSections/Description.jsx'; 5 | // import Contact from './LandingSections/Contact.jsx'; 6 | import Footer from './LandingSections/Footer.jsx'; 7 | 8 | const HeroFoot = (props) => ( 9 |
10 |
11 |

Demos down below!

12 |
13 |
14 | ) 15 | 16 | const Landing = (props) => { 17 | const [urlText, setUrlText] = useState(''); 18 | // conditional rendering for loading/error text 19 | let loadingText; 20 | if (props.invalidSchema) { 21 | loadingText =

Invalid GraphQL endpoint, please try again

; 22 | } else if (props.loadingState) { 23 | loadingText =

Processing GraphQL Schema...

; 24 | } else { loadingText =

; } 25 | const fetching = (props.loadingState && !props.invalidSchema) ? 'is-loading' : ''; 26 | return ( 27 |
28 |
66 | ); 67 | }; 68 | 69 | export default Landing; 70 | -------------------------------------------------------------------------------- /server/GraphBuild.js: -------------------------------------------------------------------------------- 1 | const LinkedList = require('./linkedList/linkedList'); 2 | 3 | function SchemaGraph(data) { 4 | this.nodes = []; 5 | this.links = []; 6 | this.nodeList = {}; 7 | this.adjacencyList = {}; 8 | this.addTypes(data); 9 | this.addAdjList(); 10 | this.bfsColor(); 11 | this.addLinkColor(); 12 | } 13 | 14 | SchemaGraph.prototype.addLinkColor = function addLinkColor() { 15 | const path = this.links; 16 | path.forEach((item) => { 17 | const { source } = item; 18 | item.color = this.nodeList[source].color; 19 | }); 20 | }; 21 | 22 | SchemaGraph.prototype.bfsColor = function bfsColor() { 23 | const list = this.adjacencyList; 24 | const data = this.nodeList; 25 | const queue = new LinkedList(); 26 | 27 | const root = Object.keys(list)[0]; 28 | queue.enqueue(root); 29 | data[root].color = 0; 30 | 31 | while (!queue.isEmpty()) { 32 | const type = queue.dequeue(); 33 | const { color } = data[type]; 34 | list[type].forEach((item) => { 35 | if (!data[item]) return; 36 | if (data[item].color) return; 37 | data[item].color = color + 1; 38 | queue.enqueue(item); 39 | }); 40 | } 41 | }; 42 | 43 | SchemaGraph.prototype.addAdjList = function addAdjList() { 44 | const types = Object.keys(this.nodeList); 45 | types.forEach((type) => { 46 | this.adjacencyList[type] = []; 47 | const sourceFilter = this.links.filter(item => item.source === type); 48 | sourceFilter.forEach(item => this.adjacencyList[type].push(item.target)); 49 | }); 50 | }; 51 | 52 | SchemaGraph.prototype.addTypes = function addTypes(data) { 53 | data.forEach((type) => { 54 | if (type.name[0] !== '_' && type.name[1] !== '_' && type.kind === 'OBJECT' && type.name !== 'Mutation') { 55 | const thisType = new Type(type); 56 | const { fields } = thisType; 57 | 58 | if (fields) { 59 | thisType.fields = fields.map((field) => { 60 | const f = { 61 | name: field.name, 62 | description: field.description, 63 | defaultValue: field.defaultValue, 64 | args: field.args, 65 | isRequired: false, 66 | isList: false, 67 | type: {}, 68 | }; 69 | 70 | updateTypes(f, field); 71 | 72 | if (f.type.kind === 'OBJECT' && Object.keys(f.type).length) { 73 | this.links.push(new Edge(thisType.name, f.type.name)); 74 | } 75 | return f; 76 | }); 77 | } 78 | this.nodeList[thisType.name] = thisType; 79 | this.nodes.push(thisType); 80 | } 81 | }); 82 | }; 83 | 84 | function Edge(source, target, color) { 85 | this.source = source; 86 | this.target = target; 87 | } 88 | 89 | function Type(type) { 90 | const { 91 | kind, name, description, fields, 92 | } = type; 93 | this.kind = kind; 94 | this.name = name; 95 | this.description = description; 96 | this.fields = fields; 97 | } 98 | 99 | function updateTypes(objectToUpdate, data) { 100 | let { type } = data; 101 | 102 | while (type !== null) { 103 | const { kind, name } = type; 104 | 105 | if (kind === 'NON_NULL') { 106 | objectToUpdate.isRequired = true; 107 | } else if (kind === 'LIST') { 108 | objectToUpdate.isList = true; 109 | } else { 110 | objectToUpdate.type.kind = kind; 111 | objectToUpdate.type.name = name; 112 | } 113 | 114 | type = type.ofType; 115 | } 116 | } 117 | 118 | module.exports = { SchemaGraph }; 119 | 120 | // b = new SchemaGraph(a.data.__schema.types) 121 | -------------------------------------------------------------------------------- /client/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | html { height: 100%; overflow: hidden; } 8 | .hid { overflow: hidden; } 9 | button:focus { outline: none; } 10 | button { 11 | padding: 9px 17px; 12 | margin:7.5px; 13 | border: 1px solid gray; 14 | color: gray; 15 | background: white; 16 | border-radius: 5px; 17 | cursor: pointer; 18 | transition: border .2s, color .2s; 19 | } 20 | button:hover { 21 | color:black; 22 | border: 1px solid black; 23 | } 24 | 25 | /*LOGO STYLING 26 | =================================*/ 27 | .fullLogo { 28 | fill: none; 29 | stroke: #000; 30 | stroke-miterlimit: 10; 31 | stroke-width: 1.5px; 32 | } 33 | .cls-1 { fill: #fff; } 34 | .cls-2 { 35 | fill: none; 36 | stroke: #fff; 37 | stroke-miterlimit:10; 38 | stroke-width:1.5px; 39 | } 40 | /*===============================*/ 41 | 42 | /*LANDING STYLING 43 | =================================*/ 44 | 45 | .landingContainer { 46 | height: 100vh; 47 | width: 100vw; 48 | display: flex; 49 | align-items: center; 50 | justify-content: center; 51 | flex-direction: column; 52 | } 53 | 54 | #landingInput { 55 | margin: 30px 0; 56 | width: 80%; 57 | } 58 | #landingInput:focus { 59 | outline: none; 60 | box-shadow: 1.2px 1.2px 2.5px rgb(252, 221, 221); 61 | } 62 | 63 | .loading { 64 | font-size: 14px; 65 | font-style: italic; 66 | color: #333; 67 | } 68 | 69 | .invalid { 70 | margin: 0px; 71 | font-size: 14px; 72 | font-weight: bold; 73 | text-decoration: underline; 74 | font-style: italic; 75 | color: #D8000C; 76 | } 77 | 78 | .navbar-item > a { color: white; } 79 | 80 | /*===============================*/ 81 | 82 | .fa-lg:hover { 83 | color: turquoise; 84 | cursor: pointer; 85 | } 86 | 87 | .overlay { 88 | position: fixed; 89 | left: 0; 90 | right: 0; 91 | top: 0; 92 | bottom: 0; 93 | pointer-events: none; 94 | } 95 | 96 | .whitesmoke { color: whitesmoke; } 97 | .activeColor { color: turquoise !important; } 98 | .clickable { cursor: pointer; } 99 | .fixed { position: fixed; } 100 | .pointerEventsAuto { pointer-events: auto; } 101 | .icon { color: #FFF; } 102 | .is-divider { 103 | width: 100%; 104 | margin: 15px 0; 105 | border: 1px solid aliceblue; 106 | } 107 | 108 | .options { 109 | top: 12px; 110 | right: 20px; 111 | } 112 | 113 | .optionsBox { 114 | top: 35px; 115 | right: 20px; 116 | } 117 | 118 | .menu { 119 | top: 12px; 120 | left: 20px; 121 | z-index: 100; 122 | } 123 | 124 | .sidebarContainer { height: 100%; } 125 | .sidebar { 126 | height: 100%; 127 | background: transparent; 128 | } 129 | 130 | .settingsContainer { 131 | width: 150px; 132 | height: auto; 133 | background: whitesmoke; 134 | border:1px solid whitesmoke; 135 | border-radius: 5px; 136 | } 137 | 138 | .settingsBtn { 139 | width: 100%; 140 | padding: 5px 0; 141 | background: whitesmoke; 142 | border: 1px solid whitesmoke; 143 | } 144 | .btnContainer > p { 145 | margin: 0; 146 | padding-left: 5px; 147 | } 148 | .modal-background { background-color: rgba(15, 15, 25, 0.45) !important; } 149 | .colorScheme > div { 150 | width:11px; 151 | height:11px; 152 | border-radius: 50%; 153 | display: inline-block; 154 | border: 1px solid black; 155 | } 156 | .btnContainer { margin: 10px 0; width: 100%; } 157 | .settingsBtn:hover { background: #e6e5e5 } 158 | 159 | .overlay>div>div>p>.fas { 160 | color: #fff !important; 161 | } 162 | 163 | @media(max-width:500px) { 164 | #introspect { font-size: 26px; } 165 | } 166 | 167 | .string { 168 | color: #e04050; 169 | } 170 | 171 | .int { 172 | color: #50e040; 173 | } 174 | 175 | .type { 176 | color: hsl(217, 71%, 53%); 177 | } 178 | 179 | .bool { 180 | color: hsl(48, 100%, 67%); 181 | } -------------------------------------------------------------------------------- /client/dthreeHelpers/graphFunctions.js: -------------------------------------------------------------------------------- 1 | import * as d3 from 'd3'; 2 | 3 | export const color = d3.scaleOrdinal(d3.schemePastel1); 4 | 5 | export default { 6 | links: { 7 | addArrows: (g) => { 8 | g.append('svg:defs').selectAll('marker') 9 | .data(['end-arrow']) // Different link/path types can be defined here 10 | .enter() 11 | .append('svg:marker') // This section adds in the arrows 12 | .attr('id', String) 13 | .attr('viewBox', '0 -5 10 10') 14 | .attr('refX', 10) 15 | .attr('refY', 0) 16 | .attr('markerWidth', 6) 17 | .attr('markerHeight', 6) 18 | .attr('orient', 'auto') 19 | .append('svg:path') 20 | .attr('d', 'M0,-5L10,0L0,5') 21 | .style('fill', 'whitesmoke') 22 | .style('opacity', d => d.opacity); 23 | }, 24 | addPath: (link) => { 25 | link.attr('class', 'link') 26 | .style('fill', 'transparent') 27 | .style('stroke', (d) => { 28 | const col = (d.color) ? d.color : 0; 29 | return d3.rgb(color(col)).darker(0.5); 30 | }) 31 | // .attr('id', d => { return `${d.source.name} => ${d.target.name}`; }) 32 | .attr('marker-end', 'url(#end-arrow)'); 33 | }, 34 | addLinkText: (edgePaths) => { 35 | 36 | }, 37 | linkedByIndex: data => data.reduce((acc, d) => { 38 | acc[`${d.source.index},${d.target.index}`] = 1; 39 | return acc; 40 | }, {}), 41 | isConnected: (a, b, obj) => obj[`${a.index},${b.index}`] || a.index === b.index, // || obj[`${b.index},${a.index}`] 42 | }, 43 | nodes: { 44 | circleColour: (d) => { 45 | const col = (d.color) ? d.color : 0; 46 | return color(col); 47 | }, 48 | darkerStroke: (d) => { 49 | const col = (d.color) ? d.color : 0; 50 | return d3.rgb(color(col)).darker(); 51 | }, 52 | radius: d => d.radius, 53 | addTextAndTitle: (node) => { 54 | // node.append('title').text(d => d.name); 55 | node.append('text') 56 | .attr('text-anchor', 'middle') 57 | // .attr('x', '15px') 58 | // .attr('y', '-15px') 59 | .attr('dy', '.35em') 60 | .text(d => d.name) 61 | .style('fill', 'black') 62 | .style('stroke-width', 0.5) 63 | .style('pointer-events', 'none'); 64 | }, 65 | }, 66 | ticks: { 67 | positionNode: d => `translate(${d.x}, ${d.y})`, 68 | linkArrowArc: (d) => { 69 | const diffX = d.target.x - d.source.x; 70 | const diffY = d.target.y - d.source.y; 71 | const offset = 20; 72 | // Length of path from center of source node to center of target node 73 | const pathLength = Math.sqrt((diffX * diffX) + (diffY * diffY)); 74 | 75 | // x and y distances from center to outside edge of target node 76 | const offsetX = (diffX * d.target.radius) / pathLength; 77 | const offsetY = (diffY * d.target.radius) / pathLength; 78 | 79 | const midPointX = (d.source.x + d.target.x) / 2; 80 | const midPointY = (d.source.y + d.target.y) / 2; 81 | const offX = midPointX + offset * (diffY / pathLength); 82 | const offY = midPointY - offset * (diffX / pathLength); 83 | 84 | return `M${d.source.x},${d.source.y}S${offX},${offY} ${d.target.x - offsetX},${d.target.y - offsetY}`; 85 | }, 86 | linkArc: (d) => { 87 | const offset = 50; 88 | 89 | const midPointX = (d.source.x + d.target.x) / 2; 90 | const midPointY = (d.source.y + d.target.y) / 2; 91 | 92 | const dx = (d.target.x - d.source.x); 93 | const dy = (d.target.y - d.source.y); 94 | 95 | const normalise = Math.sqrt((dx * dx) + (dy * dy)); 96 | 97 | const offSetX = midPointX + offset * (dy / normalise); 98 | const offSetY = midPointY - offset * (dx / normalise); 99 | 100 | return `M${d.source.x},${d.source.y}S${offSetX},${offSetY} ${d.target.x},${d.target.y}`; 101 | }, 102 | }, 103 | zoom: { 104 | zoomActions: g => () => { 105 | g.attr('transform', d3.event.transform); 106 | }, 107 | }, 108 | drag: { 109 | dragStart: sim => (d) => { 110 | if (!d3.event.active) sim.alphaTarget(0.3).restart(); 111 | d.fx = d.x; 112 | d.fy = d.y; 113 | }, 114 | dragDrag: sim => (d) => { 115 | d.fx = d3.event.x; 116 | d.fy = d3.event.y; 117 | }, 118 | dragEnd: sim => (d) => { 119 | if (!d3.event.active) sim.alphaTarget(0); 120 | d.fx = null; 121 | d.fy = null; 122 | }, 123 | }, 124 | }; 125 | -------------------------------------------------------------------------------- /client/dthreeHelpers/graphSetup.js: -------------------------------------------------------------------------------- 1 | import * as d3 from 'd3'; 2 | import { equal } from 'assert'; 3 | import graphFunctions from './graphFunctions'; 4 | 5 | const store = { 6 | g: null, 7 | node: null, 8 | link: null, 9 | data: null, 10 | }; 11 | 12 | export default { 13 | reset: () => { 14 | d3.select('g') 15 | .transition() 16 | .duration(250) 17 | .attr('transform', 'translate(0,0)scale(1)'); 18 | d3.zoom().transform(d3.select('#graph'), d3.zoomIdentity.scale(1)); 19 | }, 20 | setup: (data, handleClick) => { 21 | graphFunctions.nodes.clickNode = (d) => { 22 | handleClick(d.name); 23 | fade(0.25); 24 | }; 25 | const color = d3.scaleOrdinal(d3.schemePastel1); 26 | // const radius = d3.scaleSqrt().range([0, 6]); 27 | const svg = d3.select('#graph'); 28 | const xCenter = [400, 1400, 2400, 3400]; 29 | const width = +svg.attr('width'); 30 | const height = +svg.attr('height'); 31 | const simulation = d3.forceSimulation().nodes(data.nodes); 32 | const linkForce = d3 33 | .forceLink(data.links) 34 | .id(d => d.name) 35 | .distance(d => d.source.radius + d.target.radius + 800); 36 | // .distance(function(d) { return radius(d.source.value / 2) + radius(d.target.value / 2); }); 37 | const chargeForce = d3.forceManyBody().strength(-300); 38 | const centerForce = d3.forceCenter(width / 2, height / 2); 39 | const collideForce = d3.forceCollide().radius(d => d.radius * 1.5); 40 | simulation 41 | .force('charge', chargeForce) 42 | .force('collide', collideForce) 43 | .force('center', centerForce) 44 | .force('links', linkForce) 45 | // .force('forceX', d3.forceX(width / 2).strength(0.025)) 46 | // .force('forceY', d3.forceY(height / 2).strength(0.025)) 47 | .force('x', d3.forceX().x((d) => { 48 | if (d.color === undefined) return 0; 49 | const num = (d.color > 3) ? 3 : d.color; 50 | return xCenter[num]; 51 | })) 52 | .force('y', d3.forceY().y(d => 0)) 53 | // .force("forceX",d3.forceX(width/2).strength(function(d){ return (!d.notLinked) ? 0 : 0.05; }) ) 54 | // .force("forceY",d3.forceY(height/2).strength(function(d){ return (!d.notLinked) ? 0 : 0.05; }) ) 55 | simulation.on('tick', tickActions); 56 | 57 | const g = d3.select('.everything'); 58 | 59 | const link = g 60 | .append('svg:g') 61 | .selectAll('path') 62 | .data(data.links) 63 | .enter() 64 | .append('svg:path'); 65 | 66 | graphFunctions.links.addPath(link); 67 | 68 | const node = g 69 | .append('g') 70 | .selectAll('circle') 71 | .data(data.nodes) 72 | .enter() 73 | .append('g'); 74 | 75 | store.g = g; 76 | store.data = data; 77 | store.link = link; 78 | store.node = node; 79 | node 80 | .append('circle') 81 | .attr('class', 'nodes') 82 | .attr('r', graphFunctions.nodes.radius) 83 | .attr('fill', graphFunctions.nodes.circleColour) 84 | .style('stroke', graphFunctions.nodes.darkerStroke) 85 | .style('stroke-width', 3) 86 | // .on('click', graphFunctions.nodes.clickNode) 87 | .on('click', (d) => { 88 | fade(0.04)(d); 89 | graphFunctions.nodes.clickNode(d); 90 | }) 91 | // .on('click', fade(0.25)) 92 | .on('mouseover', () => { 93 | document.querySelector('body').style.cursor = 'pointer'; 94 | }) 95 | .on('mouseout', () => { 96 | document.querySelector('body').style.cursor = 'default'; 97 | }); 98 | 99 | function equalToEventTarget() { 100 | return this == d3.event.target; 101 | } 102 | 103 | svg.on('click', () => { 104 | const circles = d3.selectAll('circle'); 105 | const outsideCircles = circles.filter(equalToEventTarget).empty(); 106 | if (outsideCircles) { 107 | fade(1)(node); 108 | } 109 | }); 110 | 111 | graphFunctions.nodes.addTextAndTitle(node); 112 | 113 | const dragHandler = d3 114 | .drag() 115 | .on('start', graphFunctions.drag.dragStart(simulation)) 116 | .on('drag', graphFunctions.drag.dragDrag(simulation)) 117 | .on('end', graphFunctions.drag.dragEnd(simulation)); 118 | dragHandler(node); 119 | 120 | const zoomHandler = d3 121 | .zoom() 122 | .scaleExtent([0.1, 7]) 123 | .on('zoom', graphFunctions.zoom.zoomActions(g)); 124 | zoomHandler(svg); 125 | zoomHandler.scaleTo(svg, 0.5); 126 | svg.on('dblclick.zoom', null); 127 | 128 | function tickActions() { 129 | link.attr('d', graphFunctions.ticks.linkArrowArc); 130 | node.attr('transform', graphFunctions.ticks.positionNode); 131 | } 132 | 133 | const linkedByIndex = graphFunctions.links.linkedByIndex(data.links); 134 | 135 | function fade(opacity) { 136 | return (d) => { 137 | // update nodes opacity; loops through all nodes 138 | node.style('stroke-opacity', function (o) { 139 | const thisOpacity = graphFunctions.links.isConnected(d, o, linkedByIndex) ? 1 : opacity; 140 | this.setAttribute('fill-opacity', thisOpacity); 141 | return thisOpacity; 142 | }); 143 | node.select('circle').style('color', function (o) { 144 | const col = o.color ? o.color : 0; 145 | const bright = d3.rgb(color(col)).brighter(0.3); 146 | const original = d3.rgb(color(col)); 147 | const fill = opacity === 1 ? original : bright; 148 | this.setAttribute('fill', fill); 149 | }); 150 | link.style('stroke-opacity', o => (o.source === d ? 1 : opacity)); 151 | link.attr('marker-end', o => (opacity === 1 || o.source === d ? 'url(#end-arrow)' : 'url(#end-arrow-fade)')); 152 | }; 153 | } 154 | }, 155 | fade(typeName) { 156 | // const g = d3.select('.everything'); 157 | 158 | const color = d3.scaleOrdinal(d3.schemePastel1); 159 | 160 | const { g, node, link, data } = store; 161 | 162 | const linkedByIndex = graphFunctions.links.linkedByIndex(data.links); 163 | 164 | function fade(opacity) { 165 | console.log('fade called'); 166 | return (d) => { 167 | // update nodes opacity; loops through all nodes 168 | node.style('stroke-opacity', function (o) { 169 | const thisOpacity = graphFunctions.links.isConnected(d, o, linkedByIndex) ? 1 : opacity; 170 | this.setAttribute('fill-opacity', thisOpacity); 171 | return thisOpacity; 172 | }); 173 | node.select('circle').style('color', function (o) { 174 | const col = o.color ? o.color : 0; 175 | const bright = d3.rgb(color(col)).brighter(0.3); 176 | const original = d3.rgb(color(col)); 177 | const fill = opacity === 1 ? original : bright; 178 | this.setAttribute('fill', fill); 179 | }); 180 | link.style('stroke-opacity', o => (o.source === d ? 1 : opacity)); 181 | link.attr('marker-end', o => (opacity === 1 || o.source === d ? 'url(#end-arrow)' : 'url(#end-arrow-fade)')); 182 | }; 183 | } 184 | 185 | d3.selectAll('circle').each((d) => { 186 | if (d.name === typeName) { 187 | console.log('its a me'); 188 | fade(0.04)(d); 189 | } 190 | }); 191 | }, 192 | }; 193 | -------------------------------------------------------------------------------- /client/assets/novaFullLogo.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const WhiteLogo = ({ width }) => ( 4 | 8 | 9 | whiteLogo 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ) 24 | 25 | const Logo = ({ width }) => ( 26 | 30 | novaFullLogo 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | ); 46 | export default Logo; 47 | 48 | export const NovaCircle = ({ width }) => ( 49 | 53 | 54 | novaCircle 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | ); 65 | 66 | export const Nova = ({ width }) => ( 67 | 71 | nova 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | ); 82 | export const OuterCircle = ({ width }) => ( 83 | 87 | 88 | outerCircle 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | ); 97 | export const InnerCircle = ({ width }) => ( 98 | 102 | 103 | innerCircle 104 | 105 | 106 | 107 | 108 | 109 | 110 | ); 111 | export const InnerFullCircle = ({ width }) => ( 112 | 116 | innerFullCircle 117 | 118 | 119 | 120 | 121 | 122 | 123 | ); --------------------------------------------------------------------------------