├── .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 |
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 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | Table of Contents
40 |
41 | Introduction
42 | Built With
43 | Usage
44 | Installation
45 | Contributing
46 | License
47 | Creators
48 | Contact Us
49 | Acknowledgements
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,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkGBxASEhUSEhIVFRUVFRUVFRUVGBcVFRUXFRcXFxcVFRUYHSggGBolHRcXITEiJSkrLi4uGB8zODMtNygtLisBCgoKDg0OGhAQGi0lICUtLS0tLy8tLS0tLS0tLS8tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLf/AABEIAOEA4QMBEQACEQEDEQH/xAAbAAEBAAIDAQAAAAAAAAAAAAAAAQUGAwQHAv/EAD8QAAIBAgIDCwoFBAMBAAAAAAABAgMRBAYhMZIFEhY0QVFTcrHB0QcTMjNxc4GRoeEiYWOy8BRCUoIjJMKi/8QAGgEBAAIDAQAAAAAAAAAAAAAAAAEFAwQGAv/EADQRAQABAgMECQQCAgIDAAAAAAABAgMEBREVUnGREiExMjNhgcHhNEFRoROxFNEiQmJy8P/aAAwDAQACEQMRAD8A7Jwzs1AoAgUABSAApAoAgW4FIC4FAtyBbgCAAAAAAAAAAAAADqGwAFIFAECgCBQAFIFApAICgCBQKQKAIAAAAAAAAAAAAdM2RSAApAoAgAKQKBSAAoFIACgCBQLcgUAQAAAAAAAAAAB0jZFApAAUgUgAKAIFAAUgUAQKBQBAoFIACkAAAAAAAAAA6KNoUgUAQKQKAApAAUgVAVACBmcFlrFVYKaiknpW+e9bXPY37WWYi5T0ojTi07mPsW6ujM9fk5+CGL5obX2MmyMR5c/hj2nY8+S8EcXzQ2vsNj4jy5/BtOx58jgji+aG19hsfEeXP4Np2PPkvBHF80Nr7DY+I8ufwbTsefI4I4vmhtfYjY+J8ufwbTsefI4JYvmhtfYbHxHlz+Dadjz5LwSxXNDa+w2PifLn8G07HnyOCeK5obX2I2NifLn8G07HnyOCeK5obX2GxsT5c/g2nY8+TD4ihOnJwnFxktaZW3Ldduqaa40mG9RXTXT0qZ1hxHh6AAAAB0TaFAECgUgAKQAFIACgUgc2EinUgnpTnFNPlTaMlmIm5TE/mP7eLk6UTMfiXpu7u6Dw9F1FFSs0rPRrdjrcZiJw9qa4jXscxhbEX7nQmdGtcN6nQx2mVG269yOfwtNk0b08jhvPoY7T8CNt17kc/hGyaN6eS8N6nQx2n4Dbde5HP4Nk0b08jhtU6KO0/Abbr3I5/BsmjenkcNqnQx2n4Dbde5HP4Nk0b08jhrU6KO0/Abcr3I5/BsmjenkvDWp0Udp+A25XuRz+DZNG9PI4aVOijtPwI25XuRz+DZNG9PJkNwsyTxFXzbpxit63dNvVY3MFmdWIu9CadOrXta2LwFNm304q1Y7PkV5ym7aXB3fPZ6O1mlnkR06J8pbWUzPQqjzauUa2AAAAB0EbYpAAUgUCogAKQAFIFAEDnwPrafXh+5GWz4lPGP7eLvcq4T/T0DO3FZdaPadJm3008Y/tz+WePHCXnRyro1AECgUAQKBSBnslcZXUl3Frk/1PpPsr8z8D1h3M++spdWXajPnneo4T7MOU9yrjDViiWwAAAAMebYpAoFAECgUgAKQABMgUD7pVN61Ja0017U7k01dGYmPsiqnWJiWV3RzFiK8HTqOG9bT0Rtq/O5t38wvXqOhXppwatnBWrVXSp11YlM0W2twKmAuQKBQBAoGfyVxldSXcWmT/AFPpPsr8z8D1h3M/espdWXajYzzv0cJ9mHKe5VxhqxQrYAAAAGPRuCkABSBQBAoFIADnwPrKfXh+5Huz4lPGP7eLvcq4S37O1CCwsmoxT30NKST1nRZpTEYedI+8f2oMtqqm/Gs/aWnZaSeKop2f4tT9jKTARriKOPsucZOlirT8Nlz/AEoxp0t7FL8b1JL+1lpnFMRbp0j7+ytyqqZrq1n7MPkmCeJSaTW8nrV+Y0cpiJxHX+J9m5mUzFjq/MO7n6nGNSlvUl+GWpJcqM+cxEVUafifZhyqqZoq1/MGQacZVKu+Sf4Y61flfOMmiJqr1/EGa1TFFOn5l1M6QSxLSSS3kNStzmvm0RGI6vxHuzZbMzY6/wAyy+QqUZU6l4p/jWtJ/wBpv5NTE26tY+/s081qmK6dJ+zXcxxSxVVJWW+1LVqRUZhGmJr4+0LLBzrYp1/DHI02yoAgZ/JPGV1JdxaZP9T6T7K/M/A9YdzP3rKXVl2o2M879HCfZgynuVcYasUS3UgAAADHG4KBSAuBSBf52ACBQOdYSr0c9mXge/4bm7PKXj+Wjejm3TKmW6apqtWjeUtMYvQoLQ1df5XReYDAU00xcuRrP9fKlx2Oqmqbduer+/hseK/p6sd5UcJR0Ozato1cpZ1xbuR0atJhX0fy256VOsS6tDc/BQkpwjSjJaU01dfUxUWMPRPSpiIlkqvYiqOjVM6OfGU8NVSVTzc0ndJtaH8zJcptXI0r0l4t1Xrc60aw48Lg8HSlvqcaUZWtdNXs/ieLdmxbnWiIiXqu7fuR0apmYfWMw+FqtOoqc2tCu07fUm5bs3e/ESi3Xet9VGsGDw+FpNumqcG9Ds0r/UW7dm13IiC5XeudVesvnFYPB1Jb6oqUpWtdtX0fEi5Zw9yda4iZTRdv240pmYhy4OnhqSap+bgm7tJrS/merdNm3GlGkPNyq9cnWvWWLzBl6lWjKrT0VLb66f4Z2/yXLo5V+Rp43L7d+ma6e9/fFtYTG12qooq7v9NFWEq9HPZl4HNfwXd2eUr/APlo3o5w4TFMPagZ/JPGf9JdxZ5P9T6T7K/M/A9YdzP3rKXVl2o2M879HCfZgynuVcYasUS3AKQAADHG4AFApAAUgUAyJHouHzfhFGKcp3UUn+B8iOlpzPDxERrPKXPVZbfmqZ0jmzP9TGrQdSF97KDavodrPkN2K4rt9KnsmGl0Jt3ejV2xLyJI4vR162GgtiNELYaBYjRJYaC2GiFsQPUMvTSwlJvUqab+B2OCnTDUT5OWxca4iqPN1Fm/Cf5S2WYNq4b8zylm2ZiPxHNoOKmpTnJanKTXsbbX0OYu1RVXVVH3mXQ0RpTET+HGY3psGSOM/wCku4tMo+p9J9lfmfgesO5n/wBZS6su1GfPO9RwlgynuVcYaqUa3UgUAQAGONwAFwKgKQCApAXIGUhl/GNJqhOz0rV4m3/g4if+k/prTjLEf94eg7l0ZQwcYTVpRpNNPkdmdFYomjDxTV2xDn71UVYiaqezV5ZE5F1ShCkCoAQAFIFA9N3Di3gqaWt0rfRnX4SNcLT/AOrl8TOmJq4tFWX8Z0Evp4nN7PxO5P6X/wDm4ffhj5RabT1ptNfmjUmJidJbMTrGsIeRsGSOM/6S7i0yj6n0n2V+Z+B6w7mf/WUurLtRnzvvUcJ9mDKe5VxhqpRrdQKiBQBAxxuAAAAUgc8cJVaTVObT0pqMmnflWg9xarnrimeUvE3KI6pmOa/0dXoqmxLwJ/hubs8pP5aN6OcOOrTlHRJOL5mmnp/JmOqmY6pjR6pqieuJbvQzzSjGMfMz0JLXHkVucvac3txER0Z/SlqyquZmelH7bJSxarYfzqTSnTcknrWhllTci5a6cfeFdVbm3e6E/aXkaONh1r6CACkCgCAApA9Qy/Pe4OlLmp3+R2GDnTDUT5OWxca4iqPNiFnil0M/nHxNHbVrdn9NzZNe9H7aZXqb6cpL+6Tfzdzn7lXSrmr8zK7op6NMQ+Dw9NgyRxn/AEl3FnlH1PpPsr8z8D1h3PKB6yl1ZdqNjO+/Rwn2Ycp7lXGGqlGthECgUgWxAxxuAAAAZvJuDhVxUYzV0lKdnqbjquubTf4G7l9qm5fiKvt1tPH3KrdiZp4PQN1t3KGGcVVclvk2rRb1ewvr+KtWJiK57VDYwty/EzR9nQ4aYP8AynsMwbTw/wCZ5Sz7MxH4jm03NW6NOvXdSm247yK0q2lXvoKbH3qL13pUdmi4wNmq1a6NXbq7NPJ2LaTShZpNfj5/gZIyu/Ma9XNjnMrETp18m8YDDSpYRU5230aTTtpWhPUXtq3NuxFE9sQpLtyLl+aqeyZeUI5CHVqEKQAFIACkAB6fuFBvBU0tbpWXxTOuwka4WmP/ABcvip0xNU+bUVk/F80Nr7FHsjEeXP4XG07HnyYKpBxbi9abT9quitqpmmqYn7N+mdYiYQ8pbBkfjP8ApLuLPKPqfSfZX5n4HrDueUD1lLqy7UbGd96jhPsw5T3KuMNVKNbKAIFAtyNBjzbAAAA2LIPG17ufcWOV+P6T7K/M/A9YZDyk+nR6s+1GbN+/RwlhyjuVcYacVC2CJG3Us9VIxUfMw0JL0nyaOYt4zeqI06Ec/hVVZVRM69KeTbsLi3WwqqtWc6blZaldMt7dz+SzFf5hU12/473Qj7S8licf9nWSoQoFRAAUgAKQPUNwJ73B05c1K/yTOvwc6YaifJy+KjXE1R5tcWeanQw2n4FVtqrcjn8LLZFG9PJgdz6CxGIjCT3qqTd2uS93ouVtmiMRfimerpTP+2/dr/hszVHXpDbOA9LpZ/KJc7Ftb0/pU7Wr3Y/bvbjZZhh6nnI1JSdmrNK2n2Gxhcuow9fTpmZYMRj6r1HQmIhgs/VoutCKd3GD3y5t81Yrc6rpm5TTHbEdfq38ppmLdUz95auUq1UCgCBf5yEDoG2AAABsWQeNr3c+4scr8f0n2V+Z+B6wyHlJ9Oj1Z9qM2b9+jhLDlHcq4w00qFspEjbqWRasoqXnoaUn6L5dPOW8ZRVMa9OOXyqqs1oidOjPNt2Fwjo4VUm03Cm43WhOyZb27f8AHZij8Qqa7n8l7px95eSROPh1j6JQEC/zxApAAUgAPUdwIb7B0489K3zTOuwka4aiPJy+KnTE1T5tcWRanTQ2X4lVsWrfjl8rLa9G7PNrSnKlUvGVpQk0pLnTauip1qtXP+M9cTKz0puUdcdUw7qzDjOnn9PAz7QxO/P6/wBMH+Fh9yDhBi+nn9PAbQxO/P6/0f4WH3IY6cm222227tvS2+dvlNOZmZ1lsxERGkCISECgUB/P5pIHRNoAAADYsg8bXu59xY5X4/pPsr8z8D1hkPKT6dHqz7UZs379HCWHKO5VxhppULYA2GnnLGJJJwskkvwc3xLCMzvxGnVy+WhOW2JnXr5t5wGKlVwiqTtvpUm3bQr2epF5arm5Yiqe2YUl2iLd+aaeyJeTxOR+zq1QQAUgUCkABSB6huDNrBU2tapXXwTOuwk6YamfJy+KjXE1R5tQWcsXzw2fuUe1sR5cvlcbMsefNgqk3JuT1ttv2t3K2qqapmZ+7fpjSIiHyeUqAApAoAgUAB0jZAAAA2LIPG17ufcWOV+P6T7K/M/A9YZDyk+nR6s+1GbN+/RwlhyjuVcYaaVC2Rgb5QyLSlGMvPVNKT1R5VcvYym3Ma9Kf0pKs1riZjox+2yUsIqOG80m2oU3FN63ZMsabcW7XQj7Qrqrk3L3Tn7y8hjqOQh1sqEKQKAuQKAIFA9Sy9DfYOlHnp2+aOtwca4aiPJy+LnTEVT5sQsi0umn8omlsa1vT+m3tavdj9tKxEN7OUVp3spL22diguU9GqafxMruirpUxP5cZ4elIFuAApAtwBAoHSNkAAADYsg8bXu59xY5X4/pPsr8z8D1hkPKT6dHqz7UZs379HCWHKO5VxhppULYAyccxYxJJV52WhavA2oxuIj/ALz+mtODsT/0huuVcwQr0lTrTXnVeLUtG/XI1yPRrX5ai4wWMpu0dCuf+X9qfG4Oq1X06I/4/wBMhPcDApXdGmlztWRsTg8PHbRDXjF4ieyqXxHcXc9uypUW+ZW8SIwuG3YTOKxMdtUvupuFgY+lRpL22RM4TDx20QRi8RPZVL5huLgHoVKk3zKz7yIwmGnspgnFYmO2qVqbh4GPpUaS9tkJwmGjtogjFYmeyqSnuJgZejSpP2WYjCYaeyiCcViY7apJ7i4BOzpUk+Z2QnCYaO2mCMViZ7KpWnuHgZejRpP2WYjB4aeyiETi8RHbVLj3a3Vo4Si4wcVK29pwWmz1aUtSV76TxisRRhrWlPb9oe8Nh68Rc1q7PvLReEWM6ef/AM+BQf5+J35/S8/wsPuQx05Ntt6W3d/m2akzMzrLZiIiNIQ8pAKQKAApAqAfHsA6ZsAAAAbFkHja93PuLHK/H9J9lfmfgesMh5SfTo9WfajNm/fo4Sw5R3KuMNNKhbAADnwHrafvIfuRks+JTxj+3i73KuE/09Fz7xSXXh+46DM/p59P7c/ln1EcJaRlVL+ro9f/AMspcD9RRx9l1jfp6+DavKP6ql7x/tZZ5v4dPH2VeUd+rh7sJkJf9te7n3GllX1HpPs3cz8D1h3vKP6yj1JdqM2cd6jhPsw5R3KuMHk4X/JW6ke1jJ+9Xwj3M37lPGXSz4v+2/dw7zBmv1HpHuzZZ4HrLN+Tpf8AFV94v2o3sn8Orj7NLN+/Tw92sZn43W6/cipx/wBRXx9lngvp6ODGGo2lIACkCgCBbgAKQL/NRA6ZsgAAAbFkHja93PuLHK/H9J9lfmfgesMh5SfTo9WfajNm/fo4Sw5R3KuMNNKhbAAAmBy1MTUkrSnNrmcm18mz3VcrqjSZmfV5iimOuIjk44Tad02mtTWhr2M8xMxOsPUxE9UuSpXnL0pyl1pN/K7Jqrqq70zPq8xRTT2RolOpKLvFuL507P5o8xVNM6xOiZiJ6ph3cNgMViFvoRnUUXa972eu2lmai1evxrTE1aef+2Gu7Zs9VUxGrgmqtGTg3KElokk2n7HYxz/Jaqmnrifv/wDQyR0LlMT1TDjnUlJ3k23zttv6niqqap1mdXqKYjqiH1Srzj6MpR5fwtrsJprqp7szHqiqimrtjV8ym27ttvlbd2/izxMzM6y9RER1QIgAKQAFIACkCoAAA6pnAAAA2LIPG17ufcWOV+P6T7K/M/A9YZDyk+nR6s+1GbN+/RwlhyjuVcYaaVC2AAAAAAAUgb/5OfU1Pef+UX2Udyrj7KLNu/Tw92rZp43W6/cirx31FfH2hZ4L6ejgxZqNpQBAoAgUCkABSAApAqAgHWM4AAAGxZB42vdz7ixyvx/SfZX5n4HrDY847hVsTKm6W9tFST3ztra/I38wwly/VTNGnU0MBi7dmmqK/u17gTjP09r7FfszEeXNv7TsefI4E4z9Pa+w2ZiPLmbTsefI4E4z9Pa+w2ZiPLmbTsefI4E4z9Pa+w2ZiPLmbTsefI4E4z9Pa+w2ZiPLmbTsefI4E4z9Pa+w2ZiPLmbTsefI4E4z9Pa+w2ZiPLmbTsefI4E4z9Pa+w2XiPLn8G07HnybXlDcmrhqc41N7eU7reu+iyRaZfhq7FNUV/eVXj8RReqiaPtDRs0P/t1uv3Ipcd9RXx9oXWC+no4MWajaUgUAQKAIFApAoAgAKQAHWM4AAAHLhcTOnONSD3sou6f81o9UV1UVRVT2w810U10zTV2S2qGfq1lejBvld2r/AA5C1jN69OumOarnKaNeqqV4fVeghtPwG16tyOfwbIo3p5HD6r0ENp+A2vVuRz+DZFG9PI4fVeghtPwG16tyOfwbIo3p5HD6r0ENp+A2vVuRz+DZFG9PI4fVeghtPwG16tyOfwbIo3p5HD6r0ENp+A2vVuRz+DZFG9PI4fVeghtPwG16tyOfwbIo3p5HD6r0ENp+A2vVuRz+DZFG9PJx18+V3FqNKEW9UruVvzszzXm1yY0ppiJ5ppym3E6zVMtWnNybk3dtttvW29LbKqZmZ1laxERGkIQAFIFAECgUgAKQKAIACgdYzAAAAAAAAAAAAAAAAAXAoFIC4FIFAECgCBSBQKARAoHWMwAAAAAAAAAAAAAAAAAACogVAAKQKAIFApAAUgAPogdYzAAAAAAAAAAAAAAAAAAAAFApAAUgUAQKBSAApAXGg4DKAAAAAAAAAAAAAAAAAAAAAKBSAApAqAIgUCkAB9XPI65mAAAAAAAAAAAAAAAAAAAAABAAPrxIAC8v8/Mj7CoiRV4CQAq/n0IH0eR//9k=';
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 |
16 | Get Started
17 |
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 |
73 | Logout
74 |
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 |
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 |
Select Metric:
59 |
60 |
61 | Select a metric
62 |
63 | {sortedMetrics.map((metric, index) => (
64 |
65 | {metric}
66 |
67 | ))}
68 |
69 |
70 | Toggle to {chartType === 'bar' ? 'Line Chart' : 'Bar Chart'}
71 |
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 |
62 | {isAuthenticated ? (
63 |
64 |
65 | SkyScraper
66 |
67 | ) : (
68 |
69 |
70 |
SkyScraper
71 |
72 | )}
73 |
74 | {isAuthenticated && (
75 |
83 | )}
84 | {isAuthenticated &&
}
85 |
86 |
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 |
--------------------------------------------------------------------------------