├── .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 |
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 |

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 |
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 |  |
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 |
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 |
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 |
29 |
30 |
31 |
32 |
33 |
34 | Introspect an endpoint!
35 |
36 |
37 |
38 |
51 | {loadingText}
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | {/* */}
63 |
64 |
65 |
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 |
23 | )
24 |
25 | const Logo = ({ width }) => (
26 |
45 | );
46 | export default Logo;
47 |
48 | export const NovaCircle = ({ width }) => (
49 |
64 | );
65 |
66 | export const Nova = ({ width }) => (
67 |
81 | );
82 | export const OuterCircle = ({ width }) => (
83 |
96 | );
97 | export const InnerCircle = ({ width }) => (
98 |
110 | );
111 | export const InnerFullCircle = ({ width }) => (
112 |
123 | );
--------------------------------------------------------------------------------