├── .eslintrc.json ├── .gitignore ├── Code_of_Conduct.md ├── Contributions.md ├── LICENSE.md ├── README-DeveloperGuide.md ├── README.md ├── __tests__ ├── apiRouter_tests.js └── db_tests.js ├── index.html ├── jest.config.js ├── package-lock.json ├── package.json ├── postgres_table_create.sql ├── server ├── aws │ └── cloudFormation.yaml ├── controllers │ ├── MetricsController.js │ ├── authControllers.js │ ├── cookieControllers.js │ ├── credentialController.js │ ├── dbControllers.js │ ├── encryptionController.js │ ├── fileController.js │ ├── lambdaLogsController.js │ ├── listLambdasController.js │ └── tracesController.js ├── models │ └── dbPool.js ├── redis.js ├── routes │ └── api.js └── server.js ├── src ├── App.jsx ├── Chart │ ├── BarChart.jsx │ ├── BubbleChart.jsx │ ├── Circle.jsx │ ├── CircleChart.jsx │ ├── index.html │ ├── miserables.json │ └── service.json ├── assets │ ├── desktop.png │ ├── docs-gif-01.gif │ ├── docs-gif-02.gif │ ├── docs-gif-03.gif │ ├── enteryourarn.gif │ ├── getyourstack.gif │ ├── logo-text.png │ ├── logo.png │ ├── logowhite.png │ ├── mascot.png │ ├── mascot_head.svg │ ├── metrics.gif │ ├── mobile-logs.png │ ├── mobile-map.png │ ├── mobile-metrics.png │ ├── readme-bar.gif │ ├── readme-bubble.gif │ ├── readme-node.gif │ ├── signup.gif │ └── signyouup.gif ├── components │ ├── Animation.jsx │ ├── Auth.jsx │ ├── ChartDropDown.jsx │ ├── DataWindow.jsx │ ├── Docs.jsx │ ├── LamdaButton.jsx │ ├── Log.jsx │ ├── Navbar.jsx │ ├── Panel.jsx │ ├── Refresh.jsx │ ├── SettingsForm.jsx │ ├── SignInForm.jsx │ ├── SignUpForm.jsx │ └── footer.jsx ├── containers │ ├── DashboardContainer.jsx │ ├── DiagramContainer.jsx │ ├── LandingPageContainer.jsx │ └── SettingsContainer.jsx ├── main.jsx └── styles │ ├── _animation.scss │ ├── _auth.scss │ ├── _dashboard.scss │ ├── _docs.scss │ ├── _dropdown.scss │ ├── _footer.scss │ ├── _form.scss │ ├── _landingpage.scss │ ├── _metrics.scss │ ├── _navbar.scss │ ├── _newnavbar.scss │ ├── _settings.scss │ ├── _variables.scss │ └── application.scss └── vite.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["**/test", "**/__tests__"], 4 | "env": { 5 | "node": true, 6 | "browser": true, 7 | "es2021": true 8 | }, 9 | "plugins": ["react"], 10 | "extends": ["eslint:recommended", "plugin:react/recommended"], 11 | "parserOptions": { 12 | "sourceType": "module", 13 | "ecmaFeatures": { 14 | "jsx": true 15 | } 16 | }, 17 | "rules": { 18 | "indent": ["warn", 2], 19 | "no-unused-vars": ["off", { "vars": "local" }], 20 | "no-case-declarations": "off", 21 | "prefer-const": "warn", 22 | "quotes": ["warn", "single"], 23 | "react/prop-types": "off", 24 | "semi": ["warn", "always"], 25 | "space-infix-ops": "warn" 26 | }, 27 | "settings": { 28 | "react": { "version": "detect" } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # General 3 | .DS_Store 4 | 5 | # dependencies 6 | node_modules/ 7 | 8 | # logs 9 | npm-debug.log* 10 | 11 | # SecretPage 12 | 13 | server/aws/ 14 | 15 | #.env 16 | 17 | .env 18 | 19 | # postgres table creator 20 | 21 | postgres_table_create.sql 22 | 23 | # redis dumper file 24 | 25 | dump.rdb 26 | -------------------------------------------------------------------------------- /Code_of_Conduct.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | email. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /Contributions.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |

Contributions

5 | 6 |
7 | 8 | # Contributing to Lambdawg 9 | 10 | Thank you for your contribution! 11 | 12 | ## Reporting Bugs 13 | 14 | All code changes happen through Github Pull Requests and we actively welcome them. Please submit your pull request to the _dev_ branch, including a description of the work done in your pull request. 15 | 16 | Note: Any contributions you make will be under the MIT Software License and your submissions are understood to be under the same that covers the project. Please reach out to the team if you have any questions. 17 | 18 | ## Issues 19 | 20 | To help us better identify and reproduce the issue, please include the following information: 21 | 22 | **Describe the bug** 23 | A clear and concise description of what the bug is. 24 | 25 | **To Reproduce** 26 | Steps to reproduce the behavior: 27 | 28 | 1. Go to '...' 29 | 2. Click on '....' 30 | 3. Scroll down to '....' 31 | 4. See error 32 | 33 | **Expected behavior** 34 | A clear and concise description of what you expected to happen. 35 | 36 | **Screenshots** 37 | If applicable, add screenshots to help explain your problem. 38 | 39 | **Desktop (please complete the following information):** 40 | 41 | - OS: [e.g. iOS] 42 | - Browser [e.g. chrome, safari] 43 | - Version [e.g. 22] 44 | 45 | **Additional context** 46 | Add any other context about the problem here. 47 | 48 | ## Feature Requests 49 | 50 | We are always looking to enhance our users' experience with the Lambdawg UI. We would love to hear your ideas! To suggest an idea for this project, please be sure to include the following information so we can best address your suggestion. 51 | 52 | **Is your feature request related to a problem? Please describe.** 53 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 54 | 55 | **Describe the solution you'd like** 56 | A clear and concise description of what you want to happen. 57 | 58 | **Describe alternatives you've considered** 59 | A clear and concise description of any alternative solutions or features you've considered. 60 | 61 | **Additional context** 62 | Add any other context or screenshots about the feature request here. 63 | 64 | ## License 65 | 66 | By contributing, you agree that your contributions will be licensed under Cloudband's MIT License. 67 | 68 | ### References 69 | 70 | This document was adapted from the open-source contribution guidelines for [Facebook's Draft](https://github.com/facebook/draft-js/blob/a9316a723f9e918afde44dea68b5f9f39b7d9b00/CONTRIBUTING.md) 71 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Lambdawg 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-DeveloperGuide.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 |
8 | 9 | 10 |

Lambdawg Developer's Guide

11 | 12 |
13 | 14 |
15 | Table of Contents 16 |
    17 |
  1. AWS Account Creation
  2. 18 |
  3. IAM Setup
  4. 19 |
  5. Streamlining the User Sign-Up Experience
  6. 20 |
  7. YAML Template
  8. 21 |
  9. Template Storage in an S3 Bucket
  10. 22 |
  11. Stack Creation Link
  12. 23 |
  13. Finish Setup
  14. 24 |
25 |
26 | 27 | ## AWS Account Creation 28 | 29 |

Lambdawg uses the AWS-SDK to pull data from a user's AWS Lambda functions in order to render trace and metrics data on the Lambdawg UI. It is highly encouraged to create a new AWS account specific to Lambdawg to make full use of the app's features.

30 | 31 | ## IAM Setup 32 | 33 |

Once you have created an AWS root account, you will need to set up an IAM role with the required permissions to access users' AWS data.

34 | 35 |

On your AWS account, do the following:

36 | 37 |

1. Create an IAM user called lambdawg-user with programmatic access (not console access, as there is no need for users to access the AWS console)

38 | 39 |

2. Attach the following policies directly to lambdawg-user:

40 | 41 | 51 | 52 |

3. Create an access key secret access key for lambdawg-user. This will be stored in a .env file in the Lambdawg application.

53 | 54 |

(back to top)

55 | 56 | ## Streamlining the User Sign-Up Experience 57 | 58 |

From the Lambdawg landing page, users can navigate to Settings for a streamlined sign-up experience. The user will first connect to their AWS account by logging into AWS Cloudformation using the link provided. This will initialize a Cloudformation Stack that will create IAM resources for the user to access Lambdawg's services and provide Lambdawg the necessary permissions. Once the stack is created, users can navigate to the "Resources" tab to access their ARN key. Finally, users will simply paste their ARN key and region to access their metrics on the Lambdawg UI.

59 | 60 | ## YAML Template 61 | 62 |

As a developer, you will need to create a yaml file to allow users access to Lambdawg's UI. This file will allow CloudFormation to automate the creation of an IAM role upon user sign-up. The yaml file content should look like the example below. (replacing the Principal / AWS ARN with the lambdawg-user’s ARN & replacing the sts:External Id with a secure external ID. You can use a generator such as UUID https://www.uuidgenerator.net/ for this step.:

63 | 64 |
65 | 66 | ``` 67 | Description: "CloudFormation stack" 68 | 69 | Resources: 70 | LambdawgDelegationRole: 71 | Type: "AWS::IAM::Role" 72 | Properties: 73 | AssumeRolePolicyDocument: 74 | Version: 2012-10-17 75 | Statement: - Effect: Allow 76 | Principal: 77 | AWS: - arn:aws:iam::403777712406:user/lambdawg-user 78 | Action: - "sts:AssumeRole" 79 | Condition: 80 | StringEquals: 81 | "sts:ExternalId": !Ref ExternalId 82 | Path: / 83 | RoleName: LambdawgDelegationRole 84 | Policies: - PolicyName: Resources 85 | PolicyDocument: 86 | Version: 2012-10-17 87 | Statement: - Effect: Allow 88 | Action: "apigateway:GET" 89 | Resource: "_" - Effect: Allow 90 | Action: "apigateway:HEAD" 91 | Resource: "_" - Effect: Allow 92 | Action: "apigateway:OPTIONS" 93 | Resource: "_" - Effect: Allow 94 | Action: "appsync:get_" 95 | Resource: "_" - Effect: Allow 96 | Action: "appsync:list_" 97 | Resource: "_" - Effect: Allow 98 | Action: "athena:list_" 99 | Resource: "_" - Effect: Allow 100 | Action: "athena:batchGet_" 101 | Resource: "_" - Effect: Allow 102 | Action: "athena:getNamedQuery" 103 | Resource: "_" - Effect: Allow 104 | Action: "athena:getQueryExecution" 105 | Resource: "_" - Effect: Allow 106 | Action: "athena:getQueryExecution" 107 | Resource: "_" - Effect: Allow 108 | Action: "autoscaling:describe*" 109 | Resource: "*" - Effect: Allow 110 | Action: "batch:describe*" 111 | Resource: "*" - Effect: Allow 112 | Action: "cloudformation:describe*" 113 | Resource: "*" - Effect: Allow 114 | Action: "cloudformation:get*" 115 | Resource: "*" - Effect: Allow 116 | Action: "cloudformation:list*" 117 | Resource: "*" - Effect: Allow 118 | Action: "cloudfront:get*" 119 | Resource: "*" - Effect: Allow 120 | Action: "cloudfront:list*" 121 | Resource: "*" - Effect: Allow 122 | Action: "cloudwatch:describe*" 123 | Resource: "*" - Effect: Allow 124 | Action: "cloudwatch:list*" 125 | Resource: "*" - Effect: Allow 126 | Action: "dax:describe*" 127 | Resource: "*" - Effect: Allow 128 | Action: "dax:list*" 129 | Resource: "*" - Effect: Allow 130 | Action: "discovery:describe*" 131 | Resource: "*" - Effect: Allow 132 | Action: "discovery:list*" 133 | Resource: "*" - Effect: Allow 134 | Action: "dynamodb:describe*" 135 | Resource: "*" - Effect: Allow 136 | Action: "dynamodb:list*" 137 | Resource: "*" - Effect: Allow 138 | Action: "ec2:describe*" 139 | Resource: "*" - Effect: Allow 140 | Action: "ecs:describe*" 141 | Resource: "*" - Effect: Allow 142 | Action: "ecs:list*" 143 | Resource: "*" - Effect: Allow 144 | Action: "ecr:describe*" 145 | Resource: "*" - Effect: Allow 146 | Action: "ecr:get*" 147 | Resource: "*" - Effect: Allow 148 | Action: "ecr:list*" 149 | Resource: "*" - Effect: Allow 150 | Action: "eks:describe*" 151 | Resource: "*" - Effect: Allow 152 | Action: "eks:list*" 153 | Resource: "*" - Effect: Allow 154 | Action: "elasticache:describe*" 155 | Resource: "*" - Effect: Allow 156 | Action: "elasticache:list*" 157 | Resource: "*" - Effect: Allow 158 | Action: "elasticloadbalancing:describe*" 159 | Resource: "*" - Effect: Allow 160 | Action: "es:describe*" 161 | Resource: "*" - Effect: Allow 162 | Action: "es:list*" 163 | Resource: "*" - Effect: Allow 164 | Action: "events:describe*" 165 | Resource: "*" - Effect: Allow 166 | Action: "events:list*" 167 | Resource: "*" - Effect: Allow 168 | Action: "firehose:describe*" 169 | Resource: "*" - Effect: Allow 170 | Action: "firehose:list*" 171 | Resource: "*" - Effect: Allow 172 | Action: "glacier:describe*" 173 | Resource: "*" - Effect: Allow 174 | Action: "glacier:getDataRetrievalPolicy" 175 | Resource: "_" - Effect: Allow 176 | Action: "glacier:getVaultAccessPolicy" 177 | Resource: "_" - Effect: Allow 178 | Action: "glacier:getVaultLock" 179 | Resource: "_" - Effect: Allow 180 | Action: "glacier:getVaultNotifications" 181 | Resource: "_" - Effect: Allow 182 | Action: "glacier:listTagsForVault" 183 | Resource: "_" - Effect: Allow 184 | Action: "glacier:listVaults" 185 | Resource: "_" - Effect: Allow 186 | Action: "iot:describe*" 187 | Resource: "*" - Effect: Allow 188 | Action: "iot:get*" 189 | Resource: "*" - Effect: Allow 190 | Action: "iot:list*" 191 | Resource: "*" - Effect: Allow 192 | Action: "kinesis:describe*" 193 | Resource: "*" - Effect: Allow 194 | Action: "kinesis:list*" 195 | Resource: "*" - Effect: Allow 196 | Action: "kinesisanalytics:describe*" 197 | Resource: "*" - Effect: Allow 198 | Action: "kinesisanalytics:list*" 199 | Resource: "*" - Effect: Allow 200 | Action: "lambda:listFunctions" 201 | Resource: "_" - Effect: Allow 202 | Action: "lambda:listTags" 203 | Resource: "_" - Effect: Allow 204 | Action: "rds:describe*" 205 | Resource: "*" - Effect: Allow 206 | Action: "rds:list*" 207 | Resource: "*" - Effect: Allow 208 | Action: "route53:list*" 209 | Resource: "*" - Effect: Allow 210 | Action: "route53:get*" 211 | Resource: "*" - Effect: Allow 212 | Action: "s3:getBucket*" 213 | Resource: "*" - Effect: Allow 214 | Action: "s3:list*" 215 | Resource: "*" - Effect: Allow 216 | Action: "sdb:domainMetadata" 217 | Resource: "_" - Effect: Allow 218 | Action: "sdb:get_" 219 | Resource: "_" - Effect: Allow 220 | Action: "sdb:list_" 221 | Resource: "_" - Effect: Allow 222 | Action: "sns:get_" 223 | Resource: "_" - Effect: Allow 224 | Action: "sns:list_" 225 | Resource: "_" - Effect: Allow 226 | Action: "sqs:get_" 227 | Resource: "_" - Effect: Allow 228 | Action: "sqs:list_" 229 | Resource: "_" - Effect: Allow 230 | Action: "states:describe_" 231 | Resource: "_" - Effect: Allow 232 | Action: "states:get_" 233 | Resource: "_" - Effect: Allow 234 | Action: "states:list_" 235 | Resource: "_" - Effect: Allow 236 | Action: "tag:get_" 237 | Resource: "_" - PolicyName: Logs 238 | PolicyDocument: 239 | Version: 2012-10-17 240 | Statement: - Effect: Allow 241 | Action: "logs:deleteSubscriptionFilter" 242 | Resource: "_" - Effect: Allow 243 | Action: "logs:describeLogStreams" 244 | Resource: "_" - Effect: Allow 245 | Action: "logs:describeSubscriptionFilters" 246 | Resource: "_" - Effect: Allow 247 | Action: "logs:filterLogEvents" 248 | Resource: "_" - Effect: Allow 249 | Action: "logs:putSubscriptionFilter" 250 | Resource: "_" - Effect: Allow 251 | Action: "logs:startQuery" 252 | Resource: "_" - Effect: Allow 253 | Action: "logs:stopQuery" 254 | Resource: "_" - PolicyName: Metrics 255 | PolicyDocument: 256 | Version: 2012-10-17 257 | Statement: - Effect: Allow 258 | Action: "cloudwatch:get*" 259 | Resource: "*" - PolicyName: Traces 260 | PolicyDocument: 261 | Version: 2012-10-17 262 | Statement: - Effect: Allow 263 | Action: "xray:batch*" 264 | Resource: "*" - Effect: Allow 265 | Action: "xray:get*" 266 | Resource: "*" 267 | Parameters: 268 | ExternalId: 269 | Description: "The external ID for the LAMBDAWG delegation role" 270 | Type: String 271 | 272 | Outputs: 273 | Version: 274 | Description: LAMBDAWG CF template version 275 | Value: 2020-02-06 276 | LambdawgDelegationRoleArn: 277 | Description: "The ARN for the LAMBDAWG delegation role" 278 | Value: !GetAtt - LambdawgDelegationRole - Arn 279 | 280 | ``` 281 | 282 |
283 | 284 | 285 | ## Template Storage in an S3 Bucket 286 | 287 |

The template must be stored on your Lambdawg-specific AWS account. The simplest way to do this is to create an S3 bucket and upload the template yaml file with the following steps:

288 | 289 |
    290 |
  1. Navigate to the AWS S3.
  2. 291 |
  3. Select Create Bucket.
  4. 292 |
  5. Name the bucket "lambdawg".
  6. 293 |
  7. Unselect "Block all public access".
  8. 294 |
  9. Create bucket.
  10. 295 |
  11. Add to bucket policy the text below step 8.
  12. 296 |
  13. Click upload and upload your created yaml file template.
  14. 297 |
  15. In the list of objects in your S3 bucket, copy the URL of your lambdawg template.
  16. 298 |
299 | 300 | ``` 301 | 302 | { 303 | "Version": "2008-10-17", 304 | "Statement": [ 305 | { 306 | "Sid": "AllowPublicRead", 307 | "Effect": "Allow", 308 | "Principal": { 309 | "AWS": "*" 310 | }, 311 | "Action": "s3:GetObject", 312 | "Resource": "arn:aws:s3:::lambdawg/*" 313 | } 314 | ] 315 | } 316 | 317 | ``` 318 | 319 | ## Stack Creation Link: 320 |

Use the following link to allow your user to automatically create a stack. This link can be attached to the “Connect your AWS account” link on the Settings Page (in SettingsForm.jsx - line 71). Add in your template URL, region, and external id into the link to ensure the stack is properly configured.

321 | 322 | ``` 323 | 324 | https://console.aws.amazon.com/cloudformation/home?region=#/stacks/quickcreate?stackName=cloudband-permission¶m_ExternalId=&templateURL= 325 | 326 | ``` 327 | 328 | ## Finish Setup: 329 | 330 | Continue following the main [README](https://github.com/oslabs-beta/lambdawg/blob/dev/README.md). 331 | ``` 332 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Lambdawg 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 |

Lambdawg Users's Guide

11 |

(Developer Readme here)

12 |
13 | 14 |
15 | Table of Contents 16 |
    17 |
  1. About LAMBDAWG
  2. 18 |
  3. Getting Started
  4. 19 |
  5. Monitoring Features
  6. 20 |
  7. Contributing
  8. 21 |
  9. Built With
  10. 22 |
  11. License
  12. 23 |
  13. Authors
  14. 24 |
25 |
26 | 27 | ## About LAMBDAWG 28 | 29 |

LAMBDAWG is a free tool designed to simplify the management and visualization of AWS Lambda functions and metrics. It provides easy access and straightforward insights for AWS developers, eliminating the need to navigate complex documentation. With LAMBDAWG, you can quickly visualize the correlation between Lambda functions within your app and keep an organized log of errors, all in one place. This tool aims to speed up your development process and enhance your overall experience with AWS Lambda.

30 | 31 |
    32 |

    Key Features

    33 |
  1. Centralized Error Log Management: Keep an organized list of error logs across your AWS Lambda Functions in a single location for easy access and troubleshooting.
  2. 34 |
  3. Invocation Monitoring: Gain visibility into the number of invocations for your Lambda functions, allowing you to track usage and performance.
  4. 35 |
  5. Function Execution Duration: Easily track the duration of each Lambda function's execution to identify potential bottlenecks and areas for improvement.
  6. 36 |
  7. Response Time Analysis: Measure the time taken to generate the response for each function, helping you evaluate efficiency and optimize user experiences.
  8. 37 |
38 |

(back to top)

39 | 40 | ## Getting Started 41 |
    42 |
  1. Install LAMBDAWG on your AWS development environment.
  2. 43 |
  3. Connect your AWS Lambda Functions to LAMBDAWG for streamlined monitoring and management.
  4. 44 |
  5. Connect to your AWS account and Copy your ARN
  6. 45 |
    46 | 47 | 48 |
  7. Paste your ARN in the form
  8. 49 |
  9. Select your region
  10. 50 |
  11. confirm your LAMBDAWG password
  12. 51 |
52 | 53 | ## Monitoring Features 54 | 55 | 56 |
57 | 58 | 59 |
  • Access the LAMBDAWG dashboard to view error logs, invocations, execution duration, and response time metrics.
  • 60 |
  • Leverage the provided insights to troubleshoot errors, improve performance, and optimize your application.
  • 61 | 62 |
  • View active service trace data for each Lambda function:
  • 63 | 64 | 65 | 66 |
  • View duration and response times for each active Lambda function:
  • 67 | 68 | 69 | 70 |
  • View invocation data to easily grasp which functions are used most frequently:
  • 71 |
    72 | 73 | 74 | 75 |

    (back to top)

    76 | 77 | 78 | ## Contributing 79 | 80 | We welcome contributions to LAMBDAWG from the community. Whether you have a great idea to enhance the tool or you've discovered and fixed a bug, we appreciate your involvement. Follow the steps below to contribute: 81 | 82 |
      83 |
    1. Fork the LAMBDAWG repository.
    2. 84 |
    3. Clone the repository to your local machine using the command: `git clone your_repo_url`
    4. 85 |
    5. Create a new branch for your feature: `git checkout -b your/amazingFeature`
    6. 86 |
    7. Make the necessary changes and commit them with a brief comment: `git commit -m "Quick comment about your amazing feature"`
    8. 87 |
    9. Push the changes to your branch: `git push origin your/amazingFeature`
    10. 88 |
    11. Open a pull request to submit your changes for review and integration
    12. 89 |
    90 | We value community feedback and participation. Don't forget to give LAMBDAWG a ⭐️ and share it with your friends.
    91 | Together, we can make LAMBDAWG even better! 92 |

    (back to top)

    93 | 94 | ## Built With 95 | 96 | [React](https://reactjs.org/) 97 | | [React Router](https://reactrouter.com/en/main) 98 | | [NodeJS](https://nodejs.org/en/) 99 | | [Express](https://expressjs.com/) 100 | | [PostgreSQL](https://www.postgresql.org/) 101 | | [JWT](https://jwt.io/) 102 | [D3](https://d3js.org/) 103 | | [Chart.js](https://www.chartjs.org/) 104 | | [Redis](https://redis.com/) 105 | | [Jest](https://jestjs.io/) 106 | | [Vite](https://vitejs.dev/) 107 | | [AWS SDK](https://aws.amazon.com/sdk-for-javascript/) 108 | | [AWS IAM](https://aws.amazon.com/iam/) 109 | | [AWS Lambda](https://aws.amazon.com/lambda/) 110 | | [AWS Cloudwatch](https://aws.amazon.com/cloudwatch/) 111 | | [AWS STS](https://docs.aws.amazon.com/STS/latest/APIReference/welcome.html) 112 | | [AWS Could Formation](https://docs.aws.amazon.com/cloudformation/index.html) 113 | | [BCrypt](https://bcrypt.online/) 114 | | [Validator](https://www.npmjs.com/package/validator) 115 | | [Supertest](https://www.npmjs.com/package/supertest) 116 | | [Client XRay](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-xray/index.html) 117 | 118 |

    (back to top)

    119 | 120 | ## License 121 | 122 |

    Distributed under the MIT License

    123 |

    (back to top)

    124 | 125 | ## Authors 126 | 127 | - Chanda Gonet - [LinkedIn](https://www.linkedin.com/in/chanda-gonet-317b91237/)|[Github](https://github.com/Chanduh) 128 | - Erica Park - [LinkedIn](https://www.linkedin.com/in/erica-s-park/)|[Github](https://github.com/EPsparky) 129 | - Vincent Jacquemin - [LinkedIn](https://www.linkedin.com/in/vincentjacquemin/)|[Github](https://github.com/GIjack2001) 130 | - Ted Gusek - [LinkedIn](https://www.linkedin.com/in/tedgusek/)|[Github](https://github.com/tedgusek) 131 | 132 |

    (back to top)

    133 | -------------------------------------------------------------------------------- /__tests__/apiRouter_tests.js: -------------------------------------------------------------------------------- 1 | const { beforeEach, afterEach } = require('node:test'); 2 | const { afterAll } = require('@jest/globals'); 3 | const request = require('supertest'); 4 | const server = require('../server/server'); 5 | const db = require('../server/models/dbPool'); 6 | require('dotenv').config(); 7 | 8 | describe('Our apiRoutes Unit Tests', () => { 9 | const testKey = process.env.TEST_TOKEN; 10 | //These are our test users to run through the tests 11 | //Passing User 12 | const newUser = [ 13 | { 14 | user_name: 'testUser', 15 | password_: 'password', 16 | full_name: 'Test User', 17 | email: 'testuser@example.com', 18 | arn: null, 19 | region: null, 20 | [testKey]: true, 21 | }, 22 | ]; 23 | //User Name is too long user- 272 chars 24 | const newUser272CharsUN_1 = [ 25 | { 26 | user_name: 27 | 'testUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUser', 28 | password_: 'password', 29 | full_name: 'Test User_1', 30 | email: 'testuser_1@example.com', 31 | arn: null, 32 | region: null, 33 | [testKey]: true, 34 | }, 35 | ]; 36 | //Password is too short User 37 | const newUserShortPass_2 = [ 38 | { 39 | user_name: 'testUser_2', 40 | password_: 'passwor', 41 | full_name: 'Test User_2', 42 | email: 'testuser_2@example.com', 43 | arn: null, 44 | region: null, 45 | [testKey]: true, 46 | }, 47 | ]; 48 | //Password is too long User 49 | const newUser272CharsPW_3 = [ 50 | { 51 | user_name: 'testUser_3', 52 | password_: 53 | 'testUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUser', 54 | full_name: 'Test User_3', 55 | email: 'testuser_3@example.com', 56 | arn: null, 57 | region: null, 58 | [testKey]: true, 59 | }, 60 | ]; 61 | //Email is wrong format User 62 | const newUserBadEmail_4 = [ 63 | { 64 | user_name: 'testUser_4', 65 | password_: 'password', 66 | full_name: 'Test User_4', 67 | email: 'testuserexample.com', 68 | arn: null, 69 | region: null, 70 | [testKey]: true, 71 | }, 72 | ]; 73 | //Full Name is too long User 74 | const newUser272CharsFN_5 = [ 75 | { 76 | user_name: 'testUser_5', 77 | password_: 'password', 78 | full_name: 79 | 'testUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUsertestUser', 80 | email: 'testuser_5@example.com', 81 | arn: null, 82 | region: null, 83 | [testKey]: true, 84 | }, 85 | ]; 86 | 87 | // want to make sure the user being passed into the test does not 88 | //already exist in our database, so we will invoke this function before and after each test 89 | const deleteUser = async () => { 90 | const text = `DELETE FROM "public"."users" WHERE ${testKey} = true`; 91 | console.log('how many times do you see me'); 92 | await db.query(text); 93 | }; 94 | // Might need to handle some Open Handle issues, not sure if this is the direction to go 95 | // beforeAll(async () => { 96 | // await db.connect(); 97 | // }); 98 | // const closeDB = async () => { 99 | // await db.end(); 100 | // }; 101 | 102 | //Setting up our tests 103 | //The outter most scope is testing our Post request at the Path api/newUser 104 | //The syntax of Jest will explain what each test is doing 105 | describe('POST /newUser', () => { 106 | describe('Testing Validator Middleware', () => { 107 | beforeEach(deleteUser()); 108 | afterEach(deleteUser()); 109 | test('Should Post newUser successfully', (done) => { 110 | request(server) 111 | .post('/api/newUser') 112 | .send(newUser) 113 | .expect(200) 114 | .end((err, res) => { 115 | if (err) return done(err); 116 | return done(); 117 | }); 118 | }); 119 | test('Should Not Post User because User Name is too Long', (done) => { 120 | request(server) 121 | .post('/api/newUser') 122 | .send(newUser272CharsUN_1) 123 | .expect('User Name must be between 1-255 Characters') 124 | .expect(400) 125 | .end((err, res) => { 126 | if (err) return done(err); 127 | return done(); 128 | }); 129 | }); 130 | 131 | test('Should Not Post User because Password is too Short', (done) => { 132 | request(server) 133 | .post('/api/newUser') 134 | .send(newUserShortPass_2) 135 | .expect('Password must be at least 8 characters long') 136 | .expect(400) 137 | .end((err, res) => { 138 | if (err) return done(err); 139 | return done(); 140 | }); 141 | }); 142 | test('Should Not Post User because Password is too Long', (done) => { 143 | request(server) 144 | .post('/api/newUser') 145 | .send(newUser272CharsPW_3) 146 | .expect('Password must be at least 8 characters long') 147 | .expect(400) 148 | .end((err, res) => { 149 | if (err) return done(err); 150 | return done(); 151 | }); 152 | }); 153 | test('Should Not Post User because Password is too Long', (done) => { 154 | request(server) 155 | .post('/api/newUser') 156 | .send(newUserBadEmail_4) 157 | .expect('Invalid Email Address') 158 | .expect(400) 159 | .end((err, res) => { 160 | if (err) return done(err); 161 | return done(); 162 | }); 163 | }); 164 | test('Should Not Post User because Password is too Long', (done) => { 165 | request(server) 166 | .post('/api/newUser') 167 | .send(newUser272CharsFN_5) 168 | .expect('Full Name must be between 1-255 Characters') 169 | .expect(400) 170 | .end((err, res) => { 171 | if (err) return done(err); 172 | return done(); 173 | }); 174 | }); 175 | }); 176 | //This needs to be Changed around, it's just some copy pasta from a previous test 177 | /* 178 | describe('Testing HashPW Middleware', () => { 179 | beforeEach(deleteUser()); 180 | afterEach(deleteUser()); 181 | test('Should Hash Password successfully', (done) => { 182 | request(server) 183 | .post('/api/newUser') 184 | .send(newUser) 185 | .expect('Content-Type', /json/) 186 | .expect('{}') 187 | .expect(200) 188 | .end((err, res) => { 189 | if (err) return done(err); 190 | // _id = res.body.data[0]._id; 191 | return done(); 192 | }); 193 | }); 194 | }); 195 | */ 196 | }); 197 | 198 | //Here are some more tests that would be good to implement, the below is just a starting point, and need to be refined 199 | //for actual testing of our code 200 | /* 201 | describe('POST /:user_name', () => { 202 | it('should set a session cookie', async () => { 203 | const response = await request(server) 204 | .post('/testuser') 205 | .auth('testuser', 'testpassword'); // set basic authentication 206 | 207 | expect(response.status).toBe(200); 208 | expect(response.headers['set-cookie']).toBeDefined(); 209 | }); 210 | }); 211 | 212 | describe('GET /', () => { 213 | it('should return a user whose cookie matches', async () => { 214 | const response = await request(server) 215 | .get('/') 216 | .set('Cookie', 'sessionid=abc123'); // set a session cookie 217 | 218 | expect(response.status).toBe(200); 219 | expect(response.body).toBeInstanceOf(Array); 220 | }); 221 | }); 222 | 223 | describe('PATCH /edit', () => { 224 | it('should update a user', async () => { 225 | const response = await request(server) 226 | .patch('/edit') 227 | .auth('testuser', 'testpassword') // set basic authentication 228 | .send({ 229 | full_name: 'New Name', 230 | email: 'newemail@example.com', 231 | }); 232 | 233 | expect(response.status).toBe(200); 234 | }); 235 | }); 236 | 237 | describe('DELETE /delete/:user_name', () => { 238 | it('should delete a user', async () => { 239 | const response = await request(server) 240 | .delete('/delete/testuser') 241 | .auth('testuser', 'testpassword'); // set basic authentication 242 | 243 | expect(response.status).toBe(200); 244 | }); 245 | }); 246 | 247 | describe('GET /logout', () => { 248 | it('should delete the session cookie', async () => { 249 | const response = await request(server) 250 | .get('/logout') 251 | .set('Cookie', 'sessionid=abc123'); // set a session cookie 252 | 253 | expect(response.status).toBe(200); 254 | expect(response.headers['set-cookie']).toBeDefined(); 255 | }); 256 | });*/ 257 | }); 258 | -------------------------------------------------------------------------------- /__tests__/db_tests.js: -------------------------------------------------------------------------------- 1 | const db = require('../server/models/dbPool'); 2 | require('dotenv').config(); 3 | const { beforeEach, before, afterEach } = require('node:test'); 4 | 5 | const testKey = process.env.TEST_TOKEN; 6 | const deleteUser = async function () { 7 | const text = `DELETE FROM "public"."users" WHERE ${testKey} = true`; 8 | console.log('how many times do you see me'); 9 | await db.query(text); 10 | }; 11 | //cleans the database of any test users before running tests 12 | beforeEach(deleteUser()); 13 | 14 | describe('database unit tests', () => { 15 | //assigns a test user that we will use in our testing 16 | //I made a new row to assign to every document that defaults to false, and test users will be true 17 | //I was concerned about there being a row that may end up being able to give access to 18 | //areas users should not be able to access, and started making the column name hidden 19 | //I ended up not needing to do this, so this could be altered at a later time 20 | const newUser = { 21 | full_name: 'Test User', 22 | email: 'testUser@example.com', 23 | user_name: 'testUser', 24 | password_: 'password', 25 | arn: null, 26 | region: null, 27 | [testKey]: true, 28 | }; 29 | 30 | describe('#addUser', () => { 31 | it('adds a new user to the database', async () => { 32 | const values = [ 33 | newUser.full_name, 34 | newUser.email, 35 | newUser.user_name, 36 | newUser.password_, 37 | newUser.arn, 38 | newUser.region, 39 | newUser[testKey], 40 | ]; 41 | const text = `INSERT INTO "public"."users" ("full_name", "email", "user_name", "password_", "arn", "region",${testKey}) 42 | VALUES ($1, $2, $3, $4, $5, $6, $7)`; 43 | 44 | await db.query(text, values); 45 | 46 | const queryText = `SELECT * FROM "public"."users" WHERE "${testKey}" = 'true'`; 47 | const queryResult = await db.query(queryText); 48 | 49 | const { _id } = queryResult.rows[0]; 50 | 51 | newUser._id = _id; 52 | 53 | expect(queryResult.rows.length).toBe(1); 54 | expect(queryResult.rows[0]).toMatchObject(newUser); 55 | }); 56 | }); 57 | 58 | describe('#getUser', () => { 59 | it('returns an object of our test users info from the database', async () => { 60 | const text = `SELECT * FROM "public"."users" WHERE "${testKey}" = 'true'`; 61 | const result = await db.query(text); 62 | 63 | expect(result.rows[0]).toMatchObject(newUser); 64 | }); 65 | }); 66 | 67 | describe('#deleteUser', () => { 68 | it('deletes a user from the database', async () => { 69 | // Delete the test user 70 | const text = `DELETE FROM "public"."users" WHERE "${testKey}" = 'true'`; 71 | const result = await db.query(text); 72 | 73 | expect(result.rows.length).toBe(0); 74 | // Check that the user was deleted 75 | const selectText = `SELECT * FROM "public"."users" WHERE "${testKey}" = 'true'`; 76 | const selectResult = await db.query(selectText); 77 | expect(selectResult.rows.length).toBe(0); 78 | }); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Lambdawg 9 | 10 | 11 |
    12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @returns {Promise} */ 2 | /**Allows for detailed description of the tests */ 3 | module.exports = async () => { 4 | return { 5 | verbose: true, 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lambdawg", 3 | "private": true, 4 | "version": "0.0.0", 5 | "description": "Lambdawg is a visualization UI for your AWS Lambda Functions", 6 | "main": "./client/index.js", 7 | "scripts": { 8 | "dev": "vite; export NODE_ENV=development", 9 | "build": "vite build", 10 | "preview": "vite preview", 11 | "fulldev": "concurrently \"nodemon server/server.js\" \"vite\"", 12 | "test": "jest", 13 | "testopen": "jest --detectOpenHandles" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/oslabs-beta/Lambdawg" 18 | }, 19 | "keywords": [], 20 | "author": "Vincent Jacquemin, Chanda Gonet, Ted Gusek, Erica Park", 21 | "license": "ISC", 22 | "bugs": { 23 | "url": "https://github.com/oslabs-beta/Lambdawg/blob/main/Contributions.md" 24 | }, 25 | "homepage": "https://github.com/oslabs-beta/Lambdawg/blob/main/README.md", 26 | "dependencies": { 27 | "@aws-sdk/client-cloudformation": "^3.272.0", 28 | "@aws-sdk/client-cloudwatch": "^3.272.0", 29 | "@aws-sdk/client-cloudwatch-logs": "^3.272.0", 30 | "@aws-sdk/client-lambda": "^3.272.0", 31 | "@aws-sdk/client-sts": "^3.272.0", 32 | "@aws-sdk/client-xray": "^3.279.0", 33 | "@emotion/react": "^11.10.6", 34 | "@emotion/styled": "^11.10.6", 35 | "@mui/material": "^5.11.12", 36 | "axios": "^1.3.3", 37 | "bcrypt": "^5.1.0", 38 | "chart.js": "^4.2.1", 39 | "concurrently": "^7.6.0", 40 | "cookie-parser": "^1.4.6", 41 | "cors": "^2.8.5", 42 | "d3": "^7.8.2", 43 | "dotenv": "^16.0.3", 44 | "eslint": "^8.36.0", 45 | "express": "^4.18.2", 46 | "fs": "^0.0.1-security", 47 | "html-webpack-plugin": "^5.5.0", 48 | "jest": "^29.4.3", 49 | "jsonwebtoken": "^9.0.0", 50 | "node": "^19.6.0", 51 | "nodemon": "^2.0.20", 52 | "path": "^0.12.7", 53 | "pg": "^8.9.0", 54 | "react": "^18.2.0", 55 | "react-chartjs-2": "^5.2.0", 56 | "react-dom": "^18.2.0", 57 | "react-icons": "^4.8.0", 58 | "react-router-dom": "^6.8.1", 59 | "react-scrollbars-custom": "^4.1.1", 60 | "react-switch": "^7.0.0", 61 | "redis": "^4.6.5", 62 | "redis-server": "^1.2.2", 63 | "sass": "^1.58.3", 64 | "validator": "^13.9.0" 65 | }, 66 | "devDependencies": { 67 | "@types/react": "^18.0.27", 68 | "@types/react-dom": "^18.0.10", 69 | "@vitejs/plugin-react": "^3.1.0", 70 | "supertest": "^6.3.3", 71 | "vite": "^4.1.0" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /postgres_table_create.sql: -------------------------------------------------------------------------------- 1 | -- 2 | --PostgreSQL database Creation 3 | -- 4 | --Connects to env file 5 | \i .env 6 | 7 | --This file sets up the schema of our database, and will maintain the connection to the database across users 8 | -- 9 | --To invoke this file, insert the below code in the terminal 10 | --psql -d ${PG_URI} -f postgres_table_create.sql 11 | --I did not set a port initially, but to do so you would add -p 12 | -- and instead of ${PG_URI} use your own URI 13 | ------------------- 14 | SET statement_timeout = 0; 15 | SET lock_timeout = 0; 16 | SET idle_in_transaction_session_timeout = 0; 17 | SET client_encoding = 'UTF8'; 18 | SET standard_conforming_strings = on; 19 | SELECT pg_catalog.set_config('search_path','',false); 20 | SET check_function_bodies = false; 21 | SET xmloption = content; 22 | SET client_min_messages = warning; 23 | SET row_security = off; 24 | SET idleTimeoutMillis = 5000; 25 | SET connectionTimeoutMillis = 7500; 26 | --these last 2 items are in place to correct thr open handle issue 27 | ------------------- 28 | -- CREATE EXTENSION pgcrypto; 29 | ------------------- 30 | CREATE TABLE public.users ( 31 | _id serial NOT NULL, 32 | full_name varchar NOT NULL, 33 | user_name varchar NOT NULL UNIQUE, 34 | email varchar NOT NULL UNIQUE, 35 | password_ varchar NOT NULL, 36 | ${TEST_TOKEN} BOOLEAN DEFAULT false, 37 | CONSTRAINT users_pk PRIMARY KEY ("_id") 38 | ) WITH ( 39 | OIDS=FALSE 40 | ); 41 | ------------------- 42 | -- 43 | --Test DATA for users 44 | ------------------- 45 | -- INSERT INTO public.users (full_name, user_name, email, password_) VALUES ('John Doe', 'JohnnyD', 'johnnyd@johnnyd.com','jdpassword'); 46 | ------------------- 47 | 48 | 49 | --To clear Table, uncomment the below line, and remove the '/' 50 | -- -- -- D/R/O/P T/A/B/L/E public.users 51 | -- 52 | --To retrieve the information 53 | -- 54 | -- SELECT _id 55 | -- FROM users 56 | -- WHERE email = 'johnnyd@johnnyd.com' 57 | -- AND password = crypt('jdpassword', password); 58 | -- 59 | -- Expected Output 60 | -- 61 | -- _id 62 | -- ------ 63 | -- 1 64 | -- (1 row) 65 | -- 66 | -- Example of Bad Password 67 | -- 68 | -- SELECT _id 69 | -- FROM users 70 | -- WHERE email = 'johnnyd@johnnyd.com' 71 | -- AND password = crypt('wrongpassword', password); 72 | -- 73 | -- Expected Output 74 | -- 75 | -- _id 76 | -- ------ 77 | -- (0 rows) 78 | -------------------------------------------------------------------------------- /server/aws/cloudFormation.yaml: -------------------------------------------------------------------------------- 1 | Description: "CloudFormation stack" 2 | 3 | Resources: 4 | LambdawgDelegationRole: 5 | Type: "AWS::IAM::Role" 6 | Properties: 7 | AssumeRolePolicyDocument: 8 | Version: 2012-10-17 9 | Statement: 10 | - Effect: Allow 11 | Principal: 12 | AWS: 13 | - arn:aws:iam::403777712406:user/New-Lambdawg 14 | Action: 15 | - "sts:AssumeRole" 16 | Condition: 17 | StringEquals: 18 | "sts:ExternalId": !Ref ExternalId 19 | Path: / 20 | RoleName: LambdawgDelegationRole 21 | Policies: 22 | - PolicyName: Resources 23 | PolicyDocument: 24 | Version: 2012-10-17 25 | Statement: 26 | - Effect: Allow 27 | Action: "apigateway:GET" 28 | Resource: "*" 29 | - Effect: Allow 30 | Action: "apigateway:HEAD" 31 | Resource: "*" 32 | - Effect: Allow 33 | Action: "apigateway:OPTIONS" 34 | Resource: "*" 35 | - Effect: Allow 36 | Action: "appsync:get*" 37 | Resource: "*" 38 | - Effect: Allow 39 | Action: "appsync:list*" 40 | Resource: "*" 41 | - Effect: Allow 42 | Action: "athena:list*" 43 | Resource: "*" 44 | - Effect: Allow 45 | Action: "athena:batchGet*" 46 | Resource: "*" 47 | - Effect: Allow 48 | Action: "athena:getNamedQuery" 49 | Resource: "*" 50 | - Effect: Allow 51 | Action: "athena:getQueryExecution" 52 | Resource: "*" 53 | - Effect: Allow 54 | Action: "athena:getQueryExecution" 55 | Resource: "*" 56 | - Effect: Allow 57 | Action: "autoscaling:describe*" 58 | Resource: "*" 59 | - Effect: Allow 60 | Action: "batch:describe*" 61 | Resource: "*" 62 | - Effect: Allow 63 | Action: "cloudformation:describe*" 64 | Resource: "*" 65 | - Effect: Allow 66 | Action: "cloudformation:get*" 67 | Resource: "*" 68 | - Effect: Allow 69 | Action: "cloudformation:list*" 70 | Resource: "*" 71 | - Effect: Allow 72 | Action: "cloudfront:get*" 73 | Resource: "*" 74 | - Effect: Allow 75 | Action: "cloudfront:list*" 76 | Resource: "*" 77 | - Effect: Allow 78 | Action: "cloudwatch:describe*" 79 | Resource: "*" 80 | - Effect: Allow 81 | Action: "cloudwatch:list*" 82 | Resource: "*" 83 | - Effect: Allow 84 | Action: "dax:describe*" 85 | Resource: "*" 86 | - Effect: Allow 87 | Action: "dax:list*" 88 | Resource: "*" 89 | - Effect: Allow 90 | Action: "discovery:describe*" 91 | Resource: "*" 92 | - Effect: Allow 93 | Action: "discovery:list*" 94 | Resource: "*" 95 | - Effect: Allow 96 | Action: "dynamodb:describe*" 97 | Resource: "*" 98 | - Effect: Allow 99 | Action: "dynamodb:list*" 100 | Resource: "*" 101 | - Effect: Allow 102 | Action: "ec2:describe*" 103 | Resource: "*" 104 | - Effect: Allow 105 | Action: "ecs:describe*" 106 | Resource: "*" 107 | - Effect: Allow 108 | Action: "ecs:list*" 109 | Resource: "*" 110 | - Effect: Allow 111 | Action: "ecr:describe*" 112 | Resource: "*" 113 | - Effect: Allow 114 | Action: "ecr:get*" 115 | Resource: "*" 116 | - Effect: Allow 117 | Action: "ecr:list*" 118 | Resource: "*" 119 | - Effect: Allow 120 | Action: "eks:describe*" 121 | Resource: "*" 122 | - Effect: Allow 123 | Action: "eks:list*" 124 | Resource: "*" 125 | - Effect: Allow 126 | Action: "elasticache:describe*" 127 | Resource: "*" 128 | - Effect: Allow 129 | Action: "elasticache:list*" 130 | Resource: "*" 131 | - Effect: Allow 132 | Action: "elasticloadbalancing:describe*" 133 | Resource: "*" 134 | - Effect: Allow 135 | Action: "es:describe*" 136 | Resource: "*" 137 | - Effect: Allow 138 | Action: "es:list*" 139 | Resource: "*" 140 | - Effect: Allow 141 | Action: "events:describe*" 142 | Resource: "*" 143 | - Effect: Allow 144 | Action: "events:list*" 145 | Resource: "*" 146 | - Effect: Allow 147 | Action: "firehose:describe*" 148 | Resource: "*" 149 | - Effect: Allow 150 | Action: "firehose:list*" 151 | Resource: "*" 152 | - Effect: Allow 153 | Action: "glacier:describe*" 154 | Resource: "*" 155 | - Effect: Allow 156 | Action: "glacier:getDataRetrievalPolicy" 157 | Resource: "*" 158 | - Effect: Allow 159 | Action: "glacier:getVaultAccessPolicy" 160 | Resource: "*" 161 | - Effect: Allow 162 | Action: "glacier:getVaultLock" 163 | Resource: "*" 164 | - Effect: Allow 165 | Action: "glacier:getVaultNotifications" 166 | Resource: "*" 167 | - Effect: Allow 168 | Action: "glacier:listTagsForVault" 169 | Resource: "*" 170 | - Effect: Allow 171 | Action: "glacier:listVaults" 172 | Resource: "*" 173 | - Effect: Allow 174 | Action: "iot:describe*" 175 | Resource: "*" 176 | - Effect: Allow 177 | Action: "iot:get*" 178 | Resource: "*" 179 | - Effect: Allow 180 | Action: "iot:list*" 181 | Resource: "*" 182 | - Effect: Allow 183 | Action: "kinesis:describe*" 184 | Resource: "*" 185 | - Effect: Allow 186 | Action: "kinesis:list*" 187 | Resource: "*" 188 | - Effect: Allow 189 | Action: "kinesisanalytics:describe*" 190 | Resource: "*" 191 | - Effect: Allow 192 | Action: "kinesisanalytics:list*" 193 | Resource: "*" 194 | - Effect: Allow 195 | Action: "lambda:listFunctions" 196 | Resource: "*" 197 | - Effect: Allow 198 | Action: "lambda:listTags" 199 | Resource: "*" 200 | - Effect: Allow 201 | Action: "rds:describe*" 202 | Resource: "*" 203 | - Effect: Allow 204 | Action: "rds:list*" 205 | Resource: "*" 206 | - Effect: Allow 207 | Action: "route53:list*" 208 | Resource: "*" 209 | - Effect: Allow 210 | Action: "route53:get*" 211 | Resource: "*" 212 | - Effect: Allow 213 | Action: "s3:getBucket*" 214 | Resource: "*" 215 | - Effect: Allow 216 | Action: "s3:list*" 217 | Resource: "*" 218 | - Effect: Allow 219 | Action: "sdb:domainMetadata" 220 | Resource: "*" 221 | - Effect: Allow 222 | Action: "sdb:get*" 223 | Resource: "*" 224 | - Effect: Allow 225 | Action: "sdb:list*" 226 | Resource: "*" 227 | - Effect: Allow 228 | Action: "sns:get*" 229 | Resource: "*" 230 | - Effect: Allow 231 | Action: "sns:list*" 232 | Resource: "*" 233 | - Effect: Allow 234 | Action: "sqs:get*" 235 | Resource: "*" 236 | - Effect: Allow 237 | Action: "sqs:list*" 238 | Resource: "*" 239 | - Effect: Allow 240 | Action: "states:describe*" 241 | Resource: "*" 242 | - Effect: Allow 243 | Action: "states:get*" 244 | Resource: "*" 245 | - Effect: Allow 246 | Action: "states:list*" 247 | Resource: "*" 248 | - Effect: Allow 249 | Action: "tag:get*" 250 | Resource: "*" 251 | - PolicyName: Logs 252 | PolicyDocument: 253 | Version: 2012-10-17 254 | Statement: 255 | - Effect: Allow 256 | Action: "logs:deleteSubscriptionFilter" 257 | Resource: "*" 258 | - Effect: Allow 259 | Action: "logs:describeLogStreams" 260 | Resource: "*" 261 | - Effect: Allow 262 | Action: "logs:describeSubscriptionFilters" 263 | Resource: "*" 264 | - Effect: Allow 265 | Action: "logs:filterLogEvents" 266 | Resource: "*" 267 | - Effect: Allow 268 | Action: "logs:putSubscriptionFilter" 269 | Resource: "*" 270 | - Effect: Allow 271 | Action: "logs:startQuery" 272 | Resource: "*" 273 | - Effect: Allow 274 | Action: "logs:stopQuery" 275 | Resource: "*" 276 | - PolicyName: Metrics 277 | PolicyDocument: 278 | Version: 2012-10-17 279 | Statement: 280 | - Effect: Allow 281 | Action: "cloudwatch:get*" 282 | Resource: "*" 283 | - PolicyName: Traces 284 | PolicyDocument: 285 | Version: 2012-10-17 286 | Statement: 287 | - Effect: Allow 288 | Action: "xray:batch*" 289 | Resource: "*" 290 | - Effect: Allow 291 | Action: "xray:get*" 292 | Resource: "*" 293 | Parameters: 294 | ExternalId: 295 | Description: "The external ID for the LAMBDAWG delegation role" 296 | Type: String 297 | 298 | Outputs: 299 | Version: 300 | Description: LAMBDAWG CF template version 301 | Value: 2020-02-06 302 | LambdawgDelegationRoleArn: 303 | Description: "The ARN for the LAMBDAWG delegation role" 304 | Value: !GetAtt 305 | - LambdawgDelegationRole 306 | - Arn 307 | -------------------------------------------------------------------------------- /server/controllers/MetricsController.js: -------------------------------------------------------------------------------- 1 | const Redis = require('redis'); 2 | // Open a new redis client to send requests to the redis server. 3 | const redisClient = Redis.createClient(); 4 | // Check if the connection is open before proceeding. 5 | (async () => { 6 | await redisClient.connect().catch((err) => { 7 | console.log('Redis Connect Error: ' + err.message); 8 | }); 9 | })(); 10 | // Use AWS SDK for javascript v3 to import cloudWatch Metrics. 11 | const { 12 | CloudWatch, 13 | GetMetricDataCommand, 14 | } = require('@aws-sdk/client-cloudwatch'); 15 | 16 | //declare an rdsMetricsController object 17 | const MetricsController = {}; 18 | 19 | MetricsController.getMetrics = async (req, res, next) => { 20 | console.log('in metrics'); 21 | 22 | //check if the user's log data is present in redis before making the api calls. 23 | redisClient 24 | .get('LambdaMetrics' + req.body.arn) 25 | .then((data) => JSON.parse(data)) 26 | .then((data) => { 27 | console.log(data); 28 | if (data !== null) { 29 | res.locals.getLambdaMetrics = data; 30 | return next(); 31 | } else { 32 | console.log('cache miss'); 33 | const fullfunc = async () => { 34 | //declare a constant variable called cloudwatch, which will be used to call the AWS CloudWatch API 35 | const cloudwatch = new CloudWatch({ 36 | region: 'us-east-1', 37 | credentials: res.locals.credentials, 38 | }); 39 | 40 | //declare a constant variable called params, which will be used to pass the parameters of the RDSCpuUtilization metrics to the AWS CloudWatch API 41 | const params = { 42 | MetricDataQueries: [], 43 | StartTime: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), // one hour ago 44 | EndTime: new Date(), // now 45 | ScanBy: 'TimestampDescending', // the order to return data, newest first 46 | }; 47 | 48 | //try to call the AWS CloudWatch API to get the RDSCpuUtilization metrics 49 | for (const Lambda in res.locals.lambdaNames) { 50 | console.log('lambdaName ' + Lambda); 51 | params.MetricDataQueries.push({ 52 | Id: `invocations${Lambda}`, 53 | MetricStat: { 54 | Metric: { 55 | Namespace: 'AWS/Lambda', 56 | MetricName: 'Invocations', 57 | Dimensions: [ 58 | { 59 | Name: 'FunctionName', 60 | Value: res.locals.lambdaNames[Lambda], 61 | }, 62 | ], 63 | }, 64 | Period: 300, // the granularity of the data, in seconds 65 | Stat: 'Sum', // the statistic to retrieve for this metric 66 | }, 67 | ReturnData: true, // whether to return the data for this query 68 | }); 69 | params.MetricDataQueries.push({ 70 | Id: `errors${Lambda}`, 71 | MetricStat: { 72 | Metric: { 73 | Namespace: 'AWS/Lambda', 74 | MetricName: 'Errors', 75 | Dimensions: [ 76 | { 77 | Name: 'FunctionName', 78 | Value: res.locals.lambdaNames[Lambda], 79 | }, 80 | ], 81 | }, 82 | Period: 300, // the granularity of the data, in seconds 83 | Stat: 'Sum', // the statistic to retrieve for this metric 84 | }, 85 | ReturnData: true, // whether to return the data for this query 86 | }); 87 | } 88 | try { 89 | console.log('in try'); 90 | // const data = await cloudwatch.getMetricData(params); 91 | const data = await cloudwatch.send( 92 | new GetMetricDataCommand(params) 93 | ); 94 | console.log('full montey'); 95 | res.locals.getLambdaMetrics = data; 96 | await redisClient.set( 97 | 'LambdaMetrics' + req.body.arn, 98 | JSON.stringify(data), 99 | 'EX', 100 | 60 * 60 101 | ); 102 | return next(); 103 | //if there is an error, log the error and throw the error 104 | } catch (error) { 105 | console.error(error); 106 | throw error; 107 | } 108 | }; 109 | fullfunc(); 110 | } 111 | }); 112 | }; 113 | 114 | module.exports = MetricsController; 115 | -------------------------------------------------------------------------------- /server/controllers/authControllers.js: -------------------------------------------------------------------------------- 1 | const db = require('../models/dbPool'); 2 | const bcrypt = require('bcrypt'); 3 | const validator = require('validator'); 4 | 5 | const authControllers = {}; 6 | //Verifies that the User Name and Password exist and match 7 | authControllers.verifyUN_Pass = (req, res, next) => { 8 | const { user_name } = req.params; 9 | const { password_ } = req.body[0]; 10 | 11 | const text = 'SELECT * FROM public.users WHERE user_name = $1'; 12 | db.query(text, [user_name], async (err, result) => { 13 | if (err) { 14 | console.log('Error Executing Authorization Query', err); 15 | return res.status(500).send('Error Executing Authorization Query'); 16 | } 17 | 18 | if (result.rows.length === 0) { 19 | console.log('Invalid Credentials'); 20 | return res.status(401).send('Invalid Credentials'); 21 | } 22 | 23 | const password_hash = result.rows[0].password_; 24 | 25 | const match = await bcrypt.compare(password_, password_hash); 26 | 27 | if (!match) { 28 | console.log('username/pw do not match'); 29 | return res.status(401).send('Invalid Credentials!'); 30 | } 31 | 32 | console.log('User Authentication Successful'); 33 | res.locals.user = result.rows[0]; 34 | return next(); 35 | }); 36 | }; 37 | //Using Validator to sanitize the incoming data, to help prevent any code injections 38 | authControllers.validator = (req, res, next) => { 39 | const { full_name, user_name, email, password_ } = req.body[0]; 40 | if (!validator.isLength(full_name, { min: 1, max: 255 })) { 41 | return res.status(400).send('Full Name must be between 1-255 Characters'); 42 | } 43 | if (!validator.isLength(user_name, { min: 1, max: 255 })) { 44 | return res.status(400).send('User Name must be between 1-255 Characters'); 45 | } 46 | if (!validator.isEmail(email, { min: 1, max: 255 })) { 47 | return res.status(400).send('Invalid Email Address'); 48 | } 49 | if (!validator.isLength(password_, { min: 8, max: 255 })) { 50 | console.log('Password must be at least 8 characters long'); 51 | return res.status(400).send('Password must be at least 8 characters long'); 52 | } 53 | //the below regex will ensure that the user puts in a more secure password 54 | // else if ( 55 | // !validator.matches( 56 | // password_, 57 | // /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#\$%\^&\*])/ 58 | // ) 59 | // ) { 60 | // console.log( 61 | // 'Password must contain at least one lowercase letter, one uppercase letter, one digit, and one special character' 62 | // ); 63 | // return res 64 | // .status(400) 65 | // .send( 66 | // 'Password must contain at least one lowercase letter, one uppercase letter, one digit, and one special character' 67 | // ); 68 | // } 69 | else { 70 | console.log('Password is valid'); 71 | return next(); 72 | } 73 | }; 74 | 75 | module.exports = authControllers; 76 | -------------------------------------------------------------------------------- /server/controllers/cookieControllers.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const jwt = require('jsonwebtoken'); 3 | 4 | const cookieControllers = {}; 5 | 6 | cookieControllers.setCookie = (req, res, next) => { 7 | const { user_name } = req.params; 8 | 9 | const accessToken = jwt.sign(user_name, process.env.ACCESS_SECRET_TOKEN); 10 | 11 | //sends the token as a cookie 12 | //Add ' secure: true ' if you want to make it via https only but would need to have certificate first 13 | 14 | res.cookie('jwt', accessToken, { 15 | httpOnly: true, 16 | }); 17 | return next(); 18 | }; 19 | 20 | cookieControllers.authenticateCookie = (req, res, next) => { 21 | console.log('inside cookie controller', JSON.stringify(req.cookie)); 22 | 23 | //assigns the jwt string to authHeader 24 | const authHeader = req.cookies.jwt; 25 | //this is checking to see if we have a cookie, and if we do, assign the cookie to token 26 | const token = authHeader && authHeader; 27 | 28 | //checks if token doesn't exist 29 | //if it does, verifies if its legit 30 | //if not legit sends back an error 31 | if (!token) { 32 | return res.status(401).json({ message: 'No Cookie' }); 33 | } 34 | try { 35 | const decoded = jwt.verify(token, process.env.ACCESS_SECRET_TOKEN); 36 | res.locals.user_name = decoded; 37 | return next(); 38 | } catch (err) { 39 | return res.status(401).json({ message: 'No Cookie' }); 40 | } 41 | }; 42 | 43 | cookieControllers.deleteCookie = (req, res, next) => { 44 | try { 45 | res.clearCookie('jwt'); 46 | return next(); 47 | } catch (err) { 48 | return res.status(401).json({ message: 'Issue Deleting Cookie' }); 49 | } 50 | }; 51 | 52 | module.exports = cookieControllers; 53 | -------------------------------------------------------------------------------- /server/controllers/credentialController.js: -------------------------------------------------------------------------------- 1 | const Redis = require('redis'); 2 | // Open a new redis client to send requests to the redis server. 3 | const redisClient = Redis.createClient(); 4 | // Check if the connection is open befors proceeding. 5 | (async () => { 6 | await redisClient.connect().catch((err) => { 7 | console.log('Redis Connect Error: ' + err.message); 8 | }); 9 | })(); 10 | 11 | // Use AWS SDK for javascript v3 to import session Token. 12 | const { STSClient, AssumeRoleCommand } = require('@aws-sdk/client-sts'); 13 | const { triggerAsyncId } = require('async_hooks'); 14 | const dotenv = require('dotenv').config(); 15 | const { _KEY, _SKEY, USER_ARN } = process.env; 16 | 17 | const credentialController = {}; 18 | //declare a constant variable called credentials, which will be used to call the AWS STS API, passing in the access key id and secret access key of the cloudband account 19 | const credentials = { 20 | accessKeyId: _KEY, 21 | secretAccessKey: _SKEY, 22 | }; 23 | 24 | //get user's access key id and secret access key from AWS 25 | credentialController.getCredentials = async (req, res, next) => { 26 | // console.log('credential controller arn', req.body.arn); 27 | const { arn } = req.body; 28 | // const arn = USER_ARN; 29 | const region = 'us-east-1'; 30 | const info = { 31 | RoleArn: arn, 32 | RoleSessionName: 'LambdawgRoleSession', 33 | DurationSeconds: 900, 34 | ExternalId: 'Lambdawg', 35 | }; 36 | //create new STS client instance with cloudband's region and credentials 37 | const stsClient = new STSClient({ region: region, credentials: credentials }); 38 | 39 | //send request to AWS to assume role in user's account to retrieve temporary credentials for read-only access 40 | try { 41 | const assumedRole = await stsClient.send(new AssumeRoleCommand(info)); 42 | const accessKeyId = assumedRole.Credentials.AccessKeyId; 43 | const secretAccessKey = assumedRole.Credentials.SecretAccessKey; 44 | const sessionToken = assumedRole.Credentials.SessionToken; 45 | res.locals.credentials = { accessKeyId, secretAccessKey, sessionToken }; 46 | return next(); 47 | } catch (error) { 48 | console.log(error); 49 | next(error); 50 | } 51 | }; 52 | 53 | //delete user-specific redis data 54 | credentialController.deleteRedis = async (req, res, next) => { 55 | redisClient.del('LambdaTraces' + req.body.arn, (err, result) => { 56 | if (err) { 57 | console.log(err); 58 | return next(err); 59 | } 60 | }); 61 | redisClient.del('LambdaLogs' + req.body.arn, (err, result) => { 62 | if (err) { 63 | console.log(err); 64 | return next(err); 65 | } 66 | }); 67 | redisClient.del('LambdaList' + req.body.arn, (err, result) => { 68 | if (err) { 69 | console.log(err); 70 | return next(err); 71 | } 72 | }); 73 | redisClient.del('LambdaMetrics' + req.body.arn, (err, result) => { 74 | if (err) { 75 | console.log(err); 76 | return next(err); 77 | } 78 | }); 79 | console.log('redis user data deleted !'); 80 | return res.status(200).json('Redis user data deleted'); 81 | }; 82 | 83 | module.exports = credentialController; -------------------------------------------------------------------------------- /server/controllers/dbControllers.js: -------------------------------------------------------------------------------- 1 | const db = require('../models/dbPool'); 2 | const validator = require('validator'); 3 | require('dotenv').config(); 4 | 5 | const testKey = process.env.TEST_TOKEN; 6 | 7 | const dbControllers = {}; 8 | 9 | dbControllers.getUser = (req, res, next) => { 10 | //we will be getting the user name from the authenticate cookie here 11 | const { user_name } = res.locals; 12 | const text = `SELECT * FROM "public"."users" WHERE "user_name" = '${user_name}'`; 13 | db.query(text) 14 | .then((response) => { 15 | res.locals.data = response; 16 | return next(); 17 | }) 18 | .catch((err) => { 19 | next({ 20 | log: `Express Error Handler caught getUsers error: ${err}`, 21 | status: 500, 22 | message: { 23 | err: 'dbControllerTest.getUsers encountered an error', 24 | }, 25 | }); 26 | }); 27 | }; 28 | 29 | dbControllers.addUser = (req, res, next) => { 30 | const text = `INSERT INTO "public"."users" (full_name, user_name, email, password_,${testKey}) VALUES ($1, $2, $3, $4, $5)`; 31 | const { full_name, user_name, email } = req.body[0]; 32 | const forTesting = req.body[0][testKey]; 33 | const { password_ } = res.locals; 34 | 35 | db.query( 36 | text, 37 | [full_name, user_name, email, password_, forTesting], 38 | (err, result) => { 39 | if (err) { 40 | console.log('Error at dbControllers.addUser: ', err); 41 | return res.status(500).send('Error Executing Insert Query '); 42 | } 43 | console.log('Add User Query Executed Successfully'); 44 | res.locals.user_name = user_name; 45 | next(); 46 | } 47 | ); 48 | }; 49 | 50 | dbControllers.deleteUser = (req, res, next) => { 51 | const text = 'DELETE FROM "public"."users" WHERE user_name = $1'; 52 | const { user_name } = req.params; 53 | 54 | db.query(text, [user_name], (err, result) => { 55 | if (err) { 56 | console.log('Error at dbControllers.deleteUser: ', err); 57 | return res.status(500).send('Error Executing Delete Query'); 58 | } 59 | if (result.rowCount === 0) { 60 | console.log('User Not Found'); 61 | return res.status(404).send('User Not Found'); 62 | } 63 | console.log('Delete User Query Executed Successfully'); 64 | next(); 65 | }); 66 | }; 67 | 68 | dbControllers.editUser = (req, res, next) => { 69 | const { arn, region, _id, user_name } = req.body[0]; 70 | 71 | const text = 72 | 'UPDATE "public"."users" SET arn = $1, region = $2 WHERE _id = $3'; 73 | 74 | db.query(text, [arn, region, _id], (err, result) => { 75 | if (err) { 76 | console.log(`Error Updating User: ${user_name}`, err); 77 | return res.status(500).send(`Error Updating User: ${user_name}`); 78 | } 79 | console.log(`${user_name} updated Successfully`); 80 | next(); 81 | }); 82 | }; 83 | 84 | module.exports = dbControllers; 85 | -------------------------------------------------------------------------------- /server/controllers/encryptionController.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require('bcrypt'); 2 | 3 | //Using Bcrypt to encrypt the users password 4 | const encryptionController = {}; 5 | 6 | encryptionController.hashPW = (req, res, next) => { 7 | const { password_ } = req.body[0]; 8 | bcrypt.hash(password_, 10, function (err, hash) { 9 | if (err) { 10 | console.log('Error Saving User Credentials'); 11 | return res.status(500).send('Error Saving User Credentials'); 12 | } else { 13 | console.log('Credentials Saved Successfully'); 14 | 15 | res.locals.password_ = hash; 16 | return next(); 17 | } 18 | }); 19 | }; 20 | 21 | module.exports = encryptionController; 22 | -------------------------------------------------------------------------------- /server/controllers/fileController.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const fileController = {}; 4 | 5 | //d3 node chart needs to read data from a json file. fs system does not work from the front end 6 | fileController.writeToFile = (req, res, next) => { 7 | const { serviceIds } = req.body; 8 | console.log("in filecontroller", serviceIds); 9 | //writing to service.json file in Charts. D3 node chart is set to read from service.json from the same Charts folder 10 | const filePath = path.resolve(__dirname, "../../src/Chart/service.json"); 11 | 12 | //separate service parser function to convert serviceIds array to a format readable by the d3 chart 13 | function ServiceParser(arrayObj) { 14 | const serviceJSON = { 15 | nodes: [], 16 | links: [], 17 | }; 18 | arrayObj.forEach((obj) => { 19 | const serviceArray = obj.serviceIds; 20 | //each service id needs a unique key so the links can work from service to service (if related) 21 | serviceArray.forEach((serviceObj, i) => { 22 | serviceJSON.nodes.push({ 23 | id: `${serviceObj.Name}${i}`, 24 | name: serviceObj.Type, 25 | }); 26 | //source needs a "0" unique id as the first item in each serviceObj is the source for the other 2 services 27 | serviceJSON.links.push({ 28 | source: `${serviceObj.Name}0`, 29 | target: `${serviceObj.Name}${i}`, 30 | }); 31 | }); 32 | }); 33 | //convert the dataResults to JSON format before writing to file 34 | const dataResults = JSON.stringify(serviceJSON); 35 | 36 | //the "w" flag will overwrite whatever is already in the JSON file to update it 37 | fs.writeFile(filePath, dataResults, { flag: "w" }, (err) => { 38 | console.log("filePath", filePath); 39 | if (err) { 40 | console.error(err); 41 | return; 42 | } 43 | console.log("Data written to file"); 44 | res.locals.writtenServices = dataResults; 45 | return next(); 46 | }); 47 | } 48 | ServiceParser(serviceIds); 49 | }; 50 | module.exports = fileController; 51 | -------------------------------------------------------------------------------- /server/controllers/lambdaLogsController.js: -------------------------------------------------------------------------------- 1 | const Redis = require('redis'); 2 | // Open a new redis client to send requests to the redis server. 3 | const redisClient = Redis.createClient(); 4 | // Check if the connection is open before proceeding. 5 | (async () => { 6 | await redisClient.connect().catch((err) => { 7 | console.log('Redis Connect Error: ' + err.message); 8 | }); 9 | })(); 10 | 11 | // Use AWS SDK for javascript v3 to import cloudWatch logs. 12 | const { 13 | CloudWatchLogsClient, 14 | FilterLogEventsCommand, 15 | } = require('@aws-sdk/client-cloudwatch-logs'); 16 | 17 | //retrieve logs of each lambda function 18 | const getLambdaLogs = async (req, res, next) => { 19 | //check if the user's log data is present in redis before making the api calls. 20 | redisClient 21 | .get('LambdaLogs' + req.body.arn) 22 | .then((data) => JSON.parse(data)) 23 | .then((data) => { 24 | if (data !== null) { 25 | res.locals.functionLogs = data; 26 | return next(); 27 | } else { 28 | console.log('cache miss'); 29 | //executes call if no valid redis data is found 30 | const fullfunc = async () => { 31 | res.locals.functionLogs = []; 32 | console.log(res.locals.lambdaNames); 33 | for (let Lambda of res.locals.lambdaNames) { 34 | console.log('in lambda ' + Lambda); 35 | const currFunc = Lambda; 36 | const logGroupName = '/aws/lambda/' + currFunc; 37 | 38 | //create new instance of CloudWatchLogsClient with user's region and credentials 39 | const cloudWatchLogs = new CloudWatchLogsClient({ 40 | region: 'us-east-1', 41 | credentials: res.locals.credentials, 42 | }); 43 | 44 | //initiate starttime to be 7 days before endtime in milliseconds from epoch time 45 | const now = new Date(); 46 | const EndTime = now.valueOf(); 47 | const StartTime = new Date( 48 | now.getTime() - 7 * 24 * 60 * 60 * 1000 49 | ).valueOf(); 50 | 51 | //helper function to recursively retrieve logs if there's a nextToken 52 | const nextTokenHelper = async (nextToken, data = []) => { 53 | if (!nextToken) { 54 | return data; 55 | } 56 | const nextLogEvents = await cloudWatchLogs.send( 57 | new FilterLogEventsCommand({ 58 | logGroupName, 59 | endTime: EndTime.valueOf(), 60 | startTime: StartTime.valueOf(), 61 | nextToken, 62 | filterPattern: '- START - END ', 63 | }) 64 | ); 65 | data.push(nextLogEvents.events); 66 | return nextTokenHelper(nextLogEvents.nextToken, data); 67 | }; 68 | 69 | //AWS query format for each individual lambda function to list logs 70 | try { 71 | const logEvents = await cloudWatchLogs.send( 72 | new FilterLogEventsCommand({ 73 | logGroupName, 74 | endTime: EndTime.valueOf(), 75 | startTime: StartTime.valueOf(), 76 | filterPattern: '- START - END ', 77 | }) 78 | ); 79 | 80 | const Redis = require('redis'); 81 | // const getOrSetCache = require('../redis'); 82 | const redisClient = Redis.createClient(); 83 | (async () => { 84 | await redisClient.connect().catch((err) => { 85 | console.log('Redis Connect Error: ' + err.message); 86 | }); 87 | })(); 88 | // console.log('LogEvents ' + JSON.stringify(logEvents)); 89 | //if there are no logs, return to next middleware 90 | if (!logEvents) { 91 | return next(); 92 | } 93 | 94 | //if there is a nextToken, recursively retrieve logs with helper function 95 | if (logEvents.nextToken) { 96 | const nextTokenData = await nextTokenHelper( 97 | logEvents.nextToken 98 | ); 99 | logEvents.events = logEvents.events.concat(...nextTokenData); 100 | } 101 | 102 | //only return the first 50 logs 103 | const fiftyLogEvents = logEvents.events.slice(0, 50); 104 | 105 | //format the logs to remove unnecessary information 106 | const logEventsMessages = []; 107 | fiftyLogEvents.forEach((event) => { 108 | if ( 109 | event.message.slice(0, 4) !== 'LOGS' && 110 | event.message.slice(0, 9) !== 'EXTENSION' 111 | ) { 112 | logEventsMessages.push({ 113 | message: event.message.slice(67), 114 | timestamp: new Date(event.timestamp), 115 | }); 116 | } else { 117 | logEventsMessages.push({ 118 | message: event.message, 119 | timeStamp: new Date(event.timestamp), 120 | }); 121 | } 122 | }); 123 | // console.log('logeventmessages ' + logEventsMessages); 124 | res.locals.functionLogs.push({ 125 | FunctionName: Lambda, 126 | logs: logEventsMessages, 127 | }); 128 | await redisClient.set( 129 | 'LambdaLogs' + req.body.arn, 130 | JSON.stringify(res.locals.functionLogs), 131 | 'EX', 132 | 60 * 60 133 | ); 134 | } catch (err) { 135 | if (err) console.error(err); 136 | console.log('there was an error in the loop : ' + err); 137 | //return next(err); 138 | } 139 | } 140 | return next(); 141 | }; 142 | fullfunc(); 143 | } 144 | }); 145 | }; 146 | 147 | module.exports = { 148 | getLambdaLogs, 149 | }; 150 | -------------------------------------------------------------------------------- /server/controllers/listLambdasController.js: -------------------------------------------------------------------------------- 1 | const Redis = require('redis'); 2 | // Open a new redis client to send requests to the redis server. 3 | const redisClient = Redis.createClient(); 4 | // Check if the connection is open before proceeding. 5 | (async () => { 6 | await redisClient.connect().catch((err) => { 7 | console.log('Redis Connect Error: ' + err.message); 8 | }); 9 | })(); 10 | 11 | // Use AWS SDK for javascript v3 to import cloudWatch logs. 12 | const { 13 | LambdaClient, 14 | ListFunctionsCommand, 15 | } = require('@aws-sdk/client-lambda'); 16 | const { get } = require('../routes/api'); 17 | const { json } = require('stream/consumers'); 18 | 19 | const listLambdasController = {}; 20 | 21 | //retrieve name of all user's lambda functions 22 | listLambdasController.getLambdas = (req, res, next) => { 23 | redisClient 24 | //check if the user's function list is present in redis before making the api call 25 | .get('LambdaList' + req.body.arn) 26 | .then((data) => json(data)) 27 | .then((data) => (res.locals.lambdaNames = data)) 28 | .then((lambdaNames) => { 29 | console.log(res.locals.lambdaNames); 30 | }) 31 | .then((data) => next()) 32 | .catch((err) => { 33 | console.log('cache miss'); 34 | //executes call if no valid redis data is found 35 | const fullfunc = async () => { 36 | //create new instance of LambdaClient with user's region and credentials 37 | const lambdaClient = new LambdaClient({ 38 | region: 'us-east-1', 39 | credentials: res.locals.credentials, 40 | }); 41 | 42 | //retrieve names of lambda functions (max 10) 43 | try { 44 | const allFuncs = await lambdaClient.send( 45 | new ListFunctionsCommand({ 46 | MaxItems: 10, 47 | FunctionVersion: 'ALL', 48 | }) 49 | ); 50 | 51 | //create a list of all functions and save it to redis and res.locals 52 | const funcList = allFuncs.Functions.map((func) => func.FunctionName); 53 | await redisClient.set( 54 | 'LambdaList' + req.body.arn, 55 | JSON.stringify(funcList), 56 | 'EX', 57 | 60 * 60 58 | ); 59 | console.log('full montey'); 60 | res.locals.lambdaNames = funcList; 61 | 62 | return next(); 63 | } catch (err) { 64 | console.log('Error in listLambdas', err); 65 | return next(err); 66 | } 67 | }; 68 | fullfunc(); 69 | }); 70 | }; 71 | 72 | module.exports = listLambdasController; 73 | -------------------------------------------------------------------------------- /server/controllers/tracesController.js: -------------------------------------------------------------------------------- 1 | const Redis = require('redis'); 2 | // const getOrSetCache = require('../redis'); 3 | const redisClient = Redis.createClient(); 4 | (async () => { 5 | await redisClient.connect().catch((err) => { 6 | console.log('Redis Connect Error: ' + err.message); 7 | }); 8 | })(); 9 | 10 | const { 11 | XRayClient, 12 | GetTraceSummariesCommand, 13 | } = require('@aws-sdk/client-xray'); 14 | const tracesController = {}; 15 | 16 | tracesController.getTraces = async (req, res, next) => { 17 | console.log('in tracescontroller getTraces'); 18 | 19 | redisClient 20 | .get('LambdaTraces' + req.body.arn) 21 | .then((data) => JSON.parse(data)) 22 | .then((data) => { 23 | console.log(data); 24 | if (data !== null) { 25 | res.locals.traces = data; 26 | return next(); 27 | } else { 28 | console.log('cache miss'); 29 | const fullfunc = async () => { 30 | const { lambdaNames } = res.locals; 31 | 32 | console.log(lambdaNames); 33 | // Set the AWS Region and X-Ray client object 34 | const REGION = 'us-east-1'; 35 | const xrayClient = new XRayClient({ 36 | region: REGION, 37 | credentials: res.locals.credentials, 38 | }); 39 | const dataPromise = []; 40 | const dataArray = []; 41 | lambdaNames.forEach((lambda) => { 42 | console.log(lambda); 43 | const name = `service("${lambda}")`; 44 | const params = { 45 | StartTime: new Date(Date.now() - 1000 * 60 * 60 * 10), //past 10 hours 46 | EndTime: new Date(), 47 | Sampling: false, 48 | FilterExpression: name, 49 | // NextToken: nextToken, 50 | }; 51 | try { 52 | dataPromise.push( 53 | xrayClient 54 | .send(new GetTraceSummariesCommand(params)) 55 | .then((data) => { 56 | dataArray.push({ 57 | name: lambda, 58 | summary: data.TraceSummaries, 59 | }); 60 | }) 61 | ); 62 | } catch (err) { 63 | console.error(err); 64 | } 65 | }); 66 | await Promise.all(dataPromise); 67 | res.locals.traces = dataArray; 68 | console.log('full montey'); 69 | await redisClient.set( 70 | 'LambdaTraces' + req.body.arn, 71 | JSON.stringify(res.locals.traces), 72 | 'EX', 73 | 60 * 60 74 | ); 75 | console.log('dataArray', dataArray); 76 | return next(); 77 | }; 78 | fullfunc(); 79 | } 80 | }); 81 | }; 82 | 83 | module.exports = tracesController; 84 | -------------------------------------------------------------------------------- /server/models/dbPool.js: -------------------------------------------------------------------------------- 1 | const { Pool } = require('pg'); 2 | const dotenv = require('dotenv').config(); 3 | 4 | //our address is stored in an env file 5 | const { PG_URI } = process.env; 6 | 7 | //create a new pool using the connection string above that brings us to our database 8 | // 9 | const pool = new Pool({ 10 | connectionString: PG_URI, 11 | idleTimeoutMillis: 5000, 12 | connectionTimeoutMillis: 7500, 13 | end: function () { 14 | return pool.end(); 15 | }, 16 | connect: function () { 17 | return pool.connect(); 18 | }, 19 | }); 20 | 21 | //Now we need to export an object that has a query method in it, that we'll name query 22 | //this method will return the invocation of the pool.query() after logging the query 23 | //***This will be required in our controllers to gain access to pur database*** 24 | module.exports = { 25 | query: (text, params, callback) => { 26 | console.log('Executed Query: ', text); 27 | return pool.query(text, params, callback); 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /server/redis.js: -------------------------------------------------------------------------------- 1 | // const Redis = require('redis'); //for prod: Redis.createClient({url : xxxx}) 2 | // const redisClient = Redis.createClient(); 3 | // (async () => { 4 | // await redisClient.connect().catch((err) => { 5 | // console.log('Redis Connect Error: ' + err.message); 6 | // }); 7 | // })(); 8 | // // const DEFAULT_EXPIRATION_REDIS = 3600; 9 | // // incontroller = () => { 10 | // // //check if Redis Data present before making an API call 11 | // // redisClient.get('metrics', (error, metrics) => { 12 | // // if (error) console.error(error); 13 | // // if (metrics) { 14 | // // res.locals.xxxxx = JSON.parse(metrics); 15 | // // } else { 16 | // // //API CALL HERE 17 | // // } 18 | // // }); 19 | // // //save data after call 20 | // // redisClient.setex('metrics', DEFAULT_EXPIRATION_REDIS, JSON.stringify(data)); //replace data by the returned value of the API call 21 | // // }; 22 | 23 | // async function getOrSetCache(key, expiration, cb) { 24 | // console.log('in redis client'); 25 | // return new Promise((resolve, reject) => { 26 | // redisClient.get(key, async (error, data) => { 27 | // if (error) { 28 | // console.log('error'); 29 | // return reject(error); 30 | // } 31 | // if (data) { 32 | // console.log('loading2'); 33 | // return resolve(JSON.parse(data)); 34 | // } 35 | // console.log('loading'); 36 | // const freshData = await cb(); 37 | // redisClient.set(key, JSON.stringify(freshData), EX, expiration); 38 | // resolve(freshData); 39 | // }); 40 | // }); 41 | // } 42 | 43 | // // incotroller2 = async () => { 44 | // // const metrics = await getOrSetCache('querystring', async () => { 45 | // // //put API CALL HERE 46 | // // }); 47 | // // }; 48 | 49 | // module.exports = getOrSetCache; 50 | -------------------------------------------------------------------------------- /server/routes/api.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const cookieParser = require('cookie-parser'); 3 | 4 | const dbController = require('../controllers/dbControllers'); 5 | const authController = require('../controllers/authControllers'); 6 | const encryptionController = require('../controllers/encryptionController'); 7 | const cookieController = require('../controllers/cookieControllers.js'); 8 | 9 | const router = express.Router(); 10 | 11 | router.use(cookieParser()); 12 | 13 | router.get( 14 | '/', 15 | cookieController.authenticateCookie, 16 | dbController.getUser, 17 | (req, res) => { 18 | res.status(200).json(res.locals.data.rows); 19 | } 20 | ); 21 | 22 | // Create new user 23 | router.post( 24 | '/newUser', 25 | authController.validator, 26 | encryptionController.hashPW, 27 | dbController.addUser, 28 | dbController.getUser, 29 | (req, res) => { 30 | res.status(200).json(res.locals.data.rows[0]); 31 | } 32 | ); 33 | 34 | // Delete user 35 | router.delete( 36 | '/delete/:user_name', 37 | authController.verifyUN_Pass, 38 | dbController.deleteUser, 39 | (req, res) => res.status(200).json({}) 40 | ); 41 | 42 | // Updates ARN and region 43 | router.patch( 44 | '/edit/:user_name', 45 | authController.verifyUN_Pass, 46 | dbController.editUser, 47 | (req, res) => { 48 | console.log(res.locals.user); 49 | res.status(200).json(res.locals.user); 50 | } 51 | ); 52 | 53 | // Sign In 54 | router.post( 55 | '/:user_name', 56 | authController.verifyUN_Pass, 57 | cookieController.setCookie, 58 | dbController.getUser, 59 | (req, res) => { 60 | res.status(200).json(res.locals.user); 61 | } 62 | ); 63 | 64 | // Delete JWT 65 | router.get('/logout', cookieController.deleteCookie, (req, res) => { 66 | res.status(200).json(); 67 | }); 68 | 69 | module.exports = router; 70 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | 2 | const express = require('express'); 3 | const cookieParser = require('cookie-parser'); 4 | const path = require('path'); 5 | const cors = require('cors'); 6 | const credentialController = require('./controllers/credentialController'); 7 | const listLambdasController = require('./controllers/listLambdasController'); 8 | const MetricsController = require('./controllers/MetricsController.js'); 9 | const lambdaLogsController = require('./controllers/lambdaLogsController'); 10 | const tracesController = require('./controllers/tracesController.js'); 11 | const jwt = require('jsonwebtoken'); 12 | const fileController = require("./controllers/fileController.js"); 13 | 14 | const app = express(); 15 | 16 | const apiRouter = require("./routes/api"); 17 | 18 | const PORT = process.env.PORT || 3000; 19 | 20 | /** 21 | * Automatically parse urlencoded body content and form data from incoming requests and place it 22 | * in req.body 23 | */ 24 | 25 | app.use(express.json()); 26 | app.use(express.urlencoded({ extended: true })); 27 | app.use( 28 | cors({ 29 | origin: "http://localhost:5173", 30 | credentials: true, 31 | }) 32 | ); 33 | app.use(cookieParser()); 34 | 35 | app.use(express.static(path.resolve(__dirname, "../src"))); 36 | 37 | //Define Route handlers Here 38 | //--------------- 39 | app.use("/api", apiRouter); 40 | 41 | app.post( 42 | "/getLambdaNames", 43 | credentialController.getCredentials, 44 | listLambdasController.getLambdas, 45 | // lambdaLogsController.getLambdaLogs, 46 | // rdsMetricsController.getRDSCPUUtilizationMetrics, 47 | (req, res) => { 48 | return res.status(200).json(res.locals.lambdaNames); 49 | } 50 | ); 51 | 52 | //Return a JSON object containing all lambda function traces. 53 | //The route first requests fresh credentials from the user's AWS accounts. Then collects lambda function names and uses them to request AWS XRAY trace data for all functions 54 | // Redis implemented 55 | app.post( 56 | "/getTraces", 57 | credentialController.getCredentials, 58 | listLambdasController.getLambdas, 59 | tracesController.getTraces, 60 | (req, res) => { 61 | return res.status(200).json(res.locals.traces); 62 | } 63 | ); 64 | 65 | //Return a JSON object containing all lambda function traces. 66 | //The route first requests fresh credentials from the user's AWS accounts. Then collects lambda function names and uses them to request AWS Cloudwatch log data for all functions 67 | // Redis implemented 68 | app.post( 69 | "/getLambdaLogs", 70 | credentialController.getCredentials, 71 | listLambdasController.getLambdas, 72 | lambdaLogsController.getLambdaLogs, 73 | (req, res) => { 74 | return res.status(200).json(res.locals.functionLogs); 75 | } 76 | ); 77 | 78 | //Return a JSON object containing all lambda function traces. 79 | //The route first requests fresh credentials from the user's AWS accounts. Then collects lambda function names and uses them to request AWS Cloudwatch Metrics data for all functions 80 | // Redis implemented 81 | app.post( 82 | "/getLambdaMetrics", 83 | credentialController.getCredentials, 84 | listLambdasController.getLambdas, 85 | MetricsController.getMetrics, 86 | (req, res) => { 87 | return res.status(200).json(res.locals.getLambdaMetrics); 88 | } 89 | ); 90 | 91 | app.post("/writeToFile", fileController.writeToFile, (req, res) => { 92 | return res.status(200).json(res.locals.writtenServices); 93 | }); 94 | 95 | app.delete('/deleteRedis', credentialController.deleteRedis, (req, res) => { 96 | return res.status(200).json('redis cache cleared'); 97 | }); 98 | 99 | 100 | //Catch All Route Handler for any requests to an unkown route 101 | //---------------- 102 | app.use((req, res) => res.status(404).send("This page cannot be found...")); 103 | 104 | //Default Express Error Handler here 105 | //____________ 106 | app.use((err, req, res, next) => { 107 | const defaultErr = { 108 | log: `Express error handler caught unknown middleware error: ${err}`, 109 | status: 500, 110 | message: { err: "An error occurred" }, 111 | }; 112 | const errorObj = Object.assign({}, defaultErr, err); 113 | console.log(errorObj.log); 114 | return res.status(errorObj.status).json(errorObj.message); 115 | }); 116 | 117 | //Start Server 118 | //__________ 119 | 120 | app.listen(PORT, () => { 121 | console.log(`Listening on port ${PORT}...`); 122 | }); 123 | 124 | module.exports = app; 125 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import { Route, Routes } from "react-router-dom"; 3 | import DashboardContainer from "./containers/DashboardContainer.jsx"; 4 | import LandingPageContainer from "./containers/LandingPageContainer.jsx"; 5 | import Auth from "./components/Auth.jsx"; 6 | import Docs from "./components/Docs.jsx"; 7 | import SettingsContainer from "./containers/SettingsContainer.jsx"; 8 | import Navbar from "./components/Navbar.jsx"; 9 | import "./styles/application.scss"; 10 | 11 | const App = (props) => { 12 | const [loggedIn, setLoggedIn] = useState(false); 13 | const [user, setUser] = useState({}); 14 | 15 | // Log user in and fetch data if JWT is present 16 | useEffect(() => { 17 | const checkAuth = async () => { 18 | try { 19 | const response = await fetch("/api/", { 20 | credentials: "include", 21 | }); 22 | if (response.ok) { 23 | const data = await response.json(); 24 | console.log("jwt fetch ok", data[0]); 25 | const { user_name, full_name, email, _id, arn, region } = data[0]; 26 | setUser({ 27 | full_name: full_name, 28 | user_name: user_name, 29 | email: email, 30 | _id: _id, 31 | arn: arn, 32 | region: region, 33 | }); 34 | setLoggedIn(true); 35 | } else { 36 | console.error("JWT app.jsx Unable to authenticate jwt"); 37 | setLoggedIn(false); 38 | } 39 | } catch (error) { 40 | console.error("JWT app.jsx", error); 41 | setLoggedIn(false); 42 | } 43 | }; 44 | checkAuth(); 45 | }, []); 46 | 47 | return ( 48 |
    49 | 50 |
    51 | 52 | 58 | ) : ( 59 | 60 | ) 61 | } 62 | /> 63 | 69 | ) : ( 70 | 71 | ) 72 | } 73 | /> 74 | 80 | ) : ( 81 | 82 | ) 83 | } 84 | /> 85 | } /> 86 | } /> 87 | 88 |
    89 |
    90 | ); 91 | }; 92 | 93 | export default App; 94 | -------------------------------------------------------------------------------- /src/Chart/BarChart.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { Chart as ChartJS, CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend } from "chart.js"; 3 | import { Bar } from "react-chartjs-2"; 4 | 5 | ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend); 6 | 7 | //"Duration": duration of the function execution in seconds. 8 | //"ResponseTime": time taken to generate the response in seconds. 9 | 10 | function HorizontalBarChart(props) { 11 | const { msTraces } = props; 12 | const [lambdaNames, setLambdaNames] = useState([]); 13 | const [duration, setDuration] = useState([]); 14 | const [responseTime, setResponseTime] = useState([]); 15 | const [lambdaServices, setLambdaServices] = useState([]); 16 | 17 | //configurations of the bar graph 18 | const options = { 19 | indexAxis: "y", 20 | elements: { 21 | bar: { 22 | borderWidth: 2, 23 | }, 24 | }, 25 | responsive: true, 26 | maintainAspectRatio: true, 27 | plugins: { 28 | legend: { 29 | position: "bottom", 30 | }, 31 | title: { 32 | display: false, 33 | text: "Lambda Latencies", 34 | }, 35 | media: [ 36 | { 37 | query: "(max-width: 1000px)", 38 | options: { 39 | plugins: { 40 | legend: { 41 | display: false, 42 | }, 43 | }, 44 | maintainAspectRatio: false, 45 | }, 46 | }, 47 | ], 48 | }, 49 | }; 50 | 51 | const data = { 52 | labels: lambdaNames, 53 | datasets: [ 54 | { 55 | label: "Duration", 56 | data: duration, 57 | borderColor: "#f75215", 58 | backgroundColor: "#f75215", 59 | }, 60 | { 61 | label: "Response Time", 62 | data: responseTime, 63 | borderColor: "#fad6c9", 64 | backgroundColor: "#fad6c9", 65 | }, 66 | ], 67 | }; 68 | 69 | useEffect(() => { 70 | if (!msTraces || !Array.isArray(msTraces)) { 71 | return console.log("Data incoming, please wait"); 72 | } 73 | const tempDuration = []; 74 | const tempResponseTime = []; 75 | const tempLambdaServices = []; 76 | const tempLambdaNames = []; 77 | 78 | msTraces.forEach((traceObj) => { 79 | tempDuration.push(traceObj.duration); 80 | tempResponseTime.push(traceObj.responseTime); 81 | tempLambdaNames.push(traceObj.name); 82 | tempLambdaServices.push(traceObj.serviceIds); 83 | }); 84 | //all parts of msTraces needs to be parsed separately to add to the bar chart options 85 | setDuration(tempDuration); 86 | setResponseTime(tempResponseTime); 87 | setLambdaServices(tempLambdaServices); 88 | setLambdaNames(tempLambdaNames); 89 | }, []); 90 | 91 | const width = window.screen.width / 2; 92 | const height = window.screen.height / 2; 93 | 94 | return ( 95 | //manually inputting style to inherit sizing of parent div 96 | 97 | ); 98 | } 99 | export default HorizontalBarChart; 100 | -------------------------------------------------------------------------------- /src/Chart/BubbleChart.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect, useState } from "react"; 2 | import * as d3 from "d3"; 3 | import Circle from "./Circle"; 4 | 5 | function BubbleChart({ bubbleChartData, handleTogglePanel }) { 6 | const [state, setState] = useState({ bubbleChartData: [] }); 7 | useEffect(() => { 8 | if (!bubbleChartData.length) console.log("No data yet, please wait"); 9 | setState({ bubbleChartData }); 10 | simulation(bubbleChartData); 11 | }, [bubbleChartData]); 12 | 13 | //sets size and relative radius of bubbles in chart 14 | function simulation(bubbleChartData) { 15 | const maxRadius = d3.max(bubbleChartData, (d) => d.count); 16 | const minRadius = d3.min(bubbleChartData, (d) => d.count); 17 | const radiusScale = d3.scaleSqrt().domain([minRadius, maxRadius]).range([10, 110]); 18 | 19 | const ticked = () => setState({ bubbleChartData }); 20 | //this force chart animates bubbles to flow toward the middle 21 | d3.forceSimulation() 22 | .nodes(bubbleChartData) 23 | .force("xTowardsTheCenter", d3.forceX(0).strength(0.01)) 24 | .force("yTowardsTheCenter", d3.forceY(100).strength(0.01)) 25 | .force( 26 | "collide", 27 | d3 28 | .forceCollide((d) => radiusScale(d.count)) 29 | .strength(1.5) 30 | .iterations(1) 31 | ) 32 | .on("tick", ticked); 33 | } 34 | 35 | //passes the name (circle id) of the bubble selected back to dashboard to select corresponding panel 36 | const handleCircleClick = (circle) => { 37 | handleTogglePanel(circle.name); 38 | }; 39 | 40 | //d3 chart boilerplate sizing 41 | const margins = { top: 20, right: 50, bottom: 20, left: 50 }; 42 | const svgDimensions = { width: window.screen.width, height: window.screen.height / 2 }; 43 | 44 | return ( 45 | 46 | 50 | 51 | 52 | 53 | tooltip 54 | 55 | 56 | ); 57 | } 58 | 59 | export default BubbleChart; 60 | -------------------------------------------------------------------------------- /src/Chart/Circle.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect, useState } from "react"; 2 | import * as d3 from "d3"; 3 | 4 | //function to render each bubble in the bubble chart 5 | function Circle({ data, onClick }) { 6 | const maxRadius = d3.max(data, (d) => d.count); 7 | const minRadius = d3.min(data, (d) => d.count); 8 | const radiusScale = d3.scaleSqrt().domain([minRadius, maxRadius]).range([20, 120]); 9 | 10 | //returns each circle with properties to render on bubble chart 11 | return data.map((circle, index) => ( 12 | 13 | { 22 | d3.select(".bubbleChartTooltip") 23 | .style("visibility", "visible") 24 | .text(circle.name + " (" + circle.count + ")") 25 | .attr("x", e.nativeEvent.offsetX + 10 + "px") 26 | .attr("y", e.nativeEvent.offsetY - 10 + "px"); 27 | }} 28 | onMouseOut={() => { 29 | d3.select(".bubbleChartTooltip").style("visibility", "hidden"); 30 | }} 31 | onClick={() => onClick(circle)} 32 | /> 33 | 42 | {circle.name + " (" + circle.count + ")"} 43 | 44 | 45 | )); 46 | } 47 | 48 | export default Circle; 49 | -------------------------------------------------------------------------------- /src/Chart/CircleChart.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect, useState } from "react"; 2 | import BubbleChart from "./BubbleChart"; 3 | 4 | //this is the parent element that renders the bubble chart. Circle -> BubbleChart -> CircleChart 5 | const CircleChart = (props) => { 6 | const { msMetrics, handleTogglePanel } = props; 7 | const [bubbleChartData, setBubbleChartData] = useState([]); 8 | //fetch metrics to render in chart 9 | useEffect(() => { 10 | if (!msMetrics || !Array.isArray(msMetrics)) return console.log("no metrics data"); 11 | 12 | function bubbleDataParse(metricsArr) { 13 | const bubbleArray = []; 14 | 15 | //parse metrics string from aws-sdk 16 | metricsArr.forEach((obj, i) => { 17 | const functionName = obj.Label.split(" ")[0]; 18 | const type = obj.Label.split(" ")[1]; 19 | const invocations = obj.Values.reduce((sum, value) => sum + value, 0); 20 | 21 | const result = { 22 | name: functionName, 23 | type: type, 24 | invocations: invocations, 25 | }; 26 | if (type === "Invocations") { 27 | bubbleArray.push(result); 28 | } 29 | }); 30 | return bubbleArray; 31 | } 32 | 33 | const bubbleData = bubbleDataParse(msMetrics); 34 | //parsing label and value. Data structure of d3 data: [{name: blah, count: blah}] 35 | const graphData = bubbleData.map((obj) => { 36 | return { name: obj.name, count: obj.invocations }; 37 | }); 38 | setBubbleChartData(graphData); 39 | }, []); 40 | 41 | const width = window.screen.width / 1; 42 | const height = window.screen.height / 1; 43 | //setting some styles here to overwrite predefined styles from d3 chart 44 | return ( 45 |
    49 | 50 |
    51 | ); 52 | }; 53 | 54 | export default CircleChart; 55 | -------------------------------------------------------------------------------- /src/Chart/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 34 | 35 |
    36 | 37 |
    38 | 39 | 40 | 154 | -------------------------------------------------------------------------------- /src/Chart/miserables.json: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": [ 3 | { "id": "4773bf7d1aa9b37b", "name": "GET /date" }, 4 | { "id": "db2894c84c643164", "name": "request handler - /date" }, 5 | { "id": "9a054dc4257fd82b", "name": "middleware - expressInit" }, 6 | { "id": "d297cf02aad4bc9b", "name": "middleware - query" }, 7 | { "id": "bc8a725770883100", "name": "HTTP POST" }, 8 | { "id": "6955e0a59650ee66", "name": "middleware - expressInit" }, 9 | { "id": "f7924394cc345ead", "name": "middleware - query" }, 10 | { "id": "f7924394cc345ead1", "name": "fakey" }, 11 | { "id": "f7924394cc345ead2", "name": "fakey2" }, 12 | { "id": "f7924394cc345ead3", "name": "fakey3" }, 13 | { "id": "f7924394cc345ead4", "name": "fakey4" }, 14 | { "id": "f7924394cc345ead5", "name": "fakey5" }, 15 | { "id": "f7924394cc345ead6", "name": "fakey6" }, 16 | { "id": "f7924394cc345ead7", "name": "fakey7" }, 17 | 18 | { "id": "f7924394cc345ead8", "name": "fakey8" }, 19 | { "id": "f7924394cc345ead9", "name": "fakey9" }, 20 | { "id": "f7924394cc345ead10", "name": "fakey10" } 21 | ], 22 | "links": [ 23 | { "source": "4773bf7d1aa9b37b", "target": "4773bf7d1aa9b37b" }, 24 | { "source": "db2894c84c643164", "target": "4773bf7d1aa9b37b" }, 25 | { "source": "9a054dc4257fd82b", "target": "4773bf7d1aa9b37b" }, 26 | { "source": "d297cf02aad4bc9b", "target": "4773bf7d1aa9b37b" }, 27 | { "source": "bc8a725770883100", "target": "bc8a725770883100" }, 28 | { "source": "6955e0a59650ee66", "target": "bc8a725770883100" }, 29 | { "source": "f7924394cc345ead", "target": "bc8a725770883100" }, 30 | 31 | { "source": "f7924394cc345ead1", "target": "bc8a725770883100" }, 32 | { "source": "f7924394cc345ead2", "target": "bc8a725770883100" }, 33 | { "source": "f7924394cc345ead3", "target": "bc8a725770883100" }, 34 | { "source": "f7924394cc345ead4", "target": "bc8a725770883100" }, 35 | { "source": "f7924394cc345ead5", "target": "bc8a725770883100" }, 36 | { "source": "f7924394cc345ead6", "target": "f7924394cc345ead5" }, 37 | { "source": "f7924394cc345ead7", "target": "f7924394cc345ead5" }, 38 | 39 | { "source": "f7924394cc345ead8", "target": "f7924394cc345ead7" }, 40 | { "source": "f7924394cc345ead9", "target": "f7924394cc345ead7" }, 41 | { "source": "f7924394cc345ead10", "target": "f7924394cc345ead7" } 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /src/Chart/service.json: -------------------------------------------------------------------------------- 1 | {"nodes":[{"id":"cakerFourFunction0","name":"client"},{"id":"cakerFourFunction1","name":"AWS::Lambda"},{"id":"cakerFourFunction2","name":"AWS::Lambda::Function"},{"id":"cakerThreeFunction0","name":"client"},{"id":"cakerThreeFunction1","name":"AWS::Lambda::Function"},{"id":"cakerThreeFunction2","name":"AWS::Lambda"},{"id":"cakerFirstFunction0","name":"AWS::Lambda::Function"},{"id":"cakerFirstFunction1","name":"client"},{"id":"cakerFirstFunction2","name":"AWS::Lambda"},{"id":"cakerFiveFunction0","name":"AWS::Lambda"},{"id":"cakerFiveFunction1","name":"client"},{"id":"cakerFiveFunction2","name":"AWS::Lambda::Function"}],"links":[{"source":"cakerFourFunction0","target":"cakerFourFunction0"},{"source":"cakerFourFunction0","target":"cakerFourFunction1"},{"source":"cakerFourFunction0","target":"cakerFourFunction2"},{"source":"cakerThreeFunction0","target":"cakerThreeFunction0"},{"source":"cakerThreeFunction0","target":"cakerThreeFunction1"},{"source":"cakerThreeFunction0","target":"cakerThreeFunction2"},{"source":"cakerFirstFunction0","target":"cakerFirstFunction0"},{"source":"cakerFirstFunction0","target":"cakerFirstFunction1"},{"source":"cakerFirstFunction0","target":"cakerFirstFunction2"},{"source":"cakerFiveFunction0","target":"cakerFiveFunction0"},{"source":"cakerFiveFunction0","target":"cakerFiveFunction1"},{"source":"cakerFiveFunction0","target":"cakerFiveFunction2"}]} -------------------------------------------------------------------------------- /src/assets/desktop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Lambdawg/50cc8c190f40b4e301944ce723c625ee7a08836b/src/assets/desktop.png -------------------------------------------------------------------------------- /src/assets/docs-gif-01.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Lambdawg/50cc8c190f40b4e301944ce723c625ee7a08836b/src/assets/docs-gif-01.gif -------------------------------------------------------------------------------- /src/assets/docs-gif-02.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Lambdawg/50cc8c190f40b4e301944ce723c625ee7a08836b/src/assets/docs-gif-02.gif -------------------------------------------------------------------------------- /src/assets/docs-gif-03.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Lambdawg/50cc8c190f40b4e301944ce723c625ee7a08836b/src/assets/docs-gif-03.gif -------------------------------------------------------------------------------- /src/assets/enteryourarn.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Lambdawg/50cc8c190f40b4e301944ce723c625ee7a08836b/src/assets/enteryourarn.gif -------------------------------------------------------------------------------- /src/assets/getyourstack.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Lambdawg/50cc8c190f40b4e301944ce723c625ee7a08836b/src/assets/getyourstack.gif -------------------------------------------------------------------------------- /src/assets/logo-text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Lambdawg/50cc8c190f40b4e301944ce723c625ee7a08836b/src/assets/logo-text.png -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Lambdawg/50cc8c190f40b4e301944ce723c625ee7a08836b/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/logowhite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Lambdawg/50cc8c190f40b4e301944ce723c625ee7a08836b/src/assets/logowhite.png -------------------------------------------------------------------------------- /src/assets/mascot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Lambdawg/50cc8c190f40b4e301944ce723c625ee7a08836b/src/assets/mascot.png -------------------------------------------------------------------------------- /src/assets/metrics.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Lambdawg/50cc8c190f40b4e301944ce723c625ee7a08836b/src/assets/metrics.gif -------------------------------------------------------------------------------- /src/assets/mobile-logs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Lambdawg/50cc8c190f40b4e301944ce723c625ee7a08836b/src/assets/mobile-logs.png -------------------------------------------------------------------------------- /src/assets/mobile-map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Lambdawg/50cc8c190f40b4e301944ce723c625ee7a08836b/src/assets/mobile-map.png -------------------------------------------------------------------------------- /src/assets/mobile-metrics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Lambdawg/50cc8c190f40b4e301944ce723c625ee7a08836b/src/assets/mobile-metrics.png -------------------------------------------------------------------------------- /src/assets/readme-bar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Lambdawg/50cc8c190f40b4e301944ce723c625ee7a08836b/src/assets/readme-bar.gif -------------------------------------------------------------------------------- /src/assets/readme-bubble.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Lambdawg/50cc8c190f40b4e301944ce723c625ee7a08836b/src/assets/readme-bubble.gif -------------------------------------------------------------------------------- /src/assets/readme-node.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Lambdawg/50cc8c190f40b4e301944ce723c625ee7a08836b/src/assets/readme-node.gif -------------------------------------------------------------------------------- /src/assets/signup.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Lambdawg/50cc8c190f40b4e301944ce723c625ee7a08836b/src/assets/signup.gif -------------------------------------------------------------------------------- /src/assets/signyouup.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/Lambdawg/50cc8c190f40b4e301944ce723c625ee7a08836b/src/assets/signyouup.gif -------------------------------------------------------------------------------- /src/components/Animation.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | const mascot = "src/assets/mascot_head.svg"; 3 | 4 | const BouncingDotsLoader = (props) => { 5 | return ( 6 |
    7 |
    8 | loading mascot{" "} 9 |
    10 |
    11 | loading mascot{" "} 12 |
    13 |
    14 | loading mascot{" "} 15 |
    16 |
    17 | ); 18 | }; 19 | 20 | export default BouncingDotsLoader; 21 | -------------------------------------------------------------------------------- /src/components/Auth.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import SignUpForm from './SignUpForm.jsx'; 3 | import SignInForm from './SignInForm.jsx'; 4 | import { useNavigate } from 'react-router-dom'; 5 | 6 | const Auth = (props) => { 7 | const mascot = 'src/assets/logo.png' 8 | const [formType, setFormType] = useState("signIn"); 9 | const { loggedIn, setLoggedIn, user, setUser } = props 10 | const navigate = useNavigate(); 11 | 12 | 13 | const toggleFormType = () => { 14 | setFormType(prevFormType => prevFormType === 'signIn' ? 'signUp' : 'signIn'); 15 | }; 16 | 17 | const handleSignUpSuccess = () => { 18 | setLoggedIn(true); 19 | navigate('/settings'); 20 | } 21 | 22 | return( 23 |
    24 | 25 |
    26 | { 27 | formType === 'signIn' ? 28 | : 35 | 43 | } 44 |
    45 |
    46 | ) 47 | } 48 | 49 | export default Auth; -------------------------------------------------------------------------------- /src/components/ChartDropDown.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { RiBarChartHorizontalFill } from "react-icons/ri"; 3 | import { GiBubbles } from "react-icons/gi"; 4 | import { GrNodes } from "react-icons/gr"; 5 | 6 | //renders each chart icon in the dropdown 7 | function DropdownItem(props) { 8 | const { onClick, chartIcons } = props; 9 | return ( 10 |
    11 | {chartIcons} 12 |
    13 | ); 14 | } 15 | //renders the dropdown menu 16 | function Dropdown(props) { 17 | const { setActiveChart } = props; 18 | const [selectedChart, setSelectedChart] = useState(null); 19 | const [dropIsOpen, setDropIsOpen] = useState(false); 20 | 21 | //options and corresponding icons 22 | const options = ["Bar", "Bubble", "Node"]; 23 | const chartIcons = [, , ]; 24 | 25 | //icons don't render properly in JSX, so using a func to render icon based on selectedChart 26 | const showIcon = (optionsArr, iconsArr, option) => { 27 | return chartIcons[optionsArr.indexOf(option)]; 28 | }; 29 | 30 | //sets SelectedChart with the corresponding chart option for the icon clicked. setActiveChart to send it to DiagramContainer for chart render 31 | const handleOptionClick = (chart) => { 32 | console.log("you clicked " + chart); 33 | setSelectedChart(chart); 34 | setActiveChart(chart); 35 | setDropIsOpen(false); 36 | }; 37 | 38 | return ( 39 |
    40 |
    setDropIsOpen(!dropIsOpen)}> 41 | {selectedChart ? showIcon(options, chartIcons, selectedChart) : "View"} 42 |
    43 |
    44 | {options.map((chart, i) => ( 45 | handleOptionClick(chart)} 50 | /> 51 | ))} 52 |
    53 |
    54 | ); 55 | } 56 | export default Dropdown; 57 | -------------------------------------------------------------------------------- /src/components/DataWindow.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import Log from './Log.jsx'; 3 | import { Scrollbar } from 'react-scrollbars-custom'; 4 | import BouncingDotsLoader from "../components/Animation"; 5 | 6 | const DataWindow = (props) => { 7 | const { dataWindowFullScreen, msLogs } = props; 8 | const [logData, setLogData] = useState({}); 9 | 10 | // Parse and sort logs 11 | useEffect(() => { 12 | if (Array.isArray(msLogs)) { 13 | const parsedLogs = {}; 14 | 15 | msLogs.forEach((log) => { 16 | if (!parsedLogs[log.FunctionName]) { 17 | parsedLogs[log.FunctionName] = []; 18 | } 19 | 20 | log.logs.forEach((stamp) => { 21 | parsedLogs[log.FunctionName].push({ 22 | timestamp: stamp.timestamp, 23 | message: stamp.message, 24 | }); 25 | }); 26 | }); 27 | setLogData(parsedLogs); 28 | } 29 | }, [msLogs]); 30 | 31 | return ( 32 |
    36 | 37 | 38 | {Object.keys(logData).length > 0 ? ( 39 | Object.entries(logData).map(([functionName, logs]) => ( 40 | 41 | )) 42 | ) : 43 |
    44 |

    Loading...

    45 | 46 |
    47 | } 48 |
    49 |
    50 |
    51 | ); 52 | 53 | }; 54 | 55 | export default DataWindow; 56 | 57 | -------------------------------------------------------------------------------- /src/components/Docs.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import logo from '../assets/logo.png'; 3 | import setup1 from '../assets/docs-gif-01.gif'; 4 | import setup2 from '../assets/docs-gif-02.gif'; 5 | import setup3 from '../assets/docs-gif-03.gif'; 6 | import signup from '../assets/signyouup.gif'; 7 | import arn from '../assets/enteryourarn.gif'; 8 | import stack from '../assets/getyourstack.gif'; 9 | 10 | 11 | 12 | const Docs = () => { 13 | return ( 14 |
    15 | 16 | 17 |

    18 | Lambdawg is your go-to AWS Lambda Function visualization UI. 19 |

    20 |

    21 | Simply connect your AWS account in the Settings page above, and boom 22 | boom bam - you'll get your trace, metrics, and log data in one simple, 23 | streamlined interface. 24 |

    25 | 26 |

    Set up & Installation

    27 | 28 |
    29 |
    30 |

    Connect to your AWS account

    31 | Connect your AWS account 32 |
    33 | 34 |
    35 |

    Copy and paste your ARN key

    36 | Locate your ARN key 37 |
    38 | 39 |
    40 |

    Select your region

    41 | Select your region 42 |
    43 | 44 |
    45 | 46 |

    47 | 48 | Check us out on{' '} 49 | Github! 50 | 51 |

    52 | 53 |

    54 | And if you're a developer...{' '} 55 | 56 | click here to 57 | learn more about Lambdawg! 58 | 59 |

    60 |
    61 | ); 62 | }; 63 | 64 | export default Docs; 65 | -------------------------------------------------------------------------------- /src/components/LamdaButton.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React, { useState, useEffect, useRef } from 'react'; 3 | 4 | const LamdaButton = (props) => { 5 | const { name, msMetrics, user, msLogs, setMsLogs } = props; 6 | const [sortedMetrics, setSortedMetrics] = useState({}); 7 | const [isPanelOpen, setIsPanelOpen] = useState(false); 8 | 9 | // Parse metrics for specific func name 10 | useEffect(() => { 11 | 12 | if (!msMetrics || !Array.isArray(msMetrics)) return; 13 | 14 | const tempSortedMetrics = {}; 15 | let invocationsSum = 0; 16 | let errorsSum = 0; 17 | 18 | msMetrics.forEach((metricsObj) => { 19 | const microName = metricsObj.Label.split(' ')[0]; 20 | const label = metricsObj.Label.split(' ')[1]; 21 | 22 | const timeStamps = []; 23 | 24 | if (label === 'Invocations') { 25 | invocationsSum = metricsObj.Values.reduce((a, b) => {return a + b} ,0); 26 | } else if (label === 'Errors') { 27 | metricsObj.Values.forEach((num, i) => { 28 | errorsSum += num; 29 | if (num > 0) { 30 | timeStamps.push( 31 | {metricsObj.Timestamps[i]} 32 | ); 33 | 34 | } else { 35 | timeStamps.push( 36 | {metricsObj.Timestamps[i]} 37 | ); 38 | } 39 | }); 40 | 41 | tempSortedMetrics[microName] = { 42 | invocationsSum, 43 | errorsSum, 44 | timeStamps 45 | }; 46 | } 47 | }); 48 | 49 | setSortedMetrics(tempSortedMetrics); 50 | }, [msMetrics]); 51 | 52 | // Fetch all Logs 53 | useEffect(() => { 54 | if (msMetrics) { 55 | 56 | const fetchLogs = async () => { 57 | try { 58 | const response = await fetch('/aws/getLambdaLogs', { 59 | method: 'POST', 60 | headers: { 'Content-Type': 'application/json' }, 61 | body: JSON.stringify({ 62 | arn: user.arn, 63 | user_name: user.user_name 64 | }), 65 | muteHttpExceptions: true, 66 | }); 67 | const data = await response.json(); 68 | setMsLogs(data); 69 | } catch (error) { 70 | console.log('error fetching logs', error); 71 | } 72 | }; 73 | fetchLogs(); 74 | } 75 | }, [sortedMetrics]); 76 | 77 | // Populate logs in data window 78 | const togglePanel = () => { 79 | setIsPanelOpen(!isPanelOpen); 80 | 81 | const logElement = document.getElementById(`${name}log`); 82 | if (logElement) { 83 | logElement.scrollIntoView(); 84 | } 85 | } 86 | 87 | return ( 88 |
    89 | {sortedMetrics[name] && ( 90 |
    91 | 98 | 99 |
    103 |
      104 |
    • Invocations: {sortedMetrics[name].invocationsSum}
    • 105 |
    • Errors: {sortedMetrics[name].errorsSum}
    • 106 |
    • TimeStamps: {sortedMetrics[name].timeStamps}
    • 107 |
    108 |
    109 |
    110 | )} 111 |
    112 | ); 113 | 114 | } 115 | 116 | export default LamdaButton; 117 | -------------------------------------------------------------------------------- /src/components/Log.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | 4 | const Log = (props) => { 5 | const { logs, functionName } = props; 6 | 7 | return ( 8 |
    9 |

    {functionName}

    10 |
      11 | {logs.map((log, index) => ( 12 |
    • 13 |
      14 | {log.timestamp}:
      15 | {log.message} 16 |
      17 |
    • 18 | ))} 19 |
    20 |
    21 | ); 22 | } 23 | 24 | export default Log; 25 | -------------------------------------------------------------------------------- /src/components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { useNavigate } from 'react-router-dom'; 4 | import Switch from '@mui/material/Switch'; 5 | 6 | const Navbar = (props) => { 7 | const { loggedIn, setLoggedIn, user } = props; 8 | const [navChecked, setNavChecked] = useState(false); 9 | 10 | const docsLink = '/docs'; 11 | const dashLink = '/dashboard'; 12 | const settingsLink = './settings'; 13 | const authLink = './auth'; 14 | const homeLink = '/'; 15 | const mascot = 'src/assets/logowhite.png'; 16 | const logoText = 'src/assets/logo-text.png'; 17 | 18 | const navigate = useNavigate(); 19 | const handleLinkClick = () => { 20 | setNavChecked(false); 21 | }; 22 | 23 | // create a tracker for dark mode in state 24 | const [mode, setMode] = useState('light'); 25 | // If state is 'dark' transition to 'light' and vice-versa 26 | //the function creates a progressive transition to dark or light 27 | const darkMode = (mode = 'dark') => { 28 | let transition; 29 | mode === 'dark' ? (transition = 1) : (transition = 0); 30 | const htmlElement = document.querySelector('html'); 31 | 32 | if (!htmlElement) { 33 | console.error('Unable to find HTML element.'); 34 | return; 35 | } 36 | 37 | const applyFilter = () => { 38 | htmlElement.style.filter = `invert(${transition})`; 39 | mode === 'dark' 40 | ? (() => { 41 | if (transition >= 0) { 42 | transition -= 0.1; 43 | setTimeout(applyFilter, 30); 44 | } 45 | })() 46 | : (() => { 47 | if (transition <= 1) { 48 | transition += 0.1; 49 | setTimeout(applyFilter, 30); 50 | } 51 | })(); 52 | }; 53 | 54 | setTimeout(applyFilter, 30); 55 | }; 56 | 57 | //handle state and triggest transition on click 58 | const darkClick = () => { 59 | setMode(mode === 'dark' ? 'light' : 'dark'); 60 | darkMode(mode); 61 | }; 62 | 63 | const handleLinkClickAuth = async () => { 64 | if (loggedIn) { 65 | try { 66 | const response = await fetch('/api/logout', { 67 | credentials: 'include', 68 | }); 69 | if (response.ok) { 70 | setLoggedIn(false); 71 | navigate('/auth'); 72 | } 73 | else console.log('logout failed'); 74 | } 75 | catch (error) { 76 | console.log('catch block in logout'); 77 | } 78 | } 79 | setNavChecked(false); 80 | }; 81 | 82 | const label = { inputProps: { 'aria-label': 'Switch demo' } }; 83 | 84 | return ( 85 |
    86 | setNavChecked(!navChecked)} 91 | /> 92 |
    93 |
    94 |
    95 | 96 | 97 | 98 | 99 |
    100 |
    101 |
    102 |
    103 | 108 |
    109 | 110 |
    111 |
    112 | 113 | 116 | 117 | 118 |
    119 | 120 | Documentation 121 | 122 | {user.arn && ( 123 | 124 | Dashboard 125 | 126 | )} 127 | 128 | Settings 129 | 130 | 131 | {loggedIn ? 'Log out' : 'Log in'} 132 | 133 |
    134 |
    135 | ); 136 | }; 137 | 138 | export default Navbar; 139 | -------------------------------------------------------------------------------- /src/components/Panel.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useMemo } from 'react'; 2 | import LamdaButton from '../components/LamdaButton.jsx'; 3 | import Refresh from '../components/Refresh.jsx'; 4 | 5 | const Panel = (props) => { 6 | 7 | const { panelFullScreen, msNames, setMsNames, msMetrics, user, setUser, msLogs, setMsLogs, refreshRedis, setRefreshRedis } = props; 8 | const [names, setNames] = useState([]); 9 | 10 | // Populate metrics buttons 11 | useEffect(()=>{ 12 | if (msNames && msMetrics){ 13 | const namesArr = [] 14 | let i = 0; 15 | 16 | msNames.forEach((name) => { 17 | i++; 18 | namesArr.push( 19 | 28 | ) 29 | }) 30 | setNames(namesArr) 31 | } 32 | }, [msMetrics, msNames]) 33 | 34 | 35 | return( 36 |
    41 | 42 | 50 |
    51 | 52 | {(names.length > 0)? 53 | names : 54 |

    Loading...

    } 55 | 56 |
    57 | 58 | ) 59 | } 60 | export default Panel; 61 | 62 | -------------------------------------------------------------------------------- /src/components/Refresh.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | 4 | const Refresh = (props) => { 5 | const { setRefreshRedis, user } = props; 6 | const navigate = useNavigate() 7 | 8 | const refreshMetrics = async () => { 9 | setRefreshRedis(false); 10 | try{ 11 | const response = await fetch('/aws/deleteRedis', { 12 | method: 'DELETE', 13 | header: {'content-type': 'application/json'}, 14 | body: JSON.stringify({ 15 | user_name: user.user_name 16 | }), 17 | muteHttpExceptions: true 18 | }) 19 | if (response.ok){ 20 | setRefreshRedis(true); 21 | } 22 | else { 23 | console.log('Unable to refresh cached data') 24 | } 25 | } 26 | catch(error){ 27 | console.log('error reaching redis for refesh: ', error) 28 | } 29 | } 30 | 31 | return( 32 | 38 | ) 39 | } 40 | 41 | export default Refresh; -------------------------------------------------------------------------------- /src/components/SettingsForm.jsx: -------------------------------------------------------------------------------- 1 | 2 | import React, { useState } from 'react'; 3 | import { Link, useNavigate } from "react-router-dom"; 4 | 5 | const Settings = (props) => { 6 | const { user, setUser } = props; 7 | const [formData, setFormData] = useState({ 8 | password_: '', 9 | arn: '', 10 | region: '', 11 | }); 12 | 13 | const navigate = useNavigate(); 14 | 15 | const handleInputChange = (event) => { 16 | const { name, value } = event.target; 17 | setFormData((prevFormData) => ({ ...prevFormData, [name]: value })); 18 | }; 19 | 20 | const handleInvalidArn = () => { 21 | console.log('not a valid arn') 22 | const arnInput = document.getElementById('arnInputField'); 23 | arnInput.style.border = '2px solid red'; 24 | } 25 | 26 | const handleAuthFail = () => { 27 | const pwInput = document.getElementById('arnPasswordField'); 28 | pwInput.style.border = '2px solid red'; 29 | setFormData((prevFormData) => ({ ...prevFormData, password_: '' })); 30 | } 31 | 32 | // will update user in db and state 33 | const handleSubmit = async (event) => { 34 | event.preventDefault(); 35 | if (formData.arn.substring(0, 12) !== 'arn:aws:iam:') return handleInvalidArn(); 36 | 37 | const arnFormData = { 38 | full_name: user.full_name, 39 | user_name: user.user_name, 40 | email: user.email, 41 | password_: formData.password_, 42 | _id: user._id, 43 | arn: formData.arn, 44 | region: formData.aws_region, 45 | }; 46 | 47 | try{ 48 | const response = await fetch(`/api/edit/${user.user_name}`, { 49 | method: 'PATCH', 50 | credentials: 'include', 51 | headers: {'content-type': 'application/json'}, 52 | body: JSON.stringify([arnFormData]) 53 | }) 54 | 55 | if (response.ok){ 56 | const data = await response.json(); 57 | navigate('/dashboard'); 58 | } 59 | else { 60 | handleAuthFail(); 61 | console.log('Unable to patch arn from settings') 62 | } 63 | 64 | } 65 | catch(error){ 66 | console.log('Something went wrong in settings patch req') 67 | 68 | } 69 | // setUser({ user_name: user.user_name, arn: formData.arn, region: formData.aws_region }) 70 | setUser((prevUser) => ({ ...prevUser, arn: formData.arn, region: formData.aws_region })); 71 | } 72 | 73 | return( 74 |
    75 |

    76 | Step 1: 77 | 80 | 83 | Connect your AWS account 84 |

    85 |
    86 | Step 2: Paste your ARN key below 87 |
    88 |

    89 |
    90 |
    91 | 100 | 101 |

    Step 3. Select your region

    102 |
    120 | 130 |
    131 |
    132 | 133 | 138 | 139 | 140 | 146 | 147 |
    148 | 149 | 150 | ) 151 | 152 | } 153 | export default Settings; 154 | 155 | 156 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /src/components/SignInForm.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | 3 | const SignInForm = (props) => { 4 | const [formData, setFormData] = useState({ 5 | user_name: '', 6 | password_: '' 7 | }); 8 | const { setLoggedIn, user, setUser } = props; 9 | 10 | const handleInputChange = (event) => { 11 | const { name, value } = event.target; 12 | setFormData((prevFormData) => ({ ...prevFormData, [name]: value })); 13 | }; 14 | 15 | // will add user data to state 16 | const handleSubmit = async (event) => { 17 | event.preventDefault(); 18 | const signInFormData = { 19 | user_name: formData.user_name, 20 | password_: formData.password_, 21 | }; 22 | 23 | try { 24 | const response = await fetch(`/api/${formData.user_name}`, { 25 | method: 'POST', 26 | credentials: 'include', 27 | headers: { 'content-type': 'application/json' }, 28 | body: JSON.stringify([signInFormData]), 29 | }); 30 | 31 | if (response.ok) { 32 | const data = await response.json(); 33 | const { user_name, full_name, email, _id, arn, region } = data 34 | setUser({ 35 | full_name: full_name, 36 | user_name: user_name, 37 | email: email, 38 | _id: _id, 39 | arn: arn, 40 | region: region 41 | }) 42 | setLoggedIn(true); 43 | } 44 | else { 45 | console.log('Invalid password'); 46 | const passwordInput = document.getElementById('password_'); 47 | passwordInput.style.border = '2px solid red'; 48 | passwordInput.value = ''; 49 | } 50 | } 51 | catch (error) { 52 | console.error('error signing in: ', error); 53 | } 54 | }; 55 | 56 | useEffect(() => { 57 | console.log('user has been updated in state') 58 | }, [user]) 59 | 60 | return ( 61 |
    62 |

    SIGN IN

    63 | 64 |
    65 | 75 | 76 | 87 | 88 |
    89 |
    90 | 95 | 100 |
    101 |
    102 | 103 | {/* */} 104 |
    105 |
    106 | ); 107 | }; 108 | 109 | export default SignInForm; 110 | -------------------------------------------------------------------------------- /src/components/SignUpForm.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | const signUpForm = (props) => { 4 | 5 | const [formData, setFormData] = useState({ 6 | full_name: '', 7 | user_name: '', 8 | email: '', 9 | password_: '', 10 | confirmPassword: '' 11 | }); 12 | const { toggleFormType, setUser, onSignUpSuccess } = props 13 | 14 | const handleInputChange = (event) => { 15 | const { name, value } = event.target; 16 | setFormData((prevFormData) => ({ ...prevFormData, [name]: value })); 17 | }; 18 | 19 | const handleSubmit = async (event) => { 20 | event.preventDefault(); 21 | const signUpFormData = { 22 | full_name: formData.full_name, 23 | user_name: formData.user_name, 24 | email: formData.email, 25 | password_: formData.password_ 26 | } 27 | if (formData.password_ != formData.confirmPassword) return handleMismatchedPasswords(); 28 | if (formData.password_ == formData.confirmPassword) console.log('signing up....') 29 | 30 | try { 31 | const response = await fetch('/api/newUser', { 32 | method: 'POST', 33 | headers: { 'content-type': 'application/json' }, 34 | body: JSON.stringify([signUpFormData]), 35 | }); 36 | 37 | if (response.ok) { 38 | const data = await response.json(); 39 | const { user_name, full_name, email, _id } = data 40 | console.log(data, 'from signup') 41 | setUser({ 42 | full_name: full_name, 43 | user_name: user_name, 44 | email: email, 45 | _id: _id 46 | }) 47 | onSignUpSuccess(); 48 | } 49 | else { 50 | console.log('Sign up failed'); 51 | } 52 | } 53 | catch (error) { 54 | console.error(error); 55 | console.log('Unable to sign-up at this time: '); 56 | } 57 | }; 58 | 59 | const handleMismatchedPasswords = () => { 60 | console.log('passwords do not match') 61 | const passwordInput = document.getElementById('confirmPassword'); 62 | passwordInput.style.border = '2px solid red'; 63 | }; 64 | 65 | 66 | 67 | return( 68 | 69 |
    70 |

    SIGN UP

    71 | 72 |
    73 | 77 | 81 | 85 | 89 | 93 |
    94 |
    95 | 96 | 97 |
    98 |
    99 |
    100 | 101 | ) 102 | } 103 | 104 | 105 | export default signUpForm; 106 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /src/components/footer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const Footer = () => { 4 | return ( 5 |
    6 | Lambdawg was developped for OSLabs by Chanda Gonet, Erica Park, Ted Gusek, 7 | and Vincent Jacquemin 8 |
    9 | ); 10 | }; 11 | 12 | export default Footer; 13 | -------------------------------------------------------------------------------- /src/containers/DashboardContainer.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import Panel from '../components/Panel.jsx'; 3 | import DiagramContainer from '../containers/DiagramContainer.jsx'; 4 | import DataWindow from '../components/DataWindow.jsx'; 5 | 6 | const DashboardContainer = (props) => { 7 | const { user, setUser } = props; 8 | const [panelFullScreen, setPanelFullScreen] = useState(false); 9 | const [diagramFullScreen, setDiagramFullScreen] = useState(true); 10 | const [dataWindowFullScreen, setDataWindowFullScreen] = useState(false); 11 | const [msNames, setMsNames] = useState([]); 12 | const [msMetrics, setMsMetrics] = useState({}); 13 | const [msLogs, setMsLogs] = useState({}); 14 | const [msTraces, setMsTraces] = useState([]); 15 | const [msServiceIds, setMsServiceIds] = useState([]); 16 | const [refreshRedis, setRefreshRedis] = useState(false); 17 | const [activePanel, setActivePanel] = useState(""); 18 | 19 | const handleTogglePanel = (panelName) => { 20 | //panelName is the name of the bubble that was clicked. Passed back up from bubble chart 21 | setActivePanel(panelName); 22 | //now we can use the panelName id to select the corresponding button in the panel 23 | 24 | if (panelName) { 25 | const button = document.getElementById(panelName); 26 | button.click(); 27 | } 28 | }; 29 | 30 | useEffect(() => { 31 | console.log('listening in dashboard'); 32 | }, [user]); 33 | 34 | /// toggle full screen for mobile Panel 35 | const handlePanelClick = () => { 36 | if (panelFullScreen) { 37 | return; 38 | } 39 | setPanelFullScreen(!panelFullScreen); 40 | setDiagramFullScreen(false); 41 | setDataWindowFullScreen(false); 42 | document 43 | .getElementById('panelButton') 44 | .classList.add('current-window-button'); 45 | document 46 | .getElementById('dataButton') 47 | .classList.remove('current-window-button'); 48 | }; 49 | 50 | // toggle full screen for mobile charts 51 | const handleDiagramClick = () => { 52 | if (diagramFullScreen) { 53 | return; 54 | } 55 | setPanelFullScreen(false); 56 | setDiagramFullScreen(!diagramFullScreen); 57 | setDataWindowFullScreen(false); 58 | document 59 | .getElementById('panelButton') 60 | .classList.remove('current-window-button'); 61 | document 62 | .getElementById('dataButton') 63 | .classList.remove('current-window-button'); 64 | }; 65 | 66 | // toggle full screen for mobile logs 67 | const handleDataClick = () => { 68 | if (dataWindowFullScreen) { 69 | return; 70 | } 71 | setPanelFullScreen(false); 72 | setDiagramFullScreen(false); 73 | setDataWindowFullScreen(!dataWindowFullScreen); 74 | document 75 | .getElementById('dataButton') 76 | .classList.add('current-window-button'); 77 | document 78 | .getElementById('panelButton') 79 | .classList.remove('current-window-button'); 80 | }; 81 | 82 | // fetch names 83 | useEffect(() => { 84 | if(user){ 85 | const fetchNames = async() => { 86 | try{ 87 | const response = await fetch('/aws/getLambdaNames', { 88 | method: 'POST', 89 | headers: {'Content-Type': 'application/json'}, 90 | body: JSON.stringify({ 91 | arn: user.arn, 92 | user_name: user.user_name 93 | }), 94 | muteHttpExceptions: true 95 | }); 96 | if (response.ok){ 97 | const data = await response.json() 98 | setMsNames(data); 99 | } 100 | else { 101 | alert('Please confirm correct ARN and region in settings') 102 | } 103 | } 104 | catch(error){ 105 | } 106 | }; 107 | fetchNames(); 108 | } 109 | }, [user, refreshRedis]) 110 | 111 | // fetch metrics 112 | useEffect(() => { 113 | if (msNames) { 114 | const fetchMetrics = async () => { 115 | try { 116 | const response = await fetch('/aws/getLambdaMetrics', { 117 | method: 'POST', 118 | headers: { 'Content-Type': 'application/json' }, 119 | body: JSON.stringify({ 120 | arn: user.arn, 121 | user_name: user.user_name 122 | }), 123 | muteHttpExceptions: true, 124 | }); 125 | const data = await response.json(); 126 | setMsMetrics(data.MetricDataResults); 127 | } 128 | catch (error) { 129 | console.log('error fetching metrics', error); 130 | } 131 | }; 132 | fetchMetrics(); 133 | } 134 | }, [msNames]); 135 | 136 | //fetch trace data 137 | useEffect(() => { 138 | //we need names to fetch traces also 139 | const fetchTraces = async () => { 140 | try { 141 | const response = await fetch('/aws/getTraces', { 142 | method: 'POST', 143 | headers: { 'Content-Type': 'application/json' }, 144 | body: JSON.stringify({ 145 | arn: user.arn, 146 | }), 147 | muteHttpExceptions: true, 148 | }); 149 | const data = await response.json(); 150 | 151 | //parsing name, duration, responseTime, service ids 152 | const serviceData = []; 153 | const traceData = await data.map((obj) => { 154 | if (!obj.summary.length) { 155 | return { 156 | name: obj.name, 157 | duration: undefined, 158 | responseTime: undefined, 159 | serviceIds: undefined, 160 | }; 161 | } else { 162 | //create a separate serviceData array to pass as msServiceIds 163 | serviceData.push({ 164 | name: obj.name, 165 | serviceIds: obj.summary[0].ServiceIds, 166 | }); 167 | return { 168 | name: obj.name, 169 | duration: obj.summary[0].Duration, 170 | responseTime: obj.summary[0].ResponseTime, 171 | serviceIds: obj.summary[0].ServiceIds, 172 | }; 173 | } 174 | }); 175 | //trying to parse serviceIdData all at once 176 | setMsTraces(traceData); 177 | setMsServiceIds(serviceData); 178 | 179 | } catch (error) { 180 | console.log('error fetching traces', error); 181 | } 182 | }; 183 | fetchTraces(); 184 | }, []); 185 | 186 | return ( 187 |
    188 |
    189 | 202 | 211 |
    212 | 213 | 219 | 220 |
    221 | 228 | 235 | 236 |
    237 |
    238 | ); 239 | }; 240 | 241 | export default DashboardContainer; 242 | -------------------------------------------------------------------------------- /src/containers/DiagramContainer.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import HorizontalBarChart from "../Chart/BarChart"; 3 | import CircleChart from "../Chart/CircleChart"; 4 | import Dropdown from "../components/ChartDropDown"; 5 | 6 | const DiagramContainer = (props) => { 7 | const { diagramFullScreen } = props; 8 | const { msNames, msMetrics, msTraces, msServiceIds, handleTogglePanel } = props; 9 | const [activeChart, setActiveChart] = useState("Node"); 10 | 11 | //writing serviceIds to the JSON file so the D3 node chart can read it 12 | useEffect(() => { 13 | if (msServiceIds.length) { 14 | const writeToFile = async (services) => { 15 | console.log(msServiceIds, 'msids') 16 | try { 17 | const response = await fetch("/aws/writeToFile", { 18 | method: "POST", 19 | headers: { "Content-Type": "application/json" }, 20 | body: JSON.stringify({ 21 | serviceIds: msServiceIds, 22 | }), 23 | muteHttpExceptions: true, 24 | }); 25 | if (response.ok) { 26 | const data = await response.json(); 27 | } 28 | } catch (error) { 29 | console.log(error, "error posting service data"); 30 | } 31 | }; 32 | writeToFile(msServiceIds); 33 | } 34 | }, []); 35 | 36 | //When a node is clicked in the iframe, the message will be sent back 37 | window.addEventListener("message", async (event) => { 38 | if (event.origin !== "http://localhost:5173") return; 39 | 40 | const serviceNodeId = event.data; 41 | if (!serviceNodeId.id) return; 42 | 43 | let nodeId = serviceNodeId.id; 44 | 45 | nodeId = nodeId.slice(0, nodeId.length - 1); 46 | let button = await document.getElementById(nodeId); 47 | if (button) { 48 | button.click(); 49 | } else { 50 | console.log("was the button clicked? noooo"); 51 | } 52 | }); 53 | 54 | // active chart updates from dropdown, and renders corresponding chart 55 | return ( 56 |
    57 | {activeChart === "Bar" && } 58 | {activeChart === "Bubble" && } 59 | {activeChart === "Node" &&