├── .babelrc
├── .eslintrc.json
├── .gitignore
├── .prettierrc.json
├── Config Gif.gif
├── LICENSE
├── README.md
├── __mocks__
├── fs.js
└── graphql-request.js
├── __test__
├── UserDashboard.test.js
├── cli.test.js
├── fetchCatData.js
├── fetchProjData.js
└── mockContext.js
├── build
└── bundle.js
├── dashboard.gif
├── demo.gif
├── package-lock.json
├── package.json
├── src
├── assets
│ └── logo.png
├── commands
│ ├── configure.js
│ ├── default.js
│ ├── less.js
│ ├── mo.js
│ ├── templates
│ │ └── config.json
│ ├── testing.js
│ ├── utility
│ │ ├── fileHelpers.js
│ │ └── serverHelpers.js
│ ├── watch.js
│ └── watchmo.js
├── display
│ ├── Components
│ │ ├── App.jsx
│ │ ├── CategoriesContainer.jsx
│ │ ├── Category.jsx
│ │ ├── CategoryData.jsx
│ │ ├── ConfigDashboard.jsx
│ │ ├── ConfigResetModal.jsx
│ │ ├── ConfigSaveModal.jsx
│ │ ├── Context
│ │ │ └── ProjectContext.js
│ │ ├── FileSavedAlert.jsx
│ │ ├── ProjectSelect.jsx
│ │ ├── QueryItem.jsx
│ │ ├── QueryList.jsx
│ │ ├── TimeViztsx.tsx
│ │ ├── UserConfig.jsx
│ │ ├── UserDashboard.jsx
│ │ └── VertColViztsx.tsx
│ ├── index.html
│ ├── index.jsx
│ └── stylesheets
│ │ ├── index.css
│ │ └── style.scss
├── js
│ └── input-hook.js
├── server
│ ├── controllers
│ │ └── dataController.js
│ └── server.js
└── watchmoData
│ ├── default
│ └── config.json
│ ├── demo
│ ├── config.json
│ ├── parsedData.json
│ └── snapshots.txt
│ └── projectNames.json
├── tsconfig.json
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/preset-env",
4 | "@babel/preset-react",
5 | "@babel/preset-typescript"
6 | ]
7 | }
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es6": true,
5 | "node": true
6 | },
7 | "extends": ["airbnb", "plugin:prettier/recommended"],
8 | "globals": {
9 | "Atomics": "readonly",
10 | "SharedArrayBuffer": "readonly"
11 | },
12 | "parser": "@typescript-eslint/parser",
13 | "parserOptions": {
14 | "ecmaFeatures": {
15 | "jsx": true
16 | },
17 | "ecmaVersion": 2018,
18 | "sourceType": "module"
19 | },
20 | "plugins": ["react", "@typescript-eslint"],
21 | "rules": {
22 | "comma-dangle": ["error", "only-multiline"]
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true
3 | }
--------------------------------------------------------------------------------
/Config Gif.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/watchmo/b25ca8a58dbc992cdd11d77c9000b7cde90a5e2c/Config Gif.gif
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 APIsomorphic
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 | # _watchMo_
2 |
3 | Gather data over time from a Graph-Ql endpoint with a single command.
4 |
5 | Analyze response times of queries with simple but effective visualizations.
6 |
7 | Configure different categories of queries with either CLI or GUI.
8 |
9 | **_The more you watch the more you know, watchMo watch, watchMo mo!_**
10 |
11 | ## Getting Started
12 | To get access to the watchmo CLI, install watchmo globally:
13 |
14 | `npm install -g watchmo`
15 |
16 | To start, run:
17 |
18 | `watchmo --view`
19 |
20 | This should print to the terminal a logo and the project names 'default, demo'. If you want to run further tests, see the testing section below.
21 |
22 | The easiest way to get familiar with this tool is to use our built in demo project, which gathers data from an [open source GQL Database](https://countries.trevorblades.com/).
23 |
24 | To visualize the configuration for this project, run
25 |
26 | `watchmo configure demo --view`
27 |
28 | This configuration will work as is, but feel free to reconfigure this however you would like. The `watchmo configure --help` command provides information on how to do this.
29 |
30 | To begin gathering data, run:
31 |
32 | `watchmo watch demo`
33 |
34 | Let watchmo gather some data for a minute or two, then run
35 |
36 | `watchmo mo demo --open`
37 |
38 | and navigate to the demo project dashboard. Congratulations, you've successfully gathered and visualized GQL timing data!
39 |
40 | Once you want to start your own project, run:
41 |
42 | `watchmo configure [project name]`
43 |
44 | If you get stuck, run `watchmo --help` or `watchmo command --help` To see what you can do.
45 |
46 | Happy watching!
47 |
48 | ## Testing
49 |
50 | Watchmo comes pre-built with a testing suite to ensure everything is up and running correctly. Once you have installed watchmo, go ahead and run this testing suite with
51 |
52 | `watchmo test`
53 |
54 | If there are any problems, feel free to write up an issue.
55 |
56 | ## Built With
57 |
58 | * [React](https://reactjs.org/) - The web framework used
59 | * [Yargs](https://github.com/yargs/yargs) - To build the CLI tools
60 | * [Jest](https://jestjs.io/) - To build a testing suite
61 |
62 | ## Contributors
63 |
64 | * **Evan Hilton** - [GitHub](https://github.com/EH1537)
65 | * **Jason Jones** - [GitHub](https://github.com/JsonRoyJones)
66 | * **Sarah Song** - [GitHub](https://github.com/zavagezong)
67 | * **Spencer Wyman** - [GitHub](https://github.com/spencerWyman)
68 |
69 | ## License
70 |
71 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details
72 |
--------------------------------------------------------------------------------
/__mocks__/fs.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const fs = jest.genMockFromModule('fs');
3 |
4 | //the keys will be paths to files or directories
5 | // the values will be the file value, or a boolean signifying a directory exists
6 | let mockFiles = {};
7 |
8 | function __setMockFiles(newMockFiles) {
9 | mockFiles = Object.assign({}, newMockFiles);
10 | }
11 |
12 | function readFileSync(mockFilePath) {
13 | return mockFiles[mockFilePath];
14 | }
15 |
16 | function readFile(mockFilePath, dummyEncoding, callback) {
17 | if (callback) { callback(null, mockFiles[mockFilePath]); }
18 | return mockFiles[mockFilePath];
19 | }
20 |
21 | function writeFileSync(filePath, data) {
22 | mockFiles[filePath] = data;
23 | }
24 |
25 | function existsSync(filePath) {
26 | return (mockFiles[filePath] !== undefined);
27 | }
28 |
29 | function appendFile(filePath, data) {
30 | mockFiles[filePath] = mockFiles[filePath] + data;
31 | }
32 |
33 | function unlinkSync(filePath) {
34 | delete mockFiles[filePath];
35 | }
36 |
37 | function isPrefix(prefix, str) {
38 | if (prefix.length === str.length) {
39 | return false;
40 | }
41 | for (let i=0; i < prefix.length; i++) {
42 | if (prefix[i] !== str[i]) {
43 | return false;
44 | }
45 | }
46 | return true;
47 | }
48 |
49 | function rmdirSync(dirPath) {
50 | if (mockFiles[dirPath] === true) {
51 | for (let path in mockFiles) {
52 | if (isPrefix(dirPath, path)) {
53 | throw new Error("this directory is not empty")
54 | }
55 | }
56 | delete mockFiles[dirPath];
57 | }
58 | }
59 |
60 | function mkdirSync(dirPath) {
61 | mockFiles[dirPath] = true;
62 | }
63 |
64 | function copyFileSync(src, destination) {
65 | mockFiles[destination] = mockFiles[src];
66 | }
67 |
68 | fs.__setMockFiles = __setMockFiles;
69 | fs.readFileSync = readFileSync;
70 | fs.readFile = readFile;
71 | fs.writeFileSync = writeFileSync;
72 | fs.writeFile = writeFileSync;
73 | fs.existsSync = existsSync;
74 | fs.appendFile = appendFile;
75 | fs.unlinkSync = unlinkSync;
76 | fs.rmdirSync = rmdirSync;
77 | fs.mkdirSync = mkdirSync;
78 | fs.copyFileSync = copyFileSync;
79 |
80 | module.exports = fs;
81 |
--------------------------------------------------------------------------------
/__mocks__/graphql-request.js:
--------------------------------------------------------------------------------
1 | const graphql_request = jest.genMockFromModule('graphql-request');
2 |
3 | function request(endpoint, query) {
4 | return new Promise((resolve, reject) => {
5 | resolve("Test1");
6 | reject("Error");
7 | })
8 | }
9 |
10 | graphql_request.request = request;
11 |
12 | module.exports = graphql_request
13 |
--------------------------------------------------------------------------------
/__test__/UserDashboard.test.js:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react';
2 | import { shallow, mount } from 'enzyme';
3 | import fetchCatData from './fetchCatData'
4 | import fetchProjData from './fetchProjData'
5 | import ProjectSelect from '../src/display/Components/ProjectSelect'
6 | import UserDashboard from '../src/display/Components/UserDashboard'
7 | import VertColViz from '../src/display/Components/VertColViztsx';
8 | import TimeViz from '../src/display/Components/TimeViztsx';
9 | import renderer from 'react-test-renderer';
10 | import ProjectContext, { useProjectContext } from './mockContext'
11 | import * as ProjectContextModule from "./mockContext";
12 | import { stratify } from 'd3';
13 |
14 | const Enzyme = require("enzyme");
15 | const Adapter = require("enzyme-adapter-react-16");
16 |
17 | Enzyme.configure({ adapter: new Adapter() });
18 |
19 |
20 | describe(' ', () => {
21 | global.fetch = fetchProjData;
22 | let wrapper;
23 | let context = { project: "default" }
24 | it('Should render ', () => {
25 | jest.spyOn(ProjectContextModule, "useProjectContext").mockImplementation(() => ({
26 | project: ""
27 | }))
28 | wrapper = shallow(
29 |
30 | {context}
31 |
32 | );
33 | expect(wrapper).toBeDefined();
34 | })
35 | })
36 |
37 | describe(' ', () => {
38 | global.fetch = fetchCatData;
39 | let wrapper;
40 | let context = { project: "default" }
41 | it('Should render ', () => {
42 | jest.spyOn(ProjectContextModule, "useProjectContext").mockImplementation(() => ({
43 | project: "default"
44 | }))
45 | wrapper = shallow(
46 |
47 | {context}
48 |
49 | );
50 | expect(wrapper).toBeDefined();
51 | })
52 | })
53 |
54 | describe(' ', () => {
55 | let wrapper;
56 | let context = { project: "default" }
57 | const props = {
58 | dataCat: { "test": [{ time: "str", response: { key: "value" }, timing: [0, 3333] }] },
59 | }
60 | it('Should render ', () => {
61 | wrapper = shallow( , { context })
62 | expect(wrapper).toBeDefined();
63 | })
64 | })
65 |
66 | describe(' ', () => {
67 | let wrapper;
68 | const props = {
69 | timeData: { "test": [{ time: "str", response: { key: "value" }, timing: [0, 3333] }] },
70 | selectedQueries: ["test"],
71 | }
72 | it('Should render ', () => {
73 |
74 | wrapper = shallow(< TimeViz {...props} />)
75 | expect(wrapper).toBeDefined();
76 | })
77 | })
--------------------------------------------------------------------------------
/__test__/cli.test.js:
--------------------------------------------------------------------------------
1 | const fileHelpers = require('../src/commands/utility/fileHelpers');
2 | const { watch } = require('../src/commands/watch');
3 | const { cliDefault } = require('../src/commands/default');
4 | const { mo } = require('../src/commands/mo');
5 | const { less } = require('../src/commands/less');
6 | const { configure } = require('../src/commands/configure');
7 | const fs = require('fs');
8 |
9 | jest.mock('fs');
10 | jest.useFakeTimers();
11 | console.log = jest.fn();
12 |
13 | const {
14 | projectPath,
15 | configPath,
16 | rawDataPath,
17 | parsedDataPath,
18 | templatePath,
19 | projectNamesPath,
20 | } = fileHelpers.dataPaths('testProject');
21 |
22 | const mockConfig = {
23 | "endpoint": "testEndpoint",
24 | "categories": {
25 | "testing": {
26 | "queries": ["{ testingName { testingProp }}"],
27 | "frequency": 5000
28 | }
29 | }
30 | }
31 |
32 | const mockRawData = {
33 | "category":"testingCategory",
34 | "data":[
35 | {
36 | "query":"{ testingName { testingProp }}",
37 | "response":{"testingName":[{"testingProp":"test prop value"}]},
38 | "timing":[0,55907518],
39 | "timestamp":"2020-02-10T18:56:11.463Z"
40 | }
41 | ]
42 | }
43 |
44 | // same as mockRawData but the timestamp is for 1 minute later
45 |
46 |
47 | const mockParsedData = {
48 | "testingCategory":{
49 | "{ testingName { testingProp }}":[
50 | {
51 | "timestamp":"2020-02-10T18:56:11.463Z",
52 | "response":{
53 | "testingName":[{"testingProp":"test prop value"}]
54 | },
55 | "timing":[0,55907518]
56 | }
57 | ]
58 | }
59 | }
60 |
61 | const mockTemplate = {
62 | "endpoint": "",
63 | "categories": {
64 | "default": {
65 | "queries": [],
66 | "frequency": -1
67 | }
68 | }
69 | }
70 |
71 | const mockProjectNames = ['default', 'testProject'];
72 |
73 | const MOCK_FILES = {};
74 | MOCK_FILES[projectPath] = true;
75 | MOCK_FILES[configPath] = JSON.stringify(mockConfig);
76 | MOCK_FILES[rawDataPath] = JSON.stringify(mockRawData)+fileHelpers.DEMARCATION;
77 | MOCK_FILES[parsedDataPath] = JSON.stringify(mockParsedData);
78 | MOCK_FILES[projectNamesPath] = JSON.stringify(mockProjectNames);
79 | MOCK_FILES[templatePath] = JSON.stringify(mockTemplate);
80 |
81 |
82 | describe('fileHelpers', () => {
83 |
84 | beforeEach(() => {
85 | fs.__setMockFiles(MOCK_FILES);
86 |
87 | const projectDirectoryExists = fs.readFileSync(projectPath);
88 | const configObject = fs.readFileSync(configPath);
89 | const rawData = fs.readFileSync(rawDataPath);
90 | const parsedData = fs.readFileSync(parsedDataPath);
91 | expect(projectDirectoryExists).toBeTruthy();
92 | expect(configObject).toBeDefined();
93 | expect(rawData).toBeDefined();
94 | expect(parsedData).toBeDefined();
95 | })
96 |
97 | it('can check and parse files', () => {
98 | const configObject = fileHelpers.checkAndParseFile(configPath);
99 | const projectNames = fileHelpers.checkAndParseFile(projectNamesPath);
100 | const noFileObject = fileHelpers.checkAndParseFile('fakePath');
101 | expect(configObject).toEqual(mockConfig);
102 | expect(projectNames).toEqual(mockProjectNames);
103 | expect(noFileObject).toEqual({});
104 | })
105 |
106 | it('can append raw data', () => {
107 | const mockRawData2 = {
108 | "category":"testingCategory",
109 | "data":[
110 | {
111 | "query":"{ testingName { testingProp }}",
112 | "response":{"testingName":[{"testingProp":"test prop value"}]},
113 | "timing":[0,55907518],
114 | "timestamp":"2020-02-10T18:57:11.463Z"
115 | }
116 | ]
117 | };
118 |
119 | fileHelpers.appendRawData(mockRawData2, rawDataPath);
120 | const rawDataObject = fs.readFileSync(rawDataPath);
121 | expect(`${rawDataObject}`).toBe(`${JSON.stringify(mockRawData)}${fileHelpers.DEMARCATION}${JSON.stringify(mockRawData2)}${fileHelpers.DEMARCATION}`);
122 | })
123 |
124 | it('can write JSON', () => {
125 | const projectNames = ['default', 'testProject', 'secondTestProject'];
126 | fileHelpers.writeJSON(projectNamesPath, projectNames);
127 | expect(JSON.stringify(projectNames)).toEqual(fs.readFileSync(projectNamesPath));
128 | })
129 |
130 | it('can clean multiple files', () => {
131 | fileHelpers.cleanAllFiles([configPath, rawDataPath, parsedDataPath]);
132 |
133 | const configString = fs.readFileSync(configPath);
134 | const rawData = fs.readFileSync(rawDataPath);
135 | const parsedData = fs.readFileSync(parsedDataPath);
136 | expect(configString).toBe('');
137 | expect(rawData).toBe('');
138 | expect(parsedData).toBe('');
139 | })
140 |
141 | it('can remove a project', () => {
142 | fileHelpers.removeProject('testProject');
143 |
144 | let projectDirectoryExists = fs.readFileSync(projectPath);
145 | let configObject = fs.readFileSync(configPath);
146 | let rawData = fs.readFileSync(rawDataPath);
147 | let parsedData = fs.readFileSync(parsedDataPath);
148 | expect(projectDirectoryExists).toBeUndefined;
149 | expect(configObject).toBeUndefined();
150 | expect(rawData).toBeUndefined();
151 | expect(parsedData).toBeUndefined();
152 | })
153 | })
154 |
155 |
156 | describe('watchmo configure', () => {
157 |
158 | beforeEach(() => {
159 | fs.__setMockFiles(MOCK_FILES);
160 | })
161 |
162 | it('builds new files', () => {
163 | const newProjectPath = fileHelpers.dataPaths('newProject').projectPath;
164 | const newConfigPath = fileHelpers.dataPaths('newProject').configPath;
165 | let newProject = fs.readFileSync(newProjectPath);
166 | let newConfig = fs.readFileSync(newConfig);
167 | expect(newProject).toBeUndefined();
168 | expect(newConfig).toBeUndefined();
169 |
170 | configure('newProject');
171 |
172 | newProject = fs.readFileSync(newProjectPath);
173 | newConfig = JSON.parse(fs.readFileSync(newConfigPath));
174 | expect(newProject).toBeTruthy();
175 | expect(newConfig).toEqual(mockTemplate);
176 | })
177 |
178 | it('changes project names list', () => {
179 | let projectNames = JSON.parse(fs.readFileSync(projectNamesPath));
180 | expect(projectNames).toEqual(["default", "testProject"]);
181 |
182 | configure('newProject');
183 |
184 | projectNames = JSON.parse(fs.readFileSync(projectNamesPath));
185 | expect(projectNames).toEqual(["default", "testProject", "newProject"])
186 | })
187 |
188 | it('does nothing for existing projects', () => {
189 | const defaultConfigPath = fileHelpers.dataPaths('default').configPath
190 | let defaultConfig = fs.readFileSync(defaultConfigPath);
191 | expect(defaultConfig).toBeUndefined();
192 |
193 | //establishing the default project
194 | configure('default');
195 |
196 | defaultConfig = fs.readFileSync(defaultConfigPath);
197 |
198 | //configuring again, this should do nothing
199 | configure('default');
200 |
201 | const afterConfigureConfig = fs.readFileSync(defaultConfigPath);
202 |
203 | expect(defaultConfig).toBe(afterConfigureConfig);
204 | })
205 |
206 | it('changes the endpoint with the --endpoint flag', () => {
207 | let configObject = JSON.parse(fs.readFileSync(configPath));
208 |
209 | expect(configObject.endpoint).toEqual("testEndpoint");
210 |
211 | configure('testProject', true);
212 | configObject = JSON.parse(fs.readFileSync(configPath));
213 | expect(configObject.endpoint).toEqual("testEndpoint");
214 |
215 | configure('testProject', 'changedEndpoint');
216 | configObject = JSON.parse(fs.readFileSync(configPath));
217 | expect(configObject.endpoint).toEqual("changedEndpoint");
218 | })
219 |
220 | it('adds a category with the --category flag', () => {
221 | let configObject = JSON.parse(fs.readFileSync(configPath));
222 | const newCategoryObject = {
223 | queries: [],
224 | frequency: -1,
225 | }
226 |
227 | expect(configObject.categories.newCategory).toBeUndefined();
228 |
229 | configure('testProject', undefined, true, false, false, false, false);
230 | configObject = JSON.parse(fs.readFileSync(configPath));
231 | expect(configObject.categories.newCategory).toBeUndefined();
232 |
233 | configure('testProject', undefined, 'newCategory', false, false, false, false);
234 | configObject = JSON.parse(fs.readFileSync(configPath));
235 | expect(configObject.categories.newCategory).toBeDefined();
236 | expect(configObject.categories.newCategory).toEqual(newCategoryObject);
237 | })
238 |
239 | it('removes a category with --category --remove flag', () => {
240 | let configObject = JSON.parse(fs.readFileSync(configPath));
241 |
242 | expect(configObject.categories.testing).toBeDefined();
243 |
244 | configure('testProject', undefined, true, false, false, false, false);
245 | configObject = JSON.parse(fs.readFileSync(configPath));
246 | expect(configObject.categories.testing).toBeDefined();
247 |
248 | configure('testProject', undefined, 'testing', false, false, true, false);
249 | configObject = JSON.parse(fs.readFileSync(configPath));
250 | expect(configObject.categories.testing).toBeUndefined();
251 | })
252 |
253 | })
254 |
255 | describe("watchmo mo", () => {
256 |
257 | beforeEach(() => {
258 | fs.__setMockFiles(MOCK_FILES);
259 | fs.unlinkSync(parsedDataPath);
260 |
261 | const parsedData = fs.readFileSync(parsedDataPath);
262 | expect(parsedData).toBeUndefined();
263 | })
264 |
265 | it('writes data to the correct position', () => {
266 | mo('testProject', false);
267 | const parsedData = fs.readFileSync(parsedDataPath);
268 | expect(parsedData).toBeDefined();
269 | })
270 |
271 | it('correctly parses the raw data', () => {
272 | mo('testProject', false);
273 | const parsedData = JSON.parse(fs.readFileSync(parsedDataPath));
274 | expect(parsedData).toEqual(mockParsedData);
275 | })
276 |
277 | it('suppresses parsing with the -b option', () => {
278 | mo('testProject', false, true);
279 | const parsedData = fs.readFileSync(parsedDataPath);
280 | expect(parsedData).toBeUndefined();
281 | })
282 |
283 | it('does nothing when project does not exist', () => {
284 | const nonExistentProjectRawPath = fileHelpers.dataPaths('nonExistentProject');
285 | const nonExistentProjectParsedPath = fileHelpers.dataPaths('nonExistentProject');
286 | const nonExistentProjectRawData = fs.readFileSync(nonExistentProjectRawPath);
287 | expect(nonExistentProjectRawData).toBeUndefined();
288 |
289 | mo('nonExistentProject', false);
290 |
291 | const parsedData = fs.readFileSync(nonExistentProjectParsedPath);
292 | expect(parsedData).toBeUndefined();
293 | })
294 | })
295 | //
296 | describe("watchmo less", () => {
297 |
298 | beforeEach(() => {
299 | fs.__setMockFiles(MOCK_FILES);
300 | const parsedData = fs.readFileSync(parsedDataPath);
301 | const rawData = fs.readFileSync(rawDataPath);
302 | const projectNames = fs.readFileSync(projectNamesPath);
303 | const projectExists = fs.readFile(projectPath);
304 | expect(projectExists).toBeTruthy();
305 | expect(parsedData).toBeDefined();
306 | expect(rawData).toBeDefined();
307 | expect(projectNames).toBeDefined();
308 | })
309 |
310 | it("deletes data from correct position", () => {
311 | less("testProject");
312 | const parsedData = fs.readFileSync(parsedDataPath);
313 | const rawData = fs.readFileSync(rawDataPath);
314 | expect(parsedData).toBe("")
315 | expect(rawData).toBe("");
316 | })
317 |
318 | it("removes entire project with the -r option", () => {
319 | less("testProject", true);
320 | const parsedData = fs.readFileSync(parsedDataPath);
321 | const rawData = fs.readFileSync(rawDataPath);
322 | const projectExists = fs.readFile(projectPath);
323 | expect(projectExists).toBeUndefined();
324 | expect(parsedData).toBeUndefined();
325 | expect(rawData).toBeUndefined();
326 | })
327 |
328 | it("changes project names list with the -r option", () => {
329 | less("testProject", true);
330 | const projectNames = JSON.parse(fs.readFileSync(projectNamesPath));
331 | expect(projectNames).toEqual(["default"]);
332 | })
333 |
334 | it("won't delete the default project", () => {
335 | less("default", true);
336 | const projectNames = JSON.parse(fs.readFileSync(projectNamesPath));
337 | expect(projectNames).toEqual(["default", "testProject"]);
338 | })
339 | })
340 |
--------------------------------------------------------------------------------
/__test__/fetchCatData.js:
--------------------------------------------------------------------------------
1 | export default function() {
2 | return Promise.resolve({
3 | json: () =>
4 | Promise.resolve({
5 | "q1": [{ "timestamp": "str", "response": { "key": "value" }, "timing": [0, 3333] }],
6 | "q2": [{ "timestamp": "str", "response": { "key": "value" }, "timing": [0, 3333] }]
7 | })
8 | })
9 | }
--------------------------------------------------------------------------------
/__test__/fetchProjData.js:
--------------------------------------------------------------------------------
1 | export default function() {
2 | return Promise.resolve({
3 | json: () =>
4 | Promise.resolve(
5 | ["project1", "project2"]
6 | )
7 | })
8 | }
--------------------------------------------------------------------------------
/__test__/mockContext.js:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext } from "react";
2 |
3 | const ProjectContext = React.createContext({});
4 |
5 | export default ProjectContext;
6 |
7 | export const useProjectContext = () => useContext(ProjectContext)
8 |
--------------------------------------------------------------------------------
/dashboard.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/watchmo/b25ca8a58dbc992cdd11d77c9000b7cde90a5e2c/dashboard.gif
--------------------------------------------------------------------------------
/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/watchmo/b25ca8a58dbc992cdd11d77c9000b7cde90a5e2c/demo.gif
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "watchmo",
3 | "version": "1.1.2",
4 | "description": "",
5 | "main": "index.tsx",
6 | "scripts": {
7 | "test": "jest",
8 | "start": "node src/server/server.js",
9 | "build": "NODE_ENV=production & webpack",
10 | "dev": "NODE_ENV=development & nodemon src/server/server.js & webpack-dev-server --open",
11 | "lint": "eslint ."
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "git+https://github.com/oslabs-beta/watchmo.git"
16 | },
17 | "author": "",
18 | "license": "ISC",
19 | "bugs": {
20 | "url": "https://github.com/oslabs-beta/watchmo/issues"
21 | },
22 | "bin": {
23 | "watchmo": "src/commands/watchmo.js"
24 | },
25 | "homepage": "https://github.com/oslabs-beta/watchmo#readme",
26 | "jest": {
27 | "transformIgnorePatterns": [
28 | "/node_modules/(?!(reactstrap)/)"
29 | ],
30 | "moduleNameMapper": {
31 | "^.+\\.(css|less|scss)$": "babel-jest"
32 | }
33 | },
34 | "dependencies": {
35 | "@types/chrome": "0.0.91",
36 | "@types/d3": "^5.7.2",
37 | "@types/react": "^16.9.19",
38 | "@types/react-dom": "^16.9.4",
39 | "@types/reactstrap": "^8.4.1",
40 | "body-parser": "^1.19.0",
41 | "bootstrap": "^4.4.1",
42 | "chalk": "^3.0.0",
43 | "codemirror-graphql": "^0.11.6",
44 | "d3": "^5.15.0",
45 | "express": "^4.17.1",
46 | "express-graphql": "^0.9.0",
47 | "graphql": "^14.6.0",
48 | "graphql-request": "^1.8.2",
49 | "graphql-syntax-highlighter-react": "^0.4.0",
50 | "node-sass": "^4.13.1",
51 | "opn": "^6.0.0",
52 | "react": "^16.12.0",
53 | "react-dom": "^16.12.0",
54 | "react-router-dom": "^5.1.2",
55 | "reactstrap": "^8.4.1",
56 | "regenerator": "^0.14.2",
57 | "regenerator-runtime": "^0.13.3",
58 | "yargs": "^15.1.0"
59 | },
60 | "devDependencies": {
61 | "@babel/core": "^7.8.4",
62 | "@babel/preset-env": "^7.8.4",
63 | "@babel/preset-react": "^7.8.3",
64 | "@babel/preset-typescript": "^7.8.3",
65 | "@typescript-eslint/eslint-plugin": "^2.19.2",
66 | "@typescript-eslint/parser": "^2.19.2",
67 | "babel": "^6.23.0",
68 | "babel-jest": "^24.8.0",
69 | "babel-loader": "^8.0.6",
70 | "babel-plugin-transform-export-extensions": "^6.22.0",
71 | "cheerio": "^1.0.0-rc.3",
72 | "css-loader": "^3.4.2",
73 | "enzyme": "^3.10.0",
74 | "enzyme-adapter-react-16": "^1.15.1",
75 | "enzyme-to-json": "^3.4.4",
76 | "eslint": "^6.8.0",
77 | "eslint-config-airbnb": "^18.0.1",
78 | "eslint-config-node": "^4.0.0",
79 | "eslint-config-prettier": "^6.10.0",
80 | "eslint-plugin-import": "^2.20.1",
81 | "eslint-plugin-jsx-a11y": "^6.2.3",
82 | "eslint-plugin-node": "^11.0.0",
83 | "eslint-plugin-prettier": "^3.1.2",
84 | "eslint-plugin-react": "^7.18.3",
85 | "eslint-plugin-react-hooks": "^1.7.0",
86 | "jest": "^24.9.0",
87 | "jest-cli": "^25.1.0",
88 | "nodemon": "^2.0.2",
89 | "prettier": "^1.19.1",
90 | "react-test-renderer": "^16.12.0",
91 | "sass-loader": "^8.0.2",
92 | "style-loader": "^1.1.3",
93 | "ts-loader": "^6.2.1",
94 | "typescript": "^3.7.5",
95 | "webpack": "^4.41.6",
96 | "webpack-cli": "^3.3.11",
97 | "webpack-dev-server": "^3.10.3"
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/watchmo/b25ca8a58dbc992cdd11d77c9000b7cde90a5e2c/src/assets/logo.png
--------------------------------------------------------------------------------
/src/commands/configure.js:
--------------------------------------------------------------------------------
1 | const {
2 | dataPaths,
3 | checkAndParseFile,
4 | writeJSON,
5 | } = require('./utility/fileHelpers');
6 | const fs = require('fs');
7 | const chalk = require('chalk');
8 |
9 | /*
10 | Default behavior: builds directory '/watchmoData/[projectName]'
11 | and copies config file to '/watchmoData/[projectName]/config.json'
12 | */
13 |
14 | function createProject(projectName) {
15 | const { projectPath, configPath, templatePath, projectNamesPath } = dataPaths(projectName);
16 | const projectNamesArray = checkAndParseFile(projectNamesPath);
17 |
18 | const addProjectName = () => {
19 | projectNamesArray.push(projectName);
20 | writeJSON(projectNamesPath, projectNamesArray);
21 | }
22 |
23 | if (!fs.existsSync(projectPath)) {
24 | fs.mkdirSync(projectPath, (err) => console.log(err));
25 | fs.copyFileSync(templatePath, configPath);
26 | console.log(chalk.green.bold(`Project ${projectName} initialized.`));
27 | addProjectName();
28 | } else {
29 | console.log(chalk.yellow.underline.bold('Project already exists.'));
30 | }
31 | }
32 |
33 | function changeEndpoint(projectName, endpoint) {
34 | const { configPath } = dataPaths(projectName);
35 | const configObject = checkAndParseFile(configPath);
36 | if (typeof endpoint === 'boolean') {
37 | console.log(chalk.yellow("The '--endpoint' option requires a positional argument."));
38 | } else {
39 | configObject.endpoint = endpoint;
40 | writeJSON(configPath, configObject);
41 | }
42 | }
43 |
44 | function changeCategory(projectName, category, remove = false) {
45 | const { configPath } = dataPaths(projectName);
46 | const configObject = checkAndParseFile(configPath);
47 | if (typeof category === 'boolean') {
48 | console.log(chalk.yellow("The '--category' option requires a positional argument."));
49 | } else if (remove) {
50 | if (configObject.categories[category] !== undefined) {
51 | delete configObject.categories[category];
52 | writeJSON(configPath, configObject);
53 | } else {
54 | console.log(chalk.magenta(`The category ${category} you are trying to remove does not exist`));
55 | }
56 | } else {
57 | if (configObject.categories[category] === undefined) {
58 | configObject.categories[category] = {
59 | queries: [],
60 | frequency: -1,
61 | };
62 | writeJSON(configPath, configObject);
63 | } else {
64 | console.log(chalk.magenta(`The category ${category} already exists`));
65 | }
66 | }
67 | }
68 |
69 |
70 | // If remove is true, the query must be an integer. If false, it is a string
71 | function changeQuery(projectName, category, query, remove = false) {
72 | const { configPath } = dataPaths(projectName);
73 | const configObject = checkAndParseFile(configPath);
74 | if (typeof category === 'boolean' || typeof query === 'boolean') {
75 | console.log(chalk.yellow("The '--query' '--category' options both require a positional argument."));
76 | } else if (configObject.categories[category] === undefined) {
77 | console.log(chalk.magenta(`The category ${category} does not exist`));
78 | } else if (!remove) {
79 | configObject.categories[category].queries.push(query);
80 | writeJSON(configPath, configObject);
81 | } else if (remove) {
82 | if (configObject.categories[category].queries[query] === undefined) {
83 | console.log(chalk.magenta("\nThe query specified does not exist. Please give an index corresponding to the appropriate query. \nRun 'watchmo configure [project name] --view' to view the configuration and query indices\n"));
84 | } else {
85 | configObject.categories[category].queries.splice(query, 1);
86 | writeJSON(configPath, configObject);
87 | }
88 | }
89 | }
90 |
91 | function changeFrequency(projectName, category, frequency) {
92 | const { configPath } = dataPaths(projectName);
93 | const configObject = checkAndParseFile(configPath);
94 | console.log(parseInt(frequency));
95 | if (!parseInt(frequency)) {
96 | console.log(chalk.yellow("The --frequency option requires an integer as a positional argument"));
97 | } else {
98 | if (parseInt(frequency) <= 0) {
99 | console.log(chalk.yellow("Please give a positive integer for the frequency."));
100 | } else {
101 | configObject.categories[category].frequency = frequency;
102 | writeJSON(configPath, configObject);
103 | }
104 | }
105 | }
106 |
107 | function viewConfig(projectName) {
108 | const { configPath } = dataPaths(projectName);
109 | const configObject = checkAndParseFile(configPath);
110 | if (configObject.endpoint === undefined) {
111 | console.log(chalk.magenta(`\nThe project ${projectName} does not exist. \nRun 'watchmo configure ${projectName}' if you would like to create it.\n`));
112 | return;
113 | }
114 | console.log(chalk.green(`Project: ${projectName}\n`));
115 | console.log(chalk.green(`Endpoint: ${configObject.endpoint}`));
116 | console.log(chalk.green('Categories:'));
117 | for (let category in configObject.categories) {
118 | console.log(chalk.green(` ${category}: `));
119 | console.log(chalk.green(` queries:`));
120 | for (let i = 0; i < configObject.categories[category].queries.length; i++) {
121 | console.log(chalk.green(` ${i}: ${configObject.categories[category].queries[i]}`));
122 | }
123 | console.log(chalk.green(` frequency:${configObject.categories[category].frequency}`));
124 | }
125 | console.log('\n');
126 | }
127 |
128 | function configure(projectName, endpoint, category, query = false, frequency = false, remove = false, view = false) {
129 | // not if-else block to avoid bug with config.json auto deleting
130 | // default behavior, create project with the given project Name
131 | if (!endpoint && !category && !frequency && !view) { createProject(projectName); }
132 | else if (endpoint) { changeEndpoint(projectName, endpoint); }
133 | else if (category && query === false && frequency === false) { changeCategory(projectName, category, remove); }
134 | else if (category && query !== false && frequency === false) { changeQuery(projectName, category, query, remove); }
135 | else if (category && frequency !== false && query === false) { changeFrequency(projectName, category, frequency); }
136 | else if (view && !endpoint && !category && !frequency) { viewConfig(projectName); }
137 | else {
138 | console.log(chalk.red("Not a valid combination of flags. Please configure values one at a time."));
139 | }
140 | }
141 |
142 |
143 | module.exports = {
144 | configure
145 | }
146 |
--------------------------------------------------------------------------------
/src/commands/default.js:
--------------------------------------------------------------------------------
1 | const chalk = require('chalk');
2 | const {
3 | dataPaths,
4 | checkAndParseFile,
5 | } = require('./utility/fileHelpers');
6 |
7 | function cliDefault(view) {
8 | console.log(chalk.red.bold(` _ _ ___ ___ `));
9 | console.log(chalk.yellow.bold(` | | | | | \\/ | `));
10 | console.log(chalk.green.bold(`__ ____ _| |_ ___| |__ | . . | ___ `));
11 | console.log(chalk.blue.bold("\\ \\ /\\ / / _` | __/ __| '_ \\| |\\/| |/ _ \\ "));
12 | console.log(chalk.cyan.bold(` \\ V V / (_| | || (__| | | | | | | (_) |`));
13 | console.log(chalk.magenta.bold(` \\_/\\_/ \\__,_|\\__\\___|_| |_\\_| |_/\\___/ `));
14 | console.log(" ")
15 | if (view) {
16 | const { projectNamesPath } = dataPaths('default');
17 | const projectNames = checkAndParseFile(projectNamesPath);
18 | console.log("Current Projects:");
19 | for (let name of projectNames) {
20 | console.log(` ${name}`);
21 | }
22 | }
23 | }
24 |
25 | module.exports = { cliDefault };
26 |
--------------------------------------------------------------------------------
/src/commands/less.js:
--------------------------------------------------------------------------------
1 | const { cleanAllFiles, removeProject, dataPaths, checkAndParseFile, writeJSON } = require('./utility/fileHelpers');
2 | const fs = require('fs');
3 | const chalk = require('chalk');
4 | /*
5 | THIS FUNCTION DEPENDS UPON THE FOLLOWING FILE STRUCTURE:
6 | /src
7 | |
8 | -> /commands
9 | | |
10 | | -> /less.js
11 | |
12 | -> /watchmoData
13 | |
14 | -> /parsedData.json
15 | -> /snapshots.txt
16 | */
17 |
18 | function less(projectName, remove = false) {
19 | const { rawDataPath, parsedDataPath, projectPath, projectNamesPath } = dataPaths(projectName);
20 | let projectNamesArray = checkAndParseFile(projectNamesPath);
21 |
22 | const removeProjectName = () => {
23 | projectNamesArray = projectNamesArray.filter((el) => (el !== projectName));
24 | writeJSON(projectNamesPath, projectNamesArray);
25 | }
26 |
27 | if (!fs.existsSync(projectPath)) {
28 | console.log(chalk.cyan.bold(`\nProject ${projectName} is not configured\nRun "watchmo configure ${projectName}" to create this project\n`));;
29 | }
30 | else if (!remove) {
31 | cleanAllFiles([parsedDataPath, rawDataPath]);
32 | console.log(chalk.cyan.italic.underline('FILES CLEAN'));
33 | } else {
34 | if (projectName === 'default') {
35 | console.log(chalk.red.bold.underline('Cannot remove default file'));
36 | } else {
37 | removeProject(projectName);
38 | removeProjectName();
39 | console.log(chalk.black.bgRed.bold.underline(`PROJECT ${projectName} DELETED`));
40 |
41 | }
42 |
43 | }
44 |
45 | }
46 |
47 | module.exports = {
48 | less
49 | };
50 |
--------------------------------------------------------------------------------
/src/commands/mo.js:
--------------------------------------------------------------------------------
1 | //Helpers
2 | const {
3 | DEMARCATION,
4 | dataPaths,
5 | readParseWriteJSON,
6 | checkAndParseFile,
7 | } = require('./utility/fileHelpers');
8 | const { openServer } = require('./utility/serverHelpers');
9 | const chalk = require('chalk');
10 |
11 |
12 | //dataString is a string of JSON objects separated by DEMARCATION (a stylized WM right now)
13 | function parseData(dataString) {
14 | let categoricalResponses = dataString.split(DEMARCATION).filter(str => str);
15 | const parsed = {};
16 | categoricalResponses.forEach(catRes => {
17 | let parsedRes = JSON.parse(catRes);
18 | let category = parsedRes.category;
19 | if (!parsed[category]) {
20 | parsed[category] = {};
21 | }
22 | parsedRes.data.forEach(queryData => {
23 | let { timestamp, query, response, timing } = queryData;
24 | if (!parsed[category][query]) {
25 | parsed[category][query] = [{ timestamp, response, timing }];
26 | } else {
27 | parsed[category][query].push({ timestamp, response, timing });
28 | }
29 | });
30 | });
31 | return parsed;
32 | }
33 |
34 | function mo(projectName, shouldOpen, noBundle=false) {
35 | let { rawDataPath, parsedDataPath, projectNamesPath } = dataPaths(projectName);
36 | const projectNames = checkAndParseFile(projectNamesPath);
37 | if (projectNames.includes(projectName)) {
38 | if (!noBundle) {
39 | readParseWriteJSON(rawDataPath, parseData, parsedDataPath);
40 | }
41 | if (shouldOpen) { openServer(); }
42 | } else {
43 | console.log(chalk.cyan.bold(`\nProject ${projectName} is not configured\nRun "watchmo configure ${projectName}" to create this project\n`));;
44 | }
45 |
46 | }
47 |
48 | module.exports = { mo };
49 |
--------------------------------------------------------------------------------
/src/commands/templates/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "endpoint": "",
3 | "categories": {
4 | "default": {
5 | "queries": [],
6 | "frequency": -1
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/commands/testing.js:
--------------------------------------------------------------------------------
1 | const { exec } = require('child_process');
2 | const path = require('path');
3 | const TEST_PATH = path.join(__dirname, '../../../__test__/cli.test.js');
4 |
5 | function testing() {
6 | console.log("TESTING...");
7 | exec(`npm test`, (err, stdout, stderr) => {
8 | console.log(stdout);
9 | console.log(stderr);
10 | })
11 | }
12 |
13 | module.exports = {
14 | testing
15 | };
16 |
--------------------------------------------------------------------------------
/src/commands/utility/fileHelpers.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const PATH_TO_DATA = '../../watchmoData/';
4 | const chalk = require('chalk');
5 |
6 |
7 | //the characters demarcating the space between different responses in the rawData file
8 | const DEMARCATION = '*W*M*O*';
9 |
10 | //*** HELPER FUNCTIONS ***
11 |
12 | const dataPaths = (projectName) => ({
13 | projectPath: path.join(__dirname, PATH_TO_DATA, projectName),
14 | configPath: path.join(__dirname, PATH_TO_DATA, projectName, 'config.json'),
15 | rawDataPath: path.join(__dirname, PATH_TO_DATA, projectName, 'snapshots.txt'),
16 | parsedDataPath: path.join(__dirname, PATH_TO_DATA, projectName, 'parsedData.json'),
17 | templatePath: path.join(__dirname, '../templates/config.json'),
18 | projectNamesPath: path.join(__dirname, PATH_TO_DATA, 'projectNames.json'),
19 | })
20 |
21 | const checkAndParseFile = filePath => {
22 | if (fs.existsSync(filePath)) {
23 | return JSON.parse(fs.readFileSync(filePath));
24 | } else return {};
25 | };
26 |
27 |
28 | //DEMARCATION is used to demarcate new entries in the textfile
29 | const appendRawData = (data, savePath) => {
30 | fs.appendFile(savePath, JSON.stringify(data) + DEMARCATION, err => {
31 | if (err) {
32 | console.log(err);
33 | } else {
34 | console.log(chalk.green.bold.underline(`DATA SAVED TO ${savePath}`));
35 | }
36 | });
37 | }
38 |
39 | function writeJSON(savePath, object) {
40 | fs.writeFile(savePath, JSON.stringify(object), err => {
41 | if (err) {
42 | console.log(err);
43 | } else {
44 | console.log(chalk.green.bold.underline(`DATA SAVED TO ${savePath}`));
45 | }
46 | });
47 | }
48 |
49 | function readParseWriteJSON(readPath, parser, writePath) {
50 | fs.readFile(readPath, 'utf-8', (err, data) => {
51 | if (err) {
52 | console.log(chalk.red.bold.underline('Error reading file', err));
53 | } else {
54 | writeJSON(writePath, parser(data));
55 | }
56 | })
57 | }
58 |
59 | //Higher order function for creating functions for performing a file system action on all files in a given array
60 | const actionOnAllFiles = fileAction => (pathArray => {
61 | pathArray.forEach(path => {
62 | fileAction(path, err => {
63 | if (err) {
64 | console.log(err);
65 | }
66 | })
67 | })
68 | })
69 |
70 | const cleanAllFiles = actionOnAllFiles((path, errCb) => fs.writeFileSync(path, '', errCb));
71 |
72 | const checkAndRemoveAllFiles = actionOnAllFiles((path, errCb) => {
73 | if (fs.existsSync(path)) {
74 | fs.unlinkSync(path, errCb);
75 | }
76 | })
77 |
78 | const removeProject = projectName => {
79 | const { projectPath, configPath, rawDataPath, parsedDataPath } = dataPaths(projectName);
80 | checkAndRemoveAllFiles([configPath, rawDataPath, parsedDataPath]);
81 | fs.rmdirSync(projectPath);
82 | }
83 |
84 |
85 |
86 |
87 | module.exports = {
88 | checkAndParseFile,
89 | appendRawData,
90 | dataPaths,
91 | removeProject,
92 | cleanAllFiles,
93 | writeJSON,
94 | readParseWriteJSON,
95 | DEMARCATION,
96 | }
97 |
--------------------------------------------------------------------------------
/src/commands/utility/serverHelpers.js:
--------------------------------------------------------------------------------
1 | const { exec } = require('child_process');
2 | const opn = require('opn');
3 | const path = require('path');
4 | const PATH_TO_SERVER = path.join(__dirname, '../../server/server.js');
5 | const PORT = '3333';
6 | const chalk = require('chalk');
7 |
8 | const openServer = (serverPath = PATH_TO_SERVER, port = PORT) => {
9 | exec(`node ${serverPath}`);
10 | opn(`http://localhost:${port}`)
11 | }
12 |
13 |
14 |
15 |
16 | module.exports = {
17 | openServer,
18 | PORT,
19 | }
20 |
--------------------------------------------------------------------------------
/src/commands/watch.js:
--------------------------------------------------------------------------------
1 | const {
2 | dataPaths,
3 | checkAndParseFile,
4 | appendRawData,
5 | } = require('./utility/fileHelpers');
6 | const { request } = require('graphql-request');
7 | const chalk = require('chalk');
8 |
9 | // returns a promise that resolves to the response/timing object to be saved
10 | const buildQueryPromise = (endpoint, query) =>
11 | new Promise((resolve, reject) => {
12 | const start = process.hrtime();
13 | request(endpoint, query)
14 | .then(response => {
15 | const timing = process.hrtime(start);
16 | resolve({ response, timing });
17 | })
18 | .catch(err => {
19 | reject(err);
20 | });
21 | });
22 |
23 | // This function waits for the query Promise to resolve to the appropriate data for each query.
24 | // This ensures that the timer starting and stopping in the Promise itself is starting and stopping at the appropriate times
25 | // Finally, it saves the data built up from each query and saves it in the the given path as a JSON file.
26 | async function sendQueriesAndSave(endpoint, categoryName, category, rawDataPath, frequency) {
27 | if (frequency <= 0 || category.queries.length === 0) {
28 | console.log(chalk.magenta.bold("This category has not been configured. Consider using watchmo mo -o to look at the web Browser"));
29 | return;
30 | }
31 | const timingInfo = [];
32 | let responseObject;
33 |
34 | for (let i = 0; i < category.queries.length; i += 1) {
35 | let query = category.queries[i];
36 | let { response, timing } = await buildQueryPromise(endpoint, query).catch(err =>
37 | console.log(err)
38 | );
39 | responseObject = { query, response, timing, timestamp: new Date() };
40 | timingInfo.push(responseObject);
41 | }
42 | // this structure is necessary for parsing the saved data later, see 'mo.js', parseDataFileAndSave
43 | // saveData({ category: categoryName, data: timingInfo }, rawDataPath);
44 | appendRawData({ category: categoryName, data: timingInfo }, rawDataPath)
45 |
46 | // allows for calls to be made at the specified intervals
47 | setTimeout(
48 | () => sendQueriesAndSave(endpoint, categoryName, category, rawDataPath, frequency),
49 | frequency
50 | );
51 | }
52 |
53 | // sets an interval for each category of query (via recursive setTimeout)
54 | // Promises resolve with priority over setInterval, so the timing data isn't affected
55 | // We may want this to be a cron job or something else in the future
56 | function watch(projectName) {
57 | const { rawDataPath, configPath } = dataPaths(projectName);
58 | const { endpoint, categories } = checkAndParseFile(configPath);
59 |
60 | if (endpoint && categories) {
61 | for (let cat in categories) {
62 | setTimeout(
63 | () => sendQueriesAndSave(endpoint, cat, categories[cat], rawDataPath, categories[cat].frequency),
64 | categories[cat].frequency
65 | );
66 | }
67 | }
68 | else {
69 | console.log(chalk.cyan.bold(`\nProject ${projectName} is not configured\nRun "watchmo configure ${projectName}" to create this project\n`));
70 | }
71 | }
72 |
73 | module.exports = { watch };
74 |
--------------------------------------------------------------------------------
/src/commands/watchmo.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | // shebang necessary to interact with command line
3 |
4 | //Packages
5 | const yargs = require('yargs');
6 | //CLI functions
7 | const { watch } = require('./watch');
8 | const { cliDefault } = require('./default');
9 | const { mo } = require('./mo');
10 | const { less } = require('./less');
11 | const { configure } = require('./configure');
12 | const { testing } = require('./testing');
13 |
14 | //helper functions
15 | const projectPositional = (yargs, optionObject) => {
16 | yargs.positional('project', {
17 | describe: 'name of the project. Run `watchmo --view` to view existing projects, and `watchmo configure [project name] --view` to view that project configuration',
18 | type: 'string',
19 | default: 'default'
20 | }).options(optionObject)
21 | }
22 |
23 | const options = {
24 | default: {
25 | view: {
26 | alias: 'v',
27 | describe: 'prints the existing projects',
28 | type: 'boolean',
29 | }
30 | },
31 | mo: {
32 | bundle: {
33 | alias: 'b',
34 | describe: 'suppresses the parsing method. Used as an optimization for the --open option',
35 | type: 'boolean',
36 | },
37 | open: {
38 | alias: 'o',
39 | describe: 'starts the server and opens the browser GUI. Used with `watchmo mo`',
40 | type: 'boolean',
41 | },
42 | },
43 | less: {
44 | remove: {
45 | alias: 'r',
46 | describe: 'removes categories or queries from the specified config file',
47 | type: 'boolean',
48 | },
49 | },
50 | configure: {
51 | endpoint: {
52 | alias: 'e',
53 | describe: 'sets the endpoint for the specified project',
54 | type: 'string'
55 | },
56 | category: {
57 | alias: 'c',
58 | describe: 'creates the given category for the specified project, or specified a category for other options',
59 | type: 'string'
60 | },
61 | query: {
62 | alias: 'q',
63 | describe: 'creates the given query for the specified project in the specified category, or specifies the query for other options. Only used with `-c [categoryName]` or `-c [category name] -r`',
64 | type: 'string | integer'
65 | },
66 | frequency: {
67 | alias: 'f',
68 | describe: 'sets the frequency for a specified category in the specified project. Only used with `-c [category name]`',
69 | type: 'integer',
70 | },
71 | remove: {
72 | alias: 'r',
73 | describe: 'removes the category or query. Only used with `-c [category name]` or `-c [category name] -q [query index]`',
74 | type: 'boolean',
75 | },
76 | view: {
77 | alias: 'v',
78 | describe: 'prints the corresponding project config file',
79 | type: 'boolean',
80 | }
81 | },
82 | }
83 |
84 |
85 |
86 | //Defining the CLI functionality
87 | yargs
88 | .command('$0', 'welcome to watchmo! Use watchmo -v to view the existing projects', (yargs) => yargs.options(options.default), (argv) => cliDefault(argv.view))
89 | .command(
90 | 'watch [project]',
91 | 'gathers timing data from the project endpoint at the configured frequency',
92 | (yargs) => projectPositional(yargs, {}),
93 | (argv) => watch(argv.project)
94 | )
95 | .command(
96 | 'mo [project]',
97 | 'parses snapshot data',
98 | (yargs) => projectPositional(yargs, options.mo),
99 | (argv) => {
100 | mo(
101 | argv.project,
102 | argv.open,
103 | argv.bundle
104 | )
105 | }
106 | )
107 | .command(
108 | 'less [project]',
109 | 'cleans up data files',
110 | (yargs) => projectPositional(yargs, options.less),
111 | (argv) => less(argv.project, argv.remove)
112 | )
113 | .command(
114 | 'configure [project]',
115 | 'creates given project',
116 | (yargs) => projectPositional(yargs, options.configure),
117 | (argv) => configure(
118 | argv.project,
119 | argv.endpoint,
120 | argv.category,
121 | argv.query,
122 | argv.frequency,
123 | argv.remove,
124 | argv.view,
125 | )
126 | )
127 | .command(
128 | 'test',
129 | 'runs tests (powered by Jest)',
130 | {},
131 | (argv) => testing()
132 | )
133 | .help().argv;
134 |
--------------------------------------------------------------------------------
/src/display/Components/App.jsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from 'react';
2 | import { Route, BrowserRouter as Router } from 'react-router-dom';
3 | import ReactDOM from 'react-dom';
4 | import UserDashboard from './UserDashboard';
5 | import ConfigDashboard from './ConfigDashboard.jsx';
6 | import ProjectSelect from './ProjectSelect.jsx';
7 | import { ProjectProvider } from './Context/ProjectContext';
8 | import '../stylesheets/style.scss';
9 |
10 | export const App = () => {
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/src/display/Components/CategoriesContainer.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/destructuring-assignment */
2 | /* eslint-disable no-restricted-syntax */
3 | /* eslint-disable guard-for-in */
4 | /* eslint-disable react/prop-types */
5 | import React from 'react';
6 | import '../stylesheets/style.scss';
7 | import Category from './Category';
8 | // import { GraphqlCodeBlock } from 'graphql-syntax-highlighter-react';
9 |
10 | const CategoriesContainer = props => {
11 | // declaring an array to collect each set of data by category
12 | const localCats = [];
13 | // building array of category objects
14 | for (const category in props.configData.categories) {
15 | const lilCats = {};
16 | lilCats.name = category;
17 | lilCats.queries = props.configData.categories[category].queries;
18 | lilCats.frequency = props.configData.categories[category].frequency;
19 | localCats.push(lilCats);
20 | }
21 |
22 | return (
23 |
24 |
25 |
33 |
34 |
41 |
42 | Add Category
43 |
44 |
45 | Delete Category
46 |
47 |
48 | );
49 | };
50 | export default CategoriesContainer;
51 |
--------------------------------------------------------------------------------
/src/display/Components/Category.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-syntax */
2 | /* eslint-disable react/destructuring-assignment */
3 | /* eslint-disable react/prop-types */
4 | import React from 'react';
5 | import { Card, CardBody } from 'reactstrap';
6 | import '../stylesheets/style.scss';
7 | import CategoryData from './CategoryData';
8 | // import { GraphqlCodeBlock } from 'graphql-syntax-highlighter-react';
9 |
10 | const Category = props => {
11 | const categoryCards = [];
12 | function categoryBuilder(catDataParam) {
13 | for (const category of catDataParam) {
14 | categoryCards.push(
15 |
16 |
17 |
18 |
25 |
26 |
27 |
28 |
29 |
30 | );
31 | }
32 | }
33 | categoryBuilder(props.categories);
34 | return categoryCards;
35 | };
36 |
37 | export default Category;
38 |
--------------------------------------------------------------------------------
/src/display/Components/CategoryData.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-cycle */
2 | /* eslint-disable react/destructuring-assignment */
3 | /* eslint-disable react/prop-types */
4 | import React from 'react';
5 | import { Button, CardTitle, CardSubtitle, Input } from 'reactstrap';
6 | import '../stylesheets/style.scss';
7 | import QueryList from './QueryList';
8 | // import { GraphqlCodeBlock } from 'graphql-syntax-highlighter-react';
9 |
10 | const CategoryData = props => {
11 | return (
12 |
13 |
14 | {props.catData.name}
15 |
16 |
17 | Frequency(ms): {' '}
18 |
19 | (ex: 1 sec = 1000; 1 min = 60000; 30 min = 1800000; 1 hour = 3600000; 1 day = 86400000)
20 |
21 |
22 |
30 |
31 |
Queries:
32 |
39 |
40 |
48 | Add Query
49 |
50 |
51 |
52 | );
53 | };
54 | export default CategoryData;
55 |
--------------------------------------------------------------------------------
/src/display/Components/ConfigDashboard.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 | import React, { useState, useContext, useEffect } from 'react';
3 | import {
4 | Button,
5 | Col,
6 | Container,
7 | Form,
8 | FormGroup,
9 | Input,
10 | Label,
11 | Row
12 | } from 'reactstrap';
13 | import { Link } from 'react-router-dom';
14 | import { ProjectContext } from './Context/ProjectContext';
15 | import '../stylesheets/style.scss';
16 | import 'bootstrap/dist/css/bootstrap.css';
17 | import CategoriesContainer from './CategoriesContainer';
18 | import ConfigSaveModal from './ConfigSaveModal';
19 | import ConfigResetModal from './ConfigResetModal';
20 | // import FileSavedAlert from './FileSavedAlert';
21 |
22 | const ConfigDashboard = props => {
23 | const [origConfig, setOrigConfig] = useState({});
24 | const [dataFromConfig, setDataFromConfig] = useState({});
25 | const [endpointConfig, setEndpointConfig] = useState('');
26 | const [typedCat, setTypedCat] = useState('');
27 | // const [fileSavedAlert, setFileSavedAlert] = useState(false);
28 | const { project } = useContext(ProjectContext);
29 |
30 | if (!project.projects) {
31 | props.history.push('/');
32 | }
33 |
34 | async function fetchData() {
35 | const response = await fetch(`${project.projects}/config.json`);
36 | const result = await response.json().then(res => {
37 | setOrigConfig(res);
38 | setDataFromConfig(res);
39 | setEndpointConfig(res.endpoint);
40 | });
41 | }
42 |
43 | useEffect(() => {
44 | fetchData();
45 | }, []);
46 |
47 | const handleEndpointChange = e => {
48 | const url = e.target.value;
49 | const JSONified = JSON.stringify(dataFromConfig);
50 | const newDataFromConfig = JSON.parse(JSONified);
51 | newDataFromConfig.endpoint = url;
52 | setDataFromConfig(newDataFromConfig);
53 | setEndpointConfig(url);
54 | };
55 |
56 | const addTypedCat = e => {
57 | setTypedCat(e.target.value);
58 | };
59 |
60 | const addCategory = () => {
61 | const JSONified = JSON.stringify(dataFromConfig);
62 | const newDataFromConfig = JSON.parse(JSONified);
63 | newDataFromConfig.categories[typedCat] = {};
64 | newDataFromConfig.categories[typedCat].queries = [''];
65 | newDataFromConfig.categories[typedCat].frequency = '';
66 | setTypedCat('');
67 | setDataFromConfig(newDataFromConfig);
68 | };
69 |
70 | const delCategory = () => {
71 | const JSONified = JSON.stringify(dataFromConfig);
72 | const newDataFromConfig = JSON.parse(JSONified);
73 | delete newDataFromConfig.categories[typedCat];
74 | setTypedCat('');
75 | setDataFromConfig(newDataFromConfig);
76 | };
77 |
78 | const queryChange = e => {
79 | const catName = e.target.id.split('-')[0];
80 | const queryIdx = e.target.id.split('-')[1];
81 | const JSONified = JSON.stringify(dataFromConfig);
82 | const newDataFromConfig = JSON.parse(JSONified);
83 | newDataFromConfig.categories[catName].queries[queryIdx] = e.target.value;
84 | setDataFromConfig(newDataFromConfig);
85 | };
86 |
87 | const addQuery = e => {
88 | const catName = e.target.id.split('-')[0];
89 | const JSONified = JSON.stringify(dataFromConfig);
90 | const newDataFromConfig = JSON.parse(JSONified);
91 | newDataFromConfig.categories[catName].queries.push('');
92 | setDataFromConfig(newDataFromConfig);
93 | };
94 |
95 | const deleteQuery = e => {
96 | const catName = e.target.id.split('-')[0];
97 | const queryIdx = e.target.id.split('-')[1];
98 | const JSONified = JSON.stringify(dataFromConfig);
99 | const newDataFromConfig = JSON.parse(JSONified);
100 | newDataFromConfig.categories[catName].queries.splice(queryIdx, 1);
101 | setDataFromConfig(newDataFromConfig);
102 | };
103 |
104 | const frequencyChange = e => {
105 | const catName = e.target.id.split('-')[0];
106 | const JSONified = JSON.stringify(dataFromConfig);
107 | const newDataFromConfig = JSON.parse(JSONified);
108 | newDataFromConfig.categories[catName].frequency = e.target.value;
109 | setDataFromConfig(newDataFromConfig);
110 | };
111 |
112 | // func to update data within config file
113 | async function handleSubmit() {
114 | const data = { project: project.projects, data: dataFromConfig };
115 |
116 | //ensure that frequencies are whole positive integers
117 | for (let catName in dataFromConfig.categories) {
118 | if (/^\d+$/.test(dataFromConfig.categories[catName].frequency)===false){
119 | alert("Please ensure Frequencies are whole positive integers")
120 | return;
121 | }
122 | }
123 |
124 | await fetch('/api/configDash', {
125 | method: 'post',
126 | headers: {
127 | Accept: 'application/json',
128 | 'Content-Type': 'application/json'
129 | },
130 | body: JSON.stringify(data)
131 | });
132 | }
133 |
134 | const handleReset = () => {
135 | setDataFromConfig(origConfig);
136 | setEndpointConfig(origConfig.endpoint);
137 | props.history.push('/configDash');
138 | };
139 |
140 | // const fileSaved = () => {
141 | // setFileSavedAlert(true);
142 | // };
143 |
144 | return (
145 |
146 |
147 |
148 |
149 |
150 |
151 |
157 | Back to User Dashboard
158 |
159 |
160 |
161 |
162 |
163 |
169 | Back to Project Select
170 |
171 |
172 |
173 |
174 |
175 |
176 |
225 |
226 | );
227 | };
228 |
229 | export default ConfigDashboard;
230 |
--------------------------------------------------------------------------------
/src/display/Components/ConfigResetModal.jsx:
--------------------------------------------------------------------------------
1 | /* eslint react/no-multi-comp: 0, react/prop-types: 0 */
2 |
3 | import React, { useState } from 'react';
4 | import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
5 |
6 | const ConfigResetModal = props => {
7 | const { buttonLabel, className, handleReset } = props;
8 |
9 | const [modal, setModal] = useState(false);
10 |
11 | const toggle = () => setModal(!modal);
12 |
13 | const resetReload = () => {
14 | handleReset();
15 | toggle();
16 | };
17 |
18 | const center = true;
19 |
20 | return (
21 |
22 |
23 | {buttonLabel}
24 |
25 |
31 | Confirm Reset
32 |
33 | Form data will revert to your last saved configuration. Are you sure
34 | you want to do this?
35 |
36 |
37 |
38 | Reset Form
39 | {' '}
40 |
41 | Cancel Reset
42 |
43 |
44 |
45 |
46 | );
47 | };
48 |
49 | export default ConfigResetModal;
50 |
--------------------------------------------------------------------------------
/src/display/Components/ConfigSaveModal.jsx:
--------------------------------------------------------------------------------
1 | /* eslint react/no-multi-comp: 0, react/prop-types: 0 */
2 |
3 | import React, { useState } from 'react';
4 | import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
5 | // import FileSavedAlert from './FileSavedAlert';
6 |
7 | const ConfigSaveModal = props => {
8 | const { buttonLabel, className, handleSubmit } = props;
9 |
10 | const [modal, setModal] = useState(false);
11 |
12 | const toggle = () => setModal(!modal);
13 |
14 | const submitToggle = () => {
15 | handleSubmit();
16 | toggle();
17 | // fileSaved();
18 | };
19 |
20 | const center = true;
21 |
22 | return (
23 |
24 |
25 | {buttonLabel}
26 |
27 |
33 | Confirm Current Configuration
34 |
35 | Saving will overwrite your existing configuration. Are you sure you
36 | want to do this?
37 |
38 |
39 |
40 | Save
41 | {' '}
42 |
43 | Cancel
44 |
45 |
46 |
47 |
48 | );
49 | };
50 |
51 | export default ConfigSaveModal;
52 |
--------------------------------------------------------------------------------
/src/display/Components/Context/ProjectContext.js:
--------------------------------------------------------------------------------
1 | import React, { createContext, useState } from 'react';
2 |
3 | export const ProjectContext = createContext();
4 |
5 | export function ProjectProvider(props) {
6 | const [project, setProject] = useState({});
7 |
8 | // custom update function
9 | const updateProject = newProj => {
10 | setProject(newProj);
11 | };
12 |
13 | return (
14 |
20 | {props.children}
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/src/display/Components/FileSavedAlert.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { UncontrolledAlert } from 'reactstrap';
3 |
4 | function FileSavedAlert() {
5 | console.log('here');
6 | return File saved! ;
7 | }
8 |
9 | export default FileSavedAlert;
10 |
--------------------------------------------------------------------------------
/src/display/Components/ProjectSelect.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-syntax */
2 | import React, { useEffect, useState, useContext } from 'react';
3 | import {
4 | Button,
5 | ButtonDropdown,
6 | DropdownToggle,
7 | DropdownMenu,
8 | DropdownItem
9 | } from 'reactstrap';
10 | import { Link } from 'react-router-dom';
11 | import 'bootstrap/dist/css/bootstrap.css';
12 | import { ProjectContext } from './Context/ProjectContext';
13 |
14 | function ProjectSelect() {
15 | // setting the state for the drop down button with typescript
16 | const [dropdownProjOpen, setProjOpen] = useState(false);
17 |
18 | const { project, updateProject } = useContext(ProjectContext);
19 |
20 | // these are used to grab data from watchmo and loaded it into the state
21 | const [projsFromServer, setProjsFromServer] = useState([1, 2]);
22 | const [projGained, setDataGained] = useState(false);
23 |
24 | // function that is in charge of changing the state
25 | const toggleCat = () => {
26 | setProjOpen(!dropdownProjOpen);
27 | };
28 |
29 | useEffect(() => {
30 | if (!projGained) {
31 | fetch('/projectNames.json')
32 | .then(data => data.json())
33 | .then(parsed => {
34 | setProjsFromServer(parsed);
35 | });
36 | setDataGained(true);
37 | }
38 | }, [projsFromServer]);
39 |
40 | const projcategoriesInDropDown = [];
41 | for (const projects of projsFromServer) {
42 | projcategoriesInDropDown.push(
43 | updateProject({ projects })}>
44 | {projects}
45 |
46 | );
47 | }
48 |
49 | return (
50 |
51 |
52 |
53 |
54 | Projects:
55 |
56 | {projcategoriesInDropDown}
57 |
58 |
59 | {project.projects && (
60 |
61 |
62 |
63 |
64 | DASHBOARD
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | CONFIG
73 |
74 |
75 |
76 |
77 | )}
78 |
79 | );
80 | }
81 |
82 | export default ProjectSelect;
83 |
--------------------------------------------------------------------------------
/src/display/Components/QueryItem.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 | /* eslint-disable react/destructuring-assignment */
3 | import React from 'react';
4 | import { Button, Input } from 'reactstrap';
5 | import { runtime } from 'regenerator-runtime';
6 | import '../stylesheets/style.scss';
7 | // import { GraphqlCodeBlock } from 'graphql-syntax-highlighter-react';
8 |
9 | const QueryItem = props => {
10 | // const [queryStrings, setQueryString] = useState(props.queryItem);
11 |
12 | return (
13 |
14 |
15 |
23 |
32 | X
33 |
34 |
35 |
36 | );
37 | };
38 |
39 | export default QueryItem;
40 |
--------------------------------------------------------------------------------
/src/display/Components/QueryList.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 | import React from 'react';
3 | import { FormGroup } from 'reactstrap';
4 | import QueryItem from './QueryItem';
5 | import '../stylesheets/style.scss';
6 | // import { GraphqlCodeBlock } from 'graphql-syntax-highlighter-react';
7 |
8 | const QueryList = props => {
9 | const queryItems = [];
10 | for (let i = 0; i < props.queries.length; i += 1) {
11 | queryItems.push(
12 |
20 | );
21 | }
22 | return (
23 |
24 |
25 | {queryItems}
26 | {/* */}
27 |
28 |
29 | );
30 | };
31 |
32 | export default QueryList;
33 |
--------------------------------------------------------------------------------
/src/display/Components/TimeViztsx.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | select,
3 | line,
4 | curveCardinal,
5 | axisBottom,
6 | axisRight,
7 | scaleLinear,
8 | mouse
9 | } from 'd3';
10 | import React, { useRef, useEffect } from 'react';
11 | import '../stylesheets/style.scss'
12 | /* The useEffect Hook is for running side effects outside of React,
13 | for instance inserting elements into the DOM using D3 */
14 |
15 | interface TimeVizProps {
16 | key: string;
17 | timeData: any;
18 | selectedQueries: string[];
19 | }
20 | const TimeViz: React.FC = (props) => {
21 | let timeData = props.timeData;
22 | let selectedQueries = props.selectedQueries;
23 | let timing = {}; //this will be a {'querr': [array of all responsetimes]}
24 | let timeStamps: string[] = []; //this will be an array of timestamp strings
25 |
26 | for (let quer in timeData) {
27 | timing[quer] = [];
28 | timeData[quer].forEach(response => {
29 | timeStamps.push(response.timestamp);
30 | timing[quer].push((response.timing[0] + response.timing[1] / 1000000000))
31 | });
32 | timing[quer].shift()
33 | }
34 |
35 |
36 | // let [data, setData] = useState();
37 | let data : number[];
38 | if (timing[selectedQueries[0]]) {
39 | data = timing[selectedQueries[0]];
40 | }
41 | else {
42 | data = [.10, .10, .10, .10, .10, .10, .10];
43 | }
44 |
45 |
46 | // let [time, setTime] = useState([{ name: 'Query 1', labelOffset: 60, value: function (t) { return d3.10l(t, 1, 0.5); } },
47 | // ]);
48 | let svgRef = useRef();
49 | /*The most basic SVG file contains the following format:
50 | --Size of the viewport (Think of this as the image resolution)
51 | --Grouping instructions via the element -- How should elements be grouped?
52 | --Drawing instructions using the shapes elements
53 | --Style specifications describing how each element should be drawn.*/
54 | // will be called initially and on every data change
55 |
56 | useEffect(() => {
57 |
58 | // if (timing[selectedQueries[0]]) {
59 |
60 | // }
61 |
62 | let chartDiv = document.getElementById('chartArea') //grab the chart area that the graph lives in
63 | let margin = { yheight: chartDiv.clientHeight, xwidth: chartDiv.clientWidth } //margins required for resizing
64 |
65 | function redrawLine() {
66 | margin.yheight = chartDiv.clientHeight
67 | margin.xwidth = chartDiv.clientWidth
68 | xScale.range([0, margin.xwidth]);
69 | xAxis = axisBottom(xScale).ticks(data.length)//.tickFormat(index => Math.floor(index + 1));
70 | svg.select('.x-axis').call(xAxis)
71 | svg.select('.y-axis').style('transform', `translateX(${margin.xwidth}px)`)
72 | // svg.select('.y-axis').append('text')
73 |
74 | newLine = line().x((_value, index) => xScale(index)).y(yScale).curve(curveCardinal);
75 |
76 | svg
77 | .select('rect')
78 | .style('pointer-events', 'all')
79 | .attr('width', `${margin.xwidth}`);
80 |
81 | svg
82 | .selectAll('.line').attr('d', newLine)
83 | }
84 | window.addEventListener('resize', redrawLine); //force a redraw when the page resizes
85 |
86 |
87 | let max = Math.max(...data)
88 | let upperLine = 1.5 * max;
89 |
90 | let svg = select(svgRef.current);
91 |
92 | //range in the scales control how long the axis line is on the graph
93 | //domain is the complete set of values and the range is the set of resulting values of a function
94 | let xScale:any = scaleLinear().domain([0, data.length - 1]).range([0, margin.xwidth]);
95 | let yScale:any = scaleLinear().domain([0, upperLine]).range([300, 0]);
96 |
97 | //calling the xAxis function with current selection
98 | let xAxis:any = axisBottom(xScale).ticks(data.length)
99 | xAxis.tickFormat((domain) => (Math.floor(domain)).toString());
100 | svg.select('.x-axis').style('transform', 'translateY(300px)').style('filter', 'url(#glow)').call(xAxis)
101 | //\ticks are each value in the line
102 | let yAxis:any = axisRight(yScale).ticks(20)
103 | yAxis.tickFormat((domain) => (Math.round((domain) * 1000) / 1000).toString());
104 | svg.select('.y-axis').style('transform', `translateX(${margin.xwidth}px)`).style('filter', 'url(#glow)').call(yAxis)
105 | svg.select('.y-axis').append('text')
106 | .attr('class', 'yaxislabel')
107 | .attr('transform', 'rotate(90)')
108 | .attr('y', 20)
109 | .attr('dy', '-3em')
110 | .attr('x', '3.75em')
111 | // .attr('dx', '0.5em')
112 | .style('text-anchor', 'start')
113 | .style('fill', 'white')
114 | .attr('font-size', '20px')
115 | .text('Response Time(s)');
116 | //initialize a line to the value of line
117 | //x line is rendering xscale and y is rendering yscale
118 | let newLine : any = line().x((_value, index) => xScale(index)).y(yScale).curve(curveCardinal);
119 | //select all the line elements you find in the svg and synchronize with data provided
120 | //wrap data in another array so d3 doesn't generate a new path element for every element in data array
121 | //join creates a new path element for every new piece of data
122 | //class line is to new updating path elements
123 | //Container for the gradients
124 | let defs = svg.append('defs');
125 |
126 | //Filter for the outside glow
127 | let filter = defs.append('filter')
128 | .attr('id', 'glow');
129 | filter.append('feGaussianBlur')
130 | .attr('stdDeviation', '3.5')
131 | .attr('result', 'coloredBlur');
132 | let feMerge = filter.append('feMerge');
133 | feMerge.append('feMergeNode')
134 | .attr('in', 'coloredBlur');
135 | feMerge.append('feMergeNode')
136 | .attr('in', 'SourceGraphic');
137 |
138 | svg
139 | .append('rect')
140 | .style('fill', 'none')
141 | .style('pointer-events', 'all')
142 | .attr('width', `${margin.xwidth}`)
143 | .attr('height', 300)
144 | .on('mouseover', mouseover)
145 | .on('mousemove', mousemove)
146 | .on('mouseout', mouseout);
147 |
148 | svg
149 | .selectAll('.line')
150 | .data([data])
151 | .join('path')
152 | .attr('class', 'line')
153 | .attr('d', newLine)
154 | .attr('fill', 'none')
155 | .attr('stroke', 'rgb(6, 75, 115)')
156 | .style('filter', 'url(#glow)');
157 |
158 | // Create the circle that travels along the curve of chart
159 | // This allows to find the closest X index of the mouse:
160 | // let bisect = bisector(function (d) { return d.xScale; }).left;
161 | //the circle on the line
162 | let focus = svg
163 | .append('g')
164 | .append('circle')
165 | .style('fill', 'none')
166 | .attr('stroke', 'black')
167 | .attr('r', 8.5)
168 | .style('opacity', 0)
169 | //the the vertical line on the graph where the mouse is
170 | let mouseLine = svg
171 | .append('g')
172 | .append('path')
173 | .attr('class', 'mouse-line')
174 | .style('stroke', 'black')
175 | .style('stroke-width', '1px')
176 | .attr('height', 300)
177 | .style('opacity', 0);
178 | //the the text on the graph where the mouse is
179 | let focusText = svg
180 | .append('g')
181 | .append('text')
182 | .style('opacity', 0)
183 | .attr('text-anchor', 'left')
184 | .attr('alignment-baseline', 'middle')
185 |
186 | function mouseover() : void {
187 | focus.style('opacity', 1)
188 | focusText.style('opacity', 1)
189 | mouseLine.style('opacity', 1)
190 | }
191 |
192 | function mousemove():void {
193 | // recover coordinate we need
194 | let x0 : number = Math.ceil(xScale.invert(mouse(this)[0])) - 1;
195 | let selectedDataY : number = data[x0]
196 | focus
197 | .attr('cx', xScale(x0))
198 | .attr('cy', yScale(selectedDataY))
199 | focusText
200 | .html((x0) + ' : ' + selectedDataY + 's')
201 | .attr('x', xScale(x0) + 15)
202 | .attr('y', yScale(selectedDataY) - 25)
203 | mouseLine
204 | .attr('d', function () {
205 | let d = 'M' + xScale(x0) + ',' + 300; //this is drawing the line from 0 to 300px
206 | d += ' ' + xScale(x0) + ',' + 0;
207 | return d;
208 | })
209 |
210 | }
211 | function mouseout() : void {
212 | focus.style('opacity', 0)
213 | focusText.style('opacity', 0)
214 | mouseLine.style('opacity', 0)
215 | }
216 |
217 |
218 | },
219 | //rerender data here
220 | [data]);
221 | return (
222 |
223 |
224 |
225 |
226 |
227 | {/* setData(data.map(value => value + 5))}>
228 | Update Data */}
229 |
230 | )
231 | }
232 | export default TimeViz;
233 |
--------------------------------------------------------------------------------
/src/display/Components/UserConfig.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { ButtonDropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap';
3 | import { Link } from 'react-router-dom';
4 | import 'bootstrap/dist/css/bootstrap.css';
5 | import ConfigDashboard from './ConfigDashboard';
6 |
7 | // typescript: testing heading and caption
8 | const UserConfig = () => {
9 | // setting the state for the drop down button with typescript
10 | const [dropdownCatOpen, setCatOpen] = React.useState;
11 |
12 | // function that is in charge of changing the state
13 | const toggleCat = () => {
14 | setCatOpen(!dropdownCatOpen);
15 | };
16 |
17 | // request file from backend and loop through categories to create category selectors
18 | // replace this arrayOfCategories with logic that parses the response object into specific cats
19 | const arrayOfCategories = ['Cat 1', 'Cat 2', 'Cat 3'];
20 | const categories = [];
21 | for (let i = 0; i < arrayOfCategories.length; i += 1) {
22 | categories.push( {arrayOfCategories[i]} );
23 | }
24 | return (
25 |
26 |
User Config
27 |
28 | Back to User Dashboard
29 |
30 |
31 |
32 |
33 | Categories:
34 |
35 | {categories}
36 |
37 |
38 |
39 |
40 |
41 |
42 | );
43 | };
44 |
45 | export default UserConfig;
46 |
--------------------------------------------------------------------------------
/src/display/Components/UserDashboard.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable react/prop-types */
2 | /* eslint-disable no-restricted-syntax */
3 | /* eslint-disable guard-for-in */
4 | import React, { useContext, useEffect, useState } from 'react';
5 | import {
6 | Container,
7 | Row,
8 | Col,
9 | Button,
10 | ButtonDropdown,
11 | DropdownToggle,
12 | DropdownMenu,
13 | DropdownItem
14 | } from 'reactstrap';
15 |
16 | import { Link } from 'react-router-dom';
17 | import 'bootstrap/dist/css/bootstrap.css';
18 | import VertColViz from './VertColViztsx.tsx';
19 | import { ProjectContext } from './Context/ProjectContext';
20 |
21 | function UserDashboard(props) {
22 | const [dropdownCatOpen, setCatOpen] = useState(false);
23 |
24 | // these are used to grab data from watchmo and loaded it into the state
25 | const [dataFromServer, setDataFromServer] = useState([]);
26 | const [dataGained, setDataGained] = useState(false);
27 |
28 | // eslint-disable-next-line no-unused-vars
29 | const { project, updateProject } = useContext(ProjectContext);
30 |
31 | if (!project.projects) {
32 | props.history.push('/');
33 | }
34 |
35 | // this is to hold the current category to be displayed int he bar graph
36 | const [currentCat, setCurrentCat] = useState('');
37 |
38 | useEffect(() => {
39 | if (!dataGained) {
40 | fetch(`${project.projects}/parsedData.json`)
41 | .then(data => data.json())
42 | .then(parsed => {
43 | setDataFromServer(parsed);
44 | })
45 | .catch(err => console.log(err));
46 | setDataGained(true);
47 | }
48 | }, [project]);
49 |
50 | // function that is in charge of changing the state
51 | const toggleCat = () => {
52 | setCatOpen(!dropdownCatOpen);
53 | };
54 | // dropdown menu items construction with categories from the data (the first layer of keys in the object)
55 | const categoriesInDropDown = [];
56 | for (const category in dataFromServer) {
57 | categoriesInDropDown.push(
58 | setCurrentCat(category)}>
59 | {category}
60 |
61 | );
62 | }
63 |
64 | return (
65 |
66 |
67 |
68 |
69 |
70 |
71 | Project Select
72 |
73 |
74 |
75 |
76 |
77 |
78 | Config Dashboard
79 |
80 |
81 |
82 |
83 |
84 |
85 |
User Dashboard
86 |
87 |
88 |
89 | Categories:
90 |
91 | {categoriesInDropDown}
92 |
93 |
94 |
95 |
96 |
97 |
98 | );
99 | }
100 |
101 | export default UserDashboard;
102 |
--------------------------------------------------------------------------------
/src/display/Components/VertColViztsx.tsx:
--------------------------------------------------------------------------------
1 | import { select, axisBottom, axisRight, scaleLinear, scaleBand } from 'd3';
2 | import React, { useRef, useEffect, useState } from 'react';
3 | import '../stylesheets/style.scss';
4 | import TimeViz from './TimeViztsx';
5 |
6 | interface VertColVisProps {
7 | dataCat: any[];
8 | }
9 | const VertColViz: React.FC = props => {
10 | let queries: string[] = [];
11 | let responses: any[] = [];
12 | let localQuerySelected: string[] = [];
13 | let timeGraph: JSX.Element =
;
14 | const [selectedQuery, setSelectedQuery] = useState([]);
15 | const [renderLine, setRenderLine] = useState(false);
16 |
17 | function addOrRemove(queryIn) {
18 | if (localQuerySelected.includes(queryIn)) {
19 | localQuerySelected = [];
20 | setSelectedQuery([]);
21 | setRenderLine(false);
22 | } else {
23 | localQuerySelected = [];
24 | localQuerySelected.push(queryIn);
25 | setSelectedQuery([]);
26 | setSelectedQuery([queryIn]);
27 | setRenderLine(true);
28 | }
29 | }
30 |
31 | const svgRef = useRef();
32 | /*The most basic SVG file contains the following format:
33 |
34 | --Size of the viewport (Think of this as the image resolution)
35 | --Grouping instructions via the element -- How should elements be grouped?
36 | --Drawing instructions using the shapes elements
37 | --Style specifications describing how each element should be drawn.*/
38 | // will be called initially and on every data changes
39 |
40 | useEffect(() => {
41 | setRenderLine(false); //these are necessary to effectively blank out the graph and line charts when switching categories
42 | setSelectedQuery([]); //this is necessary to keep switching categories from messing things up
43 |
44 | for (let query in props.dataCat) {
45 | let timeTot: number = 0;
46 | queries.push(query);
47 | props.dataCat[query].forEach(time => {
48 | timeTot += time.timing[1] / 1000000000;
49 | });
50 | responses.push(timeTot / props.dataCat[query].length);
51 | }
52 |
53 | const svg: any = select(svgRef.current);
54 |
55 | //used for dynamic y-axis
56 | let max: number = Math.max(...responses);
57 | let upper: number = 1.5 * max;
58 |
59 | const chartDiv: HTMLElement = document.getElementById('chartArea'); //grab the chart area that the graph lives in
60 | const margin = { yheight: chartDiv.clientHeight, xwidth: chartDiv.clientWidth }; //margins required for resizing
61 |
62 | function redrawBar() {
63 | margin.yheight = chartDiv.clientHeight;
64 | margin.xwidth = chartDiv.clientWidth;
65 |
66 | xScale.range([0, margin.xwidth]);
67 |
68 | svg
69 | .select('.y-axis')
70 | .style('transform', `translateX(${margin.xwidth}px)`)
71 | .call(yAxis);
72 |
73 | xAxis = axisBottom(xScale).ticks(responses.length + 1);
74 |
75 | svg.select('.x-axis').call(xAxis);
76 |
77 | svg
78 | .selectAll('.bar')
79 | .attr('x', (_value, index) => xScale(index))
80 | .attr('width', xScale.bandwidth());
81 | }
82 |
83 | window.addEventListener('resize', redrawBar);
84 | // scales
85 | let xScale = scaleBand()
86 | .domain(responses.map((_value, index) => index)) //x-axis labeled here
87 | .range([0, margin.xwidth])
88 | .padding(0.5);
89 |
90 | const yScale = scaleLinear()
91 | .domain([0, upper])
92 | .range([300, 0]);
93 |
94 | const colorScale = scaleLinear()
95 | .domain([upper * 0.2, upper])
96 | .range(['blue', 'red'])
97 | .clamp(true);
98 | let defs = svg.append('defs');
99 |
100 | //Filter for the outside glow
101 | let filter = defs.append('filter').attr('id', 'glow');
102 | filter
103 | .append('feGaussianBlur')
104 | .attr('stdDeviation', '3.5')
105 | .attr('result', 'coloredBlur');
106 | let feMerge = filter.append('feMerge');
107 | feMerge.append('feMergeNode').attr('in', 'coloredBlur');
108 | feMerge.append('feMergeNode').attr('in', 'SourceGraphic');
109 | // create x-axis
110 | let xAxis = axisBottom(xScale).ticks(responses.length);
111 | svg
112 | .select('.x-axis')
113 | .style('transform', 'translateY(300px)')
114 | .call(xAxis)
115 | .style('filter', 'url(#glow)');
116 |
117 | // create y-axis
118 | //location of bars, the higher the number, the higher the position on the graph
119 | const yAxis = axisRight(yScale);
120 | svg
121 | .select('.y-axis')
122 | .style('transform', `translateX(${margin.xwidth}px)`)
123 | .style('filter', 'url(#glow)')
124 | .call(yAxis);
125 |
126 | if (responses.length !== 0) {
127 | svg
128 | .select('.y-axis')
129 | .append('text')
130 | .attr('class', 'yaxislabel')
131 | .attr('transform', 'rotate(90)')
132 | .attr('y', 20)
133 | .attr('dy', '-3em')
134 | .attr('x', '3em')
135 | .style('text-anchor', 'start')
136 | .style('fill', 'white')
137 | .attr('font-size', '20px')
138 | .text('Avg. Response Time(s)');
139 | }
140 |
141 | // draw the bars
142 | svg
143 | .selectAll('.bar')
144 | .data(responses)
145 | .join('rect')
146 | .attr('class', 'bar')
147 | .style('transform', 'scale(1, -1)')
148 | .attr('x', (_value, index) => xScale(index))
149 | .attr('y', -300)
150 | .attr('width', xScale.bandwidth())
151 | .on('mouseenter', (value, index) => {
152 | svg
153 | .selectAll('.tooltip')
154 | .append('div')
155 | .data([value])
156 | .join(enter => enter.append('text').attr('y', yScale(value) - 50))
157 | .attr('class', 'tooltip')
158 | .text(`${queries[index]}`)
159 | .attr('x', xScale(index) + xScale.bandwidth() / 2)
160 | .attr('text-anchor', 'middle')
161 | .transition()
162 | .attr('y', yScale(value) - 80)
163 | .style('opacity', 1);
164 | })
165 | .on('mouseleave', () => svg.select('.tooltip').remove())
166 | .on('click', (_value, index) => {
167 | addOrRemove(`${queries[index]}`);
168 | })
169 | .transition()
170 | .attr('fill', colorScale)
171 | .attr('height', value => 350 - yScale(value));
172 | }, [props.dataCat]);
173 |
174 | if (renderLine === true) {
175 | timeGraph = (
176 |
177 | );
178 | }
179 |
180 | /*React fragments let you group a list of children without adding extra nodes to the DOM
181 | because fragments are not rendered to the DOM. */
182 | return (
183 |
184 |
185 |
186 |
187 |
188 | {timeGraph}
189 |
190 | );
191 | };
192 |
193 | export default VertColViz;
194 |
--------------------------------------------------------------------------------
/src/display/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | watchMo
5 |
6 |
7 |
8 |
9 |
10 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/display/index.jsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import * as ReactDom from 'react-dom';
3 | import { App } from './Components/App';
4 | import { ProjectProvider } from './Components/Context/ProjectContext';
5 |
6 | ReactDom.render(
7 |
8 |
9 | ,
10 | document.getElementById('root'));
11 |
12 |
--------------------------------------------------------------------------------
/src/display/stylesheets/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/src/display/stylesheets/style.scss:
--------------------------------------------------------------------------------
1 | footer {
2 | display: flex;
3 | justify-content: center;
4 | }
5 |
6 | h1,
7 | h2 {
8 | display: flex;
9 | justify-content: center;
10 | align-items: center;
11 | }
12 | div a {
13 | width: 100%;
14 | height: 100%;
15 | }
16 |
17 | .configButton {
18 | position: absolute;
19 | right: 10px;
20 | top: 15px;
21 | }
22 |
23 | #catInput {
24 | width: 100%;
25 | }
26 |
27 | #UserDashboard {
28 | color: white;
29 | font-family: 'Audiowide', cursive;
30 | }
31 |
32 | #configDashboard {
33 | width: 80%;
34 | color: black;
35 | font-family: 'Audiowide', cursive;
36 | }
37 |
38 | div#configHeader {
39 | background-color: lightgray;
40 | border-radius: 5px;
41 | }
42 |
43 | .router {
44 | width: 100%;
45 | display: flex;
46 | flex-direction: row;
47 | align-items: center;
48 | justify-content: center;
49 | }
50 |
51 | .categoriesDrop {
52 | display: flex;
53 | justify-content: center;
54 | align-items: center;
55 | }
56 |
57 | .btn {
58 | width: 100%;
59 | }
60 | #chartArea {
61 | width: 80%;
62 | }
63 | svg {
64 | width: 100%;
65 | height: 300px;
66 | overflow: visible;
67 | margin-bottom: 2rem;
68 | display: block;
69 | box-shadow: inset 1px 2000px rgba(208, 208, 208, 0.75);
70 | }
71 |
72 | text {
73 | color: whitesmoke;
74 | }
75 |
76 | span.querySpan.input {
77 | margin-right: 10px;
78 | }
79 |
80 | #root {
81 | font-family: 'Audiowide', cursive;
82 | flex-basis: 100%;
83 | display: flex;
84 | flex-direction: column;
85 | align-items: center;
86 | justify-content: center;
87 | }
88 |
89 | #userDashboard {
90 | display: flex;
91 | flex-direction: column;
92 | align-items: center;
93 | justify-content: center;
94 | flex-basis: 100%;
95 | height: 100vh;
96 | }
97 |
98 | #navBtn {
99 | display: flex;
100 | flex-direction: row;
101 | align-self: flex-end;
102 | align-items: center;
103 | }
104 |
105 | div#navBtn {
106 | display: flex;
107 | flex-direction: row-reverse;
108 | }
109 | span {
110 | display: flex;
111 | justify-content: space-evenly;
112 | }
113 |
114 | button {
115 | margin-top: 10px;
116 | margin-bottom: 5px;
117 | width: 100%;
118 | }
119 |
120 | button:hover {
121 | color: rgb(114, 120, 171);
122 | box-shadow: 0 5px 15px rgb(114, 120, 171);
123 | }
124 |
125 | div .tooltip {
126 | background: lightsteelblue;
127 | }
128 |
129 | text.tooltip {
130 | background: lightsteelblue;
131 | }
132 | .tooltip {
133 | padding: 5;
134 | font-size: 20px;
135 | fill: black;
136 | font-weight: 600;
137 | }
138 |
139 | .bar:hover {
140 | fill: rgb(255, 183, 75);
141 | fill-opacity: 70%;
142 | box-shadow: 0 5px 15px rgb(114, 120, 171);
143 | }
144 |
145 | #endpointHeader {
146 | margin-left: 10px;
147 | }
148 |
149 | #categoriesHeader {
150 | margin-left: 10px;
151 | }
152 |
153 | #btnAddQuery button {
154 | width: 100%;
155 | }
156 |
157 | button.btnSecondary.btn.btn-outline-secondary {
158 | width: 100%;
159 | background-color: whitesmoke;
160 | color: black;
161 | }
162 |
163 | .deleteBtn {
164 | color: white;
165 | margin-left: 10px;
166 | size: 10%;
167 | height: 50px;
168 | width: 50px;
169 | border-radius: 5px;
170 | }
171 |
172 | span#navBtnSpan {
173 | width: 100%;
174 | align-content: space-between;
175 | align-items: center;
176 | }
177 |
178 | #navBtnGroup {
179 | width: 100%;
180 | align-items: center;
181 | }
182 |
183 | #btnContainer {
184 | width: 100%;
185 | }
186 |
187 | div.col-6 {
188 | display: flex;
189 | flex-direction: column;
190 | align-items: center;
191 | }
192 |
193 | button#navUserDash {
194 | margin: auto;
195 | margin-top: 10px;
196 | margin-bottom: 10px;
197 | }
198 |
199 | button#navProjectSelect {
200 | margin: auto;
201 | margin-top: 10px;
202 | margin-bottom: 10px;
203 | }
204 |
205 | button#navConfigDash {
206 | display: flex;
207 | flex-direction: column;
208 | align-items: center;
209 | }
210 |
211 | a#navConfigLink {
212 | color: whitesmoke;
213 | text-decoration-line: none;
214 | width: 100%;
215 | }
216 |
217 | a#navUserDashLink {
218 | color: whitesmoke;
219 | text-decoration-line: none;
220 | width: 100%;
221 | }
222 |
223 | a#navProjLink {
224 | color: whitesmoke;
225 | text-decoration-line: none;
226 | width: 100%;
227 | }
228 |
229 | #projectSelect {
230 | display: flex;
231 | flex-direction: column;
232 | align-items: center;
233 | }
234 |
235 | link {
236 | color: white;
237 | // font-size: 100%;
238 | text-size-adjust: 100%;
239 | }
240 |
241 | .btn-primary {
242 | background-color: #073b66;
243 | border-color: #194779;
244 | }
245 |
246 | .close {
247 | width: 25%;
248 | }
249 |
250 | .btn-danger {
251 | margin: auto;
252 | }
253 |
254 | div#freqExamples {
255 | color: gray;
256 | }
257 |
258 | @media only screen and (max-width: 700px) {
259 | #navProjectSelect {
260 | width: max-content;
261 | max-width: 230px;
262 | flex-wrap: wrap;
263 | }
264 |
265 | .router {
266 | min-width: 500;
267 | }
268 |
269 | #navUserDash {
270 | width: max-content;
271 | }
272 |
273 | link {
274 | color: white;
275 | min-width: 230px;
276 | height: max-content;
277 | text-size-adjust: 100%;
278 | }
279 | }
280 |
--------------------------------------------------------------------------------
/src/js/input-hook.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 |
3 | export const useInput = initialValue => {
4 | const [value, setValue] = useState(initialValue);
5 |
6 | return {
7 | value,
8 | setValue,
9 | reset: () => setValue(''),
10 | bind: {
11 | value,
12 | onChange: event => {
13 | setValue(event.target.value);
14 | }
15 | }
16 | };
17 | };
18 |
19 | export default useInput;
20 |
--------------------------------------------------------------------------------
/src/server/controllers/dataController.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const chalk = require('chalk');
4 |
5 | const WMD = path.join(__dirname, '../../watchmoData');
6 |
7 | const dataController = {};
8 |
9 | dataController.getConfig = (req, res, next) => {
10 | res.locals.config = fs.readFileSync(WMD, (err, data) => {
11 | if (err) {
12 | return next(err);
13 | }
14 | const config = JSON.parse(data);
15 | res.locals.config = config;
16 | return next();
17 | });
18 | };
19 |
20 | dataController.updateConfig = (req, res, next) => {
21 | res.locals.config = req.body.data;
22 | const { project } = req.body;
23 | const configPost = `${WMD}/${project}/config.json`;
24 | fs.writeFile(configPost, JSON.stringify(res.locals.config), err => {
25 | if (err) {
26 | return next(err);
27 | }
28 | console.log(chalk.green.bold('file saved!'));
29 | return next();
30 | });
31 | };
32 |
33 | module.exports = dataController;
34 |
--------------------------------------------------------------------------------
/src/server/server.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable consistent-return */
2 | const express = require('express');
3 | const path = require('path');
4 | const bodyParser = require('body-parser');
5 | const chalk = require('chalk');
6 | const dataController = require('./controllers/dataController');
7 | const { PORT } = require('../commands/utility/serverHelpers');
8 |
9 | const router = express.Router();
10 |
11 | const app = express();
12 |
13 | app.use(bodyParser.json());
14 | app.use(bodyParser.urlencoded({ extended: true }));
15 |
16 | app.post('/api/configDash', dataController.updateConfig, (req, res) => {
17 | res.status(200).json();
18 | });
19 |
20 | app.use(express.static(path.join(__dirname, '../display')));
21 | app.use(express.static(path.join(__dirname, '../watchmoData')));
22 | app.use(express.static(path.join(__dirname, '../../src/assets')));
23 |
24 | app.get('/build/bundle.js', (req, res) => {
25 | res.sendFile(path.join(__dirname, '../../build/bundle.js'));
26 | });
27 |
28 | // if you are in the page and you refresh, this will boot you back to the first page.
29 | app.get('/*', (req, res) => {
30 | res.sendFile(path.join(__dirname, '../display/index.html'));
31 | });
32 |
33 | // this need to be modified to work with the config updater
34 | app.post('/configDash', dataController.updateConfig, (req, res) => {
35 | res.status(200).json();
36 | });
37 |
38 | app.use('*', (req, res) => {
39 | res.sendStatus(404);
40 | });
41 |
42 | // global error handler
43 | // eslint-disable-next-line consistent-return
44 | // eslint-disable-next-line no-unused-vars
45 | function errorHandler(err, req, res, next) {
46 | console.log(err)
47 | res.sendStatus(500);
48 | }
49 |
50 | app.listen(PORT);
51 | console.log(chalk.cyan.bold(`app listening on ${PORT}`));
52 |
53 | module.exports = {
54 | app
55 | };
56 |
--------------------------------------------------------------------------------
/src/watchmoData/default/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "endpoint": "",
3 | "categories": {
4 | "default": {
5 | "queries": [],
6 | "frequency": -1
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/watchmoData/demo/config.json:
--------------------------------------------------------------------------------
1 | {"endpoint":"https://countries.trevorblades.com/","categories":{"countries":{"queries":["{ countries { name native code currency }}","{ countries { name }}","{ countries { name languages {name}}}","{ countries { name currency languages {name}}}"],"frequency":"3000"},"continents":{"queries":["{continents {code name}}","{continent (code:\"AF\") {code name}}","{continent (code:\"AF\") {code name countries {name}} }","{continent (code:\"AF\") {code name countries {name languages {name}}} }"],"frequency":"4000"},"languages":{"queries":["{languages {name native code}}","{language (code: \"am\") {name native code}}"],"frequency":"5000"}}}
2 |
--------------------------------------------------------------------------------
/src/watchmoData/demo/parsedData.json:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/watchmo/b25ca8a58dbc992cdd11d77c9000b7cde90a5e2c/src/watchmoData/demo/parsedData.json
--------------------------------------------------------------------------------
/src/watchmoData/demo/snapshots.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/watchmo/b25ca8a58dbc992cdd11d77c9000b7cde90a5e2c/src/watchmoData/demo/snapshots.txt
--------------------------------------------------------------------------------
/src/watchmoData/projectNames.json:
--------------------------------------------------------------------------------
1 | ["default","demo"]
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "module": "commonjs",
5 | "allowJs": true,
6 | "jsx": "react",
7 | "rootDir": "./src",
8 | "strict": true,
9 | "noImplicitAny": false,
10 | "strictNullChecks": false,
11 | "strictFunctionTypes": true,
12 | "strictBindCallApply": true,
13 | "strictPropertyInitialization": false,
14 | "noImplicitThis": false,
15 | "alwaysStrict": true,
16 | "noUnusedLocals": true,
17 | "noUnusedParameters": true,
18 | "noImplicitReturns": false,
19 | "noFallthroughCasesInSwitch": true,
20 | "allowSyntheticDefaultImports": true,
21 | "esModuleInterop": true,
22 | "forceConsistentCasingInFileNames": false
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 |
4 | module.exports = {
5 | entry: path.join(__dirname, './src/display/index.jsx'), // where to build dependency graph
6 | output: {
7 | path: path.resolve(__dirname, 'build'),
8 | filename: 'bundle.js'
9 | },
10 | devServer: {
11 | publicPath: '/build/',
12 | proxy: { '/': 'http://localhost:3333' }
13 | },
14 | mode: process.env.NODE_ENV,
15 | module: {
16 | rules: [
17 | {
18 | test: /\.jsx?/, // if file ends with this text run this
19 | exclude: /(node_modules|bower_components)/,
20 | use: {
21 | loader: 'babel-loader',
22 | options: {
23 | presets: ['@babel/preset-env', '@babel/preset-react']
24 | }
25 | }
26 | },
27 | {
28 | test: /\.tsx?$/,
29 | exclude: /node_modules/,
30 | use: {
31 | loader: 'ts-loader'
32 | }
33 | },
34 | {
35 | test: /\.s[ac]ss$/i,
36 | use: [
37 | // Creates `style` nodes from JS strings
38 | 'style-loader',
39 | // Translates CSS into CommonJS
40 | 'css-loader',
41 | // Compiles Sass to CSS
42 | 'sass-loader'
43 | ]
44 | },
45 | {
46 | test: /\.css$/i,
47 | use: [
48 | // Creates `style` nodes from JS strings
49 | 'style-loader',
50 | // Translates CSS into CommonJS
51 | 'css-loader',
52 | // Compiles Sass to CSS
53 | 'sass-loader'
54 | ]
55 | }
56 | ]
57 | },
58 | resolve: {
59 | extensions: ['.ts', '.tsx', '.js', '.jsx']
60 | },
61 | devtool: 'inline-source-map'
62 | };
63 |
--------------------------------------------------------------------------------