├── .babelrc
├── .eslintignore
├── .eslintrc
├── .github
└── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── .gitignore
├── .travis.yml
├── CODE_OF_CONDUCT.md
├── LICENSE
├── README.md
├── assets
└── screenshots
│ ├── destination-panel.png
│ ├── mockups-panel.png
│ └── source-panel.png
├── lintresults
├── package.json
├── public
├── icon.icns
├── icon_png.png
├── index.html
└── main.js
├── src
├── assets
│ └── no-data.svg
├── main
│ ├── components
│ │ ├── AssertionsForm.jsx
│ │ ├── Button.jsx
│ │ ├── DataTree.jsx
│ │ ├── EmptyState.jsx
│ │ ├── HeaderBar.jsx
│ │ ├── InlineForm.jsx
│ │ ├── InlineInput.jsx
│ │ ├── InlineSelect.jsx
│ │ ├── Mockup.jsx
│ │ ├── MockupName.jsx
│ │ ├── NameForm.jsx
│ │ ├── PerformanceMetrics.jsx
│ │ ├── RequestBar.jsx
│ │ ├── ResponseComponent.jsx
│ │ ├── ResponseWrapper.jsx
│ │ └── TestComponent.jsx
│ ├── containers
│ │ ├── App.jsx
│ │ ├── DataCanvas.jsx
│ │ ├── DestinationPanel.jsx
│ │ ├── MockupsPanel.jsx
│ │ ├── Panels.jsx
│ │ ├── SourcePanel.jsx
│ │ └── StyledPanel.jsx
│ ├── dummyData.js
│ ├── index.js
│ ├── lib
│ │ └── treeView
│ │ │ ├── icons.svg
│ │ │ ├── jsonTree.css
│ │ │ └── jsonTree.js
│ └── testsContext.js
└── renderer
│ └── Nodatasvg.jsx
├── test
└── spec.js
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/env",
4 | "@babel/preset-react"
5 | ],
6 | "plugins": [
7 | "react-hot-loader/babel"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | dist/**
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "airbnb-base",
3 | "parser": "babel-eslint",
4 | "rules": {
5 | "no-console": 0,
6 | "consistent-return": 0,
7 | "func-names": 0,
8 | "padded-blocks": 0,
9 | "import/no-extraneous-dependencies": 0,
10 | "no-unused-vars": 0
11 | },
12 | "env": {
13 | "browser": true,
14 | "commonjs": true,
15 | "es6": true
16 | }
17 | }
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. iOS]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Smartphone (please complete the following information):**
32 | - Device: [e.g. iPhone6]
33 | - OS: [e.g. iOS8.1]
34 | - Browser [e.g. stock browser, safari]
35 | - Version [e.g. 22]
36 |
37 | **Additional context**
38 | Add any other context about the problem here.
39 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (https://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # TypeScript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | # next.js build output
61 | .next
62 |
63 | .DS_Store
64 | .vscode
65 | .idea/
66 | package-lock.json
67 | dist
68 | release-builds/
69 |
70 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "node"
4 | before_script:
5 | - npm install
6 |
7 | cache:
8 | yarn: true
9 | directories:
10 | - node_modules
11 | script:
12 | - npm run lint
13 | # - npm run test
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at opensourcelabs1@gmail.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 OSLabs Beta
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | [](https://travis-ci.com/github/oslabs-beta/interspect)
7 | [](https://github.com/oslabs-beta/Interspect/pulls)
8 | 
9 |
10 | Interspect is an API-mocking tool that helps you ensure data interoperability between microservices and arbitrary API endpoints. [Download the app](https://github.com/oslabs-beta/Interspect/releases/download/1.0.0/Interspect-macOS-x64.zip) for macOS to get started.
11 |
12 | ## Basic usage
13 | The app is split into three, distinct panels:
14 |
15 | ### Source
16 | This is where you can define a data source that will become the foundation for your data mockups. There are a couple ways to get data into the application.
17 |
18 | 
19 |
20 | #### Send a GET request
21 | If your microservice supports `GET` requests, add your endpoint to the request bar and hit send. You’ll then be able to view the response data, along with basic performance metrics for the request.
22 |
23 | #### Send data to Interspect
24 | You can also send data to the source panel at `http://localhost:3001/posturl` with `POST`, `PUT`, `PATCH`, or `DELETE`.
25 |
26 | **Note:** The open socket may timeout after extended use. There’s an [open issue](https://github.com/oslabs-beta/Interspect/issues/94) to resolve (and we welcome contributions 😉)
27 |
28 | #### Accpeted data types
29 | Interspect works with `JSON` and `XML`-formatted data
30 |
31 | ### Mockups
32 | In the mockups panel, you can create new data scenarios from your source data. Each data tree is editable so you can delete, update, or add new data.
33 |
34 | 
35 |
36 | If your data source is a collection, you can also create new mockups from a specific index or key.
37 |
38 | #### Creating test assertions
39 | You can add test assertions to any mockup as well. Simply click **Edit Test Assertions** and choose a specific status code or status code range that you expect to receive when sending your mockup to your destination microservice.
40 |
41 | While only status code assertions are supported at launch, we welcome contributions to add additional test domains.
42 |
43 | ### Destination
44 | In the third panel, you can enter a destination endpoint for your mock data. When you send your request, each mockup you created will be sent in sequence to the destination endpoint.
45 |
46 | 
47 |
48 | Upon a successful request, you will be able to see returned status codes as well as the result of any test assertions and basic performance metrics.
49 |
50 | **Note:** HTTP Requests in the destination panel are sent 10 times to get an average of the total request time. At launch, this behavior cannot be turned off, but a [preference will be added in a future version](https://github.com/oslabs-beta/Interspect/issues/95).
51 |
52 |
53 | ----
54 | ## Authors
55 | Interspect was created by:
56 |
57 | - Joe Pavlisko [@joepav](https://github.com/joepav)
58 | - Donald Blanc [@Donaldblanc](https://github.com/Donaldblanc)
59 | - Conor Sexton [@conorsexton](https://github.com/conorsexton)
60 | - Joel Rivera [@RealJoelRivera](https://github.com/RealJoelRivera)
61 |
62 | ## Contributing
63 | We welcome pull requests and new issues. There are also several open issues that are up for grabs.
64 |
65 | ## License
66 | MIT License
67 |
68 | © 2019 Interspect
69 |
70 | Permission is hereby granted, free of charge, to any person obtaining a copy
71 | of this software and associated documentation files (the "Software"), to deal
72 | in the Software without restriction, including without limitation the rights
73 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
74 | copies of the Software, and to permit persons to whom the Software is
75 | furnished to do so, subject to the following conditions:
76 |
77 | The above copyright notice and this permission notice shall be included in all
78 | copies or substantial portions of the Software.
79 |
80 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
81 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
82 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
83 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
84 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
85 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
86 | SOFTWARE.
87 |
--------------------------------------------------------------------------------
/assets/screenshots/destination-panel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Interspect/d62f11c50cd30286e2812adb7683a14145a15315/assets/screenshots/destination-panel.png
--------------------------------------------------------------------------------
/assets/screenshots/mockups-panel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Interspect/d62f11c50cd30286e2812adb7683a14145a15315/assets/screenshots/mockups-panel.png
--------------------------------------------------------------------------------
/assets/screenshots/source-panel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Interspect/d62f11c50cd30286e2812adb7683a14145a15315/assets/screenshots/source-panel.png
--------------------------------------------------------------------------------
/lintresults:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Interspect/d62f11c50cd30286e2812adb7683a14145a15315/lintresults
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "interspect",
3 | "productName": "Interspect",
4 | "version": "1.0.0",
5 | "description": "An tool to test communication between your microservices",
6 | "main": "public/main.js",
7 | "scripts": {
8 | "test": "mocha",
9 | "start": "ELECTRON_IS_DEV=0 electron .",
10 | "build": "webpack --mode production",
11 | "dev": "webpack-dev-server --watch --hot --inline --mode development",
12 | "lint": "npx eslint '**/*.{tsx,ts,js,jsx}' --fix --quiet",
13 | "create_electron": "electron-packager . --overwrite --platform=darwin --arch=x64 --icon=public/icon.icns --prune=true --out=release-builds"
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/oslabs-beta/Interspect.git"
18 | },
19 | "keywords": [
20 | "microservices"
21 | ],
22 | "author": "Joe Pavlisko, Conor Sexton, Joel Rivera, Donald Blanc",
23 | "license": "ISC",
24 | "bugs": {
25 | "url": "https://github.com/oslabs-beta/Interspect/issues"
26 | },
27 | "homepage": "https://interspect.io/",
28 | "dependencies": {
29 | "body-parser": "^1.19.0",
30 | "chart.js": "^1.1.1",
31 | "electron": "^9.1.0",
32 | "electron-is-dev": "^1.1.0",
33 | "express": "^4.16.4",
34 | "react": "^16.8.6",
35 | "react-chartjs": "^1.2.0",
36 | "react-diff-viewer": "^1.1.0",
37 | "react-dom": "^16.8.6",
38 | "react-feather": "^1.1.6",
39 | "react-json-view": "^1.19.1",
40 | "socket.io": "^2.2.0",
41 | "socket.io-client": "^2.2.0",
42 | "styled-components": "^4.3.1",
43 | "xml2js": "^0.4.19"
44 | },
45 | "devDependencies": {
46 | "@babel/core": "^7.4.5",
47 | "@babel/preset-env": "^7.4.5",
48 | "@babel/preset-react": "^7.0.0",
49 | "@hot-loader/react-dom": "^16.8.6",
50 | "babel-core": "^6.26.3",
51 | "babel-eslint": "^10.0.1",
52 | "babel-loader": "^8.0.6",
53 | "babel-preset-env": "^1.7.0",
54 | "babel-preset-react": "^6.24.1",
55 | "chai": "^4.2.0",
56 | "chai-as-promised": "^7.1.1",
57 | "css-loader": "^2.1.1",
58 | "electron-packager": "^14.0.0",
59 | "eslint": "^5.16.0",
60 | "eslint-config-airbnb-base": "^13.1.0",
61 | "eslint-plugin-import": "^2.17.3",
62 | "file-loader": "^4.0.0",
63 | "html-webpack-plugin": "^3.2.0",
64 | "mocha": "^6.1.4",
65 | "mocha-loader": "^2.0.1",
66 | "react-hot-loader": "^4.9.0",
67 | "spectron": "^5.0.0",
68 | "style-loader": "^0.23.1",
69 | "webpack": "^4.33.0",
70 | "webpack-cli": "^3.3.2",
71 | "webpack-dev-server": "^3.6.0"
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/public/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Interspect/d62f11c50cd30286e2812adb7683a14145a15315/public/icon.icns
--------------------------------------------------------------------------------
/public/icon_png.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/Interspect/d62f11c50cd30286e2812adb7683a14145a15315/public/icon_png.png
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Interspect
9 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/public/main.js:
--------------------------------------------------------------------------------
1 | const { app, BrowserWindow } = require('electron');
2 | const isDev = require('electron-is-dev');
3 | const path = require('path');
4 | const os = require('os');
5 | const bodyParser = require('body-parser');
6 | const expressApp = require('express')();
7 | const server = require('http').Server(expressApp);
8 | const io = require('socket.io')(server);
9 |
10 | if (isDev) {
11 | console.log('Running in development');
12 | } else {
13 | console.log('Running in production');
14 | }
15 |
16 | let mainWindow;
17 |
18 | function createWindow() {
19 | expressApp.use(bodyParser.urlencoded({ extended: true }));
20 | expressApp.use(bodyParser.json());
21 |
22 | server.listen(3001);
23 |
24 | mainWindow = new BrowserWindow({
25 | width: 1200,
26 | height: 900,
27 | webPreferences: {
28 | nodeIntegration: true,
29 | webSecurity: false,
30 | },
31 | icon: './icon_png.png'
32 | });
33 |
34 | mainWindow.loadURL(isDev ? 'http://localhost:8080' : `file://${__dirname}/../dist/index.html`);
35 |
36 | if (isDev) {
37 | BrowserWindow.addDevToolsExtension(
38 | path.join(os.homedir(), './Library/Application Support/Google/Chrome/Default/Extensions/fmkadmapgofadopljbjfkapdkoienihi/3.6.0_0'),
39 | );
40 | }
41 | mainWindow.on('closed', () => {
42 | mainWindow = null;
43 | });
44 |
45 | const handleRequest = (request, response, socket) => {
46 | try {
47 | if (request.headers['content-type'].includes('json')
48 | || request.headers['content-type'].includes('xml')) {
49 | socket.emit('post_received', request.body);
50 | response.status(200);
51 | response.end();
52 | } else {
53 | response.status(400);
54 | response.end();
55 | }
56 | } catch {
57 | response.status(500);
58 | response.end();
59 | }
60 | };
61 |
62 | const poster = io.on('connection', (socket) => {
63 | console.log('a user connected');
64 |
65 | expressApp.post('/posturl', (request, response) => {
66 | handleRequest(request, response, socket);
67 | });
68 |
69 | expressApp.patch('/posturl', (request, response) => {
70 | handleRequest(request, response, socket);
71 | });
72 |
73 | expressApp.put('/posturl', (request, response) => {
74 | handleRequest(request, response, socket);
75 | });
76 |
77 | expressApp.delete('/posturl', (request, response) => {
78 | handleRequest(request, response, socket);
79 | });
80 | });
81 |
82 | io.on('disconnect', () => {
83 | console.log('a user disconnected');
84 | });
85 | }
86 |
87 | app.on('ready', createWindow);
88 |
--------------------------------------------------------------------------------
/src/assets/no-data.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 | No-Data/title>
4 | Created with Sketch.
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
--------------------------------------------------------------------------------
/src/main/components/AssertionsForm.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useContext } from 'react';
2 | import styled from 'styled-components';
3 | import { TestsContext } from '../testsContext';
4 | import Select from './InlineSelect.jsx';
5 | import InlineInput from './InlineInput.jsx';
6 | import Form from './InlineForm.jsx';
7 | import Button from './Button.jsx';
8 |
9 | // Styles
10 | const FormWrapper = styled.section`
11 | background: #F0F3F4;
12 | border-radius: 5px;
13 | box-shadow: inset 0 0 2px 0 rgba(41, 47, 50, 0.25);
14 | padding: 0.5em 1.5em;
15 | h3 {
16 | color: #555B5E;
17 | font-family: 'Halyard Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
18 | font-feature-settings: 'ss18';
19 | font-size: 1em;
20 | letter-spacing: 0.075em;
21 | text-transform: uppercase;
22 | }
23 | `;
24 |
25 | const Input = styled(InlineInput)`
26 | background: none;
27 | margin: 0 1em;
28 | padding: 0 0 0.25em 0.25em;
29 | text-align: center;
30 | `;
31 |
32 | const Fieldset = styled.fieldset`
33 | border: none;
34 | `;
35 |
36 | const Label = styled.label`
37 | color: #292F32;
38 | font-family: 'Halyard Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
39 | `;
40 |
41 | const AssertionsForm = (props) => {
42 | const { index, setMode } = props;
43 |
44 | // State
45 | const [isRange, setIsRange] = useState(true);
46 | const [minCode, setMinCode] = useState(200);
47 | const [maxCode, setMaxCode] = useState(299);
48 | const [exactCode, setExactCode] = useState(200);
49 | const [tests, setTests] = useContext(TestsContext);
50 |
51 | // Event handlers
52 | const handleChange = (event) => {
53 | const { value, name } = event.target;
54 | switch (name) {
55 | case 'rangeSelector':
56 | if (value === 'range') setIsRange(true);
57 | else setIsRange(false);
58 | break;
59 | case 'min-code':
60 | setMinCode(Number.parseInt(value, 10));
61 | break;
62 | case 'max-code':
63 | setMaxCode(Number.parseInt(value, 10));
64 | break;
65 | case 'exact-code':
66 | setExactCode(Number.parseInt(value, 10));
67 | break;
68 | default:
69 | break;
70 | }
71 | };
72 |
73 | const handleSubmit = (event) => {
74 | event.preventDefault();
75 | const testsCopy = [...tests];
76 | const status = isRange ? [minCode, maxCode] : [exactCode];
77 | testsCopy[index].expectedStatus = status;
78 | setTests(testsCopy);
79 | setMode('view');
80 | };
81 |
82 | return (
83 |
84 | Test Assertions
85 |
116 |
117 | );
118 | };
119 |
120 | export default AssertionsForm;
121 |
--------------------------------------------------------------------------------
/src/main/components/Button.jsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | let backgroundColor;
4 |
5 | const Button = styled.button`
6 | background-color: ${(props) => {
7 | if (!props.enabled) return '#BCC1C2';
8 | if (props.variation === 'positive') return '#49893E';
9 | return '#1F4E7A';
10 | }};
11 | border: none;
12 | color: ${(props) => {
13 | if (!props.enabled) return '#555B5E';
14 | if (props.variation === 'positive') return '#DEF0DB';
15 | return '#D9EDF2';
16 | }};
17 | border-radius: 3px;
18 | font-family: 'Halyard Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif;
19 | font-size: 1em;
20 | font-weight: 500;
21 | padding: 0.25em 1em;
22 | transition: all 0.15s ease-in-out;
23 |
24 | :hover {
25 | background-color: ${(props) => {
26 | if (!props.enabled) return '#BCC1C2';
27 | if (props.variation === 'positive') return '#35612E';
28 | return '#0D273F';
29 | }};
30 | cursor: ${(props) => {
31 | if (!props.enabled) return 'not-allowed';
32 | return 'pointer';
33 | }};
34 | }
35 |
36 | :active {
37 | color: #D4E6F7;
38 | outline: 0;
39 | box-shadow: none;
40 | }
41 |
42 | :focus {
43 | outline: 0;
44 | box-shadow: ${(props) => {
45 | if (!props.enabled) return '0 0 1px 2px #949C9E';
46 | if (props.variation === 'positive') return '0 0 1px 2px #DEF0DB';
47 | return '0 0 1px 2px #96C1E9';
48 | }}
49 | }
50 | `;
51 |
52 | export default Button;
53 |
--------------------------------------------------------------------------------
/src/main/components/DataTree.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactJson from 'react-json-view';
3 |
4 | const styles = {
5 | borderRadius: '5px',
6 | fontFamily: '\'IBM Plex Mono\', monospace',
7 | fontSize: '90%',
8 | maxHeight: '250px',
9 | overflow: 'auto',
10 | margin: '0.75em auto',
11 | padding: '1em',
12 | };
13 |
14 | const DataTree = (props) => {
15 | const {
16 | treeId, data, options, saveUpdatedTree,
17 | } = props;
18 | const {
19 | onAdd, onEdit, onDelete, enableClipboard,
20 | } = options;
21 |
22 | const changeObject = (src) => {
23 | saveUpdatedTree(src.updated_src, treeId, src.new_value, src.name, src.namespace);
24 | };
25 |
26 | return (
27 |
28 | {/* Tree gets rendered here after component mounts */}
29 |
40 |
41 | );
42 | };
43 |
44 | export default DataTree;
45 |
--------------------------------------------------------------------------------
/src/main/components/EmptyState.jsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const EmptyState = styled.section`
4 | align-items: center;
5 | display: flex;
6 | flex-direction: column;
7 | justify-content: middle;
8 | margin: 4em auto;
9 | h2 {
10 | color: #171A1C;
11 | font-family: 'P22 Mackinac Pro', 'Iowan Old Style', 'Palantino', Georgia, serif;
12 | font-weight: 400;
13 | text-align: center;
14 | }
15 | p {
16 | color: #949C9E;
17 | font-family: 'Halyard Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
18 | text-align: center;
19 | margin-bottom: 2em;
20 | }
21 | `;
22 |
23 | export default EmptyState;
24 |
--------------------------------------------------------------------------------
/src/main/components/HeaderBar.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Form from './InlineForm.jsx';
3 | import Select from './InlineSelect.jsx';
4 | import Input from './InlineInput.jsx';
5 |
6 | const HeaderBar = (props) => {
7 | const {
8 | header, authType, handleChange,
9 | } = props;
10 |
11 | return (
12 |
13 |
35 |
36 | );
37 | };
38 |
39 | export default HeaderBar;
40 |
--------------------------------------------------------------------------------
/src/main/components/InlineForm.jsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const InlineForm = styled.form`
4 | align-items: center;
5 | border: ${(props) => {
6 | if (props.bordered) return '1px solid #BCC1C2';
7 | return 'none';
8 | }}
9 | border-radius: 3px;
10 | display: flex;
11 | flex-shrink: 1;
12 | justify-content: flex-start;
13 | margin-top: ${(props) => {
14 | if (!props.bordered) return '1em';
15 | return 'inherit';
16 | }}
17 | transition: all 0.15s ease-in-out
18 | &:focus-within {
19 | ${(props) => {
20 | if (props.bordered) {
21 | return `border: 1px solid #949C9E;
22 | box-shadow: 0 2px 6px -2px rgba(41, 47, 50, 0.25);`;
23 | }
24 | return 'none';
25 | }}
26 | }
27 | button {
28 | border-radius: 2px;
29 | margin-left: auto;
30 | margin-right: 0.5em;
31 | // min-width: 0;
32 | }
33 | input {
34 | min-width: 0;
35 | }
36 | `;
37 |
38 | export default InlineForm;
39 |
--------------------------------------------------------------------------------
/src/main/components/InlineInput.jsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const InlineInput = styled.input`
4 | border: none;
5 | border-left: ${(props) => {
6 | if (props.bordered) return '1px solid #D8D8D8;';
7 | return 'none;';
8 | }};
9 | border-bottom: ${(props) => {
10 | if (!props.bordered) return '1px solid #D8D8D8;';
11 | return 'none;';
12 | }};
13 | color: #292F32;
14 | font-family: 'Halyard Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
15 | font-size: 1em;
16 | line-height: 1em;
17 | padding: 0.825em;
18 | width: ${(props) => {
19 | if (!props.bordered) return 'fit-content;';
20 | return '100%;';
21 | }};
22 | &:focus {
23 | outline: none;
24 | }
25 | &::placeholder {
26 | color: #BCC1C2;
27 | font-size: 0.9em;
28 | }
29 | `;
30 |
31 | export default InlineInput;
32 |
--------------------------------------------------------------------------------
/src/main/components/InlineSelect.jsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const InlineSelect = styled.select`
4 | background: none;
5 | border: none;
6 | color: #555B5E;
7 | font-family: 'Halyard Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
8 | font-size: 0.825em;
9 | margin: 0.925em 1em 0.725em 0.725em;
10 | &:focus {
11 | outline: 0;
12 | }
13 | `;
14 |
15 | export default InlineSelect;
16 |
--------------------------------------------------------------------------------
/src/main/components/Mockup.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useContext } from 'react';
2 | import NameForm from './NameForm.jsx';
3 | import MockupName from './MockupName.jsx';
4 | import DataTree from './DataTree.jsx';
5 | import Button from './Button.jsx';
6 | import AssertionsForm from './AssertionsForm.jsx';
7 | import { TestsContext } from '../testsContext';
8 |
9 |
10 | const dataTreeOptions = {
11 | onAdd: true,
12 | onEdit: true,
13 | onDelete: true,
14 | enableClipboard: false,
15 | };
16 |
17 | const Mockup = (props) => {
18 | const { test, index, saveUpdatedTree } = props;
19 |
20 | const [tests, setTests] = useContext(TestsContext);
21 |
22 | // Mode controls test assertions and name edit form—it’s set by child components
23 | const [mode, setMode] = useState('view');
24 |
25 | function setName(name, testIndex) {
26 | const testsClone = [...tests];
27 | // This if-statement just makes sure the test name wasn't just whitespace
28 | if (name.replace(/\s/g, '') !== '') testsClone[testIndex].name = name;
29 | else testsClone[testIndex].name = `Test #${testIndex + 1}`;
30 | setTests(testsClone);
31 | }
32 |
33 | function deleteTest(testIndex) {
34 | const testsClone = [];
35 | for (let i = 0; i < tests.length; i += 1) {
36 | if (i !== testIndex) testsClone.push(tests[i]);
37 | }
38 | setTests(testsClone);
39 | }
40 |
41 | return (
42 |
43 | {mode === 'editName'
44 | ?
45 | :
46 | }
47 |
54 | {mode === 'editAssertions'
55 | ?
56 | : setMode('editAssertions')}>Edit Test Assertions
57 | }
58 | deleteTest(index)}>Delete Test
59 |
60 | );
61 | };
62 |
63 | export default Mockup;
64 |
--------------------------------------------------------------------------------
/src/main/components/MockupName.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import styled from 'styled-components';
3 |
4 | // Styles
5 | const Header = styled.header`
6 | display: flex;
7 | align-items: flex-end;
8 | h3 {
9 | margin-bottom: 0;
10 | }
11 | `;
12 |
13 | const Name = styled.h3`
14 | font-family: 'Halyard Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
15 | color: #292F32;
16 | `;
17 |
18 | const EditButton = styled.button`
19 | background: none;
20 | border: none;
21 | color: #4299EA;
22 | font-family: 'Halyard Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
23 | letter-spacing: 0.05em;
24 | margin: 0 auto 0.25em 0.825em;
25 | text-transform: uppercase;
26 | opacity: ${props => (props.hovered ? 1 : 0)};
27 | transition: all 0.2s ease-in-out;
28 | &:hover { cursor: pointer; }
29 | &:active, &:focus {
30 | color: #1F4E7A;
31 | outline: 0;
32 | }
33 | `;
34 |
35 | const MockupName = (props) => {
36 | const { name, setMode } = props;
37 | // Hovered state is passed to edit button to control visibility
38 | const [hovered, setHovered] = useState(false);
39 |
40 |
41 | return (
42 | setHovered(true)} onMouseLeave={() => setHovered(false)}>
43 | {name}
44 | { setMode('editName'); }} hovered={hovered}>
45 | Edit
46 |
47 |
48 | );
49 | };
50 |
51 | export default MockupName;
52 |
--------------------------------------------------------------------------------
/src/main/components/NameForm.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useContext } from 'react';
2 | import styled from 'styled-components';
3 | import { TestsContext } from '../testsContext';
4 | import InlineForm from './InlineForm.jsx';
5 | import InlineInput from './InlineInput.jsx';
6 | import Button from './Button.jsx';
7 |
8 | // Form styles
9 | const Form = styled(InlineForm)`
10 | flex-direction: column;
11 | align-items: flex-start;
12 | margin: 1em auto;
13 | `;
14 |
15 | const Input = styled(InlineInput)`
16 | padding: 0;
17 | padding-bottom: 0.25em;
18 | `;
19 |
20 | const Label = styled.label`
21 | color: #BCC1C2;
22 | font-family: 'Halyard Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
23 | font-size: 0.75em;
24 | `;
25 |
26 | const NameForm = (props) => {
27 | const {
28 | name, index, setMode, setName,
29 | } = props;
30 |
31 | // State
32 | const [tests, setTests] = useContext(TestsContext);
33 | const [newName, setNewName] = useState('');
34 |
35 | // Event handlers
36 | const handleSubmit = (event) => {
37 | event.preventDefault();
38 | const testsCopy = [...tests];
39 | testsCopy[index].name = newName;
40 |
41 | // Update name in test array, then set mockup “mode” to trigger re-render
42 | setName(newName, index);
43 | setTests(testsCopy);
44 | setMode('view');
45 | };
46 |
47 | const updateName = (event) => {
48 | const { value } = event.target;
49 | setNewName(value);
50 | };
51 |
52 | return (
53 |
63 | );
64 | };
65 |
66 | export default NameForm;
67 |
--------------------------------------------------------------------------------
/src/main/components/PerformanceMetrics.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { Bar } from 'react-chartjs';
4 |
5 | const PerformanceWrapper = styled.div`
6 | background-color: #F0F3F4;
7 | border-radius: 3px;
8 | box-shadow: inset;
9 | box-shadow: inset 0 0 1px 0 rgba(41,47,50,0.25);
10 | display: flex;
11 | justify-content: space-between;
12 | align-items: center;
13 | padding: 1em;
14 | margin-bottom: 1em;
15 | h3 {
16 | color: #555B5E;
17 | font-family: 'Halyard Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
18 | font-feature-settings: 'ss18';
19 | font-size: 1em;
20 | font-weight: 400;
21 | }
22 | `;
23 |
24 | const BarChart = Bar;
25 |
26 | const PerformanceMetrics = (props) => {
27 | const { fetchTimes } = props;
28 | const chartData = {
29 | labels: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
30 | datasets: [
31 | {
32 | label: 'My First dataset',
33 | fillColor: 'rgba(151,187,205,0.5)',
34 | strokeColor: 'rgba(151,187,205,0.8)',
35 | highlightFill: 'rgba(151,187,205,0.75)',
36 | highlightStroke: 'rgba(151,187,205,1)',
37 | data: fetchTimes,
38 | },
39 | ],
40 | };
41 |
42 | const average = fetchTimes.reduce((acc, val) => acc + (val / fetchTimes.length), 0);
43 | const max = Math.max(...fetchTimes);
44 | const min = Math.min(...fetchTimes);
45 | const variance = fetchTimes.reduce(
46 | (acc, val) => (acc + ((val - average) ** 2)), 0,
47 | ) / fetchTimes.length;
48 |
49 | return (
50 |
51 |
52 |
53 |
API Request Times
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | Average:
64 | {' '}
65 | {Math.floor(average)}
66 | {' '}
67 | ms |
68 | Max:
69 | {' '}
70 | {max}
71 | {' '}
72 | ms |
73 | Min:
74 | {' '}
75 | {min}
76 | {' '}
77 | ms |
78 | Standard Deviation:
79 | {' '}
80 | {Math.floor(Math.sqrt(variance))}
81 | {' '}
82 | ms
83 |
84 |
85 |
86 |
87 | );
88 | };
89 |
90 | export default PerformanceMetrics;
91 |
--------------------------------------------------------------------------------
/src/main/components/RequestBar.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useContext } from 'react';
2 | import { parseString, Builder } from 'xml2js';
3 | import HeaderBar from './HeaderBar.jsx';
4 | import Form from './InlineForm.jsx';
5 | import Select from './InlineSelect.jsx';
6 | import Input from './InlineInput.jsx';
7 | import Button from './Button.jsx';
8 | import { TestsContext } from '../testsContext';
9 |
10 |
11 | const RequestBar = (props) => {
12 | const {
13 | SourceOrDest, setData, setFetchTimes, setContentType, contentType,
14 | } = props;
15 | const [tests, setTests] = useContext(TestsContext);
16 |
17 | const method = (SourceOrDest === 'dest' ? 'POST' : 'GET');
18 | const [selected, setSelected] = useState(method);
19 | const [uri, setUri] = useState('');
20 | const [valid, setValid] = useState(false);
21 |
22 | // header info
23 | const [headerType, setHeaderType] = useState('Authorization');
24 | const [authType, setType] = useState('Bearer Token');
25 | const [headerKey, setHeaderKey] = useState('');
26 | let now = new Date();
27 |
28 | const handleChange = (e) => {
29 | const { name, value } = e.target;
30 | if (name === 'method') setSelected(value);
31 | else if (name === 'uri') setUri(value);
32 |
33 | // Header info
34 | if (name === 'Authentication') setHeaderType(value);
35 | if (name === 'headerKey') setHeaderKey(`Bearer ${value}`);
36 |
37 | setValid(e.target.parentElement.reportValidity());
38 | };
39 |
40 | const handleInvalid = (e) => {
41 | e.preventDefault();
42 | };
43 |
44 | // Extra fetches for performance metrics
45 | const fetchTimesList = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
46 | function getPerformanceMetricsData(getOrPost, sendingObj) {
47 | let successfulFetchesCounter = 0;
48 |
49 | function recordFetchTimes(i) {
50 | fetch(uri, sendingObj)
51 | .then(() => {
52 | fetchTimesList[i] = new Date() - now;
53 | successfulFetchesCounter += 1;
54 | now = new Date();
55 | if (successfulFetchesCounter === 9) {
56 | setFetchTimes(fetchTimesList);
57 | }
58 | });
59 | }
60 |
61 | for (let i = 1; i < 10; i += 1) recordFetchTimes(i);
62 | }
63 |
64 | const runTest = (link, sendingObj, testsClone, i) => {
65 | const test = testsClone;
66 | now = new Date();
67 | fetch(link, sendingObj)
68 | .then((response) => {
69 | fetchTimesList[0] = new Date() - now;
70 | test[i].status = response.status;
71 | if (i === test.length - 1) setTests(test);
72 | now = new Date();
73 | })
74 | .catch(error => console.log(error));
75 |
76 | getPerformanceMetricsData('post', sendingObj);
77 | };
78 |
79 | const parseXmlToJson = (xml) => {
80 | let json;
81 | parseString(xml, (err, result) => {
82 | json = result;
83 | return result;
84 | });
85 | return json;
86 | };
87 |
88 | const sendFetch = (e) => {
89 | e.preventDefault();
90 | if (!valid) return alert('Enter a valid URI');
91 |
92 | if (SourceOrDest === 'source') {
93 | const sendingObj = { method: selected, mode: 'cors' };
94 | if (headerType !== 'NONE') sendingObj.headers = { [headerType]: headerKey };
95 |
96 | now = new Date();
97 | fetch(uri, sendingObj)
98 | .then((res) => {
99 | fetchTimesList[0] = new Date() - now;
100 | now = new Date();
101 | const val = res.headers.get('content-type');
102 | setContentType(val);
103 | if (val.includes('xml')) {
104 | return res.text().then(xml => parseXmlToJson(xml));
105 | }
106 | return res.json();
107 | })
108 | .then((res) => {
109 | setTests([{
110 | payload: res, status: '', name: 'Test #1',
111 | }]);
112 | setData(res);
113 | });
114 |
115 | getPerformanceMetricsData('get', sendingObj);
116 |
117 | } else if (SourceOrDest === 'dest') {
118 | const testsClone = [...tests];
119 | const sendingObj = { method: selected, mode: 'cors' };
120 | sendingObj.headers = { 'Content-Type': contentType };
121 | if (headerType !== 'NONE') sendingObj.headers[headerType] = headerKey;
122 |
123 | for (let i = 0; i < testsClone.length; i += 1) {
124 | if (sendingObj.headers['Content-Type'].includes('xml')) {
125 | const jsonToXml = new Builder();
126 | sendingObj.body = jsonToXml.buildObject(testsClone[i].payload);
127 | } else {
128 | sendingObj.body = JSON.stringify(testsClone[i].payload);
129 | }
130 | runTest(uri, sendingObj, testsClone, i);
131 | }
132 | }
133 | };
134 |
135 | return (
136 |
137 |
170 |
171 |
172 | );
173 | };
174 |
175 | export default RequestBar;
176 |
--------------------------------------------------------------------------------
/src/main/components/ResponseComponent.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import ReactDiffViewer from 'react-diff-viewer';
3 | import styled from 'styled-components';
4 | import { ArrowRightCircle, CheckCircle, XCircle } from 'react-feather';
5 | import ResponseWrapper from './ResponseWrapper.jsx';
6 |
7 |
8 | // Styles
9 | const CardButton = styled.button`
10 | background: #F0F3F4;
11 | border: none;
12 | border-bottom-left-radius: 3px;
13 | border-bottom-right-radius: 3px;
14 | border-top: 1px solid #BCC1C2;
15 | box-shadow: inset 0 0 1px 0 rgba(41, 47, 50, 0.35);
16 | color: #555B5E;
17 | font-family: 'Halyard Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
18 | font-size: 0.825em;
19 | padding: 1em;
20 |
21 | :hover {
22 | cursor: pointer;
23 | background: #555B5E;
24 | color: #F0F3F4;
25 | }
26 |
27 | :active {
28 | background: #555B5E;
29 | color: #F0F3F4;
30 | outline: 0;
31 | }
32 |
33 | :focus {
34 | outline: 0;
35 | box-shadow: inset 0 0 3px 0 rgba(41, 47, 50, 0.5);
36 | }
37 | `;
38 |
39 | const Status = styled.div`
40 | color: #292F32;
41 | display: flex;
42 | align-items: center;
43 | svg {
44 | margin-right: 0.625em;
45 | }
46 | `;
47 |
48 | const diffColors = {
49 | variables: {
50 | addedBackground: '#DEF0DB',
51 | addedColor: '#1A3715',
52 | removedBackground: '#FFE5E5',
53 | removedColor: '#5C0505',
54 | wordAddedBackground: '#B2D2A2',
55 | wordRemovedBackground: '#FBB6B6',
56 | addedGutterBackground: '#B2D2A2',
57 | removedGutterBackground: '#FBB6B6',
58 | gutterBackground: '#F0F3F4',
59 | gutterBackgroundDark: '#F0F3F4',
60 | highlightBackground: '#fffbdd',
61 | highlightGutterBackground: '#fff5b1',
62 | },
63 | };
64 |
65 | const Header = styled.header`
66 | font-family: 'Halyard Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
67 | padding: 1em;
68 | h3 {
69 | color: #292F32;
70 | margin: 0;
71 | padding-bottom: 0.5em;
72 | }
73 | `;
74 |
75 | const ResponseComponent = (props) => {
76 | const {
77 | status, expectedStatus, payload, data, name, diff,
78 | } = props;
79 |
80 | const [showDiff, setShowDiff] = useState(false);
81 |
82 | // Event handlers
83 | const didPass = () => {
84 | if (!status) return 'pending';
85 | if (!expectedStatus) {
86 | return status >= 200 && status <= 299;
87 | }
88 | if (expectedStatus[1]) {
89 | const [lower, upper] = expectedStatus;
90 | return status >= lower && status <= upper;
91 | }
92 | return expectedStatus[0] === status;
93 | };
94 |
95 | const renderCheckmark = () => {
96 | if (!status) return ;
97 | return didPass()
98 | ?
99 | : ;
100 | };
101 |
102 | const renderExpectedStatus = () => {
103 | if (!expectedStatus) {
104 | return 'Expecting status code in the 200–299 range';
105 | }
106 | if (expectedStatus.length === 2) {
107 | const [lower, upper] = expectedStatus;
108 | return `Expecting status code in the ${lower}–${upper} range`;
109 | }
110 | if (expectedStatus.length === 1) {
111 | return `Expecting status code to be ${expectedStatus[0]}`;
112 | }
113 | };
114 |
115 | const renderTestResult = () => {
116 | if (!expectedStatus) {
117 | return 'Expecting status code in the 200–299 range';
118 | }
119 | if (expectedStatus[1]) {
120 | const [lower, upper] = expectedStatus;
121 | return didPass()
122 | ? 'Passed'
123 | : `Expected status code to fall between ${lower}–${upper}`;
124 | }
125 | return didPass()
126 | ? 'Passed'
127 | : `Expected status code to be ${expectedStatus[0]}`;
128 | };
129 |
130 | const originalValue = (diff === undefined)
131 | ? JSON.stringify(data, null, 2)
132 | : JSON.stringify(data[diff], null, 2);
133 | const newValue = JSON.stringify(payload, null, 2);
134 |
135 | const cardButton = (originalValue === newValue
136 | ? No Changes
137 | : setShowDiff(true)}> Show Changes );
138 |
139 | return (
140 |
141 |
142 | { name }
143 | {renderCheckmark()}
144 | { status || 'Ready to send' }
145 |
146 | {(diff === undefined) ? '' : `Test created from subset from key: ${diff}`}
147 | { status ? renderTestResult() : renderExpectedStatus() }
148 |
149 | {showDiff ? setShowDiff(false)}>Collapse Changes : null}
150 | {showDiff
151 | ?
152 |
159 |
160 | : cardButton
161 | }
162 |
163 |
164 | );
165 | };
166 |
167 | export default ResponseComponent;
168 |
--------------------------------------------------------------------------------
/src/main/components/ResponseWrapper.jsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | const ResponseWrapper = styled.article`
4 | background-color: #F0F3F4;
5 | border-radius: 3px;
6 | box-shadow: inset;
7 | box-shadow: inset 0 0 2px 0 rgba(41, 47, 50, 0.25);
8 | display: flex;
9 | flex-direction: column;
10 | justify-content: space-between;
11 | margin-bottom: 1em;
12 | a {
13 | color: #555B5E;
14 | font-family: 'Halyard Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
15 | font-size: 0.825em;
16 | text-align: center;
17 | :hover {
18 | cursor: pointer;
19 | }
20 | }
21 | div.diff {
22 | border-top: 1px solid #E2E6E9;
23 | margin: 0.25em;
24 | max-height: 480px;
25 | overflow-y: scroll;
26 | }
27 | table {
28 | border-collapse: collapse;
29 | }
30 | pre {
31 | font-family: 'IBM Plex Mono', monospace;
32 | font-size: 90%;
33 | }
34 | `;
35 |
36 | export default ResponseWrapper;
37 |
--------------------------------------------------------------------------------
/src/main/components/TestComponent.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | // import DataTree from '../components/DataTree.jsx';
3 |
4 | const TestComponent = (props) => {
5 | // data is the data response from msA
6 | // key is the unique ID for this test component-- maps to tests[i]
7 | const { data, listPosition, saveTest } = props;
8 |
9 | // Look at that, CS. Looks like I might've been wrong :)
10 | // Setting this component's initial state to a prop
11 | // because it will be the same `data` every time one is
12 | // created but each one is unique and can be changed.
13 | const [json, setJson] = useState(JSON.stringify(data));
14 |
15 | const handleChange = (e) => {
16 | setJson(e.target.value);
17 | };
18 |
19 | return (
20 |
21 |
Here's a test!
22 |
27 |
saveTest(json, listPosition)}
30 | >
31 | Save Test
32 |
33 |
34 | );
35 | };
36 |
37 | export default TestComponent;
38 |
--------------------------------------------------------------------------------
/src/main/containers/App.jsx:
--------------------------------------------------------------------------------
1 | import { hot } from 'react-hot-loader/root';
2 | import styled from 'styled-components';
3 | import React from 'react';
4 | import Panels from './Panels.jsx';
5 | import { TestsProvider } from '../testsContext';
6 |
7 | const App = () => (
8 |
13 | );
14 | export default hot(App);
15 |
--------------------------------------------------------------------------------
/src/main/containers/DataCanvas.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import DataTree from '../components/DataTree.jsx';
3 | import EmptyState from '../components/EmptyState.jsx';
4 | import Nodatasvg from '../../renderer/Nodatasvg.jsx';
5 |
6 | const DataCanvas = (props) => {
7 | const {
8 | data, treeId, options,
9 | } = props;
10 |
11 | // Display empty state for no data
12 | if (!data) {
13 | return (
14 |
15 | Let’s get some data
16 | To get started, send a request to any of your microservices or post JSON to http://localhost:3001/posturl
17 |
18 |
19 | );
20 | }
21 | // Increment tree count and render data
22 | return (
23 |
28 | );
29 | };
30 |
31 | export default DataCanvas;
32 |
--------------------------------------------------------------------------------
/src/main/containers/DestinationPanel.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useContext } from 'react';
2 | import RequestBar from '../components/RequestBar.jsx';
3 | import ResponseComponent from '../components/ResponseComponent.jsx';
4 | import StyledPanel from './StyledPanel.jsx';
5 | import { TestsContext } from '../testsContext';
6 | import PerformanceMetrics from '../components/PerformanceMetrics.jsx';
7 |
8 | const DestinationPanel = (props) => {
9 | const {
10 | active, data, fetchTimes, hContentType, onClickFunction, setFetchTimes,
11 | } = props;
12 | const [tests, setTests] = useContext(TestsContext);
13 |
14 | const responseComponentsList = [];
15 | for (let i = 0; i < tests.length; i += 1) {
16 | responseComponentsList.push(
17 | ,
27 | );
28 | }
29 |
30 | const sumReducer = (accumulator, currentValue) => accumulator + currentValue;
31 |
32 | if (active) {
33 | return (
34 |
35 |
36 |
42 | {responseComponentsList}
43 | {(fetchTimes.length > 0)
44 | && (fetchTimes.reduce(sumReducer) > 0)
45 | &&
}
46 |
47 |
48 | );
49 | }
50 |
51 | // only returned if not active
52 | return (
53 |
54 | Destination
55 |
56 | );
57 | };
58 |
59 | export default DestinationPanel;
60 |
--------------------------------------------------------------------------------
/src/main/containers/MockupsPanel.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useContext } from 'react';
2 | import StyledPanel from './StyledPanel.jsx';
3 | import { TestsContext } from '../testsContext';
4 | import Button from '../components/Button.jsx';
5 | import Select from '../components/InlineSelect.jsx';
6 | import Mockup from '../components/Mockup.jsx';
7 | import styled from 'styled-components';
8 |
9 |
10 | // will need to get data from the get request to pass to the formatted view
11 | const MockupsPanel = (props) => {
12 | const {
13 | active, datacanvas, data, onClickFunction,
14 | } = props;
15 |
16 | const [tests, setTests] = useContext(TestsContext);
17 | const [testsListCounter, setTestsListCounter] = useState(0);
18 |
19 | const saveUpdatedTree = (newData, arrayPosition, newValue, name, namespace) => {
20 | // Update tests
21 | const testsClone = [...tests];
22 | testsClone[arrayPosition].payload = newData;
23 | setTests(testsClone);
24 | };
25 |
26 | const mockupsListDisplay = [];
27 | let i = 0;
28 | tests.forEach((test, index) => {
29 | const { name } = test;
30 |
31 | mockupsListDisplay.push(
32 | // Creates a component for each test to display name and data
33 | ,
34 | );
35 | i += 1;
36 | });
37 |
38 | const createNewTest = () => {
39 | const testsClone = [...tests];
40 | testsClone.push({
41 | payload: data, status: '', name: `Test #${tests.length + 1}`,
42 | });
43 |
44 | // the ID of the test will be the same as the position in the array
45 | setTestsListCounter(testsListCounter + 1);
46 | setTests(testsClone);
47 | };
48 |
49 | const initialstate = (data ? Object.keys(data)[0] : undefined);
50 | const [createTestIndex, setCreateTestIndex] = useState(initialstate);
51 | function createTestFromIndex() {
52 | const testsClone = [...tests];
53 | testsClone.push({
54 | payload: data[createTestIndex], status: '', name: `Test #${tests.length + 1}`, diff: createTestIndex,
55 | });
56 | setTests(testsClone);
57 | }
58 |
59 | const options = [];
60 | if (data) {
61 | const testKeys = Object.keys(data);
62 | for (let j = 0; j < testKeys.length; j += 1) {
63 | // if necessary because otherwise you get errors with single values
64 | if (typeof data[testKeys[j]] === 'object') {
65 | options.push({testKeys[j]} );
66 | }
67 | }
68 | }
69 |
70 | function changeTestIndex(e) {
71 | const { value } = e.target;
72 | setCreateTestIndex(value);
73 | }
74 |
75 | const ServerResp = styled.h3`
76 | font-family: 'Halyard Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
77 | `;
78 |
79 | if (active) {
80 | return (
81 |
82 |
83 | {data &&
Server Response }
84 | {datacanvas}
85 | {data
86 | &&
87 | New Test
88 |
89 | Create Test From Index
90 |
97 | {options}
98 |
99 |
}
100 | {mockupsListDisplay}
101 |
102 |
103 | );
104 | }
105 |
106 | return (
107 |
111 | Test
112 |
113 | );
114 | };
115 |
116 | export default MockupsPanel;
117 |
--------------------------------------------------------------------------------
/src/main/containers/Panels.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useContext } from 'react';
2 | import styled from 'styled-components';
3 | import io from 'socket.io-client';
4 | import SourcePanel from './SourcePanel.jsx';
5 | import MockupsPanel from './MockupsPanel.jsx';
6 | import DestinationPanel from './DestinationPanel.jsx';
7 | import DataCanvas from './DataCanvas.jsx';
8 | import { TestsContext } from '../testsContext';
9 |
10 | const socket = io.connect('http://localhost:3001/');
11 |
12 | const Panels = () => {
13 | const [activePanel, setActivePanel] = useState('source');
14 | const [data, setData] = useState(undefined);
15 | const [getFetchTimes, setGetFetchTimes] = useState([]);
16 | const [postFetchTimes, setPostFetchTimes] = useState([]);
17 | const [hContentType, setContentType] = useState('');
18 |
19 | const [tests, setTests] = useContext(TestsContext);
20 |
21 | const PanelsWrapper = styled.section`
22 | display: flex;
23 | height: 100vh;
24 | `;
25 |
26 | // Create DataCanvas component for component composition to
27 | // be passed down as a prop to the panels that render it
28 | const dataTreeOptions = {
29 | onAdd: false,
30 | onEdit: false,
31 | onDelete: false,
32 | enableClipboard: false,
33 | };
34 |
35 | const datacanvas = (
36 |
41 | );
42 |
43 | socket.on('post_received', (postedData) => {
44 | setData(postedData);
45 | setTests([{
46 | payload: postedData, status: '', name: 'Test #1',
47 | }]);
48 |
49 | // clear old performance metrics on new post
50 | if (getFetchTimes.length) setGetFetchTimes([]);
51 | if (postFetchTimes.length) setPostFetchTimes([]);
52 | });
53 |
54 | return (
55 |
56 | setActivePanel('source')}
58 | datacanvas={datacanvas}
59 | setData={setData}
60 | active={(activePanel === 'source')}
61 | fetchTimes={getFetchTimes}
62 | setFetchTimes={setGetFetchTimes}
63 | setContentType={setContentType}
64 | />
65 |
66 | setActivePanel('test')}
68 | datacanvas={datacanvas}
69 | data={data}
70 | active={(activePanel === 'test')}
71 | />
72 |
73 | setActivePanel('dest')}
79 | setFetchTimes={setPostFetchTimes}
80 | />
81 |
82 | );
83 | };
84 |
85 | export default Panels;
86 |
--------------------------------------------------------------------------------
/src/main/containers/SourcePanel.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import RequestBar from '../components/RequestBar.jsx';
3 | import StyledPanel from './StyledPanel.jsx';
4 | import PerformanceMetrics from '../components/PerformanceMetrics.jsx';
5 |
6 | const SourcePanel = (props) => {
7 | const {
8 | setData, active, onClickFunction, datacanvas, fetchTimes, setFetchTimes, setContentType,
9 | } = props;
10 |
11 | const reducer = (accumulator, currentValue) => accumulator + currentValue;
12 |
13 | if (active) {
14 | return (
15 |
16 |
17 |
22 | {datacanvas}
23 | {(fetchTimes.length > 0)
24 | && (fetchTimes.reduce(reducer) > 0)
25 | &&
}
26 |
27 |
28 | );
29 | }
30 |
31 | return (
32 |
37 | Source
38 |
39 | );
40 | };
41 |
42 | export default SourcePanel;
43 |
--------------------------------------------------------------------------------
/src/main/containers/StyledPanel.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 |
4 | const StyledPanel = styled.section`
5 | border-right: 1px solid #BCC1C2;
6 | max-height: 100vh;
7 | overflow-y: scroll;
8 | padding: ${(props) => {
9 | if (props.active) return '3em 4em';
10 | return '3em auto';
11 | }};
12 | position: relative;
13 | width: ${props => (props.active ? '80vw' : '10vw')};
14 | h1 {
15 | color: #949C9E;
16 | font-family: 'Halyard Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
17 | font-size: 0.825em;
18 | font-weight: 400;
19 | text-align: center;
20 | text-transform: uppercase;
21 | }
22 | h3 {
23 | text-align: left;
24 | }
25 | &>div {
26 | background-color: #FFFFFF;
27 | border-bottom: 1px solid #F0F3F4;
28 | margin-bottom: 2em;
29 | margin-top: 1em;
30 | padding-bottom: 2em;
31 | position: sticky;
32 | top: 0px;
33 | z-index: 1;
34 | }
35 | `;
36 |
37 | export default StyledPanel;
38 |
--------------------------------------------------------------------------------
/src/main/dummyData.js:
--------------------------------------------------------------------------------
1 | const largeData = [
2 | {
3 | _id: '5cfc038a19e0a6ca597d3257',
4 | index: 0,
5 | guid: '23149292-ba33-478a-b6a4-21aa7467c0d8',
6 | isActive: false,
7 | balance: '$1,781.02',
8 | picture: 'http://placehold.it/32x32',
9 | age: 25,
10 | eyeColor: 'green',
11 | name: 'Durham Weaver',
12 | gender: 'male',
13 | company: 'POOCHIES',
14 | email: 'durhamweaver@poochies.com',
15 | phone: '+1 (936) 517-3157',
16 | address: '258 Eastern Parkway, Galesville, Palau, 8794',
17 | about: 'Magna cupidatat ea velit in cupidatat nulla laboris anim. Enim reprehenderit deserunt Lorem duis dolore laborum ea sunt velit non occaecat et. Elit aliqua ex do cupidatat velit id voluptate ea consectetur qui consequat in. Occaecat eu incididunt occaecat et ea magna non. Pariatur deserunt amet dolor quis ex amet sit id do eiusmod fugiat minim irure id. Reprehenderit cupidatat quis laboris veniam culpa eiusmod irure irure do cupidatat. Officia elit pariatur pariatur magna labore aute id veniam dolor deserunt.\r\n',
18 | registered: '2016-12-31T11:50:54 +05:00',
19 | latitude: -83.159903,
20 | longitude: -94.226187,
21 | tags: [
22 | 'Lorem',
23 | 'voluptate',
24 | 'sint',
25 | 'irure',
26 | 'irure',
27 | 'voluptate',
28 | 'quis',
29 | ],
30 | friends: [
31 | {
32 | id: 0,
33 | name: 'Fisher Dominguez',
34 | },
35 | {
36 | id: 1,
37 | name: 'Shannon Fuentes',
38 | },
39 | {
40 | id: 2,
41 | name: 'Orr Goodman',
42 | },
43 | ],
44 | greeting: 'Hello, Durham Weaver! You have 6 unread messages.',
45 | favoriteFruit: 'banana',
46 | },
47 | {
48 | _id: '5cfc038adf191ce8d2a36cc2',
49 | index: 1,
50 | guid: '41ece5ce-1944-41a4-9b31-c1385d13bdc0',
51 | isActive: true,
52 | balance: '$3,567.06',
53 | picture: 'http://placehold.it/32x32',
54 | age: 32,
55 | eyeColor: 'brown',
56 | name: 'Janell Mcmillan',
57 | gender: 'female',
58 | company: 'MACRONAUT',
59 | email: 'janellmcmillan@macronaut.com',
60 | phone: '+1 (888) 547-3683',
61 | address: '118 Saratoga Avenue, Itmann, Wisconsin, 2125',
62 | about: 'Cupidatat minim irure quis quis aute nisi est. Consectetur excepteur aliqua dolore in ipsum aute aliqua sunt exercitation. Nostrud sunt deserunt sint amet. Ad qui elit ut id duis cillum laborum ex occaecat incididunt non ex sit exercitation. Velit incididunt id duis eu consequat. Pariatur eu pariatur fugiat proident laborum voluptate est.\r\n',
63 | registered: '2018-05-21T10:29:51 +04:00',
64 | latitude: -63.488049,
65 | longitude: -147.630589,
66 | tags: [
67 | 'Lorem',
68 | 'sunt',
69 | 'eiusmod',
70 | 'proident',
71 | 'deserunt',
72 | 'consequat',
73 | 'eiusmod',
74 | ],
75 | friends: [
76 | {
77 | id: 0,
78 | name: 'Rogers Combs',
79 | },
80 | {
81 | id: 1,
82 | name: 'Daniels Dunlap',
83 | },
84 | {
85 | id: 2,
86 | name: 'Myers Newton',
87 | },
88 | ],
89 | greeting: 'Hello, Janell Mcmillan! You have 3 unread messages.',
90 | favoriteFruit: 'banana',
91 | },
92 | {
93 | _id: '5cfc038a5efeba1c1dc19aa6',
94 | index: 2,
95 | guid: '7caf10c5-13b7-43fb-b943-bf0506b3efc1',
96 | isActive: true,
97 | balance: '$3,484.24',
98 | picture: 'http://placehold.it/32x32',
99 | age: 32,
100 | eyeColor: 'brown',
101 | name: 'Vega Bennett',
102 | gender: 'male',
103 | company: 'ZOXY',
104 | email: 'vegabennett@zoxy.com',
105 | phone: '+1 (829) 418-2650',
106 | address: '818 Crawford Avenue, Cobbtown, Marshall Islands, 8275',
107 | about: 'Proident duis culpa aliqua cillum qui elit aute do. Esse incididunt excepteur reprehenderit qui veniam laborum commodo id nostrud ex labore exercitation ipsum. Ipsum dolore pariatur amet laborum minim ea. Amet do eu cupidatat irure fugiat id proident commodo pariatur nisi. Magna sunt ea et fugiat et irure qui labore fugiat proident dolore consequat consequat. Consectetur est elit consequat pariatur dolor minim et cupidatat elit fugiat consectetur officia adipisicing. Anim amet ex ipsum ipsum elit voluptate incididunt do esse et esse Lorem.\r\n',
108 | registered: '2017-04-24T04:01:25 +04:00',
109 | latitude: 37.07711,
110 | longitude: -15.498175,
111 | tags: [
112 | 'consequat',
113 | 'occaecat',
114 | 'ullamco',
115 | 'qui',
116 | 'est',
117 | 'officia',
118 | 'cillum',
119 | ],
120 | friends: [
121 | {
122 | id: 0,
123 | name: 'Charles Walsh',
124 | },
125 | {
126 | id: 1,
127 | name: 'Medina Lancaster',
128 | },
129 | {
130 | id: 2,
131 | name: 'Randall Guerra',
132 | },
133 | ],
134 | greeting: 'Hello, Vega Bennett! You have 6 unread messages.',
135 | favoriteFruit: 'banana',
136 | },
137 | {
138 | _id: '5cfc038a446edfde3da5cdfb',
139 | index: 3,
140 | guid: '64d2400a-e0aa-4ae2-af0f-a6876ee860d0',
141 | isActive: false,
142 | balance: '$3,680.63',
143 | picture: 'http://placehold.it/32x32',
144 | age: 27,
145 | eyeColor: 'blue',
146 | name: 'Trisha Dawson',
147 | gender: 'female',
148 | company: 'WAAB',
149 | email: 'trishadawson@waab.com',
150 | phone: '+1 (931) 434-2622',
151 | address: '103 Lynch Street, Elrama, Montana, 3094',
152 | about: 'Occaecat ullamco deserunt velit eu. Tempor exercitation veniam duis minim deserunt incididunt aute eu irure culpa consequat dolor aliquip. Minim quis aliquip do est do excepteur pariatur veniam. Nulla sunt ipsum quis eiusmod aliquip laborum. Mollit duis veniam qui aliquip. Nulla quis incididunt commodo eu eiusmod ipsum.\r\n',
153 | registered: '2015-05-24T11:51:44 +04:00',
154 | latitude: -26.547163,
155 | longitude: 48.642857,
156 | tags: [
157 | 'aliquip',
158 | 'commodo',
159 | 'consectetur',
160 | 'ullamco',
161 | 'et',
162 | 'eu',
163 | 'consectetur',
164 | ],
165 | friends: [
166 | {
167 | id: 0,
168 | name: 'Cortez Bradley',
169 | },
170 | {
171 | id: 1,
172 | name: 'Dianne Michael',
173 | },
174 | {
175 | id: 2,
176 | name: 'Loretta Rodgers',
177 | },
178 | ],
179 | greeting: 'Hello, Trisha Dawson! You have 2 unread messages.',
180 | favoriteFruit: 'apple',
181 | },
182 | {
183 | _id: '5cfc038aa88732edfb0cd254',
184 | index: 4,
185 | guid: '71dc7a55-64b1-4cd2-839f-60c18fabb998',
186 | isActive: false,
187 | balance: '$1,681.55',
188 | picture: 'http://placehold.it/32x32',
189 | age: 21,
190 | eyeColor: 'brown',
191 | name: 'Jacobs Barrera',
192 | gender: 'male',
193 | company: 'ENVIRE',
194 | email: 'jacobsbarrera@envire.com',
195 | phone: '+1 (873) 403-3402',
196 | address: '735 Channel Avenue, Sutton, American Samoa, 2964',
197 | about: 'Incididunt culpa quis dolor et ea eu officia excepteur labore eiusmod quis cillum cillum. Cupidatat eiusmod laboris aute sunt fugiat cupidatat aute dolor velit ipsum magna Lorem nostrud veniam. Eu id proident qui consequat qui incididunt. Magna officia voluptate ipsum non aute exercitation sint ad. Consectetur anim nulla eu amet tempor veniam ullamco ullamco consequat aliquip dolor aliqua adipisicing irure.\r\n',
198 | registered: '2014-07-14T03:12:01 +04:00',
199 | latitude: 59.477283,
200 | longitude: -136.208247,
201 | tags: [
202 | 'et',
203 | 'incididunt',
204 | 'minim',
205 | 'culpa',
206 | 'nulla',
207 | 'nostrud',
208 | 'aliquip',
209 | ],
210 | friends: [
211 | {
212 | id: 0,
213 | name: 'Carver Moreno',
214 | },
215 | {
216 | id: 1,
217 | name: 'Figueroa Diaz',
218 | },
219 | {
220 | id: 2,
221 | name: 'Fran Park',
222 | },
223 | ],
224 | greeting: 'Hello, Jacobs Barrera! You have 10 unread messages.',
225 | favoriteFruit: 'banana',
226 | },
227 | ];
228 |
229 | const smallData = {
230 | firstName: 'Hello2',
231 | lastName: 'Joe',
232 | phones: [
233 | '123-45-67',
234 | '987-65-43',
235 | ],
236 | };
237 |
238 | const testsData = [{
239 | payload: JSON.stringify({ message: 'hello by joe', created_by: 'joe' }),
240 | status: '',
241 | },
242 | {
243 | payload: JSON.stringify({ message: 'hello by conor', created_by: 'conor' }),
244 | status: '',
245 | },
246 | {
247 | payload: JSON.stringify({ message: 'hello by joe2', created_by: 'joe' }),
248 | status: '',
249 | },
250 | {
251 | payload: JSON.stringify({ message: 'hello by conor2', created_by: 'conor' }),
252 | status: '',
253 | }];
254 |
255 |
256 | module.exports = { largeData, smallData, testsData };
257 |
--------------------------------------------------------------------------------
/src/main/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './containers/App.jsx';
4 |
5 | ReactDOM.render( , document.getElementById('app'));
6 |
--------------------------------------------------------------------------------
/src/main/lib/treeView/icons.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/main/lib/treeView/jsonTree.css:
--------------------------------------------------------------------------------
1 | /*
2 | * JSON Tree Viewer
3 | * http://github.com/summerstyle/jsonTreeViewer
4 | *
5 | * Copyright 2017 Vera Lobacheva (http://iamvera.com)
6 | * Released under the MIT license (LICENSE.txt)
7 | */
8 |
9 | /* Background for the tree. May use for element */
10 | .jsontree_bg {
11 | background: #FFF;
12 | }
13 |
14 | /* Styles for the container of the tree (e.g. fonts, margins etc.) */
15 | .jsontree_tree {
16 | margin-left: 30px;
17 | font-family: 'PT Mono', monospace;
18 | font-size: 14px;
19 | }
20 |
21 | /* Styles for a list of child nodes */
22 | .jsontree_child-nodes {
23 | display: none;
24 | margin-left: 35px;
25 | margin-bottom: 5px;
26 | line-height: 2;
27 | }
28 | .jsontree_node_expanded > .jsontree_value-wrapper > .jsontree_value > .jsontree_child-nodes {
29 | display: block;
30 | }
31 |
32 | /* Styles for labels */
33 | .jsontree_label-wrapper {
34 | float: left;
35 | margin-right: 8px;
36 | }
37 | .jsontree_label {
38 | font-weight: normal;
39 | vertical-align: top;
40 | color: #000;
41 | position: relative;
42 | padding: 1px;
43 | border-radius: 4px;
44 | cursor: default;
45 | }
46 | .jsontree_node_marked > .jsontree_label-wrapper > .jsontree_label {
47 | background: #fff2aa;
48 | }
49 |
50 | /* Styles for values */
51 | .jsontree_value-wrapper {
52 | display: block;
53 | overflow: hidden;
54 | }
55 | .jsontree_node_complex > .jsontree_value-wrapper {
56 | overflow: inherit;
57 | }
58 | .jsontree_value {
59 | vertical-align: top;
60 | display: inline;
61 | }
62 | .jsontree_value_null {
63 | color: #777;
64 | font-weight: bold;
65 | }
66 | .jsontree_value_string {
67 | color: #025900;
68 | font-weight: bold;
69 | }
70 | .jsontree_value_number {
71 | color: #000E59;
72 | font-weight: bold;
73 | }
74 | .jsontree_value_boolean {
75 | color: #600100;
76 | font-weight: bold;
77 | }
78 |
79 | /* Styles for active elements */
80 | .jsontree_expand-button {
81 | position: absolute;
82 | top: 3px;
83 | left: -15px;
84 | display: block;
85 | width: 11px;
86 | height: 11px;
87 | background-image: url('icons.svg');
88 | }
89 | .jsontree_node_expanded > .jsontree_label-wrapper > .jsontree_label > .jsontree_expand-button {
90 | background-position: 0 -11px;
91 | }
92 | .jsontree_show-more {
93 | cursor: pointer;
94 | }
95 | .jsontree_node_expanded > .jsontree_value-wrapper > .jsontree_value > .jsontree_show-more {
96 | display: none;
97 | }
98 | .jsontree_node_empty > .jsontree_label-wrapper > .jsontree_label > .jsontree_expand-button,
99 | .jsontree_node_empty > .jsontree_value-wrapper > .jsontree_value > .jsontree_show-more {
100 | display: none !important;
101 | }
102 | .jsontree_node_complex > .jsontree_label-wrapper > .jsontree_label {
103 | cursor: pointer;
104 | }
105 | .jsontree_node_empty > .jsontree_label-wrapper > .jsontree_label {
106 | cursor: default !important;
107 | }
108 | ul {
109 | list-style-type: none;
110 | }
111 |
--------------------------------------------------------------------------------
/src/main/lib/treeView/jsonTree.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | /**
4 | * JSON Tree library (a part of jsonTreeViewer)
5 | * http://github.com/summerstyle/jsonTreeViewer
6 | *
7 | * Copyright 2017 Vera Lobacheva (http://iamvera.com)
8 | * Released under the MIT license (LICENSE.txt)
9 | */
10 |
11 |
12 | const jsonTree = (() => {
13 | /* ---------- Utilities ---------- */
14 | const utils = {
15 | /*
16 | * Returns js-"class" of value
17 | *
18 | * @param val {any type} - value
19 | * @returns {string} - for example, "[object Function]"
20 | */
21 | getClass: val => Object.prototype.toString.call(val),
22 | /**
23 | * Checks for a type of value (for valid JSON data types).
24 | * In other cases - throws an exception
25 | *
26 | * @param val {any type} - the value for new node
27 | * @returns {string} ("object" | "array" | "null" | "boolean" | "number" | "string")
28 | */
29 | getType: (val) => {
30 | if (val === null) {
31 | return 'null';
32 | }
33 |
34 | switch (typeof val) {
35 | case 'number':
36 | return 'number';
37 | case 'string':
38 | return 'string';
39 | case 'boolean':
40 | return 'boolean';
41 | default: break;
42 | }
43 |
44 | switch (utils.getClass(val)) {
45 | case '[object Array]':
46 | return 'array';
47 |
48 | case '[object Object]':
49 | return 'object';
50 | default: break;
51 | }
52 |
53 | throw new Error(`Bad type: + ${utils.getClass(val)}`);
54 | },
55 |
56 | /**
57 | * Applies for each item of list some function
58 | * and checks for last element of the list
59 | *
60 | * @param obj {Object | Array} - a list or a dict with child nodes
61 | * @param func {Function} - the function for each item
62 | */
63 | forEachNode: (obj, func) => {
64 | const type = utils.getType(obj);
65 | let isLast;
66 | let keys;
67 | switch (type) {
68 | case 'array':
69 | isLast = obj.length - 1;
70 | obj.forEach((item, i) => {
71 | func(i, item, i === isLast);
72 | });
73 | break;
74 |
75 | case 'object':
76 | keys = Object.keys(obj).sort();
77 | isLast = keys.length - 1;
78 | keys.forEach((item, i) => {
79 | func(item, obj[item], i === isLast);
80 | });
81 | break;
82 | default: break;
83 | }
84 |
85 | },
86 |
87 | /**
88 | * Implements the kind of an inheritance by
89 | * using parent prototype and
90 | * creating intermediate constructor
91 | *
92 | * @param Child {Function} - a child constructor
93 | * @param Parent {Function} - a parent constructor
94 | */
95 | inherits: (() => {
96 | const F = function () { };
97 |
98 | return function (C, Parent) {
99 | const Child = C;
100 | F.prototype = Parent.prototype;
101 | Child.prototype = new F();
102 | Child.prototype.constructor = Child;
103 | };
104 | })(),
105 |
106 | /*
107 | * Checks for a valid type of root node*
108 | *
109 | * @param {any type} jsonObj - a value for root node
110 | * @returns {boolean} - true for an object or an array, false otherwise
111 | */
112 | isValidRoot: (jsonObj) => {
113 | switch (utils.getType(jsonObj)) {
114 | case 'object':
115 | case 'array':
116 | return true;
117 | default:
118 | return false;
119 | }
120 | },
121 |
122 | /**
123 | * Extends some object
124 | */
125 | extend: (targetObj, sourceObj) => {
126 | for (let prop in sourceObj) {
127 | if (sourceObj.hasOwnProperty(prop)) {
128 | targetObj[prop] = sourceObj[prop];
129 | }
130 | }
131 | },
132 | };
133 |
134 |
135 | /* ---------- Node constructors ---------- */
136 |
137 | /**
138 | * The factory for creating nodes of defined type.
139 | *
140 | * ~~~ Node ~~~ is a structure element of an onject or an array
141 | * with own label (a key of an object or an index of an array)
142 | * and value of any json data type. The root object or array
143 | * is a node without label.
144 | * {...
145 | * [+] "label": value,
146 | * ...}
147 | *
148 | * Markup:
149 | *
150 | *
151 | *
152 | *
153 | * "label"
154 | *
155 | * :
156 | *
157 | * <(div|span) class="jsontree_value jsontree_value_(object|array|boolean|null|number|string)">
158 | * ...
159 | * (div|span)>
160 | *
161 | *
162 | * @param label {string} - key name
163 | * @param val {Object | Array | string | number | boolean | null} - a value of node
164 | * @param isLast {boolean} - true if node is last in list of siblings
165 | *
166 | * @return {Node}
167 | */
168 | function Node(label, val, isLast) {
169 | const nodeType = utils.getType(val);
170 |
171 | if (nodeType in Node.CONSTRUCTORS) {
172 | return new Node.CONSTRUCTORS[nodeType](label, val, isLast);
173 | }
174 | throw new Error(`Bad type: ' ${utils.getClass(val)}`);
175 | }
176 |
177 | Node.CONSTRUCTORS = {
178 | boolean: NodeBoolean,
179 | number: NodeNumber,
180 | string: NodeString,
181 | null: NodeNull,
182 | object: NodeObject,
183 | array: NodeArray
184 | };
185 |
186 |
187 | /*
188 | * The constructor for simple types (string, number, boolean, null)
189 | * {...
190 | * [+] "label": value,
191 | * ...}
192 | * value = string || number || boolean || null
193 | *
194 | * Markup:
195 | *
196 | *
197 | * "age"
198 | * :
199 | *
200 | * 25
201 | * ,
202 | *
203 | *
204 | * @abstract
205 | * @param label {string} - key name
206 | * @param val {string | number | boolean | null} - a value of simple types
207 | * @param isLast {boolean} - true if node is last in list of parent childNodes
208 | */
209 | function _NodeSimple(label, val, isLast) {
210 | if (this.constructor === _NodeSimple) {
211 | throw new Error('This is abstract class');
212 | }
213 |
214 | let self = this,
215 | el = document.createElement('li'),
216 | labelEl,
217 | template = function (label, val) {
218 | let str = '\
219 | \
220 | "' +
221 | label +
222 | '" : \
223 | \
224 | \
225 | ' +
226 | val +
227 | ' ' +
228 | (!isLast ? ',' : '') +
229 | ' ';
230 |
231 | return str;
232 | };
233 |
234 | self.label = label;
235 | self.isComplex = false;
236 |
237 | el.classList.add('jsontree_node');
238 | el.innerHTML = template(label, val);
239 |
240 | self.el = el;
241 |
242 | labelEl = el.querySelector('.jsontree_label');
243 |
244 | labelEl.addEventListener('click', function (e) {
245 | if (e.altKey) {
246 | self.toggleMarked();
247 | return;
248 | }
249 |
250 | if (e.shiftKey) {
251 | document.getSelection().removeAllRanges();
252 | alert(self.getJSONPath());
253 | return;
254 | }
255 | }, false);
256 | }
257 |
258 | _NodeSimple.prototype = {
259 | constructor: _NodeSimple,
260 |
261 | /**
262 | * Mark node
263 | */
264 | mark: function () {
265 | this.el.classList.add('jsontree_node_marked');
266 | },
267 |
268 | /**
269 | * Unmark node
270 | */
271 | unmark: function () {
272 | this.el.classList.remove('jsontree_node_marked');
273 | },
274 |
275 | /**
276 | * Mark or unmark node
277 | */
278 | toggleMarked: function () {
279 | this.el.classList.toggle('jsontree_node_marked');
280 | },
281 |
282 | /**
283 | * Expands parent node of this node
284 | *
285 | * @param isRecursive {boolean} - if true, expands all parent nodes
286 | * (from node to root)
287 | */
288 | expandParent: function (isRecursive) {
289 | if (!this.parent) {
290 | return;
291 | }
292 |
293 | this.parent.expand();
294 | this.parent.expandParent(isRecursive);
295 | },
296 |
297 | /**
298 | * Returns JSON-path of this
299 | *
300 | * @param isInDotNotation {boolean} - kind of notation for returned json-path
301 | * (by default, in bracket notation)
302 | * @returns {string}
303 | */
304 | getJSONPath: function (isInDotNotation) {
305 | if (this.isRoot) {
306 | return "$";
307 | }
308 |
309 | let currentPath;
310 |
311 | if (this.parent.type === 'array') {
312 | currentPath = "[" + this.label + "]";
313 | } else {
314 | currentPath = isInDotNotation ? "." + this.label : "['" + this.label + "']";
315 | }
316 |
317 | return this.parent.getJSONPath(isInDotNotation) + currentPath;
318 | }
319 | };
320 |
321 |
322 | /*
323 | * The constructor for boolean values
324 | * {...
325 | * [+] "label": boolean,
326 | * ...}
327 | * boolean = true || false
328 | *
329 | * @constructor
330 | * @param label {string} - key name
331 | * @param val {boolean} - value of boolean type, true or false
332 | * @param isLast {boolean} - true if node is last in list of parent childNodes
333 | */
334 | function NodeBoolean(label, val, isLast) {
335 | this.type = 'boolean';
336 |
337 | _NodeSimple.call(this, label, val, isLast);
338 | }
339 | utils.inherits(NodeBoolean, _NodeSimple);
340 |
341 |
342 | /*
343 | * The constructor for number values
344 | * {...
345 | * [+] "label": number,
346 | * ...}
347 | * number = 123
348 | *
349 | * @constructor
350 | * @param label {string} - key name
351 | * @param val {number} - value of number type, for example 123
352 | * @param isLast {boolean} - true if node is last in list of parent childNodes
353 | */
354 | function NodeNumber(label, val, isLast) {
355 | this.type = "number";
356 |
357 | _NodeSimple.call(this, label, val, isLast);
358 | }
359 | utils.inherits(NodeNumber, _NodeSimple);
360 |
361 |
362 | /*
363 | * The constructor for string values
364 | * {...
365 | * [+] "label": string,
366 | * ...}
367 | * string = "abc"
368 | *
369 | * @constructor
370 | * @param label {string} - key name
371 | * @param val {string} - value of string type, for example "abc"
372 | * @param isLast {boolean} - true if node is last in list of parent childNodes
373 | */
374 | function NodeString(label, val, isLast) {
375 | this.type = "string";
376 |
377 | _NodeSimple.call(this, label, '"' + val + '"', isLast);
378 | }
379 | utils.inherits(NodeString, _NodeSimple);
380 |
381 |
382 | /*
383 | * The constructor for null values
384 | * {...
385 | * [+] "label": null,
386 | * ...}
387 | *
388 | * @constructor
389 | * @param label {string} - key name
390 | * @param val {null} - value (only null)
391 | * @param isLast {boolean} - true if node is last in list of parent childNodes
392 | */
393 | function NodeNull(label, val, isLast) {
394 | this.type = "null";
395 |
396 | _NodeSimple.call(this, label, val, isLast);
397 | }
398 | utils.inherits(NodeNull, _NodeSimple);
399 |
400 |
401 | /*
402 | * The constructor for complex types (object, array)
403 | * {...
404 | * [+] "label": value,
405 | * ...}
406 | * value = object || array
407 | *
408 | * Markup:
409 | *
410 | *
411 | *
412 | *
413 | * "label"
414 | *
415 | * :
416 | *
417 | *
418 | *
{
419 | *
420 | *
}
421 | * ,
422 | *
423 | *
424 | *
425 | * @abstract
426 | * @param label {string} - key name
427 | * @param val {Object | Array} - a value of complex types, object or array
428 | * @param isLast {boolean} - true if node is last in list of parent childNodes
429 | */
430 | function _NodeComplex(label, val, isLast) {
431 | if (this.constructor === _NodeComplex) {
432 | throw new Error('This is abstract class');
433 | }
434 |
435 | let self = this,
436 | el = document.createElement('li'),
437 | template = function (label, sym) {
438 | let comma = (!isLast) ? ',' : '',
439 | str = '\
440 | \
441 |
\
442 |
' + sym[0] + ' \
443 |
… \
444 |
\
445 |
' + sym[1] + ' ' +
446 | '
' + comma +
447 | '
';
448 |
449 | if (label !== null) {
450 | str = '\
451 | \
452 | ' +
453 | ' ' +
454 | '"' + label +
455 | '" : \
456 | ' + str;
457 | }
458 |
459 | return str;
460 | },
461 | childNodesUl,
462 | labelEl,
463 | moreContentEl,
464 | childNodes = [];
465 |
466 | self.label = label;
467 | self.isComplex = true;
468 |
469 | el.classList.add('jsontree_node');
470 | el.classList.add('jsontree_node_complex');
471 | el.innerHTML = template(label, self.sym);
472 |
473 | childNodesUl = el.querySelector('.jsontree_child-nodes');
474 |
475 | if (label !== null) {
476 | labelEl = el.querySelector('.jsontree_label');
477 | moreContentEl = el.querySelector('.jsontree_show-more');
478 |
479 | labelEl.addEventListener('click', function (e) {
480 | if (e.altKey) {
481 | self.toggleMarked();
482 | return;
483 | }
484 |
485 | if (e.shiftKey) {
486 | document.getSelection().removeAllRanges();
487 | alert(self.getJSONPath());
488 | return;
489 | }
490 |
491 | self.toggle(e.ctrlKey || e.metaKey);
492 | }, false);
493 |
494 | moreContentEl.addEventListener('click', function (e) {
495 | self.toggle(e.ctrlKey || e.metaKey);
496 | }, false);
497 |
498 | self.isRoot = false;
499 | } else {
500 | self.isRoot = true;
501 | self.parent = null;
502 |
503 | el.classList.add('jsontree_node_expanded');
504 | }
505 |
506 | self.el = el;
507 | self.childNodes = childNodes;
508 | self.childNodesUl = childNodesUl;
509 |
510 | utils.forEachNode(val, function (label, node, isLast) {
511 | self.addChild(new Node(label, node, isLast));
512 | });
513 |
514 | self.isEmpty = !Boolean(childNodes.length);
515 | if (self.isEmpty) {
516 | el.classList.add('jsontree_node_empty');
517 | }
518 | }
519 |
520 | utils.inherits(_NodeComplex, _NodeSimple);
521 |
522 | utils.extend(_NodeComplex.prototype, {
523 | constructor: _NodeComplex,
524 |
525 | /*
526 | * Add child node to list of child nodes
527 | *
528 | * @param child {Node} - child node
529 | */
530 | addChild: function (child) {
531 | this.childNodes.push(child);
532 | this.childNodesUl.appendChild(child.el);
533 | child.parent = this;
534 | },
535 |
536 | /*
537 | * Expands this list of node child nodes
538 | *
539 | * @param isRecursive {boolean} - if true, expands all child nodes
540 | */
541 | expand: function (isRecursive) {
542 | if (this.isEmpty) {
543 | return;
544 | }
545 |
546 | if (!this.isRoot) {
547 | this.el.classList.add('jsontree_node_expanded');
548 | }
549 |
550 | if (isRecursive) {
551 | this.childNodes.forEach(function (item, i) {
552 | if (item.isComplex) {
553 | item.expand(isRecursive);
554 | }
555 | });
556 | }
557 | },
558 |
559 | /*
560 | * Collapses this list of node child nodes
561 | *
562 | * @param isRecursive {boolean} - if true, collapses all child nodes
563 | */
564 | collapse: function (isRecursive) {
565 | if (this.isEmpty) {
566 | return;
567 | }
568 |
569 | if (!this.isRoot) {
570 | this.el.classList.remove('jsontree_node_expanded');
571 | }
572 |
573 | if (isRecursive) {
574 | this.childNodes.forEach(function (item, i) {
575 | if (item.isComplex) {
576 | item.collapse(isRecursive);
577 | }
578 | });
579 | }
580 | },
581 |
582 | /*
583 | * Expands collapsed or collapses expanded node
584 | *
585 | * @param {boolean} isRecursive - Expand all child nodes if this node is expanded
586 | * and collapse it otherwise
587 | */
588 | toggle: function (isRecursive) {
589 | if (this.isEmpty) {
590 | return;
591 | }
592 |
593 | this.el.classList.toggle('jsontree_node_expanded');
594 |
595 | if (isRecursive) {
596 | let isExpanded = this.el.classList.contains('jsontree_node_expanded');
597 |
598 | this.childNodes.forEach(function (item, i) {
599 | if (item.isComplex) {
600 | item[isExpanded ? 'expand' : 'collapse'](isRecursive);
601 | }
602 | });
603 | }
604 | },
605 |
606 | /**
607 | * Find child nodes that match some conditions and handle it
608 | *
609 | * @param {Function} matcher
610 | * @param {Function} handler
611 | * @param {boolean} isRecursive
612 | */
613 | findChildren: function (matcher, handler, isRecursive) {
614 | if (this.isEmpty) {
615 | return;
616 | }
617 |
618 | this.childNodes.forEach(function (item, i) {
619 | if (matcher(item)) {
620 | handler(item);
621 | }
622 |
623 | if (item.isComplex && isRecursive) {
624 | item.findChildren(matcher, handler, isRecursive);
625 | }
626 | });
627 | }
628 | });
629 |
630 |
631 | /*
632 | * The constructor for object values
633 | * {...
634 | * [+] "label": object,
635 | * ...}
636 | * object = {"abc": "def"}
637 | *
638 | * @constructor
639 | * @param label {string} - key name
640 | * @param val {Object} - value of object type, {"abc": "def"}
641 | * @param isLast {boolean} - true if node is last in list of siblings
642 | */
643 | function NodeObject(label, val, isLast) {
644 | this.sym = ['{', '}'];
645 | this.type = "object";
646 |
647 | _NodeComplex.call(this, label, val, isLast);
648 | }
649 | utils.inherits(NodeObject, _NodeComplex);
650 |
651 |
652 | /*
653 | * The constructor for array values
654 | * {...
655 | * [+] "label": array,
656 | * ...}
657 | * array = [1,2,3]
658 | *
659 | * @constructor
660 | * @param label {string} - key name
661 | * @param val {Array} - value of array type, [1,2,3]
662 | * @param isLast {boolean} - true if node is last in list of siblings
663 | */
664 | function NodeArray(label, val, isLast) {
665 | this.sym = ['[', ']'];
666 | this.type = 'array';
667 |
668 | _NodeComplex.call(this, label, val, isLast);
669 | }
670 | utils.inherits(NodeArray, _NodeComplex);
671 |
672 | /* ---------- The tree constructor ---------- */
673 |
674 | /*
675 | * The constructor for json tree.
676 | * It contains only one Node (Array or Object), without property name.
677 | * CSS-styles of .tree define main tree styles like font-family,
678 | * font-size and own margins.
679 | *
680 | * Markup:
681 | *
684 | *
685 | * @constructor
686 | * @param jsonObj {Object | Array} - data for tree
687 | * @param domEl {DOMElement} - DOM-element, wrapper for tree
688 | */
689 | function Tree(jsonObj, domEl) {
690 | this.wrapper = document.createElement('ul');
691 | this.wrapper.className = 'jsontree_tree clearfix';
692 |
693 | this.rootNode = null;
694 |
695 | this.sourceJSONObj = jsonObj;
696 |
697 | this.loadData(jsonObj);
698 | this.appendTo(domEl);
699 | }
700 |
701 | Tree.prototype = {
702 | constructor: Tree,
703 |
704 | /**
705 | * Fill new data in current json tree
706 | *
707 | * @param {Object | Array} jsonObj - json-data
708 | */
709 | loadData: function (jsonObj) {
710 | if (!utils.isValidRoot(jsonObj)) {
711 | alert('The root should be an object or an array');
712 | return;
713 | }
714 |
715 | this.sourceJSONObj = jsonObj;
716 |
717 | this.rootNode = new Node(null, jsonObj, 'last');
718 | this.wrapper.innerHTML = '';
719 | this.wrapper.appendChild(this.rootNode.el);
720 | },
721 |
722 | /**
723 | * Appends tree to DOM-element (or move it to new place)
724 | *
725 | * @param {DOMElement} domEl
726 | */
727 | appendTo: function (domEl) {
728 | domEl.appendChild(this.wrapper);
729 | },
730 |
731 | /**
732 | * Expands all tree nodes (objects or arrays) recursively
733 | *
734 | * @param {Function} filterFunc - 'true' if this node should be expanded
735 | */
736 | expand: function (filterFunc) {
737 | if (this.rootNode.isComplex) {
738 | if (typeof filterFunc == 'function') {
739 | this.rootNode.childNodes.forEach(function (item, i) {
740 | if (item.isComplex && filterFunc(item)) {
741 | item.expand();
742 | }
743 | });
744 | } else {
745 | this.rootNode.expand('recursive');
746 | }
747 | }
748 | },
749 |
750 | /**
751 | * Collapses all tree nodes (objects or arrays) recursively
752 | */
753 | collapse: function () {
754 | if (typeof this.rootNode.collapse === 'function') {
755 | this.rootNode.collapse('recursive');
756 | }
757 | },
758 |
759 | /**
760 | * Returns the source json-string (pretty-printed)
761 | *
762 | * @param {boolean} isPrettyPrinted - 'true' for pretty-printed string
763 | * @returns {string} - for exemple, '{"a":2,"b":3}'
764 | */
765 | toSourceJSON: function (isPrettyPrinted) {
766 | if (!isPrettyPrinted) {
767 | return JSON.stringify(this.sourceJSONObj);
768 | }
769 |
770 | let DELIMETER = "[%^$#$%^%]",
771 | jsonStr = JSON.stringify(this.sourceJSONObj, null, DELIMETER);
772 |
773 | jsonStr = jsonStr.split("\n").join(" ");
774 | jsonStr = jsonStr.split(DELIMETER).join(" ");
775 |
776 | return jsonStr;
777 | },
778 |
779 | /**
780 | * Find all nodes that match some conditions and handle it
781 | */
782 | findAndHandle: function (matcher, handler) {
783 | this.rootNode.findChildren(matcher, handler, 'isRecursive');
784 | },
785 |
786 | /**
787 | * Unmark all nodes
788 | */
789 | unmarkAll: function () {
790 | this.rootNode.findChildren(function (node) {
791 | return true;
792 | }, function (node) {
793 | node.unmark();
794 | }, 'isRecursive');
795 | }
796 | };
797 |
798 |
799 | /* ---------- Public methods ---------- */
800 | return {
801 | /**
802 | * Creates new tree by data and appends it to the DOM-element
803 | *
804 | * @param jsonObj {Object | Array} - json-data
805 | * @param domEl {DOMElement} - the wrapper element
806 | * @returns {Tree}
807 | */
808 | create: (jsonObj, domEl) => new Tree(jsonObj, domEl),
809 |
810 | };
811 | })();
812 |
813 | export default jsonTree;
814 |
--------------------------------------------------------------------------------
/src/main/testsContext.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | const TestsContext = React.createContext([{}, () => { }]);
4 |
5 | const TestsProvider = (props) => {
6 | // Tests are objects with
7 | // { payload: JSON that represents test,
8 | // status: initial value of '' };
9 | const [tests, setTests] = useState([]);
10 | return (
11 |
12 | {props.children}
13 |
14 | );
15 | };
16 |
17 | export { TestsContext, TestsProvider };
18 |
--------------------------------------------------------------------------------
/test/spec.js:
--------------------------------------------------------------------------------
1 | const { Application } = require('spectron');
2 | const electronPath = require('electron');
3 | const path = require('path');
4 | const chai = require('chai');
5 | const chaiAsPromised = require('chai-as-promised');
6 | const mocha = require('mocha');
7 |
8 | const {
9 | describe, beforeEach, afterEach, it,
10 | } = mocha;
11 |
12 | chai.should();
13 | chai.use(chaiAsPromised);
14 |
15 | describe('App Launch', () => {
16 |
17 | beforeEach(() => {
18 | this.app = new Application({
19 | path: electronPath,
20 | args: [path.join(__dirname, '..')],
21 | });
22 | return this.app.start();
23 | });
24 |
25 | afterEach(() => {
26 | if (this.app && this.app.isRunning()) {
27 | return this.app.stop();
28 | }
29 | });
30 |
31 | it('opens a window', () => this.app.client.waitUntilWindowLoaded()
32 | .getWindowCount().should.eventually.equal(1));
33 | });
34 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const webpack = require('webpack');
2 | const path = require('path');
3 |
4 | // Config directories
5 | const SRC_DIR = path.resolve(__dirname, 'src');
6 | const OUTPUT_DIR = path.resolve(__dirname, 'dist');
7 | const defaultInclude = [SRC_DIR];
8 | const HtmlWebpackPlugin = require('html-webpack-plugin');
9 | const { spawn } = require('child_process');
10 |
11 | module.exports = {
12 | // original: entry: SRC_DIR + '/index.js'
13 | entry: `${SRC_DIR}/main/index.js`,
14 | output: {
15 | path: OUTPUT_DIR,
16 | publicPath: '../dist/',
17 | filename: 'bundle.js',
18 | },
19 |
20 | module: {
21 | rules: [
22 | {
23 | test: /\.css$/,
24 | use: [{ loader: 'style-loader' }, { loader: 'css-loader' }],
25 | include: defaultInclude,
26 | },
27 | {
28 | test: /\.jsx?$/,
29 | use: [{
30 | loader: 'babel-loader',
31 | options: {
32 | presets: ['@babel/preset-env'],
33 | plugins: ['@babel/plugin-proposal-object-rest-spread'],
34 | },
35 | }],
36 | include: defaultInclude,
37 | },
38 | {
39 | test: /\.(jpe?g|png|gif)$/,
40 | use: [{ loader: 'file-loader?name=img/[name]__[hash:base64:5].[ext]' }],
41 | include: defaultInclude,
42 | },
43 | {
44 | test: /\.(eot|svg|ttf|woff|woff2)$/,
45 | use: [{ loader: 'file-loader?name=font/[name]__[hash:base64:5].[ext]' }],
46 | include: defaultInclude,
47 | },
48 | {
49 | test: /test\.jsx?$/,
50 | use: 'mocha-loader',
51 | exclude: /node_modules/,
52 | },
53 | ],
54 | },
55 | resolve: {
56 | extensions: ['*', '.js', '.jsx'],
57 | alias: {
58 | 'react-dom': '@hot-loader/react-dom',
59 | },
60 | },
61 | target: 'electron-renderer',
62 | devtool: 'cheap-source-map',
63 | plugins: [
64 | new webpack.DefinePlugin({
65 | 'process.env.NODE_ENV': JSON.stringify('development'),
66 | }),
67 | new HtmlWebpackPlugin({
68 | template: 'public/index.html',
69 | inject: 'body',
70 | }),
71 | ],
72 | devServer: {
73 | contentBase: OUTPUT_DIR,
74 | publicPath: '/dist/',
75 | historyApiFallback: true,
76 | stats: {
77 | colors: true,
78 | chunks: false,
79 | children: false,
80 | },
81 | before() {
82 | spawn(
83 | 'electron',
84 | ['.'],
85 | { shell: true, env: process.env, stdio: 'inherit' },
86 | )
87 | .on('close', () => process.exit(0))
88 | .on('error', spawnError => console.error(spawnError));
89 | },
90 | },
91 | };
92 |
--------------------------------------------------------------------------------