├── .babelrc ├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── __test__ ├── backend │ ├── jest-setup.js │ ├── jest-teardown.js │ └── server.test.js └── frontend │ ├── accountTotal.test.js │ ├── redux-components.test.js │ ├── simpleTests.test.js │ ├── test-utils.jsx │ └── testingState.test.js ├── package.json ├── public ├── account-totals.gif ├── astro-banner.jpeg ├── creds-demo.gif └── function-demo.gif ├── server ├── controllers │ ├── aws │ │ ├── Logs │ │ │ ├── getLogs.js │ │ │ └── updateLogs.js │ │ ├── credentials │ │ │ ├── getCreds.js │ │ │ └── getSTSCreds.js │ │ └── metrics │ │ │ ├── getLambdaFuncs.js │ │ │ ├── getMetricsAllFuncs.js │ │ │ ├── getMetricsByFunc.js │ │ │ └── utils │ │ │ └── AWSUtilFunc.js │ └── userController.js ├── db.js ├── routers │ ├── aws.js │ └── userRouter.js └── server.js ├── src ├── App.jsx ├── components │ ├── AccountTotals.jsx │ ├── Dashboard.jsx │ ├── LineChart.jsx │ ├── TimePeriod.jsx │ └── TotalsByFunc.jsx ├── features │ ├── slices │ │ ├── chartSlice.js │ │ ├── credSlice.js │ │ ├── dataSlice.js │ │ ├── funcListSlice.js │ │ ├── insightsToggleSlice.js │ │ ├── timePeriodSlice.js │ │ └── userSlice.js │ └── store.js ├── index.html ├── index.js ├── pages │ ├── Login.jsx │ ├── Navigation.jsx │ ├── Signup.jsx │ └── styles │ │ └── styles.css └── utils │ ├── getAWSCreds.js │ ├── getMetricsAllFunc.js │ └── getMetricsByFunc.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | // need the following code to avoid the import regenerator-runtime/runtime issue 7 | "targets": { 8 | "node": "current" // the target node version, boolean true, or "current". 9 | } 10 | } 11 | ], 12 | "@babel/preset-react" 13 | ], 14 | "plugins": [ 15 | [ 16 | "@babel/plugin-transform-runtime", 17 | { 18 | "helpers": true, 19 | "regenerator": true 20 | } 21 | ], 22 | ["@babel/plugin-transform-async-to-generator"] 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["**/test", "**/__tests__"], 4 | "env": { 5 | "node": true, 6 | "es2021": true 7 | }, 8 | "extends": "eslint:recommended", 9 | "parser": "babel-eslint", 10 | "parserOptions": { "sourceType": "module" }, 11 | "rules": { 12 | "indent": ["warn", 2], 13 | "no-unused-vars": ["off", { "vars": "local" }], 14 | "prefer-const": "warn", 15 | "quotes": ["warn", "single"], 16 | "space-infix-ops": "warn" 17 | } 18 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | package-lock.json 3 | dist/ 4 | .env -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 ASTRO 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 | Logo 5 |

AWS Lambda Metrics Monitoring Tool

6 | 7 | 8 | 9 | [![Contributors][contributors-shield]][contributors-url] 10 | [![Forks][forks-shield]][forks-url] 11 | [![Stargazers][stars-shield]][stars-url] 12 | [![Issues][issues-shield]][issues-url] 13 | [![MIT License][license-shield]][license-url] 14 | 15 |
16 | 17 | 18 | 19 |
20 | Table of Contents 21 |
    22 |
  1. About Astro
  2. 23 |
  3. Tech Stack
  4. 24 | 28 | 31 |
  5. License
  6. 32 |
  7. Authors
  8. 33 |
34 |
35 | 36 | 37 | 38 | ## About Astro 39 | 40 | Serverless architecture is an exciting mainstay of cloud computing. Amazon's Web Services (AWS) Lambda is a giant in the serverless space and is widely utilized by various companies. Its event-driven paradigm to building distributed, on-demand infrastructure is also cost-effective since Lambda functions are billed only when they are executed. This reduces the physical need for servers, eliminating expensive hosting costs just to keep a server running even if it’s not in use. One issue is that navigating through the AWS console can be daunting and frustrating. Specifically, to measure a user's lambda functions, there are too many options and this massive flexibility proves cumbersome when one only needs to visualize specific metrics at a glance. 41 | 42 | As a way to solve this, we built Astro: a free, open-source lambda function monitoring tool that users can connect to their AWS account to securely and easily monitor and track key metrics. 43 | 44 |

(back to top)

45 | 46 | ### Tech Stack 47 | 48 | - [Redux Toolkit](https://redux-toolkit.js.org/) 49 | - [React](https://reactjs.org/) 50 | - [Material-UI](https://material-ui.com) 51 | - [Node](https://nodejs.org/en/) 52 | - [Express](https://expressjs.com) 53 | - [PostgreSQL](https://postgresql.org) 54 | - [AWS SDK](https://aws.amazon.com/sdk-for-javascript/) 55 | - [AWS CloudFormation](https://aws.amazon.com/cloudformation/) 56 | - [AWS STS](https://docs.aws.amazon.com/STS/latest/APIReference/welcome.html) 57 | - [Jest](https://jestjs.io/) 58 | - [Supertest](https://www.npmjs.com/package/supertest) 59 | - [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/) 60 | 61 |

(back to top)

62 | 63 | 64 | 65 | ## Getting Started 66 | 67 | If you are a developer trying to add/improve functionality, you can skip step 4 and go to step 5. If you are an AWS end user, do not worry about step 5. 68 | 69 | 1. Fork and clone the forked repo 70 | 71 | ```sh 72 | git clone 73 | cd Astro 74 | ``` 75 | 76 | 2. Install package devDependencies 77 | 78 | ```sh 79 | npm install 80 | ``` 81 | 82 | 3. If you are an AWS End User then use the following command to build the application and the necessary .env template file, which you should fill in with your AWS credentials (region, security key id, and access key id). 83 | 84 |
85 | Logo 86 |

87 |
88 | 89 | ```sh 90 | npm run build 91 | ``` 92 | 93 | 4. Afterwards, you can run Astro by using the following command and then navigating to localhost:1111 in your browser 94 | 95 | ```sh 96 | npm run start 97 | ``` 98 | 99 | 5. If you are a developer trying to add/improve functionality, instead of step 4 you should use the following command to run Astro in development and navigate to localhost:8080 in your browser to take advantage of hot module reloading. 100 | 101 | ```sh 102 | npm run dev 103 | ``` 104 | 105 | ### Lambda Metrics 106 | 107 | The key AWS Lambda function metrics we focused on are: throttles, invocations, and errors. One can see their total metric values in Account Totals. To see metrics by function, click the Functions tab to see a list of your lambda functions and the associated metrics for each function. Within the function tab, users can visualize their metrics over a specific time period using the drop down menu. This will also update the account total metrics in the account total tab. 108 | 109 |
110 | Logo 111 |

112 |
113 | 114 |

(back to top)

115 | 116 | 117 | 118 | ## Contributing 119 | 120 | Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. 121 | 122 | If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". 123 | Don't forget to give the project a star! Thanks again! 124 | 125 | 1. Fork the Project 126 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) 127 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) 128 | 4. Push to the Branch (`git push origin feature/AmazingFeature`) 129 | 5. Open a Pull Request 130 | 131 |

(back to top)

132 | 133 | 134 | 135 | ## License 136 | 137 | Distributed under the MIT License. See `LICENSE` for more information. 138 | 139 |

(back to top)

140 | 141 | 142 | 143 | ## Contributors 144 | 145 | - Adam White [Github](https://github.com/adam-k-w) | [Linkedin](https://www.linkedin.com/in/adam-karn-white/) 146 | - Anthony Piscocama [Github](https://github.com/adavid1696) | [Linkedin](https://www.linkedin.com/in/anthony-piscocama-07858b167/) 147 | - Michelle Shahid [Github](https://github.com/emshahh) | [Linkedin](https://www.linkedin.com/in/michelleshahid/) 148 | - Nehreen Anam [Github](https://github.com/Issafeature) | [Linkedin](https://www.linkedin.com/in/nehreen/) 149 | - Samuel Carrasco [Github](https://github.com/samhcarrasco) | [Linkedin](https://www.linkedin.com/in/samuelhcarrasco/) 150 | 151 |

(back to top)

152 | 153 | 154 | 155 | 156 | [contributors-shield]: https://img.shields.io/github/contributors/oslabs-beta/ASTRO.svg?style=for-the-badge 157 | [contributors-url]: https://github.com/oslabs-beta/ASTRO/graphs/contributors 158 | [forks-shield]: https://img.shields.io/github/forks/oslabs-beta/ASTRO.svg?style=for-the-badge 159 | [forks-url]: https://github.com/oslabs-beta/ASTRO/network/members 160 | [stars-shield]: https://img.shields.io/github/stars/oslabs-beta/ASTRO.svg?style=for-the-badge 161 | [stars-url]: https://github.com/oslabs-beta/ASTRO/stargazers 162 | [issues-shield]: https://img.shields.io/github/issues/oslabs-beta/ASTRO.svg?style=for-the-badge 163 | [issues-url]: https://github.com/oslabs-beta/ASTRO/issues 164 | [license-shield]: https://img.shields.io/github/license/oslabs-beta/ASTRO.svg?style=for-the-badge 165 | [license-url]: https://github.com/oslabs-beta/ASTRO/blob/master/LICENSE.txt 166 | [linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555 167 | [linkedin-url]: https://linkedin.com/in/projectASTRO 168 | [product-screenshot]: public/astro-banner.jpeg 169 | -------------------------------------------------------------------------------- /__test__/backend/jest-setup.js: -------------------------------------------------------------------------------- 1 | // const regeneratorRuntime = require('regenerator-runtime'); 2 | // import 'regenerator-runtime/runtime'; 3 | 4 | module.exports = () => { 5 | global.testServer = require('../../server/server.js'); 6 | }; -------------------------------------------------------------------------------- /__test__/backend/jest-teardown.js: -------------------------------------------------------------------------------- 1 | // import 'regenerator-runtime'; 2 | 3 | module.exports = async (globalConfig) => { 4 | global.testServer.close(); 5 | }; 6 | -------------------------------------------------------------------------------- /__test__/backend/server.test.js: -------------------------------------------------------------------------------- 1 | import 'regenerator-runtime/runtime'; 2 | 3 | const request = require('supertest'); 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | 7 | const server = 'http://localhost:1111' 8 | 9 | describe('Route integration', () => { 10 | describe('/aws/getCreds', () => { 11 | describe('GET', () => { 12 | it ('responds with 200 status and json content type', async () => { 13 | return request(server) 14 | .get('/aws/getCreds') 15 | .expect('Content-Type', /json/) 16 | .expect(200) 17 | }) 18 | }) 19 | 20 | }) 21 | }) -------------------------------------------------------------------------------- /__test__/frontend/accountTotal.test.js: -------------------------------------------------------------------------------- 1 | // import React from "react"; 2 | // import { render, fireEvent, screen } from "./test-utils"; 3 | // import { rest } from 'msw' 4 | // import { setupServer} from 'msw/node' 5 | // import { Insights } from '../../src/pages/Insights' 6 | 7 | // const handlers = [ 8 | // rest.get('/aws/getCreds', (req, res, ctx) => { 9 | // return res(ctx.json({ 10 | // region: 'testRegion', 11 | // credentials: { 12 | // accessKeyId: 'testAKI', 13 | // secretAccessKey: 'testSAK', 14 | // } 15 | // }), ctx.delay(150)) 16 | // }) 17 | // ] 18 | 19 | // const server = setupServer(...handlers) 20 | 21 | // beforeAll(() => server.listen()) 22 | 23 | // // Reset any runtime request handlers we may add during the tests. 24 | // afterEach(() => server.resetHandlers()) 25 | 26 | // // Disable API mocking after the tests are done. 27 | // afterAll(() => server.close()) 28 | 29 | // describe('switch to account totals when it is clicked', () => { 30 | // // render() 31 | 32 | // it("switch to account totals", () => { 33 | // render() 34 | // expect(screen.queryAllByText(/Account Totals/i)).toBeInTheDocument(); 35 | // }); 36 | 37 | 38 | // }); 39 | -------------------------------------------------------------------------------- /__test__/frontend/redux-components.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import "regenerator-runtime/runtime"; 3 | import { render, fireEvent, screen, funcList } from './test-utils' 4 | import { Insights } from '../../src/pages/Insights' 5 | import { Dashboard } from '../../src/components/Dashboard'; 6 | // import subject from '../../src/features/store' 7 | 8 | 9 | describe('render react components', () => { 10 | // let initialState; 11 | 12 | // beforeEach(() => { 13 | // initialState = { 14 | // funcList: [], 15 | // region: '', 16 | // credentials: { 17 | // accessKeyId: '', 18 | // secretAccessKey: '', 19 | // } 20 | // }; 21 | // }); 22 | 23 | // it("renders Insights", async () => { 24 | // const insights = await render(); 25 | // expect(await insights).toBeTruthy(); 26 | // }); 27 | 28 | it("renders dashboard", () => { 29 | const dash = render (); 30 | expect(dash).toBeTruthy(); 31 | }) 32 | }) 33 | 34 | -------------------------------------------------------------------------------- /__test__/frontend/simpleTests.test.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | unit testing - testing a small part of code 4 | integration testing - multiple units working together 5 | end to end - testing from front to backend 6 | */ 7 | 8 | // possible tests we can implement: 9 | 10 | // -and onclick or onchange events including toggling the sidebar or navbar 11 | //data fetching 12 | 13 | import { render, screen } from '@testing-library/react' 14 | import { NavBar } from '../../src/components/NavBar.jsx' 15 | import React from 'react' 16 | import { Dashboard } from '../../src/components/Dashboard.jsx' 17 | import { Provider } from 'react-redux'; 18 | import { Insights } from '../../src/pages/Insights.jsx' 19 | 20 | 21 | 22 | 23 | it("renders navbar", () => { 24 | 25 | const navBar = render() 26 | expect(navBar).toBeTruthy() 27 | 28 | }); 29 | 30 | it("renders a button named Dashboard on the navbar", () => { 31 | 32 | const { getByTestId } = render() 33 | expect(getByTestId("dashBtnNavBar").textContent).toBe("Dashboard") 34 | }); 35 | 36 | 37 | it("has a button named github", () => { 38 | render() 39 | const githubButton = screen.getByRole('button', {name: /Github/i}) 40 | expect(githubButton).toBeInTheDocument() 41 | }) 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /__test__/frontend/test-utils.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render as rtlRender } from '@testing-library/react'; 3 | import { configureStore } from '@reduxjs/toolkit'; 4 | import { Provider } from 'react-redux'; 5 | import insightsToggleReducer from '../../src/features/slices/insightsToggleSlice' 6 | import chartSliceReducer from '../../src/features/slices/chartSlice' 7 | import credSliceReducer from '../../src/features/slices/credSlice' 8 | import chartDataReducer from '../../src/features/slices/dataSlice' 9 | import funcListReducer from '../../src/features/slices/funcListSlice' 10 | import userReducer from '../../src/features/slices/userSlice' 11 | 12 | function render( 13 | ui, 14 | { 15 | preloadedState, 16 | store = configureStore({ 17 | reducer: { 18 | toggleInsights: insightsToggleReducer, 19 | chart: chartSliceReducer, 20 | creds: credSliceReducer, 21 | data: chartDataReducer, 22 | funcList: funcListReducer, 23 | user: userReducer 24 | }, 25 | preloadedState, 26 | }), 27 | ...renderOptions 28 | } = {} 29 | ) { 30 | function Wrapper({ children }) { 31 | return {children}; 32 | } 33 | return rtlRender(ui, { wrapper: Wrapper, ...renderOptions }); 34 | } 35 | 36 | // re-export everything 37 | export * from '@testing-library/react' 38 | // override render method 39 | export { render } -------------------------------------------------------------------------------- /__test__/frontend/testingState.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, fireEvent, screen } from "./test-utils"; 3 | 4 | import toggleChange from '../../src/features/slices/insightsToggleSlice' 5 | import nameChange from "../../src/features/slices/chartSlice"; 6 | import getBackendCreds from "../../src/features/slices/credSlice"; 7 | import chartDataReducer from "../../src/features/slices/dataSlice"; 8 | import funcListReducer from "../../src/features/slices/funcListSlice"; 9 | import userReducer from "../../src/features/slices/userSlice"; 10 | 11 | 12 | 13 | describe('default state', () => { 14 | let initialState; 15 | 16 | beforeEach(() => { 17 | initialState = { 18 | toggle: 'Functions', 19 | name: 0 20 | } 21 | }) 22 | 23 | 24 | it("should return a default state when given an undefined input for INSIGHTS TOGGLE SLICE", () => { 25 | expect(toggleChange(initialState.toggle, { payload: undefined })).toEqual(initialState.toggle); 26 | }); 27 | 28 | it("should return a default state when given an undefined input for CHART SLICE", () => { 29 | expect(nameChange(initialState.name, { payload: undefined })).toEqual(initialState.name); 30 | }); 31 | }); 32 | 33 | 34 | 35 | ////////////////////////////////////////////////////////////////////////// 36 | 37 | describe('default state', () => { 38 | let initialState; 39 | 40 | beforeEach(() => { 41 | initialState = { 42 | region: '', 43 | credentials: { 44 | accessKeyId: '', 45 | secretAccessKey: '', 46 | } 47 | } 48 | }) 49 | 50 | 51 | it("should return a default state when given an undefined input for CRED SLICE", async () => { 52 | expect(getBackendCreds(initialState.region, { payload: undefined })).toHaveLength(0); 53 | }); 54 | 55 | }); 56 | 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "astro", 3 | "version": "1.0.0", 4 | "description": "Monitoring tool for AWS Lambda services", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "NODE_ENV=production nodemon server/server.js", 8 | "dev": "NODE_ENV=development nodemon server/server.js & NODE_ENV=development webpack-dev-server", 9 | "build": "NODE_ENV=development webpack & (echo 'AWS_REGION=\"insert region here\"'; echo 'AWS_ACCESS_KEY_ID=\"insert aws access key here\"'; echo 'AWS_SECRET_ACCESS_KEY=\"insert aws secret access key here\"') > .env", 10 | "test": "jest --verbose" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/oslabs-beta/ASTRO.git" 15 | }, 16 | "author": "Anthony, Michelle, Adam, Samuel, Nehreen", 17 | "license": "ISC", 18 | "bugs": { 19 | "url": "https://github.com/oslabs-beta/ASTRO/issues" 20 | }, 21 | "homepage": "https://github.com/oslabs-beta/ASTRO#readme", 22 | "jest": { 23 | "verbose": true, 24 | "globalSetup": "./__test__/backend/jest-setup.js", 25 | "globalTeardown": "./__test__/backend/jest-teardown.js", 26 | "testEnvironment": "jest-environment-jsdom", 27 | "setupFilesAfterEnv": [ 28 | "@testing-library/jest-dom/extend-expect" 29 | ] 30 | }, 31 | "devDependencies": { 32 | "@babel/core": "^7.17.5", 33 | "@babel/plugin-transform-runtime": "^7.17.0", 34 | "@babel/preset-env": "^7.16.11", 35 | "@babel/preset-react": "^7.16.7", 36 | "@testing-library/react": "^12.1.4", 37 | "babel-loader": "^8.2.3", 38 | "cross-env": "^7.0.3", 39 | "css-loader": "^6.7.0", 40 | "eslint": "^8.10.0", 41 | "html-webpack-plugin": "^5.5.0", 42 | "msw": "^0.39.2", 43 | "nodemon": "^2.0.15", 44 | "sass": "^1.49.9", 45 | "sass-loader": "^12.6.0", 46 | "style-loader": "^3.3.1", 47 | "webpack": "^5.70.0", 48 | "webpack-cli": "^4.9.2", 49 | "webpack-dev-server": "^4.7.4", 50 | "webpack-hot-middleware": "^2.25.1" 51 | }, 52 | "dependencies": { 53 | "@aws-sdk/client-cloudwatch": "^3.53.0", 54 | "@aws-sdk/client-cloudwatch-logs": "^3.53.0", 55 | "@aws-sdk/client-lambda": "^3.53.0", 56 | "@aws-sdk/client-sts": "^3.54.1", 57 | "@babel/core": "^7.17.5", 58 | "@babel/plugin-transform-async-to-generator": "^7.16.8", 59 | "@babel/plugin-transform-runtime": "^7.17.0", 60 | "@babel/preset-env": "^7.16.11", 61 | "@babel/preset-react": "^7.16.7", 62 | "@babel/runtime": "^7.17.8", 63 | "@devexpress/dx-react-chart": "^3.0.2", 64 | "@devexpress/dx-react-chart-material-ui": "^3.0.2", 65 | "@devexpress/dx-react-core": "^3.0.2", 66 | "@emotion/react": "^11.8.2", 67 | "@emotion/styled": "^11.8.1", 68 | "@material-ui/core": "^4.12.3", 69 | "@mui/icons-material": "^5.5.1", 70 | "@mui/material": "^5.5.1", 71 | "@reduxjs/toolkit": "^1.8.0", 72 | "@testing-library/jest-dom": "^5.16.2", 73 | "babel-loader": "^8.2.3", 74 | "bcryptjs": "^2.4.3", 75 | "body-parser": "^1.19.2", 76 | "chart.js": "^3.7.1", 77 | "cookie-parser": "^1.4.6", 78 | "cors": "^2.8.5", 79 | "cross-env": "^7.0.3", 80 | "css-loader": "^6.7.0", 81 | "dotenv": "^16.0.0", 82 | "eslint": "^8.10.0", 83 | "express": "^4.17.3", 84 | "express-session": "^1.17.2", 85 | "html-webpack-plugin": "^5.5.0", 86 | "jest": "^27.5.1", 87 | "jest-environment-jsdom": "^27.5.1", 88 | "jest-puppeteer": "^6.1.0", 89 | "moment": "^2.29.1", 90 | "nodemon": "^2.0.15", 91 | "pg": "^8.7.3", 92 | "puppeteer": "^13.5.1", 93 | "react": "^17.0.2", 94 | "react-chartjs-2": "^4.0.1", 95 | "react-dom": "^17.0.2", 96 | "react-hook-form": "^7.27.1", 97 | "react-redux": "^7.2.6", 98 | "react-router-dom": "^6.2.2", 99 | "regenerator-runtime": "^0.13.9", 100 | "sass": "^1.49.9", 101 | "sass-loader": "^12.6.0", 102 | "style-loader": "^3.3.1", 103 | "supertest": "^6.2.2", 104 | "webpack": "^5.70.0", 105 | "webpack-cli": "^4.9.2", 106 | "webpack-dev-server": "^4.7.4", 107 | "webpack-hot-middleware": "^2.25.1" 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /public/account-totals.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ASTRO/89272ae4fba75ab97920a4b70ba271146e77e2c4/public/account-totals.gif -------------------------------------------------------------------------------- /public/astro-banner.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ASTRO/89272ae4fba75ab97920a4b70ba271146e77e2c4/public/astro-banner.jpeg -------------------------------------------------------------------------------- /public/creds-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ASTRO/89272ae4fba75ab97920a4b70ba271146e77e2c4/public/creds-demo.gif -------------------------------------------------------------------------------- /public/function-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ASTRO/89272ae4fba75ab97920a4b70ba271146e77e2c4/public/function-demo.gif -------------------------------------------------------------------------------- /server/controllers/aws/Logs/getLogs.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment'); 2 | 3 | const { 4 | CloudWatchLogsClient, 5 | FilterLogEventsCommand, 6 | DescribeLogStreamsCommand, 7 | } = require('@aws-sdk/client-cloudwatch-logs'); 8 | 9 | const getLogs = async (req, res, next) => { 10 | // append name of function to the format necessary for grabbing logs 11 | const logGroupName = '/aws/lambda/' + req.body.function; 12 | 13 | // start a new CloudWatchLogsClient connection with provided region and credentials 14 | const cwLogsClient = new CloudWatchLogsClient({ 15 | region: req.body.region, 16 | credentials: req.body.credentials, 17 | }); 18 | 19 | // StartTime and EndTime for CloudWatchLogsClient need to be in millisecond format so need to find what the provided time period equates to 20 | let StartTime; 21 | if (req.body.timePeriod === '30min') { 22 | StartTime = new Date( 23 | new Date().setMinutes(new Date().getMinutes() - 30) 24 | ).valueOf(); 25 | } else if (req.body.timePeriod === '1hr') { 26 | StartTime = new Date( 27 | new Date().setMinutes(new Date().getMinutes() - 60) 28 | ).valueOf(); 29 | } else if (req.body.timePeriod === '24hr') { 30 | StartTime = new Date( 31 | new Date().setDate(new Date().getDate() - 1) 32 | ).valueOf(); 33 | } else if (req.body.timePeriod === '7d') { 34 | StartTime = new Date( 35 | new Date().setDate(new Date().getDate() - 7) 36 | ).valueOf(); 37 | } else if (req.body.timePeriod === '14d') { 38 | StartTime = new Date( 39 | new Date().setDate(new Date().getDate() - 14) 40 | ).valueOf(); 41 | } else if (req.body.timePeriod === '30d') { 42 | StartTime = new Date( 43 | new Date().setDate(new Date().getDate() - 30) 44 | ).valueOf(); 45 | } 46 | 47 | // nextToken is a parameter specified by AWS CloudWatch for the FilterLogEventsCommand; this token is needed to fetch the next set of events 48 | // helperFunc provides a recursive way to get all the logs 49 | async function helperFunc(nextToken, data = []) { 50 | // once we run out of nextTokens, return data (base case) 51 | if (!nextToken) { 52 | return data; 53 | } 54 | const nextLogEvents = await cwLogsClient.send( 55 | // FilterLogEventsCommand is a class that lists log events from a specified log group, which can be filtered using a filter pattern, a time range, and/or the name of the log stream 56 | // by default this lists logs up to 1 megabyte of log events (~10,000 log events) but we are limiting the data to the most recent 50 log events 57 | // query will return results from LEAST recent to MOST recent 58 | new FilterLogEventsCommand({ 59 | logGroupName, 60 | endTime: new Date().valueOf(), 61 | startTime: StartTime, 62 | nextToken, 63 | // START, END, REPORT are keywords that appear at the start of the message for specific log events and our filter pattern detects only these events to be included in our logs 64 | filterPattern: '- START - END - REPORT', 65 | }) 66 | ); 67 | data.push(nextLogEvents.events); 68 | return helperFunc(nextLogEvents.nextToken, data); 69 | } 70 | 71 | try { 72 | // find the logEvents with given logGroupName and time period 73 | const logEvents = await cwLogsClient.send( 74 | new FilterLogEventsCommand({ 75 | logGroupName, 76 | endTime: new Date().valueOf(), 77 | startTime: StartTime, 78 | filterPattern: '- START - END - REPORT', 79 | }) 80 | ); 81 | 82 | // if no log events exist, return back to frontend 83 | if (!logEvents) { 84 | res.locals.functionLogs = false; 85 | return next(); 86 | } 87 | // only send back most recent 50 logs to reduce size of payload 88 | const shortenedEvents = []; 89 | 90 | // if we received a nextToken, start helperFunc to recursively parse through most recent data (meaning we grab data from the end since that is the most recent log stream) 91 | if (logEvents.nextToken) { 92 | const helperFuncResults = await helperFunc(logEvents.nextToken); 93 | 94 | // poppedEl gets the most recent log stream that currently exists in helperFunc (log streams that are even more recent will have already been added to shortenedEvents) 95 | let poppedEl; 96 | 97 | // while we still have logs to grab from the helperFunc and shortenedEvents is shorter than 50 logs, add to shortenedEvents array from the end (the most recent log stream) 98 | while ( 99 | helperFuncResults.length && 100 | shortenedEvents.length <= 50 101 | ) { 102 | // poppedEl gets the most recent log stream that currently exists in helperFunc (log streams that are even more recent will have already been added to shortenedEvents) 103 | // but the for loop below is iterating through helperFunc such that we are adding the most recent log stream at the beginning of the shortenedEvent array 104 | poppedEl = helperFuncResults.pop(); 105 | /** 106 | 107 | shortenedEvent = [ helperFuncResults = [ 108 | index 0: { most recent event log stream }, index 0: { least recent event log stream } 109 | . . 110 | . . 111 | . . 112 | index N: { least recent event log stream }, index N: { most recent event log stream } 113 | ] ] 114 | 115 | */ 116 | for (let i = poppedEl.length - 1; i >= 0; i -= 1) { 117 | // we don't want to have more than 50 logs at any point in time to reduce operational load and size 118 | if (shortenedEvents.length === 50) break; 119 | else shortenedEvents.push(poppedEl[i]); 120 | } 121 | } 122 | } 123 | /** 124 | * If a nextToken exists, we can't populate shortenedEvents with event log data without the second part of 125 | * the or clause since we want to consider the situation when there are < 50 event log streams; 126 | */ 127 | if (!logEvents.nextToken || shortenedEvents.length < 50) { 128 | // grab from the end to grab most recent logs and stop once we reach 50 to send back to frontend 129 | for (let i = logEvents.events.length - 1; i >= 0; i -= 1) { 130 | if (shortenedEvents.length === 50) break; 131 | shortenedEvents.push(logEvents.events[i]); 132 | } 133 | } 134 | 135 | // start forming what it'll look like to send back to frontend 136 | const eventLog = { 137 | name: req.body.function, 138 | timePeriod: req.body.timePeriod, 139 | }; 140 | 141 | const streams = []; 142 | 143 | // loop through logs in order to eventually add to eventLog object 144 | for (let i = 0; i < shortenedEvents.length; i += 1) { 145 | // the very first shortenedEvent element is the most recent log stream 146 | let eventObj = shortenedEvents[i]; 147 | // create the individual arrays to populate the table; note that this will represent a single row of info (log stream name + time stamp + stream message) 148 | const dataArr = []; 149 | // cut off the last five characters from the log stream name to create an identifier for this specific log stream 150 | // note that logStreamName appears before the timestamp 151 | dataArr.push('...' + eventObj.logStreamName.slice(-5)); 152 | // format('lll') creates a human readable date from the specific log stream's timestamp 153 | dataArr.push(moment(eventObj.timestamp).format('lll')); 154 | 155 | // if message is just from a normal log, remove the first 67 characters of the message as it's all just metadata/a string of timestamps and unnecessary info 156 | if ( 157 | eventObj.message.slice(0, 4) !== 'LOGS' && 158 | eventObj.message.slice(0, 9) !== 'EXTENSION' 159 | ) 160 | dataArr.push(eventObj.message.slice(67)); 161 | // messages starting with LOGS or EXTENSION represents different/pertinent info and we don't want to mutate the message like we did within the if block just above 162 | else dataArr.push(eventObj.message); 163 | // push the formatted dataArr into the outer array, streams, to make the table for our logs 164 | streams.push(dataArr); 165 | } 166 | eventLog.streams = streams; 167 | /** 168 | 169 | streams = [ 170 | index 0: [ { most recent event log stream } ], 171 | . 172 | . 173 | . 174 | index N: [ { least recent event log stream } ], 175 | ] 176 | */ 177 | 178 | // grab just the ERROR logs 179 | try { 180 | const errorEvents = await cwLogsClient.send( 181 | new FilterLogEventsCommand({ 182 | logGroupName, 183 | endTime: new Date().valueOf(), 184 | startTime: StartTime, 185 | filterPattern: 'ERROR', 186 | }) 187 | ); 188 | const errorStreams = []; 189 | // grab from the end to sort the most recent first 190 | for (let i = errorEvents.events.length - 1; i >= 0; i -= 1) { 191 | let errorObj = errorEvents.events[i]; 192 | const rowArr = []; 193 | // just cut off the last five characters for the log stream name as an identifier 194 | rowArr.push('...' + errorObj.logStreamName.slice(-5)); 195 | // format the date of the log timestamp to be more readable 196 | rowArr.push(moment(errorObj.timestamp).format('lll')); 197 | // remove the first 67 characters as it's all just metadata/a string of timestamps and unnecessary info 198 | rowArr.push(errorObj.message.slice(67)); 199 | errorStreams.push(rowArr); 200 | } 201 | eventLog.errors = errorStreams; 202 | // send entire object back to frontend 203 | res.locals.functionLogs = eventLog; 204 | return next(); 205 | } catch (err) { 206 | if (err) { 207 | console.error(err); 208 | 209 | return next(err); 210 | } 211 | } 212 | } catch (err) { 213 | if (err) console.error(err); 214 | return next(err); 215 | } 216 | }; 217 | 218 | module.exports = getLogs; 219 | -------------------------------------------------------------------------------- /server/controllers/aws/Logs/updateLogs.js: -------------------------------------------------------------------------------- 1 | const moment = require('moment'); 2 | const { 3 | CloudWatchLogsClient, 4 | FilterLogEventsCommand, 5 | DescribeLogStreamsCommand, 6 | } = require('@aws-sdk/client-cloudwatch-logs'); 7 | 8 | const updateLogs = async (req, res, next) => { 9 | const oldFunctionLogs = req.body.logs; 10 | const functionsToFetch = []; 11 | 12 | // create an array with just the names of the functions that need to be refetched 13 | for (let i = 0; i < oldFunctionLogs.length; i += 1) { 14 | if (oldFunctionLogs[i].timePeriod !== req.body.newTimePeriod) { 15 | functionsToFetch.push(oldFunctionLogs[i].name); 16 | } 17 | } 18 | 19 | // StartTime and EndTime for CloudWatchLogsClient need to be in millisecond format so need to find what the provided time period equates to 20 | let StartTime; 21 | if (req.body.newTimePeriod === '30min') { 22 | StartTime = new Date( 23 | new Date().setMinutes(new Date().getMinutes() - 30) 24 | ).valueOf(); 25 | } else if (req.body.newTimePeriod === '1hr') { 26 | StartTime = new Date( 27 | new Date().setMinutes(new Date().getMinutes() - 60) 28 | ).valueOf(); 29 | } else if (req.body.newTimePeriod === '24hr') { 30 | StartTime = new Date( 31 | new Date().setDate(new Date().getDate() - 1) 32 | ).valueOf(); 33 | } else if (req.body.newTimePeriod === '7d') { 34 | StartTime = new Date( 35 | new Date().setDate(new Date().getDate() - 7) 36 | ).valueOf(); 37 | } else if (req.body.newTimePeriod === '14d') { 38 | StartTime = new Date( 39 | new Date().setDate(new Date().getDate() - 14) 40 | ).valueOf(); 41 | } else if (req.body.newTimePeriod === '30d') { 42 | StartTime = new Date( 43 | new Date().setDate(new Date().getDate() - 30) 44 | ).valueOf(); 45 | } 46 | 47 | const updatedArr = []; 48 | // loop through the function names and refetch logs for each of them using loopFunc 49 | for (let i = 0; i < functionsToFetch.length; i += 1) { 50 | const functionName = functionsToFetch[i]; 51 | const newLogObj = await loopFunc( 52 | functionName, 53 | StartTime, 54 | req.body.credentials, 55 | req.body.newTimePeriod, 56 | req.body.region 57 | ); 58 | // push individual log object onto updatedArr to be sent back to frontend 59 | updatedArr.push(newLogObj); 60 | } 61 | res.locals.updatedLogs = updatedArr; 62 | return next(); 63 | }; 64 | 65 | module.exports = updateLogs; 66 | 67 | // handles fetching for individual function 68 | const loopFunc = async ( 69 | functionName, 70 | StartTime, 71 | credentials, 72 | timePeriod, 73 | region 74 | ) => { 75 | // create new CloudWatchLogsClient 76 | const cwLogsClient = new CloudWatchLogsClient({ 77 | region, 78 | credentials: credentials, 79 | }); 80 | 81 | // if a nextToken exists (meaning there are more logs to fetch), helperFunc provides a recursive way to get all the logs 82 | async function helperFunc(nextToken, data = []) { 83 | // once we run out of nextTokens, return data 84 | if (!nextToken) { 85 | return data; 86 | } 87 | const nextLogEvents = await cwLogsClient.send( 88 | new FilterLogEventsCommand({ 89 | logGroupName: '/aws/lambda/' + functionName, 90 | endTime: new Date().valueOf(), 91 | startTime: StartTime, 92 | nextToken, 93 | filterPattern: '- START - END - REPORT', 94 | }) 95 | ); 96 | data.push(nextLogEvents.events); 97 | return helperFunc(nextLogEvents.nextToken, data); 98 | } 99 | 100 | try { 101 | // find the logEvents with given logGroupName and time period 102 | const logEvents = await cwLogsClient.send( 103 | new FilterLogEventsCommand({ 104 | logGroupName: '/aws/lambda/' + functionName, 105 | endTime: new Date().valueOf(), 106 | startTime: StartTime, 107 | filterPattern: '- START - END - REPORT', 108 | }) 109 | ); 110 | // only send back most recent 50 logs to reduce size 111 | const shortenedEvents = []; 112 | 113 | // if we received a nextToken, start helperFunc process and make sure to parse through that data in order to grab from the end 114 | if (logEvents.nextToken) { 115 | const helperFuncResults = await helperFunc(logEvents.nextToken); 116 | let poppedEl; 117 | // while we still have logs to grab from the helperFunc and shortenedEvents is shorter than 50 logs, add to it from the end (giving us the most recent first instead) 118 | while (helperFuncResults.length) { 119 | poppedEl = helperFuncResults.pop(); 120 | for (let i = poppedEl.length - 1; i >= 0; i -= 1) { 121 | if (shortenedEvents.length === 50) { 122 | break; 123 | } 124 | shortenedEvents.push(poppedEl[i]); 125 | } 126 | } 127 | } 128 | 129 | // if we didn't have a nextToken and got all logs in one request to the CloudWatchLogsClient 130 | if (!logEvents.nextToken) { 131 | // grab from the end to grab most recent logs and stop once we reach 50 to send back to frontend 132 | for (let i = logEvents.events.length - 1; i >= 0; i -= 1) { 133 | if (shortenedEvents.length === 50) break; 134 | shortenedEvents.push(logEvents.events[i]); 135 | } 136 | } 137 | 138 | // start forming what it'll look like to send back to frontend 139 | const eventLog = { 140 | name: functionName, 141 | timePeriod, 142 | }; 143 | const streams = []; 144 | 145 | // loop through logs in order to eventually add to eventLog object 146 | for (let i = 0; i < shortenedEvents.length; i += 1) { 147 | let eventObj = shortenedEvents[i]; 148 | // create the individual arrays to populate the table, this info makes up one row 149 | const dataArr = []; 150 | // just cut off the last five characters for the log stream name as an identifier 151 | dataArr.push('...' + eventObj.logStreamName.slice(-5)); 152 | // format the date of the log timestamp to be more readable 153 | dataArr.push(moment(eventObj.timestamp).format('lll')); 154 | // if message is just from a normal log, remove the first 67 characters as it's all just metadata/a string of timestamps and unnecessary info 155 | if ( 156 | eventObj.message.slice(0, 4) !== 'LOGS' && 157 | eventObj.message.slice(0, 9) !== 'EXTENSION' 158 | ) { 159 | dataArr.push(eventObj.message.slice(67)); 160 | // if the message starts with LOGS or EXTENSION, it's usually different type of info and the beginning part has to stay 161 | } else { 162 | dataArr.push(eventObj.message); 163 | } 164 | // push to the larger array to then make up the table 165 | streams.push(dataArr); 166 | } 167 | eventLog.streams = streams; 168 | 169 | // grab just the ERROR logs 170 | try { 171 | const errorEvents = await cwLogsClient.send( 172 | new FilterLogEventsCommand({ 173 | logGroupName: '/aws/lambda/' + functionName, 174 | endTime: new Date().valueOf(), 175 | startTime: StartTime, 176 | filterPattern: 'ERROR', 177 | }) 178 | ); 179 | const errorStreams = []; 180 | // grab from the end to sort the most recent first 181 | for (let i = errorEvents.events.length - 1; i >= 0; i -= 1) { 182 | let errorObj = errorEvents.events[i]; 183 | const rowArr = []; 184 | // just cut off the last five characters for the log stream name as an identifier 185 | rowArr.push('...' + errorObj.logStreamName.slice(-5)); 186 | // format the date of the log timestamp to be more readable 187 | rowArr.push(moment(errorObj.timestamp).format('lll')); 188 | // remove the first 67 characters as it's all just metadata/a string of timestamps and unnecessary info 189 | rowArr.push(errorObj.message.slice(67)); 190 | errorStreams.push(rowArr); 191 | } 192 | eventLog.errors = errorStreams; 193 | // return eventLog object to then be pushed to the array that's sent back to frontend with updated logs 194 | return eventLog; 195 | } catch (err) { 196 | if (err) { 197 | console.error(err); 198 | } 199 | } 200 | } catch (err) { 201 | console.error(err); 202 | } 203 | }; 204 | -------------------------------------------------------------------------------- /server/controllers/aws/credentials/getCreds.js: -------------------------------------------------------------------------------- 1 | const dotenv = require('dotenv'); 2 | dotenv.config(); 3 | 4 | const getCreds = (req, res, next) => { 5 | const creds = { 6 | region: process.env.AWS_REGION, 7 | credentials: { 8 | accessKeyId: process.env.AWS_ACCESS_KEY_ID, 9 | secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, 10 | } 11 | } 12 | res.locals.credentials = creds; 13 | return next(); 14 | } 15 | 16 | module.exports = getCreds; -------------------------------------------------------------------------------- /server/controllers/aws/credentials/getSTSCreds.js: -------------------------------------------------------------------------------- 1 | const STSCreds = {}; 2 | const dotenv = require('dotenv'); 3 | dotenv.config(); 4 | 5 | const { 6 | AssumeRoleCommand, 7 | STSClient, 8 | } = require('@aws-sdk/client-sts'); 9 | 10 | STSCreds.get = async (req, res, next) => { 11 | const roleParams = { 12 | RoleArn: res.locals.arn, 13 | RoleSessionName: 'AstroSession', 14 | }; 15 | 16 | const credentials = { 17 | accessKeyId: process.env.AWS_ACCESS_KEY_ID, 18 | secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, 19 | }; 20 | 21 | const region = process.env.AWS_REGION; 22 | 23 | const stsClient = await new STSClient({ 24 | credentials, 25 | region, 26 | }); 27 | 28 | try { 29 | // Class AssumeRole Command returns a temporary access key ID, secret access key, and security token to access AWS resources 30 | // these temporary security credentials can be used to access any AWS resource 31 | const assumedRole = await stsClient.send( 32 | new AssumeRoleCommand(roleParams) 33 | ); 34 | const accessKeyId = assumedRole.Credentials.AccessKeyId; 35 | const secretAccessKey = assumedRole.Credentials.SecretAccessKey; 36 | const sessionToken = assumedRole.Credentials.SessionToken; 37 | 38 | // what gets sent back to the client 39 | res.locals.STSCreds = { 40 | credentials: { 41 | accessKeyId, 42 | secretAccessKey, 43 | sessionToken, 44 | }, 45 | region, 46 | }; 47 | 48 | return next(); 49 | 50 | } catch (err) { 51 | if (err) { 52 | console.error(err); 53 | return next(err); 54 | } 55 | } 56 | }; 57 | 58 | module.exports = STSCreds; 59 | -------------------------------------------------------------------------------- /server/controllers/aws/metrics/getLambdaFuncs.js: -------------------------------------------------------------------------------- 1 | const { 2 | LambdaClient, 3 | ListFunctionsCommand, 4 | } = require('@aws-sdk/client-lambda'); 5 | 6 | //Extract Lambda Functions for the Assumed Role 7 | //***********************Begin************************ */ 8 | 9 | const getFunctions = async (req, res, next) => { 10 | // constructing a new client object (LambdaClient) to invoke service methods on AWS Lambda using the specified AWS account credentials provider, client config options, and request metric collector. 11 | // service calls made using the new client object is blocking (other code won't be allowed to run) and won't return until the service call completes 12 | const client = new LambdaClient({ 13 | // note that we do not need to specify config.region or config.credentials; aws automatically recognizes we want to set the config object's region & credentials properties to specific values 14 | region: req.body.region, 15 | credentials: req.body.credentials, 16 | }); 17 | 18 | // set FunctionVersion to 'ALL' to include all published/unpublished versions of each function 19 | const lamParams = { FunctionVersion: 'ALL' }; 20 | 21 | try { 22 | // calling send operation on client with lamParams object as input 23 | const listOfLambdaFuncs = await client.send( 24 | // ListFunctionsCommand is a class that returns a list of Lambda functions (50 max) with version-specific configuration of each one 25 | new ListFunctionsCommand(lamParams) 26 | ); 27 | 28 | const funcNames = listOfLambdaFuncs.Functions.map(el => el.FunctionName); 29 | res.locals.functions = funcNames; 30 | 31 | return next(); 32 | } catch (err) { 33 | console.error('Error in Lambda List Functions: ', err); 34 | return next(err); 35 | } 36 | }; 37 | //***********************End************************ */ 38 | module.exports = getFunctions; -------------------------------------------------------------------------------- /server/controllers/aws/metrics/getMetricsAllFuncs.js: -------------------------------------------------------------------------------- 1 | const AWSUtilFunc = require('./utils/AWSUtilFunc.js'); 2 | const { 3 | CloudWatchClient, 4 | GetMetricDataCommand, 5 | } = require('@aws-sdk/client-cloudwatch'); 6 | 7 | //Extract the CloudWatch Metrics for the Lambda Functions 8 | //***********************Begin************************ */ 9 | 10 | const getMetricsAllFunc = async (req, res, next) => { 11 | 12 | const cwClient = new CloudWatchClient({ 13 | region: req.body.region, 14 | credentials: req.body.credentials, 15 | }); 16 | 17 | //initialize the variables for creating the inputs for AWS request 18 | let graphPeriod, graphUnits, graphMetricName, graphMetricStat; 19 | 20 | graphMetricName = req.params.metricName; 21 | 22 | if (req.body.timePeriod === '30min') { 23 | [graphPeriod, graphUnits] = [30, 'minutes']; 24 | } else if (req.body.timePeriod === '1hr') { 25 | [graphPeriod, graphUnits] = [60, 'minutes']; 26 | } else if (req.body.timePeriod === '24hr') { 27 | [graphPeriod, graphUnits] = [24, 'hours']; 28 | } else if (req.body.timePeriod === '7d') { 29 | [graphPeriod, graphUnits] = [7, 'days']; 30 | } else if (req.body.timePeriod === '14d') { 31 | [graphPeriod, graphUnits] = [14, 'days']; 32 | } else if (req.body.timePeriod === '30d') { 33 | [graphPeriod, graphUnits] = [30, 'days']; 34 | } 35 | 36 | if (!req.body.metricStat) graphMetricStat = 'Sum'; 37 | else graphMetricStat = req.body.metricStat; 38 | 39 | // Metrics for All Functions (combined) 40 | // Prepare the input parameters for the AWS getMetricsData API Query 41 | const metricAllFuncInputParams = AWSUtilFunc.prepCwMetricQueryLambdaAllFunc( 42 | graphPeriod, 43 | graphUnits, 44 | graphMetricName, 45 | graphMetricStat 46 | ); 47 | 48 | try { 49 | const metricAllFuncResult = await cwClient.send( 50 | new GetMetricDataCommand(metricAllFuncInputParams) 51 | ); 52 | 53 | //Format of the MetricDataResults 54 | //******************************* */ 55 | // "MetricDataResults": [ 56 | // { 57 | // "Id": "m0", 58 | // "Label": "Lambda Invocations CryptoRefreshProfits", 59 | // "Timestamps": [ 60 | // "2021-07-17T02:54:00.000Z", 61 | // "2021-07-17T01:54:00.000Z" 62 | // ], 63 | // "Values": [ 64 | // 1400, 65 | // 34 66 | // ], 67 | // "StatusCode": "Complete", 68 | // "Messages": [] 69 | // }, 70 | // ] 71 | //******************************* */ 72 | 73 | const metricAllFuncData = 74 | metricAllFuncResult.MetricDataResults[0].Timestamps.map( 75 | (timeStamp, index) => { 76 | return { 77 | x: timeStamp, 78 | y: metricAllFuncResult.MetricDataResults[0].Values[index], 79 | }; 80 | } 81 | ); 82 | const metricMaxValue = Math.max( 83 | ...metricAllFuncResult.MetricDataResults[0].Values, 84 | 0 85 | ); 86 | 87 | //Request response JSON Object send to the FrontEnd 88 | 89 | res.locals.metricAllFuncData = { 90 | title: metricAllFuncResult.MetricDataResults[0].Label, 91 | data: metricAllFuncData.reverse(), 92 | options: { 93 | startTime: metricAllFuncInputParams.StartTime, 94 | endTime: metricAllFuncInputParams.EndTime, 95 | graphPeriod, 96 | graphUnits, 97 | metricMaxValue, 98 | }, 99 | }; 100 | 101 | return next(); 102 | } catch (err) { 103 | console.error('Error in CW getMetricsData All Functions', err); 104 | } 105 | }; 106 | 107 | module.exports = getMetricsAllFunc; 108 | -------------------------------------------------------------------------------- /server/controllers/aws/metrics/getMetricsByFunc.js: -------------------------------------------------------------------------------- 1 | const AWSUtilFunc = require('./utils/AWSUtilFunc.js'); 2 | const { 3 | CloudWatchClient, 4 | GetMetricDataCommand, 5 | } = require('@aws-sdk/client-cloudwatch'); 6 | 7 | //Extract the CloudWatch Metrics for the Lambda Functions 8 | //***********************Begin************************ */ 9 | 10 | const getMetricsByFunc = async (req, res, next) => { 11 | const cwClient = new CloudWatchClient({ 12 | region: req.body.region, 13 | credentials: req.body.credentials, 14 | }); 15 | 16 | //initialize the variables for creating the inputs for AWS request 17 | let graphPeriod, graphUnits, graphMetricName, funcNames, graphMetricStat; 18 | 19 | funcNames = res.locals.functions; 20 | 21 | graphMetricName = req.params.metricName; 22 | 23 | if (req.body.timePeriod === '30min') { 24 | [graphPeriod, graphUnits] = [30, 'minutes']; 25 | } else if (req.body.timePeriod === '1hr') { 26 | [graphPeriod, graphUnits] = [60, 'minutes']; 27 | } else if (req.body.timePeriod === '24hr') { 28 | [graphPeriod, graphUnits] = [24, 'hours']; 29 | } else if (req.body.timePeriod === '7d') { 30 | [graphPeriod, graphUnits] = [7, 'days']; 31 | } else if (req.body.timePeriod === '14d') { 32 | [graphPeriod, graphUnits] = [14, 'days']; 33 | } else if (req.body.timePeriod === '30d') { 34 | [graphPeriod, graphUnits] = [30, 'days']; 35 | } 36 | 37 | if (!req.body.metricStat) graphMetricStat = 'Sum'; 38 | else graphMetricStat = req.body.metricStat; 39 | 40 | //Metrics for By Lambda Function 41 | //Prepare the input parameters for the AWS getMetricsData API Query 42 | 43 | const metricByFuncInputParams = AWSUtilFunc.prepCwMetricQueryLambdaByFunc( 44 | graphPeriod, 45 | graphUnits, 46 | graphMetricName, 47 | graphMetricStat, 48 | funcNames 49 | ); 50 | 51 | try { 52 | const metricByFuncResult = await cwClient.send( 53 | new GetMetricDataCommand(metricByFuncInputParams) 54 | ); 55 | 56 | //Format of the MetricDataResults 57 | //******************************* */ 58 | // "MetricDataResults": [ 59 | // { 60 | // "Id": "m0", 61 | // "Label": "Lambda Invocations CryptoRefreshProfits", 62 | // "Timestamps": [ 63 | // "2021-07-22T21:00:00.000Z", 64 | // "2021-07-22T20:00:00.000Z", 65 | // "2021-07-22T00:00:00.000Z" 66 | // ], 67 | // "Values": [ 68 | // 19, 69 | // 6, 70 | // 5 71 | // ], 72 | // "StatusCode": "Complete", 73 | // "Messages": [] 74 | // }, 75 | // { 76 | // "Id": "m1", 77 | // "Label": "Lambda Invocations RequestUnicorn2", 78 | // "Timestamps": [], 79 | // "Values": [], 80 | // "StatusCode": "Complete", 81 | // "Messages": [] 82 | // }, 83 | // { 84 | // "Id": "m2", 85 | // "Label": "Lambda Invocations CryptoLogin", 86 | // "Timestamps": [ 87 | // "2021-07-23T15:00:00.000Z", 88 | // "2021-07-22T21:00:00.000Z", 89 | // "2021-07-22T20:00:00.000Z", 90 | // "2021-07-22T00:00:00.000Z", 91 | // "2021-07-19T13:00:00.000Z", 92 | // "2021-07-18T02:00:00.000Z" 93 | // ], 94 | // "Values": [ 95 | // 1, 96 | // 1, 97 | // 3, 98 | // 1, 99 | // 1, 100 | // 3 101 | // ], 102 | // "StatusCode": "Complete", 103 | // "Messages": [] 104 | // }, 105 | // ] 106 | //******************************* */ 107 | 108 | const metricByFuncData = metricByFuncResult.MetricDataResults.map( 109 | (metricDataResult) => { 110 | const metricName = metricDataResult.Label; 111 | const timeStamps = metricDataResult.Timestamps.reverse(); 112 | const values = metricDataResult.Values.reverse(); 113 | const metricData = timeStamps.map((timeStamp, index) => { 114 | return { 115 | x: timeStamp, 116 | y: values[index], 117 | }; 118 | }); 119 | 120 | const maxValue = Math.max(0, Math.max(...values)); 121 | const total = values.reduce((accum, curr) => accum + curr, 0); 122 | 123 | return { 124 | name: metricName, 125 | data: metricData, 126 | maxValue: maxValue, 127 | total: total, 128 | }; 129 | } 130 | ); 131 | 132 | const metricMaxValueAllFunc = metricByFuncData.reduce( 133 | (maxValue, dataByFunc) => { 134 | return Math.max(maxValue, dataByFunc.maxValue); 135 | }, 136 | 0 137 | ); 138 | 139 | //Request response JSON Object send to the FrontEnd 140 | 141 | res.locals.metricByFuncData = { 142 | title: `Lambda ${graphMetricName}`, 143 | series: metricByFuncData, 144 | options: { 145 | startTime: metricByFuncInputParams.StartTime, 146 | endTime: metricByFuncInputParams.EndTime, 147 | graphPeriod, 148 | graphUnits, 149 | metricMaxValueAllFunc, 150 | funcNames: funcNames, 151 | }, 152 | }; 153 | 154 | return next(); 155 | } catch (err) { 156 | console.error('Error in CW getMetricsData By Functions', err); 157 | } 158 | }; 159 | 160 | module.exports = getMetricsByFunc; 161 | -------------------------------------------------------------------------------- /server/controllers/aws/metrics/utils/AWSUtilFunc.js: -------------------------------------------------------------------------------- 1 | //The following assumes that User inputs the time timeRange (either in min, hours, days) 2 | const moment = require('moment'); 3 | 4 | /* 5 | input time range period for aggregating the metrics 6 | 7 | e.g. if the time range selected on the frontend is minutes, metrics from CloudWatch will be 8 | aggregated by 1 minute (60 seconds) 9 | */ 10 | 11 | const timeRangePeriod = { 12 | minutes: 60, // 1 min 13 | hours: 300, // 5 mins 14 | days: 3600, // 1 hour 15 | }; 16 | 17 | // routing parameters for defining the EndTime 18 | 19 | const roundTimeMultiplier = { 20 | minutes: 5, // the EndTime time stamps will be rounded to nearest 5 minutes 21 | hours: 15, // rounded to nearest 15 minutes 22 | days: 60, // rounded to nearest hour 23 | }; 24 | 25 | // routing parameters to compute the startTime 26 | 27 | const timeRangeMultiplier = { 28 | minutes: 60, // the EndTime time stamps will be rounded to nearest 5 minutes 29 | hours: 3600, // 3600 seconds in an hour 30 | days: 86400, // 86400 seconds in a day 31 | }; 32 | 33 | const AWSUtilFunc = {}; 34 | 35 | AWSUtilFunc.prepCwMetricQueryLambdaAllFunc = ( 36 | timeRangeNum, 37 | timeRangeUnits, 38 | metricName, 39 | metricStat 40 | ) => { 41 | // roundTime will round to the nearest 5 minutes, 15 minutes for hours, and nearest hour for days 42 | const roundTime = roundTimeMultiplier[timeRangeUnits]; 43 | 44 | /* 45 | define the End and Start times in UNIX time Stamp format (milliseconds) for getMetricsData method 46 | Current time in Unix TimeStamp is # of milliseconds since UTC January 1, 1970 (Unix Epoch) 47 | Unix epoch useful bc it allows computers track and sort dated info in dynamic and distributed apps both online and client side 48 | */ 49 | const EndTime = Math.round( new Date().getTime() / 1000 / 60 / roundTime ) * 60 * roundTime; 50 | //end time: 1648494000 51 | const StartTime = EndTime - ( timeRangeNum * timeRangeMultiplier[timeRangeUnits] ); 52 | const period = timeRangePeriod[timeRangeUnits]; 53 | 54 | // initialize the parameters 55 | const metricParamsBaseAllFunc = { 56 | StartTime: new Date(StartTime * 1000), 57 | EndTime: new Date(EndTime * 1000), 58 | LabelOptions: { 59 | // -0400 represents 4 hours and 0 minutes behind UTC 60 | Timezone: '-0400', 61 | }, 62 | // MetricDataQueries: [], 63 | }; 64 | 65 | const metricDataQueryAllfunc = [ 66 | { 67 | Id: `m${metricName}_AllLambdaFunc`, 68 | Label: `Lambda ${metricName} All Functions`, 69 | MetricStat: { 70 | Metric: { 71 | Namespace: 'AWS/Lambda', 72 | MetricName: `${metricName}`, 73 | }, 74 | Period: period, 75 | Stat: metricStat, 76 | }, 77 | }, 78 | ]; 79 | 80 | const metricParamsAllfunc = { 81 | ...metricParamsBaseAllFunc, 82 | MetricDataQueries: metricDataQueryAllfunc, 83 | }; 84 | 85 | return metricParamsAllfunc; 86 | }; 87 | 88 | AWSUtilFunc.prepCwMetricQueryLambdaByFunc = ( 89 | timeRangeNum, 90 | timeRangeUnits, 91 | metricName, 92 | metricStat, 93 | funcNames 94 | ) => { 95 | const roundTime = roundTimeMultiplier[timeRangeUnits]; 96 | //define the End and Start times in UNIX time Stamp format for getMetricsData method 97 | //Rounded off to nearest roundTimeMultiplier 98 | const EndTime = 99 | Math.round(new Date().getTime() / 1000 / 60 / roundTime) * 60 * roundTime; //current time in Unix TimeStamp 100 | const StartTime = 101 | EndTime - timeRangeNum * timeRangeMultiplier[timeRangeUnits]; 102 | 103 | const period = timeRangePeriod[timeRangeUnits]; 104 | 105 | //initialize the parameters 106 | const metricParamsBaseByFunc = { 107 | StartTime: new Date(StartTime * 1000), 108 | EndTime: new Date(EndTime * 1000), 109 | LabelOptions: { 110 | Timezone: '-0400', 111 | }, 112 | // MetricDataQueries: [], 113 | }; 114 | 115 | const metricDataQueryByFunc = []; 116 | 117 | funcNames.forEach((func, index) => { 118 | const metricDataQuery = { 119 | Id: `m${index}`, 120 | Label: `Lambda ${metricName} ${func}`, 121 | MetricStat: { 122 | Metric: { 123 | Namespace: `AWS/Lambda`, 124 | MetricName: `${metricName}`, 125 | Dimensions: [ 126 | { 127 | Name: `FunctionName`, 128 | Value: `${func}`, 129 | }, 130 | ], 131 | }, 132 | Period: period, 133 | Stat: metricStat, 134 | }, 135 | }; 136 | 137 | metricDataQueryByFunc.push(metricDataQuery); 138 | }); 139 | 140 | const metricParamsByFunc = { 141 | ...metricParamsBaseByFunc, 142 | MetricDataQueries: metricDataQueryByFunc, 143 | }; 144 | return metricParamsByFunc; 145 | }; 146 | 147 | // export default AWSUtilFunc; 148 | 149 | module.exports = AWSUtilFunc; 150 | -------------------------------------------------------------------------------- /server/controllers/userController.js: -------------------------------------------------------------------------------- 1 | const pool = require('../db.js'); 2 | const userController = {}; 3 | 4 | userController.createUser = async (req, res, next) => { 5 | const { firstName, lastName, email, password, arn } = req.body; 6 | 7 | const sqlQuery = `INSERT INTO Users (firstName, lastName, email, password, arn) 8 | VALUES ($1, $2, $3, $4, $5)`; 9 | 10 | const values = [firstName, lastName, email, password, arn]; 11 | 12 | try { 13 | const result = await pool.query(sqlQuery, values); 14 | res.locals.arn = arn; 15 | return next(); 16 | } catch (e) { 17 | return next(e); 18 | } 19 | }; 20 | 21 | userController.getUser = async (req, res, next) => { 22 | const { email, password } = req.body; 23 | 24 | const sqlQuery = `Select * FROM Users WHERE email=$1`; 25 | 26 | const values = [email]; 27 | 28 | try { 29 | const result = await pool.query(sqlQuery, values); 30 | console.log('result from userController.getUser: ', result); 31 | 32 | if (result.rows[0].password !== password) return next('ERROR: incorrect email or password'); 33 | 34 | res.locals.arn = result.rows[0].arn; 35 | 36 | return next(); 37 | 38 | } catch (e) { 39 | return next(e); 40 | } 41 | }; 42 | 43 | /* 44 | Succesful database query returns the following information : 45 | rows: [ 46 | { 47 | _id: 1, 48 | firstname: 'Tony', 49 | lastname: 'Carrasco', 50 | email: 'email', 51 | password: 'password', 52 | arn: 'arn', 53 | region: 'region' 54 | } 55 | ], 56 | */ 57 | 58 | module.exports = userController; 59 | -------------------------------------------------------------------------------- /server/db.js: -------------------------------------------------------------------------------- 1 | const { Pool, Client } = require('pg'); 2 | 3 | const myURI = 'postgres://ynlsirdy:x-NmJXBlDSEQgTNPru_raQksrAxrseH_@salt.db.elephantsql.com/ynlsirdy'; 4 | 5 | const pool = new Pool({ 6 | connectionString: myURI, 7 | }); 8 | 9 | module.exports = { 10 | query: (text, params, callback) => { 11 | console.log('executed query', text); 12 | return pool.query(text, params, callback); 13 | } 14 | }; -------------------------------------------------------------------------------- /server/routers/aws.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | 4 | //AWS middleware 5 | const getCreds = require('../controllers/aws/credentials/getCreds'); 6 | const getLambdaFunctions = require('../controllers/aws/metrics/getLambdaFuncs'); 7 | const getMetricsAllFunc = require('../controllers/aws/metrics/getMetricsAllFuncs'); 8 | const getMetricsByFunc = require('../controllers/aws/metrics/getMetricsByFunc'); 9 | const getLogs = require('../controllers/aws/Logs/getLogs'); 10 | const updateLogs = require('../controllers/aws/Logs/updateLogs'); 11 | 12 | // only used when credentials are inputted into .env file 13 | router.route('/getCreds').get(getCreds, (req,res) => { 14 | return res.status(200).json(res.locals.credentials); 15 | }) 16 | 17 | // Returning all Lambda funcs for an account 18 | router.route('/getLambdaFunctions').post(getLambdaFunctions, (req, res) => { 19 | return res.status(200).json(res.locals.functions); 20 | }); 21 | 22 | // Returning specified metric for all Lambda funcs 23 | // http://localhost:1111/aws/getMetricsAllFunc/:metricName 24 | router 25 | .route('/getMetricsAllFunc/:metricName') 26 | .post(getMetricsAllFunc, (req, res) => { 27 | return res.status(200).json(res.locals.metricAllFuncData) 28 | }); 29 | 30 | // Return metric for specified func 31 | router 32 | .route('/getMetricsByFunc/:metricName') 33 | .post(getLambdaFunctions, getMetricsByFunc, (req, res) => { 34 | return res.status(200).json(res.locals.metricByFuncData); 35 | }); 36 | 37 | // Returning Lambda Functions Logs 38 | router 39 | .route('/getLogs') 40 | .post(getLogs, (req, res) => { 41 | return res.status(200).json(res.locals.functionLogs); 42 | }); 43 | 44 | // Updating Lambda Function Logs 45 | router 46 | .route('/updateLogs') 47 | .post(updateLogs, (req, res) => { 48 | return res.status(200).json(res.locals.updatedLogs); 49 | }); 50 | 51 | module.exports = router; -------------------------------------------------------------------------------- /server/routers/userRouter.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const userController = require('../controllers/userController.js'); 4 | const STSCreds = require('../controllers/aws/credentials/getSTSCreds'); 5 | 6 | 7 | router.route('/login').post( 8 | userController.getUser, 9 | STSCreds.get, 10 | (req, res) => { 11 | return res.status(200).json(res.locals.STSCreds); 12 | } 13 | ) 14 | 15 | router.route('/register').post( 16 | userController.createUser, 17 | STSCreds.get, 18 | (req, res) => { 19 | return res.status(200).json(res.locals.STSCreds); 20 | } 21 | ) 22 | 23 | module.exports = router; -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const cors = require('cors'); 3 | const path = require('path'); 4 | const PORT = 1111; 5 | 6 | const app = express(); 7 | app.use(express.json()); 8 | app.use(express.urlencoded({ extended: true })); 9 | app.use(express.static(path.join(__dirname, '../dist'))); 10 | app.use(cors()); 11 | 12 | const userRouter = require('./routers/userRouter.js'); 13 | const awsRouter = require('./routers/aws.js'); 14 | 15 | app.use('/aws', awsRouter); 16 | app.use('/user', userRouter); 17 | 18 | app.use('*', (req, res) => { 19 | return res 20 | .status(404) 21 | .json({ err: 'endpoint requested is not found' }); 22 | }); 23 | 24 | app.use((err, req, res, next) => { 25 | const defaultErr = { 26 | log: `Express error handler caught unknown middleware error ${err}`, 27 | status: 500, 28 | message: { 29 | err: 'An error occurred. Please contact the Astro team.', 30 | }, 31 | }; 32 | 33 | const errorObj = Object.assign({}, defaultErr, err); 34 | console.log(errorObj.log); 35 | return res.status(errorObj.status).json(errorObj.message); 36 | }); 37 | 38 | const server = app.listen(PORT, () => { 39 | console.log('Listening on port ' + PORT); 40 | }); 41 | 42 | module.exports = server; 43 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useSelector, useDispatch } from 'react-redux'; 3 | import { Navigation } from './pages/Navigation' 4 | import { getCreds } from './utils/getAWSCreds'; 5 | import { getBackendCreds } from './features/slices/credSlice'; 6 | 7 | //MATERIAL UI// 8 | import CircularProgress from '@mui/material/CircularProgress'; 9 | import CssBaseline from "@mui/material/CssBaseline"; 10 | import { ThemeProvider, createTheme } from "@mui/material/styles"; 11 | 12 | const themeLight = createTheme({ 13 | palette: { 14 | background: { 15 | default: "#eeeeee" 16 | } 17 | } 18 | }); 19 | 20 | 21 | function App() { 22 | 23 | const creds = useSelector((state) => state.creds) 24 | const dispatch = useDispatch(); 25 | 26 | useEffect( () => { 27 | Promise.resolve(getCreds()) 28 | .then((data) => { 29 | dispatch(getBackendCreds(data)) 30 | return; 31 | }) 32 | .catch(err => console.log(err)) 33 | }, []) 34 | 35 | 36 | return ( 37 | 38 | creds.region.length ? 39 | <> 40 | 41 | 42 | 43 | 44 | : 45 | 46 | 47 | ) 48 | } 49 | 50 | export default App; 51 | -------------------------------------------------------------------------------- /src/components/AccountTotals.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | import { metricsAllFunc } from '../utils/getMetricsAllFunc'; 4 | 5 | ///STYLING - MATERIAL UI && CHART.JS/// 6 | import Alert from '@mui/material/Alert'; 7 | import AlertTitle from '@mui/material/AlertTitle'; 8 | import Stack from '@mui/material/Stack'; 9 | import Box from '@mui/material/Box'; 10 | import Container from '@mui/material/Container'; 11 | import Typography from '@mui/material/Typography'; 12 | import Card from '@mui/material/Card'; 13 | import CardContent from '@mui/material/CardContent'; 14 | import { CardActionArea } from '@mui/material'; 15 | import Paper from '@mui/material/Paper'; 16 | import CircularProgress from '@mui/material/CircularProgress'; 17 | import { Doughnut } from "react-chartjs-2"; 18 | import 'chart.js/auto'; 19 | 20 | 21 | export const AccountTotals = () => { 22 | 23 | const creds = useSelector((state) => state.creds) 24 | const chartData = useSelector((state) => state.data); 25 | const list = useSelector((state) => state.funcList.funcList); 26 | 27 | const [totalInvocations, setInvocations] = useState(0); 28 | const [totalThrottles, setThrottles] = useState(0); 29 | const [totalErrors, setErrors] = useState(0); 30 | const [pieChartInvocations, setPCI] = useState([]); 31 | const [pieChartErrors, setPCE] = useState([]); 32 | const [pieChartThrottles, setPCT] = useState([]) 33 | 34 | /* 35 | Helper function that is called on load - retrieves the data needed to sum metric totals and store it in local state 36 | */ 37 | const promise = (metric, setter) => { 38 | Promise.resolve(metricsAllFunc(creds, metric)) 39 | .then(data => data.data.reduce((x, y) => x + y.y, 0)) 40 | .then(data => setter(data)) 41 | .catch(e => console.log(e)) 42 | } 43 | 44 | /* 45 | Helper function to create customized formatted chart.js data based on function metric 46 | */ 47 | const pieChartData = (funcNames, metric) =>{ 48 | return { 49 | labels: [...funcNames], 50 | datasets: [ 51 | { 52 | data: metric, 53 | backgroundColor: [ 54 | "#64b5f6", 55 | "#9575cd", 56 | "#26a69a", 57 | "rgb(122,231,125)", 58 | "rgb(195,233,151)" 59 | ], 60 | hoverBackgroundColor: ["#1565c0", "#6200ea", "#004d40"] 61 | } 62 | ], 63 | 64 | plugins: { 65 | labels: { 66 | render: "percentage", 67 | fontColor: ["green", "white", "red"], 68 | precision: 2 69 | }, 70 | }, 71 | text: "23%", 72 | }; 73 | } 74 | 75 | useEffect(() => { 76 | 77 | if (creds.region.length) { 78 | promise('Invocations', setInvocations); 79 | promise('Throttles', setThrottles); 80 | promise('Errors', setErrors); 81 | } 82 | if (chartData.data.invocations && chartData.data.errors && chartData.data.throttles) { 83 | const chartInvocations = []; 84 | for (let i = 0; i < chartData.data.invocations.length; i++) { 85 | chartInvocations.push(chartData.data.invocations[i].total); 86 | } 87 | setPCI(chartInvocations); 88 | 89 | const chartErrors = []; 90 | for (let i = 0; i < chartData.data.errors.length; i++) { 91 | chartErrors.push(chartData.data.errors[i].total); 92 | } 93 | setPCE(chartErrors); 94 | 95 | const chartThrottles = []; 96 | for (let i = 0; i < chartData.data.throttles.length; i++) { 97 | chartThrottles.push(chartData.data.throttles[i].total); 98 | } 99 | setPCT(chartThrottles); 100 | 101 | } 102 | } , [creds, chartData]) 103 | 104 | 105 | 106 | return ( 107 | 108 | chartData ? 109 | 110 | 113 | 114 | 122 |

Account Totals

123 |
124 | 125 | 126 | 127 | 135 | 136 | 137 | 138 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | {/* INVOCATIONS CARD */} 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | Invocations 161 | {totalInvocations} 162 | 163 | 164 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | Invocations are the number of times a function was invoked by 190 | either an API call or an event response from another AWS 191 | service. 192 | 193 | 194 | 195 | 196 | 197 | {/* ERRORS CARD */} 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | Errors 206 | {totalErrors} 207 | 208 | 209 | 228 | 229 | 230 | 231 | 232 | 233 | Errors log the number of errors thrown by a function. It can 234 | be used with the Invocations metric to calculate the total 235 | percentage of errors. 236 | 237 | 238 | 239 | 240 | 241 | 242 | {/* THROTTLES CARD */} 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | Throttles 251 | {totalThrottles} 252 | 253 | 254 | 273 | 274 | 275 | 276 | 277 | 278 | Throttles occur when the number of invocations for a function 279 | exceeds its concurrency pool, which causes Lambda to start 280 | rejecting incoming requests. 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 |
289 | 290 | : 291 | 292 | 293 | 294 | ); 295 | } 296 | -------------------------------------------------------------------------------- /src/components/Dashboard.jsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment, useEffect } from 'react'; 2 | import { useSelector, useDispatch } from 'react-redux'; 3 | import { LineChart } from '../components/LineChart.jsx'; 4 | import { TotalsByFunc } from '../components/TotalsByFunc.jsx'; 5 | import { metricsByFunc } from '../utils/getMetricsByFunc'; 6 | import { invocationsChange, errorsChange, throttlesChange } from '../features/slices/dataSlice.js'; 7 | import { TimePeriod } from './TimePeriod' 8 | 9 | //Material UI Components// 10 | import Grid from '@mui/material/Grid'; 11 | 12 | 13 | export const Dashboard = () => { 14 | 15 | const dispatch = useDispatch(); 16 | const creds = useSelector((state) => state.creds) 17 | const timePeriod = useSelector((state) => state.time.time) 18 | const currentFunc = useSelector((state) => state.chart.name); 19 | const list = useSelector((state) => state.funcList.funcList); 20 | 21 | 22 | useEffect(() => { 23 | Promise.resolve(metricsByFunc(creds, 'Invocations', timePeriod)) 24 | .then((data) => dispatch(invocationsChange(data.series))) 25 | .catch((e) => console.log(e)); 26 | 27 | Promise.resolve(metricsByFunc(creds, 'Errors', timePeriod)) 28 | .then((data) => dispatch(errorsChange(data.series))) 29 | .catch((e) => console.log(e)); 30 | 31 | Promise.resolve(metricsByFunc(creds, 'Throttles', timePeriod)) 32 | .then((data) => dispatch(throttlesChange(data.series))) 33 | .catch((e) => console.log(e)); 34 | 35 | }, [timePeriod]) 36 | 37 | return ( 38 | 39 | 40 | 41 | 42 | 43 | 44 |

{list[currentFunc]}

45 |
46 | 47 | 48 | 49 | 50 | 51 |
52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 |
67 | 68 | ) 69 | } 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/components/LineChart.jsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from 'react'; 2 | import {useSelector} from 'react-redux'; 3 | const moment = require('moment'); 4 | 5 | //MATERIAL UI// 6 | import Paper from '@mui/material/Paper'; 7 | import { 8 | ArgumentAxis, 9 | ValueAxis, 10 | Chart, 11 | LineSeries, 12 | Legend, 13 | } from '@devexpress/dx-react-chart-material-ui'; 14 | 15 | export const LineChart = () => { 16 | 17 | //this shows the chart data that is in state - we populate state in Dashboard 18 | const chartData = useSelector((state) => state.data); 19 | //this gives you the index of the function you need 20 | const currentFunc = useSelector((state) => state.chart.name); 21 | 22 | const [data, setData] = useState([]) 23 | 24 | 25 | useEffect(() => { 26 | if (chartData.data.invocations && chartData.data.errors && chartData.data.throttles){ 27 | 28 | const yData = []; 29 | const eData = []; 30 | const tData = []; 31 | const xAxis = []; 32 | 33 | //sets the x axis 34 | for (let i = 0; i < chartData.data.invocations[currentFunc].data.length; i++) { 35 | chartData.data.invocations[currentFunc].data.forEach((element) => { 36 | let num = moment(`${element.x}`).format("MM/DD, h a "); 37 | xAxis.push(num); 38 | }) 39 | } 40 | 41 | //sets the invocation data - saved in the yData variable 42 | for (let i = 0; i < chartData.data.invocations[currentFunc].data.length; i++) { 43 | yData.push(chartData.data.invocations[currentFunc].data[i].y); 44 | } 45 | 46 | //sets the errors data - saved in the eData variable 47 | for (let i = 0; i < chartData.data.errors[currentFunc].data.length; i++) { 48 | eData.push(chartData.data.errors[currentFunc].data[i].y); 49 | } 50 | 51 | //sets the throttles data - saved in the tData variable 52 | for (let i = 0; i < chartData.data.throttles[currentFunc].data.length; i++) { 53 | tData.push(chartData.data.throttles[currentFunc].data[i].y); 54 | } 55 | 56 | const data = []; 57 | 58 | //configures data to the correct format for the MUI graph 59 | for (let i = 0; i < xAxis.length; i++) { 60 | data.push({x: xAxis[i], y: yData[i], e: eData[i], t: tData[i]}); 61 | } 62 | 63 | setData(data); 64 | 65 | } 66 | }, [chartData, currentFunc]) 67 | 68 | 69 | //A component that renders the root layout. 70 | const Root = (props) => ( 71 | 72 | ); 73 | 74 | //A component that renders the label. 75 | const Label = props => ( 76 | 77 | ); 78 | 79 | //A component that renders an item. 80 | const Item = props => ( 81 | 82 | ); 83 | 84 | 85 | return ( 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | ); 99 | }; 100 | -------------------------------------------------------------------------------- /src/components/TimePeriod.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useSelector, useDispatch } from 'react-redux'; 3 | import { timeChange } from '../features/slices/timePeriodSlice' 4 | 5 | ///MATERIAL UI/// 6 | import InputLabel from '@mui/material/InputLabel'; 7 | import MenuItem from '@mui/material/MenuItem'; 8 | import FormHelperText from '@mui/material/FormHelperText'; 9 | import FormControl from '@mui/material/FormControl'; 10 | import Select from '@mui/material/Select'; 11 | 12 | 13 | export const TimePeriod = () => { 14 | 15 | const dispatch = useDispatch(); 16 | 17 | const timePeriod = useSelector((state) => state.time.time) 18 | 19 | const handleChange = (event) => { 20 | dispatch(timeChange(event.target.value)) 21 | }; 22 | 23 | 24 | return ( 25 | 26 | 27 | 28 | Time period 29 | 30 | 49 | 50 | Choose your time period 51 | 52 | 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /src/components/TotalsByFunc.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { useSelector, useDispatch } from 'react-redux'; 3 | 4 | ///MATERIAL UI 5 | import Alert from '@mui/material/Alert'; 6 | import AlertTitle from '@mui/material/AlertTitle'; 7 | import Stack from '@mui/material/Stack'; 8 | import Typography from '@mui/material/Typography'; 9 | import { createTheme, ThemeProvider } from '@mui/material/styles'; 10 | import CardContent from '@mui/material/CardContent'; 11 | import { CardActionArea } from '@mui/material'; 12 | import Paper from '@mui/material/Paper'; 13 | 14 | 15 | export const TotalsByFunc = () => { 16 | 17 | const currentFunc = useSelector((state) => state.chart.name) 18 | const chartData = useSelector((state) => state.data); 19 | 20 | const [totalInvocations, setInvocations] = useState(0); 21 | const [totalThrottles, setThrottles] = useState(0); 22 | const [totalErrors, setErrors] = useState(0); 23 | 24 | const theme = createTheme({ 25 | typography: { 26 | fontFamily: [ 27 | "Nanum Gothic", 28 | "sans-serif" 29 | ].join(","), 30 | }, 31 | }); 32 | 33 | 34 | useEffect(() => { 35 | 36 | if (chartData.data.invocations && chartData.data.errors && chartData.data.throttles) { 37 | 38 | const invocations = chartData.data.invocations[currentFunc].data.reduce((x, y) => x + y.y, 0) 39 | setInvocations(invocations) 40 | 41 | const errors = chartData.data.errors[currentFunc].data.reduce((x, y) => x + y.y, 0) 42 | setErrors(errors) 43 | 44 | const throttles = chartData.data.throttles[currentFunc].data.reduce((x, y) => x + y.y, 0) 45 | setThrottles(throttles) 46 | 47 | } 48 | }, [currentFunc, chartData]); 49 | 50 | 51 | return ( 52 | 53 | <> 54 | 55 | 56 | 60 | 61 | {/* INVOCATIONS CARD */} 62 | 63 | 64 | 65 | 66 | Invocations 67 | {totalInvocations} 68 | 69 | 70 | 71 | 72 | 73 | {/* ERRORS CARD */} 74 | 75 | 76 | 77 | 78 | Errors 79 | {totalErrors} 80 | 81 | 82 | 83 | 84 | 85 | {/* THROTTLES CARD */} 86 | 87 | 88 | 89 | 90 | Throttles 91 | {totalThrottles} 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | ); 103 | } 104 | -------------------------------------------------------------------------------- /src/features/slices/chartSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | 3 | export const chartName = createSlice({ 4 | name: 'chartName', 5 | initialState: { 6 | name: 0 7 | }, 8 | reducers: { 9 | nameChange: (state, action) => { 10 | state.name = action.payload; 11 | } 12 | } 13 | }); 14 | 15 | 16 | export const { nameChange } = chartName.actions; 17 | export default chartName.reducer; -------------------------------------------------------------------------------- /src/features/slices/credSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; 2 | 3 | export const userCreds = createSlice({ 4 | name: 'userCreds', 5 | initialState: { 6 | region: '', 7 | credentials: { 8 | accessKeyId: '', 9 | secretAccessKey: '', 10 | } 11 | }, 12 | reducers: { 13 | getBackendCreds: (state, action) => { 14 | state.region = action.payload.region; 15 | state.credentials.accessKeyId = action.payload.credentials.accessKeyId; 16 | state.credentials.secretAccessKey = action.payload.credentials.secretAccessKey; 17 | } 18 | } 19 | }); 20 | 21 | 22 | export const { getBackendCreds } = userCreds.actions; 23 | export default userCreds.reducer; -------------------------------------------------------------------------------- /src/features/slices/dataSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | 3 | export const chartData = createSlice({ 4 | name: 'chartData', 5 | initialState: { 6 | data: { 7 | invocations: undefined, 8 | errors: undefined, 9 | throttles: undefined, 10 | }, 11 | }, 12 | reducers: { 13 | invocationsChange: (state, action) => { 14 | state.data.invocations = action.payload; 15 | }, 16 | errorsChange: (state, action) => { 17 | state.data.errors = action.payload; 18 | }, 19 | throttlesChange: (state, action) => { 20 | state.data.throttles = action.payload; 21 | }, 22 | }, 23 | }); 24 | 25 | export const { invocationsChange, errorsChange, throttlesChange } = chartData.actions; 26 | export default chartData.reducer; 27 | -------------------------------------------------------------------------------- /src/features/slices/funcListSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; 2 | 3 | export const getFuncs = createAsyncThunk( 4 | 'funcs/getFuncs', 5 | 6 | async (credentials) => { 7 | 8 | try { 9 | const data = await fetch('http://localhost:1111/aws/getLambdaFunctions', { 10 | method: "POST", 11 | headers: { 12 | 'Content-Type': 'application/json' 13 | }, 14 | body: JSON.stringify({ 15 | region: credentials.region, 16 | credentials: { 17 | accessKeyId: credentials.credentials.accessKeyId, 18 | secretAccessKey: credentials.credentials.secretAccessKey 19 | } 20 | }) 21 | }) 22 | 23 | const formattedResponse = await data.json(); 24 | return formattedResponse; 25 | } 26 | catch(e) {console.log(e)} 27 | } 28 | ) 29 | 30 | export const funcList = createSlice({ 31 | name: 'funcList', 32 | initialState: { 33 | funcList: [] 34 | }, 35 | extraReducers: { 36 | [getFuncs.fulfilled]: (state, action) => { 37 | state.funcList = action.payload; 38 | } 39 | } 40 | }); 41 | 42 | //createSlice already makes an action creator for each of the different methods inside our reducers// 43 | export const { listChange } = funcList.actions; 44 | //exporting your reducer// 45 | // do we need to export this again if we're already exporting the entire function it's part of? 46 | export default funcList.reducer; -------------------------------------------------------------------------------- /src/features/slices/insightsToggleSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | 4 | export const insightsToggle = createSlice({ 5 | name: 'insightsToggle', 6 | initialState: { 7 | toggle: 'Functions' 8 | }, 9 | reducers: { 10 | toggleChange: (state, action) => { 11 | state.toggle = action.payload; 12 | } 13 | } 14 | }) 15 | 16 | export const { toggleChange } = insightsToggle.actions 17 | export default insightsToggle.reducer; -------------------------------------------------------------------------------- /src/features/slices/timePeriodSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | 3 | 4 | export const timePeriod = createSlice({ 5 | name: 'timePeriod', 6 | initialState: { 7 | time: '7d' 8 | }, 9 | reducers: { 10 | timeChange: (state, action) => { 11 | state.time = action.payload; 12 | } 13 | } 14 | }) 15 | 16 | export const { timeChange } = timePeriod.actions 17 | export default timePeriod.reducer; -------------------------------------------------------------------------------- /src/features/slices/userSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | 3 | 4 | export const userSlice = createSlice({ 5 | name: 'user', 6 | initialState: { 7 | logged: true 8 | }, 9 | reducers: { 10 | login: (state) => { 11 | state.logged = !state.logged 12 | }, 13 | logout: (state) => { 14 | state.logged = !state.logged 15 | } 16 | } 17 | }); 18 | 19 | 20 | 21 | //createSlice already makes an action creator for each of the difference methods inside our reducers// 22 | export const { login, logout } = userSlice.actions; 23 | //exporting your reducer// 24 | export default userSlice.reducer; -------------------------------------------------------------------------------- /src/features/store.js: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import userReducer from './slices/userSlice'; 3 | import funcListReducer from './slices/funcListSlice'; 4 | import chartSliceReducer from './slices/chartSlice'; 5 | import userCredsReducer from './slices/credSlice'; 6 | import insightsToggleReducer from './slices/insightsToggleSlice'; 7 | import chartDataReducer from './slices/dataSlice'; 8 | import timePeriodReducer from './slices/timePeriodSlice' 9 | 10 | export const store = configureStore({ 11 | reducer: { 12 | //store all slices here in key/val format 13 | //each key/val is saying we want to have a state.key = userReducer func that decides how to update state when action is dispatched 14 | user: userReducer, 15 | funcList: funcListReducer, 16 | chart: chartSliceReducer, 17 | creds: userCredsReducer, 18 | toggleInsights: insightsToggleReducer, 19 | data: chartDataReducer, 20 | time: timePeriodReducer 21 | } 22 | }); 23 | 24 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ASTRO 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import App from './App.jsx'; 4 | import { Provider } from 'react-redux'; 5 | import { store } from './features/store' 6 | import '../src/pages/styles/styles.css' 7 | 8 | 9 | render( 10 | //Provider passes down the redux store to our App// 11 | 12 | 13 | 14 | 15 | 16 | ,document.getElementById('root') 17 | ); -------------------------------------------------------------------------------- /src/pages/Login.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useForm } from 'react-hook-form'; 3 | import { useState } from 'react' 4 | 5 | function Login() { 6 | 7 | const { register, handleSubmit, formState: { errors} } = useForm(); 8 | 9 | const [info, setInfo] = useState() 10 | 11 | // const handleSubmit = () => { 12 | // //does something with submit 13 | // } 14 | 15 | return ( 16 |
17 |

Login

18 |

Fill in the details below to login

19 |
{ 22 | setInfo(data) 23 | //send fetch request to verify that user account exists 24 | //if it doesnt then do something 25 | //if it does then change userSlice state to true and take person to home page 26 | })}> 27 | 28 | 34 | {errors.email?.message} 35 | 36 | 48 | {errors.password?.message} 49 | 50 | 55 | 56 |
57 |
58 | ) 59 | }; 60 | 61 | export default Login; 62 | -------------------------------------------------------------------------------- /src/pages/Navigation.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useSelector, useDispatch } from 'react-redux'; 3 | import { AccountTotals } from '../components/AccountTotals.jsx'; 4 | import { Dashboard } from '../components/Dashboard.jsx'; 5 | 6 | import { toggleChange } from '../features/slices/insightsToggleSlice'; 7 | import { nameChange } from '../features/slices/chartSlice'; 8 | import { getFuncs } from '../features/slices/funcListSlice'; 9 | 10 | //////////////////////////////////// 11 | ///////// MUI STYLING ////////////// 12 | //////////////////////////////////// 13 | 14 | import { styled, useTheme } from '@mui/material/styles'; 15 | import Box from '@mui/material/Box'; 16 | import MuiDrawer from '@mui/material/Drawer'; 17 | import MuiAppBar from '@mui/material/AppBar'; 18 | import Toolbar from '@mui/material/Toolbar'; 19 | import List from '@mui/material/List'; 20 | import CssBaseline from '@mui/material/CssBaseline'; 21 | import Divider from '@mui/material/Divider'; 22 | import IconButton from '@mui/material/IconButton'; 23 | import MenuIcon from '@mui/icons-material/Menu'; 24 | import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; 25 | import ChevronRightIcon from '@mui/icons-material/ChevronRight'; 26 | import ListItemButton from '@mui/material/ListItemButton'; 27 | import ListItemIcon from '@mui/material/ListItemIcon'; 28 | import ListItemText from '@mui/material/ListItemText'; 29 | import Button from '@mui/material/Button'; 30 | import Collapse from '@mui/material/Collapse'; 31 | import ExpandLess from '@mui/icons-material/ExpandLess'; 32 | import ExpandMore from '@mui/icons-material/ExpandMore'; 33 | import FunctionsTwoToneIcon from '@mui/icons-material/FunctionsTwoTone'; 34 | import AddBoxTwoToneIcon from '@mui/icons-material/AddBoxTwoTone'; 35 | 36 | const drawerWidth = 240; 37 | 38 | const openedMixin = (theme) => ({ 39 | width: drawerWidth, 40 | transition: theme.transitions.create('width', { 41 | easing: theme.transitions.easing.sharp, 42 | duration: theme.transitions.duration.enteringScreen, 43 | }), 44 | overflowX: 'hidden', 45 | }); 46 | 47 | const closedMixin = (theme) => ({ 48 | transition: theme.transitions.create('width', { 49 | easing: theme.transitions.easing.sharp, 50 | duration: theme.transitions.duration.leavingScreen, 51 | }), 52 | overflowX: 'hidden', 53 | width: `calc(${theme.spacing(7)} + 1px)`, 54 | [theme.breakpoints.up('sm')]: { 55 | width: `calc(${theme.spacing(8)} + 1px)`, 56 | }, 57 | }); 58 | 59 | const DrawerHeader = styled('div')(({ theme }) => ({ 60 | display: 'flex', 61 | alignItems: 'center', 62 | justifyContent: 'flex-end', 63 | padding: theme.spacing(0, 1), 64 | // necessary for content to be below app bar 65 | ...theme.mixins.toolbar, 66 | })); 67 | 68 | const AppBar = styled(MuiAppBar, { 69 | shouldForwardProp: (prop) => prop !== 'open', 70 | })(({ theme, open }) => ({ 71 | zIndex: theme.zIndex.drawer + 1, 72 | transition: theme.transitions.create(['width', 'margin'], { 73 | easing: theme.transitions.easing.sharp, 74 | duration: theme.transitions.duration.leavingScreen, 75 | }), 76 | ...(open && { 77 | marginLeft: drawerWidth, 78 | width: `calc(100% - ${drawerWidth}px)`, 79 | transition: theme.transitions.create(['width', 'margin'], { 80 | easing: theme.transitions.easing.sharp, 81 | duration: theme.transitions.duration.enteringScreen, 82 | }), 83 | }), 84 | })); 85 | 86 | const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => prop !== 'open' })( 87 | ({ theme, open }) => ({ 88 | width: drawerWidth, 89 | flexShrink: 0, 90 | whiteSpace: 'nowrap', 91 | boxSizing: 'border-box', 92 | ...(open && { 93 | ...openedMixin(theme), 94 | '& .MuiDrawer-paper': openedMixin(theme), 95 | }), 96 | ...(!open && { 97 | ...closedMixin(theme), 98 | '& .MuiDrawer-paper': closedMixin(theme), 99 | }), 100 | }), 101 | ); 102 | 103 | //////////////////////////////////////// 104 | ///////// MUI STYLING END ////////////// 105 | //////////////////////////////////////// 106 | 107 | 108 | export const Navigation = () => { 109 | 110 | const dispatch = useDispatch(); 111 | const theme = useTheme(); 112 | 113 | const componentChange = useSelector((state) => state.toggleInsights.toggle); 114 | const list = useSelector((state) => state.funcList.funcList); 115 | const creds = useSelector((state) => state.creds); 116 | 117 | useEffect(() => { 118 | dispatch(getFuncs(creds)) 119 | }, []) 120 | 121 | const [open, setOpen] = React.useState(false); 122 | const [dropDown, setDropDown] = React.useState(false); 123 | 124 | const handleDrawerOpen = () => { 125 | setOpen(true); 126 | }; 127 | 128 | const handleDrawerClose = () => { 129 | setOpen(false); 130 | }; 131 | 132 | const handleFunctionToggle = (key) => { 133 | dispatch(nameChange(key)) 134 | }; 135 | 136 | const handleDropDownComponentChange = (tab) => { 137 | dispatch(toggleChange(tab)) 138 | setDropDown(!dropDown); 139 | }; 140 | 141 | const handleComponentChange = (tab) => { 142 | dispatch(toggleChange(tab)) 143 | }; 144 | 145 | 146 | const componentSwitch = (componentName) => { 147 | switch(componentName){ 148 | case 'Account Totals': 149 | return 150 | case 'Functions': 151 | return 152 | } 153 | } 154 | 155 | 156 | return ( 157 | 158 | 159 | 160 | {/* NAVIGATION HEADER */} 161 | 162 | 163 | 164 | 174 | 175 | 176 | 177 | 184 | 185 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | {theme.direction === 'rtl' ? : } 200 | 201 | 202 | 203 | 204 | 205 | {/* NAVIGATION SIDEBAR */} 206 | 207 | 208 | 209 | handleComponentChange("Account Totals")} 211 | > 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | {handleDropDownComponentChange("Functions")}} 221 | > 222 | 223 | 224 | 225 | 226 | {dropDown ? : } 227 | 228 | 229 | 230 | 231 | 232 | {list.map((text, index) => ( 233 | handleFunctionToggle(index)}> 234 | 235 | 236 | ))} 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | {/* COMPONENT RENDERING */} 247 | 248 | 249 | 250 | 251 | 252 | {componentSwitch(componentChange)} 253 | 254 | 255 | 256 | 257 | ); 258 | } 259 | -------------------------------------------------------------------------------- /src/pages/Signup.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useForm } from 'react-hook-form'; 3 | 4 | 5 | function Signup(){ 6 | 7 | // const handleSubmit = () => { 8 | // //does something with submit 9 | // console.log('submitted') 10 | // } 11 | 12 | //register is a callback function that will return some of the props and inject into your inputs 13 | const { register, handleSubmit, formState: { errors } } = useForm(); 14 | 15 | return ( 16 |
17 |

Sign Up

18 |

Fill in the details below to create your account

19 |
{ 22 | console.log(data) 23 | })}> 24 | 25 | 31 | 32 | {errors.firstName?.message} 33 | 34 | 40 | 41 | {errors.lastName?.message} 42 | 43 | 49 | 50 | {errors.email?.message} 51 | 52 | 64 | 65 | {errors.password?.message} 66 | 67 | 73 | 74 | 86 | 87 | {errors.ARN?.message} 88 | 89 | 96 | 97 |
98 |
99 | ) 100 | }; 101 | 102 | export default Signup; -------------------------------------------------------------------------------- /src/pages/styles/styles.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | a:link, a:visited, a:active { 8 | text-decoration: none; 9 | color: white; 10 | } 11 | 12 | .navbar-astro { 13 | font-size: 2em; 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/getAWSCreds.js: -------------------------------------------------------------------------------- 1 | export const getCreds = async () => { 2 | 3 | try { 4 | const data = await fetch('http://localhost:1111/aws/getCreds'); 5 | const formattedResponse = await data.json(); 6 | 7 | return formattedResponse; 8 | } 9 | catch (e) { console.log(e) } 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/getMetricsAllFunc.js: -------------------------------------------------------------------------------- 1 | 2 | export const metricsAllFunc = async (credentials, metric) => { 3 | try { 4 | const data = await fetch(`http://localhost:1111/aws/getMetricsAllFunc/${metric}`, { 5 | method: 'POST', 6 | headers: { 7 | 'Content-Type': 'application/json' 8 | }, 9 | body: JSON.stringify({ 10 | region: credentials.region, 11 | credentials: { 12 | accessKeyId: credentials.credentials.accessKeyId, 13 | secretAccessKey: credentials.credentials.secretAccessKey 14 | }, 15 | timePeriod: "30d" 16 | }) 17 | }) 18 | // console.log('this is data in metricsAllFunc: ', await data.json()) 19 | const formattedData = await data.json(); 20 | return formattedData; 21 | } 22 | 23 | catch (e) { 24 | console.log(e) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/getMetricsByFunc.js: -------------------------------------------------------------------------------- 1 | 2 | export const metricsByFunc = async (credentials, metric, time) => { 3 | // console.log('in get metrics by func', time) 4 | try { 5 | const data = await fetch(`http://localhost:1111/aws/getMetricsByFunc/${metric}`, { 6 | method: 'POST', 7 | headers: { 8 | 'Content-Type': 'application/json' 9 | }, 10 | body: JSON.stringify({ 11 | region: credentials.region, 12 | credentials: { 13 | accessKeyId: credentials.credentials.accessKeyId, 14 | secretAccessKey: credentials.credentials.secretAccessKey 15 | }, 16 | timePeriod: time 17 | }) 18 | }) 19 | // console.log('this is data in metrics by func', data) 20 | return data.json() 21 | } 22 | 23 | catch (e) { 24 | console.log(e) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const HWP = require('html-webpack-plugin'); 2 | const path = require('path'); 3 | const webpack = require('webpack'); 4 | 5 | module.exports = { 6 | mode: process.env.NODE_ENV, 7 | entry: path.join(__dirname, '/src/index.js'), 8 | output: { 9 | filename: 'build.js', 10 | path: path.join(__dirname, '/dist') 11 | }, 12 | module:{ 13 | rules:[ 14 | { 15 | test: /\.jsx?/, 16 | exclude: /node_modules/, 17 | loader: 'babel-loader', 18 | options: { 19 | presets: ['@babel/env', '@babel/react'], 20 | plugins: ['@babel/plugin-transform-runtime', '@babel/transform-async-to-generator'], 21 | } 22 | }, 23 | { 24 | test: /\.s?css/, 25 | use: [ 26 | 'style-loader', 'css-loader', 'sass-loader', 27 | ] 28 | }, 29 | { 30 | test: /\.(png|jpg|gif|svg|eot|ttf|woff|woff2)$/, 31 | use: [ 32 | { 33 | loader: 'file-loader', 34 | }, 35 | ], 36 | }, 37 | ] 38 | }, 39 | resolve: { 40 | extensions: ['.js', '.jsx'], 41 | }, 42 | plugins:[ 43 | new HWP({ 44 | title: 'Development', 45 | template: path.join(__dirname,'./src/index.html') 46 | }) 47 | ], 48 | devServer: { 49 | historyApiFallback: true, 50 | static: { 51 | directory: path.resolve(__dirname, 'dist'), 52 | publicPath: '/build' 53 | }, 54 | proxy: { 55 | '/api': 'http://localhost:1111', 56 | } 57 | } 58 | }; --------------------------------------------------------------------------------