├── .gcloudignore ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── __tests__ ├── supertest.js └── unitTesting.js ├── assets ├── NimbusGIFs │ ├── apis-LQ.gif │ ├── apis-light-LQ.gif │ ├── functions-LQ.gif │ ├── functions-light-LQ.gif │ ├── login-to-home-HQ.gif │ ├── login-to-home-LQ.gif │ ├── login-to-home-light-LQ.gif │ ├── logs-LQ.gif │ ├── logs-light-LQ.gif │ ├── register-LQ.gif │ ├── register-light-LQ.gif │ ├── settings-LQ.gif │ └── settings-light-LQ.gif ├── apis.gif ├── aws-logo-color.png ├── chartjs-logo-color.svg ├── cloud.png ├── daisyui-logo-color.svg ├── docker-logo-color.png ├── electron-logo-color.png ├── express-logo-color.png ├── functions.gif ├── jest-logo-color.png ├── login.gif ├── logs.gif ├── mongodb-logo-color.png ├── nimbus-logo-color.png ├── nimbus.png ├── nimbus2.png ├── nimbus3.png ├── node-logo-color.png ├── react-logo-color.png ├── settings.gif ├── tailwind-logo-color.png ├── ts-logo-long-blue.png └── webpack-logo-color.png ├── client ├── components │ ├── ApiMetrics.tsx │ ├── ApiRelations.tsx │ ├── Apis.tsx │ ├── DonutChart.tsx │ ├── Function.tsx │ ├── Functions.tsx │ ├── HeadBar.tsx │ ├── Home.tsx │ ├── LineChart.tsx │ ├── Login.tsx │ ├── Logout.tsx │ ├── Logs.tsx │ ├── Register.tsx │ └── Settings.tsx ├── containers │ ├── App.tsx │ ├── Layout.tsx │ ├── UserAuth.tsx │ └── UserDashboard.tsx ├── index.html ├── index.tsx ├── styles.css └── types.ts ├── dist └── styles.scss ├── electron └── main.js ├── environment.d.ts ├── package-lock.json ├── package.json ├── postcss.config.js ├── server ├── controllers │ ├── authController.ts │ ├── aws │ │ ├── apiController.ts │ │ ├── apiMetricsController.tsx │ │ ├── credentialsController.ts │ │ ├── lambdaController.ts │ │ ├── logsController.ts │ │ └── metricsController.ts │ └── userController.ts ├── routes │ ├── authRouter.ts │ ├── dashboardRouter.ts │ └── settingsRouter.ts ├── server.ts └── types.ts ├── tailwind.config.js ├── tsconfig.json └── webpack.config.js /.gcloudignore: -------------------------------------------------------------------------------- 1 | # This file specifies files that are *not* uploaded to Google Cloud 2 | # using gcloud. It follows the same syntax as .gitignore, with the addition of 3 | # "#!include" directives (which insert the entries of the given .gitignore-style 4 | # file at that point). 5 | # 6 | # For more information, run: 7 | # $ gcloud topic gcloudignore 8 | # 9 | .gcloudignore 10 | # If you would like to upload your .git directory, .gitignore file or files 11 | # from your .gitignore file, remove the corresponding line 12 | # below: 13 | .git 14 | .gitignore 15 | 16 | # Node.js dependencies: 17 | node_modules/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .env 3 | .DS_Store -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16.13 2 | WORKDIR /usr/src/app 3 | COPY package.json /usr/src/app/ 4 | RUN npm install 5 | COPY . /usr/src/app 6 | RUN npm run build 7 | EXPOSE 3000 8 | ENTRYPOINT [ "node", "./server/server.js" ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 OSLabs Beta 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 Performance Tool 6 |
7 |

8 | 9 | 10 | ### About Nimbus 11 | Serverless architecture has become very popular in the field of software engineering, and Amazon's launch of AWS Lambda in 2014 has been a big part of its growth. AWS Lambda is a serverless computing service that lets users run code in response to events like changes to a DynamoDB table, an API call to API Gateway, or the addition of a file to an S3 bucket. As a function-as-a-service (FaaS) offering, AWS Lambda allows developers to focus on the business logic of their applications by abstracting away the underlying server infrastructure, such as maintenance, capacity planning, and scaling. 12 | 13 | AWS Lambda is a well-known service for serverless computing that lets users run code in response to event triggers. It is increasingly difficult to keep track of how well Lambda functions are working and how they are being used, especially as applications grow in size. This is where Nimbus comes in. Nimbus is a cross-platform desktop application that aims to solve this problem by allowing developers to keep an eye on the metrics of Lambda functions. With Nimbus, developers can see information about how their functions are being used and performance metrics in real time. This includes the number of function invocations, execution durations, CPU usage, and any errors or throttles that occurred. Nimbus also provides an estimate of how much function calls will cost, so developers can keep track of how much is being spent. In short, Nimbus is a valuable tool that simplifies the process of monitoring AWS Lambda functions. 14 | 15 | Nimbus is a tool for monitoring, but it also has a number of features that make your AWS development experience better. For example, Nimbus lets developers observe and analyze the logstream that each Lambda function invocation creates. Nimbus' log feature allows developers to filter through different log streams and sort through timelines. Additionally, Nimbus provides the ability to monitor resources within API Gateway. It displays API metrics including the number of calls, 4XX errors, 5XX errors, and latency. Users can view the relationships between specific API resources and Lambda functions. Having a discernible chain of responsibility in software engineering helps to ensure that applications are developed in a consistent and organized manner. 16 | 17 | 18 | ### Installation 19 | Download the desktop application [HERE](https://www.nimbusos.io/) 20 | 21 | ### User Guide 22 | - Visit the landing page and download the app for your operating system. Install it on your computer to get started. 23 | 24 | - Create an account by entering your information and linking it to your AWS account following the instructions provided on the Register page. 25 | 26 | 27 | login gif 28 | 29 | 30 | - If you already have an account, simply log in. 31 | 32 | login gif 33 | 34 | On the home page, you'll find a lot of information about the health of your AWS application, especially as it relates to lambda functions. This includes important metrics like the number of calls, errors, throttles, costs, and runtimes. 35 | 36 | - Head over to the Functions tab to see metrics broken down by individual functions, including invocations, errors, throttles, and durations. 37 | 38 | functions gif 39 | 40 | - The Logs tab is where you can find all your lambda function logs and filter them by time period, reports only, errors only, or any keyword. 41 | 42 | logs gif 43 | 44 | - The APIs tab lets you view common API metrics, endpoints, and the lambda functions they're connected to. 45 | 46 | apis gif 47 | 48 | - In the Settings tab, you can update your personal information (including AWS Cloudformation Stack ARN and region) or change your login details. 49 | 50 | settings gif 51 | 52 | 53 | ### Technologies Used 54 | 55 | 56 | Electron
57 | React
58 |

ChartJS Chart JS

59 | DaisyUI
60 | Tailwind
61 | Typescript
62 | 63 | 64 | Express
65 | Node.js
66 | MongoDB
67 | Docker
68 | Jest
69 | Webpack
70 | AWS
71 | 72 | 73 | ### How To Contribute 74 | Nimbus is an open-source product supported by the tech accelerator OS Labs. We welcome and appreciate contributions from the community. If you are interested in contributing to the development of our AWS serverless component monitoring and visualization tool, here are a few ways to get started: 75 | 76 | 1. Fork the repository: Go to the main repository on GitHub and click the “Fork” button to create a copy of the code under your own account. This will allow you to make changes to the code without affecting the original repository. 77 | 2. Set up your development environment: 78 | - To install the dependencies configured in the package.json file, run the following command: 79 | ``` 80 | npm install 81 | ``` 82 | - To set up a MongoDB database, you will need to obtain a connection string. Once you have the connection string, create an ENV file in the root directory and input the connection string as follows: 83 | ``` 84 | MONGO_URI='your_connection_string' 85 | ``` 86 | - Next, input your desired port number in the ENV file: 87 | ``` 88 | PORT=your_port_number 89 | ``` 90 | - The Nimbus application requires the use of JWT tokens for implementation, so you will need to create tokens for this purpose. You can input these tokens in the ENV file as follows: 91 | ``` 92 | ACCESS_TOKEN_SECRET=your_access_token_secret 93 | REFRESH_TOKEN_SECRET=your_refresh_token_secret 94 | ``` 95 | - Finally, you will need to obtain the access key id, secret key, and region from your AWS IAM account. Input these values in the ENV file as follows: 96 | ``` 97 | AWS_ACCESS_KEY_ID=your_access_key_id 98 | AWS_SECRET_KEY=your_secret_key 99 | AWS_REGION=your_region 100 | ``` 101 | 3. Choose an issue to work on: Browse the open issues in the repository and pick one that interests you. Alternatively, you can also propose your own changes by opening a new issue and describing the feature or improvement you would like to see. 102 | 4. Create a branch: Once you have chosen an issue to work on, create a new branch in your fork of the repository. Name the branch something descriptive, such as “add-feature-x” or “fix-bug-y”. This will allow you to work on your changes without affecting the main branch of the repository. 103 | 5. Make your changes: Make the necessary changes to the code in your branch. Be sure to follow the repository’s style guidelines and best practices, and make sure to test your changes thoroughly before submitting them. 104 | 6. Commit and push your changes: Once you are satisfied with your changes, commit them to your branch and push them to your fork of the repository. 105 | 7. Open a pull request: Go to the main repository on GitHub and click the “Compare & pull request” button. Describe the changes you have made and why they are necessary. Then, submit the pull request for review. 106 | 107 | A member of the repository’s maintainer team will review your pull request and either merge it into the codebase or provide feedback for changes that need to be made. Thank you for considering contributing to our project! 108 | 109 | 110 | ### License 111 | Distributed under the MIT License 112 | 113 | ### Meet The Team 114 | * Madeline Doctor - LinkedIn | GitHub 115 | 116 | * Arturo Kim - LinkedIn | 117 | GitHub 118 | 119 | * Georges Maroun - LinkedIn | GitHub 120 | 121 | * Arthur Su - LinkedIn | 122 | GitHub 123 | 124 | * Zhaowei Sun - LinkedIn | 125 | GitHub 126 | 127 | 128 | -------------------------------------------------------------------------------- /__tests__/supertest.js: -------------------------------------------------------------------------------- 1 | const request = require('supertest'); 2 | const dotenv = require('dotenv'); 3 | dotenv.config(); 4 | 5 | const app = require('../server/server.js'); 6 | 7 | 8 | describe ("POST /", () => { 9 | describe("should respond to a successful login", () => { 10 | test("should respond with a JWT access token", async () => { 11 | const response = await request(app).post("/login").send({ 12 | email: process.env.DEMO_USERNAME, 13 | password: process.env.DEMO_PASSWORD, 14 | }); 15 | expect(response.body.accessToken).toBeDefined(); 16 | }); 17 | test("should respond with a 200 status code", async () => { 18 | const response = await request(app).post("/login").send({ 19 | email: process.env.DEMO_USERNAME, 20 | password: process.env.DEMO_PASSWORD, 21 | }) 22 | expect(response.statusCode).toBe(200); 23 | }); 24 | }); 25 | 26 | describe("should reject invalid credentials on login", () => { 27 | test("should send error when user does not exist", async () => { 28 | const response = await request(app).post("/login").send({ 29 | email: process.env.DEMO_USERNAME, 30 | password: "invalid", 31 | }) 32 | expect(response.body.err).toEqual("Wrong password"); 33 | }); 34 | test("should send error when user does not exist", async () => { 35 | const response = await request(app).post("/login").send({ 36 | email: "invalid", 37 | password: "invalid", 38 | }) 39 | expect(response.body.err).toEqual("User not in database"); 40 | }); 41 | }); 42 | }); 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /__tests__/unitTesting.js: -------------------------------------------------------------------------------- 1 | const { convertToChartJSStructure } = require('../client/types.js'); 2 | 3 | // The frontend is receiving data in this format: 4 | const input = { 5 | values: [ 15, 4 ], 6 | timestamp: [ '2023-01-10T02:55:00.000Z', '2023-01-09T02:55:00.000Z' ] 7 | }; 8 | 9 | /* In order for ChartJS to graph out data, it has to be an array of objects 10 | with x and y coordinates in date chronological order 11 | const output = [ 12 | {y: 4, x: '1/9/23'}, 13 | {y: 15, x: '1/10/23'}, 14 | */ 15 | 16 | describe("Data formatting to be ChartJS compatible", () => { 17 | it("should return the same array size", () => { 18 | expect(convertToChartJSStructure(input).length).toEqual(2); 19 | }); 20 | it("should map the cooresponding values to their timestamps", () => { 21 | expect(convertToChartJSStructure(input)[0]['y']).toEqual(4); 22 | expect(convertToChartJSStructure(input)[0]['x']).toEqual('1/9/23'); 23 | }); 24 | }); -------------------------------------------------------------------------------- /assets/NimbusGIFs/apis-LQ.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/NimbusGIFs/apis-LQ.gif -------------------------------------------------------------------------------- /assets/NimbusGIFs/apis-light-LQ.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/NimbusGIFs/apis-light-LQ.gif -------------------------------------------------------------------------------- /assets/NimbusGIFs/functions-LQ.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/NimbusGIFs/functions-LQ.gif -------------------------------------------------------------------------------- /assets/NimbusGIFs/functions-light-LQ.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/NimbusGIFs/functions-light-LQ.gif -------------------------------------------------------------------------------- /assets/NimbusGIFs/login-to-home-HQ.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/NimbusGIFs/login-to-home-HQ.gif -------------------------------------------------------------------------------- /assets/NimbusGIFs/login-to-home-LQ.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/NimbusGIFs/login-to-home-LQ.gif -------------------------------------------------------------------------------- /assets/NimbusGIFs/login-to-home-light-LQ.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/NimbusGIFs/login-to-home-light-LQ.gif -------------------------------------------------------------------------------- /assets/NimbusGIFs/logs-LQ.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/NimbusGIFs/logs-LQ.gif -------------------------------------------------------------------------------- /assets/NimbusGIFs/logs-light-LQ.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/NimbusGIFs/logs-light-LQ.gif -------------------------------------------------------------------------------- /assets/NimbusGIFs/register-LQ.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/NimbusGIFs/register-LQ.gif -------------------------------------------------------------------------------- /assets/NimbusGIFs/register-light-LQ.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/NimbusGIFs/register-light-LQ.gif -------------------------------------------------------------------------------- /assets/NimbusGIFs/settings-LQ.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/NimbusGIFs/settings-LQ.gif -------------------------------------------------------------------------------- /assets/NimbusGIFs/settings-light-LQ.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/NimbusGIFs/settings-light-LQ.gif -------------------------------------------------------------------------------- /assets/apis.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/apis.gif -------------------------------------------------------------------------------- /assets/aws-logo-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/aws-logo-color.png -------------------------------------------------------------------------------- /assets/chartjs-logo-color.svg: -------------------------------------------------------------------------------- 1 | 2 | 40 | 45 | 50 | 56 | 61 | 62 | -------------------------------------------------------------------------------- /assets/cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/cloud.png -------------------------------------------------------------------------------- /assets/daisyui-logo-color.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /assets/docker-logo-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/docker-logo-color.png -------------------------------------------------------------------------------- /assets/electron-logo-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/electron-logo-color.png -------------------------------------------------------------------------------- /assets/express-logo-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/express-logo-color.png -------------------------------------------------------------------------------- /assets/functions.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/functions.gif -------------------------------------------------------------------------------- /assets/jest-logo-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/jest-logo-color.png -------------------------------------------------------------------------------- /assets/login.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/login.gif -------------------------------------------------------------------------------- /assets/logs.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/logs.gif -------------------------------------------------------------------------------- /assets/mongodb-logo-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/mongodb-logo-color.png -------------------------------------------------------------------------------- /assets/nimbus-logo-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/nimbus-logo-color.png -------------------------------------------------------------------------------- /assets/nimbus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/nimbus.png -------------------------------------------------------------------------------- /assets/nimbus2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/nimbus2.png -------------------------------------------------------------------------------- /assets/nimbus3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/nimbus3.png -------------------------------------------------------------------------------- /assets/node-logo-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/node-logo-color.png -------------------------------------------------------------------------------- /assets/react-logo-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/react-logo-color.png -------------------------------------------------------------------------------- /assets/settings.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/settings.gif -------------------------------------------------------------------------------- /assets/tailwind-logo-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/tailwind-logo-color.png -------------------------------------------------------------------------------- /assets/ts-logo-long-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/ts-logo-long-blue.png -------------------------------------------------------------------------------- /assets/webpack-logo-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/nimbus/e4223aa3d0a8823426fd720fcc515cf2dabd06b5/assets/webpack-logo-color.png -------------------------------------------------------------------------------- /client/components/ApiMetrics.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import LineChart from "./LineChart"; 3 | import { SelectedApiMetrics, Metric, Message, ApiMetricsProps } from "../types"; 4 | 5 | // Display the metrics for the selected API 6 | const ApiMetrics: React.FC = ({ selectedApi, apiMetrics }: ApiMetricsProps) => { 7 | const [message, setMessage] = useState('fetching data...'); 8 | 9 | // If data not found, set message 10 | if (apiMetrics === undefined) { 11 | if (message !== 'data not found') { 12 | setMessage('data not found') 13 | } 14 | } 15 | 16 | // Make chart for each metric for the selected API 17 | const makeCharts = (selectedApiMetrics: SelectedApiMetrics) => { 18 | if (!selectedApiMetrics) return; 19 | // Declare array to store the LineChart elements 20 | const lineChartElements = []; 21 | // Loop over each metric 22 | for (let metric in selectedApiMetrics) { 23 | const timeValArr = []; 24 | const currMetricsObj = selectedApiMetrics[metric as Metric]; 25 | // Loop over data points: value and timestamp 26 | for (let i = currMetricsObj.values.length - 1; i >= 0; i--) { 27 | const subElement: any = { 28 | y: currMetricsObj.values[i], 29 | x: new Date(currMetricsObj.timestamps[i]).toLocaleString([], {year: "2-digit", month: "numeric", day: "numeric"}), 30 | }; 31 | timeValArr.push(subElement); 32 | // Get the date of the current iteration 33 | let date = new Date(currMetricsObj.timestamps[i]) 34 | // If the next day is less than the next date in our iteration push a value of 0 and the next day into our object 35 | if ((date.getTime() + 1) < (new Date (currMetricsObj.timestamps[i - 1])).getTime()) { 36 | date.setDate(date.getDate() + 1) 37 | while (date.getTime() < (new Date (currMetricsObj.timestamps[i - 1])).getTime()) { 38 | const subElement: any = { 39 | y: 0, 40 | x: new Date(date).toLocaleString([], {year: "2-digit", month: "numeric", day: "numeric"}) 41 | } 42 | timeValArr.push(subElement); 43 | date.setDate(date.getDate() + 1) 44 | } 45 | } 46 | } 47 | // Add lineChart element to array 48 | lineChartElements.push( 49 |
50 |
51 | 52 |
53 |
54 | ) 55 | } 56 | return lineChartElements; 57 | } 58 | 59 | let chartElements; 60 | // Make chart if there is a selected API 61 | if (selectedApi) { 62 | chartElements = makeCharts(apiMetrics[selectedApi]); 63 | } 64 | 65 | return ( 66 |
67 |
68 | {chartElements ? chartElements : message} 69 |
70 |
71 | ); 72 | }; 73 | 74 | export default ApiMetrics; -------------------------------------------------------------------------------- /client/components/ApiRelations.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { Method, Message, ApiRelationsProps } from "../types"; 3 | 4 | // Display the relations for the selected API: routes, methods, and functions. 5 | const ApiRelations: React.FC = ({ selectedApi, apiRelations }: ApiRelationsProps) => { 6 | const [message, setMessage] = useState('fetching data...') 7 | 8 | // If data not found, set message 9 | if (apiRelations === undefined || selectedApi === '') { 10 | if (message !== 'data not found') { 11 | setMessage('data not found'); 12 | } 13 | } 14 | 15 | // Grab data for the selected API; if not found set to null 16 | const selectedApiRelations = apiRelations 17 | && selectedApi 18 | ? 19 | apiRelations.filter((apiRel:any) => apiRel.apiName === selectedApi) 20 | : null; 21 | 22 | 23 | // Get endpoints data 24 | const endpoints = selectedApiRelations && selectedApiRelations.length > 0 ? selectedApiRelations[0].endpoints : null; 25 | 26 | // If endpoints exist, render api relations, else render message 27 | return ( 28 |
29 | {endpoints ? 30 |
31 | {Object.keys(endpoints).map((key) => { 32 | return ( 33 |
34 |
35 |

{key}

36 |
    37 | {(endpoints)[key].map((method:Method) => { 38 | return ( 39 |
  • 40 |
    41 |
    42 | {method.method} 43 | 44 | λ : {method.func} 45 |
    46 |
    47 |
  • 48 | ); 49 | })} 50 |
51 |
52 |
53 | ); 54 | })} 55 |
56 | : 57 | message} 58 |
59 | ); 60 | }; 61 | 62 | export default ApiRelations; -------------------------------------------------------------------------------- /client/components/Apis.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useCallback } from 'react'; 2 | import { v4 as uuidv4 } from 'uuid'; 3 | import ApiMetrics from './ApiMetrics'; 4 | import ApiRelations from './ApiRelations'; 5 | import { View } from "../types"; 6 | 7 | const Apis = () => { 8 | const [apiRelations, setApiRelations] = useState(null); 9 | const [apiMetrics, setApiMetrics] = useState(null); 10 | const [selectedApi, setSelectedApi] = useState(''); 11 | const [showInfo, setShowInfo] = useState('metrics'); 12 | 13 | // Switch between metrics and relations 14 | const toggleDisplay = useCallback((e: React.BaseSyntheticEvent) => { 15 | if (e.target.value !== showInfo) { 16 | setShowInfo(e.target.value); 17 | } 18 | }, [showInfo]); 19 | 20 | // Change the selected api 21 | const handleSelectedApi = useCallback((e: React.BaseSyntheticEvent) => { 22 | setSelectedApi(() => e.target.value) 23 | }, [selectedApi]); 24 | 25 | // Fetch Api relations data and set apiRelation state 26 | const getApiRelations = async (signal:AbortSignal) => { 27 | let res; 28 | try { 29 | res = await fetch('/dashboard/apiRelations', { 30 | method: 'POST', 31 | headers: { 32 | 'Content-Type': 'Application/JSON', 33 | authorization: `BEARER ${localStorage.getItem('accessToken')}`, 34 | }, 35 | signal 36 | }); 37 | res = await res.json(); 38 | const apiRel = res.apiRelations || undefined; 39 | setApiRelations(apiRel); 40 | } 41 | catch(err){ 42 | console.log("Error occurred grabbing API Relations: ", err); 43 | } 44 | } 45 | 46 | // Get api metrics and setApiMetrics 47 | // setSelectedApi to the first api in the metrics object 48 | const getApiMetrics = async (signal:AbortSignal) => { 49 | let res; 50 | try { 51 | res = await fetch('/dashboard/apiMetrics', { 52 | method: 'GET', 53 | headers: { 54 | 'Content-Type': 'Application/JSON', 55 | authorization: `BEARER ${localStorage.getItem('accessToken')}`, 56 | }, 57 | signal 58 | }); 59 | res = await res.json(); 60 | let metrics:any; 61 | if (res.allApiMetrics) { 62 | metrics = res.allApiMetrics; 63 | setSelectedApi(Object.keys(metrics)[0]) 64 | } 65 | setApiMetrics(metrics); 66 | } 67 | catch(err){ 68 | console.log("Error occurred grabbing API Metrics: ", err); 69 | } 70 | } 71 | 72 | // Invoke getApiRelations if apiRelations if falsy 73 | useEffect(() => { 74 | const controller = new AbortController(); 75 | const signal = controller.signal; 76 | if (!apiRelations) { 77 | getApiRelations(signal); 78 | } 79 | 80 | return () => { 81 | controller.abort(); 82 | } 83 | }, []); 84 | 85 | // Invoke getApiMetrics if apiMetrics if falsy 86 | useEffect(() => { 87 | const controller = new AbortController(); 88 | const signal = controller.signal; 89 | 90 | if (!apiMetrics) { 91 | getApiMetrics(signal); 92 | } 93 | return () => { 94 | controller.abort(); 95 | } 96 | }, []); 97 | 98 | // Get API names and create and array of button elements 99 | const getApiNames = () => { 100 | return Object.keys(apiMetrics as any).map((el:string) => { 101 | return ( 102 |
  • 103 | 110 |
  • 111 | ) 112 | }) 113 | }; 114 | 115 | return ( 116 |
    117 |
    118 |
      119 |
    • 120 | API list 121 |
    • 122 | {apiMetrics ? getApiNames() : ''} 123 |
    124 |
    125 |
    126 | 131 | 136 |
    137 |
    138 | {showInfo === 'metrics' ? 139 | : } 140 |
    141 |
    142 | 143 |
    144 |
    145 | 146 | ); 147 | }; 148 | 149 | export default Apis; 150 | -------------------------------------------------------------------------------- /client/components/DonutChart.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Chart, ArcElement, Tooltip, Legend } from 'chart.js'; 3 | import { Doughnut } from 'react-chartjs-2' 4 | import chroma from "chroma-js" 5 | import { DonutChartProps } from "../types"; 6 | 7 | const DonutChart = (props: DonutChartProps) => { 8 | 9 | Chart.register(ArcElement, Tooltip, Legend); 10 | 11 | const colors = chroma.scale(['#623cad','#fb9ce5']).mode('lch').colors(props.rawData.data?.length); 12 | 13 | return ( 14 | 40 | ); 41 | } 42 | 43 | export default DonutChart; -------------------------------------------------------------------------------- /client/components/Function.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import LineChart from "./LineChart"; 3 | import moment from "moment"; 4 | import { FunctionProps, Data, RawData, chartJSData } from "../types"; 5 | import { convertToChartJSStructure } from "../types"; 6 | 7 | // Component to display a single function's metrics 8 | const Function: React.FC = (props: FunctionProps) => { 9 | const [isClicked, setIsClicked] = useState(false) 10 | 11 | const [totalInvocations, setTotalInvocations] = useState(0) 12 | const [totalErrors, setTotalErrors] = useState(0) 13 | const [totalThrottles, setTotalThrottles] = useState(0) 14 | const [totalDuration, setTotalDuration] = useState(0) 15 | 16 | const [invocations, setInvocations] = useState([]) 17 | const [errors, setErrors] = useState([]) 18 | const [throttles, setThrottles] = useState([]) 19 | const [duration, setDuration] = useState([]) 20 | 21 | 22 | useEffect(() => { 23 | // If our metric array has at least one value, accumulate the values 24 | if (props.invocations.values.length > 0) setTotalInvocations(props.invocations.values.reduce((acc: number, curr: number):number => acc + curr)) 25 | if (props.errors.values.length > 0) setTotalErrors(props.errors.values.reduce((acc: number, curr: number):number => acc + curr)) 26 | if (props.throttles.values.length > 0) setTotalThrottles(props.throttles.values.reduce((acc: number, curr: number):number => acc + curr)) 27 | if (props.duration.values.length > 0) setTotalDuration(Math.ceil(props.duration.values.reduce((acc: number, curr: number):number => acc + curr)/props.duration.values.length)) 28 | }, []) 29 | 30 | // Generate the chart when the user clicks on the row 31 | const generateChart = () => { 32 | if (!isClicked) { 33 | setInvocations(convertToChartJSStructure(props.invocations)) 34 | setErrors(convertToChartJSStructure(props.errors)) 35 | setThrottles(convertToChartJSStructure(props.throttles)) 36 | setDuration(convertToChartJSStructure(props.duration)) 37 | setIsClicked(true); 38 | } else { 39 | setIsClicked(false); 40 | } 41 | } 42 | 43 | return ( 44 | <> 45 | 46 | {props.funcName} 47 | {totalInvocations} 48 | {totalErrors} 49 | {totalThrottles} 50 | {totalDuration} 51 | 52 | {isClicked && 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | } 68 | 69 | ) 70 | } 71 | 72 | export default Function -------------------------------------------------------------------------------- /client/components/Functions.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import Function from './Function' 3 | import { v4 as uuidv4 } from 'uuid'; 4 | 5 | // Component to fetch all functions metrics and display them 6 | const Functions = () => { 7 | const [funcMetrics, setFuncMetrics] = useState({}); 8 | 9 | // Grab each functions metrics when the component mounts 10 | const grabFuncsMetrics = async() => { 11 | let response; 12 | response = await fetch('/dashboard/funcmetrics', { 13 | method: 'GET', 14 | headers: { 15 | 'Content-Type': 'Application/JSON', 16 | authorization: `BEARER ${localStorage.getItem('accessToken')}`, 17 | }, 18 | }) 19 | response = await response.json() 20 | setFuncMetrics(response.eachFuncMetrics) 21 | }; 22 | 23 | // Grab functions metrics when the component mounts 24 | useEffect(() => { 25 | grabFuncsMetrics() 26 | }, []); 27 | 28 | // Display table head and each function metrics component 29 | return ( 30 |
    31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | {/* Update the funcMetric parameter type */} 43 | {Object.entries(funcMetrics).map((funcMetric:any) => ( 44 | 50 | ))} 51 | 52 |
    Lambda FunctionInvocationsErrorsThrottlesDuration (ms)
    53 |
    54 | ) 55 | }; 56 | 57 | export default Functions; 58 | -------------------------------------------------------------------------------- /client/components/HeadBar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback } from 'react'; 2 | import { HeadBarProps } from "../types"; 3 | 4 | const HeadBar: React.FC = ({ toggleTheme, theme }: HeadBarProps) => { 5 | 6 | const [checked, setChecked] = useState(false); 7 | 8 | // Toggle theme (light/dark) 9 | const handleToggle = () => { 10 | toggleTheme(); 11 | setChecked(prev => !prev); 12 | }; 13 | 14 | return ( 15 |
    16 | {/* nimbus */} 17 |
    18 | 19 | {theme === 'myThemeDark' ? 20 | : } 21 | 22 |
    23 |
    24 | 28 |
    29 |
    30 | ) 31 | 32 | } 33 | 34 | export default HeadBar; -------------------------------------------------------------------------------- /client/components/Home.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from 'react'; 2 | import LineChart from './LineChart' 3 | import DonutChart from './DonutChart' 4 | import { RawData, chartJSData, costProps, HomeProps } from "../types"; 5 | import { convertToChartJSStructure } from "../types"; 6 | 7 | const Home: React.FC = (props: HomeProps) => { 8 | const [invocationsData, setInvocations] = useState([]); 9 | const [errorsData, setErrors] = useState([]); 10 | const [throttlesData, setThrottles] = useState([]); 11 | const [durationData, setDurations] = useState([]); 12 | const [cost, setCost] = useState(0) 13 | const [totalInvocations, setTotalInvocations] = useState(0); 14 | const [totalErrors, setTotalErrors] = useState(0); 15 | const [totalThrottles, setTotalThrottles] = useState(0); 16 | const [averageDuration, setAverageDuration] = useState(0); 17 | const [invocationsByFunc, setInvocationsByFunc] = useState({}) 18 | 19 | const route = useRef({ 20 | allMetrics: '/dashboard/allMetrics', 21 | funcMetrics: '/dashboard/funcmetrics' 22 | }); 23 | 24 | // Sends a GET request to the '/dashboard/allMetrics' route 25 | // Uses ReactHooks to change the states based on data received from AWS 26 | const getMetrics = async () => { 27 | let res; 28 | try { 29 | res = await fetch(route.current.allMetrics, { 30 | method: 'GET', 31 | headers: { 32 | 'Content-Type': 'Application/JSON', 33 | authorization: `BEARER ${localStorage.getItem('accessToken')}`, 34 | refresh: `BEARER ${localStorage.getItem('refreshToken')}`, 35 | }, 36 | }); 37 | res = await res.json(); 38 | // Convert the data to a format that Chart JS can use and set the states to the new data 39 | setInvocations(convertToChartJSStructure({ 40 | values: res.allFuncMetrics.invocations.values, 41 | timestamp: res.allFuncMetrics.invocations.timestamp 42 | })); 43 | setErrors(convertToChartJSStructure({ 44 | values: res.allFuncMetrics.errors.values, 45 | timestamp: res.allFuncMetrics.errors.timestamp 46 | })); 47 | setThrottles(convertToChartJSStructure({ 48 | values: res.allFuncMetrics.throttles.values, 49 | timestamp: res.allFuncMetrics.throttles.timestamp 50 | })); 51 | setDurations(convertToChartJSStructure({ 52 | values: res.allFuncMetrics.duration.values, 53 | timestamp: res.allFuncMetrics.duration.timestamp 54 | })); 55 | setCost(calculateCost(res.cost)); 56 | if (res.allFuncMetrics.invocations.values.length > 0) { 57 | setTotalInvocations(res.allFuncMetrics.invocations.values.reduce((a:number, b:number) => a + b, 0)); 58 | } 59 | if (res.allFuncMetrics.errors.values.length > 0) { 60 | setTotalErrors(res.allFuncMetrics.errors.values.reduce((a:number, b:number) => a + b, 0)); 61 | } 62 | if (res.allFuncMetrics.throttles.values.length > 0) { 63 | setTotalThrottles(res.allFuncMetrics.throttles.values.reduce((a:number, b:number) => a + b, 0)); 64 | } 65 | if (res.allFuncMetrics.duration.values.length > 0) { 66 | setAverageDuration(res.allFuncMetrics.duration.values.reduce((a:number, b:number) => a + b, 0) / res.allFuncMetrics.duration.values.length); 67 | } 68 | 69 | } catch(error) { 70 | console.log(error); 71 | } 72 | } 73 | 74 | const getFuncMetrics = async () => { 75 | let res; 76 | try { 77 | res = await fetch(route.current.funcMetrics, { 78 | method: 'GET', 79 | headers: { 80 | 'Content-Type': 'Application/JSON', 81 | authorization: `BEARER ${localStorage.getItem('accessToken')}`, 82 | }, 83 | }); 84 | res = await res.json(); 85 | const labelArr = []; 86 | const invocationArr = []; 87 | for (const func in res.eachFuncMetrics) { 88 | labelArr.push(func); 89 | const invocations = res.eachFuncMetrics[func].invocations.values; 90 | if (invocations.length > 0) { 91 | invocationArr.push(invocations.reduce((a: number, b: number) => a + b, 0)); 92 | } else { 93 | invocationArr.push(0); 94 | } 95 | } 96 | setInvocationsByFunc({labels: labelArr, data: invocationArr}) 97 | } catch (error) { 98 | console.log(error); 99 | } 100 | } 101 | 102 | // Calculates the running cost of all functions 103 | const calculateCost = (costObj: costProps) => { 104 | let totalCost = 0; 105 | for (let i = 0; i < costObj.memory.length; i++) { 106 | totalCost += costObj.memory[i] * 0.0009765625 * costObj.duration[i] * 0.001 107 | } 108 | return Math.round(totalCost * 100) / 100 109 | } 110 | 111 | // Invokes the getMetrics function 112 | useEffect(() => { 113 | getMetrics(); 114 | getFuncMetrics(); 115 | }, []); 116 | 117 | return ( 118 | <> 119 |
    120 |
    121 |
    122 |

    Welcome to Your Dashboard, {props.firstName}

    123 |
    124 |
    125 |
    126 |
    127 |
    128 |
    129 |
    130 |

    Total Invocations

    131 |
    {totalInvocations.toLocaleString(undefined, {maximumFractionDigits:2})}
    132 |
    133 |
    134 |
    135 |
    136 |

    Total Errors

    137 |
    {totalErrors.toLocaleString(undefined, {maximumFractionDigits:2})}
    138 |
    139 |
    140 |
    141 |
    142 |

    Total Throttles

    143 |
    {totalThrottles.toLocaleString(undefined, {maximumFractionDigits:2})}
    144 |
    145 |
    146 |
    147 |
    148 |

    Average Duration

    149 |
    {averageDuration.toLocaleString(undefined, {maximumFractionDigits:2})}ms
    150 |
    151 |
    152 |
    153 |
    154 |

    Cost

    155 |
    ${cost.toLocaleString(undefined, {maximumFractionDigits:2})}
    156 |
    157 |
    158 |
    159 |
    160 |
    161 |
    162 |

    Invocations by Functions

    163 | 164 |
    165 |
    166 |
    167 |
    168 | 169 |
    170 |
    171 |
    172 | 173 |
    174 |
    175 |
    176 |
    177 | 178 |
    179 |
    180 |
    181 |
    182 | 183 |
    184 |
    185 |
    186 |
    187 | 188 |
    189 |
    190 |
    191 | 192 | ); 193 | }; 194 | 195 | export default Home; 196 | 197 | 198 | 199 | -------------------------------------------------------------------------------- /client/components/LineChart.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Chart, CategoryScale, TimeScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend } from "chart.js"; 3 | import { Line } from "react-chartjs-2"; 4 | import { RawData, LineChartProps } from "../types"; 5 | 6 | const LineChart = (props: LineChartProps) => { 7 | 8 | // Registers plugins to be applied on all charts 9 | Chart.register( 10 | TimeScale, 11 | CategoryScale, 12 | LinearScale, 13 | PointElement, 14 | LineElement, 15 | Title, 16 | Tooltip, 17 | Legend 18 | ); 19 | 20 | // Set chart data 21 | const data = { 22 | datasets: [ 23 | { 24 | label: props.label, 25 | data: props.rawData, 26 | fill: false, 27 | borderColor: [ 28 | "#fb9ce5", 29 | ], 30 | tension: 0.3, 31 | }, 32 | ] 33 | }; 34 | 35 | return ( 36 | 40 | ); 41 | }; 42 | 43 | export default LineChart; -------------------------------------------------------------------------------- /client/components/Login.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { AuthProps } from "../types"; 3 | 4 | 5 | const Login: React.FC = ({ swapAuthView, handleUserLogin }: AuthProps) => { 6 | const [email, setEmail] = useState(""); 7 | const [password, setPassword] = useState(""); 8 | const [errorMessage, setErrorMessage] = useState(""); 9 | 10 | // Update state when user types email or password 11 | const updateEmail = (e: React.ChangeEvent) => { 12 | setEmail(e.target.value) 13 | } 14 | const updatePassword = (e: React.ChangeEvent) => { 15 | setPassword(e.target.value) 16 | } 17 | 18 | // Hnadle wrong user input 19 | const handleError = (err: string) => { 20 | setErrorMessage(err) 21 | } 22 | 23 | // Send user credentials to server and receive access and refresh tokens 24 | const submitForm = (e: React.FormEvent) => { 25 | e.preventDefault(); 26 | const credentials = { 27 | email, 28 | password 29 | } 30 | 31 | fetch('/login', { 32 | method: 'POST', 33 | headers: { 'Content-Type': 'Application/JSON' }, 34 | body: JSON.stringify(credentials), 35 | }) 36 | .then(res => res.json()) 37 | .then((result) => { 38 | if (result.err) { 39 | handleError('Wrong username or password'); 40 | } 41 | else { 42 | handleUserLogin(); 43 | // Save access token to local storage 44 | localStorage.setItem("accessToken", result.accessToken) 45 | } 46 | }); 47 | } 48 | 49 | return ( 50 |
    51 |
    52 |

    Login Now

    53 |

    54 | Go Serverless with Confidence. Monitor and Visualize your AWS Lamda with Ease. 55 |

    56 |
    57 |
    58 |
    59 |
    60 |
    61 | 62 | 69 | 70 |
    71 |
    72 | 73 | 80 | 81 |
    82 |
    83 | 84 |
    85 |
    86 | 87 |
    88 |
    89 | { (errorMessage !== '') 90 | && 91 |
    92 |
    93 | 94 | {errorMessage} 95 |
    96 |
    97 | } 98 |
    99 | 100 | ) 101 | } 102 | 103 | export default Login; -------------------------------------------------------------------------------- /client/components/Logout.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Logout: React.FC = () => { 4 | const logOut = () => { 5 | fetch('/logout') 6 | .then(res => { 7 | window.location.reload(); 8 | }) 9 | } 10 | 11 | return ( 12 | <> 13 | 14 | 15 | ) 16 | } 17 | 18 | export default Logout; -------------------------------------------------------------------------------- /client/components/Logs.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | // import { userContext } from 'react' 3 | // import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/20/solid'; 4 | 5 | // To Do: 6 | // highlight selected buttons 7 | 8 | // Types 9 | type Period = '30d' | '14d' | '7d' | '1d' | '1hr'; 10 | type Search = String; 11 | 12 | const Logs = () => { 13 | const [functions, setFunctions] = useState([]); 14 | const [selectedFunc, setSelectedFunc] = useState(''); 15 | const [logs, setLogs] = useState(['Fetching logs...']); 16 | const [period, setPeriod] = useState('30d'); 17 | const [search, setSearch] = useState(''); 18 | // const [buttonIsActive, setButtonIsActive] = useState(false); 19 | // const [buttonState, setButtonState] = useState({ 20 | // activeObject: null, 21 | // objects: functionsList, 22 | // }); 23 | const [selectedTimeButton, setSelectedTimeButton] = useState('30d'); 24 | const [selectedLogsButton, setSelectedLogsButton] = useState('All logs'); 25 | const [selectedFunctionButton, setSelectedFunctionButton] = useState(''); 26 | 27 | const routes = { 28 | functions: '/dashboard/functions', 29 | logs: '/dashboard/filteredLogs', 30 | }; 31 | 32 | // Change period 33 | const changePeriod = (e: any) => { 34 | if (e.target.value !== period) { 35 | setPeriod(e.target.value); 36 | } 37 | }; 38 | 39 | // Change search keyword 40 | const changeSearch = (e: any) => { 41 | if (e.target.value === 'allLogs') { 42 | setSearch(''); 43 | } else if (e.target.value === 'reports') { 44 | setSearch('REPORT'); 45 | } else if (e.target.value === 'errors') { 46 | setSearch('error'); 47 | } else { 48 | setSearch(e.target.value); 49 | } 50 | }; 51 | 52 | const changeSelectedFunc = (e: any) => { 53 | if (e.target.value !== selectedFunc) { 54 | setSelectedFunc(e.target.value); 55 | } 56 | }; 57 | 58 | // Get the names of Lambda functions in a string[], setFunctions to result and setSelectedFunc to first function 59 | const getFunctions = async () => { 60 | let res; 61 | try { 62 | res = await fetch(`${routes.functions}`, { 63 | method: 'GET', 64 | headers: { 65 | 'Content-Type': 'Application/JSON', 66 | authorization: `BEARER ${localStorage.getItem('accessToken')}`, 67 | }, 68 | }); 69 | // convert response to JS object 70 | res = await res.json(); 71 | 72 | // func arr is an array of strings (function names) 73 | const funcArr = res.functions || ['unable to fetch lambda functions']; 74 | // change functions to be array with all function names 75 | setFunctions(funcArr); 76 | // 77 | setSelectedFunc(funcArr[0]); 78 | setSelectedFunctionButton(funcArr[0]); 79 | } catch (err) { 80 | console.log('ERROR FROM GET FUNCTIONS', err); 81 | } 82 | }; 83 | 84 | // Fetch logs for the selectedFunc in a string[] and setLogs 85 | const getLogs = async () => { 86 | let res; 87 | const reqBody = { 88 | functionName: selectedFunc, 89 | filterPattern: search, 90 | period: period, 91 | }; 92 | try { 93 | res = await fetch(`${routes.logs}`, { 94 | method: 'POST', 95 | headers: { 96 | 'Content-Type': 'Application/JSON', 97 | authorization: `BEARER ${localStorage.getItem('accessToken')}`, 98 | }, 99 | body: JSON.stringify(reqBody), 100 | }); 101 | // convert response to JS object 102 | res = await res.json(); 103 | let logsArr = res.filteredLogs || ['Logs not found']; 104 | setLogs(logsArr); 105 | } catch (err) { 106 | console.log('ERROR FROM GET LOGS', err); 107 | } 108 | }; 109 | 110 | // On component mount: get all lambda functions 111 | useEffect(() => { 112 | getFunctions(); 113 | }, []); 114 | 115 | // On state change selectedFunc, period, search: get logs based on selected lambda func and options 116 | useEffect(() => { 117 | if (selectedFunc !== '') { 118 | getLogs(); 119 | } 120 | }, [selectedFunc, period, search]); 121 | 122 | const logsList = logs.map((log, i) => ( 123 | 124 | {/* 132 | 137 | */} 138 | {i + 1} 139 | {/* {log} */} 140 | {log} 141 | 142 | // overflow-hidden 143 | )); 144 | 145 | /*
    146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 |
    Logs
    157 |
    */ 158 | // ['func1', 'func2, 'func3'] 159 | const functionsList = functions.map((funcStr, i) => ( 160 | 171 | )); 172 | 173 | return ( 174 | <> 175 |
    176 |
    177 | 183 |
    184 | 197 | 210 | 221 | 232 | 245 |
    246 | 247 |
    248 | 260 | 272 | 284 |
    285 | 286 |
    287 |
    288 | 294 | 310 |
    311 |
    312 |
    313 |
    314 |
    315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | {logsList} 323 |
    Logs
    324 |
    325 |
    326 |
    327 | 328 | ); 329 | }; 330 | 331 | export default Logs; -------------------------------------------------------------------------------- /client/components/Register.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback } from 'react'; 2 | import { UserData, AuthProps } from "../types"; 3 | 4 | type FormErrors = {email:boolean; firstName:boolean; lastName:boolean; password:boolean; confirmation:boolean; arn:boolean}; 5 | 6 | const Register: React.FC = ({swapAuthView, handleUserLogin }: AuthProps) => { 7 | const [email, setEmail] = useState(''); 8 | const [firstName, setFirstName] = useState(''); 9 | const [lastName, setLastName] = useState(''); 10 | const [password, setPassword] = useState(''); 11 | const [confirmation, setConfirmation] = useState(''); 12 | const [arn, setArn] = useState(''); 13 | const [region, setRegion] = useState(''); 14 | const [errorMessage, setErrorMessage] = useState(''); 15 | const [errors, setErrors] = useState({email: false, firstName: false, lastName: false, password: false, confirmation: false, arn: false}); 16 | 17 | // Update state when user types email, password etc. 18 | const updateEmail = useCallback((e: React.ChangeEvent) => { 19 | setEmail(e.target.value); 20 | }, [email]); 21 | 22 | const updateFirstName = useCallback((e: React.ChangeEvent) => { 23 | setFirstName(e.target.value); 24 | }, [firstName]); 25 | 26 | const updateLastName = useCallback((e: React.ChangeEvent) => { 27 | setLastName(e.target.value); 28 | }, [lastName]); 29 | 30 | const updatePassword = useCallback((e: React.ChangeEvent) => { 31 | setPassword(e.target.value); 32 | }, [password]); 33 | 34 | const updateConfirmation = useCallback((e: React.ChangeEvent) => { 35 | setConfirmation(e.target.value); 36 | }, [confirmation]); 37 | 38 | const updateArn = useCallback((e: React.ChangeEvent) => { 39 | setArn(e.target.value); 40 | }, [arn]); 41 | 42 | const updateRegion = useCallback((e: any) => { 43 | setRegion(e.target.value); 44 | }, [region]); 45 | 46 | // Hnadle wrong user input 47 | const handleError = useCallback(() => { 48 | setErrorMessage('Some information is missing or incorrect'); 49 | }, [errorMessage]); 50 | 51 | // Update errors object 52 | const updateErrors = (errors: Array): void => { 53 | const errorObj:any = {email:false, firstName:false, lastName:false, password:false, confirmation:false, arn:false} 54 | 55 | errors.forEach((el) => { 56 | errorObj[el] = true; 57 | }); 58 | setErrors(errorObj); 59 | }; 60 | 61 | // Send user credentials to server and receive access and refresh tokens 62 | const submitForm = (e: any) => { 63 | e.preventDefault(); 64 | 65 | const userData: UserData = { 66 | email, 67 | firstName, 68 | lastName, 69 | password, 70 | confirmation, 71 | arn, 72 | region, 73 | }; 74 | 75 | fetch('/register', { 76 | method: 'POST', 77 | headers: { 'Content-Type': 'Application/JSON' }, 78 | body: JSON.stringify(userData), 79 | }) 80 | .then((res) => res.json()) 81 | .then((result) => { 82 | if (result.errMessage) { 83 | handleError(); 84 | updateErrors(result.errors); 85 | } else { 86 | handleUserLogin(); 87 | // Save access token to local storage 88 | localStorage.setItem('accessToken', result.accessToken); 89 | } 90 | }); 91 | }; 92 | 93 | // List of AWS regions 94 | const regionsOptions = [ 95 | 'us-east-2', 96 | 'us-east-1', 97 | 'us-west-1', 98 | 'us-west-2', 99 | 'af-south-1', 100 | 'ap-east-1', 101 | 'ap-south-2', 102 | 'ap-southeast-3', 103 | 'ap-south-1', 104 | 'ap-northeast-3', 105 | 'ap-northeast-2', 106 | 'ap-southeast-1', 107 | 'ap-southeast-2', 108 | 'ap-northeast-1', 109 | 'ca-central-1', 110 | 'eu-central-1', 111 | 'eu-west-1', 112 | 'eu-west-2', 113 | 'eu-south-1', 114 | 'eu-west-3', 115 | 'eu-south-2', 116 | 'eu-north-1', 117 | 'eu-central-2', 118 | 'me-south-1', 119 | 'me-central-1', 120 | 'sa-east-1', 121 | 'us-gov-east-1', 122 | 'us-gov-west-1', 123 | ]; 124 | 125 | return ( 126 |
    127 |
    128 |
    129 |
    130 |
    131 |

    Register

    132 |
    133 | 134 | 141 |
    142 |
    143 | 144 | 151 |
    152 |
    153 | 154 | 161 |
    162 |
    163 | 164 | 171 |
    172 |
    173 | 174 | 181 |
    182 |
    183 |
    184 |
    185 |
    186 |
    187 |

    Connect Your AWS Account

    188 |

    189 | Please follow the steps below to set up the connection to your AWS 190 | account. Click{' '} 191 | 196 | HERE 197 | {' '} 198 | to start the process. 199 |

    200 |
      201 |
    • 202 | Log into your AWS account. 203 |
    • 204 |
    • 205 | You will be directed to a Create Stack page with a pre-populated Nimbus stack template and details. 206 |
    • 207 |
    • 208 | At the bottom of the page, please make sure you check “I acknowledge that AWS CloudFormation might create IAM resources with custom names.” 209 |
    • 210 |
    • 211 | Click “Create Stack” to generate a Nimbus stack under your AWS CloudFormation. 212 |
    • 213 |
    • 214 | When the stack creation completes, navigate to the “Outputs” tab and copy the Nimbus ARN value (e.g. arn:aws:iam::00000000000:role/NimbusDelegationRole). 215 |
    • 216 |
    • 217 | Paste the ARN value onto the corresponding sign-up field. 218 |
    • 219 |
    220 |
    221 |
    222 | 223 | 224 |
    225 |
    226 | 234 |
    235 |
    236 | 237 |
    238 | 239 |
    240 |
    241 | { (errorMessage !== '') 242 | && 243 |
    244 |
    245 | 246 | {errorMessage} 247 |
    248 |
    249 | } 250 |
    251 |
    252 |
    253 | 254 | 255 | ); 256 | }; 257 | 258 | export default Register; 259 | -------------------------------------------------------------------------------- /client/components/Settings.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useRef } from 'react'; 2 | import { ProfileData, PasswordData, SettingsProps } from "../types"; 3 | 4 | const Settings: React.FC = (props: SettingsProps) => { 5 | const [errorMessage, setErrorMessage] = useState(''); 6 | const [successMessage, setSuccessMessage] = useState(''); 7 | 8 | // Create refs for password and confirmation 9 | const passwordRef = useRef() as React.MutableRefObject; 10 | const confirmationRef = useRef() as React.MutableRefObject; 11 | 12 | // Store routes in object 13 | const routes = { 14 | updateProfile: '/dashboard/settings/updateProfile', 15 | updatePassword: '/dashboard/settings/updatePassword' 16 | } 17 | 18 | // Update state on change 19 | const updateFirstName = (e: React.ChangeEvent) => { 20 | props.setFirstName(e.target.value); 21 | }; 22 | 23 | const updateLastName = (e: React.ChangeEvent) => { 24 | props.setLastName(e.target.value); 25 | }; 26 | 27 | const updatePassword = (e: React.ChangeEvent) => { 28 | props.setPassword(e.target.value); 29 | }; 30 | 31 | const updateConfirmation = (e: React.ChangeEvent) => { 32 | props.setConfirmation(e.target.value); 33 | }; 34 | 35 | const updateArn = (e: React.ChangeEvent) => { 36 | props.setArn(e.target.value); 37 | }; 38 | 39 | const updateRegion = (e: any) => { 40 | props.setRegion(e.target.value); 41 | }; 42 | 43 | // Reset password fields 44 | const resetPasswords = () => { 45 | passwordRef.current.value = ""; 46 | confirmationRef.current.value = ""; 47 | } 48 | 49 | const regionsOptions = [ 50 | 'us-east-2', 51 | 'us-east-1', 52 | 'us-west-1', 53 | 'us-west-2', 54 | 'af-south-1', 55 | 'ap-east-1', 56 | 'ap-south-2', 57 | 'ap-southeast-3', 58 | 'ap-south-1', 59 | 'ap-northeast-3', 60 | 'ap-northeast-2', 61 | 'ap-southeast-1', 62 | 'ap-southeast-2', 63 | 'ap-northeast-1', 64 | 'ca-central-1', 65 | 'eu-central-1', 66 | 'eu-west-1', 67 | 'eu-west-2', 68 | 'eu-south-1', 69 | 'eu-west-3', 70 | 'eu-south-2', 71 | 'eu-north-1', 72 | 'eu-central-2', 73 | 'me-south-1', 74 | 'me-central-1', 75 | 'sa-east-1', 76 | 'us-gov-east-1', 77 | 'us-gov-west-1', 78 | ]; 79 | 80 | const filteredRegionsOptions = regionsOptions.filter(r => r !== props.region); 81 | 82 | // Set error and success messages 83 | const handleError = () => { 84 | setErrorMessage('Some information is missing or incorrect!'); 85 | }; 86 | 87 | const handleSuccess= () => { 88 | setSuccessMessage('Your profile details are updated successfully!'); 89 | }; 90 | 91 | const handlePasswordSuccess= () => { 92 | setSuccessMessage('Password updated successfully'); 93 | }; 94 | 95 | // Highlight erroneusly filled fields in red 96 | const highlightInput = (errors: Array): void => { 97 | errors.forEach((el) => { 98 | const input = document.querySelector(`#${el}`); 99 | if (input) { 100 | input.style.borderColor = 'red'; 101 | } 102 | }); 103 | }; 104 | 105 | // Update profile 106 | const submitProfileForm = (e: any) => { 107 | e.preventDefault(); 108 | const updatedProfileData: ProfileData = { 109 | firstName: props.firstName, 110 | lastName: props.lastName, 111 | arn: props.arn, 112 | region: props.region, 113 | }; 114 | fetch(routes.updateProfile, { 115 | method: 'POST', 116 | headers: { 117 | 'Content-Type': 'Application/JSON' , 118 | authorization: `BEARER ${localStorage.getItem('accessToken')}`, 119 | }, 120 | body: JSON.stringify(updatedProfileData), 121 | }).then(res => res.json()) 122 | .then((result) => { 123 | if (result.errMessage) { 124 | handleError(); 125 | highlightInput(result.errors); 126 | } else { 127 | handleSuccess(); 128 | props.setFirstName(result.firstName); 129 | props.setLastName(result.lastName); 130 | props.setArn(result.arn); 131 | props.setRegion(result.region); 132 | } 133 | }) 134 | } 135 | 136 | // Update password 137 | const submitPasswordForm = (e: any) => { 138 | e.preventDefault(); 139 | const updatedPasswordData: PasswordData = { 140 | password: props.password, 141 | confirmation: props.confirmation 142 | }; 143 | fetch(routes.updatePassword, { 144 | method: 'POST', 145 | headers: { 146 | 'Content-Type': 'Application/JSON' , 147 | authorization: `BEARER ${localStorage.getItem('accessToken')}`, 148 | }, 149 | body: JSON.stringify(updatedPasswordData), 150 | }).then(res => res.json()) 151 | .then((result) => { 152 | if (result.errMessage) { 153 | handleError(); 154 | highlightInput(result.errors); 155 | } else if (result.successMessage) { 156 | handlePasswordSuccess(); 157 | props.setPassword(''); 158 | props.setConfirmation(''); 159 | resetPasswords(); 160 | } 161 | }) 162 | } 163 | 164 | return ( 165 | <> 166 |
    167 |
    168 |

    Profile

    169 |
    170 |
    171 | 172 | 180 |
    181 |
    182 | 183 | 191 |
    192 |
    193 | 194 | 195 |
    196 |
    197 | 198 | 206 |
    207 |
    208 | 209 |
    210 |
    211 |
    212 | 213 |
    214 |

    Login Details

    215 |
    216 |
    217 | 218 | 226 |
    227 |
    228 | 229 | 237 |
    238 |
    239 | 240 | 248 |
    249 |
    250 | 251 |
    252 |
    253 |
    254 |
    255 | { successMessage !== '' 256 | && 257 |
    258 |
    259 | 260 | {successMessage} 261 |
    262 |
    263 | } 264 | { (errorMessage !== '' && successMessage === '') 265 | && 266 |
    267 |
    268 | 269 | {errorMessage} 270 |
    271 |
    272 | } 273 | ) 274 | 275 | }; 276 | 277 | export default Settings; 278 | -------------------------------------------------------------------------------- /client/containers/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState , useCallback} from 'react'; 2 | import { BrowserRouter, Routes, Route } from 'react-router-dom'; 3 | import { Theme } from 'react-daisyui' 4 | import UserAuth from './UserAuth'; 5 | import UserDashboard from './UserDashboard'; 6 | import HeadBar from '../components/HeadBar' 7 | 8 | const App = () => { 9 | const [userLoggedIn, setUserLoggedIn] = useState(false); 10 | const [theme, setTheme] = React.useState('myThemeDark'); 11 | 12 | const toggleTheme = useCallback(() => { 13 | setTheme(theme === 'myThemeDark' ? 'myThemeLight' : 'myThemeDark'); 14 | }, [theme]); 15 | 16 | const handleUserLogin = useCallback(() => { 17 | setUserLoggedIn((userLoggedIn) => !userLoggedIn); 18 | }, [userLoggedIn]); 19 | 20 | 21 | // If user is logged in, render UserDashboard component, otherwise render UserAuth component 22 | return ( 23 | 24 | 25 |
    26 | {userLoggedIn ? ( 27 | 28 | ) : ( 29 | 30 | )} 31 |
    32 |
    33 | 34 | ); 35 | }; 36 | 37 | export default App; 38 | -------------------------------------------------------------------------------- /client/containers/Layout.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState} from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import Logout from '../components/Logout'; 4 | 5 | // Sidebar component 6 | const Layout = () => { 7 | const [selectedTab, setSelectedTab] = useState('Home'); 8 | 9 | return ( 10 |
      11 |
    • setSelectedTab('Home')}> 12 | Home 13 |
    • 14 |
    • setSelectedTab('Functions')}> 15 | Functions 16 |
    • 17 |
    • setSelectedTab('Logs')}> 18 | Logs 19 |
    • 20 |
    • setSelectedTab('APIs')}> 21 | APIs 22 |
    • 23 |
    • setSelectedTab('Settings')}> 24 | Settings 25 |
    • 26 |
      27 | 28 |
      29 | 30 |
    31 | ); 32 | }; 33 | 34 | export default Layout; 35 | -------------------------------------------------------------------------------- /client/containers/UserAuth.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback } from "react"; 2 | import Login from "../components/Login.js"; 3 | import Register from "../components/Register.js"; 4 | import { UserAuthProps } from "../types"; 5 | 6 | // UserAuth component, displays login or register component depending on state 7 | const UserAuth: React.FC = ({ handleUserLogin, toggleTheme }: UserAuthProps) => { 8 | const [showLogin, setShowLogin] = useState(true); 9 | 10 | // Swap between login and register views 11 | const swapAuthView = useCallback(() => { 12 | setShowLogin((showLogin) => !showLogin); 13 | }, [showLogin]); 14 | 15 | return ( 16 |
    17 | {showLogin === true ? : } 18 |
    19 | ) 20 | } 21 | 22 | export default UserAuth; 23 | -------------------------------------------------------------------------------- /client/containers/UserDashboard.tsx: -------------------------------------------------------------------------------- 1 | import { request } from 'http'; 2 | import React, { useState, useEffect, useCallback } from 'react'; 3 | import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; 4 | import Layout from './Layout'; 5 | import Home from '../components/Home'; 6 | import Functions from '../components/Functions'; 7 | import Logs from '../components/Logs'; 8 | import Apis from '../components/Apis'; 9 | import Settings from '../components/Settings'; 10 | import Logout from '../components/Logout' 11 | import { UserAuthProps, FetchHeader} from "../types"; 12 | 13 | const UserDashboard: React.FC = ({ handleUserLogin, toggleTheme }: UserAuthProps) => { 14 | 15 | const routes = { 16 | userDetails: '/dashboard/settings/userDetails', 17 | } 18 | 19 | const [data, setData] = useState([]); 20 | const [email, setEmail] = useState(''); 21 | const [firstName, setFirstName] = useState(''); 22 | const [lastName, setLastName] = useState(''); 23 | const [password, setPassword] = useState(''); 24 | const [confirmation, setConfirmation] = useState(''); 25 | const [arn, setArn] = useState(''); 26 | const [region, setRegion] = useState(''); 27 | 28 | const getUserDetails = useCallback(async () => { 29 | let res; 30 | try { 31 | res = await fetch(`${routes.userDetails}`, { 32 | method: 'GET', 33 | headers: { 34 | 'Content-Type': 'Application/JSON', 35 | authorization: `BEARER ${localStorage.getItem('accessToken')}`, 36 | refresh: `BEARER ${localStorage.getItem('refreshToken')}`, 37 | }, 38 | }); 39 | // convert response to JS object 40 | res = await res.json(); 41 | setEmail(res.email); 42 | setFirstName(res.firstName); 43 | setLastName(res.lastName); 44 | setArn(res.arn); 45 | setRegion(res.region); 46 | } catch (err) { 47 | console.log(err); 48 | } 49 | }, [email, firstName, lastName, arn, region]); 50 | 51 | useEffect(() => { 52 | getUserDetails(); 53 | }, []); 54 | 55 | return ( 56 | <> 57 | 58 |
    59 | 60 |
    61 | 62 | }> 63 | }> 64 | }> 65 | }> 66 | } > 82 | 83 | 84 | 87 |
    88 |
    89 | 90 | 91 |
    92 |
    93 |
    94 |
    95 | 96 |
    97 | 98 | ); 99 | }; 100 | 101 | export default UserDashboard; 102 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | nimbus 8 | 9 | 10 |
    11 | 12 | -------------------------------------------------------------------------------- /client/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import App from './containers/App'; 4 | import './styles.css'; 5 | 6 | const container = document.getElementById('root')!; 7 | const root = createRoot(container); 8 | root.render( 9 | 10 | 11 | 12 | ); -------------------------------------------------------------------------------- /client/styles.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /client/types.ts: -------------------------------------------------------------------------------- 1 | import React, { Dispatch, SetStateAction } from 'react'; 2 | 3 | export interface SelectedApiMetrics { 4 | Latency: { timestamps: Date[], values: number[] }, 5 | Count: { timestamps: Date[], values: number[] }, 6 | '5XXError': { timestamps: Date[], values: number[] }, 7 | '4XXError': { timestamps: Date[], values: number[] } 8 | }; 9 | 10 | export type ApiMetricsProps = { 11 | selectedApi: string 12 | apiMetrics: any; 13 | }; 14 | 15 | export type Metric = 'Latency' | 'Count' | '5XXError' | '4XXError'; 16 | 17 | export type Message = 'fetching data...' | 'data not found'; 18 | 19 | export type ApiRelationsProps = { 20 | selectedApi: string 21 | apiRelations: Array<{apiName: string, endpoints: {[key: string]: Method[]}}> | null | undefined; 22 | }; 23 | 24 | export type Method = { 25 | func: string, 26 | method: string, 27 | }; 28 | 29 | export type View = 'metrics' | 'relations'; 30 | 31 | export type DonutChartProps = { 32 | rawData: {labels?: string[], data?: number[]} 33 | }; 34 | 35 | export type FunctionProps = { 36 | funcName: string 37 | invocations: Data 38 | errors: Data 39 | throttles: Data 40 | duration: Data 41 | } 42 | 43 | export type Data = { 44 | values: Array, 45 | timestamp: Array 46 | } 47 | 48 | export type RawData = { 49 | y: number, 50 | x: string, 51 | }; 52 | 53 | export type chartJSData = Array; 54 | 55 | export type costProps = { 56 | memory: number[], 57 | invocations: number[], 58 | duration: number[] 59 | }; 60 | 61 | export type HomeProps = { 62 | firstName: string; 63 | }; 64 | 65 | export interface HeadBarProps { 66 | toggleTheme: () => void; 67 | theme: String; 68 | }; 69 | 70 | export type LineChartProps = { 71 | rawData : Array, 72 | label : string, 73 | }; 74 | 75 | export interface AuthProps { 76 | swapAuthView: () => void 77 | handleUserLogin: () => void 78 | }; 79 | 80 | export interface UserAuthProps { 81 | handleUserLogin: () => void; 82 | toggleTheme: () => void; 83 | }; 84 | 85 | export interface FetchHeader { 86 | headers: { 87 | 'Content-Type': string; 88 | authorization: { 89 | accessToken: string; 90 | refreshToken: string; 91 | }; 92 | }; 93 | }; 94 | 95 | export interface UserData { 96 | email: String; 97 | firstName: String; 98 | lastName: String; 99 | password: String; 100 | confirmation: String; 101 | arn: String; 102 | region: String; 103 | }; 104 | 105 | export interface ProfileData { 106 | firstName: String; 107 | lastName: String; 108 | arn: String; 109 | region: String; 110 | }; 111 | 112 | export interface PasswordData { 113 | password: String; 114 | confirmation: String; 115 | }; 116 | 117 | export type SettingsProps = { 118 | email: string; 119 | firstName: string; 120 | lastName: string; 121 | password: string; 122 | confirmation: string; 123 | arn: string; 124 | region: string; 125 | setEmail: Dispatch>; 126 | setFirstName: Dispatch>; 127 | setLastName: Dispatch>; 128 | setPassword: Dispatch>; 129 | setConfirmation: Dispatch>; 130 | setArn: Dispatch>; 131 | setRegion: Dispatch>; 132 | }; 133 | 134 | // Function is used in Home&Function components 135 | // Converted RawData into a structure that is compatible with ChartJS 136 | export const convertToChartJSStructure = (rawData: Data) => { 137 | const output = []; 138 | 139 | for (let i = rawData.values.length - 1; i >= 0; i--) { 140 | const subElement: RawData = { 141 | y: rawData.values[i], 142 | x: new Date(rawData.timestamp[i]).toLocaleString([], {year: "2-digit", month: "numeric", day: "numeric"}) 143 | } 144 | output.push(subElement); 145 | // Get the date of the current iteration 146 | let date = new Date(rawData.timestamp[i]) 147 | // If the next day is less than the next date in our iteration push a value of 0 and the next day into our object 148 | if ((date.getTime() + 1) < (new Date (rawData.timestamp[i - 1])).getTime()) { 149 | date.setDate(date.getDate() + 1) 150 | while (date.getTime() < (new Date (rawData.timestamp[i - 1])).getTime()) { 151 | const subElement: RawData = { 152 | y: 0, 153 | x: new Date(date).toLocaleString([], {year: "2-digit", month: "numeric", day: "numeric"}) 154 | } 155 | output.push(subElement); 156 | date.setDate(date.getDate() + 1) 157 | } 158 | } 159 | } 160 | return output; 161 | }; 162 | 163 | -------------------------------------------------------------------------------- /dist/styles.scss: -------------------------------------------------------------------------------- 1 | /* 2 | ! tailwindcss v3.2.4 | MIT License | https://tailwindcss.com 3 | */ 4 | 5 | /* 6 | 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) 7 | 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) 8 | */ 9 | 10 | *, 11 | ::before, 12 | ::after { 13 | box-sizing: border-box; 14 | /* 1 */ 15 | border-width: 0; 16 | /* 2 */ 17 | border-style: solid; 18 | /* 2 */ 19 | border-color: #e5e7eb; 20 | /* 2 */ 21 | } 22 | 23 | ::before, 24 | ::after { 25 | --tw-content: ''; 26 | } 27 | 28 | /* 29 | 1. Use a consistent sensible line-height in all browsers. 30 | 2. Prevent adjustments of font size after orientation changes in iOS. 31 | 3. Use a more readable tab size. 32 | 4. Use the user's configured `sans` font-family by default. 33 | 5. Use the user's configured `sans` font-feature-settings by default. 34 | */ 35 | 36 | html { 37 | line-height: 1.5; 38 | /* 1 */ 39 | -webkit-text-size-adjust: 100%; 40 | /* 2 */ 41 | -moz-tab-size: 4; 42 | /* 3 */ 43 | -o-tab-size: 4; 44 | tab-size: 4; 45 | /* 3 */ 46 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 47 | /* 4 */ 48 | font-feature-settings: normal; 49 | /* 5 */ 50 | } 51 | 52 | /* 53 | 1. Remove the margin in all browsers. 54 | 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. 55 | */ 56 | 57 | body { 58 | margin: 0; 59 | /* 1 */ 60 | line-height: inherit; 61 | /* 2 */ 62 | } 63 | 64 | /* 65 | 1. Add the correct height in Firefox. 66 | 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) 67 | 3. Ensure horizontal rules are visible by default. 68 | */ 69 | 70 | hr { 71 | height: 0; 72 | /* 1 */ 73 | color: inherit; 74 | /* 2 */ 75 | border-top-width: 1px; 76 | /* 3 */ 77 | } 78 | 79 | /* 80 | Add the correct text decoration in Chrome, Edge, and Safari. 81 | */ 82 | 83 | abbr:where([title]) { 84 | -webkit-text-decoration: underline dotted; 85 | text-decoration: underline dotted; 86 | } 87 | 88 | /* 89 | Remove the default font size and weight for headings. 90 | */ 91 | 92 | h1, 93 | h2, 94 | h3, 95 | h4, 96 | h5, 97 | h6 { 98 | font-size: inherit; 99 | font-weight: inherit; 100 | } 101 | 102 | /* 103 | Reset links to optimize for opt-in styling instead of opt-out. 104 | */ 105 | 106 | a { 107 | color: inherit; 108 | text-decoration: inherit; 109 | } 110 | 111 | /* 112 | Add the correct font weight in Edge and Safari. 113 | */ 114 | 115 | b, 116 | strong { 117 | font-weight: bolder; 118 | } 119 | 120 | /* 121 | 1. Use the user's configured `mono` font family by default. 122 | 2. Correct the odd `em` font sizing in all browsers. 123 | */ 124 | 125 | code, 126 | kbd, 127 | samp, 128 | pre { 129 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 130 | /* 1 */ 131 | font-size: 1em; 132 | /* 2 */ 133 | } 134 | 135 | /* 136 | Add the correct font size in all browsers. 137 | */ 138 | 139 | small { 140 | font-size: 80%; 141 | } 142 | 143 | /* 144 | Prevent `sub` and `sup` elements from affecting the line height in all browsers. 145 | */ 146 | 147 | sub, 148 | sup { 149 | font-size: 75%; 150 | line-height: 0; 151 | position: relative; 152 | vertical-align: baseline; 153 | } 154 | 155 | sub { 156 | bottom: -0.25em; 157 | } 158 | 159 | sup { 160 | top: -0.5em; 161 | } 162 | 163 | /* 164 | 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) 165 | 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) 166 | 3. Remove gaps between table borders by default. 167 | */ 168 | 169 | table { 170 | text-indent: 0; 171 | /* 1 */ 172 | border-color: inherit; 173 | /* 2 */ 174 | border-collapse: collapse; 175 | /* 3 */ 176 | } 177 | 178 | /* 179 | 1. Change the font styles in all browsers. 180 | 2. Remove the margin in Firefox and Safari. 181 | 3. Remove default padding in all browsers. 182 | */ 183 | 184 | button, 185 | input, 186 | optgroup, 187 | select, 188 | textarea { 189 | font-family: inherit; 190 | /* 1 */ 191 | font-size: 100%; 192 | /* 1 */ 193 | font-weight: inherit; 194 | /* 1 */ 195 | line-height: inherit; 196 | /* 1 */ 197 | color: inherit; 198 | /* 1 */ 199 | margin: 0; 200 | /* 2 */ 201 | padding: 0; 202 | /* 3 */ 203 | } 204 | 205 | /* 206 | Remove the inheritance of text transform in Edge and Firefox. 207 | */ 208 | 209 | button, 210 | select { 211 | text-transform: none; 212 | } 213 | 214 | /* 215 | 1. Correct the inability to style clickable types in iOS and Safari. 216 | 2. Remove default button styles. 217 | */ 218 | 219 | button, 220 | [type='button'], 221 | [type='reset'], 222 | [type='submit'] { 223 | -webkit-appearance: button; 224 | /* 1 */ 225 | background-color: transparent; 226 | /* 2 */ 227 | background-image: none; 228 | /* 2 */ 229 | } 230 | 231 | /* 232 | Use the modern Firefox focus style for all focusable elements. 233 | */ 234 | 235 | :-moz-focusring { 236 | outline: auto; 237 | } 238 | 239 | /* 240 | Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) 241 | */ 242 | 243 | :-moz-ui-invalid { 244 | box-shadow: none; 245 | } 246 | 247 | /* 248 | Add the correct vertical alignment in Chrome and Firefox. 249 | */ 250 | 251 | progress { 252 | vertical-align: baseline; 253 | } 254 | 255 | /* 256 | Correct the cursor style of increment and decrement buttons in Safari. 257 | */ 258 | 259 | ::-webkit-inner-spin-button, 260 | ::-webkit-outer-spin-button { 261 | height: auto; 262 | } 263 | 264 | /* 265 | 1. Correct the odd appearance in Chrome and Safari. 266 | 2. Correct the outline style in Safari. 267 | */ 268 | 269 | [type='search'] { 270 | -webkit-appearance: textfield; 271 | /* 1 */ 272 | outline-offset: -2px; 273 | /* 2 */ 274 | } 275 | 276 | /* 277 | Remove the inner padding in Chrome and Safari on macOS. 278 | */ 279 | 280 | ::-webkit-search-decoration { 281 | -webkit-appearance: none; 282 | } 283 | 284 | /* 285 | 1. Correct the inability to style clickable types in iOS and Safari. 286 | 2. Change font properties to `inherit` in Safari. 287 | */ 288 | 289 | ::-webkit-file-upload-button { 290 | -webkit-appearance: button; 291 | /* 1 */ 292 | font: inherit; 293 | /* 2 */ 294 | } 295 | 296 | /* 297 | Add the correct display in Chrome and Safari. 298 | */ 299 | 300 | summary { 301 | display: list-item; 302 | } 303 | 304 | /* 305 | Removes the default spacing and border for appropriate elements. 306 | */ 307 | 308 | blockquote, 309 | dl, 310 | dd, 311 | h1, 312 | h2, 313 | h3, 314 | h4, 315 | h5, 316 | h6, 317 | hr, 318 | figure, 319 | p, 320 | pre { 321 | margin: 0; 322 | } 323 | 324 | fieldset { 325 | margin: 0; 326 | padding: 0; 327 | } 328 | 329 | legend { 330 | padding: 0; 331 | } 332 | 333 | ol, 334 | ul, 335 | menu { 336 | list-style: none; 337 | margin: 0; 338 | padding: 0; 339 | } 340 | 341 | /* 342 | Prevent resizing textareas horizontally by default. 343 | */ 344 | 345 | textarea { 346 | resize: vertical; 347 | } 348 | 349 | /* 350 | 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) 351 | 2. Set the default placeholder color to the user's configured gray 400 color. 352 | */ 353 | 354 | input::-moz-placeholder, textarea::-moz-placeholder { 355 | opacity: 1; 356 | /* 1 */ 357 | color: #9ca3af; 358 | /* 2 */ 359 | } 360 | 361 | input::placeholder, 362 | textarea::placeholder { 363 | opacity: 1; 364 | /* 1 */ 365 | color: #9ca3af; 366 | /* 2 */ 367 | } 368 | 369 | /* 370 | Set the default cursor for buttons. 371 | */ 372 | 373 | button, 374 | [role="button"] { 375 | cursor: pointer; 376 | } 377 | 378 | /* 379 | Make sure disabled buttons don't get the pointer cursor. 380 | */ 381 | 382 | :disabled { 383 | cursor: default; 384 | } 385 | 386 | /* 387 | 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) 388 | 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) 389 | This can trigger a poorly considered lint error in some tools but is included by design. 390 | */ 391 | 392 | img, 393 | svg, 394 | video, 395 | canvas, 396 | audio, 397 | iframe, 398 | embed, 399 | object { 400 | display: block; 401 | /* 1 */ 402 | vertical-align: middle; 403 | /* 2 */ 404 | } 405 | 406 | /* 407 | Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) 408 | */ 409 | 410 | img, 411 | video { 412 | max-width: 100%; 413 | height: auto; 414 | } 415 | 416 | /* Make elements with the HTML hidden attribute stay hidden by default */ 417 | 418 | [hidden] { 419 | display: none; 420 | } 421 | 422 | *, ::before, ::after { 423 | --tw-border-spacing-x: 0; 424 | --tw-border-spacing-y: 0; 425 | --tw-translate-x: 0; 426 | --tw-translate-y: 0; 427 | --tw-rotate: 0; 428 | --tw-skew-x: 0; 429 | --tw-skew-y: 0; 430 | --tw-scale-x: 1; 431 | --tw-scale-y: 1; 432 | --tw-pan-x: ; 433 | --tw-pan-y: ; 434 | --tw-pinch-zoom: ; 435 | --tw-scroll-snap-strictness: proximity; 436 | --tw-ordinal: ; 437 | --tw-slashed-zero: ; 438 | --tw-numeric-figure: ; 439 | --tw-numeric-spacing: ; 440 | --tw-numeric-fraction: ; 441 | --tw-ring-inset: ; 442 | --tw-ring-offset-width: 0px; 443 | --tw-ring-offset-color: #fff; 444 | --tw-ring-color: rgb(59 130 246 / 0.5); 445 | --tw-ring-offset-shadow: 0 0 #0000; 446 | --tw-ring-shadow: 0 0 #0000; 447 | --tw-shadow: 0 0 #0000; 448 | --tw-shadow-colored: 0 0 #0000; 449 | --tw-blur: ; 450 | --tw-brightness: ; 451 | --tw-contrast: ; 452 | --tw-grayscale: ; 453 | --tw-hue-rotate: ; 454 | --tw-invert: ; 455 | --tw-saturate: ; 456 | --tw-sepia: ; 457 | --tw-drop-shadow: ; 458 | --tw-backdrop-blur: ; 459 | --tw-backdrop-brightness: ; 460 | --tw-backdrop-contrast: ; 461 | --tw-backdrop-grayscale: ; 462 | --tw-backdrop-hue-rotate: ; 463 | --tw-backdrop-invert: ; 464 | --tw-backdrop-opacity: ; 465 | --tw-backdrop-saturate: ; 466 | --tw-backdrop-sepia: ; 467 | } 468 | 469 | ::backdrop { 470 | --tw-border-spacing-x: 0; 471 | --tw-border-spacing-y: 0; 472 | --tw-translate-x: 0; 473 | --tw-translate-y: 0; 474 | --tw-rotate: 0; 475 | --tw-skew-x: 0; 476 | --tw-skew-y: 0; 477 | --tw-scale-x: 1; 478 | --tw-scale-y: 1; 479 | --tw-pan-x: ; 480 | --tw-pan-y: ; 481 | --tw-pinch-zoom: ; 482 | --tw-scroll-snap-strictness: proximity; 483 | --tw-ordinal: ; 484 | --tw-slashed-zero: ; 485 | --tw-numeric-figure: ; 486 | --tw-numeric-spacing: ; 487 | --tw-numeric-fraction: ; 488 | --tw-ring-inset: ; 489 | --tw-ring-offset-width: 0px; 490 | --tw-ring-offset-color: #fff; 491 | --tw-ring-color: rgb(59 130 246 / 0.5); 492 | --tw-ring-offset-shadow: 0 0 #0000; 493 | --tw-ring-shadow: 0 0 #0000; 494 | --tw-shadow: 0 0 #0000; 495 | --tw-shadow-colored: 0 0 #0000; 496 | --tw-blur: ; 497 | --tw-brightness: ; 498 | --tw-contrast: ; 499 | --tw-grayscale: ; 500 | --tw-hue-rotate: ; 501 | --tw-invert: ; 502 | --tw-saturate: ; 503 | --tw-sepia: ; 504 | --tw-drop-shadow: ; 505 | --tw-backdrop-blur: ; 506 | --tw-backdrop-brightness: ; 507 | --tw-backdrop-contrast: ; 508 | --tw-backdrop-grayscale: ; 509 | --tw-backdrop-hue-rotate: ; 510 | --tw-backdrop-invert: ; 511 | --tw-backdrop-opacity: ; 512 | --tw-backdrop-saturate: ; 513 | --tw-backdrop-sepia: ; 514 | } 515 | 516 | .container { 517 | width: 100%; 518 | } 519 | 520 | @media (min-width: 640px) { 521 | .container { 522 | max-width: 640px; 523 | } 524 | } 525 | 526 | @media (min-width: 768px) { 527 | .container { 528 | max-width: 768px; 529 | } 530 | } 531 | 532 | @media (min-width: 1024px) { 533 | .container { 534 | max-width: 1024px; 535 | } 536 | } 537 | 538 | @media (min-width: 1280px) { 539 | .container { 540 | max-width: 1280px; 541 | } 542 | } 543 | 544 | @media (min-width: 1536px) { 545 | .container { 546 | max-width: 1536px; 547 | } 548 | } 549 | 550 | .flex { 551 | display: flex; 552 | } 553 | 554 | .gap-8 { 555 | gap: 2rem; 556 | } 557 | -------------------------------------------------------------------------------- /electron/main.js: -------------------------------------------------------------------------------- 1 | const { app, BrowserWindow } = require("electron"); 2 | 3 | // const server = require("../server/server.js"); 4 | 5 | let mainWindow; 6 | 7 | function createWindow() { 8 | mainWindow = new BrowserWindow({ 9 | width: 1200, 10 | height: 700, 11 | webPreferences: { 12 | nodeIntegration: true, 13 | }, 14 | }); 15 | 16 | mainWindow.loadURL("http://localhost:8080"); 17 | mainWindow.on("closed", function () { 18 | mainWindow = null; 19 | }); 20 | } 21 | 22 | app.on("ready", createWindow); 23 | 24 | app.on("resize", function (e, x, y) { 25 | mainWindow.setSize(x, y); 26 | }); 27 | 28 | app.on("window-all-closed", function () { 29 | if (process.platform !== "darwin") { 30 | app.quit(); 31 | } 32 | }); 33 | 34 | app.on("activate", function () { 35 | if (mainWindow === null) { 36 | createWindow(); 37 | } 38 | }); -------------------------------------------------------------------------------- /environment.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | namespace NodeJS { 3 | interface ProcessEnv { 4 | AWS_ACCESS_KEY_ID: string; 5 | AWS_SECRET_KEY: string; 6 | } 7 | } 8 | } 9 | 10 | declare module "*.png" { 11 | export default "" as string; 12 | } 13 | declare module "*.svg" { 14 | export default "" as string; 15 | } 16 | declare module "*.jpeg" { 17 | export default "" as string; 18 | } 19 | declare module "*.jpg" { 20 | export default "" as string; 21 | } 22 | 23 | 24 | export {} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nimbus", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "electron/main.js", 6 | "scripts": { 7 | "test": "jest", 8 | "start": "node server/server.js", 9 | "dev": "webpack serve --mode development & npm start", 10 | "build": "webpack --config ./webpack.config.js" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/oslabs-beta/nimbus.git" 15 | }, 16 | "keywords": [], 17 | "author": "Arturo Kim https://github.com/arturokim, Zhaowei Sun https://github.com/zhaowei-sun, Arthur Su https://github.com/suster22, Georges Maroun https://github.com/george-maroun, Madeline Doctor https://github.com/madelinedoctor1", 18 | "license": "ISC", 19 | "bugs": { 20 | "url": "https://github.com/oslabs-beta/nimbus/issues" 21 | }, 22 | "homepage": "https://github.com/oslabs-beta/nimbus#readme", 23 | "dependencies": { 24 | "@aws-sdk/client-api-gateway": "^3.231.0", 25 | "@aws-sdk/client-cloudwatch": "^3.231.0", 26 | "@aws-sdk/client-cloudwatch-logs": "^3.231.0", 27 | "@aws-sdk/client-lambda": "^3.231.0", 28 | "@aws-sdk/client-sts": "^3.231.0", 29 | "@aws-sdk/types": "^3.226.0", 30 | "@headlessui/react": "^1.7.7", 31 | "@heroicons/react": "^2.0.13", 32 | "@types/jsonwebtoken": "^8.5.9", 33 | "aws-sdk": "^2.1339.0", 34 | "bcrypt": "^5.1.0", 35 | "chart.js": "^4.1.2", 36 | "chartjs-adapter-moment": "^1.0.1", 37 | "chroma-js": "^2.4.2", 38 | "cookie-parser": "^1.4.6", 39 | "cors": "^2.8.5", 40 | "date-fns": "^2.29.3", 41 | "express": "^4.18.2", 42 | "jsonwebtoken": "^8.5.1", 43 | "moment": "^2.29.4", 44 | "mongoose": "^6.8.0", 45 | "react": "^18.2.0", 46 | "react-chartjs-2": "^5.1.0", 47 | "react-daisyui": "^2.5.0", 48 | "react-dom": "^18.2.0", 49 | "react-router": "^6.4.3", 50 | "react-router-dom": "^6.4.3", 51 | "reactflow": "^11.4.0", 52 | "sass": "^1.56.1", 53 | "typescript": "^4.9.4", 54 | "uuid": "^9.0.0" 55 | }, 56 | "devDependencies": { 57 | "@babel/core": "^7.20.2", 58 | "@babel/preset-env": "^7.20.2", 59 | "@babel/preset-react": "^7.18.6", 60 | "@testing-library/react": "^13.4.0", 61 | "@types/bcryptjs": "^2.4.2", 62 | "@types/chroma-js": "^2.1.4", 63 | "@types/express": "^4.17.14", 64 | "@types/node": "^18.11.18", 65 | "@types/react": "^18.0.26", 66 | "@types/react-dom": "^18.0.9", 67 | "@types/uuid": "^9.0.0", 68 | "autoprefixer": "^10.4.13", 69 | "babel-loader": "^9.1.0", 70 | "css-loader": "^6.7.2", 71 | "daisyui": "^2.46.0", 72 | "docker": "^1.0.0", 73 | "dotenv": "^16.0.3", 74 | "dotenv-webpack": "^8.0.1", 75 | "electron": "^22.0.0", 76 | "eslint": "^8.29.0", 77 | "file-loader": "^6.2.0", 78 | "html-webpack-plugin": "^5.5.0", 79 | "jest": "^29.3.1", 80 | "nodemon": "^2.0.20", 81 | "postcss": "^8.4.20", 82 | "postcss-loader": "^7.0.2", 83 | "sass-loader": "^13.2.0", 84 | "style-loader": "^3.3.1", 85 | "supertest": "^6.3.3", 86 | "tailwindcss": "^3.2.4", 87 | "ts-jest": "^29.0.3", 88 | "ts-loader": "^9.4.2", 89 | "webpack": "^5.75.0", 90 | "webpack-cli": "^5.0.0", 91 | "webpack-dev-server": "^4.11.1" 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /server/controllers/authController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | import { getUserToken, authController } from '../types'; 3 | const { ACCESS_TOKEN_SECRET, REFRESH_TOKEN_SECRET } = process.env; 4 | const jwt = require('jsonwebtoken') 5 | require('dotenv').config(); 6 | 7 | 8 | const authController: authController = { 9 | async generateJWT (req, res, next) { 10 | try { 11 | const { email } = req.body; 12 | // Grab user from database 13 | res.locals.user = { 14 | email 15 | } 16 | // Generate an access and refresh token for user 17 | const accessToken = jwt.sign(res.locals.user, ACCESS_TOKEN_SECRET, { expiresIn: '24h'}); 18 | const refreshToken = jwt.sign(res.locals.user, REFRESH_TOKEN_SECRET); 19 | // Store refresh token in cookies 20 | res.cookie('refreshToken', refreshToken, { secure: true, httpOnly: true, sameSite: 'strict' }); 21 | res.locals.accessToken = accessToken; 22 | return next(); 23 | } catch (err) { 24 | return next({ 25 | log: "Error caught in userController.generateJWT middleware function", 26 | status: 500, 27 | message: {err: `Error generating JWT for user`} 28 | }) 29 | } 30 | }, 31 | 32 | async verifyToken (req, res, next) { 33 | const { authorization } = req.headers; 34 | // Token should be 'BEARER TOKEN NUMBER' so slice the string to grab only the token number 35 | const token = authorization && authorization.slice(7); 36 | // Check if user has an access token 37 | if (token) { 38 | // Verify if token is valid 39 | let user; 40 | try { 41 | user = jwt.verify(token, ACCESS_TOKEN_SECRET); 42 | } catch (err) { 43 | console.log(err); 44 | } 45 | if (user) { 46 | res.locals.accessToken = token; 47 | res.locals.email = user.email; 48 | } 49 | else { 50 | // If the user's token was not verified, check for refresh token 51 | const refreshToken = req.cookies.refreshToken 52 | try { 53 | // Verify if user's refresh token is valid 54 | const user = jwt.verify(refreshToken, REFRESH_TOKEN_SECRET); 55 | if (user) { 56 | // Generate new access token 57 | user.iat = Date.now() 58 | const newAccessToken = jwt.sign(user, ACCESS_TOKEN_SECRET, { expiresIn: '24h'}) 59 | res.locals.accessToken = newAccessToken; 60 | res.locals.email = user.email 61 | } 62 | } catch (err) { 63 | console.log(err); 64 | } 65 | } 66 | } 67 | return next(); 68 | } , 69 | 70 | removeToken (req, res, next) { 71 | res.clearCookie("refreshToken"); 72 | return next(); 73 | } 74 | }; 75 | 76 | export default authController; -------------------------------------------------------------------------------- /server/controllers/aws/apiController.ts: -------------------------------------------------------------------------------- 1 | import { APIGatewayClient, GetRestApisCommand, GetResourcesCommand, GetResourcesCommandInput, GetRestApisCommandOutput } from "@aws-sdk/client-api-gateway"; 2 | import { Request, Response, NextFunction } from "express"; 3 | import { LambdaClient, GetPolicyCommand, GetPolicyCommandOutput } from "@aws-sdk/client-lambda"; 4 | import { Endpoint, LambdaAPIs, API, Relation } from "../../types"; 5 | 6 | // Controller for the API Gateway endpoints 7 | const apiController = { 8 | // Get relations between API Gateway endpoints and Lambda functions 9 | async getAPIRelations(req: Request, res: Response, next: NextFunction) { 10 | // Create new APIGatewayClient 11 | const apiClient = new APIGatewayClient({ 12 | region: res.locals.region, 13 | credentials: res.locals.credentials, 14 | }); 15 | 16 | // Create new LambdaClient 17 | const lambdaClient = new LambdaClient({ 18 | region: res.locals.region, 19 | credentials: res.locals.credentials 20 | }); 21 | 22 | // Declare array to store APIs 23 | const apiList: API[] = []; 24 | const lambdaAPIsList: LambdaAPIs[] = []; 25 | 26 | try { 27 | // Get list of APIs and store in apiList 28 | const restAPIs: GetRestApisCommandOutput = await apiClient.send(new GetRestApisCommand({})); 29 | const restAPIsItems = restAPIs?.items; 30 | 31 | if (restAPIsItems !== undefined) { 32 | for (const item of restAPIsItems) { 33 | const getResourcesInput: GetResourcesCommandInput = { restApiId: item.id }; 34 | const resources = await apiClient.send(new GetResourcesCommand(getResourcesInput)); 35 | const paths = resources?.items?.map(item => item.path); 36 | const apiDetails: API = {apiName: item.name, apiId: item.id, paths: paths}; 37 | apiList.push(apiDetails); 38 | } 39 | } 40 | 41 | // Get list of Lambda functions from res.locals 42 | const functions: string[] = res.locals.functions; 43 | 44 | // For each function, get the policy, create apiInfo and store apiInfo in lambdaAPIsList 45 | for (const func of functions) { 46 | try { 47 | const functionName = func; 48 | 49 | const getPolicyCommand = new GetPolicyCommand({ 50 | FunctionName: functionName 51 | }); 52 | 53 | const policyResults: GetPolicyCommandOutput = await lambdaClient.send(getPolicyCommand); 54 | 55 | if (policyResults) { 56 | const policy: (string | undefined) = policyResults.Policy; 57 | if (policy) { 58 | 59 | const statements = JSON.parse(policy).Statement; 60 | const lambdaAPIs: LambdaAPIs = { 61 | functionName, 62 | endpoints: [] 63 | } 64 | for (const statement of statements) { 65 | const apiEndpoint = statement.Condition.ArnLike['AWS:SourceArn']; 66 | const apiEndpointStrArr = apiEndpoint.split('/'); 67 | const apiIdStrArr = apiEndpointStrArr[apiEndpointStrArr.length - 4].split(':'); 68 | const apiId = apiIdStrArr[apiIdStrArr.length - 1]; 69 | const apiMethod = apiEndpointStrArr[apiEndpointStrArr.length - 2]; 70 | const apiPath = apiEndpointStrArr[apiEndpointStrArr.length - 1]; 71 | const apiInfo = { apiMethod, apiPath: '/' + apiPath, apiId }; 72 | lambdaAPIs.endpoints.push(apiInfo); 73 | } 74 | lambdaAPIsList.push(lambdaAPIs); 75 | } 76 | } 77 | 78 | } catch (err) { 79 | console.log(err); 80 | } 81 | } 82 | // Declare array to store relations 83 | const relations = []; 84 | 85 | // For each API, create relationObj and store in relations 86 | for (const api of apiList) { 87 | const relationObj:Relation = { apiName: api.apiName, endpoints: {} }; 88 | if (relationObj.endpoints) { 89 | for (const lambdaAPI of lambdaAPIsList) { 90 | for (const ep of lambdaAPI.endpoints) { 91 | if (ep && ep?.apiId === api.apiId) { 92 | const path: string = ep.apiPath 93 | if (relationObj.endpoints[path] === undefined) { 94 | relationObj.endpoints[path] = []; 95 | } 96 | relationObj.endpoints[ep.apiPath].push({ 97 | method: ep.apiMethod, 98 | func: lambdaAPI.functionName 99 | }); 100 | } 101 | } 102 | } 103 | } 104 | relations.push(relationObj); 105 | } 106 | 107 | res.locals.apiRelations = relations; 108 | return next(); 109 | } 110 | // If error, pass to error handler 111 | catch (err) { 112 | next({ 113 | log: "Error caught in apiController.getAPIRelations middleware function", 114 | status: 500, 115 | message: {errMessage: `Error getting API relations for the account`, err: err} 116 | }); 117 | } 118 | }, 119 | 120 | // Get list of API endpoints 121 | async getAPIList(req: Request, res: Response, next: NextFunction) { 122 | 123 | // Create new APIGatewayClient 124 | const apiClient = new APIGatewayClient({ 125 | region: res.locals.region, 126 | credentials: res.locals.credentials, 127 | }); 128 | 129 | // Declare array to store APIs 130 | const apiList: API[] = []; 131 | 132 | try { 133 | // Get list of APIs and store in apiList 134 | const restAPIs: GetRestApisCommandOutput = await apiClient.send(new GetRestApisCommand({})); 135 | const restAPIsItems = restAPIs?.items; 136 | 137 | if (restAPIsItems !== undefined) { 138 | for (const item of restAPIsItems) { 139 | const getResourcesInput: GetResourcesCommandInput = { restApiId: item.id }; 140 | const resources = await apiClient.send(new GetResourcesCommand(getResourcesInput)); 141 | const paths = resources?.items?.map(item => item.path); 142 | const apiDetails: API = {apiName: item.name, apiId: item.id, paths: paths}; 143 | apiList.push(apiDetails); 144 | } 145 | } 146 | res.locals.apiList = apiList; 147 | return next(); 148 | 149 | } catch (err) { 150 | // If error, pass to error handler 151 | next({ 152 | log: "Error caught in apiController.getAPIList middleware function", 153 | status: 500, 154 | message: {errMessage: `Error getting API relations for the account`, err: err} 155 | }); 156 | } 157 | } 158 | } 159 | 160 | export default apiController; 161 | -------------------------------------------------------------------------------- /server/controllers/aws/apiMetricsController.tsx: -------------------------------------------------------------------------------- 1 | import { CloudWatchClient, GetMetricDataCommand, ListDashboardsCommand, GetMetricDataCommandInput, GetMetricDataCommandOutput, MetricDataQuery, MetricDataResult } from "@aws-sdk/client-cloudwatch"; 2 | import { Request, Response, NextFunction } from "express"; 3 | require('dotenv').config(); 4 | 5 | // Controller for getting metrics for all APIs 6 | const apiMetricsController = { 7 | // Gets metrics for each API in res.locals.apiList (from apiController.getAPIList) 8 | getAPIMetrics: async (req: Request, res: Response, next: NextFunction) => { 9 | 10 | // Create a new CloudWatch client with provided credentials and region 11 | const cwClient = new CloudWatchClient({ 12 | region: res.locals.region, 13 | credentials: res.locals.credentials, 14 | }); 15 | 16 | // Declare output object 17 | const allApiMetrics:any = {}; 18 | 19 | const metrics = ['Latency', 'Count', '5XXError', '4XXError'] 20 | 21 | // For each API in res.locals.apiList, get metrics and store in allApiMetrics 22 | for (let apiObj of res.locals.apiList) { 23 | const { apiName } = apiObj; 24 | // Declare obj to store all the metrics of the current API 25 | const currApiMetrics = {}; 26 | 27 | for (let metric of metrics) { 28 | // Obtain input for GetMetricDataCommand using helper function: getCommandInput 29 | const metricParams: GetMetricDataCommandInput = getCommandInput( 30 | apiName, 31 | metric 32 | ); 33 | 34 | try { 35 | // Obtain the data for curr API and curr metric 36 | const currMetricData: GetMetricDataCommandOutput = await cwClient.send( 37 | new GetMetricDataCommand(metricParams) 38 | ); 39 | 40 | interface TimeValObj { 41 | timestamps?: any; 42 | values?: any; 43 | } 44 | 45 | // Declare an object to store the timestamps and values 46 | const timeValObj: TimeValObj = {} 47 | const results = currMetricData?.MetricDataResults; 48 | if (results) { 49 | timeValObj.timestamps = results[0].Timestamps; 50 | timeValObj.values = results[0].Values; 51 | } 52 | (currApiMetrics as any)[metric] = timeValObj; 53 | } 54 | // If error, invoke next middleware function 55 | catch (err) { 56 | next({ 57 | log: "Error caught in apiMetricsController.getAPIMetrics middleware function", 58 | status: 500, 59 | message: {errMessage: `Error getting all API metrics`, err: err} 60 | }); 61 | } 62 | } 63 | allApiMetrics[apiName] = currApiMetrics; 64 | } 65 | res.locals.allApiMetrics = allApiMetrics 66 | return next(); 67 | } 68 | } 69 | 70 | // Helper function to obtain input for GetMetricDataCommand 71 | const getCommandInput = (apiName:string, metricName:string, stat='Sum') => { 72 | 73 | if (metricName === 'Count') { 74 | stat = 'SampleCount'; 75 | } 76 | 77 | const metricParamsBaseAllFunc = { 78 | StartTime: new Date(new Date().setDate(new Date().getDate() - 7)), 79 | EndTime: new Date(), 80 | LabelOptions: { 81 | Timezone: '-0400', 82 | }, 83 | }; 84 | 85 | const metricDataQueryAllfunc = [ 86 | { 87 | Id: `m_API_Gateway_${metricName}`, 88 | Label: `${apiName} API ${metricName}`, 89 | MetricStat: { 90 | Metric: { 91 | Namespace: 'AWS/ApiGateway', 92 | MetricName: metricName, 93 | Dimensions: [{ Name: 'ApiName', Value: apiName }], 94 | }, 95 | Period: 60 * 60 * 24, 96 | Stat: stat, 97 | }, 98 | }, 99 | ]; 100 | 101 | const metricParamsAllfunc = { 102 | ...metricParamsBaseAllFunc, 103 | MetricDataQueries: metricDataQueryAllfunc, 104 | }; 105 | 106 | return metricParamsAllfunc; 107 | 108 | } 109 | 110 | 111 | export default apiMetricsController; 112 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /server/controllers/aws/credentialsController.ts: -------------------------------------------------------------------------------- 1 | import { STSClient, AssumeRoleCommand } from "@aws-sdk/client-sts"; 2 | import { AwsCredentialIdentity } from "@aws-sdk/types"; 3 | import { Request, Response, NextFunction } from "express"; 4 | import dotenv from 'dotenv'; 5 | dotenv.config(); 6 | const AWS = require('aws-sdk'); 7 | 8 | AWS.config.update({region: process.env.AWS_REGION}); 9 | const dynamodb = new AWS.DynamoDB.DocumentClient(); 10 | 11 | const credentials: AwsCredentialIdentity = { 12 | accessKeyId: process.env.AWS_ACCESS_KEY_ID, 13 | secretAccessKey: process.env.AWS_SECRET_KEY, 14 | }; 15 | 16 | const region = process.env.AWS_REGION; 17 | 18 | // Create Amazon Cloudwatch Logs service client object 19 | const client = new STSClient({ region, credentials }); 20 | 21 | // Establish relationship between Nimbus AWS account and client's account 22 | const credentialsController = { 23 | // Get credentials for client's account from AWS and store in res.locals 24 | async getCredentials(req: Request, res: Response, next: NextFunction) { 25 | const roleDetails = { 26 | RoleArn: req.body.arn, //example: 'arn:aws:iam::588640996282:role/NimbusDelegationRole', 27 | RoleSessionName: 'NimbusSession' 28 | }; 29 | 30 | try { 31 | // Granting ourselves permission to client's account 32 | const assumedRole = await client.send(new AssumeRoleCommand(roleDetails)); 33 | const accessKeyId = assumedRole?.Credentials?.AccessKeyId; 34 | const secretAccessKey = assumedRole?.Credentials?.SecretAccessKey; 35 | const sessionToken = assumedRole?.Credentials?.SessionToken; 36 | const expiration = assumedRole?.Credentials?.Expiration; 37 | res.locals.credentials = { accessKeyId, secretAccessKey, sessionToken, expiration }; 38 | res.locals.arnValidation = {validated: true}; 39 | return next(); 40 | } 41 | // If the ARN user input is invalid, send info to front end so that field will be highlighted red 42 | catch (err) { 43 | console.log(err); 44 | res.locals.arnValidation = {validated: false}; 45 | return next(); 46 | } 47 | }, 48 | 49 | // Get credentials for client's account from database and store in res.locals 50 | // This function is used when grabbing information from Lambda, Gateway, etc 51 | async getCredentialsFromDB(req: Request, res: Response, next: NextFunction) { 52 | const { email } = res.locals; 53 | 54 | const params = { 55 | TableName: process.env.TABLE_NAME, 56 | Key: { 57 | 'email' : email, 58 | }, 59 | }; 60 | 61 | const user: any = await dynamodb.get(params).promise(); 62 | 63 | if (user) { 64 | res.locals.arn = user.Item.arn; 65 | res.locals.region = user.Item.region; 66 | } 67 | 68 | const roleDetails = { 69 | RoleArn: res.locals.arn, //example: 'arn:aws:iam::588640996282:role/NimbusDelegationRole', 70 | RoleSessionName: 'NimbusSession' 71 | }; 72 | 73 | try { 74 | // Granting ourselves permission to client's account 75 | const assumedRole = await client.send(new AssumeRoleCommand(roleDetails)); 76 | const accessKeyId = assumedRole?.Credentials?.AccessKeyId; 77 | const secretAccessKey = assumedRole?.Credentials?.SecretAccessKey; 78 | const sessionToken = assumedRole?.Credentials?.SessionToken; 79 | res.locals.credentials = { accessKeyId, secretAccessKey, sessionToken }; 80 | return next(); 81 | } 82 | catch (err) { 83 | console.log(err); 84 | return next({ 85 | log: "Error caught in credentialsController.getCredentialsFromDB middleware function", 86 | status: 500, 87 | message: {errMessage: `Error assigning assumed role to the provided ARN`, errors: err} 88 | }); 89 | } 90 | } 91 | }; 92 | 93 | export default credentialsController; 94 | -------------------------------------------------------------------------------- /server/controllers/aws/lambdaController.ts: -------------------------------------------------------------------------------- 1 | import { LambdaClient, ListFunctionsCommand } from "@aws-sdk/client-lambda"; 2 | import { Request, Response, NextFunction } from "express"; 3 | import dotenv from 'dotenv'; 4 | dotenv.config(); 5 | 6 | const lambdaController = { 7 | // Get list of Lambda functions and store in res.locals.functions 8 | async getFunctions(req: Request, res: Response, next: NextFunction) { 9 | // Create new LambdaClient 10 | const lambdaClient = new LambdaClient({ 11 | region: res.locals.region, 12 | credentials: res.locals.credentials 13 | }); 14 | 15 | // Create new ListFunctionsCommand 16 | const getFunctionsCommand = new ListFunctionsCommand({}); 17 | 18 | try { 19 | const commandResults = await lambdaClient.send(getFunctionsCommand); 20 | const lambdaFunctions = commandResults?.Functions; 21 | const lambdaFunctionDetails = lambdaFunctions?.map(f => f.FunctionName); 22 | res.locals.functions = lambdaFunctionDetails; 23 | return next(); 24 | } catch (err) { 25 | console.log(err); 26 | return next({ 27 | log: "Error caught in lambdaController.getFunctions middleware function", 28 | status: 500, 29 | message: {errMessage: `Error getting functions for the account`, errors: err} 30 | }); 31 | } 32 | } 33 | } 34 | 35 | export default lambdaController; 36 | -------------------------------------------------------------------------------- /server/controllers/aws/logsController.ts: -------------------------------------------------------------------------------- 1 | import { CloudWatchLogsClient, DescribeLogStreamsCommand, GetLogEventsCommand, OutputLogEvent, FilterLogEventsCommand, FilterLogEventsCommandInput, GetLogEventsCommandInput, DescribeLogStreamsCommandInput } from "@aws-sdk/client-cloudwatch-logs"; 2 | import { Request, Response, NextFunction } from "express"; 3 | 4 | 5 | const logsController = { 6 | async getAllLogs(req: Request, res: Response, next: NextFunction) { 7 | try { 8 | // Start a new CloudWatchLogsClient connection with provided region and credentials 9 | const cwLogsClient = new CloudWatchLogsClient({ 10 | region: res.locals.region, //'us-east-1' 11 | credentials: res.locals.credentials, 12 | }); 13 | 14 | const functionName = req.body.functionName; 15 | 16 | // Create inputs for DescribeLogStreamsCommand 17 | const streamsCommandInputs: DescribeLogStreamsCommandInput = { 18 | logGroupName: "/aws/lambda/" + functionName, 19 | } 20 | 21 | const getStreamsCommand = new DescribeLogStreamsCommand(streamsCommandInputs); 22 | const getStreamsCommandResults = await cwLogsClient.send(getStreamsCommand); 23 | const streams = getStreamsCommandResults.logStreams; 24 | const logEvents: OutputLogEvent[] = []; 25 | 26 | if (streams !== undefined) { 27 | // Get logs for each stream 28 | for (const stream of streams) { 29 | const logsCommandInputs: GetLogEventsCommandInput = { 30 | logGroupName: "/aws/lambda/" + functionName, 31 | logStreamName: stream.logStreamName 32 | } 33 | 34 | const getLogsCommand = new GetLogEventsCommand(logsCommandInputs); 35 | const getLogsCommandResults = await cwLogsClient.send(getLogsCommand); 36 | getLogsCommandResults.events?.forEach(e => logEvents.push(e)); 37 | } 38 | } 39 | 40 | res.locals.logs = logEvents.map(e => e.message); 41 | return next(); 42 | } 43 | catch (err) { 44 | return next({ 45 | log: "Error caught in logsController.getAllLogs middleware function", 46 | status: 500, 47 | message: {errMessage: `Error getting logs for this function`, errors: err} 48 | }) 49 | } 50 | 51 | }, 52 | 53 | // Get filtered logs for a given func and store in res.locals.filteredLogs 54 | async getFilteredLogs(req: Request, res: Response, next: NextFunction) { 55 | 56 | // StartTime and EndTime for CloudWatchLogsClient need to be in millisecond 57 | try { 58 | let StartTime; 59 | if (req.body.period === '1hr') { 60 | StartTime = new Date( 61 | new Date().setMinutes(new Date().getMinutes() - 60) 62 | ).valueOf(); 63 | } else if (req.body.period === '1d') { 64 | StartTime = new Date( 65 | new Date().setDate(new Date().getDate() - 1) 66 | ).valueOf(); 67 | } else if (req.body.period === '7d' || req.body.period === '' ) { 68 | StartTime = new Date( 69 | new Date().setDate(new Date().getDate() - 7) 70 | ).valueOf(); 71 | } else if (req.body.period === '14d') { 72 | StartTime = new Date( 73 | new Date().setDate(new Date().getDate() - 14) 74 | ).valueOf(); 75 | } else if (req.body.period === '30d') { 76 | StartTime = new Date( 77 | new Date().setDate(new Date().getDate() - 30) 78 | ).valueOf(); 79 | } 80 | 81 | const filterPattern: string = req.body.filterPattern; 82 | 83 | // Start a new CloudWatchLogsClient connection with provided region and credentials 84 | const cwLogsClient = new CloudWatchLogsClient({ 85 | region: res.locals.region, //'us-east-1' 86 | credentials: res.locals.credentials, //credentials 87 | }); 88 | 89 | const functionName = req.body.functionName 90 | 91 | // Create input for FilterLogEventsCommand 92 | const filterCommandInputs: FilterLogEventsCommandInput = { 93 | logGroupName: "/aws/lambda/" + functionName, 94 | startTime: StartTime, 95 | endTime: new Date().valueOf(), 96 | filterPattern: filterPattern 97 | } 98 | 99 | // Get filtered logs and store in res.locals.filteredLogs 100 | const filterLogsCommand = new FilterLogEventsCommand(filterCommandInputs); 101 | const filterLogsCommandResults = await cwLogsClient.send(filterLogsCommand); 102 | res.locals.filteredLogs = filterLogsCommandResults.events?.map(e => e.message); 103 | return next(); 104 | } 105 | catch(err) { 106 | return next({ 107 | log: "Error caught in logsController.getFilteredLogs middleware function", 108 | status: 500, 109 | message: {errMessage: `Error getting filtered logs for this function`, err: err} 110 | }) 111 | } 112 | } 113 | } 114 | 115 | export default logsController; -------------------------------------------------------------------------------- /server/controllers/aws/metricsController.ts: -------------------------------------------------------------------------------- 1 | import { CloudWatchClient, GetMetricDataCommand, GetMetricDataCommandInput, MetricDataQuery, MetricDataResult} from "@aws-sdk/client-cloudwatch"; 2 | import { LambdaClient, GetFunctionConfigurationCommand} from "@aws-sdk/client-lambda" 3 | import { Request, Response, NextFunction } from "express"; 4 | import { subMetrics, Metrics } from "../../types"; 5 | require('dotenv').config(); 6 | 7 | const metricsController = { 8 | // Grab the Invocation, Error, Duration, and Throttle metrics for all functions 9 | async getAllMetrics(req: Request, res: Response, next: NextFunction) { 10 | try { 11 | // Initiate client with credentials 12 | const client = new CloudWatchClient({ 13 | region: res.locals.region, 14 | credentials: res.locals.credentials 15 | }) 16 | 17 | // Specify parameters for each metric 18 | const metricMemoryData = { 19 | Id: "m1", 20 | MetricStat: { 21 | Metric: { 22 | MetricName: "MemoryUsage", 23 | Namespace: "AWS/Lambda", 24 | }, 25 | Period: 60 * 60 * 24, 26 | Stat: "Average", 27 | }, 28 | Label: "Average memory usage of Lambda Functions" 29 | } 30 | const metricInvocationData = { 31 | Id: "i1", 32 | MetricStat: { 33 | Metric: { 34 | MetricName: "Invocations", 35 | Namespace: "AWS/Lambda", 36 | }, 37 | Period: 60 * 60 * 24, 38 | Stat: "Sum", 39 | }, 40 | Label: "Total Invocations of Lambda Functions" 41 | } 42 | const metricErrorData = { 43 | Id: "e1", 44 | MetricStat: { 45 | Metric: { 46 | MetricName: "Errors", 47 | Namespace: "AWS/Lambda" 48 | }, 49 | Period: 60 * 60 * 24, 50 | Stat: "Sum", 51 | }, 52 | Label: "Total Errors of Lambda Functions" 53 | } 54 | const metricThrottlesData = { 55 | Id: "t1", 56 | MetricStat: { 57 | Metric: { 58 | MetricName: "Throttles", 59 | Namespace: "AWS/Lambda" 60 | }, 61 | Period: 60 * 60 * 24, 62 | Stat: "Sum", 63 | }, 64 | Label: "Total Throttles of Lambda Functions" 65 | } 66 | const metricDurationData = { 67 | Id: "d1", 68 | MetricStat: { 69 | Metric: { 70 | MetricName: "Duration", 71 | Namespace: "AWS/Lambda" 72 | }, 73 | Period: 60 * 60 * 24, 74 | Stat: "Sum", 75 | }, 76 | Label: "Total Duration of Lambda Functions" 77 | } 78 | 79 | // Create input for GetMetricDataCommand 80 | const input: GetMetricDataCommandInput = { 81 | "StartTime": new Date(new Date().setDate(new Date().getDate() - 30)), 82 | "EndTime": new Date(), 83 | "MetricDataQueries": [metricInvocationData, metricErrorData, metricThrottlesData, metricDurationData, metricMemoryData], 84 | } 85 | const command = new GetMetricDataCommand(input) 86 | const response = await client.send(command); 87 | // Create a metrics object to send the values and timestamps of each metric to front end 88 | if (response.MetricDataResults) { 89 | const metrics: Metrics = { 90 | invocations: { 91 | values: response.MetricDataResults[0].Values, 92 | timestamp: response.MetricDataResults[0].Timestamps 93 | }, 94 | errors: { 95 | values: response.MetricDataResults[1].Values, 96 | timestamp: response.MetricDataResults[1].Timestamps 97 | }, 98 | throttles: { 99 | values: response.MetricDataResults[2].Values, 100 | timestamp: response.MetricDataResults[2].Timestamps 101 | }, 102 | duration: { 103 | values: response.MetricDataResults[3].Values, 104 | timestamp: response.MetricDataResults[3].Timestamps 105 | }, 106 | }; 107 | res.locals.allFuncMetrics = metrics; 108 | } 109 | return next(); 110 | } catch (err) { 111 | return next({ 112 | log: "Error caught in metricsController.getAllMetrics middleware function", 113 | status: 500, 114 | message: { err: "Error grabbing metrics for all Lambda Functions" } 115 | }) 116 | } 117 | }, 118 | // Grab metrics from cloudwatch depending on user input (seleted func) 119 | async getMetricsByFunc (req: Request, res: Response, next: NextFunction) { 120 | try { 121 | // Start a new CloudWatchClient instance 122 | const client = new CloudWatchClient({ 123 | region: res.locals.region, 124 | credentials: res.locals.credentials 125 | }) 126 | const metricData: MetricDataQuery[] = [] 127 | // functions from lamda controller 128 | res.locals.functions.forEach((functionName:string, i:number) => { 129 | const metricInvocationData = { 130 | Id: `i${i}`, 131 | MetricStat: { 132 | Metric: { 133 | MetricName: "Invocations", 134 | Namespace: "AWS/Lambda", 135 | Dimensions: [ 136 | { 137 | Name: 'FunctionName', 138 | Value: `${functionName}` 139 | }, 140 | ], 141 | }, 142 | Period: 60 * 60 * 24, 143 | Stat: "Sum", 144 | }, 145 | Label: `${functionName} Total invocations of Lambda Function` 146 | } 147 | metricData.push(metricInvocationData) 148 | 149 | const metricErrorData = { 150 | Id: `e${i}`, 151 | MetricStat: { 152 | Metric: { 153 | MetricName: "Errors", 154 | Namespace: "AWS/Lambda", 155 | Dimensions: [ 156 | { 157 | Name: 'FunctionName', 158 | Value: `${functionName}` 159 | }, 160 | ], 161 | }, 162 | Period: 60 * 60 * 24, 163 | Stat: "Sum", 164 | }, 165 | Label: `${functionName} Total errors of Lambda Function` 166 | } 167 | metricData.push(metricErrorData) 168 | 169 | const metricThrottlesData = { 170 | Id: `t${i}`, 171 | MetricStat: { 172 | Metric: { 173 | MetricName: "Throttles", 174 | Namespace: "AWS/Lambda", 175 | Dimensions: [ 176 | { 177 | Name: 'FunctionName', 178 | Value: `${functionName}` 179 | }, 180 | ], 181 | }, 182 | Period: 60 * 60 * 24, 183 | Stat: "Sum", 184 | }, 185 | Label: `${functionName} Total throttles of Lambda Function` 186 | } 187 | metricData.push(metricThrottlesData) 188 | 189 | const metricDurationData = { 190 | Id: `d${i}`, 191 | MetricStat: { 192 | Metric: { 193 | MetricName: "Duration", 194 | Namespace: "AWS/Lambda", 195 | Dimensions: [ 196 | { 197 | Name: 'FunctionName', 198 | Value: `${functionName}` 199 | }, 200 | ], 201 | }, 202 | Period: 60 * 60 * 24, 203 | Stat: "Sum", 204 | }, 205 | Label: `${functionName} Total duration of Lambda Function` 206 | } 207 | metricData.push(metricDurationData) 208 | }) 209 | // Input to get metric data command 210 | const input: GetMetricDataCommandInput = { 211 | "StartTime": new Date(new Date().setDate(new Date().getDate() - 30)), 212 | "EndTime": new Date(), 213 | "MetricDataQueries": metricData 214 | } 215 | const command = new GetMetricDataCommand(input) 216 | 217 | const response = await client.send(command); 218 | // Create a metrics object to store the values and timestamps of specific metric 219 | if (response.MetricDataResults) { 220 | 221 | // Parse data into an object where keys are function names and values are the metrics for each function 222 | const parseData = (arr: MetricDataResult[]) => { 223 | const allFuncMetrics = {}; 224 | // :oop over elements in arr in chunks of 4 225 | for (let i = 0; i < arr.length; i+=4) { 226 | // Get function name 227 | const funcName = arr[i].Label!.split(' ')[0]; 228 | const metricsByFunc = {}; 229 | // Populate allMetricsObj 230 | // Loop over number of metrics 231 | for (let j = 0; j < 4; j++) { 232 | const metricName = arr[i+j].Label!.split(' ')[2]; 233 | // Declare func object 234 | const singleMetric: subMetrics = { 235 | values: arr[i+j].Values, 236 | timestamp: arr[i+j].Timestamps 237 | }; 238 | (metricsByFunc as any)[metricName] = singleMetric; 239 | } 240 | (allFuncMetrics as any)[funcName] = metricsByFunc; 241 | } 242 | return allFuncMetrics; 243 | } 244 | // Metrics data for the functions page 245 | res.locals.eachFuncMetrics = parseData(response.MetricDataResults) 246 | } 247 | return next(); 248 | } catch (err) { 249 | return next({ 250 | log: "Error caught in metricsController.getMetricsByFunc middleware function", 251 | status: 500, 252 | message: { err: "Error grabbing metrics for Lambda Function" } 253 | }) 254 | } 255 | }, 256 | // Grab required properties to calculate cost of application 257 | async getCostProps(req: Request, res: Response, next: NextFunction) { 258 | try { 259 | // Start a new LambdaClient instance 260 | const client = new LambdaClient({ 261 | region: res.locals.region, 262 | credentials: res.locals.credentials 263 | }); 264 | 265 | const memory: Array = []; 266 | const invocations: Array = []; 267 | const duration: Array = []; 268 | // For each function grab the memory allocated, total invocations, and total duration 269 | for (const funcName of res.locals.functions) { 270 | const command = new GetFunctionConfigurationCommand({FunctionName: funcName}); 271 | const response = await client.send(command) 272 | if (response.MemorySize) { 273 | memory.push(response.MemorySize) 274 | if (res.locals.eachFuncMetrics[funcName].invocations.values.length > 0) { 275 | invocations.push(res.locals.eachFuncMetrics[funcName].invocations.values.reduce((acc: number, curr: number) => acc + curr)) 276 | } else { 277 | invocations.push(res.locals.eachFuncMetrics[funcName].invocations.values[0]) 278 | } 279 | if (res.locals.eachFuncMetrics[funcName].duration.values.length > 0) { 280 | duration.push(res.locals.eachFuncMetrics[funcName].duration.values.reduce((acc: number, curr: number) => acc + curr)) 281 | } else { 282 | duration.push(res.locals.eachFuncMetrics[funcName].duration.values[0]) 283 | } 284 | } 285 | } 286 | 287 | // Store cost related arrays in res.locals 288 | res.locals.cost = { 289 | memory, 290 | invocations, 291 | duration 292 | } 293 | 294 | return next(); 295 | } catch (err) { 296 | return next({ 297 | log: "Error caught in metricsController.getCostProps middleware function", 298 | status: 500, 299 | message: { err: "Error grabbing cost for all Lambda Function" } 300 | }) 301 | } 302 | } 303 | } 304 | 305 | // Change to export default syntaix 306 | export default metricsController; -------------------------------------------------------------------------------- /server/controllers/userController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | import { userController , KeyType, KeyTypeSettings, KeyTypePassword } from '../types'; 3 | require('dotenv').config(); 4 | const AWS = require('aws-sdk'); 5 | const bcrypt = require('bcrypt'); 6 | const SALT_WORK_FACTOR = 10; 7 | const ACCESS_TOKEN_SECRET = process.env.ACCESS_TOKEN_SECRET; 8 | const jwt = require('jsonwebtoken'); 9 | 10 | AWS.config.update({region: process.env.AWS_REGION}); 11 | const dynamodb = new AWS.DynamoDB.DocumentClient(); 12 | 13 | const userController: userController = { 14 | 15 | // Middleware for user login 16 | async verifyUser(req, res, next) { 17 | const { email, password } = req.body; 18 | 19 | const params = { 20 | TableName: process.env.TABLE_NAME, 21 | Key: { 22 | 'email' : email, 23 | }, 24 | }; 25 | 26 | try { 27 | const user: any = await dynamodb.get(params).promise(); 28 | 29 | // If the user does not exist in the database, invoke global error handler 30 | if (!user) { 31 | return next({ 32 | log: "Error caught in userController.verifyUser middleware function", 33 | status: 500, 34 | message: {err: 'User not in database'} 35 | }); 36 | }; 37 | 38 | // If user exists with cooresponding email, compare password from client with password in database 39 | const isValid: boolean = await bcrypt.compare(password, user.Item.password); 40 | 41 | if (!isValid) { 42 | return next({ 43 | log: "Error caught in userController.verifyUser middleware function", 44 | status: 500, 45 | message: {err: 'Wrong password'} 46 | }); 47 | }; 48 | 49 | res.locals.email = email; 50 | return next(); 51 | // All other errors, invoke global error handler 52 | } catch (err) { 53 | return next({ 54 | log: "Error caught in userController.verifyUser middleware function", 55 | status: 500, 56 | message: {err: `Error logging in`} 57 | }) 58 | } 59 | }, 60 | 61 | // Middleware for user registration 62 | async createUser (req, res, next) { 63 | const { email, firstName, lastName, password, confirmation, arn, region } = req.body; 64 | const { arnValidation } = res.locals; 65 | 66 | // Declare an array to store errors 67 | const errors: Array<"email" | "firstName" | "lastName" | "password" | "confirmation" | "arn" | "region"> = []; 68 | 69 | // Validate email: 70 | if (/^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/g.test(email) === false) { 71 | errors.push("email"); 72 | } 73 | 74 | // Check if user left any input fields empty 75 | // Front end will highlight input fields if errors occur 76 | for (const key in req.body) { 77 | if (req.body[key as KeyType].length === 0) errors.push(key as KeyType); 78 | }; 79 | 80 | // Check if password matches confirmation 81 | if (password !== confirmation) errors.push("password", "confirmation"); 82 | 83 | // Check if arn is validated 84 | if (!arnValidation.validated) errors.push("arn"); 85 | 86 | // Send errors array to the front end if they exist 87 | if (errors.length > 0) { 88 | return next({ 89 | log: "Error caught in userController.createUser middleware function", 90 | status: 500, 91 | message: {errMessage: `Error found in user input`, errors: errors} 92 | }) 93 | }; 94 | 95 | // If the registration form was successfully filled out, create a new user in the database 96 | try { 97 | const hashedPass = await bcrypt.hash(password, SALT_WORK_FACTOR); 98 | 99 | const params = { 100 | TableName: process.env.TABLE_NAME, 101 | Item: { 102 | 'email' : email , 103 | 'firstName' : firstName , 104 | 'lastName' : lastName , 105 | 'password' : hashedPass , 106 | 'arn' : arn , 107 | 'region' : region , 108 | }, 109 | }; 110 | 111 | const user = await dynamodb.put(params).promise(); 112 | 113 | res.locals.user = user; 114 | return next(); 115 | // Invoke global error handler if a DB error occurs 116 | } catch (err) { 117 | return next({ 118 | log: "Error caught in userController.signupUser middleware function", 119 | status: 500, 120 | message: {errMessage: `Error inserting user to database`, errors: errors} 121 | }) 122 | } 123 | }, 124 | 125 | // Middleware for grabbing user info on Settings tab 126 | async getUser(req, res, next) { 127 | const { email } = res.locals; 128 | 129 | const params = { 130 | TableName: process.env.TABLE_NAME, 131 | Key: { 132 | 'email' : email 133 | }, 134 | }; 135 | 136 | const user: any = await dynamodb.get(params).promise(); 137 | 138 | res.locals.user = { 139 | email: user.Item.email, 140 | firstName: user.Item.firstName, 141 | lastName: user.Item.lastName, 142 | arn: user.Item.arn, 143 | region: user.Item.region 144 | } 145 | return next(); 146 | }, 147 | 148 | // Middleware for updating user settings 149 | async updateUserProfile(req, res, next) { 150 | const originalEmail = res.locals.email; 151 | const { firstName, lastName, arn, region } = req.body; 152 | const { arnValidation } = res.locals; 153 | // Declare an array to store errors 154 | const errors: Array<"firstName" | "lastName" | "arn" | "region"> = []; 155 | 156 | // Check if input fields are empty 157 | for (const key in req.body) { 158 | if (req.body[key as KeyTypeSettings].length === 0) errors.push(key as KeyTypeSettings); 159 | } 160 | 161 | //Check if arn is validated 162 | if (!arnValidation.validated) errors.push("arn"); 163 | 164 | // Send errors array to the front end 165 | if (errors.length > 0) { 166 | return next({ 167 | log: "Error caught in userController.updateUserProfile middleware function", 168 | status: 500, 169 | message: {errMessage: `Error found in user input`, errors: errors} 170 | }) 171 | }; 172 | 173 | const originalUserParams = { 174 | TableName: process.env.TABLE_NAME, 175 | Key: { 176 | 'email' : originalEmail, 177 | }, 178 | } 179 | 180 | try { 181 | const params = { 182 | TableName: process.env.TABLE_NAME, 183 | Key: { 184 | 'email' : originalEmail 185 | }, 186 | UpdateExpression: 'set firstName = :value1, lastName = :value2', 187 | ExpressionAttributeValues: { 188 | ':value1' : firstName, 189 | ':value2' : lastName, 190 | }, 191 | ReturnValues: 'ALL_NEW' 192 | }; 193 | 194 | // Update user in database with hashedPass as password 195 | const updatedUser = await dynamodb.update(params).promise(); 196 | 197 | res.locals.user = { 198 | 'email' : originalEmail , 199 | 'firstName' : firstName , 200 | 'lastName' : lastName , 201 | 'arn' : arn , 202 | 'region' : region , 203 | }; 204 | 205 | return next(); 206 | } catch (err) { 207 | return next({ 208 | log: "Error caught in userController.updateUserProfile middleware function" + err, 209 | status: 500, 210 | message: {errMessage: `Error updating user's profile`, errors: errors} 211 | }) 212 | } 213 | }, 214 | 215 | async updateUserPassword(req, res, next) { 216 | const originalEmail = res.locals.email; 217 | const { password, confirmation } = req.body; 218 | // Declare an array to store errors 219 | const errors: Array< "password" | "confirmation" > = []; 220 | 221 | // Check if input fields are empty 222 | for (const key in req.body) { 223 | if (req.body[key as KeyTypePassword].length === 0) errors.push(key as KeyTypePassword); 224 | 225 | } 226 | // Check if password matches confirmation 227 | if (password !== confirmation) errors.push("password", "confirmation"); 228 | 229 | // Send errors array back to front end 230 | if (errors.length > 0) { 231 | return next({ 232 | log: "Error caught in userController.updateUserPassword middleware function", 233 | status: 500, 234 | message: {errMessage: `Error found in user input`, errors: errors} 235 | }) 236 | }; 237 | 238 | try { 239 | const hashedPass = await bcrypt.hash(password, SALT_WORK_FACTOR); 240 | 241 | const params = { 242 | TableName: process.env.TABLE_NAME, 243 | Key: { 244 | 'email' : originalEmail 245 | }, 246 | UpdateExpression: `set password = :value1`, 247 | ExpressionAttributeValues: { 248 | ':value1' : hashedPass 249 | }, 250 | }; 251 | 252 | // create a new user in database with hashedPass as password 253 | const updatedUser = await dynamodb.update(params).promise(); 254 | 255 | res.locals.success = {successMessage: 'Password updated!'}; 256 | return next(); 257 | } catch (err) { 258 | return next({ 259 | log: "Error caught in userController.updateUserPassword middleware function", 260 | status: 500, 261 | message: {errMessage: `Error updating the password`, errors: errors} 262 | }) 263 | } 264 | }, 265 | }; 266 | 267 | export default userController; 268 | -------------------------------------------------------------------------------- /server/routes/authRouter.ts: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const path = require('path'); 3 | import { Request, Response } from 'express' 4 | import userController from '../controllers/userController' 5 | import credentialsController from '../controllers/aws/credentialsController' 6 | import authController from '../controllers/authController' 7 | import metricsController from '../controllers/aws/metricsController' 8 | const router = express.Router(); 9 | 10 | // Handle post requests sent to /login endpoint from the client 11 | router.post('/login', userController.verifyUser, authController.generateJWT, (req: Request, res: Response) => { 12 | return res.status(200).send({ 13 | email: res.locals.email, 14 | success: res.locals.success, 15 | accessToken: res.locals.accessToken, 16 | }); 17 | }); 18 | 19 | // Handle post requests sent to /signup endpoint 20 | router.post('/register', credentialsController.getCredentials, userController.createUser, authController.generateJWT, (req: Request, res: Response) => { 21 | return res.status(200).json({ 22 | accessToken: res.locals.accessToken, 23 | }) 24 | }); 25 | 26 | router.get('/verifyToken', authController.verifyToken, (req: Request, res: Response) => { 27 | return res.status(200).json({ 28 | message: res.locals.accessToken ? 'YOU ARE AUTHENTICATED' : 'NOT AUTHENTICATED', 29 | accessToken: res.locals.accessToken, 30 | }); 31 | }); 32 | 33 | router.get('/logout', authController.removeToken, (req: Request, res: Response) => { 34 | return res.status(200).json({ 35 | message: 'YOU ARE LOGGED OUT' 36 | }); 37 | }); 38 | 39 | module.exports = router; 40 | -------------------------------------------------------------------------------- /server/routes/dashboardRouter.ts: -------------------------------------------------------------------------------- 1 | const express = require('express') ; 2 | import { Request, Response } from 'express'; 3 | import apiController from '../controllers/aws/apiController'; 4 | const router = express.Router(); 5 | const settingsRouter = require('./settingsRouter'); 6 | 7 | import authController from '../controllers/authController'; 8 | import credentialsController from '../controllers/aws/credentialsController'; 9 | import lambdaController from '../controllers/aws/lambdaController'; 10 | import logsController from '../controllers/aws/logsController'; 11 | import metricsController from '../controllers/aws/metricsController'; 12 | import userController from '../controllers/userController'; 13 | import apiMetricsController from '../controllers/aws/apiMetricsController' 14 | 15 | // All routes verify JWT Token to get email 16 | // Email is used to query the database for ARN 17 | // ARN is used to get credentials from client's AWS account 18 | // Credentials used to grab metrics 19 | 20 | router.use(authController.verifyToken, credentialsController.getCredentialsFromDB); 21 | 22 | router.get('/allMetrics', metricsController.getAllMetrics, lambdaController.getFunctions, metricsController.getMetricsByFunc, metricsController.getCostProps, (req: Request, res: Response) => { 23 | return res.status(200).json({ 24 | allFuncMetrics: res.locals.allFuncMetrics, 25 | cost: res.locals.cost 26 | }); 27 | }); 28 | 29 | router.get('/funcmetrics', lambdaController.getFunctions, metricsController.getMetricsByFunc, (req: Request, res: Response) => { 30 | return res.status(200).json({ 31 | eachFuncMetrics: res.locals.eachFuncMetrics, 32 | }); 33 | }); 34 | 35 | router.get('/functions', lambdaController.getFunctions, (req: Request, res: Response) => { 36 | return res.status(200).json({ 37 | functions: res.locals.functions 38 | }); 39 | }); 40 | 41 | // Handles POST Requests to get Logs for all functions and the ability to filter 42 | router.post('/allLogs', logsController.getAllLogs, (req: Request, res: Response) => { 43 | return res.status(200).json({ 44 | logs: res.locals.logs 45 | }); 46 | }); 47 | 48 | router.post('/filteredLogs', logsController.getFilteredLogs, (req: Request, res: Response) => { 49 | return res.status(200).json({ 50 | filteredLogs: res.locals.filteredLogs 51 | }); 52 | }); 53 | 54 | // Handles GET/POST Requests to grab API Metrics + Relationships 55 | router.post('/apiRelations', lambdaController.getFunctions, apiController.getAPIRelations, (req: Request, res: Response) => { 56 | return res.status(200).json({ 57 | apiRelations: res.locals.apiRelations 58 | }); 59 | }); 60 | 61 | 62 | router.get('/apiList', apiController.getAPIList, (req: Request, res: Response) => { 63 | return res.status(200).json({ 64 | apiList: res.locals.apiList 65 | }); 66 | }); 67 | 68 | router.get('/apiMetrics', apiController.getAPIList, apiMetricsController.getAPIMetrics, (req: Request, res: Response) => { 69 | return res.status(200).json({ 70 | allApiMetrics: res.locals.allApiMetrics 71 | }); 72 | }); 73 | 74 | router.get('/apiList', apiController.getAPIList, (req: Request, res: Response) => { 75 | return res.status(200).json({ 76 | apiList: res.locals.apiList 77 | }); 78 | }); 79 | 80 | router.use('/settings', settingsRouter); 81 | 82 | module.exports = router; -------------------------------------------------------------------------------- /server/routes/settingsRouter.ts: -------------------------------------------------------------------------------- 1 | const express = require('express') ; 2 | const router = express.Router(); 3 | import { Request, Response } from 'express'; 4 | import credentialsController from '../controllers/aws/credentialsController'; 5 | import userController from '../controllers/userController'; 6 | 7 | 8 | //Handles GET/POST requests to the Settings Tab 9 | router.get('/userDetails', userController.getUser, (req: Request, res: Response) => { 10 | return res.status(200).json(res.locals.user); 11 | }); 12 | 13 | router.post('/updateProfile', credentialsController.getCredentials, userController.updateUserProfile, (req: Request, res: Response) => { 14 | return res.status(200).json(res.locals.user); 15 | }); 16 | 17 | router.post('/updatePassword', userController.updateUserPassword, (req: Request, res: Response) => { 18 | return res.status(200).json(res.locals.success); 19 | }); 20 | 21 | 22 | module.exports = router; -------------------------------------------------------------------------------- /server/server.ts: -------------------------------------------------------------------------------- 1 | import express, { Express, Request, Response, NextFunction } from 'express'; 2 | import { ErrorObj } from './types'; 3 | import authController from './controllers/authController'; 4 | const dotenv = require('dotenv'); 5 | const cookieParser = require('cookie-parser'); 6 | const path = require("path"); 7 | dotenv.config(); 8 | const app = express(); 9 | const { PORT } = process.env; 10 | 11 | app.use(express.json()); 12 | const authRouter = require('./routes/authRouter'); 13 | const dashboardRouter = require('./routes/dashboardRouter'); 14 | 15 | app.use(cookieParser()); 16 | app.use(express.static(path.resolve(__dirname, '../build'))); 17 | 18 | 19 | app.get('/', function(req, res) { 20 | return res.sendFile(path.resolve(__dirname, '../build/index.html')); 21 | }); 22 | 23 | 24 | app.use('/', authRouter); 25 | app.use('/dashboard', dashboardRouter); 26 | 27 | // Handle all remaining endpoints that are not defined in the server/routers 28 | app.use('*', (req: Request, res: Response) => { 29 | return res.status(404).json('Not Found'); 30 | }); 31 | 32 | // Global Error Handler 33 | app.use((err: Error, req: Request, res: Response, next: NextFunction) => { 34 | const defaultErr: ErrorObj = { 35 | log: "Express error handler caught unknown middleware error", 36 | status: 500, 37 | message: {err: "Global error handler invoked"}, 38 | } 39 | const error = Object.assign({}, defaultErr, err); 40 | console.log(`${error.log}: ${error.message.err}`); 41 | return res.status(error.status).json(error.message); 42 | }) 43 | 44 | app.listen(PORT, () => { 45 | console.log(`Server is running at https://localhost:${PORT}`); 46 | }); 47 | 48 | module.exports = app; -------------------------------------------------------------------------------- /server/types.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | 3 | export type ErrorObj = { 4 | log: string, 5 | status: number, 6 | message: {err: string} 7 | }; 8 | 9 | export type userController = { 10 | verifyUser: (req: Request, res: Response, next: NextFunction) => Promise>>; 11 | createUser: (req: Request, res: Response, next: NextFunction) => Promise; 12 | getUser: (req: Request, res: Response, next: NextFunction) => Promise; 13 | updateUserProfile: (req: Request, res: Response, next: NextFunction) => Promise; 14 | updateUserPassword: (req: Request, res: Response, next: NextFunction) => Promise; 15 | }; 16 | 17 | export type KeyType = "email" | "firstName" | "lastName" | "password" | "confirmation" | "arn" | "region"; 18 | 19 | export type KeyTypeSettings = "firstName" | "lastName" | "arn" | "region"; 20 | 21 | export type KeyTypePassword = "password" | "confirmation"; 22 | 23 | // Created an interface for verifyToken to store the token into the request 24 | export interface getUserToken extends Request { 25 | token: string; 26 | }; 27 | 28 | export type authController = { 29 | verifyToken: (req: getUserToken, res: Response, next: NextFunction) => Promise; 30 | generateJWT: (req: Request, res: Response, next: NextFunction) => Promise; 31 | removeToken: (req: Request, res: Response, next: NextFunction) => void; 32 | }; 33 | 34 | 35 | export type Endpoint = { 36 | apiId: string; 37 | apiMethod: string; 38 | apiPath: string; 39 | }; 40 | 41 | export type LambdaAPIs = { 42 | functionName: string; 43 | endpoints: (Endpoint | undefined)[]; 44 | }; 45 | 46 | export type API = { 47 | apiName: (string | undefined); 48 | apiId: (string | undefined); 49 | paths: (string | undefined)[] | undefined; 50 | }; 51 | 52 | export type Relation = { 53 | apiName: string | undefined; 54 | endpoints: { [key: string]: { method: string, func: string }[] } | undefined; 55 | }; 56 | 57 | export interface subMetrics { 58 | values: number[] | undefined 59 | timestamp: Date[] | undefined 60 | }; 61 | 62 | export interface Metrics { 63 | invocations: subMetrics, 64 | errors: subMetrics, 65 | throttles: subMetrics, 66 | duration: subMetrics 67 | }; -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './client/components/**/*.{js,jsx,ts,tsx}', 5 | './client/containers/**/*.{js,jsx,ts,tsx}', 6 | './client/**/*.{js,jsx,ts,tsx}' 7 | ], 8 | theme: { 9 | extend: {}, 10 | }, 11 | plugins: [require("daisyui")], 12 | daisyui: { 13 | themes: [ 14 | { 15 | myThemeDark: { 16 | "primary": "#623cad", 17 | "secondary": "#3a3279", 18 | "accent": "#ae6db0", 19 | "neutral": "#232343", 20 | "base-100": "#0f0f31", 21 | "base-300": "#e4d5ff", 22 | "info": "#0CA6E9", 23 | "success": "#2BD4BD", 24 | "warning": "#F4C152", 25 | "error": "#FB6F84", 26 | }, 27 | myThemeLight: { 28 | "primary": "#DECCFF", 29 | "secondary": "#ece2ff", 30 | "accent": "#f6d5ff", 31 | "neutral": "#f6eefd", 32 | "base-100": "#FFFFFF", 33 | "base-200": "#f6f6f7", 34 | "base-300": '#5d597a', 35 | "info": "#3ABFF8", 36 | "success": "#36D399", 37 | "warning": "#FBBD23", 38 | "error": "#F87272", 39 | }, 40 | }, 41 | ], 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 4 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 5 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 6 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 7 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 8 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 9 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 10 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 11 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 12 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 13 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 14 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 15 | 16 | /* Modules */ 17 | "module": "commonjs", /* Specify what module code is generated. */ 18 | // "rootDir": "./", /* Specify the root folder within your source files. */ 19 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 20 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 21 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 22 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 23 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 24 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 25 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 26 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 27 | // "resolveJsonModule": true, /* Enable importing .json files. */ 28 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 29 | 30 | /* JavaScript Support */ 31 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 32 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 33 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 34 | 35 | /* Emit */ 36 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 37 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 38 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 39 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 40 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 41 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 42 | // "removeComments": true, /* Disable emitting comments. */ 43 | // "noEmit": true, /* Disable emitting files from a compilation. */ 44 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 45 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 46 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 47 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 48 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 49 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 50 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 51 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 52 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 53 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 54 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 55 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 56 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 57 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 58 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 59 | 60 | /* Interop Constraints */ 61 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 62 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 63 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 64 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 65 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 66 | 67 | /* Type Checking */ 68 | "strict": true, /* Enable all strict type-checking options. */ 69 | "jsx": "react", 70 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 71 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 72 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 73 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 74 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 75 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 76 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 77 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 78 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 79 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 80 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 81 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 82 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 83 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 84 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 85 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 86 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 87 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 88 | 89 | /* Completeness */ 90 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 91 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 92 | }, 93 | } 94 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | 5 | const config = { 6 | entry: ['./client/index.tsx'], 7 | output: { 8 | path: path.resolve(__dirname, 'build'), 9 | filename: 'bundle.js', 10 | }, 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.(js|jsx)$/, 15 | use: 'babel-loader', 16 | exclude: /node_modules/, 17 | }, 18 | { 19 | test: /\.(scss|css)$/, 20 | use: [ 21 | 'style-loader', 22 | { 23 | loader: 'css-loader', 24 | options: { 25 | importLoaders: 1, 26 | }, 27 | }, 28 | 'sass-loader', 29 | 'postcss-loader', 30 | ], 31 | }, 32 | { 33 | test: /\.ts(x)?$/, 34 | loader: 'ts-loader', 35 | exclude: /node_modules/, 36 | }, 37 | { 38 | test: /\.(png|jpe?g|gif)$/i, 39 | use: [ 40 | { 41 | loader: 'file-loader', 42 | }, 43 | ], 44 | }, 45 | ], 46 | }, 47 | devServer: { 48 | port: 8080, 49 | hot: true, 50 | static: { 51 | directory: './dist', 52 | }, 53 | proxy: { 54 | '/': { 55 | target: 'http://localhost:3000', 56 | // secure: true, 57 | }, 58 | }, 59 | }, 60 | resolve: { 61 | extensions: ['.tsx', '.ts', '.js'], 62 | }, 63 | plugins: [ 64 | new HtmlWebpackPlugin({ 65 | template: path.join(__dirname, './client/index.html'), 66 | }), 67 | ], 68 | }; 69 | 70 | module.exports = config; 71 | --------------------------------------------------------------------------------