diff --git a/.d.ts b/.d.ts new file mode 100644 index 000000000..31dca6bb4 --- /dev/null +++ b/.d.ts @@ -0,0 +1 @@ +declare module '*.png' diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 000000000..887aa77db --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,68 @@ +{ + "globals": { + "NodeJS": true, + "JSX": true, + "Electron": true + }, + "plugins": ["react", "prettier"], + "parser": "babel-eslint", + "parserOptions": { + "ecmaVersion": 2018, + "sourceType": "module", + "ecmaFeatures": { + "jsx": true + } + }, + "env": { + "es6": true, + "browser": true, + "node": true, + "jest": true + }, + "extends": ["airbnb", "eslint:recommended", "plugin:react/recommended", "prettier"], + "rules": { + "react/display-name": "off", + "prettier/prettier": "error", + "no-unused-vars": [ + "warn", + { + "args": "none" + } + ], + "react/prop-types": "off", + "no-console": "warn", + "react/jsx-filename-extension": "off", + "no-alert": "off", + "no-plusplus": "off", + "no-new": "off", + "react/button-has-type": "off", + "react/jsx-one-expression-per-line": "off", + "no-restricted-globals": "off", + "react/no-array-index-key": "warn", + "no-restricted-syntax": "off", + "guard-for-in": "off", + "no-param-reassign": "off", + "import/extensions": [ + "error", + "ignorePackages", + { + "js": "never", + "jsx": "never", + "ts": "never", + "tsx": "never", + "mjs": "never" + } + ] + }, + "settings": { + "import/extensions": [".js", ".mjs", ".jsx", ".ts", ".tsx"], + // "import/parsers": { + // "@typescript-eslint/parser": [".ts", ".tsx"] + // }, + "import/resolver": { + "node": { + "extensions": [".js", ".jsx", ".ts", ".tsx"] + } + } + } +} \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..dd84ea782 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..bbcbbe7d6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/integrate.yml b/.github/workflows/integrate.yml new file mode 100644 index 000000000..6137e1e7c --- /dev/null +++ b/.github/workflows/integrate.yml @@ -0,0 +1,31 @@ +name: Frontend and Backend testing + +on: + pull_request: + branches: [master] +##Branch name may need to be changed depending on name of your dev branch +jobs: + frontend_testing: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: 18 + - run: npm install --package-lock-only + - run: npm ci + - run: npm run build + - run: npm run test + + backend_testing: + runs-on: ubuntu-latest + timeout-minutes: 7 + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: 18 + - run: npm install --package-lock-only + - run: npm ci + - run: npm run build + - run: npx jest --config __backend-tests__/jest.config.js --verbose diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 000000000..f034fc92d --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,19 @@ +name: Publish Package to npmjs +on: + release: + types: [published] +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + # Setup .npmrc file to publish to npm + - uses: actions/setup-node@v3 + with: + node-version: '18.17.1' + registry-url: 'https://registry.npmjs.org' + - run: npm ci + - run: npm publish ./chronos_npm_package --access=public + env: + # need to add NPM token to github secret if not available + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index 3c3629e64..8b53748c9 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,53 @@ +# JS Project-Specific # +####################### +# node_modules +/dist +/build +release-builds +coverage +__tests__/**/__snapshots__ +.env +databases.txt +settings.json + +# Compiled source # +################### +*.com +*.class +*.dll +*.exe +*.o +*.so + +# Packages # +############ +# it's better to unpack these files and commit the raw source +# git has its own built in compression methods +*.7z +*.dmg +*.gz +*.iso +*.jar +*.rar +*.tar +*.zip + +# Logs and databases # +###################### +*.log +*.sql +*.sqlite +.nyc_output + +# OS generated files # +###################### +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db node_modules + +out/ \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 000000000..577f14290 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "singleQuote": true, + "useTabs": false, + "endOfLine": "auto", + "arrowParens": "avoid", + "printWidth": 100 +} diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..1953022ec --- /dev/null +++ b/.travis.yml @@ -0,0 +1,18 @@ +language: node_js +node_js: + - 'stable' +os: osx +jobs: + # allow_failures: + # - os: osx + fast_finish: true +install: + - npm install +script: + npm run test:app + # safelist + # branches: + # only: + # - master + # - middleware + # - chronosWebsite diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..965c9e301 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,45 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "File", + "skipFiles": [ + "/**", + ], + "program": "${file}" + }, + { + "type": "node", + "request": "launch", + "name": "Electron: Main", + "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron", + "runtimeArgs": [ + "--remote-debugging-port=9223", + "." + ], + "windows": { + "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd" + } + }, + { + "name": "Electron: Renderer", + "type": "chrome", + "request": "attach", + "port": 9223, + "webRoot": "${workspaceFolder}", + "timeout": 30000, + "url": "http://localhost:8080/", + } + ], + "compounds": [ + { + "name": "Electron: All", + "configurations": [ + "Electron: Main", + "Electron: Renderer" + ] + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..7a73a41bf --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..3402b90b3 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,37 @@ +# Contributing + +Chronos encourages contributions to this product. + +## Pull Requests + +Chronos welcomes all pull requests. + +1. Fork the repo and create a working branch from `master`. +2. If you've added any code that requires testing, add tests. +3. If you've changed APIs, update the `README.md`. +4. Check to ensure that all tests pass. +5. Make sure code is formatted with `prettier` and follows the [Airbnb React/JSX Style Guide](https://github.com/airbnb/javascript/blob/master/react/README.md). +6. Create a pull request to `master`. + +## Getting started +- `npm run dev:app` and `npm run dev:electron`: Run Node and Electron at the same time to start Chronos app + - To make changes to codebase on the Main Process: + - Files in the main process must be compiled prior to starting the app + - In the terminal in Chronos directory, input `tsc` to compile typescript files + - Once compiled, `npm run dev:app` and `npm run dev:electron` + * Note: If typescript is not installed, `npm install -g typescript` + +## Chronos Website + +The `chronosWebsite` branch holds the code for the website. Edit the website by checking out the branch, modifying the website, and then updating the AWS S3 bucket with the changes. +## Issues + +Please do not hesitate to file issues that detail bugs or offer ways to enhace Chronos. + +Chronos is based off of community feedback and is always looking for ways to get better. The team behind Chronos is interested to hear about your experience and how we can improve it. + +When submitting issues, ensure your description is clear and has instructions to be able to reproduce the issue. + +## Get In Touch + +We use GitHub as a platform of communication between users and developers to promote transparency, community support and feedback. diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 000000000..e96b60909 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Chronos + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/LISENCE.md b/LISENCE.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/README.md b/README.md index d6c988392..537f1cdcc 100644 --- a/README.md +++ b/README.md @@ -1 +1,404 @@ + + Chronos + +
+ +![Build Passing](https://img.shields.io/badge/build-awesome-brightgreen) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/open-source-labs/Chronos) +![License: MIT](https://img.shields.io/badge/License-MIT-brightgreen.svg) +![Release: 13.0](https://img.shields.io/badge/Release-12.0-brightgreen) + +
+ # Chronos + +### ⭐️ Star us on GitHub! ⭐️ + +**Visit our website at [chronoslany.com](https://chronoslany.com/).** + +Chronos is a comprehensive developer tool that monitors the health and web traffic for containerized (Docker & Kubernetes) and non-containerized microservices communicated via REST APIs or gRPC, whether hosted locally or on Amazon Web Services (AWS). Use Chronos to see real-time data monitoring and receive automated notifications over Slack or email. + +## What's New? + +### Chronos 13.0 + +
+ +Contributors: +[Elsa Holmgren](https://github.com/ekh88), +[Mckenzie Morris](https://github.com/mckenzie-morris), +[Kelly Chandler](https://github.com/kchandler44), +[Sean Simpson](https://github.com/seantokuzo), +[Zhongyan Liang](https://github.com/ZhongyanLiang) + +
+Updates: +
  • Created new and improved microservices application with updated syntax to better demonstrate Chronos' capabilities
  • +
  • Dockerized microservices application to demonstrate Chronos within a containerized environment
  • +
  • Added visulization of Chronos' codebase to illustrate overall structure
  • +
  • Simplified installation and startup instructions in the root directory README file
  • +
  • Improved documentation in chronos_npm_package README file for easier on-boarding
  • + +Version 13.0 Medium Article + +### **Iteration Log** + +
    Chronos 12.0 + +
    + +
    Chronos 11.0 + +
    + +
    Chronos 10.0 + +
    + +
    Chronos 9.0 + +
    + +
    Chronos 8.0 + +
    + +
    Chronos 7.0 + +
    + +### With Chronos 13.0 + + + Chronos + + +
    + +## Overview of the CodeBase + +- Instead of the typical folders & files view, a visual representation of the code is created. Below, it's showing the same repository, but instead of a directory structure, each file and folder as a circle: the circle’s color is the type of file, and the circle’s size represents the size of the file. See live demo here. + codebase visulization + +## Features + +- Cloud-Based Instances: + - Option to choose between cloud hosted services and local services, giving Chronos the ability to monitor instances and clusters on AWS EC2, ECS, and EKS platforms AWS +- Local instances utilitizing `@chronosmicro/tracker` NPM package: + - Enables distributed tracing enabled across microservices applications + - Displays real-time temperature, speed, latency, and memory statistics for local services + - Displays and compares multiple microservice metrics in a single graph + - Allow Kubernetes and Docker monitoring via Prometheus server and Grafana visualization. + - Compatible with GraphQL + - Monitor an Apache Kafka cluster via the JMX Prometheus Exporter + - Supports PostgreSQL and MongoDB databases + +# Installation + +This is for the latest Chronos **version 13.0 release**. + +## NPM Package + +In order to use Chronos within your own application, you must have the `@chronosmicro/tracker` dependency installed. + +The `@chronosmicro/tracker` package tracks your application's calls and scrapes metrics from your system. + +- **NOTE:** The Chronos tracker code is included in the _chronos_npm_package_ folder for ease of development, but the published NPM package can be downloaded by running `npm install @chronosmicro/tracker`. + +For more details on the NPM package and instructions for how to use it, please view the [Chronos NPM Package README](./chronos_npm_package/README.md). + +# + + + + + + + + +## Chronos Desktop Application + + + + +### Running the Chronos desktop app in development mode (WSL Incompatible) + +1. From the root directory, run `npm install` +2. Run `npm run build` +3. For Windows users, run `npm audit fix` or `npm audit fix --force` if prompted +4. Open a new terminal and run `npm run dev:app` to start the Webpack development server +5. Open a new terminal and run `npm run dev:electron` to start the Electron UI in development mode. +6. Refer to `Examples` sections below to spin up example applications. + +# + +### Packing the Chronos desktop app into an executable + +1. From the root directory, run `npm run build` +2. Run `npm run package` +3. Find the `chronos.app` executable inside the newly created `release-builds` folder in the root directory. + +# + +### Creating User Database + +**NOTE: You must create your own user database for extended features** + +1. Create a MongoDB database in which to store user information and insert it on line 2 within the [UserModel.ts](./electron/models/UserModel.ts) (_electron/models/UserModel.ts_) file. + - This database will privately store user information. +2. Once this is set up, you can create new users, log in, and have your data persist between sessions. + +# + +# Examples + +We provide eight example applications for you to test out both the Chronos NPM package and the Chronos desktop application: + +- AWS + - [EC2 README](./examples/AWS/AWS-EC2/README.md) + - [ECS README](./examples/AWS/AWS-ECS/README.md) + - [EKS README](./examples/AWS/AWS-EKS/README.md) +- Docker + - [Docker README](./examples/docker/README.md) +- gRPC + - [gRPC README](./examples/gRPC/README.md) +- Kubernetes + - [Kubernetes README](./examples/kubernetes/README.md) +- Microservices + - [Microservices README](./examples/microservices/README.md) + +Additional documentation on how Chronos is used **in each example** can be found in the [Chronos NPM Package README](./chronos_npm_package/README.md). + +#### _AWS_ + +The `AWS` folder includes 3 example applications with instructions on how to deploy them in AWS platforms. Note that using AWS services may cause charges. + +- The ECS folder includes an web application ready to be containerized using Docker. The instruction shows how to deploy application to ECS using Docker CLI command, and it will be managed by Fargate services. +- The EC2 folder includes a React/Redux/SQL web application ready to be containerized using Docker. The instruction shows how to deploy application using AWS Beanstalk and connect application to RDS database. Beanstalk service will generate EC2 instance. +- The EKS folder includes a containerized note taking app that uses a Mongo database as its persistent volume. The instructions show how to deploy this application on EKS, how to monitor with Prometheus & Opencost, and how to use Grafana to grab visualizations. + +Refer to the [EC2 README](./examples/AWS/AWS-EC2/README.md), [ECS README](./examples/AWS/AWS-ECS/README.md), and [EKS README](./examples/AWS/AWS-EKS/README.md) example in the _AWS_ folder for more details. + +# + +#### _Docker_ + +In the Docker folder within the `master` branch, we provide a sample _dockerized_ microservices application to test out Chronos and to apply distributed tracing across different containers for your testing convenience. + +The `docker` folder includes individual Docker files in their respective directories. A docker-compose.yml is in the root directory in case you'd like to deploy all services together. + +Refer to the [Docker README](./examples/docker/README.md) in the `docker` folder for more details. + +# + +#### _gRPC_ + +The `gRPC` folder includes an HTML frontend and an Express server backend, as well as proto files necessary to build package definitions and make gRPC calls. The _reverse_proxy_ folder contains the server that requires in the clients, which contain methods and services defined by proto files. + +Refer to the [gRPC README](./examples/gRPC/README.md) in the `gRPC` folder for more details. + +# + +#### _Kubernetes_ + +The `kubernetes` folder includes a React frontend and an Express server backend, and the Dockerfiles needed to containerize them for Kubernetes deployment. The _launch_ folder includes the YAML files needed to configure the deployments, services, and configurations of the frontend, backend, Prometheus server, and Grafana. + +Refer to the [Kubernetes README](./examples/kubernetes/README.md) in the `kubernetes` folder for more details. + +# + +#### _Microservices_ + +In the `microservices` folder, we provide a sample microservice application that successfully utilizes Chronos to apply all the powerful, built-in features of our monitoring tool. You can then visualize the data with the Electron app. + +Refer to the [microservices README](./examples/microservices/README.md) in the `microservices` folder for more details. + +# + +# Testing + +We've created testing suites for Chronos with React Testing, Jest, and Selenium for unit, integration, and end-to-end tests - instructions on running them can be found in the [testing README](./__tests__/README.md). + +# + +## Contributing + +Development of Chronos is open source on GitHub through the tech accelerator OS Labs, and we are grateful to the community for contributing bug fixes and improvements. + +Read our [contributing README](CONTRIBUTING.md) to learn how you can take part in improving Chronos. + +#### Past [Contributors](contributors.md) + +# + +## Technologies + +![Electron.js](https://img.shields.io/badge/Electron-191970?style=for-the-badge&logo=Electron&logoColor=white) +![React](https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB) +![JavaScript](https://img.shields.io/badge/javascript-%23323330.svg?style=for-the-badge&logo=javascript&logoColor=%23F7DF1E) +![TypeScript](https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white) +![Postgres](https://img.shields.io/badge/postgres-%23316192.svg?style=for-the-badge&logo=postgresql&logoColor=white) +![MongoDB](https://img.shields.io/badge/MongoDB-%234ea94b.svg?style=for-the-badge&logo=mongodb&logoColor=white) +![NodeJS](https://img.shields.io/badge/node.js-6DA55F?style=for-the-badge&logo=node.js&logoColor=white) +![Express.js](https://img.shields.io/badge/express.js-%23404d59.svg?style=for-the-badge&logo=express&logoColor=%2361DAFB) +![HTTP](https://img.shields.io/badge/HTTP-394EFF?style=for-the-badge) +![gRPC](https://img.shields.io/badge/gRPC-394EFF?style=for-the-badge) +![GraphQL](https://img.shields.io/badge/-GraphQL-E10098?style=for-the-badge&logo=graphql&logoColor=white) +![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white) +![AWS](https://img.shields.io/badge/AWS-%23FF9900.svg?style=for-the-badge&logo=amazon-aws&logoColor=white) +![Jest](https://img.shields.io/badge/-jest-%23C21325?style=for-the-badge&logo=jest&logoColor=white) +![Webpack](https://img.shields.io/badge/webpack-%238DD6F9.svg?style=for-the-badge&logo=webpack&logoColor=black) +![MUI](https://img.shields.io/badge/MUI-%230081CB.svg?style=for-the-badge&logo=mui&logoColor=white) +![vis.js](https://img.shields.io/badge/vis.js-3578E5?style=for-the-badge) +![Plotly](https://img.shields.io/badge/Plotly-%233F4F75.svg?style=for-the-badge&logo=plotly&logoColor=white) +![Apache Kafka](https://img.shields.io/badge/Apache%20Kafka-000?style=for-the-badge&logo=apachekafka) +![Grafana](https://img.shields.io/badge/grafana-%23F46800.svg?style=for-the-badge&logo=grafana&logoColor=white) +![Selenium](https://img.shields.io/badge/-selenium-%43B02A?style=for-the-badge&logo=selenium&logoColor=white) +![Prometheus](https://img.shields.io/badge/Prometheus-E6522C?style=for-the-badge&logo=Prometheus&logoColor=white) +![GitHub Actions](https://img.shields.io/badge/github%20actions-%232671E5.svg?style=for-the-badge&logo=githubactions&logoColor=white) +![Threejs](https://img.shields.io/badge/threejs-black?style=for-the-badge&logo=three.js&logoColor=white) +![Kubernetes](https://img.shields.io/badge/kubernetes-%23326ce5.svg?style=for-the-badge&logo=kubernetes&logoColor=white) +![TailwindCSS](https://img.shields.io/badge/tailwindcss-%2338B2AC.svg?style=for-the-badge&logo=tailwind-css&logoColor=white) + +## License + +[MIT](https://github.com/oslabs-beta/Chronos/blob/master/LICENSE.md) + +# + +###### Return to [Top](#chronos) diff --git a/__backend-tests__/chronosMethods.test.js b/__backend-tests__/chronosMethods.test.js new file mode 100644 index 000000000..4b91e5dff --- /dev/null +++ b/__backend-tests__/chronosMethods.test.js @@ -0,0 +1,249 @@ +const { EcoTwoTone } = require('@mui/icons-material'); +const Chronos = require('../chronos_npm_package/chronos.js'); +const helpers = require('../chronos_npm_package/controllers/utilities.js'); +const hpropagate = require('hpropagate'); +const mongo = require('../chronos_npm_package/controllers/mongo.js'); +const postgres = require('../chronos_npm_package/controllers/postgres.js'); + +// Mock the utilities module functions +jest.mock('../chronos_npm_package/controllers/utilities.js', () => ({ + validateInput: jest.fn(config => config), + addNotifications: jest.fn(config => config), + testMetricsQuery: jest.fn(config => config), + getMetricsURI: jest.fn(config => config), +})); + +// mock propogate from Chronos +jest.mock('hpropagate'); + +//mock fns found in track +jest.mock('../chronos_npm_package/controllers/mongo.js', () => ({ + connect: jest.fn(config => config), + services: jest.fn(config => config), + docker: jest.fn(config => config), + health: jest.fn(config => config), + communications: jest.fn(config => config), + serverQuery: jest.fn(config => config), + storeGrafanaAPIKey: jest.fn(config => config), +})); + +jest.mock('../chronos_npm_package/controllers/postgres.js', () => ({ + connect: jest.fn(config => config), + services: jest.fn(config => config), + docker: jest.fn(config => config), + health: jest.fn(config => config), + communications: jest.fn(config => config), + serverQuery: jest.fn(config => config), +})); + +describe('Chronos Config', () => { + afterEach(() => { + // Clear mock function calls after each test + jest.clearAllMocks(); + }); + + test('should throw an error if config is undefined', () => { + expect(() => new Chronos()).toThrow('Chronos config is undefined'); + }); + + test('should call utilities functions with the correct parm', () => { + const config = { + microservice: 'test', + interval: 300, + mode: 'micro', + dockerized: false, + database: { + connection: 'REST', + type: process.env.CHRONOS_DB, + URI: process.env.CHRONOS_URI, + }, + notifications: [], + }; + + const chronos = new Chronos(config); + + // Ensure the config property is correctly set in the instance + expect(chronos.config).toEqual(config); + // Ensure the constructor called the validateInput and addNotifications functions + expect(helpers.validateInput).toHaveBeenCalledWith(config); + expect(helpers.addNotifications).toHaveBeenCalledWith(config); + }); + + describe('propagate', () => { + test('should check if propagate func properply calls hpropagate', () => { + const config = { + microservice: 'test', + interval: 300, + mode: 'micro', + dockerized: false, + database: { + connection: 'REST', + type: process.env.CHRONOS_DB, + URI: process.env.CHRONOS_URI, + }, + notifications: [], + }; + + const chronos = new Chronos(config); + chronos.propagate(); + expect(hpropagate).toHaveBeenCalledWith({ propagateInResponses: true }); + }); + }); + + describe('track', () => { + test('should check if track function for MongoDB works', () => { + //check if we can destructure database and dockerized from config + const config = { + microservice: 'test', + interval: 300, + mode: 'micro', + dockerized: true, + database: { + connection: 'REST', + type: 'MongoDB', + URI: process.env.CHRONOS_URI, + }, + notifications: [], + }; + const { database, dockerized } = config; + const falseDock = { dockerized: false }; + const chronos = new Chronos(config); + chronos.track(); + if (database.type === 'MongoDB') { + expect(mongo.connect).toHaveBeenCalledWith(config); + expect(mongo.services).toHaveBeenCalledWith(config); + if (dockerized) expect(mongo.docker).toHaveBeenCalledWith(config); + if (!falseDock) expect(mongo.health).toHaveBeenCalledWith(config); + if (database.connection === 'REST') expect(mongo.communications).not.toBeUndefined(); + } + }); + test('should check if track function for Postgres works', () => { + //check if we can destructure database and dockerized from config + const config = { + microservice: 'test', + interval: 300, + mode: 'micro', + dockerized: true, + database: { + connection: 'REST', + type: 'PostgreSQL', + URI: process.env.CHRONOS_URI, + }, + notifications: [], + }; + const { database, dockerized } = config; + const falseDock = { dockerized: false }; + const chronos = new Chronos(config); + chronos.track(); + if (database.type === 'PostgreSQL') { + expect(postgres.connect).toHaveBeenCalledWith(config); + expect(postgres.services).toHaveBeenCalledWith(config); + if (dockerized) expect(postgres.docker).toHaveBeenCalledWith(config); + if (!falseDock) expect(postgres.health).toHaveBeenCalledWith(config); + if (database.connection === 'REST') expect(postgres.communications).not.toBeUndefined(); + } + }); + }); + describe('kafka', () => { + test('should check if kafka with MongoDB functional', async () => { + const config = { + microservice: 'test', + interval: 300, + mode: 'micro', + dockerized: true, + database: { + connection: 'REST', + type: 'MongoDB', + URI: process.env.CHRONOS_URI, + }, + notifications: [], + }; + const { database, dockerized } = config; + const falseDock = { dockerized: false }; + const chronos = new Chronos(config); + chronos.kafka(); + await helpers.testMetricsQuery(config); + if (database.type === 'MongoDB') { + expect(mongo.connect).toHaveBeenCalledWith(config); + expect(mongo.serverQuery).toHaveBeenCalledWith(config); + } + }); + test('should check if kafka with PostgreSQL is functional', async () => { + const config = { + microservice: 'test', + interval: 300, + mode: 'micro', + dockerized: true, + database: { + connection: 'REST', + type: 'PostgreSQL', + URI: process.env.CHRONOS_URI, + }, + notifications: [], + }; + const { database, dockerized } = config; + const falseDock = { dockerized: false }; + const chronos = new Chronos(config); + chronos.kafka(); + await helpers.testMetricsQuery(config); + if (database.type === 'PostgreSQL') { + expect(postgres.connect).toHaveBeenCalledWith(config); + expect(postgres.serverQuery).toHaveBeenCalledWith(config); + } + }); + }); + + describe('kubernetes', () => { + test('should check if kubernetes with mongodb is functional', async () => { + const config = { + microservice: 'test', + interval: 300, + mode: 'kubernetes', + dockerized: true, + database: { + connection: 'REST', + type: 'MongoDB', + URI: process.env.CHRONOS_URI, + }, + notifications: [], + }; + const { database, dockerized } = config; + const falseDock = { dockerized: false }; + const chronos = new Chronos(config); + chronos.kubernetes(); + await helpers.testMetricsQuery(config); + if (database.type === 'MongoDB') { + await expect(mongo.connect).toHaveBeenCalledWith(config); + await expect(mongo.storeGrafanaAPIKey).toHaveBeenCalledWith(config); + expect(mongo.serverQuery).toHaveBeenCalledWith(config); + } + if (database.type === 'PostgreSQL') { + expect(postgres.connect).toHaveBeenCalledWith(config2); + expect(postgres.serverQuery).toHaveBeenCalledWith(config2); + } + }); + test('should check if kubernetes with PostGres is functional', async () => { + const config = { + microservice: 'test', + interval: 300, + mode: 'kubernetes', + dockerized: true, + database: { + connection: 'REST', + type: 'PostgreSQL', + URI: process.env.CHRONOS_URI, + }, + notifications: [], + }; + const { database, dockerized } = config; + const falseDock = { dockerized: false }; + const chronos = new Chronos(config); + chronos.kubernetes(); + await helpers.testMetricsQuery(config); + if (database.type === 'PostgreSQL') { + expect(postgres.connect).toHaveBeenCalledWith(config); + expect(postgres.serverQuery).toHaveBeenCalledWith(config); + } + }); + }); +}); diff --git a/__backend-tests__/jest.config.js b/__backend-tests__/jest.config.js new file mode 100644 index 000000000..8d298f01e --- /dev/null +++ b/__backend-tests__/jest.config.js @@ -0,0 +1,12 @@ +module.exports = { + + roots: [''], // Set the root directory for test files (adjust this path to your test folder) + + testRegex: '(/tests/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$', + + // Code coverage settings + collectCoverage: true, + coverageDirectory: 'coverage', + // Specify the test path files/patterns to ignore + testPathIgnorePatterns: ['/node_modules/', '/__tests__/', '/__backend-tests__/mongo.test.js'], +}; diff --git a/__backend-tests__/mockdbsetup.js b/__backend-tests__/mockdbsetup.js new file mode 100644 index 000000000..07d3221c9 --- /dev/null +++ b/__backend-tests__/mockdbsetup.js @@ -0,0 +1,40 @@ +const mongoose = require("mongoose"); +const { MongoMemoryServer } = require("mongodb-memory-server"); + +let mongo = null; +let uri; + +const connectDB = async () => { + mongo = await MongoMemoryServer.create(); + uri = await mongo.getUri(); + + await mongoose.connect(uri, { + useNewUrlParser: true, + useUnifiedTopology: true, + }).then((result) => { + // console.log(result.connection.readyState) + // console.log(result.connection.host) + }).catch((err) => { + // console.log('Unable to connect to MongoMemoryServer') + }); +}; + +const dropDB = async () => { + if (mongo) { + await mongoose.connection.dropDatabase(); + await mongoose.connection.close(); + await mongo.stop(); + } +}; + +const dropCollections = async () => { + if (mongo) { + const collections = await mongoose.connection.db.collections(); + for (let collection of collections) { + await collection.deleteMany(); + } + } + }; + + +module.exports = { connectDB, dropDB, dropCollections, uri } \ No newline at end of file diff --git a/__backend-tests__/mongo.test.js b/__backend-tests__/mongo.test.js new file mode 100644 index 000000000..8faab00d2 --- /dev/null +++ b/__backend-tests__/mongo.test.js @@ -0,0 +1,228 @@ +const mongoose = require('mongoose'); +const mongo = require('../chronos_npm_package/controllers/mongo'); +const ServicesModel = require('../chronos_npm_package/models/ServicesModel'); +const CommunicationModel = require('../chronos_npm_package/models/CommunicationModel'); +const { connectDB, dropDB, dropCollections, uri } = require('./mockdbsetup'); +const alert = require('../chronos_npm_package/controllers/alert'); +const dockerHelper = require('../chronos_npm_package/controllers/dockerHelper'); + +jest.spyOn(console, 'log').mockImplementation(() => {}); +jest.mock('../chronos_npm_package/controllers/alert'); +jest.useFakeTimers(); +jest.spyOn(global, 'setInterval'); + +jest.mock('../chronos_npm_package/controllers/healthHelpers', () => { + return [ + { + time: Date.now(), + metric: 'testMetric', + value: 10, + category: 'testCategory', + }, + { + time: Date.now(), + metric: 'testMetric2', + value: 12, + category: 'testCategory2', + }, + ]; +}); + +jest.mock('../chronos_npm_package/controllers/mongo', () => ({ + ...jest.requireActual('../chronos_npm_package/controllers/mongo'), + addMetrics: jest.fn(), + getSavedMetricsLength: jest.fn(), +})); + +const HealthModel = { + insertMany: jest.fn(() => Promise.resolve()), +}; +const HealthModelFunc = jest.fn(() => HealthModel); + +jest.mock('../chronos_npm_package/controllers/dockerHelper', () => ({ + ...jest.requireActual('../chronos_npm_package/controllers/dockerHelper'), + getDockerContainer: jest.fn(), + readDockerContainer: jest.fn(), +})); + +describe('mongo.connect', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('should connect to MongoDB database', async () => { + await mongo.connect({ database: { URI: uri } }); + + expect(console.log).toHaveBeenCalledWith( + expect.stringContaining('MongoDB database connected at') + ); + }); + + test('should handle connection errors', async () => { + const testDb = 'test'; + await mongo.connect({ database: { URI: testDb } }); + + expect(console.log).toHaveBeenCalledWith( + expect.stringContaining('Error connecting to MongoDB:'), + expect.any(String) + ); + }); +}); + +describe('mongo.services', () => { + beforeEach(async () => { + jest.clearAllMocks(); + await connectDB(); + }); + + afterEach(async () => { + await dropCollections(); + await dropDB(); + }); + + test('should create a new document', async () => { + await mongo.services({ microservice: 'test2', interval: 'test2' }); + const savedService = await ServicesModel.findOne({ microservice: 'test2', interval: 'test2' }); + expect(savedService).toBeDefined(); // The document should be defined if it exists + }); +}); + +describe('mongo.communications', () => { + beforeEach(async () => { + jest.clearAllMocks(); + await connectDB(); + }); + + afterEach(async () => { + await dropCollections(); + await dropDB(); + }); + + test('should record request cycle and save communication to the database', async () => { + const req = {}; + const res = { + statusCode: 200, + statusMessage: 'OK', + getHeaders: () => ({ 'x-correlation-id': 'correlation-id-123' }), + on: jest.fn((event, callback) => { + if (event === 'finish') { + callback(); + } + }), + }; + const middleware = mongo.communications({ microservice: 'test3', slack: null, email: null }); + await middleware(req, res, () => {}); + const savedCommunication = await CommunicationModel.findOne({ microservice: 'test3' }); + expect(savedCommunication).toBeDefined(); + }); + + test('should send an alert', async () => { + const req = {}; + const res = { + statusCode: 400, + statusMessage: 'Not Found', + getHeaders: () => ({ 'x-correlation-id': 'correlation-id-123' }), + on: jest.fn((event, callback) => { + if (event === 'finish') { + callback(); + } + }), + }; + const middleware = mongo.communications({ + microservice: 'test4', + slack: 'slackTest', + email: 'emailTest', + }); + await middleware(req, res, () => {}); + expect(alert.sendSlack).toHaveBeenCalledTimes(1); + expect(alert.sendEmail).toHaveBeenCalledTimes(1); + expect(alert.sendSlack).toHaveBeenCalledWith(res.statusCode, res.statusMessage, 'slackTest'); + expect(alert.sendEmail).toHaveBeenCalledWith(res.statusCode, res.statusMessage, 'emailTest'); + }); +}); + +describe('mongo.health', () => { + beforeEach(async () => { + jest.clearAllMocks(); + jest.useFakeTimers(); + await connectDB(); + }); + + afterEach(async () => { + jest.clearAllTimers(); + await dropCollections(); + await dropDB(); + }); + + test('should collect data after the set interval', async () => { + await mongo.health({ microservice: 'mongo.health test', interval: 1000, mode: 'testMode' }); + jest.advanceTimersByTime(1000); + expect(setInterval).toHaveBeenCalledWith(expect.any(Function), 1000); + expect(collectHealthData).toHaveBeenCalled(); + }); + + test('should save metrics to database', async () => { + const mockData = [ + { + time: Date.now(), + metric: 'testMetric', + value: 10, + category: 'testCategory', + }, + { + time: Date.now(), + metric: 'testMetric2', + value: 12, + category: 'testCategory2', + }, + ]; + await mongo.health({ microservice: 'mongo.health test', interval: 1000, mode: 'testMode' }); + jest.advanceTimersByTime(1000); + expect(collectHealthData).toHaveReturnedWith(mockData); + expect(HealthModelFunc).toHaveBeenCalled(); + expect(HealthModel).toHaveBeenCalled(); + }); +}); + +describe('mongo.docker', () => { + beforeEach(async () => { + jest.clearAllMocks(); + jest.useFakeTimers(); + await connectDB(); + }); + + afterEach(async () => { + await dropCollections(); + await dropDB(); + }); + + test('should collect docker container information', async () => { + const microservice = 'mongo.docker test'; + const mockContainerData = { + containername: microservice, + containerId: '234', + platform: 'testPlatform', + startime: Date.now(), + }; + const mockReadDockerContainerData = { + ...mockContainerData, + memoryusage: 234, + memorylimit: 234, + memorypercent: 32, + cpupercent: 45, + networkreceived: 345, + networksent: 345, + processcount: 343, + restartcount: 12, + time: Date.now(), + }; + + dockerHelper.getDockerContainer.mockResolvedValue(mockContainerData); + dockerHelper.readDockerContainer.mockResolvedValue(mockReadDockerContainerData); + + await mongo.docker({ microservice: microservice, interval: 1000, mode: 'testMode' }); + expect(dockerHelper.getDockerContainer).toHaveBeenCalledWith(microservice); + jest.advanceTimersByTime(1000); + expect(dockerHelper.readDockerContainer).toHaveBeenCalledWith(mockContainerData); + }); +}); diff --git a/__tests__/README.md b/__tests__/README.md new file mode 100644 index 000000000..f8e35f943 --- /dev/null +++ b/__tests__/README.md @@ -0,0 +1,105 @@ +# Testing + +### Preparation +### +### Frontend Testing + +For frontend testing, ensure you've prepared your environment as follows: + +1. React Testing Library versions 13+ require React v18. If your project uses an older version of React, be sure to install version 12 +``` +npm install --save-dev @testing-library/react@12 +``` +2. install additional packages +``` +npm install -g jest +npm i @jest/types +npm i ts-jest +npm i jest-environment-jsdom +npm i --save-dev @types/jest @testing-library/jest-dom + +npm i @types/node +npm install -D ts-node +``` +3. create jest.config.js +```js +module.exports = { + verbose: true, + setupFilesAfterEnv: ['./jest_setup/windowMock.js'], + testEnvironment: "jsdom", + preset: 'ts-jest/presets/js-with-ts', + moduleNameMapper: { + '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': + '/jest_setup/fileMock.js', + '\\.(css|less|scss)$': '/jest_setup/styleMock.js', + }, + collectCoverage: true, + types: ["jest","node"], +}; +``` +4. make sure jest_setup folder is at root directory of Chronos with styleMock.js and windowMock.js + ```js + styleMock.js + ``` + ```js + module.exports = {}; + ``` + ```js + windowMock.js + ``` + ```js + // Mock window environment + window.require = require; + + // Mock import statements for Plotly + window.URL.createObjectURL = () => {}; + + // Mock get context + HTMLCanvasElement.prototype.getContext = () => {}; + ``` +5. update database info inside `test_settings.json` + +6. use `npm run test` to run jest tests +### +### Backend Testing + +For backend testing, ensure you've prepared your environment as follows: + +1. create `jest.config.js` +```js +module.exports = { + roots: [''], // Set the root directory for test files (adjust this path to your test folder) + testRegex: '(/tests/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$', + collectCoverage: true, + coverageDirectory: 'coverage', + testPathIgnorePatterns: ['/node_modules/', '/__tests__/'], +}; +``` +2. install additional packages +``` +npm i mongodb-memory-server +``` +6. use `npm run backend-test` to run jest tests + +### End-to-End Testing + +Perform end-to-end testing with the following steps: + +1. install the following packages +``` +npm i selenium-webdriver +npm i chromedriver +``` + +2. use `npm run dev:app` to start the app + +3. use `./node_modules/.bin/chromedriver` to run the Chromedriver executable + +4. use `npm run start:e2e` to run the end-to-end tests + +## Contributing + +Chronos hopes to inspire an active community of both users and developers. For questions, comments, or contributions, please submit a pull request. + +Read our [contributing README](../../CONTRIBUTING.md) to further learn how you can take part in improving Chronos. + diff --git a/__tests__/charts/HealthChart.test.tsx b/__tests__/charts/HealthChart.test.tsx new file mode 100644 index 000000000..270aaf25c --- /dev/null +++ b/__tests__/charts/HealthChart.test.tsx @@ -0,0 +1,68 @@ +//BELOW test cases are only for instances of REACT Plotly, which is currently on microservices, gRPC and AWS. Change file from .txt to .tsx to run test + +import React from 'react'; +import HealthChart from '../../app/charts/HealthChart'; +import { render, screen } from '@testing-library/react'; + +// mockData used for testing suite +const mockData = { + ServiceName: { + Metric: { + time: [ + '2023-06-09T15:18:25.195Z', + '2023-06-09T15:18:20.194Z', + '2023-06-09T15:18:15.192Z', + '2023-06-09T15:18:10.191Z', + '2023-06-09T15:18:05.190Z', + ], + value: [1208074240, 1282670592, 1243414528, 1278115840, 117178368], + }, + }, +}; + +jest.mock('electron', () => ({ + ipcRenderer: { + send: () => jest.fn(), + on: () => mockData, + }, +})); + +// test suite for HealthChart.tsx +describe('HealthChart', () => { + const props = { + key: 'testKey', + dataType: 'Memory in Megabytes', + serviceName: 'serviceName', + chartData: mockData, + categoryName: 'Test Name', + sizing: 'all', + colourGenerator: jest.fn(), + }; + + let graph; + beforeEach(() => { + render(); + + graph = screen.getByTestId('Health Chart').firstChild; + }); + + it('Should render', () => { + expect(screen).toBeTruthy(); + }); + + it('Should render graph', () => { + expect(graph).toBeTruthy(); + }); + + it('Should not scroll', () => { + expect(graph.scrollWidth).toBe(0); + expect(graph.scrollHeight).toBe(0); + expect(graph.scrollLeft).toBe(0); + expect(graph.scrollTop).toBe(0); + }); + + it('Should have correct data on y-axis based off mock data', () => { + expect(graph.data[0].y[0]).toBe((mockData.ServiceName.Metric.value[0] / 1000000).toFixed(2)); + expect(graph.data[0].y[1]).toBe((mockData.ServiceName.Metric.value[1] / 1000000).toFixed(2)); + }); +}); diff --git a/__tests__/charts/TrafficChart.test.tsx b/__tests__/charts/TrafficChart.test.tsx new file mode 100644 index 000000000..04ba9bffb --- /dev/null +++ b/__tests__/charts/TrafficChart.test.tsx @@ -0,0 +1,73 @@ +/* eslint-disable no-underscore-dangle */ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import TrafficChart from '../../app/charts/TrafficChart'; +import { CommsContext } from '../../app/context/CommsContext'; +import '@testing-library/jest-dom'; + +const mockCommsData = [ + { + correlatingid: '7bdad8c0', + endpoint: '/customers/createcustomer', + microservice: 'customers', + request: 'GET', + responsemessage: 'OK', + responsestatus: 200, + time: '2020-06-27T05:30:43.567Z', + id: 36, + }, +]; + +jest.mock('electron', () => ({ + ipcRenderer: { + send: () => jest.fn(), + on: () => mockCommsData, + }, +})); +describe('Traffic Chart', () => { + const props = { + commsData: mockCommsData, + setCommsData: jest.fn(), + fetchCommsData: jest.fn(), + }; + let graph; + beforeEach(() => { + render( + + + + ); + graph = screen.getByTestId('Traffic Chart').firstChild; + }); + + it('Should render', () => { + expect(screen).toBeTruthy(); + }); + + it('Should render graph', () => { + expect(graph).toBeTruthy(); + }); + + it('Should be alone', () => { + expect(graph.previousSibling).toBe(null); + expect(graph.nextSibling).toBe(null); + }); + + it('Should not scroll', () => { + expect(graph.scrollWidth).toBe(0); + expect(graph.scrollHeight).toBe(0); + expect(graph.scrollLeft).toBe(0); + expect(graph.scrollTop).toBe(0); + }); + + it('Should have width 300, height 300, and white background', () => { + expect(graph._fullLayout.width).toBe(300); + expect(graph._fullLayout.height).toBe(300); + }); + + it('Should have correct data points based off mock data', () => { + expect(graph.calcdata[0][0].isBlank).toBeFalsy(); + expect(graph.data[0].x).toEqual(['customers']); + expect(graph.data[0].y).toEqual([1, 0, 11]); + }); +}); diff --git a/__tests__/components/About.test.tsx b/__tests__/components/About.test.tsx new file mode 100644 index 000000000..a579e0974 --- /dev/null +++ b/__tests__/components/About.test.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import About from '../../app/components/About'; +import DashboardContextProvider from '../../app/context/DashboardContext'; + +describe('About Page', () => { + let element; + beforeAll(() => { + render( + + + + ); + element = screen.getByTestId('aboutPage'); + }); + + // it('Should have three p tags', () => { + // expect(element.querySelectorAll('p').length).toBe(6); + // }); + + it('Should have three h3 tags', () => { + expect(element.querySelectorAll('h3').length).toBe(3); + }); + + it('Should have one div', () => { + expect(element.querySelectorAll('div').length).toBe(1); + }); +}); diff --git a/__tests__/components/Contact.test.tsx b/__tests__/components/Contact.test.tsx new file mode 100644 index 000000000..d22330b66 --- /dev/null +++ b/__tests__/components/Contact.test.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import Contact from '../../app/components/Contact'; +import DashboardContextProvider from '../../app/context/DashboardContext'; + +describe('Contact Page', () => { + let element; + beforeAll(() => { + render( + + + + ); + element = screen.getByTestId('contactPage'); + }); + + it('Should have two p tags', () => { + expect(element.querySelectorAll('p').length).toBe(2); + }); + + it('Should have a form', () => { + expect(element.querySelectorAll('form').length).toBe(1); + }); + + it('Should have six labels', () => { + expect(element.querySelectorAll('label').length).toBe(6); + }); + + it('Should have one h1 tags', () => { + expect(element.querySelectorAll('h1').length).toBe(1); + }); + + it('Should have three divs', () => { + expect(element.querySelectorAll('div').length).toBe(3); + }); + + it('Should have have two inputs', () => { + expect(element.querySelectorAll('input').length).toBe(6); + }); +}); diff --git a/__tests__/components/CreateAdmin.test.tsx b/__tests__/components/CreateAdmin.test.tsx new file mode 100644 index 000000000..2aad12c6f --- /dev/null +++ b/__tests__/components/CreateAdmin.test.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { render, fireEvent, screen } from '@testing-library/react'; +import { ipcRenderer } from 'electron'; +import CreateAdmin from '../../app/components/CreateAdmin'; +import DashboardContextProvider from '../../app/context/DashboardContext'; + +jest.mock('electron', () => ({ ipcRenderer: { sendSync: jest.fn() } })); + +describe('Create Admin Page', () => { + beforeEach(() => { + console.error = jest.fn(); + render( + + + + ); + }); + + it('Should render', () => { + expect(screen).toBeTruthy(); + }); + + it('Should contain an h1, h2, form, button, and three inputs', () => { + const element = screen.getByTestId('CreateAdmin'); + expect(element.querySelectorAll('h1').length).toBe(1); + expect(element.querySelectorAll('h2').length).toBe(1); + expect(element.querySelectorAll('form').length).toBe(1); + expect(element.querySelectorAll('button').length).toBe(1); + expect(element.querySelectorAll('input').length).toBe(3); + }); + + it('Create Account button should submit email, username, and password to addUser', () => { + const username = screen.getByPlaceholderText('enter username'); + const email = screen.getByPlaceholderText('your@email.here'); + const password = screen.getByPlaceholderText('enter password'); + const createAccountButton = screen.getByRole('button', { name: /create account/i }); + + fireEvent.change(email, { target: { value: 'me@gmail.com' } }); + fireEvent.change(username, { target: { value: 'me' } }); + fireEvent.change(password, { target: { value: 'me123' } }); + + fireEvent.click(createAccountButton); + + expect(ipcRenderer.sendSync).toHaveBeenCalledWith('addUser', { + email: 'me@gmail.com', + username: 'me', + password: 'me123', + }); + }); +}); diff --git a/__tests__/components/Header.test.tsx b/__tests__/components/Header.test.tsx new file mode 100644 index 000000000..ada95d59b --- /dev/null +++ b/__tests__/components/Header.test.tsx @@ -0,0 +1,67 @@ +/* eslint-disable import/no-named-as-default */ +/* eslint-disable import/no-named-as-default-member */ +import React from 'react'; +import { render, fireEvent, screen } from '@testing-library/react'; +import Header from '../../app/components/Header'; +import { DashboardContext } from '../../app/context/DashboardContext'; +import { ApplicationContext } from '../../app/context/ApplicationContext'; +import { HashRouter as Router } from 'react-router-dom'; +import mockData from '../mock_data.json'; +import '@testing-library/jest-dom'; + +jest.mock('electron', () => ({ + ipcRenderer: { + send: () => jest.fn(), + on: () => mockData, + }, +})); +describe('Speed Chart', () => { + const props = { + app: 'Test DB', + service: 'Test Service', + setLive: jest.fn(), + live: 'false', + }; + let element; + beforeEach(() => { + // const mockData = [ + // { microservice1: 'Test 1'}, + // { microservice2: 'Test 2'}, + // ] + render( + + + +
    + + + + ); + element = screen.getByTestId('Header'); + }); + + it('Should render', () => { + expect(screen).toBeTruthy(); + }); + + it('Should have an h1, no input, and one button', () => { + expect(element.querySelectorAll('h1').length).toBe(1); + expect(element.querySelectorAll('input').length).toBe(0); + expect(element.querySelectorAll('button').length).toBe(1); + }); + + it('Button should have an onClick function', () => { + expect(typeof element.querySelector('button').onclick).toBe('function'); + }); + + // trying to test the functionality of component not passed as props + it('Should check/uncheck the checkbox when clicking services', () => { + // const checkBox = screen.getByRole('checkbox'); + // fireEvent.click(checkBox); + // expect(checkBox.parentElement).toHaveClass('selected'); + // fireEvent.click(checkBox); + // expect(checkBox.parentElement).not.toHaveClass('selected'); + }); + + it('Should also change selectModal to true or false', () => {}); +}); diff --git a/__tests__/components/Login.test.tsx b/__tests__/components/Login.test.tsx new file mode 100644 index 000000000..0d3addfb0 --- /dev/null +++ b/__tests__/components/Login.test.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import { render, fireEvent, screen } from '@testing-library/react'; +import { ipcRenderer } from 'electron'; +import Login from '../../app/components/Login'; +import DashboardContextProvider from '../../app/context/DashboardContext'; +import { HashRouter as Router } from 'react-router-dom'; +import '@testing-library/jest-dom'; + +const navigateMock = jest.fn(); + +jest.mock('electron', () => ({ ipcRenderer: { sendSync: jest.fn() } })); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: () => navigateMock, +})); + +// testing suite for the CreateAdmin.tsx file +describe('Create Admin Page', () => { + beforeEach(() => { + render( + + + + + + ); + navigateMock.mockReset(); + }); + + it('should render', () => { + expect(screen).toBeTruthy(); + }); + + it('Should contain an h1, h2, form, two buttons, and three inputs', () => { + const element = screen.getByTestId('Login'); + expect(element.querySelectorAll('h1').length).toBe(1); + expect(element.querySelectorAll('h2').length).toBe(1); + expect(element.querySelectorAll('form').length).toBe(1); + expect(element.querySelectorAll('button').length).toBe(2); + expect(element.querySelectorAll('input').length).toBe(2); + }); + + it('Login button should submit username and password to addUser', () => { + const usernameInput = screen.getByPlaceholderText('username'); + const passwordInput = screen.getByPlaceholderText('password'); + const loginButton = screen.getByRole('button', { name: /Login/i }); + + fireEvent.change(usernameInput, { target: { value: 'St1nky' } }); + fireEvent.change(passwordInput, { target: { value: 'me123' } }); + + fireEvent.click(loginButton); + expect(ipcRenderer.sendSync).toHaveBeenCalledWith('login', { + username: 'St1nky', + password: 'me123', + }); + }); + + it('Should reroute user to signup', () => { + const button = screen.getByText(/need an account/i); + expect(button).toBeInTheDocument(); + fireEvent.click(button); + + expect(navigateMock).toHaveBeenCalledTimes(1); + }); +}); diff --git a/__tests__/components/Settings.test.tsx b/__tests__/components/Settings.test.tsx new file mode 100644 index 000000000..17c5c3470 --- /dev/null +++ b/__tests__/components/Settings.test.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { render, fireEvent, screen } from '@testing-library/react'; +import Settings from '../../app/components/Settings'; +import { DashboardContext } from '../../app/context/DashboardContext'; +import '@testing-library/jest-dom'; + +describe('Settings', () => { + let changeMode = jest.fn(); + + beforeEach(() => { + render( + + + + ); + }); + + test('Should change mode to light mode on light button click', () => { + fireEvent.click(screen.getByRole('button', { name: 'Light' })); + expect(changeMode).toHaveBeenCalledWith('light'); + }); + + test('Should change mode to dark mode on dark button click', () => { + fireEvent.click(screen.getByRole('button', { name: 'Dark' })); + expect(changeMode).toHaveBeenCalledWith('dark'); + }); +}); diff --git a/__tests__/components/SignUp.test.tsx b/__tests__/components/SignUp.test.tsx new file mode 100644 index 000000000..1f75f90a8 --- /dev/null +++ b/__tests__/components/SignUp.test.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { render, fireEvent, screen, getByPlaceholderText } from '@testing-library/react'; +import { ipcRenderer } from 'electron'; +import SignUp from '../../app/components/SignUp'; +import DashboardContextProvider from '../../app/context/DashboardContext'; +import { HashRouter as Router } from 'react-router-dom'; + +jest.mock('electron', () => ({ ipcRenderer: { sendSync: jest.fn() } })); + +describe('Create Signup Page', () => { + beforeEach(() => { + render( + + + + + + ); + }); + + it('should render', () => { + expect(screen).toBeTruthy(); + }); + + it('Should contain an h1, h2, form, two buttons, and three inputs', () => { + const element = screen.getByTestId('SignUp'); + expect(element.querySelectorAll('h1').length).toBe(1); + expect(element.querySelectorAll('h2').length).toBe(1); + expect(element.querySelectorAll('form').length).toBe(1); + expect(element.querySelectorAll('button').length).toBe(2); + expect(element.querySelectorAll('input').length).toBe(4); + }); + + it('Sign up button should submit email, username, and password to addUser', async () => { + screen.debug(); + + const username = screen.getByPlaceholderText('enter username'); + const email = screen.getByPlaceholderText('your@email.here'); + const password = screen.getByPlaceholderText('enter password'); + const signupButton = screen.getByRole('signup'); + + fireEvent.change(email, { target: { value: 'me@gmail.com' } }); + fireEvent.change(username, { target: { value: 'me' } }); + fireEvent.change(password, { target: { value: 'me123' } }); + fireEvent.click(signupButton); + }); +}); diff --git a/__tests__/mock_data.json b/__tests__/mock_data.json new file mode 100644 index 000000000..a7a284a46 --- /dev/null +++ b/__tests__/mock_data.json @@ -0,0 +1,17 @@ +[{ + "activememory": [12, 13], + "blockedprocesses": [98, 43], + "cpuloadpercent": [12, 1], + "cpuspeed": [43569, 32], + "cputemp": [5, 32], + "freememory": [55648, 76], + "id": [6, 8], + "service": ["chronos-mock","chronos-mock"], + "latency": [41, 51], + "runningprocesses": [12, 153], + "sleepingprocesses": [312, 33], + "time": ["", ""], + "totalmemory": [1, 4], + "usedmemory": [3, 6] +}] + diff --git a/__tests__/test_settings.json b/__tests__/test_settings.json new file mode 100644 index 000000000..8600797f7 --- /dev/null +++ b/__tests__/test_settings.json @@ -0,0 +1,14 @@ +{ + "services": [ + [ + "chronosDB", + "MongoDB", + "PUT YOUR MONGO URI HERE", + "", + "Dec 19, 2022 4:50 PM" + ] + ], + "mode": "light", + "splash": true, + "landingPage": "dashBoard" +} diff --git a/app/App.tsx b/app/App.tsx new file mode 100644 index 000000000..36be7d732 --- /dev/null +++ b/app/App.tsx @@ -0,0 +1,18 @@ +import React, { useState } from 'react'; +import Splash from './components/Splash/Splash'; +import DashboardContainer from './containers/DashboardContainer/DashboardContainer'; +import './index.scss'; + + +// this is the fitness gram pacer test +// React memo helps with rendering optimization. The components within React memo will only be rerendered if prompt has changed +const App: React.FC = React.memo(() => { + return ( +
    + + +
    + ); +}); + +export default App; diff --git a/app/actions/actionTypes.js b/app/actions/actionTypes.js deleted file mode 100644 index 41b11a736..000000000 --- a/app/actions/actionTypes.js +++ /dev/null @@ -1,9 +0,0 @@ -/** - * ************************************ - * - * @module actionTypes.js - * @description Action Type Constants - * - * ************************************ - */ - diff --git a/app/actions/actions.js b/app/actions/actions.js deleted file mode 100644 index e1d955640..000000000 --- a/app/actions/actions.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * ************************************ - * - * @module actions.js - * @description Action Creators - * - * ************************************ - */ - -//Import all types from constant file (actionTypes); -import * as types from "./actionTypes"; - -//For reducer to grab and use; -export const addMessage = message => ({ - type: types.ADD_MESSAGE, - payload: message // Data OBJECT we sent from the server; -}); diff --git a/app/assets/AntDesign.svg b/app/assets/AntDesign.svg new file mode 100644 index 000000000..56c997a54 --- /dev/null +++ b/app/assets/AntDesign.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/C.svg b/app/assets/C.svg new file mode 100644 index 000000000..83f0bc243 --- /dev/null +++ b/app/assets/C.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/Chronos-Demo-poster.png b/app/assets/Chronos-Demo-poster.png new file mode 100644 index 000000000..e1ce9e813 Binary files /dev/null and b/app/assets/Chronos-Demo-poster.png differ diff --git a/app/assets/Chronos-Demo.gif b/app/assets/Chronos-Demo.gif new file mode 100644 index 000000000..e2aad4ddd Binary files /dev/null and b/app/assets/Chronos-Demo.gif differ diff --git a/app/assets/admin_approval.gif b/app/assets/admin_approval.gif new file mode 100644 index 000000000..674e65c1b Binary files /dev/null and b/app/assets/admin_approval.gif differ diff --git a/app/assets/animated_logo.gif b/app/assets/animated_logo.gif new file mode 100644 index 000000000..a06db77a8 Binary files /dev/null and b/app/assets/animated_logo.gif differ diff --git a/app/assets/apple-icon-black.png b/app/assets/apple-icon-black.png new file mode 100644 index 000000000..64c373af9 Binary files /dev/null and b/app/assets/apple-icon-black.png differ diff --git a/app/assets/aws-icon-white.png b/app/assets/aws-icon-white.png new file mode 100644 index 000000000..8532571dd Binary files /dev/null and b/app/assets/aws-icon-white.png differ diff --git a/app/assets/aws-icon-whitebg.png b/app/assets/aws-icon-whitebg.png new file mode 100644 index 000000000..004fcf1ae Binary files /dev/null and b/app/assets/aws-icon-whitebg.png differ diff --git a/app/assets/aws-logo-color.png b/app/assets/aws-logo-color.png new file mode 100644 index 000000000..c84fae753 Binary files /dev/null and b/app/assets/aws-logo-color.png differ diff --git a/app/assets/clean-sprout.gif b/app/assets/clean-sprout.gif new file mode 100644 index 000000000..78ed457f7 Binary files /dev/null and b/app/assets/clean-sprout.gif differ diff --git a/app/assets/disable_sign_up.gif b/app/assets/disable_sign_up.gif new file mode 100644 index 000000000..4dcb0b7f3 Binary files /dev/null and b/app/assets/disable_sign_up.gif differ diff --git a/app/assets/docker-logo-color.png b/app/assets/docker-logo-color.png new file mode 100644 index 000000000..5f290e448 Binary files /dev/null and b/app/assets/docker-logo-color.png differ diff --git a/app/assets/dotenvSetup.png b/app/assets/dotenvSetup.png new file mode 100644 index 000000000..c775cfe41 Binary files /dev/null and b/app/assets/dotenvSetup.png differ diff --git a/app/assets/dropdown-arrow.png b/app/assets/dropdown-arrow.png new file mode 100644 index 000000000..e23260a3e Binary files /dev/null and b/app/assets/dropdown-arrow.png differ diff --git a/app/assets/electron-logo-color.png b/app/assets/electron-logo-color.png new file mode 100644 index 000000000..266c24c50 Binary files /dev/null and b/app/assets/electron-logo-color.png differ diff --git a/app/assets/email-icon-black.png b/app/assets/email-icon-black.png new file mode 100644 index 000000000..3f7fa1e2e Binary files /dev/null and b/app/assets/email-icon-black.png differ diff --git a/app/assets/enable_sign_up.gif b/app/assets/enable_sign_up.gif new file mode 100644 index 000000000..f36de8346 Binary files /dev/null and b/app/assets/enable_sign_up.gif differ diff --git a/app/assets/enzyme-logo-color.png b/app/assets/enzyme-logo-color.png new file mode 100644 index 000000000..243bc3dcd Binary files /dev/null and b/app/assets/enzyme-logo-color.png differ diff --git a/app/assets/express-logo-color.png b/app/assets/express-logo-color.png new file mode 100644 index 000000000..c58bb46a0 Binary files /dev/null and b/app/assets/express-logo-color.png differ diff --git a/app/assets/fire.png b/app/assets/fire.png new file mode 100644 index 000000000..b4152f9a2 Binary files /dev/null and b/app/assets/fire.png differ diff --git a/app/assets/graphql-logo-color.png b/app/assets/graphql-logo-color.png new file mode 100644 index 000000000..7a9d349d0 Binary files /dev/null and b/app/assets/graphql-logo-color.png differ diff --git a/app/assets/graphs.gif b/app/assets/graphs.gif new file mode 100644 index 000000000..226025eea Binary files /dev/null and b/app/assets/graphs.gif differ diff --git a/app/assets/growing-bean-1.gif b/app/assets/growing-bean-1.gif new file mode 100644 index 000000000..b3fc35e8d Binary files /dev/null and b/app/assets/growing-bean-1.gif differ diff --git a/app/assets/growing-bean-2.gif b/app/assets/growing-bean-2.gif new file mode 100644 index 000000000..12e962baf Binary files /dev/null and b/app/assets/growing-bean-2.gif differ diff --git a/app/assets/growing-bean-3.gif b/app/assets/growing-bean-3.gif new file mode 100644 index 000000000..a8a397960 Binary files /dev/null and b/app/assets/growing-bean-3.gif differ diff --git a/app/assets/grpc-logo-color.png b/app/assets/grpc-logo-color.png new file mode 100644 index 000000000..960fc1296 Binary files /dev/null and b/app/assets/grpc-logo-color.png differ diff --git a/app/assets/http-logo-color.png b/app/assets/http-logo-color.png new file mode 100644 index 000000000..e8bebcc3f Binary files /dev/null and b/app/assets/http-logo-color.png differ diff --git a/app/assets/important.png b/app/assets/important.png new file mode 100644 index 000000000..cd2d1e220 Binary files /dev/null and b/app/assets/important.png differ diff --git a/app/assets/jest-logo-color.png b/app/assets/jest-logo-color.png new file mode 100644 index 000000000..6a1c4e821 Binary files /dev/null and b/app/assets/jest-logo-color.png differ diff --git a/app/assets/js-logo-color.png b/app/assets/js-logo-color.png new file mode 100644 index 000000000..6a83dd367 Binary files /dev/null and b/app/assets/js-logo-color.png differ diff --git a/app/assets/logo.svg b/app/assets/logo.svg new file mode 100644 index 000000000..eee791654 --- /dev/null +++ b/app/assets/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/macbook-logo-color.png b/app/assets/macbook-logo-color.png new file mode 100644 index 000000000..c664b3983 Binary files /dev/null and b/app/assets/macbook-logo-color.png differ diff --git a/app/assets/material-ui-logo-color.png b/app/assets/material-ui-logo-color.png new file mode 100644 index 000000000..e40e5b66c Binary files /dev/null and b/app/assets/material-ui-logo-color.png differ diff --git a/app/assets/mit-logo-color.png b/app/assets/mit-logo-color.png new file mode 100644 index 000000000..f572c74b2 Binary files /dev/null and b/app/assets/mit-logo-color.png differ diff --git a/app/assets/mongo-icon-color.png b/app/assets/mongo-icon-color.png new file mode 100644 index 000000000..ca049ca27 Binary files /dev/null and b/app/assets/mongo-icon-color.png differ diff --git a/app/assets/mongo-icon-green-light.png b/app/assets/mongo-icon-green-light.png new file mode 100644 index 000000000..8ae1c1165 Binary files /dev/null and b/app/assets/mongo-icon-green-light.png differ diff --git a/app/assets/mongo-icon-green.png b/app/assets/mongo-icon-green.png new file mode 100644 index 000000000..31004b55a Binary files /dev/null and b/app/assets/mongo-icon-green.png differ diff --git a/app/assets/mongo-icon-white.png b/app/assets/mongo-icon-white.png new file mode 100644 index 000000000..02a03b592 Binary files /dev/null and b/app/assets/mongo-icon-white.png differ diff --git a/app/assets/mongo-logo-color.png b/app/assets/mongo-logo-color.png new file mode 100644 index 000000000..dea317a6f Binary files /dev/null and b/app/assets/mongo-logo-color.png differ diff --git a/app/assets/mountain.png b/app/assets/mountain.png new file mode 100644 index 000000000..0e4d7a81f Binary files /dev/null and b/app/assets/mountain.png differ diff --git a/app/assets/mountain_longer.png b/app/assets/mountain_longer.png new file mode 100644 index 000000000..bebb4c551 Binary files /dev/null and b/app/assets/mountain_longer.png differ diff --git a/app/assets/node-logo-color.png b/app/assets/node-logo-color.png new file mode 100644 index 000000000..3d48409d4 Binary files /dev/null and b/app/assets/node-logo-color.png differ diff --git a/app/assets/npm-logo-color.png b/app/assets/npm-logo-color.png new file mode 100644 index 000000000..f6d6c14fe Binary files /dev/null and b/app/assets/npm-logo-color.png differ diff --git a/app/assets/plotly-logo-color.png b/app/assets/plotly-logo-color.png new file mode 100644 index 000000000..7ebfd8398 Binary files /dev/null and b/app/assets/plotly-logo-color.png differ diff --git a/app/assets/pngwing.com 2.png b/app/assets/pngwing.com 2.png new file mode 100644 index 000000000..44c2ff38d Binary files /dev/null and b/app/assets/pngwing.com 2.png differ diff --git a/app/assets/pngwing.com.png b/app/assets/pngwing.com.png new file mode 100644 index 000000000..44c2ff38d Binary files /dev/null and b/app/assets/pngwing.com.png differ diff --git a/app/assets/postgres-icon-white.png b/app/assets/postgres-icon-white.png new file mode 100644 index 000000000..84e0605b0 Binary files /dev/null and b/app/assets/postgres-icon-white.png differ diff --git a/app/assets/postgres-icon-yellow-light.png b/app/assets/postgres-icon-yellow-light.png new file mode 100644 index 000000000..75755532a Binary files /dev/null and b/app/assets/postgres-icon-yellow-light.png differ diff --git a/app/assets/postgres-icon-yellow.png b/app/assets/postgres-icon-yellow.png new file mode 100644 index 000000000..7de3144ea Binary files /dev/null and b/app/assets/postgres-icon-yellow.png differ diff --git a/app/assets/postgres-logo-color.png b/app/assets/postgres-logo-color.png new file mode 100644 index 000000000..d8e6c1212 Binary files /dev/null and b/app/assets/postgres-logo-color.png differ diff --git a/app/assets/query_tool.gif b/app/assets/query_tool.gif new file mode 100644 index 000000000..ec5e1ba80 Binary files /dev/null and b/app/assets/query_tool.gif differ diff --git a/app/assets/react-logo-color.png b/app/assets/react-logo-color.png new file mode 100644 index 000000000..b68893ceb Binary files /dev/null and b/app/assets/react-logo-color.png differ diff --git a/app/assets/slack-logo-color.png b/app/assets/slack-logo-color.png new file mode 100644 index 000000000..524b6da35 Binary files /dev/null and b/app/assets/slack-logo-color.png differ diff --git a/app/assets/spectron-logo-color.png b/app/assets/spectron-logo-color.png new file mode 100644 index 000000000..c75ec6e86 Binary files /dev/null and b/app/assets/spectron-logo-color.png differ diff --git a/app/assets/ts-logo-color.png b/app/assets/ts-logo-color.png new file mode 100644 index 000000000..007f4237b Binary files /dev/null and b/app/assets/ts-logo-color.png differ diff --git a/app/assets/ts-logo-long-blue.png b/app/assets/ts-logo-long-blue.png new file mode 100644 index 000000000..cbcd5846b Binary files /dev/null and b/app/assets/ts-logo-long-blue.png differ diff --git a/app/assets/ts-logo-long.png b/app/assets/ts-logo-long.png new file mode 100644 index 000000000..670b70a67 Binary files /dev/null and b/app/assets/ts-logo-long.png differ diff --git a/app/assets/vis-logo-color.png b/app/assets/vis-logo-color.png new file mode 100644 index 000000000..ee5292b88 Binary files /dev/null and b/app/assets/vis-logo-color.png differ diff --git a/app/assets/webpack-logo-color.png b/app/assets/webpack-logo-color.png new file mode 100644 index 000000000..8f9f703d6 Binary files /dev/null and b/app/assets/webpack-logo-color.png differ diff --git a/app/charts/AwsChart.tsx b/app/charts/AwsChart.tsx new file mode 100644 index 000000000..a0cc65312 --- /dev/null +++ b/app/charts/AwsChart.tsx @@ -0,0 +1,103 @@ +import moment from 'moment'; +import React, { useState } from 'react'; +import Plot from 'react-plotly.js'; +import { all, solo as soloStyle } from './sizeSwitch'; + +// interface AwsCpuChartProps { +// key: string; +// renderService: string; +// metric: string; +// timeList: any; +// valueList: any; +// sizing: string; +// colourGenerator: Function; +// } + +interface SoloStyles { + height: number; + width: number; +} + +/** + * @params props - the props object containing relevant data. + * @desc Handles AWS Charts. Memoized component to generate an AWS chart with formatted data. + * @returns {JSX.Element} The JSX element with the AWS chart. + */ +const AwsChart: React.FC = React.memo(props => { + const { renderService, metric, timeList, valueList, colourGenerator, sizing } = props; + const [solo, setSolo] = useState(null); + setInterval(() => { + if (solo !== soloStyle) { + setSolo(soloStyle); + } + }, 20); + + const createChart = () => { + const timeArr = timeList?.map((el: any) => moment(el).format('kk:mm:ss')); + // const hashedColour = colourGenerator(renderService); + let plotlyData: { + name: any; + x: any; + y: any; + type: any; + mode: any; + marker: { color: string }; + }; + plotlyData = { + name: metric, + x: timeArr, + y: valueList, + type: 'scattergl', + mode: 'lines', + marker: { color: colourGenerator() }, + }; + const sizeSwitch = sizing === 'all' ? all : solo; + + return ( + + ); + }; + + return ( +
    + {createChart()} +
    + ); +}); + +export default AwsChart; diff --git a/app/charts/EventChart.tsx b/app/charts/EventChart.tsx new file mode 100644 index 000000000..ed21ac832 --- /dev/null +++ b/app/charts/EventChart.tsx @@ -0,0 +1,117 @@ +import moment from 'moment'; +import React, { useState } from 'react'; +import Plot from 'react-plotly.js'; +import { all, solo as soloStyle } from './sizeSwitch'; + +interface EventChartProps { + key: string; + metricName: string; + chartData: { + value: string[], + time: string[] + } + sizing: string; + colourGenerator: Function; +} + +interface SoloStyles { + height: number; + width: number; +} + +type PlotlyData = { + name: string; + x: string[]; + y: string[]; + type: string; + mode: string; + marker: { colors: string[] }; +}; + +/** + * @params {EventChartProps} props - the props object containing relevant data. + * @desc Handles k8s metrics. Memoized component to generate event chart with formatted data + * @returns {JSX.Element} The JSX element with the event chart. + */ +const EventChart: React.FC = React.memo(props => { + const { metricName, chartData, sizing, colourGenerator } = props; + const [solo, setSolo] = useState(null); + + setInterval(() => { + if (solo !== soloStyle) { + setSolo(soloStyle); + } + }, 20); + + // makes time data human-readable, and reverses it so it shows up correctly in the graph + const prettyTimeInReverse = (timeArray: string[]): string[] => { + return timeArray.map((el: any) => moment(el).format('kk:mm:ss')).reverse(); + }; + + // removes underscores from metric names to improve their look in the graph + const prettyMetricName = (metricName: string): string => { + const newName = metricName.replace(/.*\/.*\//g, ''); + return newName.replace(/_/g, ' '); + }; + + const createChart = () => { + const prettyName = prettyMetricName(metricName); + const prettyTime = prettyTimeInReverse(chartData.time); + + const plotlyDataObject: PlotlyData = { + name: prettyName, + x: prettyTime, + y: chartData.value, + type: 'scattergl', + mode: 'lines', + marker: { + colors: ['#fc4039', '#4b54ea', '#32b44f', '#3788fc', '#9c27b0', '#febc2c'], + }, + }; + const sizeSwitch = sizing === 'all' ? all : solo; + + return ( + + ); + }; + + return ( +
    + {createChart()} +
    + ); +}); + +export default EventChart; diff --git a/app/charts/GrafanaEventChart/GrafanaEventChart.tsx b/app/charts/GrafanaEventChart/GrafanaEventChart.tsx new file mode 100644 index 000000000..4728e7d96 --- /dev/null +++ b/app/charts/GrafanaEventChart/GrafanaEventChart.tsx @@ -0,0 +1,196 @@ +import React, { useState } from 'react'; +import { all, solo as soloStyle } from '../sizeSwitch'; +import './styles.scss'; + +interface EventChartProps { + metricName: string; + token: string; +} + +type TimeFrame = '5m' | '15m' | '30m' | '1h' | '2h' | '1d' | '2d'; + +/** + * @params {EventChartProps} props - the props object containing relevant data. + * @desc Handles k8s and container metrics. Memoized component to generate event chart with formatted data + * @returns {JSX.Element} The JSX element with the event chart. + */ +const GrafanaEventChart: React.FC = React.memo(props => { + const { metricName, token } = props; + const [graphType, setGraphType] = useState('timeseries'); + const [type, setType] = useState(['timeserie']); + const [timeFrame, setTimeFrame] = useState('5m'); + + // console.log("graphType: ", graphType) + // console.log("type: ", type) + // console.log("inside GrafanaEventChart") + + // console.log("metricName: ", metricName) + let uid = metricName.replace(/.*\/.*\//g, ''); + if (uid.length >= 40) { + uid = metricName.slice(metricName.length - 39); + } + + let parsedName = metricName.replace(/.*\/.*\//g, ''); + // console.log("uid: ", uid) + // console.log("parsedName: ", parsedName) + + const handleSelectionChange = async event => { + setType([...type, graphType]); + await fetch('http://localhost:1111/api/updateDashboard', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ graphType: event.target.value, metric: metricName, token: token }), + }); + console.log('event.target.value: ', event.target.value); + setGraphType(event.target.value); + }; + + return ( +
    +

    {`${parsedName} --- ${graphType}`}

    +
    + + + + + +
    + {/* create chart using grafana iframe tag*/} + {/* {type[type.length - 1] !== graphType ? + + : } */} + {graphType === 'timeseries' + ? TimeSeries(uid, parsedName, graphType, timeFrame) + : graphType === 'barchart' + ? BarChart(uid, parsedName, graphType, timeFrame) + : graphType === 'stat' + ? Stat(uid, parsedName, graphType, timeFrame) + : graphType === 'gauge' + ? Gauge(uid, parsedName, graphType, timeFrame) + : graphType === 'table' + ? Table(uid, parsedName, graphType, timeFrame) + : graphType === 'histogram' + ? Histogram(uid, parsedName, graphType, timeFrame) + : graphType === 'piechart' + ? PieChart(uid, parsedName, graphType, timeFrame) + : graphType === 'alertlist' + ? AlertList(uid, parsedName, graphType, timeFrame) + : null} +
    + ); +}); + +const TimeSeries = (uid, parsedName, graphType, timeFrame) => { + return ( + <> + +
    + + ); +}; + +const BarChart = (uid, parsedName, graphType, timeFrame) => { + return ( + + ); +}; + +const Stat = (uid, parsedName, graphType, timeFrame) => { + return ( + + ); +}; + +const Gauge = (uid, parsedName, graphType, timeFrame) => { + return ( + + ); +}; + +const Table = (uid, parsedName, graphType, timeFrame) => { + return ( + + ); +}; + +const Histogram = (uid, parsedName, graphType, timeFrame) => { + return ( + + ); +}; + +const PieChart = (uid, parsedName, graphType, timeFrame) => { + return ( + + ); +}; + +const AlertList = (uid, parsedName, graphType, timeFrame) => { + return ( + + ); +}; +export default GrafanaEventChart; diff --git a/app/charts/GrafanaEventChart/styles.scss b/app/charts/GrafanaEventChart/styles.scss new file mode 100644 index 000000000..bbf86b2eb --- /dev/null +++ b/app/charts/GrafanaEventChart/styles.scss @@ -0,0 +1,59 @@ +#graphType { + min-width: 100px; + height: 25px; + color: #fff; + padding: 5px 10px; + font-weight: bold; + cursor: pointer; + transition: all 0.3s ease; + position: relative; + display: inline-block; + outline: none; + border-radius: 5px; + border: none; + background-size: 120% auto; + background-image: linear-gradient(315deg, #bdc3c7 0%, #2c3e50 75%); + margin-top: 10px; + margin-bottom: 10px; + +} + +#graphType { + background-position: right center; +} + +#graphType { + top: 2px; +} + +#timeFrame { + min-width: 100px; + height: 25px; + color: #fff; + padding: 5px 10px; + font-weight: bold; + cursor: pointer; + transition: all 0.3s ease; + position: relative; + display: inline-block; + outline: none; + border-radius: 5px; + border: none; + background-size: 120% auto; + background-image: linear-gradient(315deg, #bdc3c7 0%, #2c3e50 75%); + margin-top: 10px; + margin-bottom: 10px; + +} + +#timeFrame { + background-position: right center; +} + +#timeFrame { + top: 2px; +} + +#Grafana_Event_Chart { + margin-bottom: 50px; +} \ No newline at end of file diff --git a/app/charts/HealthChart.tsx b/app/charts/HealthChart.tsx new file mode 100644 index 000000000..54768ca36 --- /dev/null +++ b/app/charts/HealthChart.tsx @@ -0,0 +1,136 @@ +import moment from 'moment'; +import React, { useState } from 'react'; +import Plot from 'react-plotly.js'; +import { all, solo as soloStyle } from './sizeSwitch'; + +interface HealthChartProps { + key: string; + dataType: string; + serviceName: string; + chartData: object; + categoryName: string; + sizing: string; + colourGenerator: Function; +} +interface SoloStyles { + height: number; + width: number; +} +type PlotlyData = { + name: string; + x: string[]; + y: string[]; + type: string; + mode: string; + marker: { colors: string[] }; +}; + +/** + * @params {HealthChartProps} props - the props object containing relevant data. + * @desc Handles microservices metrics. Memoized component to generate a health chart with formatted data. + * @returns {JSX.Element} The JSX element with the health chart. + */ +const HealthChart: React.FC = React.memo(props => { + const { dataType, serviceName, chartData, categoryName, sizing, colourGenerator } = props; + const [solo, setSolo] = useState(null); + + // makes time data human-readable, and reverses it so it shows up correctly in the graph + const prettyTimeInReverse = (timeArray: string[]): string[] => { + return timeArray.map((el: any) => moment(el).format('kk:mm:ss')).reverse(); + }; + + // removes underscores from metric names to improve their look in the graph + const prettyMetricName = (metricName: string): string => { + return metricName.replace(/_/g, ' '); + }; + + // generates an array of plotly data objects to be passed into our plotly chart's data prop + const generatePlotlyDataObjects = (chartDataObj: object): object[] => { + const arrayOfPlotlyDataObjects: PlotlyData[] = []; + + // loop through the list of metrics for the current service + for (const metricName in chartDataObj) { + // define the value and time arrays; allow data to be reassignable in case we need to convert the bytes data into megabytes + let dataArray = chartDataObj[metricName].value; + const timeArray = chartDataObj[metricName].time; + // specifically for `Megabyte` types, convert the original data of bytes into a value of megabytes before graphing + if (dataType === 'Memory in Megabytes' || dataType === 'Cache in Megabytes') { + dataArray = dataArray.map(value => (value / 1000000).toFixed(2)); + } + // create the plotly object + const plotlyDataObject: PlotlyData = { + name: prettyMetricName(metricName), + x: prettyTimeInReverse(timeArray), + y: dataArray, + type: 'scattergl', + mode: 'lines', + marker: { + colors: ['#fc4039', '#4b54ea', '#32b44f', '#3788fc', '#9c27b0', '#febc2c'], + }, + }; + // push the plotlyDataObject into the arrayOfPlotlyDataObjects + arrayOfPlotlyDataObjects.push(plotlyDataObject); + } + // return the array of plotlyDataObject + return arrayOfPlotlyDataObjects; + }; + + setInterval(() => { + if (solo !== soloStyle) { + setSolo(soloStyle); + } + }, 20); + + /** + * @desc Takes the chart data and configures Plotly object to render associated health chart. + * @returns {JSX.Element} Configured Plotly object representing health chart. + */ + const createChart = () => { + const dataArray = generatePlotlyDataObjects(chartData); + const sizeSwitch = sizing === 'all' ? all : solo; + + return ( + + ); + }; + + return ( +
    + {createChart()} +
    + ); +}); + +export default HealthChart; diff --git a/app/charts/LogsTable.jsx b/app/charts/LogsTable.jsx new file mode 100644 index 000000000..b5fbc5292 --- /dev/null +++ b/app/charts/LogsTable.jsx @@ -0,0 +1,169 @@ +/** From Version 5.2 Team: + * We didn't really touch this file; mostly just cleaned a little bit of linting errors... + * But we left all the 'Missing "key" props,' 'Prop spreading is forbidden,' and 'Do not nest ternary expressions.' + */ + +import React, { useContext } from 'react'; +import styled from 'styled-components'; +import { useTable, useGroupBy, useExpanded } from 'react-table'; + +import { CommsContext } from '../context/CommsContext'; + +/** + * Styling for the Logs Table + */ + +const Styles = styled.div` + padding: 1rem; + + table { + border-spacing: 0; + border: 1px solid black; + + tr { + :last-child { + td { + border-bottom: 0; + } + } + } + + th, + td { + margin: 0; + padding: 0.5rem; + border-bottom: 1px solid black; + border-right: 1px solid black; + + :last-child { + border-right: 0; + } + } + } +`; + +const Table = ({ columns, data }) => { + const { + getTableProps, + getTableBodyProps, + headerGroups, + rows, + prepareRow, + state: { groupBy, expanded }, + } = useTable({ columns, data }, useGroupBy, useExpanded) + + const numberOfRows = 20; + const firstPageRows = rows.slice(0, numberOfRows); + + return ( +
    + + + {headerGroups.map(headerGroup => ( + + {headerGroup.headers.map((column) => ( + + ))} + + ))} + + + {firstPageRows.map(row => { + prepareRow(row); + return ( + + {row.cells.map(cell => { + return ( + + ) + })} + + ); + })} + +
    + {column.canGroupBy ? ( + + {column.isGrouped ? '➖ ' : '➕ '} + + ): null} + {column.render('Header')} +
    + {cell.isGrouped ? ( + <> + + {row.isExpanded ? '⬇️' : '➡️'} + {' '} + {cell.render('Cell')} ({row.subRows.length}) + + ) : cell.isAggregated ? ( + cell.render('Aggregated') + ) : cell.isPlaceholder ? null : ( + cell.render('Cell') + )} +
    +
    + Showing the first {rows.length} of {rows.length} rows +
    +
    + ); +}; + +const LogsTable = () => { + const columns = React.useMemo( + () => [ + { + Header: 'Time', + accessor: 'time', + }, + { + Header: 'CorrelatingID', + accessor: 'correlatingid', + aggregate: 'count', + Aggregated: ({ value }) => `${value} logs`, + }, + { + Header: 'Microservice', + accessor: 'microservice', + }, + { + Header: 'Endpoint', + accessor: 'endpoint', + }, + { + Header: 'Request Method', + accessor: 'request', + }, + { + Header: 'Response Status', + accessor: 'responsestatus', + }, + { + Header: 'Response Message', + accessor: 'responsemessage', + }, + ], + [] + ); + + const data = useContext(CommsContext).commsData; + + return ( + + + + ); +}; + +export default LogsTable; diff --git a/app/charts/RequestTypesChart.tsx b/app/charts/RequestTypesChart.tsx new file mode 100644 index 000000000..8e77e75ba --- /dev/null +++ b/app/charts/RequestTypesChart.tsx @@ -0,0 +1,85 @@ +import React, { useContext } from 'react'; +import Plot from 'react-plotly.js'; +import { CommsContext } from '../context/CommsContext'; +import '../index.scss'; + +const RequestTypesChart: React.FC = React.memo(() => { + const { commsData } = useContext(CommsContext); + + interface IObject { + correlatingid: string; + endpoint: string; + id: number; + microservice: string; + request: string; + responsemessage: string; + responsestatus: string; + time: string; + } + const createRequestChart = () => { + const requestTypes: { [key: string]: number } = { + DELETE: 0, + GET: 0, + PATCH: 0, + POST: 0, + PUSH: 0, + PUT: 0, + }; + + let type; + commsData.forEach((obj: IObject) => { + type = obj.request; + if (type in requestTypes) { + requestTypes[type] += 1; + } else { + requestTypes[type] = 0; + requestTypes[type]++; + } + }); + + return ( + + ); + }; + + return
    {createRequestChart()}
    ; +}); + +export default RequestTypesChart; diff --git a/app/charts/ResponseCodesChart.tsx b/app/charts/ResponseCodesChart.tsx new file mode 100644 index 000000000..b61b85d4e --- /dev/null +++ b/app/charts/ResponseCodesChart.tsx @@ -0,0 +1,136 @@ +/** From Version 5.2 Team: + * We only cleaned up the linting errors + * Did not change any functionality in this page + */ + +import React, { useContext } from 'react'; +import Plot from 'react-plotly.js'; +import { CommsContext } from '../context/CommsContext'; + +interface IObj { + correlatingid: string; + endpoint: string; + id: number; + microservice: string; + request: string; + responsemessage: string; + responsestatus: number; + time: string; +} + +const ResponseCodesChart: React.FC = React.memo(() => { + const { commsData } = useContext(CommsContext); + + const createChart = () => { + const responseCodes: { [key: string]: number } = { + '100-199': 0, + '200-299': 0, + '300-399': 0, + '400-499': 0, + '500-599': 0, + '0': 0, + '1': 0, + '2': 0, + '3': 0, + '4': 0, + '5': 0, + '6': 0, + '7': 0, + '8': 0, + '9': 0, + '10': 0, + '11': 0, + '12': 0, + '13': 0, + '14': 0, + '15': 0, + '16': 0, + }; + + commsData.forEach((obj: IObj) => { + const status = obj.responsestatus; + if (status >= 500) { + responseCodes['500-599'] += 1; + } else if (status >= 400) { + responseCodes['400-499'] += 1; + } else if (status >= 300) { + responseCodes['300-399'] += 1; + } else if (status >= 200) { + responseCodes['200-299'] += 1; + } else if (status >= 100) { + responseCodes['100-199'] += 1; + } else { + responseCodes[status] += 1; + } + }); + + return ( + + ); + }; + + return
    {createChart()}
    ; +}); + +export default ResponseCodesChart; diff --git a/app/charts/RouteChart.jsx b/app/charts/RouteChart.jsx new file mode 100644 index 000000000..ca6e2c317 --- /dev/null +++ b/app/charts/RouteChart.jsx @@ -0,0 +1,172 @@ +import { makeStyles } from '@mui/styles'; +import React, { useContext } from 'react'; +import Graph from 'react-graph-vis'; +import { CommsContext } from '../context/CommsContext'; + +const RouteChart = React.memo(() => { + const communicationsData = useContext(CommsContext).commsData; + const resObj = {}; + const dataId = '_id'; + + if (communicationsData.length > 0 && communicationsData[0][dataId]) { + /** From Version 5.2 Team: + * @communicationsData comes back as an array with data in descending order. + * The below sorts it back into ascending order. + */ + + communicationsData.sort((a, b) => { + if (new Date(a.time) > new Date(b.time)) return 1; + if (new Date(a.time) < new Date(b.time)) return -1; + return 0; + }); + for (let i = 0; i < communicationsData.length; i += 1) { + const element = communicationsData[i]; + if (!resObj[element.correlatingid]) resObj[element.correlatingid] = []; + resObj[element.correlatingid].push({ + microservice: element.microservice, + time: element.time, + request: element.request, + }); + } + } else { + for (let i = communicationsData.length - 1; i >= 0; i--) { + const element = communicationsData[i]; + if (resObj[element.correlatingid]) { + resObj[element.correlatingid].push({ + microservice: element.microservice, + time: element.time, + }); + } else { + resObj[element.correlatingid] = [ + { + microservice: element.microservice, + time: element.time, + }, + ]; + } + } + } + + const tracePoints = Object.values(resObj).filter(subArray => subArray.length > 1); + + const useStyles = makeStyles(theme => ({ + paper: { + height: 300, + width: 300, + textAlign: 'center', + color: '#444d56', + whiteSpace: 'nowrap', + backgroundColor: '#ffffff', + borderRadius: 3, + border: '0', + boxShadow: '2px 2px 6px #bbbbbb', + }, + })); + const classes = useStyles({}); + + /** + * Graph Logic Below + */ + + const nodeListObj = {}; + const edgeListObj = {}; + for (const route of tracePoints) { + for (let i = 0; i < route.length; i += 1) { + const id = route[i].microservice; + if (nodeListObj[id] === undefined) { + nodeListObj[id] = { + id, + label: id, + }; + } + + if (i !== 0) { + const from = route[i - 1].microservice; + const to = id; + const { request } = route[i - 1]; + const edgeStr = JSON.stringify({ from, to, request }); + let duration = new Date(route[i].time) - new Date(route[i - 1].time); + + if (edgeListObj[edgeStr]) { + duration = (duration + edgeListObj[edgeStr]) / 2; + } + edgeListObj[edgeStr] = duration; + } + } + } + + const nodeList = Object.values(nodeListObj); + const edgeList = []; + for (const [edgeStr, duration] of Object.entries(edgeListObj)) { + const edge = JSON.parse(edgeStr); + edge.label = edge.request + ? `${edge.request} - ${(duration * 10).toFixed(0)} ms` + : `${(duration * 10).toFixed(0)} ms`; + edgeList.push(edge); + } + + const graph = { + nodes: nodeList, + edges: edgeList, + }; + const options = { + height: '600px', + width: '600px', + layout: { + hierarchical: false, + }, + edges: { + color: '#444d56', + physics: true, + smooth: { + type: 'curvedCCW', + forceDirection: 'none', + roundness: 0.3, + }, + font: { + color: '#444d56', + size: 9, + }, + }, + nodes: { + borderWidth: 0, + color: { + background: '#3788fc', + hover: { + background: '#febc2c', + }, + highlight: { + background: '#fc4039', + }, + }, + shape: 'circle', + font: { + color: '#ffffff', + size: 10, + face: 'roboto', + }, + }, + }; + + if (communicationsData.length > 0 && communicationsData[0].endpoint !== '/') { + return ( +
    + Route Traces + +
    + ); + } + return null; +}); + +export default RouteChart; diff --git a/app/charts/TrafficChart.tsx b/app/charts/TrafficChart.tsx new file mode 100644 index 000000000..0afc67653 --- /dev/null +++ b/app/charts/TrafficChart.tsx @@ -0,0 +1,65 @@ +import React, { useContext } from 'react'; +import Plot from 'react-plotly.js'; +import { CommsContext } from '../context/CommsContext'; + +const TrafficChart = React.memo(() => { + const { commsData } = useContext(CommsContext); + const microserviceCount: { [key: string]: number } = {}; + + for (let i = 0; i < commsData.length; i += 1) { + const curr = commsData[i].microservice; + if (!microserviceCount[curr]) microserviceCount[curr] = 0; + microserviceCount[curr] += 1; + } + + const xAxis = Object.keys(microserviceCount); + + const serverPings: number[] = Object.values(microserviceCount); + + const yAxisHeadRoom: number = Math.max(...serverPings) + 10; + + return ( +
    + +
    + ); +}); + +export default TrafficChart; diff --git a/app/charts/sizeSwitch.js b/app/charts/sizeSwitch.js new file mode 100644 index 000000000..78d1399e1 --- /dev/null +++ b/app/charts/sizeSwitch.js @@ -0,0 +1,24 @@ +let soloWidth = window.innerWidth > 800 ? 800 : window.innerWidth - 270; + +/** From Version 5.2 Team: + * @solo needs to be mutable, but eslint doesn't like exporting mutable variables + */ + +// eslint-disable-next-line import/no-mutable-exports +export let solo = { + height: 600, + width: soloWidth, +}; + +window.addEventListener('resize', () => { + soloWidth = window.innerWidth > 800 ? 800 : window.innerWidth - 270; + solo = { + ...solo, + width: soloWidth, + }; +}); + +export const all = { + height: 400, + width: 400, +}; diff --git a/app/components/About/About.tsx b/app/components/About/About.tsx new file mode 100644 index 000000000..17fe69e90 --- /dev/null +++ b/app/components/About/About.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import './styles.scss'; +import TeamMembers from './TeamMembers'; +import PastContributors from './PastContributors'; +import { useStylingContext } from './StylingContext'; + + +const About: React.FC = React.memo(() => { + const currentMode = useStylingContext(); + + return ( +
    +
    +

    About

    +

    + The Chronos Team has a passion for building tools that are powerful, beautifully + designed, and easy to use. Chronos was conceived as an Open Source endeavor that directly benefits the developer + community. It can also monitor applications deployed using AWS, EC2, and ECS from Amazon. +

    +

    + + +
    +
    + ); +}); + +export default About; \ No newline at end of file diff --git a/app/components/About/PastContributors.tsx b/app/components/About/PastContributors.tsx new file mode 100644 index 000000000..9cad8330d --- /dev/null +++ b/app/components/About/PastContributors.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { useStylingContext } from './StylingContext'; + +const contributors = [ + 'Snow', 'Taylor', 'Tim', 'Roberto', 'Nachiket', 'Tiffany', 'Bruno', 'Danny', 'Vince', + 'Matt', 'Derek', 'Kit', 'Grace', 'Jen', 'Patty', 'Stella', 'Michael', 'Ronelle', 'Todd', + 'Greg', 'Brianna', 'Brian', 'Alon', 'Alan', 'Ousman', 'Ben', 'Chris', 'Jenae', 'Tim', + 'Kirk', 'Jess', 'William', 'Alexander', 'Elisa', 'Josh', 'Troy', 'Gahl', 'Brisa', 'Kelsi', + 'Lucie', 'Jeffrey', 'Justin' +]; + +const PastContributors: React.FC = () => { + const currentMode = useStylingContext(); + + return ( +
    +

    + Past Contributors +

    +

    + {contributors.join(', ')} +

    +
    +
    + ); +}; + +export default PastContributors; \ No newline at end of file diff --git a/app/components/About/StylingContext.tsx b/app/components/About/StylingContext.tsx new file mode 100644 index 000000000..b99f255f0 --- /dev/null +++ b/app/components/About/StylingContext.tsx @@ -0,0 +1,9 @@ +import { useContext } from 'react'; +import { DashboardContext } from '../../context/DashboardContext'; +import lightAndDark from '../Styling'; + +export const useStylingContext = () => { + const { mode } = useContext(DashboardContext); + const currentMode = mode === 'light' ? lightAndDark.lightModeText : lightAndDark.darkModeText; + return currentMode; +}; \ No newline at end of file diff --git a/app/components/About/TeamMembers.tsx b/app/components/About/TeamMembers.tsx new file mode 100644 index 000000000..a7959fe04 --- /dev/null +++ b/app/components/About/TeamMembers.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { useStylingContext } from './StylingContext'; + +const names = ['Haoyu', 'Eisha', 'Edwin', 'Tyler']; + +const TeamMembers: React.FC = () => { + const currentMode = useStylingContext(); + + const nameList = names.map(name => ( + +

    {name}

    +
    + )); + + return ( +
    +

    + Current Version Authors +

    +
    + {nameList} +
    +
    +
    + ); +}; + +export default TeamMembers; \ No newline at end of file diff --git a/app/components/About/styles.scss b/app/components/About/styles.scss new file mode 100644 index 000000000..42043873c --- /dev/null +++ b/app/components/About/styles.scss @@ -0,0 +1,75 @@ +@import '../../index.scss'; + +.about { + min-width: 421px; + max-width: 600px; + margin: auto 20px; + display: flex; + justify-content: center; + align-items: center; + width: 90%; + height: 100%; +} + +.sprout { + margin: 0; +} + +.mainTitle { + font-size: 36px; + color: $background; + text-align: left; + font-weight: 600; +} + +.title { + font-size: 18px; + color: $background; + text-align: left; + font-weight: 600; +} + +.text { + font-size: 20px; + color: $background; + text-align: left; + font-weight: 200; +} + +.blurb { + width: 100%; + margin: auto; + display: flex; + flex-direction: column; + justify-content: left; + border-radius: 10px; + border: none; + box-shadow: $boxshadow2; + padding: 30px; + + &:hover { + background-color: rgba(255, 255, 255, 0.1); + } +} + +#sproutSpan { + display: inline-block; + margin-top: 0px; + margin-bottom: 0px; + margin-left: 15px; + margin-right: 10px; + height: $sproutSize; + width: $sproutSize; + background-color: transparent; + border-radius: 50%; + background-size: scale-down; + &:hover { + background-color: $hover; + opacity: 0.85; + animation: none; + } +} + +p { + padding-left: 10px; +} diff --git a/app/components/ApplicationsCard/ApplicationsCard.tsx b/app/components/ApplicationsCard/ApplicationsCard.tsx new file mode 100644 index 000000000..0dc5ed68e --- /dev/null +++ b/app/components/ApplicationsCard/ApplicationsCard.tsx @@ -0,0 +1,115 @@ + +import React, { useContext, useRef } from "react"; +import { useNavigate } from 'react-router-dom'; +import { Card,CardHeader,IconButton,CardContent,Typography } from "@mui/material"; +import { DashboardContext } from "../../context/DashboardContext"; +import { ApplicationContext } from "../../context/ApplicationContext"; +import HighlightOffIcon from '@mui/icons-material/HighlightOff'; +import UpdateIcon from '@mui/icons-material/Update'; +import './styles.scss' + +type ClickEvent = React.MouseEvent; + +const ApplicationsCard = (props) => { + + const { application, i, setModal, classes } = props + const { deleteApp,user,applications } = useContext(DashboardContext) + const { setAppIndex, setApp, setServicesData, app,example,connectToDB,setChart } = useContext(ApplicationContext) + const [ cardName,dbType,dbURI,description,serviceType ] = application + + //dynamic refs + // const delRef = useRef([]); + + const navigate = useNavigate(); + + // Handle clicks on Application cards + // v10 info: when card is clicked (not on the delete button) if the service is an AWS instance, + // you are redirected to AWSGraphsContainer passing in the state object containing typeOfService + const handleClick = ( + selectedApp: string, + selectedService: string, + i: number + ) => { + const services = ['auth','client','event-bus','items','inventory','orders'] + // if (delRef.current[i] && !delRef.current[i].contains(event.target)) { + setAppIndex(i); + setApp(selectedApp); + if ( + selectedService === 'AWS' || + selectedService === 'AWS/EC2' || + selectedService === 'AWS/ECS' || + selectedService === 'AWS/EKS' + ) { + navigate(`/aws/:${app}`, { state: { typeOfService: selectedService } }); + } + else if(example) { + setServicesData([]); + setChart('communications') + + connectToDB(user, i, app, dbURI, dbType) + navigate(`/applications/example/${services.join(' ')}`) + } + else { + setServicesData([]); + //When we open the service modal a connection is made to the db in a useEffect inside of the service modal component + setModal({isOpen:true,type:'serviceModal'}) + } + // } + }; + + // Asks user to confirm deletion + const confirmDelete = (event: ClickEvent, i: number) => { + event.stopPropagation() + const message = `The application '${app}' will be permanently deleted. Continue?`; + if (confirm(message)) deleteApp(i,""); + }; + + return ( +
    + handleClick(application[0], application[3], i)} + > +
    +
    +
    +
    + + confirmDelete(event, i)} + size="large"> + + + } + /> + + + {application[0]} + +

    Service:

    + {application[3]} +
    +
    + +
    + + +

    {application[4]}

    +
    +
    +
    +
    + ); +} + +export default ApplicationsCard \ No newline at end of file diff --git a/app/components/ApplicationsCard/styles.scss b/app/components/ApplicationsCard/styles.scss new file mode 100644 index 000000000..8fef07fab --- /dev/null +++ b/app/components/ApplicationsCard/styles.scss @@ -0,0 +1,246 @@ +@import '../../index.scss'; + +.card { + display: flex; + flex-direction: row; + justify-content: space-around; + margin: 20px; + padding: 0; + cursor: pointer; + transition: all 0.5s; + + &:after, + &:before { + content: ' '; + width: 10px; + height: 10px; + position: absolute; + transition: all 0.5s; + } + + &:hover { + position: relative; + border-top-right-radius: 0px; + border-bottom-left-radius: 0px; + + &:before, + &:after { + width: 25%; + height: 25%; + } + } +} + +.card { + &:hover .databaseIconHeader { + visibility: hidden; + // background-color: $gblue; + // opacity: 0.7; + // box-shadow: 0 4px 20px 0 rgba(0, 0, 0,.14), 0 7px 10px -5px rgba(255, 255, 255, 0.4); + } + + &:hover p { + color: $background; + font-weight: 400; + } + + &:hover .cardFooter { + color: $background; + } + + &:hover .cardLine { + background-color: $background; + } + + &:hover #cardFooterText { + color: $background; + } + + &:hover .cardFooterIcon { + color: $background; + } + + &:hover #databaseName { + color: $background; + } +} + +#card-MongoDB { + &:hover .databaseIconHeader { + visibility: hidden; + // background-color: $gblue; + // opacity: 0.7; + // box-shadow: 0 4px 20px 0 rgba(0, 0, 0,.14), 0 7px 10px -5px rgba(255, 255, 255, 0.4); + } + + .databaseIconContainer { + display: inline-block; + overflow: visible; + } + + .databaseIconHeader { + position: absolute; + background-color: $ggreen; + display: flex; + justify-content: center; + align-items: center; + width: 90px; + height: 90px; + padding: 15px; + float: left; + top: -20px; + left: 180px; + border-radius: 3px; + box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.14), 0 7px 10px -5px rgba(0, 255, 42, 0.4); + background-image: url('../../assets/mongo-icon-white.png'); + background-position: center; + background-size: 70%; + background-repeat: no-repeat; + + .databaseIcon { + width: 55px; + height: 55px; + visibility: hidden; + } + } +} + +#card-SQL { + &:hover .databaseIconHeader { + visibility: hidden; + // background-color: $gblue; + // opacity: 0.7; + // box-shadow: 0 4px 20px 0 rgba(0, 0, 0,.14), 0 7px 10px -5px rgba(255, 255, 255, 0.4); + } + + .databaseIconContainer { + display: inline-block; + overflow: visible; + } + + .databaseIconHeader { + position: absolute; + background-color: $gyellow; + display: flex; + justify-content: center; + align-items: center; + width: 90px; + height: 90px; + padding: 15px; + float: left; + top: -20px; + left: 180px; + border-radius: 3px; + box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.14), 0 7px 10px -5px rgba(255, 152, 0, 0.4); + background-image: url('../../assets/postgres-icon-white.png'); + background-size: cover; + + .databaseIcon { + visibility: hidden; + } + } +} + +#card-AWS { + &:hover .databaseIconHeader { + visibility: hidden; + // background-color: $gblue; + // opacity: 0.7; + // box-shadow: 0 4px 20px 0 rgba(0, 0, 0,.14), 0 7px 10px -5px rgba(255, 255, 255, 0.4); + } + + .databaseIconContainer { + display: inline-block; + overflow: visible; + } + + .databaseIconHeader { + position: absolute; + background-color: $gorange; + display: flex; + justify-content: center; + align-items: center; + width: 90px; + height: 90px; + padding: 15px; + float: left; + top: -20px; + left: 180px; + border-radius: 3px; + box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.14), 0 7px 10px -5px rgba(255, 152, 0, 0.4); + background-image: url('../../assets/aws-icon-white.png'); + background-size: cover; + + .databaseIcon { + visibility: hidden; + } + } +} + +.databaseIconContainer { + display: inline-block; + overflow: visible; +} + +.databaseIconHeader { + position: absolute; + background-color: $ggreen; + display: flex; + justify-content: center; + align-items: center; + width: 90px; + height: 90px; + padding: 15px; + float: left; + top: -20px; + left: 180px; + border-radius: 3px; + box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.14), 0 7px 10px -5px rgba(0, 255, 42, 0.4); + + .databaseIcon { + width: 55px; + height: 55px; + } +} + +.cardLine { + background-color: $grey; + width: 85%; + border: none; + height: 1px; + margin-top: 20px; + margin-bottom: 15px; +} + +.cardFooter { + width: 90%; + display: flex; + height: 20px; + align-items: center; + margin-left: 18px; +} + +#cardFooterText { + color: $icon; + font-size: 11px; + margin: 0; + margin-left: 10px; +} + +.cardFooterIcon { + color: $icon; + font-size: 14px; + margin: 0; +} + +#databaseName { + margin-top: 14px; + margin-bottom: 0; + font-size: 40px; + width: 280px; +} + +#serviceName { + font-size: 11px; + margin-top: 6px; +} diff --git a/app/components/AwsEC2Graphs.tsx b/app/components/AwsEC2Graphs.tsx new file mode 100644 index 000000000..6c94eb275 --- /dev/null +++ b/app/components/AwsEC2Graphs.tsx @@ -0,0 +1,54 @@ +import React, { useContext, useEffect, useState } from 'react'; +import AwsChart from '../charts/AwsChart'; +import { AwsContext } from '../context/AwsContext'; +import { CircularProgress } from '@mui/material'; +// import zIndex from '@mui/styles/zIndex'; + +const AwsEC2Graphs: React.FC = React.memo(props => { + const { awsData, setAwsData, isLoading, setLoadingState } = useContext(AwsContext); + + useEffect(() => { + return () => { + setAwsData({ CPUUtilization: [], NetworkIn: [], NetworkOut: [], DiskReadBytes: [] }); + setLoadingState(true); + }; + }, []); + + const stringToColor = (string: string, recurses = 0) => { + if (recurses > 20) return string; + function hashString(str: string) { + let hash = 0; + for (let i = 0; i < str.length; i++) { + hash = str.charCodeAt(i) + ((hash << 5) - hash); + } + let colour = '#'; + for (let i = 0; i < 3; i++) { + const value = (hash >> (i * 8)) & 0xff; + colour += `00${value.toString(16)}`.substring(-2); + } + + // console.log(colour); + return colour; + } + }; + + return ( +
    + {Object.keys(awsData)?.map(metric => { + return ( + el.time)} + valueList={awsData[metric]?.map(el => el.value)} + colourGenerator={stringToColor} + /> + ); + })} +
    + ); +}); + +export default AwsEC2Graphs; diff --git a/app/components/AwsECSClusterGraphs.tsx b/app/components/AwsECSClusterGraphs.tsx new file mode 100644 index 000000000..1e118a043 --- /dev/null +++ b/app/components/AwsECSClusterGraphs.tsx @@ -0,0 +1,68 @@ +import React, { useContext, useEffect } from 'react'; +import AwsChart from '../charts/AwsChart'; +import { AwsContext } from '../context/AwsContext'; + +const AwsECSClusterGraphs: React.FC = React.memo(props => { + const { awsEcsData, setAwsEcsData, setLoadingState } = useContext(AwsContext); + + useEffect(() => { + return () => { + setAwsEcsData({}); + setLoadingState(true); + }; + }, []); + + const stringToColor = (string: string, recurses = 0) => { + if (recurses > 20) return string; + function hashString(str: string) { + let hash = 0; + for (let i = 0; i < str.length; i++) { + hash = str.charCodeAt(i) + ((hash << 5) - hash); + } + let colour = '#'; + for (let i = 0; i < 3; i++) { + const value = (hash >> (i * 8)) & 0xff; + colour += `00${value.toString(16)}`.substring(-2); + } + + // console.log(colour); + return colour; + } + }; + + const activeServices = Object.keys(awsEcsData) + .slice(1) + .filter(el => awsEcsData[el].CPUUtilization?.value.length > 0); + const serviceGraphs = activeServices.map(service => { + return ( +
    +
    +

    Service Name:

    + {service} +
    +
    + + +
    +
    + ); + }); + + return
    {serviceGraphs}
    ; +}); + +export default AwsECSClusterGraphs; diff --git a/app/components/ClusterTable.tsx b/app/components/ClusterTable.tsx new file mode 100644 index 000000000..d1dd88c72 --- /dev/null +++ b/app/components/ClusterTable.tsx @@ -0,0 +1,88 @@ +import React, { useContext, useState } from 'react'; +import { + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Paper, +} from '@mui/material'; +import { makeStyles } from '@mui/styles/'; +import { AwsContext } from '../context/AwsContext'; + +const useStyles = makeStyles({ + table: { + minWidth: 650, + }, + tableContainer: { + margin: '0 20px', + }, + activeCell: { + color: 'green', + }, + column: { + display: 'flex', + flexDirection: 'column', + }, + body: { + fontSize: '1.2rem', + }, + title: { + color: '#888888', + }, +}); + +export interface ClusterTableProps { + typeOfService: string; + region: string; +} + +const ClusterTable: React.FC = React.memo(({ region }) => { + const classes = useStyles(); + const { awsEcsData, isLoading } = useContext(AwsContext); + + const activeServices = () => { + const serviceNames = Object.keys(awsEcsData).slice(1); + + return serviceNames.filter(el => awsEcsData[el].CPUUtilization?.value.length > 0); + }; + + return ( +
    + +
    + + + Cluster Name + Status + Services + Tasks + + + + +
    +
    {isLoading ? 'Loading...' : awsEcsData.clusterInfo?.clusterName}
    +
    + {region} +
    +
    +
    + + {activeServices().length ? 'ACTIVE' : 'INACTIVE'} + + + {isLoading ? 'Loading...' : Object.keys(awsEcsData).length - 1} + + + {isLoading ? 'Loading...' : String(activeServices().length) + '/' + String(Object.keys(awsEcsData).length - 1)} + +
    +
    + + + ); +}); + +export default ClusterTable; diff --git a/app/components/Contact/Contact.tsx b/app/components/Contact/Contact.tsx new file mode 100644 index 000000000..54d77bcbd --- /dev/null +++ b/app/components/Contact/Contact.tsx @@ -0,0 +1,33 @@ +import React, { useContext } from 'react'; +import './styles.scss'; +import { DashboardContext } from '../../context/DashboardContext'; +import lightAndDark from '../Styling'; +import ContactForm from './ContactForm'; + +const Contact: React.FC = React.memo(() => { + const { mode } = useContext(DashboardContext); + + const currentMode = mode === 'light' ? lightAndDark.lightModeText : lightAndDark.darkModeText; + + return ( +
    +
    +
    +
    +

    Contact Us

    +
    +

    Please feel free to provide any feedback, concerns, or comments.

    +

    You can find issues the team is currently working on + + here + . +

    +
    +
    + +
    +
    + ); +}); + +export default Contact; \ No newline at end of file diff --git a/app/components/Contact/ContactForm.tsx b/app/components/Contact/ContactForm.tsx new file mode 100644 index 000000000..60844c8d0 --- /dev/null +++ b/app/components/Contact/ContactForm.tsx @@ -0,0 +1,39 @@ +import React from 'react'; + +interface FormInputProps { + label: string; + name: string; + placeholder?: string; + type?: string; + isTextarea?: boolean; + accept?: string; +} + +const ContactForm = ({ currentMode }) => { + return ( +
    +
    + + + + + + + + +
    + ); +}; + +const FormInput: React.FC = ({ label, name, placeholder, type = "text", isTextarea = false, accept }) => ( +