├── .dockerignore ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── pull_request_template.md └── workflows │ └── aws.yml ├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── Dockerfile ├── LICENSE ├── README.md ├── client ├── __tests__ │ ├── LoginButton.test.tsx │ └── Navbar.test.tsx ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── App.tsx │ ├── app │ │ ├── hooks.ts │ │ ├── rootReducer.ts │ │ ├── store.ts │ │ └── types.ts │ ├── assets │ │ ├── EC2.ts │ │ ├── LoginBackground.ts │ │ ├── Moon.ts │ │ ├── SkyScraperLogo.ts │ │ └── images │ │ │ └── Nodejs.png │ ├── custom.d.ts │ ├── features │ │ ├── auth │ │ │ ├── authAPI.ts │ │ │ ├── authSlice.ts │ │ │ ├── components │ │ │ │ ├── LoginButton.tsx │ │ │ │ └── LogoutButton.tsx │ │ │ └── dropDownSlice.ts │ │ ├── dashboard │ │ │ └── DashboardPage.tsx │ │ ├── ec2Monitor │ │ │ ├── EC2MonitorPage.tsx │ │ │ └── components │ │ │ │ └── Charts.tsx │ │ ├── homepage │ │ │ └── HomePage.tsx │ │ ├── navbar │ │ │ └── Navbar.tsx │ │ └── themes │ │ │ └── themeSlice.ts │ ├── index.tsx │ └── styles │ │ ├── HomePage.css │ │ ├── LoginPage.css │ │ ├── Nav.css │ │ ├── UserIcon.css │ │ └── styles.css ├── tsconfig.json └── webpack.config.js ├── ecs-task-def.json ├── eslint.config.js ├── images ├── Auth0.png ├── Chartjs.png ├── CircleLogo.png ├── CloudWatch.png ├── Cognito.png ├── EC2.png ├── Express.png ├── FlatLogo.png ├── GitHubBlack.png ├── GitHubWhite.png ├── LinkedIn.png ├── Mail.png ├── Nodejs.png ├── React.png ├── Redux.png ├── TS.png ├── Webpack.png ├── XBlack.png └── XWhite.png ├── jest.config.ts ├── package-lock.json ├── package.json ├── server ├── src │ ├── controllers │ │ ├── authController.ts │ │ ├── cloudController.ts │ │ └── ec2Controller.ts │ ├── routers │ │ └── router.ts │ ├── server.ts │ └── utils │ │ ├── ErrorHandler.ts │ │ ├── ErrorObject.ts │ │ └── types.ts └── tsconfig.json └── template.env /.dockerignore: -------------------------------------------------------------------------------- 1 | .env -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | --- 11 | name: Bug report 12 | about: Create a report to help us improve 13 | title: '' 14 | labels: '' 15 | assignees: '' 16 | 17 | --- 18 | 19 | 20 | **Describe the bug** 21 | A clear and concise description of what the bug is. 22 | 23 | **To Reproduce** 24 | Steps to reproduce the behavior: 25 | 1. Go to '...' 26 | 2. Click on '....' 27 | 3. Scroll down to '....' 28 | 4. See error 29 | 30 | **Expected behavior** 31 | A clear and concise description of what you expected to happen. 32 | 33 | **Screenshots** 34 | If applicable, add screenshots to help explain your problem. 35 | 36 | **Desktop (please complete the following information):** 37 | - OS: [e.g. iOS] 38 | - Browser [e.g. chrome, safari] 39 | - Version [e.g. 22] 40 | 41 | **Smartphone (please complete the following information):** 42 | - Device: [e.g. iPhone6] 43 | - OS: [e.g. iOS8.1] 44 | - Browser [e.g. stock browser, safari] 45 | - Version [e.g. 22] 46 | 47 | **Additional context** 48 | Add any other context about the problem here. 49 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | --- 11 | name: Feature request 12 | about: Suggest an idea for this project 13 | title: '' 14 | labels: '' 15 | assignees: '' 16 | 17 | --- 18 | 19 | **Is your feature request related to a problem? Please describe.** 20 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 21 | 22 | **Describe the solution you'd like** 23 | A clear and concise description of what you want to happen. 24 | 25 | **Describe alternatives you've considered** 26 | A clear and concise description of any alternative solutions or features you've considered. 27 | 28 | **Additional context** 29 | Add any other context or screenshots about the feature request here. 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/pull_request_template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: pull_request_template 3 | about: Describe this issue template's purpose here. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ### Thank you for contributing! Before you make your PR: 11 | - [ ] Prefix your PR title with [chart], [feat], [fix], or [other] 12 | - [ ] If you're submitting a chart, explain exactly what data it uses and include tests as appropriate. 13 | - [ ] If you're submitting a feature, explain exactly what it does and include tests as appropriate. 14 | - [ ] If you're submitting a bug fix, include a test that fails without your PR but passes with it. 15 | 16 | ### Tests 17 | - [ ] Run your tests with `npm test` and lint with `npm run lint` 18 | -------------------------------------------------------------------------------- /.github/workflows/aws.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to AWS ECS 2 | 3 | on: 4 | push: 5 | branches: ['main'] # Triggers on push to main 6 | 7 | concurrency: 8 | group: deploy-${{ github.ref }} 9 | cancel-in-progress: true 10 | 11 | env: 12 | ECR_REPOSITORY: app # Set this to your Amazon ECR repository name 13 | ECS_SERVICE: SkyDevOps # Set this to your Amazon ECS service name 14 | ECS_CLUSTER: Sky # Set this to your Amazon ECS cluster name 15 | 16 | permissions: 17 | contents: read 18 | 19 | jobs: 20 | deploy: 21 | name: Deploy to AWS ECS 22 | runs-on: ubuntu-latest 23 | environment: production 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v4 28 | 29 | - name: Set up Docker Buildx 30 | uses: docker/setup-buildx-action@v3 31 | 32 | - name: Configure AWS credentials 33 | uses: aws-actions/configure-aws-credentials@v4 34 | with: 35 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 36 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 37 | aws-region: ${{ secrets.AWS_REGION }} 38 | 39 | - name: Login to AWS ECR 40 | id: login-ecr 41 | uses: aws-actions/amazon-ecr-login@v2 42 | 43 | - name: Build and push new Docker image to AWS ECR 44 | id: build-image 45 | env: 46 | ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} 47 | IMAGE_TAG: latest 48 | run: | 49 | # Use Docker Buildx to build the image for x86_64 50 | docker buildx build --platform linux/amd64 --push -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . 51 | 52 | echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT 53 | echo "Docker image $ECR_REPOSITORY:$IMAGE_TAG has been built and pushed to $ECR_REGISTRY" 54 | 55 | - name: Deploy to AWS ECS 56 | run: | 57 | # Update the ECS service 58 | aws ecs update-service --cluster ${{ env.ECS_CLUSTER }} --service ${{ env.ECS_SERVICE }} --force-new-deployment 59 | 60 | # Define the website URL and the desired status code 61 | WEBSITE_URL="https://skyscraper-api.com" 62 | EXPECTED_STATUS_CODE=200 63 | 64 | # Loop until the website returns the expected status code 65 | # Wait 30 seconds before sending request 66 | sleep 30 67 | while true; do 68 | # Send a HEAD request to the website and capture the HTTP status code 69 | HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" $WEBSITE_URL) 70 | 71 | # Check if the status code matches the expected status code 72 | if [ "$HTTP_STATUS" -eq "$EXPECTED_STATUS_CODE" ]; then 73 | echo "Website is back up with status code $HTTP_STATUS" 74 | break 75 | else 76 | echo "Website down. Current status code: $HTTP_STATUS" 77 | fi 78 | 79 | # Wait 20 seconds before the next check 80 | sleep 20 81 | done 82 | echo "Website online" 83 | echo "Deployment successful" 84 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | # DS Store 133 | .DS_Store -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "jsxSingleQuote": true, 4 | "trailingComma": "all", 5 | "printWidth": 100, 6 | "tabWidth": 2, 7 | "semi": true, 8 | "bracketSpacing": true, 9 | "arrowParens": "always" 10 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.experimental.useFlatConfig": true, 3 | "editor.defaultFormatter": "esbenp.prettier-vscode", 4 | "editor.formatOnSave": true, 5 | "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"], 6 | "[typescript]": { 7 | "editor.defaultFormatter": "esbenp.prettier-vscode" 8 | }, 9 | "[html]": { 10 | "editor.defaultFormatter": "vscode.html-language-features" 11 | } 12 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/docker/library/node:20-alpine 2 | 3 | # Set the working directory 4 | WORKDIR /usr/src/app 5 | 6 | # Copy package.json and package-lock.json (if available) 7 | COPY package*.json ./ 8 | 9 | # Install dependencies 10 | RUN npm install -g npm@latest 11 | RUN npm install 12 | 13 | # Copy the rest of the application code 14 | COPY ./client ./client 15 | COPY ./server ./server 16 | 17 | # Build the server and client 18 | RUN npm run build:server 19 | RUN npm run build:prd 20 | 21 | # Expose the application port 22 | EXPOSE 8080 23 | 24 | # Define the command to run the application 25 | CMD ["node", "server/dist/server.js"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Open Source Labs Beta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

8 | 9 | Logo 10 | 11 | 12 |

SkyScraper

13 | 14 |

15 | Visualizer Dashboard for AWS EC2 Instances 16 |
17 | Report Bug 18 | | 19 | Request Feature 20 | 21 |

22 | 23 |

24 | 25 | GitHub Repo stars 26 | 27 | GitHub forks 28 | 29 | GitHub 30 | 31 | Contributions 32 |

33 |

34 | 35 |
36 | 37 | 38 |
39 | Table of Contents 40 |
    41 |
  1. Introduction 42 |
  2. Built With 43 |
  3. Usage
  4. 44 |
  5. Installation
  6. 45 |
  7. Contributing
  8. 46 |
  9. License
  10. 47 |
  11. Creators
  12. 48 |
  13. Contact Us
  14. 49 |
  15. Acknowledgements
  16. 50 |
51 |
52 | 53 |
54 | 55 | ## Introduction 56 | 57 |
58 |

59 | 60 |

61 | 62 | SkyScraper is an innovative visualizer dashboard that transforms the way developers interact with AWS performance data, starting with EC2. By offering a streamlined, intuitive interface, SkyScraper optimizes the retrieval, organization, and visualization of performance metrics, enabling users to manage their AWS environments effectively. 63 | 64 | Leveraging Auth0 and AWS Cognito for secure user authentication, SkyScraper ensures safe data retrieval using AWS credentials, with multiple security checkpoints to maintain data integrity and privacy. The application abstracts complex configurations, presenting clear and actionable insights that empower users to monitor instance activity, identify optimization opportunities, and make informed decisions to minimize costs and enhance performance. 65 | 66 | Designed with a focus on clarity and aesthetics, SkyScraper features custom themes that provide a visually pleasing user experience. Data is categorized and displayed through modern charts and graphs, allowing users to quickly identify trends and anomalies. By turning complex data into easily understandable insights, SkyScraper revolutionizes AWS performance data management, making it more efficient and accessible for developers. 67 | 68 | ### Built With 69 | 70 | - [](https://www.typescriptlang.org/) [TypeScript](https://www.typescriptlang.org/) 71 | - [](https://reactjs.org/) [React](https://reactjs.org/) 72 | - [](https://redux-toolkit.js.org/) [Redux](https://redux-toolkit.js.org/) 73 | - [](https://nodejs.org/en) [Node.js](https://nodejs.org/en) 74 | - [](https://expressjs.com/) [Express](https://expressjs.com/) 75 | - [](https://www.chartjs.org/) [Chart.js](https://www.chartjs.org/) 76 | - [](https://webpack.js.org/) [Webpack](https://webpack.js.org/) 77 | - [](https://auth0.com/) [Auth0](https://auth0.com/) 78 | - [](https://docs.aws.amazon.com/cognitoidentity/latest/APIReference/Welcome.html) [AWS Cognito API](https://docs.aws.amazon.com/cognitoidentity/latest/APIReference/Welcome.html) 79 | - [](https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/Welcome.html) [AWS CloudWatch API](https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/Welcome.html) 80 | - [](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/Welcome.html) [AWS EC2 API](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/Welcome.html) 81 | 82 |

(back to top)

83 | 84 | ## Usage 85 | 86 | 1. Navigate to https://skyscraper-api.com in your browser 87 | 1. Click Get Started 88 | 1. Sign Up or Log In with Auth0 89 | 1. Once Logged In, you will see an overview of the name and status of all EC2 Instances 90 | 1. Clicking on any instance box will bring you to the metrics page where you can view detailed metrics of each instance 91 | 92 |

(back to top)

93 | 94 | ## Installation 95 | 96 | Installing from Github: 97 | 98 | 1. Clone and open the Repo in your Code Editor 99 | ```sh 100 | git clone https://github.com/oslabs-beta/SkyScraper.git 101 | ``` 102 | 1. Create a .env file in the root directory from the provided template and input values from an AWS account 103 | 1. Install dependencies 104 | ```sh 105 | npm install 106 | ``` 107 | 1. Build and run the application on your local machine 108 | ```sh 109 | npm run go 110 | ``` 111 | 1. Navigate to http://localhost:8080 in your browser to view the application 112 | 113 |

(back to top)

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

(back to top)

144 | 145 | ## License 146 | 147 | Distributed under the MIT License. See [`LICENSE`](https://github.com/oslabs-beta/SkyScraper?tab=MIT-1-ov-file#readme) for more information. 148 | 149 |

(back to top)

150 | 151 | ## Creators 152 | 153 | [](https://github.com/abelr20) [](https://www.linkedin.com/in/abel-ratanaphan/) Abel Ratanaphan 154 | 155 | [](https://github.com/b-the-coder) [](https://www.linkedin.com/in/bin-emma-he/) Bin He 156 | 157 | [](https://github.com/ChristieLaf) [](https://www.linkedin.com/in/christie-laferriere/) Christie Laferriere 158 | 159 | [](https://github.com/TrippMurphy) [](https://www.linkedin.com/in/trippmurphy/) Tripp Murphy 160 | 161 | [](https://github.com/Nikolaa92) [](https://www.linkedin.com/in/nikola-andelkovic/) Nikola Andelkovic 162 | 163 |

(back to top)

164 | 165 | ## Contact Us 166 | 167 | AppSkyScraper@gmail.com 168 | 169 | []() [@SkyScraperApp](https://x.com/SkyScraperApp) 170 | 171 | []() [github.com/oslabs-beta/SkyScraper](https://github.com/oslabs-beta/SkyScraper/) 172 | 173 |

(back to top)

174 | 175 | ## Acknowledgements 176 | 177 | - [README Template](https://github.com/othneildrew/Best-README-Template) 178 | - [shields.io Badges](https://shields.io/) 179 | - [Icons 8](https://icons8.com/icons) 180 | - [Icon Finder](https://www.iconfinder.com/) 181 | - [Icon Scout](https://iconscout.com/) 182 | - [Flat Icon](https://flaticon.com) 183 | - [AWS Icons](https://aws-icons.com/) 184 | 185 |

(back to top)

186 | -------------------------------------------------------------------------------- /client/__tests__/LoginButton.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import LoginButton from '../src/features/auth/components/LoginButton'; 4 | import '@testing-library/jest-dom'; 5 | 6 | test('renders login button and calls loginWithRedirect on click', () => { 7 | render(); 8 | 9 | { 10 | React.createElement(LoginButton as React.ComponentType); 11 | } 12 | const button = screen.getByText('Log In'); 13 | expect(button).toBeInTheDocument(); 14 | }); 15 | -------------------------------------------------------------------------------- /client/__tests__/Navbar.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import { MemoryRouter } from 'react-router-dom'; 4 | import Navbar from '../src/features/navbar/Navbar'; 5 | import '@testing-library/jest-dom'; 6 | 7 | describe('NavBar', () => { 8 | it('renders navigation links and logo', () => { 9 | render( 10 | 11 | 12 | , 13 | ); 14 | 15 | expect(screen.getByAltText('Logo')).toBeInTheDocument(); 16 | expect(screen.getByText('Home')).toBeInTheDocument(); 17 | expect(screen.getByText('EC2 Monitor')).toBeInTheDocument(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/SkyScraper/e40368b61c8d18be7907c6f6aff55e444523638d/client/public/favicon.ico -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | SkyScraper 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | -------------------------------------------------------------------------------- /client/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; 3 | import { useAppSelector } from './app/hooks'; 4 | import HomePage from './features/homepage/HomePage'; 5 | import DashboardPage from './features/dashboard/DashboardPage'; 6 | import EC2MonitorPage from './features/ec2Monitor/EC2MonitorPage'; 7 | import NavBar from './features/navbar/Navbar'; 8 | import './styles/styles.css'; 9 | import './styles/Nav.css'; 10 | import './styles/LoginPage.css'; 11 | import './styles/HomePage.css'; 12 | 13 | const App: React.FC = () => { 14 | const mode = useAppSelector((state) => state.rootReducer.theme.mode); 15 | 16 | useEffect(() => { 17 | document.body.className = mode === 'light' ? 'light-mode' : 'dark-mode'; 18 | }, [mode]); 19 | 20 | return ( 21 | 22 | 23 | 24 | } /> 25 | } /> 26 | } /> 27 | 28 | 29 | ); 30 | }; 31 | 32 | export default App; 33 | -------------------------------------------------------------------------------- /client/src/app/hooks.ts: -------------------------------------------------------------------------------- 1 | import { useDispatch, useSelector } from 'react-redux'; 2 | import type { AppDispatch, RootState } from './store'; 3 | 4 | export const useAppDispatch = useDispatch.withTypes(); 5 | export const useAppSelector = useSelector.withTypes(); 6 | -------------------------------------------------------------------------------- /client/src/app/rootReducer.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from '@reduxjs/toolkit'; 2 | import authReducer from '../features/auth/authSlice'; 3 | import themeReducer from '../features/themes/themeSlice'; 4 | import dropDownReducer from '../features/auth/dropDownSlice'; 5 | 6 | const rootReducer = combineReducers({ 7 | auth: authReducer, 8 | theme: themeReducer, 9 | dropDown: dropDownReducer, 10 | }); 11 | 12 | export type RootState = ReturnType; 13 | export default rootReducer; 14 | -------------------------------------------------------------------------------- /client/src/app/store.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import { setupListeners } from '@reduxjs/toolkit/query'; 3 | import { authApi } from '../features/auth/authAPI'; 4 | import rootReducer from './rootReducer'; 5 | 6 | export const store = configureStore({ 7 | reducer: { 8 | rootReducer, 9 | [authApi.reducerPath]: authApi.reducer, 10 | }, 11 | middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(authApi.middleware), 12 | }); 13 | 14 | setupListeners(store.dispatch); 15 | 16 | export type AppDispatch = typeof store.dispatch; 17 | export type RootState = ReturnType; 18 | 19 | export default store; 20 | -------------------------------------------------------------------------------- /client/src/app/types.ts: -------------------------------------------------------------------------------- 1 | export type EC2Stats = Record; 2 | 3 | export interface AuthState { 4 | tokens: { 5 | access_token: string | null; 6 | id_token: string | null; 7 | }; 8 | } 9 | 10 | export interface DataPoint { 11 | Timestamp: Date; 12 | Value: number; 13 | } 14 | 15 | export interface MetricData { 16 | name: string; 17 | metric: string; 18 | unit: string; 19 | datapoints: DataPoint[]; 20 | } 21 | 22 | export interface EC2Instance { 23 | InstanceId: string; 24 | InstanceType: string; 25 | Name: string; 26 | State: string; 27 | } 28 | 29 | export interface CustomBarChartProps { 30 | instanceData: MetricData[]; 31 | } 32 | 33 | export interface TransformedData { 34 | metric: string; 35 | timestamp: string; 36 | value: number; 37 | unit: string; 38 | } 39 | 40 | export interface DropdownState { 41 | showDropdown: boolean; 42 | } 43 | 44 | export interface ThemeState { 45 | mode: string; 46 | } 47 | -------------------------------------------------------------------------------- /client/src/assets/EC2.ts: -------------------------------------------------------------------------------- 1 | const EC2 = 2 | 'data:image/jpeg;base64,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'; 3 | 4 | export default EC2; 5 | -------------------------------------------------------------------------------- /client/src/assets/Moon.ts: -------------------------------------------------------------------------------- 1 | const logo = 2 | 'data:image/png;base64,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'; 3 | export default logo; 4 | -------------------------------------------------------------------------------- /client/src/assets/images/Nodejs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/SkyScraper/e40368b61c8d18be7907c6f6aff55e444523638d/client/src/assets/images/Nodejs.png -------------------------------------------------------------------------------- /client/src/custom.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace JSX { 2 | interface IntrinsicElements { 3 | 'ion-icon': any; 4 | } 5 | } 6 | 7 | 8 | -------------------------------------------------------------------------------- /client/src/features/auth/authAPI.ts: -------------------------------------------------------------------------------- 1 | import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; 2 | import { RootState } from '../../app/store'; 3 | import { EC2Instance, EC2Stats } from '../../app/types'; 4 | 5 | export const authApi = createApi({ 6 | reducerPath: 'authApi', 7 | baseQuery: fetchBaseQuery({ 8 | 9 | // Production baseURL 10 | baseUrl: 'https://skyscraper-api.com/api/', 11 | 12 | // Development baseURL 13 | // baseUrl: 'http://localhost:8080/api/', 14 | 15 | prepareHeaders: (headers, { getState }) => { 16 | const state = getState() as RootState; 17 | const { access_token, id_token } = state.rootReducer.auth.tokens; 18 | if (access_token) { 19 | headers.set('Authorization', `Bearer ${access_token}`); 20 | } 21 | if (id_token) { 22 | headers.set('id-token', id_token); 23 | } 24 | return headers; 25 | }, 26 | }), 27 | endpoints: (builder) => ({ 28 | getEC2: builder.query({ 29 | query: (_ignoredParam) => 'ec2', 30 | }), 31 | getStats: builder.query({ 32 | query: (_ignoredParam) => 'stats', 33 | }), 34 | }), 35 | }); 36 | 37 | export const { useGetEC2Query, useGetStatsQuery } = authApi; 38 | -------------------------------------------------------------------------------- /client/src/features/auth/authSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | import type { AuthState } from '../../app/types'; 3 | 4 | const initialState: AuthState = { 5 | tokens: { 6 | access_token: null, 7 | id_token: null, 8 | }, 9 | }; 10 | 11 | const authSlice = createSlice({ 12 | name: 'auth', 13 | initialState, 14 | reducers: { 15 | setTokens: (state, action: PayloadAction) => { 16 | state.tokens = action.payload.tokens; 17 | }, 18 | clearTokens: (state) => { 19 | state.tokens.access_token = null; 20 | state.tokens.id_token = null; 21 | }, 22 | }, 23 | }); 24 | 25 | export const { setTokens, clearTokens } = authSlice.actions; 26 | export default authSlice.reducer; 27 | -------------------------------------------------------------------------------- /client/src/features/auth/components/LoginButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const LoginButton: React.FC = () => { 4 | const navigate = () => { 5 | window.location.href = 6 | // Production URI 7 | 'https://skyscraperwerock.auth.us-east-2.amazoncognito.com/oauth2/authorize?client_id=6hjtfh1ddmn4afj4c29ddijj32&response_type=token&scope=email+openid+phone+profile&redirect_uri=https%3A%2F%2Fskyscraper-api.com%2Fdashboard'; 8 | 9 | // Development URI 10 | // 'https://skyscraperwerock.auth.us-east-2.amazoncognito.com/oauth2/authorize?client_id=6hjtfh1ddmn4afj4c29ddijj32&response_type=token&scope=email+openid+phone+profile&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fdashboard'; 11 | }; 12 | 13 | return ( 14 |
15 | 18 |
19 | ); 20 | }; 21 | 22 | export default LoginButton; 23 | -------------------------------------------------------------------------------- /client/src/features/auth/components/LogoutButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react'; 2 | import { Link, useNavigate } from 'react-router-dom'; 3 | import { useAppDispatch, useAppSelector } from '../../../app/hooks'; 4 | import { toggleDropdown, closeDropdown } from '../dropDownSlice'; 5 | import { clearTokens } from '../authSlice'; 6 | 7 | const LogoutButton: React.FC = () => { 8 | const dispatch = useAppDispatch(); 9 | const navigate = useNavigate(); 10 | const isAuthenticated = useAppSelector((state) => state.rootReducer.auth.tokens.access_token); 11 | const showDropdown = useAppSelector((state) => state.rootReducer.dropDown.showDropdown); 12 | const mode = useAppSelector((state) => state.rootReducer.theme.mode); 13 | const dropdownRef = useRef(null); 14 | 15 | const logout = () => { 16 | dispatch(clearTokens()); 17 | localStorage.removeItem('access_token'); 18 | localStorage.removeItem('id_token'); 19 | navigate('/'); 20 | window.history.pushState(null, '', window.location.href); 21 | window.history.go(0); 22 | }; 23 | 24 | const iconStyle: React.CSSProperties = { 25 | fontSize: '35px', 26 | cursor: 'pointer', 27 | color: '#000', 28 | }; 29 | 30 | const dropdownStyle: React.CSSProperties = { 31 | display: showDropdown ? 'block' : 'none', 32 | position: 'absolute', 33 | top: '45px', 34 | right: '0px', 35 | backgroundColor: mode === 'light' ? 'lightblue' : '#121212', 36 | boxShadow: '0px 4px 8px rgba(0, 0, 0, 0.1)', 37 | borderRadius: '4px', 38 | zIndex: 1000, 39 | color: mode === 'light' ? '#000' : '#fff', 40 | }; 41 | 42 | const handleIconClick = () => { 43 | dispatch(toggleDropdown()); 44 | }; 45 | 46 | const handleClickOutside = (event: MouseEvent) => { 47 | if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { 48 | dispatch(closeDropdown()); 49 | } 50 | }; 51 | 52 | useEffect(() => { 53 | document.addEventListener('mousedown', handleClickOutside); 54 | return () => { 55 | document.removeEventListener('mousedown', handleClickOutside); 56 | }; 57 | }, []); 58 | 59 | if (!isAuthenticated) { 60 | return null; 61 | } 62 | 63 | return ( 64 |
65 |
66 | 67 |
68 |
69 |
    70 |
  • 71 | 72 | 75 | 76 |
  • 77 |
78 |
79 |
80 | ); 81 | }; 82 | 83 | export default LogoutButton; 84 | -------------------------------------------------------------------------------- /client/src/features/auth/dropDownSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | import { DropdownState } from '../../app/types'; 3 | 4 | const initialState: DropdownState = { 5 | showDropdown: false, 6 | }; 7 | 8 | const dropDownSlice = createSlice({ 9 | name: 'dropDown', 10 | initialState, 11 | reducers: { 12 | toggleDropdown(state) { 13 | state.showDropdown = !state.showDropdown; 14 | }, 15 | closeDropdown(state) { 16 | state.showDropdown = false; 17 | }, 18 | }, 19 | }); 20 | 21 | export const { toggleDropdown, closeDropdown } = dropDownSlice.actions; 22 | 23 | export default dropDownSlice.reducer; 24 | -------------------------------------------------------------------------------- /client/src/features/dashboard/DashboardPage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { useAppDispatch, useAppSelector } from '../../app/hooks'; 4 | import { EC2Instance } from '../../app/types'; 5 | import { useGetEC2Query } from '../auth/authAPI'; 6 | import { setTokens } from '../auth/authSlice'; 7 | import EC2Logo from '../../assets/EC2'; 8 | 9 | const DashboardPage: React.FC = () => { 10 | const dispatch = useAppDispatch(); 11 | const isAuthenticated = useAppSelector((state) => state.rootReducer.auth.tokens.access_token); 12 | 13 | useEffect(() => { 14 | const urlParams = new URLSearchParams(window.location.hash.substring(1)); 15 | const accessToken = urlParams.get('access_token'); 16 | const idToken = urlParams.get('id_token'); 17 | 18 | if (accessToken && idToken) { 19 | localStorage.setItem('access_token', accessToken); 20 | localStorage.setItem('id_token', idToken); 21 | dispatch(setTokens({ tokens: { access_token: accessToken, id_token: idToken } })); 22 | } else { 23 | const storedAccessToken = localStorage.getItem('access_token'); 24 | const storedIdToken = localStorage.getItem('id_token'); 25 | 26 | if (storedAccessToken && storedIdToken) { 27 | dispatch( 28 | setTokens({ tokens: { access_token: storedAccessToken, id_token: storedIdToken } }), 29 | ); 30 | } 31 | } 32 | }, [dispatch]); 33 | 34 | const { 35 | data: instances, 36 | isLoading, 37 | isError, 38 | error, 39 | } = useGetEC2Query(undefined, { 40 | pollingInterval: 2500, 41 | skipPollingIfUnfocused: true, 42 | }); 43 | 44 | const sorted: EC2Instance[] = instances 45 | ? [...instances].sort((a: EC2Instance, b: EC2Instance) => 46 | a.Name.toLocaleLowerCase().localeCompare(b.Name.toLocaleLowerCase()), 47 | ) 48 | : []; 49 | 50 | const activeInstancesCount = instances?.filter((ele) => ele.State === 'running').length; 51 | return ( 52 | isAuthenticated && ( 53 |
54 |
55 |
56 |

EC2 Instances

57 |
66 |

67 | {instances?.length} Instances: {activeInstancesCount} Running 68 |

69 | {isLoading && ( 70 |
71 |
72 |
73 |
74 |
75 | )} 76 |
77 | {isError &&

Error: {(error as Error).message}

} 78 |
85 | {sorted.map((instance: EC2Instance) => ( 86 | 87 |
88 | 89 |

Name: {instance.Name}

90 |

Type: {instance.InstanceType}

91 |

Status: {instance.State}

92 |
93 | 94 | ))} 95 |
96 |

Other Services: coming soon

97 |
98 |
99 |
100 | ) 101 | ); 102 | }; 103 | 104 | export default DashboardPage; 105 | -------------------------------------------------------------------------------- /client/src/features/ec2Monitor/EC2MonitorPage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useGetStatsQuery } from '../auth/authAPI'; 3 | import { useAppDispatch, useAppSelector } from '../../app/hooks'; 4 | import { setTokens } from '../auth/authSlice'; 5 | import CustomBarChart from './components/Charts'; 6 | 7 | const EC2MonitorPage: React.FC = () => { 8 | const dispatch = useAppDispatch(); 9 | const isAuthenticated = useAppSelector((state) => state.rootReducer.auth.tokens.access_token); 10 | const theme = useAppSelector((state) => state.rootReducer.theme.mode); 11 | 12 | useEffect(() => { 13 | const urlParams = new URLSearchParams(window.location.hash.substring(1)); 14 | const accessToken = urlParams.get('access_token'); 15 | const idToken = urlParams.get('id_token'); 16 | 17 | if (accessToken && idToken) { 18 | localStorage.setItem('access_token', accessToken); 19 | localStorage.setItem('id_token', idToken); 20 | dispatch(setTokens({ tokens: { access_token: accessToken, id_token: idToken } })); 21 | } else { 22 | const storedAccessToken = localStorage.getItem('access_token'); 23 | const storedIdToken = localStorage.getItem('id_token'); 24 | 25 | if (storedAccessToken && storedIdToken) { 26 | dispatch( 27 | setTokens({ tokens: { access_token: storedAccessToken, id_token: storedIdToken } }), 28 | ); 29 | } 30 | } 31 | }, [dispatch]); 32 | 33 | useEffect(() => { 34 | localStorage.setItem('theme', theme); 35 | }, [theme]); 36 | 37 | const { 38 | data: statistics = {}, 39 | isError, 40 | error, 41 | } = useGetStatsQuery(undefined, { 42 | pollingInterval: 2500, 43 | skipPollingIfUnfocused: true, 44 | }); 45 | 46 | if (isError) { 47 | return
Error {(error as Error).message}
; 48 | } 49 | 50 | const sortedInstanceIds = Object.keys(statistics).sort((a, b) => { 51 | const nameA = (statistics[a][0]?.name ?? '').toUpperCase(); 52 | const nameB = (statistics[b][0]?.name ?? '').toUpperCase(); 53 | return nameA.localeCompare(nameB); 54 | }); 55 | 56 | return ( 57 | isAuthenticated && ( 58 |
59 | {sortedInstanceIds.map((instanceId) => ( 60 |
61 |

Instance Name: {statistics[instanceId][0].name}

62 | 63 |
64 | ))} 65 |
66 | ) 67 | ); 68 | }; 69 | 70 | export default EC2MonitorPage; 71 | -------------------------------------------------------------------------------- /client/src/features/ec2Monitor/components/Charts.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { 3 | BarChart, 4 | Bar, 5 | LineChart, 6 | Line, 7 | XAxis, 8 | YAxis, 9 | Tooltip, 10 | ResponsiveContainer, 11 | } from 'recharts'; 12 | import { useAppSelector } from '../../../app/hooks'; 13 | import type { MetricData, CustomBarChartProps, TransformedData } from '../../../app/types'; 14 | 15 | const transformEC2Stats = ( 16 | instanceData: MetricData[], 17 | selectedMetric: string, 18 | ): TransformedData[] => { 19 | const transformedData: TransformedData[] = []; 20 | 21 | instanceData.forEach((metric) => { 22 | if (metric.metric === selectedMetric) { 23 | metric.datapoints.forEach((datapoint) => { 24 | transformedData.push({ 25 | metric: metric.metric, 26 | timestamp: new Date(datapoint.Timestamp).toLocaleString(), 27 | value: datapoint.Value, 28 | unit: metric.unit, 29 | }); 30 | }); 31 | } 32 | }); 33 | 34 | return transformedData; 35 | }; 36 | 37 | const Charts: React.FC = ({ instanceData }) => { 38 | const [selectedMetric, setSelectedMetric] = useState(''); 39 | const [chartType, setChartType] = useState('bar'); 40 | const mode = useAppSelector((state) => state.rootReducer.theme.mode); 41 | 42 | const handleMetricChange = (event: React.ChangeEvent) => { 43 | setSelectedMetric(event.target.value); 44 | }; 45 | 46 | const handleChartTypeToggle = () => { 47 | setChartType((prevType) => (prevType === 'bar' ? 'line' : 'bar')); 48 | }; 49 | 50 | const metrics = instanceData.map((metric) => metric.metric); 51 | const data = selectedMetric ? transformEC2Stats(instanceData, selectedMetric) : []; 52 | const unit = data.length > 0 ? data[0].unit : ''; 53 | 54 | const sortedMetrics: string[] = metrics.sort(); 55 | 56 | return ( 57 |
58 | 59 | 69 | 72 | {selectedMetric && data.length > 0 ? ( 73 | 74 | {chartType === 'bar' ? ( 75 | 76 | 80 | 83 | 84 | 85 | 86 | ) : ( 87 | 88 | 92 | 95 | 96 | 102 | 103 | )} 104 | 105 | ) : ( 106 |

No data available for the selected metric

107 | )} 108 |
109 | ); 110 | }; 111 | 112 | export default Charts; 113 | -------------------------------------------------------------------------------- /client/src/features/homepage/HomePage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import LoginButton from '../auth/components/LoginButton'; 3 | import SkyScraper from '../../assets/LoginBackground'; 4 | 5 | const HomePage: React.FC = () => { 6 | useEffect(() => { 7 | document.body.style.backgroundImage = `url(${SkyScraper})`; 8 | document.body.style.backgroundSize = 'cover'; 9 | document.body.style.backgroundPosition = 'center'; 10 | document.body.style.height = '100vh'; 11 | document.body.style.width = '100vw'; 12 | document.body.style.margin = '100'; 13 | document.body.style.padding = '1000'; 14 | document.body.style.overflow = 'hidden'; 15 | 16 | return () => { 17 | document.body.style.backgroundImage = ''; 18 | document.body.style.backgroundSize = ''; 19 | document.body.style.backgroundPosition = ''; 20 | document.body.style.height = ''; 21 | document.body.style.width = ''; 22 | document.body.style.margin = ''; 23 | document.body.style.padding = ''; 24 | document.body.style.overflow = ''; 25 | }; 26 | }, []); 27 | 28 | return ( 29 |
30 |
31 |
32 |

Welcome to SkyScraper

33 |
34 |
35 | {' '} 36 | 37 |
38 |
39 |
40 | ); 41 | }; 42 | 43 | export default HomePage; 44 | -------------------------------------------------------------------------------- /client/src/features/navbar/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router-dom'; 3 | import { useAppDispatch, useAppSelector } from '../../app/hooks'; 4 | import { toggleTheme } from '../themes/themeSlice'; 5 | import LogoutButton from '../auth/components/LogoutButton'; 6 | import logo from '../../assets/SkyScraperLogo'; 7 | import moon from '../../assets/Moon'; 8 | 9 | const NavBar: React.FC = () => { 10 | const isAuthenticated = useAppSelector((state) => state.rootReducer.auth.tokens.access_token); 11 | const dispatch = useAppDispatch(); 12 | const mode = useAppSelector((state) => state.rootReducer.theme.mode); 13 | 14 | const handleClick = () => { 15 | dispatch(toggleTheme()); 16 | }; 17 | 18 | const navBarStyle: React.CSSProperties = { 19 | display: 'flex', 20 | justifyContent: 'space-between', 21 | alignItems: 'center', 22 | backgroundColor: mode === 'dark' ? '#FFD700' : '#1ab9cabe', 23 | padding: '10px 20px', 24 | position: 'fixed', 25 | top: 0, 26 | width: '100%', 27 | zIndex: 1000, 28 | boxSizing: 'border-box', 29 | transition: 'background-color 0.3s ease, box-shadow 0.3s ease, opacity 0.3s ease', 30 | }; 31 | 32 | const navLeftStyle: React.CSSProperties = { 33 | display: 'flex', 34 | alignItems: 'center', 35 | textDecoration: 'none', 36 | color: '#000', 37 | }; 38 | 39 | const navTitleStyle: React.CSSProperties = { 40 | margin: 0, 41 | marginLeft: '10px', 42 | fontSize: '1.5rem', 43 | paddingTop: '5px', 44 | }; 45 | 46 | const navItemsStyle: React.CSSProperties = { 47 | display: 'flex', 48 | alignItems: 'center', 49 | }; 50 | 51 | const modeStyle: React.CSSProperties = { 52 | marginLeft: '20px', 53 | marginRight: '10px', 54 | cursor: 'pointer', 55 | border: 'none', 56 | outline: 'none', 57 | filter: mode === 'dark' ? 'invert(0%)' : 'invert(100%)', 58 | }; 59 | 60 | return ( 61 | 87 | ); 88 | }; 89 | 90 | export default NavBar; 91 | -------------------------------------------------------------------------------- /client/src/features/themes/themeSlice.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | import { ThemeState } from '../../app/types'; 3 | 4 | const initialState: ThemeState = { 5 | mode: localStorage.getItem('theme') || 'light', 6 | }; 7 | 8 | const themeSlice = createSlice({ 9 | name: 'theme', 10 | initialState, 11 | reducers: { 12 | toggleTheme(state) { 13 | state.mode = state.mode === 'light' ? 'dark' : 'light'; 14 | localStorage.setItem('theme', state.mode); 15 | }, 16 | setTheme(state, action) { 17 | state.mode = action.payload; 18 | localStorage.setItem('theme', state.mode); 19 | }, 20 | }, 21 | }); 22 | 23 | export const { toggleTheme, setTheme } = themeSlice.actions; 24 | 25 | export default themeSlice.reducer; 26 | -------------------------------------------------------------------------------- /client/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client'; // Note the new import path for ReactDOM 2 | import { Provider } from 'react-redux'; 3 | import store from './app/store'; 4 | import App from './App'; 5 | 6 | const rootElement = document.getElementById('root'); 7 | 8 | if (!rootElement) { 9 | throw new Error('Failed to find the root element'); 10 | } 11 | 12 | createRoot(rootElement).render( 13 | 14 | 15 | , 16 | ); 17 | -------------------------------------------------------------------------------- /client/src/styles/HomePage.css: -------------------------------------------------------------------------------- 1 | body.dark-mode .login-button-container { 2 | text-align: center; 3 | background-image: '../../../assets/LoginBackground.ts'; 4 | border-radius: 10px; 5 | display: flex; 6 | flex-direction: column; 7 | align-items: center; 8 | } 9 | 10 | .login-button-container { 11 | text-align: center; 12 | background-color: rgba(255, 255, 255, 0.8); 13 | border-radius: 10px; 14 | display: flex; 15 | flex-direction: column; 16 | align-items: center; 17 | color: black; 18 | } 19 | 20 | .HpHeader h1, 21 | .HpHeader h2 { 22 | margin: 0; 23 | padding: 10px 0; 24 | } 25 | 26 | .HpHeader { 27 | text-align: center; 28 | margin-bottom: 20px; 29 | } 30 | 31 | .LoginButtonWrapper { 32 | margin-top: 5px; 33 | } 34 | -------------------------------------------------------------------------------- /client/src/styles/LoginPage.css: -------------------------------------------------------------------------------- 1 | .login-button-container { 2 | background-attachment: fixed; 3 | background-image: '../../../assets/LoginBackground.ts'; 4 | background-size: cover; 5 | background-position: center; 6 | width: 100vw; 7 | height: 100vh; 8 | display: flex; 9 | justify-content: center; 10 | align-items: center; 11 | } 12 | -------------------------------------------------------------------------------- /client/src/styles/Nav.css: -------------------------------------------------------------------------------- 1 | /* Dark Knight Theme */ 2 | body.dark-mode { 3 | background-color: #121212; /* Dark background */ 4 | color: #e0e0e0; /* Light text color */ 5 | } 6 | 7 | body.dark-mode .singleInstance:hover { 8 | background-color: #333; /* Hover state */ 9 | color: #e0e0e0; /* Hover text color */ 10 | } 11 | 12 | body.dark-mode .homebutton { 13 | background-color: #333; /* Dark button background */ 14 | color: #e0e0e0; /* Light text color */ 15 | } 16 | 17 | body.dark-mode .navbar { 18 | background-color: #1f1f1f; /* Dark navbar background */ 19 | color: #e0e0e0; /* Light text color */ 20 | } 21 | 22 | .nav-bar { 23 | display: flex; 24 | justify-content: space-between; 25 | align-items: center; 26 | background-color: #1ab9cabe; 27 | padding: 10px 20px; 28 | transition: 29 | background-color 0.3s ease, 30 | box-shadow 0.3s ease, 31 | opacity 0.3s ease; 32 | position: fixed; 33 | top: 0; 34 | width: 100%; 35 | z-index: 1000; 36 | box-sizing: border-box; 37 | } 38 | 39 | .nav-items { 40 | display: flex; 41 | gap: 15px; 42 | align-items: center; 43 | } 44 | 45 | .nav-items a { 46 | color: white; 47 | text-decoration: none; 48 | } 49 | 50 | .nav-items a:hover { 51 | background-color: #4195c5; 52 | border-radius: 10%; 53 | } 54 | 55 | .nav-bar.dark-mode { 56 | background-color: rgb(233, 233, 47); 57 | color: black; 58 | } 59 | -------------------------------------------------------------------------------- /client/src/styles/UserIcon.css: -------------------------------------------------------------------------------- 1 | .icon { 2 | font-size: 35px; 3 | cursor: pointer; 4 | } 5 | 6 | .dropdown { 7 | display: none; 8 | position: absolute; 9 | top: 45px; 10 | right: 0; 11 | background-color: white; 12 | border: 1px solid #ccc; 13 | box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1); 14 | border-radius: 4px; 15 | z-index: 1000; 16 | } 17 | 18 | .dropdown.show { 19 | display: block; 20 | } 21 | 22 | .dropdown ul { 23 | list-style-type: none; 24 | margin: 0; 25 | padding: 10px; 26 | } 27 | 28 | .dropdown li { 29 | padding: 5px 0; 30 | } 31 | 32 | .dropdown button { 33 | background: none; 34 | border: none; 35 | cursor: pointer; 36 | font: inherit; 37 | } 38 | -------------------------------------------------------------------------------- /client/src/styles/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: lightblue; 3 | margin: 0px; 4 | font-family: 'Overpass Mono', monospace; 5 | } 6 | 7 | body.dark-mode .log-button { 8 | background: #ffd700; 9 | color: black; 10 | font-family: inherit; 11 | padding: 0.35em; 12 | font-size: 17px; 13 | font-weight: 500; 14 | border-radius: 0.9em; 15 | border: none; 16 | letter-spacing: 0.05em; 17 | display: flex; 18 | align-items: center; 19 | box-shadow: inset 0 0 1.6em -0.6em #714da6; 20 | overflow: hidden; 21 | position: relative; 22 | height: 2.8em; 23 | cursor: pointer; 24 | } 25 | 26 | body.dark-mode .log-button:hover { 27 | box-shadow: 0.1em 0.1em 0.6em 0.2em #ffd700; 28 | } 29 | 30 | body.dark-mode { 31 | background-color: #121212; 32 | color: '5d46e2'; 33 | } 34 | 35 | body.dark-mode .nav-bar { 36 | background-color: #121212; 37 | color: white; 38 | } 39 | 40 | body.dark-mode .singleInstance { 41 | background-color: rgb(80, 77, 77); 42 | color: white; 43 | border-color: #ffd700; 44 | } 45 | 46 | body.dark-mode .singleInstance:hover { 47 | background-color: rgba(113, 113, 113, 0.692); 48 | color: #ffd700; 49 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); 50 | cursor: pointer; 51 | border-color: #ffd700; 52 | } 53 | 54 | body.dark-mode .three-body { 55 | --uib-size: 17px; 56 | --uib-speed: 0.2s; 57 | --uib-color: #ffd700; 58 | position: relative; 59 | display: inline-block; 60 | height: var(--uib-size); 61 | width: var(--uib-size); 62 | animation: spin78236 calc(var(--uib-speed) * 2.5) infinite linear; 63 | } 64 | 65 | .inner-body { 66 | margin-top: 80px; 67 | margin-left: 30px; 68 | } 69 | 70 | #navbar { 71 | margin: 0; 72 | position: fixed; 73 | top: 0; 74 | width: 100%; 75 | z-index: 10000; 76 | } 77 | 78 | #title { 79 | text-align: center; 80 | } 81 | 82 | #displayedinstances { 83 | display: flex; 84 | justify-content: center; 85 | } 86 | 87 | #metricSelect { 88 | height: 21.5px; 89 | text-align: center; 90 | } 91 | 92 | .isError { 93 | margin-top: 50px; 94 | padding: 20px; 95 | } 96 | .singleInstance { 97 | border: solid; 98 | background-color: #b2a8ea; 99 | margin: 10px; 100 | padding: 20px; 101 | text-decoration: none; 102 | height: 12em; 103 | width: 20em; 104 | border-radius: 10px; 105 | transition: 106 | background-color 0.3s ease, 107 | box-shadow 0.3s ease, 108 | opacity 0.3s ease; 109 | } 110 | 111 | .singleInstance:hover { 112 | box-shadow: rgb(255, 255, 255); 113 | background-color: white; 114 | color: #5d46e2; 115 | font-weight: bold; 116 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); 117 | text-decoration: none; 118 | cursor: pointer; 119 | } 120 | 121 | .link { 122 | text-decoration: none; 123 | } 124 | 125 | .homebutton { 126 | position: absolute; 127 | top: 50px; 128 | right: 50px; 129 | height: 30px; 130 | width: 150px; 131 | font-weight: bold; 132 | font-size: 20px; 133 | } 134 | 135 | .three-body { 136 | --uib-size: 17px; 137 | --uib-speed: 0.2s; 138 | --uib-color: #5d3fd3; 139 | position: relative; 140 | display: inline-block; 141 | height: var(--uib-size); 142 | width: var(--uib-size); 143 | animation: spin78236 calc(var(--uib-speed) * 2.5) infinite linear; 144 | } 145 | 146 | .three-body__dot { 147 | position: absolute; 148 | height: 100%; 149 | width: 30%; 150 | } 151 | 152 | .three-body__dot:after { 153 | content: ''; 154 | position: absolute; 155 | height: 0%; 156 | width: 100%; 157 | padding-bottom: 100%; 158 | background-color: var(--uib-color); 159 | border-radius: 50%; 160 | } 161 | 162 | .three-body__dot:nth-child(1) { 163 | bottom: 5%; 164 | left: 0; 165 | transform: rotate(60deg); 166 | transform-origin: 50% 85%; 167 | } 168 | 169 | .three-body__dot:nth-child(1)::after { 170 | bottom: 0; 171 | left: 0; 172 | animation: wobble1 var(--uib-speed) infinite ease-in-out; 173 | animation-delay: calc(var(--uib-speed) * -0.3); 174 | } 175 | 176 | .three-body__dot:nth-child(2) { 177 | bottom: 5%; 178 | right: 0; 179 | transform: rotate(-60deg); 180 | transform-origin: 50% 85%; 181 | } 182 | 183 | .three-body__dot:nth-child(2)::after { 184 | bottom: 0; 185 | left: 0; 186 | animation: wobble1 var(--uib-speed) infinite calc(var(--uib-speed) * -0.15) ease-in-out; 187 | } 188 | 189 | .three-body__dot:nth-child(3) { 190 | bottom: -5%; 191 | left: 0; 192 | transform: translateX(116.666%); 193 | } 194 | 195 | .three-body__dot:nth-child(3)::after { 196 | top: 0; 197 | left: 0; 198 | animation: wobble2 var(--uib-speed) infinite ease-in-out; 199 | } 200 | 201 | .photo { 202 | position: absolute; 203 | left: 0; 204 | right: 0; 205 | top: 0; 206 | bottom: 0; 207 | background-position: center; 208 | background-repeat: no-repeat; 209 | background-size: cover; 210 | } 211 | 212 | .no-underline { 213 | text-decoration: none; 214 | } 215 | 216 | .log-button { 217 | background: #a370f0; 218 | color: white; 219 | font-family: inherit; 220 | padding: 0.35em; 221 | font-size: 17px; 222 | font-weight: 500; 223 | border-radius: 0.9em; 224 | border: none; 225 | letter-spacing: 0.05em; 226 | display: flex; 227 | align-items: center; 228 | box-shadow: inset 0 0 1.6em -0.6em #714da6; 229 | overflow: hidden; 230 | position: relative; 231 | height: 2.8em; 232 | cursor: pointer; 233 | } 234 | 235 | .log-button:hover { 236 | box-shadow: 0.1em 0.1em 0.6em 0.2em #7b52b9; 237 | } 238 | 239 | .log-button .icon { 240 | width: 1.1em; 241 | transition: transform 0.3s; 242 | color: #7b52b9; 243 | } 244 | 245 | .log-button:active { 246 | transform: scale(0.95); 247 | } 248 | 249 | @keyframes spin78236 { 250 | 0% { 251 | transform: rotate(0deg); 252 | } 253 | 254 | 100% { 255 | transform: rotate(360deg); 256 | } 257 | } 258 | 259 | @keyframes wobble1 { 260 | 0%, 261 | 100% { 262 | transform: translateY(0%) scale(1); 263 | opacity: 1; 264 | } 265 | 266 | 50% { 267 | transform: translateY(-66%) scale(0.65); 268 | opacity: 0.8; 269 | } 270 | } 271 | 272 | @keyframes wobble2 { 273 | 0%, 274 | 100% { 275 | transform: translateY(0%) scale(1); 276 | opacity: 1; 277 | } 278 | 279 | 50% { 280 | transform: translateY(66%) scale(0.65); 281 | opacity: 0.8; 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "target": "ES6", // "target": "es6", // Target ECMAScript version 5 | "noEmit": false, // Added noEmit to complement allowImportingTsExtensions 6 | "module": "ESNext", // "module": "esnext", // Enable ES module syntax 7 | "moduleResolution": "node", // Use Node.js module resolution 8 | "jsx": "react-jsx", // Enable JSX for React 9 | "strict": true, // Enable all strict type-checking options 10 | "esModuleInterop": true, // Enables default imports from modules with no default export 11 | "outDir": "./dist", 12 | "rootDir": "./src", 13 | "skipLibCheck": true, // Skip type checking of declaration files 14 | "resolveJsonModule": true, 15 | "baseUrl": "./", // Base directory to resolve non-relative module names 16 | "paths": { 17 | "*": ["node_modules/*", "src/*"] 18 | }, 19 | "typeRoots": ["./src/app/types", "./node_modules/@types"], 20 | "lib": ["dom", "dom.iterable", "esnext"], // Library files to be included in the compilation 21 | "forceConsistentCasingInFileNames": true // Disallow 22 | }, 23 | "include": ["src/**/*", "types/custom.d.ts"], 24 | "exclude": ["node_modules"] 25 | } 26 | -------------------------------------------------------------------------------- /client/webpack.config.js: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import MiniCssExtractPlugin from 'mini-css-extract-plugin'; 3 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 4 | import TerserPlugin from 'terser-webpack-plugin'; 5 | import _ from 'lodash'; 6 | import { fileURLToPath } from 'url'; 7 | 8 | const __filename = fileURLToPath(import.meta.url); 9 | const __dirname = path.dirname(__filename); 10 | 11 | const config = { 12 | mode: 'production', 13 | entry: './client/src/index.tsx', 14 | output: { 15 | path: path.resolve(__dirname, 'dist'), 16 | filename: 'bundle.js', 17 | }, 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.(js|jsx)$/, 22 | exclude: [/node_modules/, /__tests__/], 23 | use: ['babel-loader'], 24 | }, 25 | { 26 | test: /\.(ts|tsx)$/, 27 | exclude: /node_modules/, 28 | use: ['babel-loader', 'ts-loader'], 29 | }, 30 | { 31 | test: /\.css$/i, 32 | use: ['style-loader', 'css-loader'], 33 | }, 34 | ], 35 | }, 36 | resolve: { 37 | extensions: ['.jsx', '.js', '.ts', '.tsx'], 38 | }, 39 | optimization: { 40 | minimize: true, 41 | minimizer: [new TerserPlugin()], 42 | }, 43 | plugins: [ 44 | new HtmlWebpackPlugin({ 45 | template: './client/public/index.html', 46 | filename: 'index.html', 47 | favicon: './client/public/favicon.ico', 48 | }), 49 | new MiniCssExtractPlugin({ 50 | filename: 'bundle.css', 51 | }), 52 | ], 53 | devServer: { 54 | compress: true, 55 | port: 3000, 56 | historyApiFallback: true, 57 | static: [ 58 | { 59 | directory: path.resolve('dist'), 60 | publicPath: '/', 61 | }, 62 | ], 63 | proxy: [ 64 | { 65 | context: ['/api'], 66 | target: 'http://localhost:8080', 67 | }, 68 | ], 69 | }, 70 | }; 71 | 72 | export default config; 73 | -------------------------------------------------------------------------------- /ecs-task-def.json: -------------------------------------------------------------------------------- 1 | { 2 | "family": "ecs-task-def", 3 | "containerDefinitions": [ 4 | { 5 | "name": "app", 6 | "image": "992382810552.dkr.ecr.us-east-1.amazonaws.com/app:latest", 7 | "cpu": 0, 8 | "portMappings": [ 9 | { 10 | "name": "app-8080-tcp", 11 | "containerPort": 8080, 12 | "hostPort": 8080, 13 | "protocol": "tcp" 14 | } 15 | ], 16 | "essential": true, 17 | "environment": [ 18 | { 19 | "name": "PROD_PORT", 20 | "value": "8080" 21 | }, 22 | { 23 | "name": "CLIENT_ID", 24 | "value": "6hjtfh1ddmn4afj4c29ddijj32" 25 | }, 26 | { 27 | "name": "DEV_PORT", 28 | "value": "8080" 29 | }, 30 | { 31 | "name": "IDENTITY_POOL_ID", 32 | "value": "us-east-2:ef0095f5-b5d4-4ed4-a40a-262f8022e37d" 33 | }, 34 | { 35 | "name": "NODE_ENV", 36 | "value": "development" 37 | }, 38 | { 39 | "name": "USER_POOL_ID", 40 | "value": "us-east-2_53sjBSg4Y" 41 | }, 42 | { 43 | "name": "REGION", 44 | "value": "us-east-2" 45 | } 46 | ], 47 | "mountPoints": [], 48 | "volumesFrom": [], 49 | "ulimits": [], 50 | "systemControls": [] 51 | } 52 | ], 53 | "taskRoleArn": "arn:aws:iam::992382810552:role/ecsTaskExecutionRole", 54 | "executionRoleArn": "arn:aws:iam::992382810552:role/ecsTaskExecutionRole", 55 | "networkMode": "awsvpc", 56 | "requiresCompatibilities": ["FARGATE"], 57 | "cpu": "512", 58 | "memory": "1024", 59 | "runtimePlatform": { 60 | "cpuArchitecture": "X86_64", 61 | "operatingSystemFamily": "LINUX" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js'; 2 | import tseslint from 'typescript-eslint'; 3 | import react from 'eslint-plugin-react'; 4 | import reactRecommended from 'eslint-plugin-react/configs/recommended.js'; 5 | import globals from 'globals'; 6 | const { node, browser, mocha } = globals; 7 | 8 | export default [ 9 | js.configs.recommended, 10 | ...tseslint.configs.strictTypeChecked, 11 | ...tseslint.configs.stylisticTypeChecked, 12 | { 13 | languageOptions: { 14 | parser: '@typescript-eslint/parser', 15 | parserOptions: { 16 | project: true, 17 | tsconfigRootDir: import.meta.dirname, 18 | }, 19 | }, 20 | }, 21 | { 22 | files: ['**/*.js'], 23 | ...tseslint.configs.disableTypeChecked, 24 | }, 25 | { 26 | rules: { 27 | 'require-atomic-updates': 'error', 28 | 'arrow-body-style': ['error', 'as-needed'], 29 | eqeqeq: 'error', 30 | semi: 'off', 31 | '@typescript-eslint/semi': ['error', 'always'], 32 | 'prefer-const': 'error', 33 | }, 34 | }, 35 | { 36 | files: ['server/**/*.{js,ts}'], 37 | languageOptions: { 38 | globals: { 39 | ...node, 40 | }, 41 | }, 42 | rules: { 43 | '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], 44 | }, 45 | }, 46 | { 47 | files: ['server/__tests__/**/*.{js,ts}'], 48 | languageOptions: { 49 | globals: { 50 | ...node, 51 | ...mocha, 52 | }, 53 | }, 54 | }, 55 | { 56 | files: ['client/**/*.{js,jsx,ts,tsx}'], 57 | ...reactRecommended, 58 | plugins: { 59 | react, 60 | }, 61 | languageOptions: { 62 | ...reactRecommended.languageOptions, 63 | parserOptions: { 64 | // parser: '@typescript-eslint/parser', 65 | // parserOptions: { 66 | // project: './tsconfig.json', 67 | // }, 68 | ecmaFeatures: { 69 | jsx: true, 70 | }, 71 | }, 72 | globals: { 73 | ...browser, 74 | }, 75 | }, 76 | settings: { 77 | react: { 78 | version: 'detect', 79 | }, 80 | }, 81 | }, 82 | { 83 | files: ['client/**/*.{js,jsx,ts,tsx}'], 84 | rules: { 85 | 'react/react-in-jsx-scope': 'off', 86 | 'react/jsx-uses-react': 'off', 87 | 'react/prop-types': 'off', 88 | '@typescript-eslint/no-unused-vars': [ 89 | 'error', 90 | { argsIgnorePattern: '^_', destructuredArrayIgnorePattern: '^_' }, 91 | ], 92 | }, 93 | }, 94 | { 95 | ignores: ['/dist'], 96 | }, 97 | ]; 98 | -------------------------------------------------------------------------------- /images/Auth0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/SkyScraper/e40368b61c8d18be7907c6f6aff55e444523638d/images/Auth0.png -------------------------------------------------------------------------------- /images/Chartjs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/SkyScraper/e40368b61c8d18be7907c6f6aff55e444523638d/images/Chartjs.png -------------------------------------------------------------------------------- /images/CircleLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/SkyScraper/e40368b61c8d18be7907c6f6aff55e444523638d/images/CircleLogo.png -------------------------------------------------------------------------------- /images/CloudWatch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/SkyScraper/e40368b61c8d18be7907c6f6aff55e444523638d/images/CloudWatch.png -------------------------------------------------------------------------------- /images/Cognito.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/SkyScraper/e40368b61c8d18be7907c6f6aff55e444523638d/images/Cognito.png -------------------------------------------------------------------------------- /images/EC2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/SkyScraper/e40368b61c8d18be7907c6f6aff55e444523638d/images/EC2.png -------------------------------------------------------------------------------- /images/Express.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/SkyScraper/e40368b61c8d18be7907c6f6aff55e444523638d/images/Express.png -------------------------------------------------------------------------------- /images/FlatLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/SkyScraper/e40368b61c8d18be7907c6f6aff55e444523638d/images/FlatLogo.png -------------------------------------------------------------------------------- /images/GitHubBlack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/SkyScraper/e40368b61c8d18be7907c6f6aff55e444523638d/images/GitHubBlack.png -------------------------------------------------------------------------------- /images/GitHubWhite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/SkyScraper/e40368b61c8d18be7907c6f6aff55e444523638d/images/GitHubWhite.png -------------------------------------------------------------------------------- /images/LinkedIn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/SkyScraper/e40368b61c8d18be7907c6f6aff55e444523638d/images/LinkedIn.png -------------------------------------------------------------------------------- /images/Mail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/SkyScraper/e40368b61c8d18be7907c6f6aff55e444523638d/images/Mail.png -------------------------------------------------------------------------------- /images/Nodejs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/SkyScraper/e40368b61c8d18be7907c6f6aff55e444523638d/images/Nodejs.png -------------------------------------------------------------------------------- /images/React.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/SkyScraper/e40368b61c8d18be7907c6f6aff55e444523638d/images/React.png -------------------------------------------------------------------------------- /images/Redux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/SkyScraper/e40368b61c8d18be7907c6f6aff55e444523638d/images/Redux.png -------------------------------------------------------------------------------- /images/TS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/SkyScraper/e40368b61c8d18be7907c6f6aff55e444523638d/images/TS.png -------------------------------------------------------------------------------- /images/Webpack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/SkyScraper/e40368b61c8d18be7907c6f6aff55e444523638d/images/Webpack.png -------------------------------------------------------------------------------- /images/XBlack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/SkyScraper/e40368b61c8d18be7907c6f6aff55e444523638d/images/XBlack.png -------------------------------------------------------------------------------- /images/XWhite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/SkyScraper/e40368b61c8d18be7907c6f6aff55e444523638d/images/XWhite.png -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | preset: 'ts-jest', 3 | testEnvironment: 'jsdom', 4 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 5 | setupFilesAfterEnv: ['/jest-setup.ts'], 6 | transform: { 7 | '^.+\\.(ts|tsx)$': 'ts-jest', 8 | }, 9 | transformIgnorePatterns: [ 10 | '/node_modules/', // Ignore transformations in node_modules 11 | '/dist/', // Ignore transformations in dist 12 | ], 13 | testPathIgnorePatterns: [ 14 | '/dist/', // Ignore test files in dist 15 | ], 16 | moduleNameMapper: { 17 | '\\.(css|less|scss|sass)$': 'identity-obj-proxy', // Mock CSS imports 18 | }, 19 | testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], 20 | }; 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "skyscraper", 3 | "version": "1.0.0", 4 | "description": "Visualizer Dashboard for AWS EC2 Instances", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "clean": "rm -rf ./client/dist/* && rm -rf ./server/dist/*", 9 | "dev": "webpack serve --config ./client/webpack.config.js --mode=development", 10 | "build:prd": "webpack --config ./client/webpack.config.js --mode=production", 11 | "build:server": "tsc --project ./server/tsconfig.json", 12 | "build": "npm run build:server && npm run build:prd", 13 | "start": "nodemon ./server/dist/server.js", 14 | "go": "npm run clean && npm run build && npm run start", 15 | "test": "jest", 16 | "lint": "eslint .", 17 | "_Comment1": "delete axios, cors, babel, eslint, @types react testing libary 10.2 and any other unused dependencies" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/oslabs-beta/SkyScraper.git" 22 | }, 23 | "author": "Tripp Murphy, Christie Laferriere, Bin He, Abel Ratanaphan, Nikola Andelkovic", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/oslabs-beta/SkyScraper/issues" 27 | }, 28 | "homepage": "https://github.com/oslabs-beta/SkyScraper#readme", 29 | "dependencies": { 30 | "@auth0/auth0-react": "^2.2.4", 31 | "@aws-sdk/client-cloudwatch": "^3.576.0", 32 | "@aws-sdk/client-cognito-identity": "^3.588.0", 33 | "@aws-sdk/client-ec2": "^3.576.0", 34 | "@aws-sdk/client-sts": "^3.590.0", 35 | "@aws-sdk/credential-provider-web-identity": "^3.587.0", 36 | "@reduxjs/toolkit": "^2.2.4", 37 | "@types/chart.js": "^2.9.41", 38 | "@types/express": "^4.17.21", 39 | "@types/react-redux": "^7.1.33", 40 | "aws-jwt-verify": "^4.0.1", 41 | "chart.js": "^4.4.3", 42 | "cors": "^2.8.5", 43 | "dotenv": "^16.4.5", 44 | "express": "^4.19.2", 45 | "express-openid-connect": "^2.17.1", 46 | "ionicons": "^7.4.0", 47 | "jest-fetch-mock": "^3.0.3", 48 | "react": "^18.3.1", 49 | "react-chartjs-2": "^5.2.0", 50 | "react-dom": "^18.3.1", 51 | "react-redux": "^9.1.2", 52 | "react-router-dom": "^6.23.1", 53 | "recharts": "^2.12.7", 54 | "serve-favicon": "^2.5.0" 55 | }, 56 | "devDependencies": { 57 | "@babel/core": "^7.24.5", 58 | "@babel/preset-env": "^7.24.5", 59 | "@babel/preset-react": "^7.24.1", 60 | "@babel/preset-typescript": "^7.24.1", 61 | "@eslint/js": "^9.2.0", 62 | "@reduxjs/toolkit": "^2.2.5", 63 | "@svgr/webpack": "^8.1.0", 64 | "@testing-library/jest-dom": "^6.4.5", 65 | "@testing-library/react": "^15.0.7", 66 | "@types/cors": "^2.8.17", 67 | "@types/jest": "^29.5.12", 68 | "@types/jsonwebtoken": "^9.0.6", 69 | "@types/node": "^20.12.11", 70 | "@types/react": "^18.3.2", 71 | "@types/react-dom": "^18.3.0", 72 | "@types/redux-mock-store": "^1.0.6", 73 | "@types/serve-favicon": "^2.5.7", 74 | "@types/testing-library__react": "^10.2.0", 75 | "@typescript-eslint/eslint-plugin": "^7.8.0", 76 | "@typescript-eslint/parser": "^7.8.0", 77 | "babel-loader": "^9.1.3", 78 | "copy-webpack-plugin": "^12.0.2", 79 | "css-loader": "^7.1.1", 80 | "eslint": "^8.57.0", 81 | "eslint-config-prettier": "^9.1.0", 82 | "eslint-plugin-prettier": "^5.1.3", 83 | "eslint-plugin-react": "^7.34.1", 84 | "globals": "^15.2.0", 85 | "html-webpack-plugin": "^5.6.0", 86 | "jest": "^29.7.0", 87 | "jest-environment-jsdom": "^29.7.0", 88 | "jsonwebtoken": "^9.0.2", 89 | "mini-css-extract-plugin": "^2.9.0", 90 | "nodemon": "^3.1.0", 91 | "prettier": "^3.2.5", 92 | "redux-mock-store": "^1.5.4", 93 | "sass": "^1.77.2", 94 | "sass-loader": "^14.2.1", 95 | "style-loader": "^4.0.0", 96 | "terser-webpack-plugin": "^5.3.10", 97 | "ts-jest": "^29.1.4", 98 | "ts-loader": "^9.5.1", 99 | "ts-node": "^10.9.2", 100 | "typescript": "^5.4.5", 101 | "typescript-eslint": "^7.11.0", 102 | "webpack": "^5.91.0", 103 | "webpack-cli": "^5.1.4", 104 | "webpack-dev-server": "^5.0.4" 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /server/src/controllers/authController.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CognitoIdentityClient, 3 | GetIdCommand, 4 | GetCredentialsForIdentityCommand, 5 | } from '@aws-sdk/client-cognito-identity'; 6 | import type { authController } from '../utils/types.js'; 7 | import { CognitoJwtVerifier } from 'aws-jwt-verify'; 8 | import ErrorObject from '../utils/ErrorObject.js'; 9 | 10 | const authController: authController = { 11 | verifyJWT: (req, res, next) => { 12 | void (async () => { 13 | try { 14 | const tokenUse = 'access'; 15 | 16 | const verifier = CognitoJwtVerifier.create({ 17 | userPoolId: process.env.USER_POOL_ID ?? '', 18 | tokenUse: tokenUse, 19 | clientId: process.env.CLIENT_ID ?? '', 20 | }); 21 | 22 | const token = req.headers.authorization?.split(' ')[1]; 23 | 24 | if (!token) { 25 | throw new ErrorObject('No token provided', 401, 'No token provided'); 26 | } 27 | 28 | await verifier.verify(token); 29 | 30 | next(); 31 | return; 32 | } catch (err) { 33 | if (err instanceof Error) { 34 | next( 35 | new ErrorObject( 36 | `Error in verifyJWT middleware: ${err.message}`, 37 | 500, 38 | 'Error in verifyJWT middleware', 39 | ), 40 | ); 41 | } else { 42 | next(new ErrorObject('the error', 500, 'the error')); 43 | } 44 | } 45 | })(); 46 | }, 47 | 48 | getIdentityID: (req, res, next) => { 49 | void (async () => { 50 | try { 51 | const authHeaders: string = req.headers.authorization ?? ''; 52 | if (!authHeaders.startsWith('Bearer ')) return res.status(401).json('No Access Token'); 53 | 54 | const idToken = req.headers['id-token'] as string | undefined; 55 | if (!idToken) return res.status(401).json('No ID Token'); 56 | 57 | const cognitoIdentityClient = new CognitoIdentityClient({ 58 | region: process.env.REGION ?? '', 59 | }); 60 | 61 | const getIdResponse = await cognitoIdentityClient.send( 62 | new GetIdCommand({ 63 | IdentityPoolId: process.env.IDENTITY_POOL_ID ?? '', 64 | Logins: { 65 | [`cognito-idp.${process.env.REGION ?? ''}.amazonaws.com/${process.env.USER_POOL_ID ?? ''}`]: 66 | idToken, 67 | }, 68 | }), 69 | ); 70 | 71 | res.locals.IdentityId = getIdResponse.IdentityId; 72 | 73 | next(); 74 | return; 75 | } catch (err) { 76 | if (err instanceof Error) { 77 | throw new ErrorObject( 78 | `Error in getIdentityID middleware: ${err.message}`, 79 | 500, 80 | 'Error in getIdentityID middleware', 81 | ); 82 | } else { 83 | throw new ErrorObject('the error', 500, 'the error'); 84 | } 85 | } 86 | })(); 87 | }, 88 | 89 | getTemporaryCredentials: (req, res, next) => { 90 | void (async () => { 91 | try { 92 | const IdentityId = res.locals.IdentityId as string; 93 | const idToken = req.headers['id-token'] as string | undefined; 94 | 95 | if (!idToken) return res.status(401).json('No ID Token'); 96 | 97 | const cognitoIdentityClient = new CognitoIdentityClient({ region: process.env.REGION }); 98 | const input = { 99 | IdentityId: IdentityId, 100 | Logins: { 101 | // The key should match the provider name you used when setting up the identity pool 102 | // The value is the id token you received during authentication (NOT the access token) 103 | [`cognito-idp.${process.env.REGION ?? ''}.amazonaws.com/${process.env.USER_POOL_ID ?? ''}`]: 104 | idToken, 105 | }, 106 | }; 107 | 108 | const command = new GetCredentialsForIdentityCommand(input); 109 | const { Credentials } = await cognitoIdentityClient.send(command); 110 | 111 | if (!Credentials) { 112 | throw new Error('No credentials returned from Cognito'); 113 | } 114 | 115 | res.locals.credentials = { 116 | accessKeyId: Credentials?.AccessKeyId, 117 | secretAccessKey: Credentials?.SecretKey, 118 | sessionToken: Credentials?.SessionToken, 119 | }; 120 | 121 | next(); 122 | return; 123 | } catch (err) { 124 | if (err instanceof Error) { 125 | throw new ErrorObject( 126 | `Error in getTemporaryCredentials middleware: ${err.message}`, 127 | 500, 128 | 'Error in getTemporaryCredentials middleware', 129 | ); 130 | } else { 131 | throw new ErrorObject('the error', 500, 'the error'); 132 | } 133 | } 134 | })(); 135 | }, 136 | }; 137 | 138 | export default authController; 139 | -------------------------------------------------------------------------------- /server/src/controllers/cloudController.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CloudWatchClient, 3 | GetMetricStatisticsCommand, 4 | GetMetricStatisticsCommandInput, 5 | GetMetricStatisticsCommandOutput, 6 | Datapoint, 7 | } from '@aws-sdk/client-cloudwatch'; 8 | import type { cloudController, Datapoints, SanitizedInstances, Results } from '../utils/types.js'; 9 | import ErrorObject from '../utils/ErrorObject.js'; 10 | 11 | const cloudController: cloudController = { 12 | getEC2Metrics: (req, res, next) => { 13 | void (async () => { 14 | try { 15 | const cloudwatch: CloudWatchClient = new CloudWatchClient({ 16 | region: process.env.REGION, 17 | credentials: res.locals.credentials, 18 | }); 19 | 20 | // array of metrics to fetch from AWS 21 | const metricsName: string[] = [ 22 | 'CPUUtilization', 23 | 'DiskReadBytes', 24 | 'DiskWriteBytes', 25 | 'NetworkIn', 26 | 'NetworkOut', 27 | 'StatusCheckFailed', 28 | 'StatusCheckFailed_Instance', 29 | 'StatusCheckFailed_System', 30 | ]; 31 | 32 | const results: Results = {}; 33 | 34 | // take res.locals.instances object and store to allInstances 35 | const allInstances: SanitizedInstances[] = res.locals.instances as SanitizedInstances[]; 36 | 37 | // declare start and end time with Date method for 24 hour period 38 | const startTime: Date = new Date(new Date().getTime() - 24 * 60 * 60 * 1000); 39 | const endTime: Date = new Date(); 40 | 41 | // create a promises array to be sent with Promise.all 42 | const promises: Promise[] = []; 43 | 44 | // use for of loops to get metrics for each instance 45 | for (const instance of allInstances) { 46 | for (const metric of metricsName) { 47 | const params: GetMetricStatisticsCommandInput = { 48 | Namespace: 'AWS/EC2', 49 | MetricName: metric, 50 | Dimensions: [{ Name: 'InstanceId', Value: instance.InstanceId }], 51 | StartTime: startTime, 52 | EndTime: endTime, 53 | Period: 3600, // Data points in seconds // need to return back to 3600 54 | Statistics: 55 | metric === 'StatusCheckFailed' || 56 | metric === 'StatusCheckFailed_Instance' || 57 | metric === 'StatusCheckFailed_System' 58 | ? ['Sum'] 59 | : ['Average'], 60 | }; 61 | const instanceId: string = instance.InstanceId; 62 | const command: GetMetricStatisticsCommand = new GetMetricStatisticsCommand(params); 63 | const promise: Promise = cloudwatch 64 | .send(command) 65 | .then((data: GetMetricStatisticsCommandOutput) => { 66 | // checks if key exists in results already, if not then assign its value as [] 67 | if (!Object.hasOwn(results, instanceId)) results[instanceId] = []; 68 | const name: string = instance.Name; 69 | const sumAvg: string = 70 | metric === 'StatusCheckFailed' || 71 | metric === 'StatusCheckFailed_Instance' || 72 | metric === 'StatusCheckFailed_System' 73 | ? 'Sum' 74 | : 'Average'; 75 | const unit: string = 76 | data.Datapoints && data.Datapoints.length > 0 77 | ? sumAvg + ' ' + (data.Datapoints[0].Unit ?? '') 78 | : 'no data'; 79 | const datapoints: Datapoints[] = (data.Datapoints ?? []) 80 | .map((datapoint: Datapoint) => ({ 81 | Timestamp: new Date(datapoint.Timestamp ?? new Date()), 82 | Value: sumAvg === 'Sum' ? datapoint.Sum ?? 0 : datapoint.Average ?? 0, 83 | })) 84 | .sort( 85 | (a, b) => new Date(a.Timestamp).getTime() - new Date(b.Timestamp).getTime(), 86 | ); 87 | results[instanceId].push({ 88 | name, 89 | metric, 90 | unit, 91 | datapoints, 92 | }); 93 | }); 94 | promises.push(promise); 95 | } 96 | } 97 | 98 | // send all promises at the same time 99 | await Promise.all(promises).then(() => { 100 | res.locals.metrics = results; 101 | }); 102 | 103 | next(); 104 | return; 105 | } catch (err) { 106 | if (err instanceof Error) { 107 | next( 108 | new ErrorObject( 109 | `Error in getMetrics middleware: ${err.message}`, 110 | 500, 111 | 'Error in getMetrics middleware', 112 | ), 113 | ); 114 | } else { 115 | next(new ErrorObject('the error', 500, 'the error')); 116 | } 117 | return; 118 | } 119 | })(); 120 | }, 121 | }; 122 | 123 | export default cloudController; 124 | -------------------------------------------------------------------------------- /server/src/controllers/ec2Controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | EC2Client, 3 | DescribeInstancesCommand, 4 | DescribeInstancesCommandOutput, 5 | Instance, 6 | Reservation, 7 | } from '@aws-sdk/client-ec2'; 8 | import type { ec2Controller, SanitizedInstances } from '../utils/types.js'; 9 | import ErrorObject from '../utils/ErrorObject.js'; 10 | 11 | const ec2Controller: ec2Controller = { 12 | getEC2Instances: (req, res, next) => { 13 | void (async () => { 14 | try { 15 | const ec2: EC2Client = new EC2Client({ 16 | region: process.env.REGION, 17 | credentials: res.locals.credentials, 18 | }); 19 | 20 | const command: DescribeInstancesCommand = new DescribeInstancesCommand({}); 21 | 22 | const data: DescribeInstancesCommandOutput = await ec2.send(command); 23 | 24 | if (!data.Reservations) { 25 | next(new ErrorObject('no reservation found', 500, 'no reservation found')); 26 | return; 27 | } 28 | 29 | // flatten data 30 | const flattedReservation: Instance[] = data.Reservations.map( 31 | (r: Reservation) => r.Instances, 32 | ) 33 | .flat() 34 | .filter((instance: Instance | undefined) => instance !== undefined) as Instance[]; 35 | 36 | // map and sanitize flattedReservation array 37 | const sanitizedInstances: SanitizedInstances[] = flattedReservation.map( 38 | (instance: Instance): SanitizedInstances => { 39 | const nameTag = instance.Tags?.find((tag) => tag.Key === 'Name'); 40 | return { 41 | InstanceId: instance.InstanceId ?? '', 42 | InstanceType: instance.InstanceType ?? '', 43 | Name: nameTag?.Value ?? '', 44 | State: instance.State?.Name ?? '', 45 | }; 46 | }, 47 | ); 48 | 49 | res.locals.instances = sanitizedInstances; 50 | 51 | next(); 52 | return; 53 | } catch (err) { 54 | if (err instanceof Error) { 55 | next( 56 | new ErrorObject(`The Error: ${err.message}`, 500, 'Error in EC2Instances middleware'), 57 | ); 58 | } else { 59 | next(new ErrorObject('the error', 500, 'the error')); 60 | } 61 | return; 62 | } 63 | })(); 64 | }, 65 | }; 66 | 67 | export default ec2Controller; 68 | -------------------------------------------------------------------------------- /server/src/routers/router.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from 'express'; 2 | import authController from '../controllers/authController.js'; 3 | import ec2Controller from '../controllers/ec2Controller.js'; 4 | import cloudController from '../controllers/cloudController.js'; 5 | 6 | const router = express.Router(); 7 | 8 | router.get( 9 | '/ec2', 10 | authController.verifyJWT, 11 | authController.getIdentityID, 12 | authController.getTemporaryCredentials, 13 | ec2Controller.getEC2Instances, 14 | (req: Request, res: Response) => res.status(200).send(res.locals.instances), 15 | ); 16 | 17 | router.get( 18 | '/stats', 19 | authController.verifyJWT, 20 | authController.getIdentityID, 21 | authController.getTemporaryCredentials, 22 | ec2Controller.getEC2Instances, 23 | cloudController.getEC2Metrics, 24 | (req: Request, res: Response) => res.status(200).send(res.locals.metrics), 25 | ); 26 | 27 | export default router; 28 | -------------------------------------------------------------------------------- /server/src/server.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import path from 'path'; 3 | import cors from 'cors'; 4 | import 'dotenv/config'; 5 | import router from './routers/router.js'; 6 | import { fileURLToPath } from 'url'; 7 | import { ErrorHandler } from './utils/ErrorHandler.js'; 8 | import favicon from 'serve-favicon'; 9 | 10 | const __filename = fileURLToPath(import.meta.url); 11 | const __dirname = path.dirname(__filename); 12 | 13 | const app = express(); 14 | 15 | const PORT = process.env.NODE_ENV === 'production' ? process.env.PROD_PORT : 8080; 16 | 17 | app.use(cors()); 18 | app.use(express.json()); 19 | 20 | app.use(favicon(path.join(__dirname, '../../client/dist/favicon.ico'))); 21 | 22 | app.use(express.static(path.join(__dirname, '../../client/dist'))); 23 | 24 | app.use('/api', router); 25 | 26 | // Catch All Handler 27 | app.use('*', (req, res) => { 28 | res.sendFile(path.join(__dirname, '../../client/dist/index.html')); 29 | }); 30 | 31 | // Global Error Handler 32 | app.use(ErrorHandler); 33 | 34 | app.listen(PORT, () => { 35 | console.log(`Server: Listening on PORT ${PORT}`); 36 | }); 37 | 38 | export default app; 39 | -------------------------------------------------------------------------------- /server/src/utils/ErrorHandler.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | import ErrorObject from './ErrorObject.js'; 3 | 4 | export const ErrorHandler = (err: Error, req: Request, res: Response, next: NextFunction) => { 5 | const defaultErr = { 6 | date: new Date().toLocaleString(), 7 | log: `Express error handler caught unknown middleware error. ${err.message} `, 8 | status: 500, 9 | message: { err: 'An error occured' }, 10 | }; 11 | if (err instanceof ErrorObject) { 12 | res 13 | .status(err.status) 14 | .json({ date: defaultErr.date, status: err.status, message: err.message, stack: err.stack }); 15 | } else { 16 | console.error(`${defaultErr.date}: ${defaultErr.log}`); 17 | res 18 | .status(defaultErr.status) 19 | .json({ date: defaultErr.date, status: defaultErr.status, message: defaultErr.message }); 20 | } 21 | next(); 22 | }; 23 | 24 | export default ErrorHandler; 25 | -------------------------------------------------------------------------------- /server/src/utils/ErrorObject.ts: -------------------------------------------------------------------------------- 1 | class ErrorObject extends Error { 2 | log: string; 3 | status: number; 4 | message: string; 5 | isOperational: boolean; 6 | constructor(log: string, status: number, message: string) { 7 | super(message); 8 | this.log = log; 9 | this.status = status; 10 | this.message = message; 11 | this.isOperational = true; // Flag to denote operational, trusted error: send response to client 12 | 13 | Error.captureStackTrace(this, this.constructor); // ensure the stack trace includes the ErrorObject constructor 14 | } 15 | } 16 | 17 | export default ErrorObject; 18 | -------------------------------------------------------------------------------- /server/src/utils/types.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express'; 2 | 3 | export interface ec2Controller { 4 | getEC2Instances: (req: Request, res: Response, next: NextFunction) => void; 5 | } 6 | 7 | export interface authController { 8 | verifyJWT: (req: Request, res: Response, next: NextFunction) => void; 9 | getIdentityID: (req: Request, res: Response, next: NextFunction) => void; 10 | getTemporaryCredentials: (req: Request, res: Response, next: NextFunction) => void; 11 | } 12 | 13 | export interface cloudController { 14 | getEC2Metrics: (req: Request, res: Response, next: NextFunction) => void; 15 | } 16 | 17 | export interface SanitizedInstances { 18 | InstanceId: string; 19 | InstanceType: string; 20 | Name: string; 21 | State: string; 22 | } 23 | 24 | export type Results = Record< 25 | string, 26 | { 27 | name: string; 28 | metric: string; 29 | unit: string; 30 | datapoints: { Timestamp: Date; Value: number }[]; 31 | }[] 32 | >; 33 | 34 | export interface Datapoints { 35 | Timestamp: Date; 36 | Value: number; 37 | } 38 | -------------------------------------------------------------------------------- /server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "target": "ES6", 5 | "outDir": "./dist", 6 | "rootDir": "./src", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "moduleResolution": "node", 11 | "typeRoots": ["./types", "./node_modules/@types"], 12 | "lib": ["dom", "dom.iterable", "esnext"] 13 | }, 14 | "include": ["src/**/*", "types/*"], 15 | "exclude": ["node_modules"] 16 | } 17 | -------------------------------------------------------------------------------- /template.env: -------------------------------------------------------------------------------- 1 | USER_POOL_ID= 2 | IDENTITY_POOL_ID= 3 | CLIENT_ID= 4 | REGION= 5 | NODE_ENV= 6 | PROD_PORT= 7 | --------------------------------------------------------------------------------