├── .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 | 44 | 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 | 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 | 176 |
177 |
178 |

Config Dashboard

179 |
180 |
181 |
182 | 183 | 186 | 194 |
195 | 198 | 209 |
210 |
211 | {/* */} 212 | 218 | 223 | 224 |
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 | 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 | {' '} 40 | 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 | 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 | {' '} 42 | 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 | 66 | 67 |
68 | 69 |
70 | 71 | 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 | 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 | {/* */} 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 | 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 | 73 | 74 | 75 | 76 | 77 | 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 |
watchMo logo
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 | --------------------------------------------------------------------------------