├── .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 |
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 | About Astro
23 | Tech Stack
24 |
28 |
31 | License
32 | Authors
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 |
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 |
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 |
37 |
38 |
39 | None
40 |
41 | 30min
42 | 1hr
43 | 24hr
44 | 7d
45 | 14d
46 | 30d
47 |
48 |
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 |
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 |
182 | Astro
183 |
184 |
185 |
189 | Github
190 |
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 |
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 | };
--------------------------------------------------------------------------------