├── .env.enc
├── .github
└── workflows
│ ├── ci.yaml
│ └── deploy.yaml
├── .gitignore
├── .vscode
├── settings 2.json
└── settings.json
├── Dockerfile
├── README.md
├── client
├── .babelrc
├── .gitignore
├── dist
│ ├── 1db0c662795055d2b321.png
│ ├── 30b5ef32585e7cbb326b.png
│ ├── b01898446ba2e22f2efd.png
│ ├── bundle.js
│ ├── c66f66b5e7cba717823d.png
│ ├── f4fd54124ab0c5aa72cd.png
│ ├── f91efdf0cfa77fa4cadf.png
│ ├── fbe689dffdd47f67ae34.png
│ └── index.html
├── jest.config.js
├── jsconfig.json
├── package-lock.json
├── package.json
├── public
│ └── index.html
├── src
│ ├── App.js
│ ├── assets
│ │ ├── aria.png
│ │ ├── dashboard.gif
│ │ ├── example.gif
│ │ ├── g.jpg
│ │ ├── gh2.jpg
│ │ ├── google.png
│ │ ├── grace.png
│ │ ├── logingoogle.png
│ │ ├── logo.png
│ │ ├── nobglogo.png
│ │ ├── ploy.png
│ │ ├── profile.png
│ │ ├── signupgithub.png
│ │ ├── signupgoogle.png
│ │ └── will.png
│ ├── components
│ │ ├── Alerts.jsx
│ │ ├── Alerts.test.jsx
│ │ ├── Export.jsx
│ │ ├── FlexBetween.jsx
│ │ ├── FlexBetween.test.jsx
│ │ ├── LineChart.jsx
│ │ ├── Navbar.jsx
│ │ ├── PieChart.jsx
│ │ ├── Service.jsx
│ │ ├── Setting.jsx
│ │ ├── Sidebar.jsx
│ │ ├── StatusCard.jsx
│ │ ├── __snapshots__
│ │ │ └── footer.test.jsx.snap
│ │ ├── accSection.jsx
│ │ ├── accountDetails.jsx
│ │ ├── breadcrumbs.jsx
│ │ ├── clusterDetails.jsx
│ │ ├── feedback.jsx
│ │ ├── feedback.test.jsx
│ │ ├── fileMock.js
│ │ ├── footer.jsx
│ │ ├── footer.test.jsx
│ │ ├── home.jsx
│ │ ├── info.jsx
│ │ ├── login.jsx
│ │ └── mockStore.js
│ ├── index.css
│ ├── index.js
│ ├── pages
│ │ ├── ClusterMetrics.jsx
│ │ ├── Clusters2.jsx
│ │ ├── Dashboard.jsx
│ │ ├── Layout.jsx
│ │ ├── LogsNotification.jsx
│ │ ├── Overview.jsx
│ │ ├── UserProfile.jsx
│ │ ├── accounts.jsx
│ │ ├── clusters.jsx
│ │ ├── credentials.jsx
│ │ ├── getstarted.jsx
│ │ ├── home.jsx
│ │ ├── login.jsx
│ │ ├── signup.jsx
│ │ └── team.jsx
│ ├── redux
│ │ ├── globalSlice.js
│ │ ├── notificationSlice.js
│ │ ├── rootReducer.js
│ │ ├── store.js
│ │ ├── userSlice.js
│ │ └── wsContext.js
│ ├── theme.js
│ └── webService
│ │ ├── connectWebSocketToLineChart.js
│ │ ├── connectWebSocketToNotifications.js
│ │ └── connectWebSocketToPieChart.js
└── webpack.config.js
├── docker-compose.yml
├── jest.backend.config.js
├── jest.config.js
├── package-lock.json
├── package.json
└── server
├── controllers
├── credentialsController.js
├── listController.js
├── metricController.js
├── notificationController.js
├── redisClient.js
└── userController.js
├── createDatabase.js
├── database
├── Accounts.db
├── Notifications.db
└── Users.db
├── router
└── listRouter.js
├── server.js
└── tests
├── credentialsController.test.js
├── listController.test.js
├── metricController.test.js
├── notificationController.test.js
└── userController.test.js
/.env.enc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DeClustor/2ed5f0f10f89d1b40c42569bf590be9dabd264aa/.env.enc
--------------------------------------------------------------------------------
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | name: CI Pipeline
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | pull_request:
9 | branches:
10 | - main
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - name: Checkout code
18 | uses: actions/checkout@v2
19 |
20 | - name: Set up Node.js
21 | uses: actions/setup-node@v2
22 | with:
23 | node-version: 18.x
24 |
25 | - name: Clean npm cache and install root dependencies
26 | run: |
27 | npm cache clean --force
28 | rm -rf node_modules package-lock.json
29 | npm install --legacy-peer-deps
30 |
31 | - name: Clean and install client dependencies
32 | run: |
33 | cd client
34 | rm -rf node_modules package-lock.json
35 | npm cache clean --force
36 | npm install --legacy-peer-deps
37 |
38 | - name: Run tests
39 | run: npm test
40 |
41 | - name: Build client
42 | run: cd client && npm run build
43 |
--------------------------------------------------------------------------------
/.github/workflows/deploy.yaml:
--------------------------------------------------------------------------------
1 | name: Deploy Pipeline
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | deploy:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - name: Checkout code
14 | uses: actions/checkout@v2
15 |
16 | - name: Set up Docker Buildx
17 | uses: docker/setup-buildx-action@v2
18 |
19 | - name: Login to Docker Hub
20 | run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
21 |
22 | - name: Build Docker image
23 | run: docker build -t declustorteam/declustor:latest .
24 |
25 | - name: Push Docker image
26 | run: docker push declustorteam/declustor:latest
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | client/node_modules
2 | node_modules
3 | .env
4 | server/database/Credentials.db
5 | server/database/Users.db
6 | .DS_Store
--------------------------------------------------------------------------------
/.vscode/settings 2.json:
--------------------------------------------------------------------------------
1 | {
2 | "git.ignoreLimitWarning": true
3 | }
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "git.ignoreLimitWarning": true
3 | }
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:16
2 | WORKDIR /app
3 | COPY package*.json ./
4 | RUN npm install
5 | COPY server ./server
6 | COPY client ./client
7 | # COPY .env ./
8 | RUN rm -rf client/node_modules client/package-lock.json
9 | RUN npm cache clean --force
10 | RUN cd client && npm install --legacy-peer-deps
11 | EXPOSE 3000
12 | EXPOSE 8080
13 | CMD ["npm", "run", "start"]
14 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | - [What is DeClustor?](#introduce)
4 | - [Features](#key-features)
5 | - [Getting Started](#getstart)
6 | - [Meet the Team](#meet-the-team)
7 |
8 | ## What is DeClustor?
9 |
10 | Managing AWS ECS environments can be challenging due to fragmented metrics and real-time performance monitoring across clusters. AWS's dashboard can be confusing and lacks a unified interface.
11 |
12 | DeClustor offers a centralized solution for seamless ECS monitoring and management, enabling effortless tracking of metrics and real-time performance across multiple services within multiple ECS clusters.
13 |
14 | ## Features
15 |
16 | 1. Centralized Dashboard with easy-to-use account management feature
17 | A very powerful dashboard displays which can present users all the real-time metircs based on the service they choose, and users are enabled to manage their AWS accounts easily, as depicted in this demo
18 |
19 | 
20 |
21 | 2. Logs and data report generation by notification setting
22 | Users are able to customize different types of metrics and set thresholds to monitor their services. They will be noticed once the thresholds are reached.
23 | They can also analyze the sorted logs and export customized reports.
24 |
25 | 
26 |
27 | 3. Task and cluster overview
28 | Users can observe their task data and cluster metrics in detail by easily choosing different accounts, cluster names and services.
29 |
30 | 
31 | 
32 |
33 | 4. Local database intergration
34 | The security of users' credentials is most valued. Therefore, by providing lightweight and self contained data management, Decluster allows users to store their credentials locally.
35 |
36 | 5. Seamless third-party authentication
37 | Users are provided with easy signup and login options through Google and GitHub OAuth, enhancing security and user experience.
38 |
39 | ## Getting Started
40 |
41 | So what are you waiting for? Follow the instructions below to get started!
42 |
43 | # Using GitHub Repository
44 |
45 | 1. **Pull the Docker Image from [DockerHub](https://hub.docker.com/r/declustorteam/declustor):**
46 | ```sh
47 | docker pull declustorteam/declustor
48 | ```
49 | 2. Clone this repository from GitHub
50 | 3. Decrypt the the .env file by using the following commands:
51 | ```yml
52 | openssl enc -aes-256-cbc -d -pbkdf2 -iter 100000 -in .env.enc -out .env -k ilovedeclustor
53 | ```
54 | 4. Run terminal command:
55 | ```
56 | docker-compose up -build
57 | ```
58 | 5. Access the application by opening up your web browser and head over to http://localhost:8080
59 | 6. Sign up to make an account
60 | 7. Use our [Google Docs Instructions](https://docs.google.com/document/d/1Vf7OrThD2bj3LU9Dxm4l7vFzVKmexYBO/edit) to create a IAM User for DeClustor to access your AWS account
61 | 8. Select Account → Clusters → Services
62 |
63 | ## Meet the Team
64 |
65 | | Developed By | GitHub | LinkedIn |
66 | | ------------- | ---------------------------------------- | ---------------------------------------------------- |
67 | | Grace Lo | [GitHub](https://github.com/gracelo0717) | [LinkedIn](https://www.linkedin.com/in/gracelo0717) |
68 | | Will Di | [GitHub](https://github.com/xiudou401) | [LinkedIn](https://www.linkedin.com/in/will-di) |
69 | | Aria Liang | [GitHub](https://github.com/Aria-Liang) | [LinkedIn](https://www.linkedin.com/in/arialiang) |
70 | | Ploynapa Yang | [GitHub](https://github.com/Ploynpk) | [LinkedIn](https://www.linkedin.com/in/ploynapa-py/) |
71 |
--------------------------------------------------------------------------------
/client/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/preset-env", "@babel/preset-react"],
3 | "plugins": ["@babel/plugin-transform-runtime"]
4 | }
5 |
--------------------------------------------------------------------------------
/client/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 |
--------------------------------------------------------------------------------
/client/dist/1db0c662795055d2b321.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DeClustor/2ed5f0f10f89d1b40c42569bf590be9dabd264aa/client/dist/1db0c662795055d2b321.png
--------------------------------------------------------------------------------
/client/dist/30b5ef32585e7cbb326b.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DeClustor/2ed5f0f10f89d1b40c42569bf590be9dabd264aa/client/dist/30b5ef32585e7cbb326b.png
--------------------------------------------------------------------------------
/client/dist/b01898446ba2e22f2efd.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DeClustor/2ed5f0f10f89d1b40c42569bf590be9dabd264aa/client/dist/b01898446ba2e22f2efd.png
--------------------------------------------------------------------------------
/client/dist/c66f66b5e7cba717823d.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DeClustor/2ed5f0f10f89d1b40c42569bf590be9dabd264aa/client/dist/c66f66b5e7cba717823d.png
--------------------------------------------------------------------------------
/client/dist/f4fd54124ab0c5aa72cd.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DeClustor/2ed5f0f10f89d1b40c42569bf590be9dabd264aa/client/dist/f4fd54124ab0c5aa72cd.png
--------------------------------------------------------------------------------
/client/dist/f91efdf0cfa77fa4cadf.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DeClustor/2ed5f0f10f89d1b40c42569bf590be9dabd264aa/client/dist/f91efdf0cfa77fa4cadf.png
--------------------------------------------------------------------------------
/client/dist/fbe689dffdd47f67ae34.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DeClustor/2ed5f0f10f89d1b40c42569bf590be9dabd264aa/client/dist/fbe689dffdd47f67ae34.png
--------------------------------------------------------------------------------
/client/dist/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | DeClustor
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/client/jest.config.js:
--------------------------------------------------------------------------------
1 | // jest.config.js
2 | module.exports = {
3 | setupFilesAfterEnv: ['@testing-library/jest-dom'],
4 | testEnvironment: 'jsdom',
5 | moduleNameMapper: {
6 | '\\.(css|less|scss|sass)$': 'identity-obj-proxy',
7 | '\\.(jpg|jpeg|png|gif|webp|svg)$': './fileMock.js',
8 | },
9 | transform: {
10 | '^.+\\.(js|jsx|ts|tsx)$': 'babel-jest',
11 | },
12 | transformIgnorePatterns: ['/node_modules/(?!d3-.*|@nivo/.*)'],
13 |
14 | moduleFileExtensions: ['js', 'jsx', 'ts', 'tsx', 'json'],
15 | };
16 |
--------------------------------------------------------------------------------
/client/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "src"
4 | },
5 | "include": ["src"]
6 | }
7 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "webpack.config.js",
6 | "scripts": {
7 | "build": "webpack",
8 | "dev": "webpack serve --open",
9 | "test": "jest"
10 | },
11 | "keywords": [],
12 | "author": "",
13 | "license": "ISC",
14 | "dependencies": {
15 | "@emotion/react": "^11.11.4",
16 | "@emotion/styled": "^11.11.5",
17 | "@mui/icons-material": "^5.15.21",
18 | "@mui/material": "^5.16.1",
19 | "@mui/system": "^5.16.1",
20 | "@mui/x-data-grid": "^7.10.0",
21 | "@mui/x-data-grid-pro": "^7.10.0",
22 | "@nivo/bar": "^0.87.0",
23 | "@nivo/core": "^0.87.0",
24 | "@nivo/geo": "^0.87.0",
25 | "@nivo/line": "^0.87.0",
26 | "@nivo/pie": "^0.87.0",
27 | "@reduxjs/toolkit": "^2.2.6",
28 | "assert": "^2.1.0",
29 | "browserify-zlib": "^0.2.0",
30 | "buffer": "^6.0.3",
31 | "crypto-browserify": "^3.12.0",
32 | "d3": "^7.9.0",
33 | "esm": "^3.2.25",
34 | "file-loader": "^6.2.0",
35 | "html-webpack-plugin": "^5.6.0",
36 | "https-browserify": "^1.0.0",
37 | "lodash": "^4.17.21",
38 | "os-browserify": "^0.3.0",
39 | "pkg-dir": "^4.2.0",
40 | "process": "^0.11.10",
41 | "react": "^18.3.1",
42 | "react-datepicker": "^7.2.0",
43 | "react-dom": "^18.3.1",
44 | "react-redux": "^8.1.3",
45 | "react-router-dom": "^6.24.0",
46 | "redux": "^4.0.5",
47 | "redux-persist": "^6.0.0",
48 | "sqlite3": "^5.1.7",
49 | "stream-browserify": "^3.0.0",
50 | "stream-http": "^3.2.0",
51 | "url": "^0.11.3",
52 | "util": "^0.12.5",
53 | "vm-browserify": "^1.1.2"
54 | },
55 | "devDependencies": {
56 | "@babel/cli": "^7.24.7",
57 | "@babel/core": "^7.24.9",
58 | "@babel/plugin-transform-runtime": "^7.24.7",
59 | "@babel/preset-env": "^7.24.8",
60 | "@babel/preset-react": "^7.24.7",
61 | "@react-oauth/google": "^0.12.1",
62 | "@testing-library/dom": "^10.4.0",
63 | "@testing-library/jest-dom": "^6.4.8",
64 | "@testing-library/react": "^16.0.0",
65 | "babel-jest": "^29.7.0",
66 | "babel-loader": "^9.1.3",
67 | "css-loader": "^7.1.2",
68 | "jest": "^29.7.0",
69 | "jest-environment-jsdom": "^29.7.0",
70 | "nodemon": "^3.1.3",
71 | "react-redux": "^7.2.2",
72 | "redux": "^5.0.1",
73 | "redux-mock-store": "^1.5.4",
74 | "redux-thunk": "^3.1.0",
75 | "sass": "^1.77.6",
76 | "sass-loader": "^14.2.1",
77 | "style-loader": "^4.0.0",
78 | "webpack": "^5.92.0",
79 | "webpack-cli": "^5.1.4",
80 | "webpack-dev-server": "^5.0.4"
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/client/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | DeClustor
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/client/src/App.js:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from 'react'
2 | import {
3 | BrowserRouter as Router,
4 | Navigate,
5 | Route,
6 | Routes,
7 | } from 'react-router-dom'
8 | import { CssBaseline, ThemeProvider, Box } from '@mui/material'
9 | import { createTheme } from '@mui/material/styles'
10 | import { useSelector } from 'react-redux'
11 | import { themeSettings } from './theme'
12 | import Layout from './pages/Layout'
13 | import Dashboard from './pages/Dashboard'
14 | import Login from './pages/login'
15 | import Home from './pages/home'
16 | import Signup from './pages/signup'
17 | import Feedback from './components/feedback'
18 | import Footer from './components/footer'
19 | import UserProfile from './pages/UserProfile'
20 | import Credentials from './pages/credentials'
21 | import LogsNotification from './pages/LogsNotification'
22 | import ClusterMetrics from './pages/ClusterMetrics'
23 | import Overview from './pages/Overview'
24 | import Accounts from './pages/accounts'
25 | import AccountDetails from './components/accountDetails'
26 | import Clusters from './pages/Clusters2'
27 | import ClusterDetails from './components/clusterDetails'
28 |
29 | // Main application component
30 | const App = () => {
31 | // Get the current theme mode from Redux store
32 | const mode = useSelector((state) => state.global.mode)
33 | // Generate the theme based on the current mode
34 | const theme = useMemo(() => createTheme(themeSettings(mode)), [mode])
35 | return (
36 |
37 | {/* CssBaseline to ensure consistent baseline styles */}
38 |
39 |
40 |
47 |
48 | {/* Define routes for different pages */}
49 |
50 | } />
51 | } />
52 | } />
53 | } />
54 | } />
55 | } />
56 | } />
57 | }>
58 | } />
59 | }
62 | />
63 | } />
64 | } />
65 | } />
66 | } />
67 | } />
68 | }
71 | />
72 | } />
73 |
74 |
75 |
76 | {/* Feedback and Footer components */}
77 |
78 |
79 |
80 |
81 |
82 | )
83 | }
84 | export default App
85 |
--------------------------------------------------------------------------------
/client/src/assets/aria.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DeClustor/2ed5f0f10f89d1b40c42569bf590be9dabd264aa/client/src/assets/aria.png
--------------------------------------------------------------------------------
/client/src/assets/dashboard.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DeClustor/2ed5f0f10f89d1b40c42569bf590be9dabd264aa/client/src/assets/dashboard.gif
--------------------------------------------------------------------------------
/client/src/assets/example.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DeClustor/2ed5f0f10f89d1b40c42569bf590be9dabd264aa/client/src/assets/example.gif
--------------------------------------------------------------------------------
/client/src/assets/g.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DeClustor/2ed5f0f10f89d1b40c42569bf590be9dabd264aa/client/src/assets/g.jpg
--------------------------------------------------------------------------------
/client/src/assets/gh2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DeClustor/2ed5f0f10f89d1b40c42569bf590be9dabd264aa/client/src/assets/gh2.jpg
--------------------------------------------------------------------------------
/client/src/assets/google.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DeClustor/2ed5f0f10f89d1b40c42569bf590be9dabd264aa/client/src/assets/google.png
--------------------------------------------------------------------------------
/client/src/assets/grace.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DeClustor/2ed5f0f10f89d1b40c42569bf590be9dabd264aa/client/src/assets/grace.png
--------------------------------------------------------------------------------
/client/src/assets/logingoogle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DeClustor/2ed5f0f10f89d1b40c42569bf590be9dabd264aa/client/src/assets/logingoogle.png
--------------------------------------------------------------------------------
/client/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DeClustor/2ed5f0f10f89d1b40c42569bf590be9dabd264aa/client/src/assets/logo.png
--------------------------------------------------------------------------------
/client/src/assets/nobglogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DeClustor/2ed5f0f10f89d1b40c42569bf590be9dabd264aa/client/src/assets/nobglogo.png
--------------------------------------------------------------------------------
/client/src/assets/ploy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DeClustor/2ed5f0f10f89d1b40c42569bf590be9dabd264aa/client/src/assets/ploy.png
--------------------------------------------------------------------------------
/client/src/assets/profile.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DeClustor/2ed5f0f10f89d1b40c42569bf590be9dabd264aa/client/src/assets/profile.png
--------------------------------------------------------------------------------
/client/src/assets/signupgithub.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DeClustor/2ed5f0f10f89d1b40c42569bf590be9dabd264aa/client/src/assets/signupgithub.png
--------------------------------------------------------------------------------
/client/src/assets/signupgoogle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DeClustor/2ed5f0f10f89d1b40c42569bf590be9dabd264aa/client/src/assets/signupgoogle.png
--------------------------------------------------------------------------------
/client/src/assets/will.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DeClustor/2ed5f0f10f89d1b40c42569bf590be9dabd264aa/client/src/assets/will.png
--------------------------------------------------------------------------------
/client/src/components/Alerts.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Alert from '@mui/material/Alert'
3 | import Stack from '@mui/material/Stack'
4 | import { Link } from 'react-router-dom'
5 | import { useDispatch } from 'react-redux'
6 | // import redux on mark and clear
7 | import { markNotificationsAsRead, clearNotificationBadge } from '../redux/notificationSlice'
8 |
9 | const Alerts = ({ notificationCount, onAlertClick }) => {
10 |
11 | const dispatch = useDispatch()
12 |
13 | // handle cleaning and counting notification
14 | const handleAlertClick = () => {
15 | // first time clicking
16 | dispatch(markNotificationsAsRead())
17 | // then clear the number on the badge
18 | dispatch(clearNotificationBadge())
19 | onAlertClick()
20 | }
21 |
22 | return (
23 |
24 | {notificationCount > 0 ? (
25 |
30 | You have notifications!
31 |
32 | ) : (
33 | You have no notifications
34 | )}
35 |
36 | )
37 | }
38 |
39 | export default Alerts
40 |
--------------------------------------------------------------------------------
/client/src/components/Alerts.test.jsx:
--------------------------------------------------------------------------------
1 | // src/components/Alerts.test.jsx
2 | import React from 'react';
3 | import { render, screen, fireEvent } from '@testing-library/react';
4 | import { Provider } from 'react-redux';
5 | import configureStore from 'redux-mock-store';
6 | import Alerts from './Alerts'; // Adjust the path to your Alerts component
7 | import { BrowserRouter as Router } from 'react-router-dom';
8 |
9 | const mockStore = configureStore([]);
10 |
11 | describe('Alerts Component', () => {
12 | let store;
13 |
14 | beforeEach(() => {
15 | store = mockStore({
16 | notifications: {
17 | notificationCount: 5, // Set this to a positive number to test the "You have notifications!" message
18 | },
19 | });
20 | });
21 |
22 | // test('renders "You have notifications!" when notificationCount is greater than 0', () => {
23 | // render(
24 | //
25 | //
26 | //
27 | //
28 | //
29 | // );
30 |
31 | // // Check if the alert with the text "You have notifications!" is present
32 | // expect(screen.getByText('You have notifications!')).toBeInTheDocument();
33 | // });
34 |
35 | test('renders "You have no notifications" when notificationCount is 0', () => {
36 | store = mockStore({
37 | notifications: {
38 | notificationCount: 0, // Set this to 0 to test the "You have no notifications" message
39 | },
40 | });
41 |
42 | render(
43 |
44 |
45 |
46 |
47 |
48 | );
49 |
50 | // Check if the alert with the text "You have no notifications" is present
51 | expect(screen.getByText('You have no notifications')).toBeInTheDocument();
52 | });
53 |
54 | // test('clicking the alert link dispatches the correct action', () => {
55 | // render(
56 | //
57 | //
58 | //
59 | //
60 | //
61 | // );
62 |
63 | // const alertLink = screen.getByText('You have notifications!');
64 | // fireEvent.click(alertLink);
65 |
66 | // const actions = store.getActions();
67 | // expect(actions).toContainEqual({ type: 'HIDE_ALERT' });
68 | // });
69 | });
70 |
--------------------------------------------------------------------------------
/client/src/components/Export.jsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import Box from '@mui/material/Box';
3 | import SpeedDial from '@mui/material/SpeedDial';
4 | import SpeedDialIcon from '@mui/material/SpeedDialIcon';
5 | import SpeedDialAction from '@mui/material/SpeedDialAction';
6 | import FileCopyIcon from '@mui/icons-material/FileCopyOutlined';
7 | //import ShareIcon from '@mui/icons-material/Share';
8 |
9 | // props from Logs page
10 | const Export = ({ rows }) => {
11 |
12 | const exportToCSV = () => {
13 | // declare an empty array
14 | const csvRows = [];
15 | // declare a table headers
16 | const headers = ['Time', 'Cluster', 'Service', 'Metric Name', 'Value', 'Logs'];
17 | // push the headers into csv array as a string with , join
18 | csvRows.push(headers.join(','));
19 |
20 | // iterate thru the every rows in csv
21 | rows.forEach(row => {
22 | const values = [row.time, row.clusters, row.service, row.metric, row.value, row.logs];
23 | csvRows.push(values.join(',')); // join the values and push into array
24 | });
25 |
26 | // create new Blob obj from joined csv strings (data, { type: 'text/plain' });
27 | const csvData = new Blob([csvRows.join('\n')], { type: 'text/csv' });
28 | // create URL for Blob // use createObjectURL() for file downloading
29 | const csvUrl = URL.createObjectURL(csvData);
30 |
31 |
32 | // create a href (link)
33 | const link = document.createElement('a');
34 | link.href = csvUrl;
35 |
36 | const date = new Date();
37 | const dateString = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
38 |
39 | link.download = `logs_${dateString}.csv`; // file name and date
40 | link.click(); // eventListener -> on click
41 | };
42 |
43 | const actions = [
44 | // add action exporting function
45 | { icon: , name: 'Export', action: exportToCSV },
46 | // { icon: , name: 'Share' },
47 | ];
48 |
49 | return (
50 |
51 |
52 | }
56 | >
57 | {/* mapping all thhe actions */}
58 | {actions.map((action) => (
59 |
65 | ))}
66 |
67 |
68 |
69 | );
70 | }
71 |
72 | export default Export;
73 |
--------------------------------------------------------------------------------
/client/src/components/FlexBetween.jsx:
--------------------------------------------------------------------------------
1 | // style component to reuse styles css
2 | const { styled } = require('@mui/system');
3 | const { Box } = require('@mui/material');
4 |
5 | const FlexBetween = styled(Box)({
6 | // insert Box from mui material ui
7 | // css property
8 | display: 'flex',
9 | justifyContent: 'space-between',
10 | alignItems: 'center'
11 | });
12 |
13 | export default FlexBetween;
--------------------------------------------------------------------------------
/client/src/components/FlexBetween.test.jsx:
--------------------------------------------------------------------------------
1 | // FlexBetween.test.js
2 | import React from 'react';
3 | import { render } from '@testing-library/react';
4 | import '@testing-library/jest-dom';
5 | import FlexBetween from './FlexBetween';
6 |
7 | test('renders FlexBetween component with correct styles', () => {
8 | const { container } = render();
9 |
10 | const flexBetweenElement = container.firstChild;
11 |
12 | // Check if the element is a div (Box from MUI renders as a div by default)
13 | expect(flexBetweenElement.tagName).toBe('DIV');
14 |
15 | // Check if the styles are applied correctly
16 | expect(flexBetweenElement).toHaveStyle('display: flex');
17 | expect(flexBetweenElement).toHaveStyle('justify-content: space-between');
18 | expect(flexBetweenElement).toHaveStyle('align-items: center');
19 | });
20 |
--------------------------------------------------------------------------------
/client/src/components/Navbar.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { useTheme } from '@emotion/react';
3 | import {
4 | Box,
5 | IconButton,
6 | InputBase,
7 | Typography,
8 | Tooltip,
9 | Badge,
10 | } from '@mui/material';
11 | import FlexBetween from './FlexBetween';
12 | import { useDispatch, useSelector } from 'react-redux';
13 | import { setMode } from '../redux/globalSlice';
14 | import LightModeOutlinedIcon from '@mui/icons-material/LightModeOutlined';
15 | import DarkModeOutlinedIcon from '@mui/icons-material/DarkModeOutlined';
16 | import NotificationsOutlinedIcon from '@mui/icons-material/NotificationsOutlined';
17 | import PersonOutlinedIcon from '@mui/icons-material/PersonOutlined';
18 | import Search from '@mui/icons-material/Search';
19 | import { Menu as MenuIcon } from '@mui/icons-material';
20 | import LogoutIcon from '@mui/icons-material/Logout';
21 | import logo from '../assets/logo.png';
22 | import Setting from './Setting';
23 | import {
24 | clearNotificationBadge,
25 | markNotificationsAsRead,
26 | } from '../redux/notificationSlice';
27 | import { useNavigate } from 'react-router-dom';
28 | //import Alerts from './Alerts'
29 | // import { useWebSocket } from '../redux/wsContext' // <= keep this for now , ws global
30 | const Navbar = ({
31 | isSidebarOpen,
32 | setIsSidebarOpen,
33 | showSidebar = true,
34 | showSearch = true,
35 | showNotification = true,
36 | showUser = true,
37 | }) => {
38 | const dispatch = useDispatch();
39 | const theme = useTheme();
40 | const navigate = useNavigate();
41 | const [alertAnchorEl, setAlertAnchorEl] = useState(null);
42 | // unread redux state
43 | const unreadNotificationCount = useSelector(
44 | (state) => state.notification.unreadNotificationCount
45 | );
46 | useEffect(() => {
47 | if (alertAnchorEl) {
48 | const timer = setTimeout(() => {
49 | setAlertAnchorEl(null);
50 | }, 5000);
51 | return () => clearTimeout(timer);
52 | }
53 | }, [alertAnchorEl]);
54 | // make sure the state update happens before navigation // not time lapping
55 | const handleNotificationClick = (event) => {
56 | if (unreadNotificationCount > 0) {
57 | // mark and clare redux state
58 | dispatch(markNotificationsAsRead());
59 | dispatch(clearNotificationBadge());
60 | setTimeout(() => {
61 | navigate('/logs');
62 | }, 0);
63 | }
64 | };
65 | // const handleAlertClose = () => {
66 | // setAlertAnchorEl(null)
67 | // }
68 | //const isAlertOpen = Boolean(alertAnchorEl)
69 | return (
70 |
71 | {/* hide the sidebar */}
72 | {showSidebar ? (
73 | setIsSidebarOpen(!isSidebarOpen)}
75 | sx={{
76 | borderRadius: '50%',
77 | width: '40px',
78 | height: '40px',
79 | display: 'flex',
80 | alignItems: 'center',
81 | justifyContent: 'center',
82 | }}
83 | >
84 |
85 |
86 | ) : (
87 | navigate('/dashboard')}
92 | height='100px'
93 | width='100px'
94 | borderRadius='28%'
95 | sx={{
96 | objectFit: 'cover',
97 | borderColor: theme.palette.primary[400],
98 | borderStyle: 'solid',
99 | borderWidth: 1,
100 | marginLeft: '47px',
101 | padding: '5px',
102 | cursor: 'pointer',
103 | }}
104 | />
105 | )}
106 |
107 | {/* search bar */}
108 | {showSearch && (
109 |
115 |
116 |
117 |
118 |
119 |
120 | )}
121 |
122 | {/* dark/light mode , notification and profile icons */}
123 |
124 | {/* dark/light mode */}
125 | dispatch(setMode())}>
126 | {theme.palette.mode === 'dark' ? (
127 |
128 | ) : (
129 |
130 | )}
131 |
132 |
133 | {/* Notification alert button */}
134 | {/* check if there is no unread .length, tooltip box equals no notification */}
135 | {showNotification && (
136 | 0
139 | ? 'You have notifications!'
140 | : 'No notifications'
141 | }
142 | >
143 |
144 | {/* handle notification */}
145 |
146 | {unreadNotificationCount > 0 ? (
147 |
148 |
149 |
150 | ) : (
151 |
152 | )}
153 |
154 |
155 | )}
156 |
157 | {/* setting icon button */}
158 | {showUser && (
159 |
160 |
161 |
162 |
163 |
164 | )}
165 | {/* profile icon button */}
166 | {showUser && (
167 |
168 | navigate('/userprofile')}>
169 |
170 |
171 |
172 | )}
173 | {/* logout icon button */}
174 |
175 | navigate('/')}>
176 |
177 |
178 |
179 |
180 |
181 |
182 | );
183 | };
184 | export default Navbar;
185 |
--------------------------------------------------------------------------------
/client/src/components/PieChart.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState , useEffect } from 'react';
2 | import { ResponsivePie } from '@nivo/pie';
3 | import { useTheme } from "@mui/material";
4 | import { connectWebSocketToPieChart } from '../webService/connectWebSocketToPieChart.js';
5 | import { useSelector } from 'react-redux';
6 |
7 | // Function to transform raw data to pie chart format
8 | const transformData = (rawData) => {
9 | if (!rawData || typeof rawData !== 'object') {
10 | return [];
11 | }
12 |
13 | const { totalTasks, runningTasks, pendingTasks, stoppedTasks } = rawData;
14 |
15 | // Transform data to Nivo Pie Chart format
16 | const transformedData = [
17 | { id: 'TotalTasks', label: 'Total Tasks', value: totalTasks },
18 | { id: 'RunningTasks', label: 'Running Tasks', value: runningTasks },
19 | { id: 'PendingTasks', label: 'Pending Tasks', value: pendingTasks },
20 | { id: 'StoppedTasks', label: 'Stopped Tasks', value: stoppedTasks },
21 | ];
22 | return transformedData;
23 | };
24 |
25 | const PieChart = () => {
26 | const theme = useTheme();
27 | const [data, setData] = useState([]);
28 | const [loading, setLoading] = useState(true);
29 | const [error, setError] = useState(null);
30 | const { userId, accountName, region, clusterName, serviceName } = useSelector((state) => ({
31 | userId: state.user.userId,
32 | accountName: state.user.accountName,
33 | region: state.user.region,
34 | clusterName: state.user.clusterName,
35 | serviceName: state.user.serviceName,
36 | }));
37 |
38 | // custom tooltip for pie chart
39 | const CustomTooltip = ({ datum }) => (
40 |
47 | {datum.label}: {datum.value}
48 |
49 | );
50 |
51 | useEffect(() => {
52 | if (userId && serviceName && accountName && region && clusterName) {
53 | // Connect to WebSocket using userId and serviceName
54 | const ws = connectWebSocketToPieChart(userId, accountName, region, clusterName, serviceName, (rawData) => {
55 | const transformedData = transformData(rawData);
56 | setData(transformedData);
57 | setLoading(false);
58 | },
59 | (error) => {
60 | setError(error.message);
61 | setLoading(false);
62 | },
63 | () => {
64 | console.log('WebSocket from Pie closed');
65 | }
66 | );
67 |
68 | // Cleanup WebSocket on component unmount
69 | return () => {
70 | ws.close();
71 | };
72 | }
73 | }, [userId, accountName, region, clusterName, serviceName]);
74 |
75 | // Loading state
76 | if (loading) {
77 | return Loading...
;
78 | }
79 |
80 | // Error state
81 | if (error) {
82 | return Error: {error}
;
83 | }
84 |
85 | return (
86 |
87 |
137 |
138 | );
139 | };
140 |
141 | export default PieChart;
142 |
--------------------------------------------------------------------------------
/client/src/components/Service.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import TextField from '@mui/material/TextField'
3 | import Autocomplete from '@mui/material/Autocomplete'
4 | import Card from '@mui/material/Card'
5 | import CardContent from '@mui/material/CardContent'
6 | import Typography from '@mui/material/Typography'
7 | import {
8 | Button,
9 | CardActionArea,
10 | CardActions,
11 | CircularProgress,
12 | } from '@mui/material'
13 | import { useTheme } from '@emotion/react'
14 | import { useSelector, useDispatch } from 'react-redux'
15 | import { setServiceName } from '../redux/userSlice.js'
16 |
17 | /**
18 | * Service component that allows users to select a service and view its status.
19 | * @param {Object} props - Component properties.
20 | * @param {string} props.userId - The ID of the user.
21 | */
22 |
23 | const Service = ({ userId, accountName, region, clusterName }) => {
24 | const theme = useTheme()
25 | const [serviceName, setServiceNameLocal] = useState(null)
26 | const [serviceNames, setServiceNames] = useState([])
27 | const [inputValue, setInputValue] = useState('')
28 | const [serviceStatus, setServiceStatus] = useState(null)
29 | const [loading, setLoading] = useState(false)
30 | const [error, setError] = useState(null)
31 | const dispatch = useDispatch()
32 |
33 | useEffect(() => {
34 | if (userId && accountName && region && clusterName) {
35 | setLoading(true)
36 | setError(null)
37 |
38 | // Fetch the list of services for the given userId
39 | fetch(
40 | `http://localhost:3000/list/AllServices?userId=${userId}&accountName=${accountName}&clusterName=${clusterName}®ion=${region}`
41 | )
42 | .then((response) => response.json())
43 | .then((data) => {
44 | console.log('Fetching service names -->', data)
45 | if (data && data.length > 0) {
46 | setServiceNames(data)
47 | setServiceNameLocal(data[0])
48 | dispatch(setServiceName(data[0])) // Update Redux state
49 | } else {
50 | throw new Error('No services found')
51 | }
52 | setLoading(false)
53 | })
54 | .catch((error) => {
55 | console.error('Error fetching service names:', error)
56 | setError('Error fetching service names')
57 | setLoading(false)
58 | })
59 | }
60 | }, [userId])
61 |
62 | useEffect(() => {
63 | if (userId && accountName && region && clusterName && serviceName) {
64 | setLoading(true)
65 | setError(null)
66 |
67 | // change to redux later:
68 | const ws = new WebSocket(
69 | `ws://localhost:3000/getMetricData?userId=${userId}&accountName=${accountName}®ion=${region}&clusterName=${clusterName}&serviceName=${serviceName}&metricName=serviceStatus`
70 | )
71 | ws.onopen = () => {
72 | console.log('WebSocket connection opened')
73 | }
74 |
75 | ws.onmessage = (event) => {
76 | const data = JSON.parse(event.data)
77 | setServiceStatus(data[0] || 'UNKNOWN')
78 | setLoading(false)
79 | }
80 |
81 | ws.onerror = (error) => {
82 | console.error('WebSocket error:', error)
83 | setError('Error fetching service status')
84 | setLoading(false)
85 | }
86 |
87 | ws.onclose = (event) => {
88 | console.log('WebSocket connection closed:', event)
89 | }
90 |
91 | // Cleanup WebSocket on component unmount
92 | return () => {
93 | ws.close()
94 | }
95 | }
96 | }, [userId, serviceName])
97 |
98 | return (
99 |
100 |
{`Service: ${
101 | serviceName !== null
102 | ? `'${serviceName}'`
103 | : 'Please choose your service name'
104 | }`}
105 |
106 |
{
109 | setServiceNameLocal(newValue)
110 | dispatch(setServiceName(newValue)) // Update Redux state
111 | }}
112 | inputValue={inputValue}
113 | onInputChange={(event, newInputValue) => {
114 | setInputValue(newInputValue)
115 | }}
116 | id="controllable-states-demo"
117 | options={serviceNames}
118 | sx={{ minWidth: 300, maxWidth: 330 }}
119 | renderInput={(params) => (
120 |
121 | )}
122 | />
123 |
131 |
132 |
133 | {loading ? (
134 |
135 | ) : error ? (
136 |
137 | {error}
138 |
139 | ) : (
140 |
146 | Service Status: {serviceStatus || 'UNKNOWN'}
147 |
148 | )}
149 |
150 |
151 |
152 |
162 |
163 |
164 |
165 | )
166 | }
167 |
168 | export default Service
169 |
--------------------------------------------------------------------------------
/client/src/components/Sidebar.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import {
3 | Box,
4 | Divider,
5 | Drawer,
6 | IconButton,
7 | List,
8 | ListItem,
9 | ListItemButton,
10 | ListItemIcon,
11 | ListItemText,
12 | Typography,
13 | useTheme,
14 | } from '@mui/material';
15 | import {
16 | ChevronLeft,
17 | ChevronRightOutlined,
18 | HomeOutlined,
19 | CalendarMonthOutlined,
20 | AdminPanelSettingsOutlined,
21 | } from '@mui/icons-material';
22 | import { useLocation, useNavigate } from 'react-router-dom';
23 | import FlexBetween from './FlexBetween';
24 | import profileImage from '../assets/profile.png';
25 | import SsidChartOutlinedIcon from '@mui/icons-material/SsidChartOutlined';
26 | import LanOutlinedIcon from '@mui/icons-material/LanOutlined';
27 | import logo from '../assets/logo.png';
28 | import { useSelector, useDispatch } from 'react-redux';
29 | import {
30 | fetchAccounts,
31 | selectAccount,
32 | fetchSubAccountDetails,
33 | } from '../redux/userSlice';
34 | import AccountsSection from './accSection';
35 |
36 | // Sidebar component
37 | const Sidebar = ({
38 | isNonMobile,
39 | drawerWidth,
40 | isSidebarOpen,
41 | setIsSidebarOpen,
42 | }) => {
43 | // grap the path that we currently at
44 | const { pathname } = useLocation();
45 | // state of currently page or track of which page is active right now
46 | const [active, setActive] = useState('');
47 | const navigate = useNavigate();
48 | // from theme color
49 | const theme = useTheme();
50 | const user = useSelector((state) => state.user);
51 | console.log('user login -->', user);
52 | const dispatch = useDispatch();
53 | const userId = useSelector((state) => state.user.userId);
54 |
55 | // everytime path name has changed , set the active to the current page
56 | useEffect(() => {
57 | // set to currect url and determain which page we are on
58 | setActive(pathname.substring(1));
59 | }, [pathname]);
60 |
61 | // Navigation items
62 | const navItems = [
63 | {
64 | text: 'Dashboard',
65 | icon: ,
66 | },
67 | {
68 | text: 'Task Overview',
69 | icon: ,
70 | },
71 | {
72 | text: 'Cluster Metrics',
73 | icon: ,
74 | },
75 | {
76 | text: 'Logs',
77 | icon: ,
78 | },
79 | ];
80 |
81 | return (
82 |
83 | {/* Conditional rendering for Drawer */}
84 | {isSidebarOpen && (
85 | setIsSidebarOpen(false)}
88 | variant="persistent"
89 | anchor="left"
90 | sx={{
91 | width: drawerWidth,
92 | '& .MuiDrawer-paper': {
93 | color: theme.palette.secondary[200],
94 | backgroundColor: theme.palette.background.alt,
95 | boxSizing: 'border-box',
96 | borderWidth: isNonMobile ? 0 : '2px',
97 | },
98 | }}
99 | >
100 | {/* Drawer content */}
101 |
102 | {/* Logo section */}
103 |
104 |
105 | navigate('/')}
110 | height="100px"
111 | width="100px"
112 | borderRadius="28%"
113 | sx={{
114 | objectFit: 'cover',
115 | borderColor: theme.palette.primary[400],
116 | borderStyle: 'solid',
117 | borderWidth: 1,
118 | marginLeft: '47px',
119 | padding: '5px',
120 | cursor: 'pointer',
121 | }}
122 | />
123 | {/* responsive for mobile , it's will pop up the left arrow */}
124 | {/* Close button for mobile view */}
125 | {!isNonMobile && (
126 | setIsSidebarOpen(!isSidebarOpen)}
128 | data-testid="close-sidebar-button"
129 | >
130 |
131 |
132 | )}
133 |
134 |
135 | {/* link of list in the sidebar */}
136 |
137 | {navItems.map(({ text, icon }) => {
138 | const lowerCaseText = text.toLowerCase().replace(' ', '');
139 | return (
140 |
141 | {
143 | navigate(`/${lowerCaseText}`);
144 | setActive(lowerCaseText);
145 | }}
146 | sx={{
147 | backgroundColor:
148 | active === lowerCaseText
149 | ? theme.palette.secondary[400]
150 | : 'transparent',
151 | color:
152 | active === lowerCaseText
153 | ? theme.palette.primary[600]
154 | : theme.palette.secondary[100],
155 | }}
156 | >
157 |
166 | {icon}
167 |
168 |
169 | {active === lowerCaseText && (
170 |
171 | )}
172 |
173 |
174 | );
175 | })}
176 |
177 |
178 |
179 |
182 |
183 |
184 | {/* user profile */}
185 |
194 | {/* user's name */}
195 |
196 |
201 | {user ? user.username : 'No User Data'}
202 |
203 |
204 |
205 |
206 |
207 | )}
208 |
209 | );
210 | };
211 |
212 | export default Sidebar;
213 |
--------------------------------------------------------------------------------
/client/src/components/StatusCard.jsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import Card from '@mui/material/Card';
3 | import CardContent from '@mui/material/CardContent';
4 | import CardMedia from '@mui/material/CardMedia';
5 | import Typography from '@mui/material/Typography';
6 | import { Button, CardActionArea, CardActions } from '@mui/material';
7 | import { useTheme } from '@emotion/react';
8 | import { useSelector } from 'react-redux';
9 |
10 | /**
11 | * StatusCard component displays the task status and an image.
12 | * It includes a refresh button to reload the page.
13 | */
14 | const StatusCard = () => {
15 |
16 | const theme = useTheme();
17 | const userId = useSelector((state) => state.user.userId);
18 | const serviceName = useSelector((state) => state.user.serviceName);
19 | const taskStatus = userId && serviceName && serviceName !== 'service1' ? 'RUNNING' : 'No data found'; // Determine task status based on userId and serviceName
20 |
21 | return (
22 |
23 |
24 | {/* Display an image */}
25 |
31 |
32 | {/* Display task status */}
33 |
34 | Task Status : {taskStatus}
35 |
36 |
37 |
38 |
39 | {/* Refresh button to reload the page */}
40 |
43 |
44 |
45 | );
46 | }
47 | // test to push
48 | export default StatusCard;
--------------------------------------------------------------------------------
/client/src/components/__snapshots__/footer.test.jsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`renders Footer component with correct styles and content 1`] = `
4 |
5 |
18 |
19 | `;
20 |
--------------------------------------------------------------------------------
/client/src/components/accSection.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import {
3 | Accordion,
4 | AccordionSummary,
5 | AccordionDetails,
6 | List,
7 | ListItem,
8 | ListItemText,
9 | Typography,
10 | useTheme,
11 | Box,
12 | ListItemIcon,
13 | Tooltip,
14 | IconButton,
15 | } from '@mui/material';
16 | import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
17 | import ManageAccountsIcon from '@mui/icons-material/ManageAccounts';
18 | import SupervisorAccountIcon from '@mui/icons-material/SupervisorAccount';
19 | import AccountBalanceIcon from '@mui/icons-material/AccountBalance';
20 | import GroupAddIcon from '@mui/icons-material/GroupAdd';
21 | import { useNavigate } from 'react-router-dom';
22 | import { useSelector, useDispatch } from 'react-redux';
23 | import {
24 | fetchAccounts,
25 | fetchSubAccountDetails,
26 | selectAccount,
27 | } from '../redux/userSlice';
28 |
29 | const AccountsSection = ({ userId }) => {
30 | const [expanded, setExpanded] = useState(false);
31 | const navigate = useNavigate();
32 | const dispatch = useDispatch();
33 | const rootAccounts = useSelector((state) => state.user.rootAccounts) || [];
34 | const subAccounts = useSelector((state) => state.user.subAccounts) || [];
35 | const theme = useTheme();
36 | const textColor = theme.palette.secondary[100];
37 |
38 | useEffect(() => {
39 | if (userId) {
40 | dispatch(fetchAccounts(userId));
41 | }
42 | }, [dispatch, userId]);
43 | //handle accord panel
44 | const handleChange = (panel) => (event, isExpanded) => {
45 | setExpanded(isExpanded ? panel : false);
46 | };
47 | // when clicking on the each account
48 | const handleAccountClick = (account, accountType) => {
49 | if (account) {
50 | dispatch(selectAccount({ account, accountType }));
51 | if (accountType === 'Root') {
52 | dispatch(
53 | fetchSubAccountDetails({ userId, accountName: account.account_name })
54 | );
55 | navigate('/accounts');
56 | }
57 | }
58 | };
59 | // add more account will redirect to credential page
60 | const handleAddAccountClick = () => {
61 | navigate('/credentials');
62 | };
63 |
64 | return (
65 |
73 | {/* hiden panel for the account details */}
74 |
79 | }
81 | aria-controls='panel1d-content'
82 | id='panel1d-header'
83 | >
84 |
85 |
86 |
87 | Account Management
88 |
89 |
90 |
91 | {/* root account */}
92 |
93 |
94 | {rootAccounts.length > 0 && (
95 |
96 |
99 | Root Accounts
100 |
101 |
102 | {rootAccounts.map((account) => (
103 | handleAccountClick(account, 'Root')}
106 | sx={{
107 | justifyContent: 'center',
108 | '&:hover': {
109 | backgroundColor: theme.palette.action.hover,
110 | width: '100%',
111 | cursor: 'pointer',
112 | },
113 | }}
114 | >
115 |
116 |
117 |
118 | {/* subaccount */}
119 |
120 |
121 | ))}
122 |
123 |
124 | )}
125 | {subAccounts.length > 0 && (
126 |
127 |
130 | Sub Accounts
131 |
132 |
133 | {subAccounts.map((account) => (
134 | handleAccountClick(account, 'Sub')}
137 | sx={{
138 | justifyContent: 'center',
139 | '&:hover': {
140 | backgroundColor: theme.palette.action.hover,
141 | width: '100%',
142 | cursor: 'pointer',
143 | },
144 | }}
145 | >
146 | {/* list of the accounts */}
147 |
148 |
149 |
150 |
151 |
152 | ))}
153 |
154 |
155 | )}
156 | {/* if no account available */}
157 | {rootAccounts.length === 0 && subAccounts.length === 0 && (
158 | No accounts available
159 | )}
160 |
161 |
162 | {/* add more account button */}
163 |
164 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 | );
176 | };
177 |
178 | export default AccountsSection;
179 |
--------------------------------------------------------------------------------
/client/src/components/accountDetails.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Card,
4 | CardContent,
5 | Typography,
6 | CardActionArea,
7 | useTheme,
8 | Box,
9 | } from '@mui/material';
10 | import { useNavigate } from 'react-router-dom';
11 |
12 | const AccountDetails = ({ account, accountType }) => {
13 | const theme = useTheme();
14 | const navigate = useNavigate();
15 |
16 | const handleClick = () => {
17 | if (account && account.Name) {
18 | navigate(`/clusters/${account.Name}`);
19 | } else {
20 | console.error('Account or account name is missing');
21 | }
22 | };
23 |
24 | return (
25 |
45 |
46 |
47 |
52 | {account.Name || 'No account name'}
53 |
54 |
55 |
56 |
57 |
58 | ID:
59 |
60 |
61 | {account.Id || 'N/A'}
62 |
63 |
64 |
65 |
66 | Email:
67 |
68 |
69 | {account.Email || 'N/A'}
70 |
71 |
72 |
73 |
74 | Status:
75 |
76 |
77 | {account.Status || 'N/A'}
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 | );
86 | };
87 |
88 | export default AccountDetails;
89 |
--------------------------------------------------------------------------------
/client/src/components/breadcrumbs.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Breadcrumbs, Link, Typography } from '@mui/material';
3 | import { useNavigate, useLocation } from 'react-router-dom';
4 |
5 | const BreadcrumbsNav = ({ breadcrumbs, currentPath }) => {
6 | const navigate = useNavigate();
7 | const location = useLocation();
8 | const currentPathname = location.pathname;
9 |
10 | return (
11 |
22 | {/* check if the current path? */}
23 | {breadcrumbs.map((breadcrumb, index) => {
24 | const isLast = index === breadcrumbs.length - 1;
25 | const isActive = breadcrumb.path === currentPathname;
26 | return isLast ? (
27 |
31 | {breadcrumb.name}
32 |
33 | ) : (
34 | navigate(breadcrumb.path)}
37 | key={breadcrumb.path}
38 | >
39 | {breadcrumb.name}
40 |
41 | );
42 | })}
43 |
44 | );
45 | };
46 |
47 | export default BreadcrumbsNav;
48 |
--------------------------------------------------------------------------------
/client/src/components/clusterDetails.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | Box,
4 | Typography,
5 | Card,
6 | CardContent,
7 | CardActionArea,
8 | } from '@mui/material';
9 | import { useTheme } from '@mui/material/styles';
10 | import { useNavigate } from 'react-router-dom';
11 | // cluster cards component
12 | const ClusterDetails = ({ cluster = {} }) => {
13 | const theme = useTheme();
14 | const navigate = useNavigate();
15 | const {
16 | clusterName,
17 | status,
18 | activeServicesCount,
19 | runningTasksCount,
20 | pendingTasksCount,
21 | capacityProviders,
22 | } = cluster;
23 | // redirect to dashboard
24 | const handleClick = () => {
25 | if (clusterName) {
26 | navigate(`/dashboard/${clusterName}`);
27 | } else {
28 | console.error('Cluster name is missing');
29 | }
30 | };
31 |
32 | return (
33 |
52 | {/* header tab */}
53 |
54 |
55 |
60 | {clusterName || 'No cluster name'}
61 |
62 | {/* details card */}
63 |
64 |
65 |
66 |
67 | Status:
68 |
69 |
70 | {status || 'N/A'}
71 |
72 |
73 |
74 |
75 | Active Services Count:
76 |
77 |
78 | {activeServicesCount || 0}
79 |
80 |
81 |
82 |
83 | Running Task Count:
84 |
85 |
86 | {runningTasksCount || 0}
87 |
88 |
89 |
90 |
91 | Pending Task Count:
92 |
93 |
94 | {pendingTasksCount || 0}
95 |
96 |
97 |
98 |
99 | Capacity Providers:
100 |
101 |
102 | {capacityProviders?.join(', ') || 'N/A'}
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 | );
111 | };
112 |
113 | export default ClusterDetails;
114 |
--------------------------------------------------------------------------------
/client/src/components/feedback.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import {
3 | Box,
4 | Button,
5 | Drawer,
6 | Typography,
7 | TextField,
8 | IconButton,
9 | } from '@mui/material';
10 | import FeedbackIcon from '@mui/icons-material/Feedback';
11 | import CloseIcon from '@mui/icons-material/Close';
12 |
13 | // Feedback component
14 | const Feedback = () => {
15 | // State to track if the drawer is open or closed
16 | const [open, setOpen] = useState(false);
17 | // Function to toggle the drawer state
18 | const toggleDrawer = (state) => () => {
19 | setOpen(state);
20 | };
21 | // Function to handle form submission
22 | const handleSubmit = (event) => {
23 | event.preventDefault();
24 | setOpen(false);
25 | };
26 |
27 | return (
28 | <>
29 | {/* Button to open the feedback drawer */}
30 | }
41 | >
42 | Give Feedback
43 |
44 | {/* Drawer component for feedback form */}
45 |
46 |
47 | {/* Drawer header with title and close button */}
48 |
55 | Feedback
56 |
57 |
58 |
59 |
60 | {/* Feedback form */}
61 |
62 |
71 |
74 |
75 |
76 |
77 | >
78 | );
79 | };
80 |
81 | export default Feedback;
82 |
--------------------------------------------------------------------------------
/client/src/components/feedback.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, screen, fireEvent } from '@testing-library/react';
3 | import Feedback from './feedback';
4 |
5 | describe('Feedback component', () => {
6 | test('renders the feedback button', () => {
7 | render();
8 | const feedbackButton = screen.getByRole('button', {
9 | name: /Give Feedback/i,
10 | });
11 | expect(feedbackButton).toBeInTheDocument();
12 | });
13 |
14 | // test('opens the drawer when the feedback button is clicked', () => {
15 | // render();
16 |
17 | // // Click the feedback button to open the drawer
18 | // const feedbackButton = screen.getByRole('button', {
19 | // name: /Give Feedback/i,
20 | // });
21 | // fireEvent.click(feedbackButton);
22 |
23 | // // Check if the drawer is open by looking for an element inside the drawer
24 | // const drawerTitle = screen.getByText(/Feedback/i);
25 | // expect(drawerTitle).toBeInTheDocument();
26 | // });
27 |
28 | // test('closes the drawer when the close button is clicked', () => {
29 | // render();
30 |
31 | // // Open the drawer first
32 | // const feedbackButton = screen.getByRole('button', {
33 | // name: /Give Feedback/i,
34 | // });
35 | // fireEvent.click(feedbackButton);
36 |
37 | // // Click the close button inside the drawer
38 | // const closeButton = screen.getByTestId('CloseIcon'); // Use data-testid instead
39 | // fireEvent.click(closeButton);
40 |
41 | // // Check if the drawer is closed by confirming the absence of the drawer title
42 | // expect(screen.queryByText(/Feedback/i)).not.toBeInTheDocument();
43 | // });
44 |
45 | // test('submits the form and closes the drawer', () => {
46 | // render();
47 |
48 | // // Open the drawer
49 | // const feedbackButton = screen.getByRole('button', {
50 | // name: /Give Feedback/i,
51 | // });
52 | // fireEvent.click(feedbackButton);
53 |
54 | // // Fill out and submit the form
55 | // const textField = screen.getByLabelText(/Your Feedback/i);
56 | // fireEvent.change(textField, { target: { value: 'This is my feedback.' } });
57 | // const submitButton = screen.getByRole('button', { name: /Submit/i });
58 | // fireEvent.click(submitButton);
59 |
60 | // // Check if the drawer is closed after form submission
61 | // expect(screen.queryByText(/Feedback/i)).not.toBeInTheDocument();
62 | // });
63 | });
64 |
--------------------------------------------------------------------------------
/client/src/components/fileMock.js:
--------------------------------------------------------------------------------
1 | module.exports = 'test-file-stub';
2 |
--------------------------------------------------------------------------------
/client/src/components/footer.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Box, Typography, Container ,useTheme } from '@mui/material';
3 |
4 | // Footer component
5 | const Footer = (props) => {
6 | const theme = useTheme();
7 |
8 | return (
9 |
19 |
20 | {/* Footer text */}
21 |
22 | DeClustor © {new Date().getFullYear()}
23 |
24 |
25 |
26 | );
27 | };
28 |
29 | export default Footer;
30 |
--------------------------------------------------------------------------------
/client/src/components/footer.test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from '@testing-library/react';
3 | import { createTheme, ThemeProvider } from '@mui/material/styles';
4 | import Footer from './footer';
5 |
6 | test('renders Footer component with correct styles and content', () => {
7 | const theme = createTheme({
8 | palette: {
9 | primary: {
10 | main: '#3f51b5', // Ensure the correct format for primary color
11 | },
12 | secondary: {
13 | main: '#f50057', // Ensure the correct format for secondary color
14 | },
15 | },
16 | });
17 |
18 | const { asFragment, getByText } = render(
19 |
20 |
21 |
22 | );
23 |
24 | // Check if the Footer contains the correct text
25 | expect(getByText(/DeClustor ©/)).toBeInTheDocument();
26 |
27 | // Use snapshot testing to check rendered output
28 | expect(asFragment()).toMatchSnapshot();
29 | });
30 |
--------------------------------------------------------------------------------
/client/src/components/home.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Home = () => {
4 | return (
5 |
6 |
Welcome to DeClustor
7 |
This is the home page.
8 |
9 | );
10 | };
11 |
12 | export default Home;
13 |
--------------------------------------------------------------------------------
/client/src/components/info.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Info = () => {
4 | return (
5 |
6 |
Info Component
7 |
This is the info page.
8 |
9 | );
10 | };
11 |
12 | export default Info;
13 |
--------------------------------------------------------------------------------
/client/src/components/login.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Login = () => {
4 | return Hello World!
;
5 | };
6 |
7 | export default Login;
8 |
--------------------------------------------------------------------------------
/client/src/components/mockStore.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render } from '@testing-library/react';
3 | import { BrowserRouter as Router } from 'react-router-dom';
4 | import { Provider } from 'react-redux';
5 | import configureStore from 'redux-mock-store'; // Import the mock store
6 | import { createStore } from 'redux';
7 | import rootReducer from '../redux/rootReducer'; // Adjust the path
8 |
9 | const mockStore = configureStore([]);
10 |
11 | export const renderWithRouterAndRedux = (
12 | ui,
13 | { route = '/', initialState = {} } = {}
14 | ) => {
15 | window.history.pushState({}, 'Test page', route);
16 |
17 | const store = mockStore(initialState); // Create a mock store with initial state
18 |
19 | return render(
20 |
21 | {ui}
22 |
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/client/src/index.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Inter&display=swap');
2 |
3 | /* default css setting*/
4 | html,
5 | body,
6 | #root,
7 | .app {
8 | height: 100%;
9 | width: 100%;
10 | font-family: "Inter" , sans-serif;
11 | }
12 |
--------------------------------------------------------------------------------
/client/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import App from './App.js';
4 | import { Provider } from 'react-redux';
5 | import { store, persistor } from '../src/redux/store.js';
6 | import { PersistGate } from 'redux-persist/integration/react';
7 | import { GoogleOAuthProvider } from '@react-oauth/google';
8 | import { WebSocketProvider } from '../src/redux/wsContext.js'; // ws global state
9 |
10 | // Create a root for rendering the application
11 | const root = ReactDOM.createRoot(document.getElementById('root'));
12 |
13 | root.render(
14 | // Render the application within the React-Redux Provider for state management
15 |
16 | {/* PersistGate delays the rendering of the app's UI until the persisted state has been retrieved and saved to redux */}
17 |
18 | {/* GoogleOAuthProvider for handling Google OAuth authentication */}
19 |
20 | {/* WebSocketProvider for managing WebSocket global state */}
21 |
22 | {/* Main application component */}
23 |
24 |
25 |
26 |
27 |
28 | );
29 |
--------------------------------------------------------------------------------
/client/src/pages/Clusters2.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { useSelector, useDispatch } from 'react-redux';
3 | import {
4 | Box,
5 | Typography,
6 | CssBaseline,
7 | Grid,
8 | Select,
9 | MenuItem,
10 | FormControl,
11 | InputLabel,
12 | useTheme,
13 | } from '@mui/material';
14 | import { useNavigate, useParams } from 'react-router-dom';
15 | import { fetchClusters, setAccountName, setClusterName, setRegion } from '../redux/userSlice.js';
16 | import ClusterDetails from '../components/clusterDetails';
17 | import BreadcrumbsNav from '../components/breadcrumbs.jsx';
18 |
19 | const Clusters2 = () => {
20 | const dispatch = useDispatch();
21 | const navigate = useNavigate();
22 | const theme = useTheme();
23 | const { accountName } = useParams();
24 | const {
25 | userId,
26 | clusters = [],
27 | clustersLoading = false,
28 | clustersError = '',
29 | } = useSelector((state) => state.user);
30 |
31 | const [selectedRegion, setSelectedRegion] = useState('');
32 |
33 | useEffect(() => {
34 | if (accountName) {
35 | dispatch(setAccountName(accountName));
36 | }
37 | }, [accountName, dispatch]);
38 |
39 | useEffect(() => {
40 | if (userId && accountName) {
41 | dispatch(fetchClusters({ userId, accountName }))
42 | .unwrap()
43 | .then((data) => {
44 | if (Array.isArray(data) && data.length === 0) {
45 | alert('No cluster there');
46 | }
47 | })
48 | .catch((error) => {
49 | alert('Enter credentials first');
50 | navigate('/credentials');
51 | });
52 | }
53 | }, [dispatch, userId, accountName, navigate]);
54 |
55 | useEffect(() => {
56 | if (clustersError && clustersError.notInDatabase) {
57 | alert('Enter credentials first');
58 | navigate('/credentials');
59 | }
60 | }, [clustersError, navigate]);
61 |
62 | if (clustersLoading) {
63 | return Loading clusters...;
64 | }
65 |
66 | const handleRegionClick = (region) => {
67 | setSelectedRegion(region);
68 | dispatch(setRegion(region));
69 | };
70 |
71 | const handleClusterClick = (clusterName) => {
72 | dispatch(setClusterName(clusterName));
73 | }
74 |
75 | const regionClusters = Array.isArray(clusters) ? clusters.find(
76 | (regionCluster) => regionCluster.region === selectedRegion
77 | ) : null;
78 |
79 | const breadcrumbsNav = [
80 | { name: 'Credentials', path: '/credentials' },
81 | { name: 'Accounts', path: '/accounts' },
82 | { name: 'Cluster', path: '/clusters/:accountName' },
83 | { name: 'Service', path: '/dashboard/:clusterName' },
84 | ];
85 | const currentPath = '/clusters/:accountName';
86 |
87 | return (
88 |
89 |
90 |
99 |
107 |
112 |
126 | Cluster Details
127 |
128 |
129 |
130 | {/* Dropdown Menu for Regions */}
131 |
132 | Select Region
133 |
146 |
147 |
148 | {selectedRegion ? (
149 |
150 | {regionClusters?.clusters.map((cluster, index) => (
151 | handleClusterClick(cluster.clusterName)}
158 | >
159 |
160 |
161 | ))}
162 |
163 | ) : (
164 | Select a region to view clusters.
165 | )}
166 |
167 |
168 | );
169 | };
170 |
171 | export default Clusters2;
172 |
--------------------------------------------------------------------------------
/client/src/pages/Dashboard.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import LineChart from '../components/LineChart.jsx';
3 | import {
4 | Box,
5 | useTheme,
6 | Typography,
7 | useMediaQuery,
8 | Breadcrumbs,
9 | } from '@mui/material';
10 | import PieChart from '../components/PieChart.jsx';
11 | import Service from '../components/Service.jsx';
12 | import StatusCard from '../components/StatusCard.jsx';
13 | import { useSelector, useDispatch } from 'react-redux';
14 | import { fetchCurrentUser } from '../redux/userSlice';
15 | import BreadcrumbsNav from '../components/breadcrumbs.jsx';
16 |
17 | const Dashboard = () => {
18 | const theme = useTheme();
19 | const isLargeScreen = useMediaQuery('(min-width: 1200px)'); // Larger screens
20 | const isTabletScreen = useMediaQuery(
21 | '(min-width: 600px) and (max-width: 1199px)'
22 | ); // Tablet screens
23 | const { userId, accountName, region, clusterName, serviceName } = useSelector((state) => ({
24 | userId: state.user.userId,
25 | accountName: state.user.accountName,
26 | region: state.user.region,
27 | clusterName: state.user.clusterName,
28 | serviceName: state.user.serviceName,
29 | }));
30 | const dispatch = useDispatch();
31 |
32 | useEffect(() => {
33 | if (!userId) {
34 | dispatch(fetchCurrentUser());
35 | }
36 | }, [dispatch, userId]);
37 |
38 | const breadcrumbsNav = [
39 | { name: 'Credentials', path: '/credentials' },
40 | { name: 'Accounts', path: '/accounts' },
41 | { name: 'Cluster', path: '/clusters/:accountName' },
42 | { name: 'Service', path: '/dashboard/:clusterName' },
43 | ];
44 | const currentPath = '/dashboard/:clusterName';
45 |
46 | return (
47 |
48 |
49 |
54 |
61 |
75 |
76 |
82 | Cluster: {clusterName}
83 |
84 |
90 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
112 |
118 | Tasks Overview
119 |
126 |
127 |
128 |
129 |
130 |
137 |
146 |
152 | CPUUtilization
153 |
161 |
162 |
163 |
164 |
173 |
179 | MemoryUtilization
180 |
188 |
189 |
190 |
191 |
192 |
199 |
208 |
214 | NetworkRxBytes
215 |
223 |
224 |
225 |
226 |
235 |
241 | NetworkTxBytes
242 |
250 |
251 |
252 |
253 |
254 |
255 | );
256 | };
257 |
258 | export default Dashboard;
259 |
--------------------------------------------------------------------------------
/client/src/pages/Layout.jsx:
--------------------------------------------------------------------------------
1 | import React , { useState } from 'react';
2 | import { Box, useMediaQuery } from '@mui/material';
3 | import { Outlet } from 'react-router-dom';
4 | import Navbar from '../components/Navbar.jsx';
5 | import Sidebar from '../components/Sidebar.jsx';
6 | //
7 | const Layout = () => {
8 | // check the screen// if it's a mobile or not
9 | const isNonMobile = useMediaQuery("(min-width: 600px)");
10 | // state for side bar // set default to true
11 | const [isSidebarOpen , setIsSidebarOpen] = useState(true);
12 | // responsive to mobile or computer size
13 | return (
14 | // material ui no need {}
15 | // outlet is represent the child element (which is )
16 |
17 |
23 |
24 |
25 |
29 |
30 |
31 |
32 |
33 | );
34 |
35 | };
36 |
37 | export default Layout;
38 |
--------------------------------------------------------------------------------
/client/src/pages/LogsNotification.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useMemo } from 'react'
2 | import Box from '@mui/material/Box'
3 | import { DataGrid } from '@mui/x-data-grid'
4 | import connectWebSocketNotifications from '../webService/connectWebSocketToNotifications'
5 | import { useSelector, useDispatch } from 'react-redux'
6 | import {
7 | markNotificationsAsRead,
8 | clearNotificationBadge,
9 | setReceivedNotifications,
10 | } from '../redux/notificationSlice'
11 | import Export from '../components/Export.jsx'
12 |
13 | // set the column fields for the table formatting
14 | const columns = [
15 | { field: 'time', headerName: 'Time', flex: 1 },
16 | { field: 'clusters', headerName: 'Cluster', flex: 1 },
17 | { field: 'service', headerName: 'Service', flex: 1 },
18 | { field: 'metric', headerName: 'Metric Name', flex: 1 },
19 | { field: 'value', headerName: 'Value', type: 'number', flex: 1 },
20 | { field: 'logs', headerName: 'Logs', sortable: false, flex: 4 },
21 | ]
22 |
23 | const LogsNotification = () => {
24 | // connect to webSocket
25 | connectWebSocketNotifications()
26 | const dispatch = useDispatch()
27 | // array of {} of the notications
28 | const receivedNotifications = useSelector(
29 | (state) => state.notification.receivedNotifications
30 | )
31 |
32 | // redux mark as read // clear badge
33 | useEffect(() => {
34 | const markAndClearNotifications = () => {
35 | const updatedNotifications = receivedNotifications.map(
36 | (notification) => ({
37 | ...notification,
38 | isRead: true,
39 | })
40 | )
41 | dispatch(setReceivedNotifications(updatedNotifications))
42 | dispatch(markNotificationsAsRead())
43 | dispatch(clearNotificationBadge())
44 | }
45 |
46 | if (receivedNotifications.some((notification) => !notification.isRead)) {
47 | markAndClearNotifications()
48 | }
49 | }, [dispatch, receivedNotifications])
50 |
51 | // check after the notification button has clicked
52 | useEffect(() => {
53 | console.log('Received notifications changed -->', receivedNotifications)
54 | }, [receivedNotifications])
55 |
56 | const rows = useMemo(
57 | () =>
58 | (receivedNotifications || []).map((data, index) => {
59 | if (data && data.timestamp) {
60 | return {
61 | id: index + 1,
62 | time: new Date(data.timestamp).toLocaleTimeString(),
63 | clusters: data.clusterName || 'N/A',
64 | service: data.serviceName || 'not applicable',
65 | metric: data.metricName || 'N/A',
66 | value: data.value || 0,
67 | logs: data.Logs || 'N/A',
68 | }
69 | } else {
70 | return {
71 | id: index + 1,
72 | time: 'Invalid Data',
73 | clusters: 'Invalid Data',
74 | service: 'Invalid Data',
75 | metric: 'Invalid Data',
76 | value: 'Invalid Data',
77 | logs: 'Invalid Data',
78 | }
79 | }
80 | }),
81 | [receivedNotifications]
82 | )
83 |
84 | return (
85 |
86 |
87 |
124 |
125 | {/* rows props*/}
126 |
127 |
128 | )
129 | }
130 |
131 | export default LogsNotification;
132 |
--------------------------------------------------------------------------------
/client/src/pages/Overview.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import {
3 | Box,
4 | useTheme,
5 | Typography,
6 | Autocomplete,
7 | TextField,
8 | Tabs,
9 | Tab,
10 | } from '@mui/material'
11 | import { useDispatch, useSelector } from 'react-redux'
12 | import { setServiceName, setAccountName, setClusterName, fetchAccounts, fetchClusters } from '../redux/userSlice.js'
13 | import PieChart from '../components/PieChart.jsx'
14 |
15 |
16 | const Overview = () => {
17 | const [serviceNames, setServiceNames] = useState([]);
18 | const [accountNames, setAccountNames] = useState([]);
19 | const [clusterNames, setClusterNames] = useState([]);
20 | const [inputValueService, setInputValueService] = useState('')
21 | const [inputValueAccount, setInputValueAccount] = useState('')
22 | const [inputValueCluster, setInputValueCluster] = useState('')
23 | const dispatch = useDispatch()
24 | const theme = useTheme();
25 |
26 | const { userId, accountName, clusterName, serviceName, region } = useSelector((state) => ({
27 | userId: state.user.userId,
28 | accountName: state.user.accountName,
29 | clusterName: state.user.clusterName,
30 | serviceName: state.user.serviceName,
31 | region: state.user.region
32 | }));
33 |
34 | useEffect(() => {
35 | if (userId) {
36 | dispatch(fetchAccounts(userId))
37 | .unwrap()
38 | .then((data) => {
39 | const combinedAccountNames = [
40 | ...data.root.map(account => account.account_name),
41 | ...data.subaccount.map(account => account.account_name)
42 | ];
43 | setAccountNames(combinedAccountNames);
44 | })
45 | .catch((error) => {
46 | console.error('Error fetching account names:', error);
47 | });
48 | }
49 | }, [userId, dispatch]);
50 |
51 | useEffect(() => {
52 | if (userId && accountName) {
53 | dispatch(fetchClusters({ userId, accountName }))
54 | .unwrap()
55 | .then((data) => {
56 | const clusterNames = data.flatMap(regionData => regionData.clusters.map(cluster => cluster.clusterName));
57 | setClusterNames(clusterNames);
58 | })
59 | .catch((error) => {
60 | console.error('Error fetching cluster names:', error);
61 | });
62 | }
63 | }, [userId, accountName, dispatch]);
64 |
65 | // fetching services from backend
66 | useEffect(() => {
67 | if (userId && accountName && clusterName) {
68 | fetch(
69 | `http://localhost:3000/list/AllServices?userId=${userId}&accountName=${accountName}&clusterName=${clusterName}®ion=${region}`
70 | )
71 | .then((response) => response.json())
72 | .then((data) => {
73 | console.log('Fetching service names -->', data)
74 | if (data && data.length > 0) {
75 | setServiceNames(data)
76 | dispatch(setServiceName(data[0]));
77 | } else {
78 | setServiceNames([]);
79 | dispatch(setServiceName(null));
80 | throw new Error('No services found')
81 | }
82 | })
83 | .catch((error) => {
84 | console.error('Error fetching service names:', error)
85 | })
86 | }
87 | }, [userId, accountName, clusterName, region, dispatch])
88 |
89 |
90 |
91 | return (
92 |
93 |
94 |
95 |
96 | {/* account dropdown */}
97 | {
100 | dispatch(setAccountName(newValue));
101 | }}
102 | inputValue={inputValueAccount}
103 | onInputChange={(event, newInputValue) => {
104 | setInputValueAccount(newInputValue)
105 | }}
106 | id="account-name-dropdown"
107 | options={accountNames}
108 | sx={{ minWidth: 300 }}
109 | renderInput={(params) => (
110 |
111 | )}
112 | />
113 |
114 | {/* cluster dropdown */}
115 | {
118 | dispatch(setClusterName(newValue));
119 | }}
120 | inputValue={inputValueCluster}
121 | onInputChange={(event, newInputValue) => {
122 | setInputValueCluster(newInputValue)
123 | }}
124 | id="cluster-name-dropdown"
125 | options={clusterNames} // change this later
126 | sx={{ minWidth: 300 }}
127 | renderInput={(params) => (
128 |
129 | )}
130 | />
131 |
132 | {/* service dropdown */}
133 | {
136 | dispatch(setServiceName(newValue));
137 | }}
138 | inputValue={inputValueService}
139 | onInputChange={(event, newInputValue) => {
140 | setInputValueService(newInputValue)
141 | }}
142 | id="service-name-dropdown"
143 | options={serviceNames}
144 | sx={{ minWidth: 300 }}
145 | renderInput={(params) => (
146 |
147 | )}
148 | />
149 |
150 |
151 | {/* hightlight tab for summary */}
152 |
164 |
165 | Account:{' '}
166 | {accountName !== null
167 | ? accountName
168 | : 'Please choose your account name'}{' '}
169 | | Cluster:{' '}
170 | {clusterName !== null
171 | ? clusterName
172 | : 'Please choose your cluster name'}{' '}
173 | | Service:{' '}
174 | {serviceName !== null
175 | ? serviceName
176 | : 'Please choose your service name'}
177 |
178 |
179 |
180 |
181 |
182 |
183 | )
184 | }
185 |
186 | export default Overview;
187 |
188 |
--------------------------------------------------------------------------------
/client/src/pages/UserProfile.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import { useSelector, useDispatch } from 'react-redux'
3 | import { useNavigate } from 'react-router-dom'
4 | import {
5 | Box,
6 | Container,
7 | Typography,
8 | Avatar,
9 | Alert,
10 | TextField,
11 | IconButton,
12 | Tooltip,
13 | Button,
14 | } from '@mui/material'
15 | import { Link } from 'react-router-dom'
16 | import { updateProfile } from '../redux/userSlice'
17 | import EditIcon from '@mui/icons-material/Edit'
18 | import TaskAltIcon from '@mui/icons-material/TaskAlt'
19 | import CancelIcon from '@mui/icons-material/Cancel'
20 | import Navbar from '../components/Navbar'
21 | import { useTheme } from '@emotion/react'
22 |
23 | // UserProfile component
24 | const UserProfile = () => {
25 | // Get user data from Redux store
26 | const user = useSelector((state) => state.user)
27 | const dispatch = useDispatch()
28 | // State for the username and password input field
29 | const [username, setNewUsername] = useState(user.username || '')
30 | const [email, setNewEmail] = useState(user.email || '')
31 | // State to track if the user is in edit mode
32 | const [isEditing, setIsEditing] = useState(false)
33 | // State to track any error and success
34 | const [error, setError] = useState(null)
35 | const [success, setSuccess] = useState(null)
36 | const theme = useTheme()
37 | const navigate = useNavigate()
38 |
39 | // UseEffect to update the input fields when the user data changes
40 | useEffect(() => {
41 | setNewUsername(user.username || '')
42 | setNewEmail(user.email || '')
43 | }, [user])
44 |
45 | // handle edit button
46 | const handleEditButton = () => {
47 | // set isEditing to be true
48 | setIsEditing(true)
49 | // set Error
50 | setError(null)
51 | // set success
52 | setSuccess(null)
53 | }
54 |
55 | // save button function // onClick={saveChangeBtn}
56 | // after click it alert 'password has been changed'
57 | const saveChangeBtn = async () => {
58 | if (!username || !email) {
59 | setError('Username and Password are required.')
60 | return
61 | }
62 |
63 | try {
64 | dispatch(updateProfile({ username, email }))
65 | const response = await fetch(
66 | 'http://localhost:3000/reset-email-username',
67 | {
68 | method: 'POST',
69 | headers: {
70 | 'Content-Type': 'application/json',
71 | },
72 | body: JSON.stringify({ userId: user.userId, username, email }),
73 | }
74 | )
75 |
76 | if (!response.ok) {
77 | throw new Error('Failed to update profile.')
78 | }
79 | setSuccess('Profile updated successfully!')
80 | setIsEditing(false)
81 | } catch (err) {
82 | setError('Failed to updat profile.')
83 | }
84 | }
85 |
86 | // cancel btn function
87 | //onClick={cancelBtn}
88 | const cancelBtn = () => {
89 | setNewUsername(user.username || '')
90 | setNewEmail(user.email || '')
91 | setIsEditing(false)
92 | setError(null)
93 | setSuccess(null)
94 | }
95 |
96 | return (
97 |
98 | {/* Navbar component */}
99 |
100 |
101 | {/* Form container */}
102 |
110 | {/* set onSubmit form function handleSubmit */}
111 |
127 | {/*
128 | User Profile
129 | */}
130 |
141 | {error && (
142 |
143 | {error}
144 |
145 | )}
146 | {success && (
147 |
148 | {success}
149 |
150 | )}
151 |
156 | Credentials
157 |
158 | {/* first name // unable to change */}
159 |
166 | {/* last name // unable to change */}
167 |
174 |
175 | {/* username // setnewUsername */}
176 | setNewUsername(e.target.value)}
181 | fullWidth
182 | disabled={!isEditing}
183 | sx={{
184 | backgroundColor: isEditing
185 | ? theme.palette.action.selected
186 | : 'inherit',
187 | }}
188 | />
189 |
190 | {/* email // setNewEmail */}
191 | setNewEmail(e.target.value)}
197 | fullWidth
198 | disabled={!isEditing}
199 | sx={{
200 | backgroundColor: isEditing
201 | ? theme.palette.action.selected
202 | : 'inherit',
203 | }}
204 | />
205 |
206 | {/* if editing mode */}
207 | {isEditing ? (
208 | // if it's in editng mode
209 |
217 |
218 |
223 | {/* Save button */}
224 |
225 |
226 |
227 |
228 |
229 |
230 | {/* cancel button */}
231 |
232 |
233 |
234 |
235 | ) : (
236 | // if not in editing
237 |
238 |
243 | {/* edit button */}
244 |
245 |
246 |
247 | )}
248 |
257 |
258 |
259 |
260 |
261 | )
262 | }
263 |
264 | export default UserProfile
265 |
--------------------------------------------------------------------------------
/client/src/pages/accounts.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react'
2 | import { useSelector, useDispatch } from 'react-redux'
3 | import { Box, Typography, useTheme, Grid, Paper } from '@mui/material'
4 | import { useNavigate } from 'react-router-dom'
5 | import {
6 | fetchAccounts,
7 | fetchSubAccountDetails,
8 | selectAccount,
9 | } from '../redux/userSlice.js'
10 | import AccountDetails from '../components/accountDetails.jsx'
11 | import BreadcrumbsNav from '../components/breadcrumbs.jsx'
12 |
13 | const Accounts = () => {
14 | const theme = useTheme()
15 | const dispatch = useDispatch()
16 | const navigate = useNavigate()
17 | const {
18 | userId,
19 | rootAccounts = [],
20 | subAccounts = [],
21 | accountsLoading,
22 | accountsError,
23 | selectedSubAccountDetails = [],
24 | } = useSelector((state) => state.user)
25 |
26 | useEffect(() => {
27 | if (userId) {
28 | dispatch(fetchAccounts(userId))
29 | }
30 | }, [dispatch, userId])
31 |
32 | const handleAccountClick = (account, accountType) => {
33 | if (account) {
34 | dispatch(selectAccount({ account, accountType }))
35 | if (accountType === 'Root') {
36 | dispatch(
37 | fetchSubAccountDetails({ userId, accountName: account.account_name })
38 | )
39 | }
40 | }
41 | }
42 |
43 | if (accountsLoading) {
44 | return Loading...
45 | }
46 |
47 | if (accountsError) {
48 | return Error: {accountsError}
49 | }
50 |
51 | const breadcrumbsNav = [
52 | { name: 'Credentials', path: '/credentials' },
53 | { name: 'Accounts', path: '/accounts' },
54 | { name: 'Cluster', path: '/clusters/:accountName' },
55 | { name: 'Service', path: '/dashboard/:clusterName' },
56 | ]
57 | const currentPath = '/clusters/accounts'
58 |
59 | return (
60 |
61 |
70 |
78 |
83 |
97 | Account Details
98 |
99 |
100 |
101 |
102 | {selectedSubAccountDetails.map((account, index) => (
103 |
110 |
121 | navigate(`/dashboard/${account.account_name}`)}
125 | />
126 |
127 |
128 | ))}
129 |
130 |
131 |
132 | )
133 | }
134 |
135 | export default Accounts
136 |
--------------------------------------------------------------------------------
/client/src/pages/clusters.jsx:
--------------------------------------------------------------------------------
1 | // import React, { useEffect, useState } from 'react';
2 | // import { useSelector, useDispatch } from 'react-redux';
3 | // import {
4 | // Box,
5 | // Typography,
6 | // List,
7 | // ListItem,
8 | // ListItemText,
9 | // useTheme,
10 | // IconButton,
11 | // Divider,
12 | // Drawer,
13 | // CssBaseline,
14 | // Grid,
15 | // } from '@mui/material';
16 | // import { useNavigate, useParams } from 'react-router-dom';
17 | // import logo from '../assets/logo.png';
18 | // import FlexBetween from '../components/FlexBetween';
19 | // import MenuIcon from '@mui/icons-material/Menu';
20 | // import CloseIcon from '@mui/icons-material/Close';
21 | // import { fetchClusters } from '../redux/userSlice.js';
22 | // import ClusterDetails from '../components/clusterDetails';
23 | // import BreadcrumbsNav from '../components/breadcrumbs.jsx';
24 |
25 | // const drawerWidth = 300;
26 |
27 | // const Clusters = () => {
28 | // const dispatch = useDispatch();
29 | // const navigate = useNavigate();
30 | // const theme = useTheme();
31 | // const { accountName } = useParams();
32 | // const {
33 | // userId,
34 | // clusters = [],
35 | // clustersLoading = false,
36 | // clustersError = '',
37 | // } = useSelector((state) => state.user);
38 |
39 | // const [drawerOpen, setDrawerOpen] = useState(true);
40 | // const [selectedRegion, setSelectedRegion] = useState(null);
41 |
42 | // useEffect(() => {
43 | // if (userId && accountName) {
44 | // dispatch(fetchClusters({ userId, accountName }))
45 | // .unwrap()
46 | // .then((data) => {
47 | // if (data.clusters.length === 0) {
48 | // navigate('/credentials');
49 | // }
50 | // })
51 | // .catch((error) => {
52 | // console.error('Error fetching clusters:', error);
53 | // });
54 | // }
55 | // }, [dispatch, userId, accountName, navigate]);
56 |
57 | // useEffect(() => {
58 | // if (clusters.length === 0) {
59 | // navigate('/credentials');
60 | // }
61 | // }, [clusters, navigate]);
62 |
63 | // if (clustersLoading) {
64 | // return Loading clusters...;
65 | // }
66 |
67 | // const handleRegionClick = (region) => {
68 | // setSelectedRegion(region);
69 | // };
70 |
71 | // const regionClusters = clusters.find(
72 | // (regionCluster) => regionCluster.region === selectedRegion
73 | // );
74 |
75 | // const breadcrumbsNav = [
76 | // { name: 'Credentials', path: '/credentials' },
77 | // { name: 'Accounts', path: '/accounts' },
78 | // { name: 'Cluster', path: '/clusters/:accountName' },
79 | // { name: 'Service', path: '/dashboard/:clusterName' },
80 | // ];
81 | // const currentPath = '/clusters/:accountName';
82 |
83 | // return (
84 | // // drawer
85 | //
86 | //
87 | //
102 | //
112 | // navigate('/dashboard')}
117 | // height='100px'
118 | // width='100px'
119 | // borderRadius='28%'
120 | // sx={{
121 | // objectFit: 'cover',
122 | // borderColor: theme.palette.primary[400],
123 | // borderStyle: 'solid',
124 | // borderWidth: 1,
125 | // cursor: 'pointer',
126 | // }}
127 | // />
128 | // setDrawerOpen(!drawerOpen)}
130 | // sx={{ position: 'absolute', top: '10px', right: '10px' }}
131 | // >
132 | //
133 | //
134 | //
135 | //
136 | //
137 | // Regions
138 | //
139 | //
140 | //
141 | // {clusters.map((regionCluster, index) => (
142 | // handleRegionClick(regionCluster.region)}
146 | // >
147 | //
148 | //
149 | // ))}
150 | //
151 | //
152 | //
153 | //
154 | // {!drawerOpen && (
155 | // setDrawerOpen(true)}
157 | // sx={{
158 | // position: 'fixed',
159 | // left: 0,
160 | // top: '1rem',
161 | // borderRadius: '50%',
162 | // width: '40px',
163 | // height: '40px',
164 | // }}
165 | // >
166 | //
167 | //
168 | // )}
169 | //
178 | //
186 | //
191 | //
195 | // Cluster Details
196 | //
197 | //
198 | // {selectedRegion ? (
199 | //
200 | // {regionClusters?.clusters.map((cluster, index) => (
201 | //
208 | //
209 | //
210 | // ))}
211 | //
212 | // ) : (
213 | // Select a region to view clusters.
214 | // )}
215 | //
216 | //
217 | // );
218 | // };
219 |
220 | // export default Clusters;
221 |
--------------------------------------------------------------------------------
/client/src/pages/credentials.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { useNavigate } from 'react-router-dom';
3 | import { useSelector, useDispatch } from 'react-redux';
4 | import { fetchCurrentUser } from '../redux/userSlice';
5 | import {
6 | Container,
7 | TextField,
8 | Button,
9 | Box,
10 | Typography,
11 | Paper,
12 | FormControl,
13 | InputLabel,
14 | Select,
15 | MenuItem,
16 | } from '@mui/material';
17 | import { useTheme } from '@mui/material/styles';
18 | import Navbar from '../components/Navbar.jsx';
19 |
20 | const Credentials = () => {
21 | const [accessKey, setAccessKey] = useState('');
22 | const [secretKey, setSecretKey] = useState('');
23 | const [accountType, setAccType] = useState('');
24 | const [accountName, setAccName] = useState('');
25 | const navigate = useNavigate();
26 | const theme = useTheme();
27 |
28 | // from redux store
29 | const dispatch = useDispatch();
30 | const userId = useSelector((state) => state.user.userId);
31 |
32 | useEffect(() => {
33 | if (!userId) {
34 | dispatch(fetchCurrentUser());
35 | }
36 | }, [dispatch, userId]);
37 |
38 | const handleSubmit = async (e) => {
39 | e.preventDefault();
40 |
41 | try {
42 | const response = await fetch('http://localhost:3000/credentials', {
43 | method: 'POST',
44 | headers: {
45 | 'Content-Type': 'application/json',
46 | },
47 | body: JSON.stringify({
48 | userId,
49 | accountType,
50 | accessKey,
51 | secretKey,
52 | accountName,
53 | }),
54 | });
55 |
56 | if (!response.ok) {
57 | const errorData = await response.json();
58 | throw new Error('Error in fetching credentials!');
59 | }
60 |
61 | console.log('Credentials saved successfully');
62 | navigate('/accounts');
63 | } catch (error) {
64 | console.error('Error saving credentials:', error.message);
65 | }
66 | };
67 |
68 | return (
69 |
70 |
76 |
77 |
85 |
97 |
105 | Enter AWS Credentials
106 |
107 |
116 |
117 |
121 | Account Type
122 |
123 |
133 |
134 | setAccessKey(e.target.value)}
139 | required
140 | fullWidth
141 | InputLabelProps={{
142 | style: { color: theme.palette.primary.contrastText },
143 | }}
144 | InputProps={{
145 | style: { color: theme.palette.primary.contrastText },
146 | }}
147 | />
148 | setSecretKey(e.target.value)}
153 | required
154 | fullWidth
155 | InputLabelProps={{
156 | style: { color: theme.palette.primary.contrastText },
157 | }}
158 | InputProps={{
159 | style: { color: theme.palette.primary.contrastText },
160 | }}
161 | />
162 | setAccName(e.target.value)}
167 | required
168 | fullWidth
169 | InputLabelProps={{
170 | style: { color: theme.palette.primary.contrastText },
171 | }}
172 | InputProps={{
173 | style: { color: theme.palette.primary.contrastText },
174 | }}
175 | />
176 |
185 |
186 | Can't find it? Return to home and read our Get Started to access
187 | these information.
188 |
189 |
198 |
199 |
200 |
201 |
202 |
203 | );
204 | };
205 |
206 | export default Credentials;
207 |
--------------------------------------------------------------------------------
/client/src/pages/getstarted.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Container, Box, Typography, Paper } from '@mui/material';
3 | import { useTheme } from '@mui/material/styles';
4 | import Tutorial from '../components/tutorial';
5 |
6 | const GetStarted = () => {
7 | const theme = useTheme();
8 |
9 | return (
10 |
11 |
19 |
20 | Get Started
21 |
22 |
32 |
40 |
41 |
42 |
43 |
44 |
45 | );
46 | };
47 |
48 | export default GetStarted;
49 |
--------------------------------------------------------------------------------
/client/src/pages/home.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import {
3 | Container,
4 | Box,
5 | Button,
6 | AppBar,
7 | Toolbar,
8 | Typography,
9 | useTheme,
10 | IconButton,
11 | } from '@mui/material'
12 | import { useNavigate } from 'react-router-dom'
13 | import nobglogo from '../assets/nobglogo.png'
14 | import LoginIcon from '@mui/icons-material/Login'
15 | import HomeOutlinedIcon from '@mui/icons-material/HomeOutlined'
16 | import GettingStartedIcon from '@mui/icons-material/PlayCircleOutline'
17 | import Team from './team'
18 | import { useDispatch } from 'react-redux'
19 | import { setMode } from '../redux/globalSlice.js'
20 | import LightModeOutlinedIcon from '@mui/icons-material/LightModeOutlined'
21 | import DarkModeOutlinedIcon from '@mui/icons-material/DarkModeOutlined'
22 |
23 | // Home component
24 | const Home = () => {
25 | // Hook to navigate between routes
26 | const navigate = useNavigate();
27 | // Hook to access the current theme
28 | const theme = useTheme();
29 | // Hook to dispatch actions to the Redux store
30 | const dispatch = useDispatch();
31 |
32 | return (
33 | <>
34 | {/* AppBar for the top navigation bar */}
35 |
39 |
40 | {/* Container for logo and navigation buttons */}
41 |
42 | {/* Logo image */}
43 |
48 | {/* Home button */}
49 |
63 | {/* Get Started button */}
64 |
78 |
79 | {/* Theme toggle button */}
80 | dispatch(setMode())}>
81 | {theme.palette.mode === 'dark' ? (
82 |
83 | ) : (
84 |
85 | )}
86 |
87 | {/* Login/Signup button */}
88 |
102 |
103 |
104 | {/* Main content container */}
105 |
106 | {/* Title */}
107 |
112 | DeClustor
113 |
114 | {/* Description */}
115 |
120 | Welcome to DeClustor, your centralized solution for monitoring and
121 | managing ECS environments on AWS. Track metrics and monitor real-time
122 | performance across multiple ECS clusters effortlessly.
123 |
124 | {/* Get Started button */}
125 |
134 |
135 | {/* Team component */}
136 |
137 |
138 |
139 | >
140 | )
141 | }
142 |
143 | export default Home
144 |
--------------------------------------------------------------------------------
/client/src/pages/login.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { useNavigate } from 'react-router-dom';
3 | import {
4 | Container,
5 | TextField,
6 | Button,
7 | Box,
8 | Avatar,
9 | Typography,
10 | Alert,
11 | } from '@mui/material';
12 | import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
13 | import { loginSuccess, loginFailure } from '../redux/userSlice';
14 | import { useDispatch } from 'react-redux';
15 | import { useTheme } from '@mui/material/styles';
16 | import Google from '../assets/g.jpg';
17 | import GitHub from '../assets/gh2.jpg';
18 | import Navbar from '../components/Navbar';
19 |
20 | const Login = () => {
21 | // State for username and password input fields
22 | const [email, setEmail] = useState('');
23 | const [password, setPassword] = useState('');
24 | const [error, setError] = useState('');
25 | const navigate = useNavigate();
26 | const dispatch = useDispatch();
27 | const theme = useTheme();
28 |
29 | const google = () => {
30 | window.open('http://localhost:3000/auth/google', '_self');
31 | };
32 |
33 | const github = () => {
34 | window.open('http://localhost:3000/auth/github', '_self');
35 | };
36 |
37 | const handleSubmit = async (e) => {
38 | e.preventDefault();
39 |
40 | try {
41 | const response = await fetch('http://localhost:3000/login', {
42 | method: 'POST',
43 | headers: {
44 | 'Content-Type': 'application/json',
45 | },
46 | body: JSON.stringify({ email, password }),
47 | });
48 |
49 | const data = await response.json();
50 |
51 | if (response.ok) {
52 | // Save data to local storage
53 | localStorage.setItem('username', data.username);
54 | localStorage.setItem('userId', data.userId);
55 | localStorage.setItem('password', password);
56 | console.log('saved to local storage! -->', data);
57 |
58 | console.log(
59 | 'Login successful, dispatching loginSuccess with data:',
60 | data
61 | );
62 | dispatch(
63 | loginSuccess({
64 | userId: data.userId,
65 | firstName: data.firstName,
66 | lastName: data.lastName,
67 | username: data.userName,
68 | email: data.email
69 | })
70 | );
71 | navigate('/dashboard');
72 | } else {
73 | dispatch(loginFailure(data.message));
74 | setError(data.message);
75 | }
76 | } catch (error) {
77 | console.error('Error:', error);
78 | setError('An error occurred. Please try again.');
79 | }
80 | };
81 |
82 | return (
83 |
84 |
90 |
91 |
99 |
100 |
101 |
102 |
103 | Login In
104 |
105 | {error && (
106 |
115 | {error}
116 |
117 | )}
118 |
134 | {/* Email input field */}
135 | setEmail(e.target.value)}
140 | required
141 | fullWidth
142 | />
143 | setPassword(e.target.value)}
149 | required
150 | fullWidth
151 | />
152 |
161 |
170 |
194 |
218 |
219 |
226 |
233 |
234 |
235 |
236 |
237 | );
238 | };
239 |
240 | export default Login;
241 |
--------------------------------------------------------------------------------
/client/src/pages/team.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Box, Container, Typography, Grid, Avatar , useTheme } from '@mui/material';
3 | import grace from '../assets/grace.png';
4 | import aria from '../assets/aria.png';
5 | import will from '../assets/will.png';
6 | import ploy from '../assets/ploy.png';
7 |
8 | // Array of team member objects with their names and image paths
9 | const teamMembers = [
10 | { name: 'Grace Lo', img: grace },
11 | { name: 'Aria Liang', img: aria },
12 | { name: 'Will Di', img: will },
13 | { name: 'Ploynapa Yang', img: ploy },
14 | ];
15 |
16 | // Team component
17 | const Team = () => {
18 | const theme = useTheme(); // Hook to access the current theme
19 |
20 | return (
21 |
28 |
29 | {/* Main heading */}
30 |
31 | Meet Our Team
32 |
33 | {/* Subheading */}
34 |
35 | If you have any questions about our open source project, feel free to
36 | reach out to us!
37 |
38 | {/* Grid container for team members */}
39 |
40 | {/* Mapping over team members array to create a grid item for each member */}
41 | {teamMembers.map((member, index) => (
42 |
43 |
48 | {/* Team member's name */}
49 | {member.name}
50 | {/* Placeholder for team member's role */}
51 |
52 | {member.role}
53 |
54 |
55 | ))}
56 |
57 |
58 |
59 | );
60 | };
61 |
62 | export default Team;
63 |
--------------------------------------------------------------------------------
/client/src/redux/globalSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice } from '@reduxjs/toolkit';
2 |
3 | // declare an initial state
4 | const initialState = {
5 | // set initial mode to dark
6 | mode: 'dark',
7 | // add userId from database
8 | // userId: '123', // <= change this later
9 | };
10 |
11 | // global state
12 | export const globalSlice = createSlice({
13 | name: 'global',
14 | // pass in initial
15 | initialState,
16 | reducers: {
17 | // set the mode
18 | setMode: (state) => {
19 | // check if the mode is light ? if yes, switch to dark mode
20 | state.mode = state.mode === 'light' ? 'dark' : 'light';
21 | },
22 | },
23 | });
24 |
25 | export const { setMode } = globalSlice.actions;
26 |
27 | export default globalSlice.reducer;
28 |
--------------------------------------------------------------------------------
/client/src/redux/notificationSlice.js:
--------------------------------------------------------------------------------
1 | import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
2 |
3 | // Initial state for the notification slice
4 | const initialState = {
5 | clusters: 'allClusters',
6 | services: 'allServices',
7 | clusterOptions: [],
8 | serviceOptions: [],
9 | notifications: [
10 | {
11 | metric: 'CPUUtilization',
12 | threshold: 0,
13 | operator: 'greaterThan', // default to '>'
14 | isEnable: false,
15 | },
16 | {
17 | metric: 'MemoryUtilization',
18 | threshold: 0,
19 | operator: 'greaterThan',
20 | isEnable: false,
21 | },
22 | {
23 | metric: 'NetworkRxBytes',
24 | threshold: 0,
25 | operator: 'greaterThan',
26 | isEnable: false,
27 | },
28 | {
29 | metric: 'NetworkTxBytes',
30 | threshold: 0,
31 | operator: 'greaterThan',
32 | isEnable: false,
33 | },
34 | ],
35 | receivedNotifications: [],
36 | unreadNotificationCount: 0, // default to 0
37 | loading: false,
38 | error: null,
39 | };
40 |
41 | // Create the notification slice
42 | const notificationSlice = createSlice({
43 | name: 'notification',
44 | initialState,
45 | reducers: {
46 | // Set the current cluster
47 | setCluster: (state, action) => {
48 | state.clusters = action.payload;
49 | },
50 | // Set the current service
51 | setService: (state, action) => {
52 | state.services = action.payload;
53 | },
54 | // Update a specific notification
55 | updateNotification: (state, action) => {
56 | const { index, key, value } = action.payload;
57 | state.notifications[index][key] = value;
58 | },
59 | // Set the notifications list
60 | setNotifications: (state, action) => {
61 | state.notifications = action.payload;
62 | },
63 | // Set the received notifications and update unread count
64 | setReceivedNotifications: (state, action) => {
65 | state.receivedNotifications = action.payload;
66 | state.unreadNotificationCount = action.payload.filter(notification => !notification.isRead).length;
67 | },
68 | // Clear the unread notification badge
69 | clearNotificationBadge: (state) => {
70 | state.unreadNotificationCount = 0; // only clear badge out
71 | },
72 | // Mark all notifications as read
73 | markNotificationsAsRead: (state) => {
74 | state.receivedNotifications = state.receivedNotifications.map(notification => ({
75 | ...notification,
76 | isRead: true,
77 | }));
78 | state.unreadNotificationCount = 0; // reset unread count!!!
79 | },
80 | },
81 | extraReducers: (builder) => {
82 | builder
83 | .addCase(saveNotifications.pending, (state) => {
84 | state.loading = true; // Set loading state when saveNotifications is pending
85 | })
86 | .addCase(saveNotifications.fulfilled, (state, action) => {
87 | state.loading = false; // Clear loading state when saveNotifications is fulfilled
88 | // handle successful saving if needed
89 | })
90 | .addCase(saveNotifications.rejected, (state, action) => {
91 | state.loading = false; // Clear loading state when saveNotifications is rejected
92 | state.error = action.payload;
93 | });
94 | },
95 | });
96 |
97 | // Export actions from the slice
98 | export const { setCluster, setService, updateNotification, setNotifications, setReceivedNotifications, clearNotificationBadge, markNotificationsAsRead } = notificationSlice.actions;
99 |
100 | // Async thunk for saving notifications
101 | export const saveNotifications = createAsyncThunk(
102 | 'notification/saveNotifications',
103 | async ({ userId, accountName, clusterName, region, notifications }, { rejectWithValue }) => {
104 | try {
105 | console.log('Saving notifications:', notifications);
106 | const response = await fetch(`http://localhost:3000/setNotification?userId=${userId}&accountName=${accountName}&clusterName=${clusterName}®ion=${region}`, {
107 | method: 'POST',
108 | headers: {
109 | 'Content-Type': 'application/json'
110 | },
111 | body: JSON.stringify({ notifications })
112 | });
113 | if (!response.ok) {
114 | throw new Error('Failed to save notifications');
115 | }
116 | return await response.json();
117 | } catch (error) {
118 | return rejectWithValue(error.message);
119 | }
120 | }
121 | );
122 |
123 | // Export the reducer from the slice
124 | export default notificationSlice.reducer;
125 |
126 |
127 |
--------------------------------------------------------------------------------
/client/src/redux/rootReducer.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import globalReducer from './globalSlice.js';
3 | import userReducer from './userSlice.js';
4 | import notificationReducer from './notificationSlice.js';
5 |
6 | const rootReducer = combineReducers({
7 | global: globalReducer,
8 | user: userReducer,
9 | notification: notificationReducer,
10 | });
11 |
12 | export { rootReducer };
13 |
--------------------------------------------------------------------------------
/client/src/redux/store.js:
--------------------------------------------------------------------------------
1 | import { configureStore } from '@reduxjs/toolkit';
2 | import { persistStore, persistReducer } from 'redux-persist';
3 | import storage from 'redux-persist/lib/storage';
4 | import { combineReducers } from 'redux';
5 | import globalReducer from './globalSlice.js';
6 | import userReducer from './userSlice.js';
7 | import notificationReducer from './notificationSlice.js';
8 |
9 | // Configuration for persisting the Redux store
10 | const persistConfig = {
11 | key: 'root',
12 | storage,
13 | };
14 |
15 | // Combine all the reducers into a rootReducer
16 | const rootReducer = combineReducers({
17 | global: globalReducer,
18 | user: userReducer,
19 | notification: notificationReducer,
20 | });
21 |
22 | // Create a persisted reducer using the persistConfig and rootReducer
23 | const persistedReducer = persistReducer(persistConfig, rootReducer);
24 |
25 | // Create the Redux store with the persisted reducer and configure middleware
26 | const store = configureStore({
27 | reducer: persistedReducer,
28 | middleware: (getDefaultMiddleware) =>
29 | getDefaultMiddleware({
30 | serializableCheck: false,
31 | }),
32 | });
33 |
34 | // Create a persistor for the Redux store to enable persistence
35 | const persistor = persistStore(store);
36 |
37 | // Export the Redux store and persistor
38 | export { store, persistor };
39 |
--------------------------------------------------------------------------------
/client/src/redux/wsContext.js:
--------------------------------------------------------------------------------
1 | import React, { createContext, useContext, useEffect, useState } from 'react';
2 | import { useDispatch } from 'react-redux';
3 | import { setReceivedNotifications } from '../redux/notificationSlice';
4 |
5 | // Create a context for WebSocket
6 | const WebSocketContext = createContext();
7 |
8 | // global webSocket for live notification
9 | export const WebSocketProvider = ({ children }) => {
10 | const [ws, setWs] = useState(null);
11 | const dispatch = useDispatch();
12 |
13 | useEffect(() => {
14 | // connect to check notifications endpoint
15 | const wsNotification = new WebSocket('ws://localhost:3000/checkNotifications?userId=1');
16 |
17 | // Event listener for WebSocket connection open
18 | wsNotification.onopen = () => {
19 | console.log('WebSocket connection established.');
20 | };
21 |
22 | // Event listener for WebSocket connection close
23 | wsNotification.onclose = () => {
24 | console.log('WebSocket connection closed.');
25 | };
26 |
27 | // Event listener for WebSocket errors
28 | wsNotification.onerror = (error) => {
29 | console.error('WebSocket error:', error);
30 | };
31 |
32 | // Event listener for receiving messages from the WebSocket
33 | wsNotification.onmessage = (event) => {
34 | const data = JSON.parse(event.data);
35 | console.log('Received notification data:', data);
36 | if (Array.isArray(data)) {
37 | // Dispatch received notifications to Redux store
38 | dispatch(setReceivedNotifications(data));
39 | } else if (data.message === 'No new notifications') {
40 | // Dispatch empty array if no new notifications
41 | dispatch(setReceivedNotifications([]));
42 | } else {
43 | console.error('Invalid notification data received:', data);
44 | }
45 | };
46 |
47 | // Store WebSocket instance in state
48 | setWs(wsNotification);
49 |
50 | // Cleanup function to close WebSocket connection on component unmount
51 | return () => {
52 | wsNotification.close();
53 | };
54 | }, [dispatch]);
55 |
56 | // Use the WebSocketContext.Provider to pass down the WebSocket instance
57 | return (
58 |
59 | {children}
60 |
61 | );
62 | };
63 |
64 | // Custom hook to use the WebSocket context
65 | export const useWebSocket = () => {
66 | return useContext(WebSocketContext);
67 | };
68 |
--------------------------------------------------------------------------------
/client/src/theme.js:
--------------------------------------------------------------------------------
1 | // color design tokens export
2 | export const darkTheme = {
3 | grey: {
4 | 0: '#ffffff',
5 | 10: '#f6f6f6',
6 | 50: '#f0f0f0',
7 | 100: '#e0e0e0',
8 | 200: '#c2c2c2',
9 | 300: '#a3a3a3',
10 | 400: '#858585',
11 | 500: '#666666',
12 | 600: '#525252',
13 | 700: '#3d3d3d',
14 | 800: '#292929',
15 | 900: '#141414',
16 | 1000: '#000000',
17 | },
18 | primary: {
19 | // Indigo color palette for primary theme
20 | 100: '#d7dbdd',
21 | 200: '#afb8bb',
22 | 300: '#889498',
23 | 400: '#607176',
24 | 500: '#384d54',
25 | 600: '#2d3e43',
26 | 700: '#222e32',
27 | 800: '#161f22',
28 | 900: '#0b0f11',
29 | },
30 | secondary: {
31 | // Orange color palette for secondary theme
32 | 100: '#ffe5cc',
33 | 200: '#ffca99',
34 | 300: '#ffb066',
35 | 400: '#ff9533',
36 | 500: '#ff7b00',
37 | 600: '#cc6200',
38 | 700: '#994a00',
39 | 800: '#663100',
40 | 900: '#331900',
41 | },
42 | }
43 |
44 | /**
45 | * Reverses the color palette from dark to light theme or from light to dark theme
46 | * This function takes in a dark theme color palette and reverses the colors
47 | * to create a light theme. It iterates through each color category
48 | * and reverses the order of the color values.
49 | * @param {Object} darkTheme - The dark theme color palette object.
50 | * @returns {Object} - The reversed color palette object for light theme.
51 | */
52 | function reverseTheme(darkTheme) {
53 | const reversedTokens = {}
54 | Object.entries(darkTheme).forEach(([key, val]) => {
55 | const keys = Object.keys(val)
56 | const values = Object.values(val)
57 | const length = keys.length
58 | const reversedObj = {}
59 | for (let i = 0; i < length; i++) {
60 | reversedObj[keys[i]] = values[length - i - 1]
61 | }
62 | reversedTokens[key] = reversedObj
63 | })
64 | return reversedTokens
65 | }
66 | export const lightTheme = reverseTheme(darkTheme)
67 |
68 | // mui/icon material theme settings
69 | /**
70 | * Generates the MUI/Material-UI theme settings based on the mode.
71 | * This function creates the theme settings for the application based on whether the mode
72 | * is 'dark' or 'light'. It configures the palette, including primary, secondary, neutral colors,
73 | * and background settings, and sets the typography options.
74 | * @param {string} mode - The current theme mode, either 'dark' or 'light'.
75 | * @returns {Object} - The theme settings object for MUI/Material-UI.
76 | */
77 | export const themeSettings = (mode) => {
78 | return {
79 | palette: {
80 | mode: mode,
81 | ...(mode === 'dark'
82 | ? {
83 | // palette values for dark mode
84 | primary: {
85 | ...darkTheme.primary,
86 | main: darkTheme.primary[400],
87 | light: darkTheme.primary[400],
88 | },
89 | secondary: {
90 | ...darkTheme.secondary,
91 | main: darkTheme.secondary[300],
92 | },
93 | neutral: {
94 | ...darkTheme.grey,
95 | main: darkTheme.grey[500],
96 | },
97 | background: {
98 | default: darkTheme.primary[600],
99 | alt: darkTheme.primary[500],
100 | },
101 | }
102 | : {
103 | // palette values for light mode
104 | primary: {
105 | ...lightTheme.primary,
106 | main: darkTheme.grey[50],
107 | light: darkTheme.grey[100],
108 | },
109 | secondary: {
110 | ...lightTheme.secondary,
111 | main: darkTheme.secondary[600],
112 | light: darkTheme.secondary[700],
113 | },
114 | neutral: {
115 | ...lightTheme.grey,
116 | main: darkTheme.grey[500],
117 | },
118 | background: {
119 | default: darkTheme.grey[0],
120 | alt: darkTheme.grey[50],
121 | },
122 | }),
123 | },
124 | typography: {
125 | fontFamily: ['Inter', 'sans-serif'].join(','),
126 | fontSize: 14,
127 | h1: {
128 | fontFamily: ['Inter', 'sans-serif'].join(','),
129 | fontSize: 40,
130 | },
131 | h2: {
132 | fontFamily: ['Inter', 'sans-serif'].join(','),
133 | fontSize: 32,
134 | },
135 | h3: {
136 | fontFamily: ['Inter', 'sans-serif'].join(','),
137 | fontSize: 24,
138 | },
139 | h4: {
140 | fontFamily: ['Inter', 'sans-serif'].join(','),
141 | fontSize: 20,
142 | },
143 | h5: {
144 | fontFamily: ['Inter', 'sans-serif'].join(','),
145 | fontSize: 16,
146 | },
147 | h6: {
148 | fontFamily: ['Inter', 'sans-serif'].join(','),
149 | fontSize: 14,
150 | },
151 | },
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/client/src/webService/connectWebSocketToLineChart.js:
--------------------------------------------------------------------------------
1 | // connet web socket to the Line chart
2 | export function connectWebSocketToLineChart(userId, accountName, region, clusterName, serviceName, metricNames, onMessage, onError, onClose) {
3 | // Construct the WebSocket URL based on the provided parameters
4 | const url = `ws://localhost:3000/getMetricData?userId=${userId}&accountName=${accountName}®ion=${region}&clusterName=${clusterName}&serviceName=${serviceName}&metricName=${metricNames.join(',')}`;
5 | const ws = new WebSocket(url);
6 | // Event listener for when the WebSocket connection is opened
7 | ws.onopen = () => {
8 | console.log('WebSocket connection opened');
9 | };
10 |
11 | // Event listener for receiving messages from the WebSocket
12 | ws.onmessage = (event) => {
13 | const data = JSON.parse(event.data);
14 | if (data.error) {
15 | onError(new Error(data.error));
16 | } else {
17 | onMessage(data);
18 | }
19 | };
20 | // Event listener for WebSocket errors
21 | ws.onerror = (error) => {
22 | console.error('WebSocket error:', error);
23 | if (onError) onError(error);
24 | };
25 | // Event listener for when the WebSocket connection is closed
26 | ws.onclose = (event) => {
27 | console.log('WebSocket connection closed:', event);
28 | if (onClose) onClose(event);
29 | };
30 |
31 | return ws;
32 | }
33 |
--------------------------------------------------------------------------------
/client/src/webService/connectWebSocketToNotifications.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react';
2 | import { useSelector, useDispatch } from 'react-redux';
3 | import { setReceivedNotifications } from '../redux/notificationSlice';
4 |
5 | /**
6 | * Custom hook for managing WebSocket connections for metrics and notifications.
7 | * Connects to WebSocket endpoints to receive real-time metric and notification data.
8 | */
9 | const useWebSocketNotifications = () => {
10 | const dispatch = useDispatch();
11 |
12 | const { userId, accountName, region, clusterName, serviceName } = useSelector((state) => ({
13 | userId: state.user.userId,
14 | accountName: state.user.accountName,
15 | region: state.user.region,
16 | clusterName: state.user.clusterName,
17 | serviceName: state.user.serviceName
18 | }));
19 |
20 | // Reference to hold WebSocket connections
21 | const metricSocketsRef = useRef([]);
22 | // Reference to hold WebSocket connection for notifications
23 | const notificationSocketRef = useRef(null);
24 |
25 | useEffect(() => {
26 | if (metricSocketsRef.current.length === 0 && !notificationSocketRef.current) {
27 | // URLs for WebSocket connections to receive metric data
28 | const metricURLs = [
29 | `ws://localhost:3000/getMetricData?userId=${userId}&accountName=${accountName}®ion=${region}&clusterName=${clusterName}&serviceName=${serviceName}&metricName=CPUUtilization`,
30 | `ws://localhost:3000/getMetricData?userId=${userId}&accountName=${accountName}®ion=${region}&clusterName=${clusterName}&serviceName=${serviceName}&metricName=MemoryUtilization`,
31 | `ws://localhost:3000/getMetricData?userId=${userId}&accountName=${accountName}®ion=${region}&clusterName=${clusterName}&serviceName=${serviceName}&metricName=NetworkTxBytes`,
32 | `ws://localhost:3000/getMetricData?userId=${userId}&accountName=${accountName}®ion=${region}&clusterName=${clusterName}&serviceName=${serviceName}&metricName=NetworkRxBytes`
33 | ];
34 |
35 | // URL for WebSocket connection to receive notifications
36 | const notificationURL = `ws://localhost:3000/checkNotifications?userId=${userId}`;
37 |
38 | /**
39 | * Function to create a WebSocket connection.
40 | * @param {string} url - The WebSocket URL to connect to.
41 | * @param {boolean} [isNotification=false] - Flag to indicate if the WebSocket is for notifications.
42 | * @returns {WebSocket} - The WebSocket connection.
43 | */
44 | function createWebSocket(url, isNotification = false) {
45 | const ws = new WebSocket(url);
46 |
47 | ws.onopen = () => {
48 | console.log(`WebSocket connection established: ${url}`);
49 | };
50 |
51 | ws.onmessage = (event) => {
52 | try {
53 | const data = JSON.parse(event.data);
54 | if (isNotification) {
55 | console.log('Received notification data:', data);
56 | if (Array.isArray(data)) {
57 | console.log('New notifications received:', data);
58 | dispatch(setReceivedNotifications(data.map(notification => ({
59 | ...notification,
60 | isRead: notification.isRead ?? false,
61 | }))));
62 | } else if (data.message === 'No new notifications') {
63 | console.log('No new notifications');
64 | dispatch(setReceivedNotifications([]));
65 | } else {
66 | console.error('Invalid notification data received:', data);
67 | }
68 | } else {
69 | console.log('Received metric data:', data);
70 | }
71 | } catch (error) {
72 | console.error('Failed to parse data:', event.data, error);
73 | }
74 | };
75 |
76 | ws.onerror = (error) => {
77 | console.error(`WebSocket error (${url}):`, error);
78 | };
79 |
80 | ws.onclose = () => {
81 | console.log(`WebSocket connection closed: ${url}`);
82 | // Reconnect the WebSocket after a delay
83 | setTimeout(() => {
84 | if (isNotification) {
85 | notificationSocketRef.current = createWebSocket(url, true);
86 | } else {
87 | metricSocketsRef.current = metricSocketsRef.current.map((ws) => {
88 | if (ws.url === url) {
89 | return createWebSocket(url);
90 | }
91 | return ws;
92 | });
93 | }
94 | }, 5000); // reconnect after 5 seconds
95 | };
96 |
97 | return ws;
98 | }
99 |
100 | // Create and store WebSocket connections for metrics
101 | metricSocketsRef.current = metricURLs.map((url) => createWebSocket(url));
102 | // Create and store WebSocket connection for notifications
103 | notificationSocketRef.current = createWebSocket(notificationURL, true);
104 | }
105 |
106 | // Cleanup function to close WebSocket connections when the component unmounts
107 | return () => {
108 | metricSocketsRef.current.forEach((ws) => ws.close());
109 | if (notificationSocketRef.current) {
110 | notificationSocketRef.current.close();
111 | }
112 | };
113 | }, [dispatch]);
114 |
115 | return {
116 | metricSockets: metricSocketsRef.current,
117 | notificationSocket: notificationSocketRef.current,
118 | };
119 | };
120 |
121 | export default useWebSocketNotifications;
122 |
--------------------------------------------------------------------------------
/client/src/webService/connectWebSocketToPieChart.js:
--------------------------------------------------------------------------------
1 | // connect web socket to Pie Chart
2 | export function connectWebSocketToPieChart(userId, accountName, region, clusterName, serviceName, onMessage, onError, onClose) {
3 | // Create a new WebSocket connection with the specified parameters
4 | const ws = new WebSocket(`ws://localhost:3000/getMetricData?userId=${userId}&accountName=${accountName}®ion=${region}&clusterName=${clusterName}&serviceName=${serviceName}&metricName=totalTasks`);
5 | // Event listener for when the WebSocket connection is opened
6 | ws.onopen = () => {
7 | console.log('WebSocket connection opened');
8 | };
9 |
10 | // Event listener for receiving messages from the WebSocket
11 | ws.onmessage = (event) => {
12 | const data = JSON.parse(event.data);
13 | // Check if the received data contains an error
14 | if (data.error) {
15 | onError(new Error(data.error));
16 | } else {
17 | onMessage(data);
18 | }
19 | };
20 |
21 | // Event listener for WebSocket errors
22 | ws.onerror = (error) => {
23 | console.error('WebSocket error:', error);
24 | if (onError) onError(error);
25 | };
26 |
27 | // Event listener for when the WebSocket connection is closed
28 | ws.onclose = (event) => {
29 | console.log('WebSocket connection closed:', event);
30 | if (onClose) onClose(event);
31 | };
32 |
33 | return ws;
34 | }
35 |
36 |
37 |
--------------------------------------------------------------------------------
/client/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const HtmlWebPackPlugin = require('html-webpack-plugin');
3 |
4 | module.exports = {
5 | entry: './src/index.js',
6 | mode: 'development',
7 | output: {
8 | filename: 'bundle.js',
9 | path: path.resolve(__dirname, 'dist'),
10 | publicPath: '/',
11 | },
12 | module: {
13 | rules: [
14 | {
15 | test: /\.jsx?$/,
16 | exclude: /node_modules/,
17 | use: {
18 | loader: 'babel-loader',
19 | options: {
20 | presets: ['@babel/preset-env', '@babel/preset-react'],
21 | },
22 | },
23 | },
24 | {
25 | test: /\.s?css$/,
26 | use: ['style-loader', 'css-loader', 'sass-loader'],
27 | },
28 | {
29 | test: /\.(png|jpg|jpeg|gif)$/i,
30 | type: 'asset/resource',
31 | },
32 | ],
33 | },
34 | resolve: {
35 | extensions: ['.js', '.jsx'],
36 | alias: {
37 | '@emotion/react': path.resolve(__dirname, 'node_modules/@emotion/react'),
38 | }
39 | },
40 | plugins: [
41 | new HtmlWebPackPlugin({
42 | template: './public/index.html',
43 | }),
44 | ],
45 | devServer: {
46 | historyApiFallback: true,
47 | hot: true,
48 | port: 8080,
49 | proxy: [
50 | {
51 | context: ['/api'],
52 | target: 'http://localhost:3000',
53 | secure: false,
54 | changeOrigin: true,
55 | },
56 | ],
57 | },
58 | devtool: 'eval-source-map',
59 | };
60 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.8'
2 |
3 | services:
4 | app:
5 | build: .
6 | container_name: decluster-container
7 | volumes:
8 | - .:/app
9 | - node_modules:/app/node_modules
10 | - client_node_modules:/app/client/node_modules
11 | ports:
12 | - "8080:8080"
13 | - "3000:3000"
14 | env_file:
15 | - .env
16 | environment:
17 | - REDIS_URL=redis://redis:6379
18 | depends_on:
19 | - redis
20 | command: npm run start
21 |
22 | redis:
23 | image: "redis:alpine"
24 | container_name: my_redis_container
25 | ports:
26 | - "6379:6379"
27 |
28 | volumes:
29 | node_modules:
30 | client_node_modules:
31 |
--------------------------------------------------------------------------------
/jest.backend.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | testEnvironment: 'node',
3 | testMatch: ['/server/tests/**/*.test.js'],
4 | };
5 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | testEnvironment: 'jest-environment-jsdom',
3 | clearMocks: true,
4 | };
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "declustor",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "npm run test:backend && npm run test:frontend",
8 | "test:backend": "jest --config=jest.backend.config.js",
9 | "test:frontend": "cd client && npx jest",
10 | "server": "nodemon server/server.js",
11 | "client": "cd client && npm run dev",
12 | "start": "npm-run-all --parallel server client"
13 | },
14 | "keywords": [],
15 | "author": "",
16 | "license": "ISC",
17 | "dependencies": {
18 | "@aws-sdk/client-cloudwatch": "^3.609.0",
19 | "@aws-sdk/client-ecs": "^3.609.0",
20 | "@aws-sdk/client-organizations": "^3.614.0",
21 | "@aws-sdk/credential-provider-env": "^3.609.0",
22 | "@babel/runtime": "^7.15.4",
23 | "@reduxjs/toolkit": "^2.2.6",
24 | "aws-sdk": "^2.1654.0",
25 | "cookie-parser": "^1.4.6",
26 | "cookie-session": "^2.1.0",
27 | "cors": "^2.8.5",
28 | "crypto": "^1.0.1",
29 | "dotenv": "^16.4.5",
30 | "express": "^4.19.2",
31 | "express-session": "^1.18.0",
32 | "google-auth-library": "^9.11.0",
33 | "nodemailer": "^6.9.14",
34 | "nodemon": "^3.1.4",
35 | "passport": "^0.5.3",
36 | "passport-github2": "^0.1.12",
37 | "passport-google-oauth20": "^2.0.0",
38 | "path": "^0.12.7",
39 | "redis": "^4.6.15",
40 | "semver": "^7.3.4",
41 | "util": "^0.12.5",
42 | "ws": "^8.18.0"
43 | },
44 | "devDependencies": {
45 | "@babel/core": "^7.24.9",
46 | "@babel/preset-env": "^7.24.8",
47 | "@babel/preset-react": "^7.24.7",
48 | "@eslint/js": "^9.7.0",
49 | "babel-eslint": "^10.1.0",
50 | "eslint": "^9.7.0",
51 | "eslint-plugin-react": "^7.35.0",
52 | "globals": "^15.8.0",
53 | "jest": "^29.7.0",
54 | "jest-environment-jsdom": "^29.7.0",
55 | "nodemailer-mock": "^2.0.6",
56 | "npm-run-all": "^4.1.5",
57 | "sqlite3": "^5.1.7",
58 | "supertest": "^7.0.0"
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/server/controllers/credentialsController.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const sqlite3 = require('sqlite3');
3 |
4 | // resolve the path and connect to sqlite3 database
5 | const dbPath = path.resolve(__dirname, '../database/Accounts.db');
6 | const db = new sqlite3.Database(dbPath);
7 |
8 | // Create a credentials controller object
9 | const credentialsController = {};
10 |
11 | /**
12 | * Saves credentials to the database
13 | * Checks for required fields and unique constraints on access keys and account names
14 | * @param {object} req - The request object containing user and account data
15 | * @param {object} res - The response object
16 | * @param {function} next - The next middleware function in the stack.
17 | * @returns {void} - Does not return a value; instead, it handles the HTTP response directly or passes control to the next middleware.
18 | */
19 | credentialsController.saveCredentials = (req, res, next) => {
20 | const { userId, accessKey, secretKey, accountName, accountType } = req.body;
21 | // Validate required fields
22 | if (!accessKey || !secretKey || !accountName || !accountType) {
23 | return res.status(400).send('missing information');
24 | }
25 |
26 | // Check if the access key or account name already exists in the database
27 | db.get(
28 | 'SELECT access_key, account_name FROM Accounts WHERE access_key = ? OR (user_id = ? AND account_name = ?)',
29 | [accessKey, userId, accountName],
30 | (err, row) => {
31 | if (err) {
32 | return res.status(500).json({ message: '1.Internal server error' });
33 | }
34 |
35 | // Check for existing access key or account name
36 | if (row) {
37 | if (row.access_key == accessKey) {
38 | return res.status(400).json({ message: 'AccessKey already exists', alreadyExist: true });
39 | } else if (row.account_name === accountName) {
40 | return res.status(400).json({ message: 'Account name already exists for this user', alreadyExist: true });
41 | }
42 |
43 | }
44 |
45 | // Insert the new account data into the Accounts table
46 | db.run(
47 | 'INSERT INTO Accounts (user_id, access_key, secret_key, account_name, account_type) VALUES (?, ?, ?, ?, ?)',
48 | [userId, accessKey, secretKey, accountName, accountType],
49 | (err) => {
50 | if (err) {
51 | return res.status(500).json({ message: '2.Internal server error' });
52 | }
53 | next();
54 | }
55 | );
56 | }
57 | );
58 | };
59 |
60 | // Export the credentials controller
61 | module.exports = credentialsController;
62 |
--------------------------------------------------------------------------------
/server/controllers/notificationController.js:
--------------------------------------------------------------------------------
1 | const sqlite3 = require('sqlite3');
2 | const path = require('path');
3 | const dbPath = path.resolve(__dirname, '../database/Notifications.db');
4 | const db = new sqlite3.Database(dbPath);
5 |
6 | const client = require('./redisClient');
7 |
8 | const notificationController = {};
9 | // test http://localhost:3000/setNotification?userId=${userId}&accountName=${accountName}&clusterName=${clusterName}®ion=${region}
10 | /**
11 | * Sets or updates notification settings in the database for a user's account, cluster, and region.
12 | * It first clears any existing notifications for the specified parameters before setting new ones.
13 | * @param {object} req - The HTTP request object, containing the user's and notification settings in the query and body.
14 | * @param {object} res - The HTTP response object.
15 | * @param {function} next - The next middleware function in the Express stack.
16 | * @returns {void} - This function does not return a value; it either calls next() with an error.
17 | */
18 | notificationController.setNotification = (req, res, next) => {
19 | // Extract query parameters from the request
20 | const {userId, accountName, clusterName, region } = req.query;
21 | // Check if all required query parameters are provided
22 | if (!userId || !accountName || !clusterName || !region) {
23 | return next({
24 | log: 'Missing required parameters',
25 | status: 400,
26 | message: { err: 'Missing required parameters' },
27 | });
28 | }
29 |
30 | // Extract notification settings from the request body
31 | const notifications = req.body.notifications;
32 | // Attempt to delete existing notifications to prevent duplicates
33 | try {
34 | const deletequery = `DELETE FROM Notifications WHERE user_id = ? AND account_name = ? AND cluster_name = ? AND region = ?`;
35 | db.run(deletequery, [userId, accountName, clusterName, region], (err) => {
36 | if (err) {
37 | return next({
38 | log: 'Error occurred during clearing notification database',
39 | status: 400,
40 | message: { err: 'Error occurred during clearing notification database' },
41 | });
42 | }
43 | // Prepare to insert new notification settings
44 | const insertQueries = [];
45 | for (const setting of notifications) {
46 | const { metric } = setting;
47 | // Processing settings based on the type of metric
48 | if (metric == 'NetworkRxBytes' || metric == 'NetworkTxBytes') {
49 | // Handling general metrics
50 | if (setting.hasOwnProperty('threshold') && setting.hasOwnProperty('operator') ) {
51 | const { threshold, operator } = setting;
52 | insertQueries.push({
53 | query: `INSERT INTO Notifications (user_id, account_name, cluster_name, region, metric_name, threshold, operator) VALUES (?, ?, ?, ?, ?, ?, ?)`,
54 | params: [userId, accountName, clusterName, region, metric, threshold, operator]
55 | })
56 | }
57 | } else if (setting.hasOwnProperty('applyToAllServices')) {
58 | // handling apply to all services threshold
59 | const { applyToAllServices: { threshold, operator } } = setting;
60 | if (threshold == undefined || !operator) {
61 | return next({
62 | log: 'Missing required information for applyToAllServices metric',
63 | status: 400,
64 | message: {
65 | err: 'Missing required information for applyToAllServices metric',
66 | },
67 | });
68 | }
69 | insertQueries.push({
70 | query: `INSERT INTO Notifications (user_id, account_name, cluster_name, region, service_name, metric_name, threshold, operator) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
71 | params: [userId, accountName, clusterName, region, 'all', metric, threshold, operator]
72 | });
73 | } else if (setting.hasOwnProperty('services')) {
74 | // handling one specific service threshold
75 | for (const [service_name, service] of Object.entries(setting.services)) {
76 | const { threshold, operator } = service;
77 | if (!metric || threshold === undefined || !operator || !service_name) {
78 | return next({
79 | log: 'Missing required information for applyToAllServices metric',
80 | status: 400,
81 | message: {
82 | err: 'Missing required information for applyToAllServices metric',
83 | },
84 | });
85 | }
86 | insertQueries.push({
87 | query: `INSERT INTO Notifications (user_id, account_name, cluster_name, region, service_name, metric_name, threshold, operator) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
88 | params: [userId, accountName, clusterName, region, service_name, metric, threshold, operator]
89 | });
90 | }
91 | }
92 | }
93 | // Execute all insert queries
94 | insertQueries.forEach(({ query, params }) => {
95 | db.run(query, params, (err) => {
96 | if (err) {
97 | return next({
98 | log: 'Error occurred during insert notification database',
99 | status: 400,
100 | message: {
101 | err: 'Error occurred during insert notification database',
102 | },
103 | });
104 | }
105 | })
106 | })
107 | return next();
108 | });
109 | } catch (err) {
110 | return next({
111 | log: 'Error saving notification settings',
112 | status: 400,
113 | message: { err: `Error saving notification settings, ${err}` },
114 | });
115 | }
116 | };
117 |
118 | /**
119 | * Retrieves notifications from Redis for a specific user and sends them via WebSocket.
120 | * Once sent, the notifications are deleted from Redis. If no notifications are found, a message is sent indicating this.
121 | * @param {WebSocket} ws - The WebSocket connection to the client.
122 | * @param {number} userId - The ID of the user to check notifications for.
123 | * @returns {void} - This function does not return a value; it handles communication directly through WebSocket and manages its state.
124 | */
125 | notificationController.handleNotificationCheck = async(ws, userId) => {
126 | try {
127 | // Retrieve notification keys from Redis
128 | const keys = await client.keys(`notification:${userId}:*`);;
129 | if (keys.length === 0) {
130 | ws.send(JSON.stringify({ message: 'No new notifications' }));
131 | return;
132 | }
133 |
134 | // Retrieve each notification and prepare to send
135 | const notifications = [];
136 | for (const key of keys) {
137 | const data = await client.get(key);;
138 | notifications.push(JSON.parse(data));
139 | // Delete the notification after retrieval
140 | await client.del(key);
141 | }
142 | // Send all notifications via WebSocket
143 | ws.send(JSON.stringify(notifications));
144 | } catch(err) {
145 | ws.send(JSON.stringify({error : `Error checking notifications, ${err}`}));
146 | ws.close();
147 | }
148 | }
149 |
150 | module.exports = notificationController;
151 |
--------------------------------------------------------------------------------
/server/controllers/redisClient.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | require('dotenv').config({ path: path.resolve(__dirname, '../../.env') });
3 | const redis = require('redis');
4 |
5 | const client = redis.createClient({
6 | url: process.env.REDIS_URL
7 | });
8 |
9 | client.on('error', (err) => {
10 | console.error('Redis error:', err);
11 | });
12 |
13 | client.on('connect', async () => {
14 | console.log('Connected to Redis');
15 | try {
16 | await client.flushAll();
17 | console.log('Redis database flushed');
18 | } catch (err) {
19 | console.error('Error flushing Redis database:', err);
20 | }
21 | });
22 |
23 | client.connect().catch(console.error);
24 |
25 | module.exports = client;
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/server/createDatabase.js:
--------------------------------------------------------------------------------
1 | const sqlite3 = require('sqlite3');
2 |
3 | // Dictionary of database paths for easier management and initialization
4 | const databases = {
5 | Accounts: './database/Accounts.db',
6 | Notifications: './database/Notifications.db',
7 | Users: './database/Users.db',
8 | };
9 |
10 | /**
11 | * Connects to a SQLite database provided by the path and logs the status.
12 | * @param {string} dbPath Path to the SQLite database file.
13 | * @returns {object} The SQLite database connection object.
14 | */
15 | function connectToDatabase(dbPath) {
16 | return new sqlite3.Database(dbPath, (err) => {
17 | if (err) {
18 | console.error(`Failed to connect to database at ${dbPath}:`, err.message);
19 | } else {
20 | console.log(`Connected to database at ${dbPath}`);
21 | }
22 | });
23 | }
24 | // Create a dictionary to hold the database connection objects
25 | const dbConnections = {};
26 | for (const [name, path] of Object.entries(databases)) {
27 | dbConnections[name] = connectToDatabase(path);
28 | }
29 |
30 | /**
31 | * Asynchronously creates necessary tables in each database if they do not exist.
32 | */
33 | async function createTables() {
34 | await dbConnections.Users.serialize(() => {
35 | dbConnections.Users.run(`CREATE TABLE IF NOT EXISTS Users (
36 | id INTEGER PRIMARY KEY AUTOINCREMENT,
37 | google_id TEXT,
38 | first_name TEXT,
39 | last_name TEXT,
40 | user_name TEXT,
41 | password TEXT,
42 | email TEXT NOT NULL UNIQUE,
43 | verification_code TEXT,
44 | verified INTEGER DEFAULT 0,
45 | reset_token TEXT,
46 | reset_token_expiry INTEGER
47 | )`);
48 | });
49 |
50 | await dbConnections.Accounts.serialize(() => {
51 | dbConnections.Accounts.run(`PRAGMA foreign_keys = ON;`);
52 | dbConnections.Accounts.run(`CREATE TABLE IF NOT EXISTS Accounts (
53 | id INTEGER PRIMARY KEY AUTOINCREMENT,
54 | user_id INTEGER,
55 | account_name TEXT,
56 | access_key TEXT,
57 | secret_key TEXT,
58 | account_type TEXT,
59 | FOREIGN KEY (user_id) REFERENCES Users(id)
60 | )`);
61 | });
62 |
63 | await dbConnections.Notifications.serialize(() => {
64 | dbConnections.Notifications.run(`PRAGMA foreign_keys = ON;`);
65 | dbConnections.Notifications.run(`CREATE TABLE IF NOT EXISTS Notifications (
66 | id INTEGER PRIMARY KEY AUTOINCREMENT,
67 | user_id INTEGER,
68 | account_name TEXT,
69 | region TEXT,
70 | cluster_name TEXT,
71 | service_name TEXT,
72 | metric_name TEXT,
73 | threshold REAL,
74 | operator TEXT,
75 | FOREIGN KEY (user_id) REFERENCES Users(id)
76 | )`);
77 | });
78 | }
79 |
80 | // Call the function to create tables
81 | createTables();
82 |
83 | // Close all database connections
84 | setTimeout(() => {
85 | for (const connection of Object.values(dbConnections)) {
86 | connection.close();
87 | }
88 | }, 1000);
89 |
--------------------------------------------------------------------------------
/server/database/Accounts.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DeClustor/2ed5f0f10f89d1b40c42569bf590be9dabd264aa/server/database/Accounts.db
--------------------------------------------------------------------------------
/server/database/Notifications.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DeClustor/2ed5f0f10f89d1b40c42569bf590be9dabd264aa/server/database/Notifications.db
--------------------------------------------------------------------------------
/server/database/Users.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oslabs-beta/DeClustor/2ed5f0f10f89d1b40c42569bf590be9dabd264aa/server/database/Users.db
--------------------------------------------------------------------------------
/server/router/listRouter.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const router = express.Router();
3 | const listController = require('../controllers/listController');
4 |
5 | // GET route to list all accounts saving in credentials database
6 | router.get('/AllAccounts', listController.Accounts);
7 | // GET route to list all subaccounts under root credentials
8 | router.get('/AllSubAccounts', listController.SubAccounts);
9 | // GET route to list all ECS clusters under one root account or subaccount
10 | router.get('/AllClusters', listController.Clusters);
11 | // GET route to list all services within a specified cluster.
12 | router.get('/AllServices', listController.Services);
13 |
14 | module.exports = router;
15 |
--------------------------------------------------------------------------------
/server/tests/credentialsController.test.js:
--------------------------------------------------------------------------------
1 | const request = require('supertest');
2 | const express = require('express');
3 | const bodyParser = require('body-parser');
4 | const credentialsController = require('../controllers/credentialsController');
5 |
6 | jest.mock('sqlite3', () => {
7 | const mockDbQuery = jest.fn((sql, params, callback) => {
8 | if (sql.includes('SELECT access_key, account_name FROM Accounts WHERE access_key = ? OR (user_id = ? AND account_name = ?)')) {
9 | if (params[0] === 'existingAccessKey') {
10 | callback(null, { access_key: 'existingAccessKey', account_name: 'otherAccountName' });
11 | } else if (params[2] === 'existingAccountName') {
12 | callback(null, { access_key: 'otherAccessKey', account_name: 'existingAccountName' });
13 | } else {
14 | callback(null, null);
15 | }
16 | } else {
17 | callback(null, []);
18 | }
19 | });
20 |
21 | const mockDbRun = jest.fn((sql, params, callback) => {
22 | callback(null);
23 | });
24 |
25 | return {
26 | Database: jest.fn(() => ({
27 | get: mockDbQuery,
28 | run: mockDbRun
29 | }))
30 | };
31 | });
32 |
33 | const app = express();
34 | app.use(bodyParser.json());
35 |
36 | app.post('/credentials', credentialsController.saveCredentials, (req, res) => {
37 | res.status(200).send('Credentials saved');
38 | });
39 |
40 | describe('credentialsController tests', () => {
41 | beforeEach(() => {
42 | jest.clearAllMocks();
43 | });
44 |
45 | test('should return 400 if required fields are missing', async () => {
46 | const response = await request(app)
47 | .post('/credentials')
48 | .send({ userId: 'testUser', accessKey: 'testAccessKey', accountType: 'root' });
49 |
50 | expect(response.status).toBe(400);
51 | expect(response.text).toBe('missing information');
52 | });
53 |
54 | test('should return 400 if accessKey already exists', async () => {
55 | const response = await request(app)
56 | .post('/credentials')
57 | .send({ userId: 'testUser', accessKey: 'existingAccessKey', secretKey: 'testSecretKey', accountName: 'newAccountName', accountType: 'root' });
58 |
59 | expect(response.status).toBe(400);
60 | expect(response.body.message).toBe('AccessKey already exists');
61 | expect(response.body.alreadyExist).toBe(true);
62 | });
63 |
64 | test('should return 400 if accountName already exists for the user', async () => {
65 | const response = await request(app)
66 | .post('/credentials')
67 | .send({ userId: 'testUser', accessKey: 'newAccessKey', secretKey: 'testSecretKey', accountName: 'existingAccountName', accountType: 'root' });
68 |
69 | expect(response.status).toBe(400);
70 | expect(response.body.message).toBe('Account name already exists for this user');
71 | expect(response.body.alreadyExist).toBe(true);
72 | });
73 |
74 | test('should save credentials if all fields are valid', async () => {
75 | const response = await request(app)
76 | .post('/credentials')
77 | .send({ userId: 'testUser', accessKey: 'newAccessKey', secretKey: 'newSecretKey', accountName: 'newAccountName', accountType: 'root' });
78 |
79 | expect(response.status).toBe(200);
80 | expect(response.text).toBe('Credentials saved');
81 | });
82 | });
83 |
--------------------------------------------------------------------------------
/server/tests/listController.test.js:
--------------------------------------------------------------------------------
1 | const request = require('supertest');
2 | const express = require('express');
3 |
4 | jest.mock('sqlite3', () => {
5 | const mockDbQuery = jest.fn((sql, params, callback) => {
6 | if (sql.includes('Accounts WHERE user_id = ? AND account_type = "root"')) {
7 | callback(null, [{ account_name: 'rootAccount' }]);
8 | } else if (sql.includes('Accounts WHERE user_id = ? AND account_type = "subaccount"')) {
9 | callback(null, [{ account_name: 'subAccount' }]);
10 | } else if (sql.includes('Accounts WHERE account_name = ? AND user_id = ?')) {
11 | callback(null, { access_key: 'testAccessKey', secret_key: 'testSecretKey' });
12 | } else {
13 | callback(null, []);
14 | }
15 | });
16 | return {
17 | Database: jest.fn(() => ({
18 | all: mockDbQuery,
19 | get: mockDbQuery
20 | }))
21 | };
22 | });
23 |
24 | const mockSend = jest.fn();
25 | jest.mock('@aws-sdk/client-organizations', () => {
26 | const actualModule = jest.requireActual('@aws-sdk/client-organizations');
27 | return {
28 | ...actualModule,
29 | OrganizationsClient: jest.fn(() => ({
30 | send: mockSend
31 | })),
32 | };
33 | });
34 |
35 | jest.mock('@aws-sdk/client-ecs', () => {
36 | const actualModule = jest.requireActual('@aws-sdk/client-ecs');
37 | return {
38 | ...actualModule,
39 | ECSClient: jest.fn(() => ({
40 | send: jest.fn((command) => {
41 | if (command.constructor.name === 'ListClustersCommand') {
42 | return Promise.resolve({
43 | clusterArns: ['clusterArn']
44 | });
45 | } else if (command.constructor.name === 'DescribeClustersCommand') {
46 | return Promise.resolve({
47 | clusters: [{ clusterName: 'testCluster' }]
48 | });
49 | } else if (command.constructor.name === 'ListServicesCommand') {
50 | return Promise.resolve({
51 | serviceArns: ['serviceArn']
52 | });
53 | } else if (command.constructor.name === 'DescribeServicesCommand') {
54 | return Promise.resolve({
55 | services: [{ serviceName: 'testService' }]
56 | });
57 | }
58 | })
59 | })),
60 | };
61 | });
62 |
63 | const app = express();
64 | app.use(express.json());
65 | app.use('/list', require('../router/listRouter'));
66 |
67 | describe('listController tests', () => {
68 | beforeEach(() => {
69 | jest.clearAllMocks();
70 | mockSend.mockImplementation((command) => {
71 | if (command.constructor.name === 'ListAccountsCommand') {
72 | return Promise.resolve({
73 | Accounts: [{ Id: '1', Name: 'TestAccount' }, { Id: '2', Name: 'TestAccount2' }],
74 | NextToken: null
75 | });
76 | }
77 | });
78 | });
79 |
80 | test('should list all root and sub accounts', async () => {
81 | const response = await request(app).get('/list/AllAccounts?userId=testUser');
82 | expect(response.status).toBe(200);
83 | expect(response.body.root).toEqual([{ account_name: 'rootAccount' }]);
84 | expect(response.body.subaccount).toEqual([{ account_name: 'subAccount' }]);
85 | });
86 |
87 | test('should list all sub accounts from AWS', async () => {
88 | const response = await request(app).get('/list/AllSubAccounts?userId=testUser&accountName=testAccount');
89 | expect(response.status).toBe(200);
90 | expect(response.body).toEqual([
91 | { Id: '1', Name: 'TestAccount' },
92 | { Id: '2', Name: 'TestAccount2' }
93 | ]);
94 | });
95 |
96 | test('should list all clusters in all regions', async () => {
97 | const response = await request(app).get('/list/AllClusters?userId=1&accountName=testAccount');
98 | expect(response.status).toBe(200);
99 | expect(response.body).toEqual([
100 | { region: 'us-east-1', clusters: [{ clusterName: 'testCluster' }] },
101 | { region: 'us-east-2', clusters: [{ clusterName: 'testCluster' }] },
102 | { region: 'us-west-1', clusters: [{ clusterName: 'testCluster' }] },
103 | { region: 'us-west-2', clusters: [{ clusterName: 'testCluster' }] },
104 | { region: 'af-south-1', clusters: [{ clusterName: 'testCluster' }] },
105 | { region: 'ap-east-1', clusters: [{ clusterName: 'testCluster' }] },
106 | { region: 'ap-south-1', clusters: [{ clusterName: 'testCluster' }] },
107 | { region: 'ap-south-2', clusters: [{ clusterName: 'testCluster' }] },
108 | { region: 'ap-southeast-1', clusters: [{ clusterName: 'testCluster' }] },
109 | { region: 'ap-southeast-2', clusters: [{ clusterName: 'testCluster' }] },
110 | { region: 'ap-southeast-3', clusters: [{ clusterName: 'testCluster' }] },
111 | { region: 'ap-northeast-1', clusters: [{ clusterName: 'testCluster' }] },
112 | { region: 'ap-northeast-2', clusters: [{ clusterName: 'testCluster' }] },
113 | { region: 'ap-northeast-3', clusters: [{ clusterName: 'testCluster' }] },
114 | { region: 'ca-central-1', clusters: [{ clusterName: 'testCluster' }] },
115 | { region: 'eu-central-1', clusters: [{ clusterName: 'testCluster' }] },
116 | { region: 'eu-west-1', clusters: [{ clusterName: 'testCluster' }] },
117 | { region: 'eu-west-2', clusters: [{ clusterName: 'testCluster' }] },
118 | { region: 'eu-west-3', clusters: [{ clusterName: 'testCluster' }] },
119 | { region: 'eu-south-1', clusters: [{ clusterName: 'testCluster' }] },
120 | { region: 'eu-south-2', clusters: [{ clusterName: 'testCluster' }] },
121 | { region: 'eu-north-1', clusters: [{ clusterName: 'testCluster' }] },
122 | { region: 'me-central-1', clusters: [{ clusterName: 'testCluster' }] },
123 | { region: 'me-south-1', clusters: [{ clusterName: 'testCluster' }] },
124 | { region: 'sa-east-1', clusters: [{ clusterName: 'testCluster' }] }
125 | ]);
126 | });
127 |
128 | test('should list all services in a cluster', async () => {
129 | const response = await request(app).get('/list/AllServices?userId=1&accountName=testAccount&clusterName=testCluster®ion=us-east-1');
130 | expect(response.status).toBe(200);
131 | expect(response.body).toEqual(['testService']);
132 | });
133 | });
134 |
--------------------------------------------------------------------------------
/server/tests/metricController.test.js:
--------------------------------------------------------------------------------
1 | const WebSocket = require('ws');
2 | const { handleMetricRequest } = require('../controllers/metricController');
3 |
4 | // Mock CloudWatchClient and ECSClient
5 | jest.mock('@aws-sdk/client-cloudwatch', () => {
6 | return {
7 | CloudWatchClient: jest.fn(() => ({
8 | send: jest.fn(() => Promise.resolve({
9 | Datapoints: [
10 | { Timestamp: new Date(), Average: 50, Minimum: 40, Maximum: 60, Sum: 150 }
11 | ]
12 | }))
13 | })),
14 | GetMetricStatisticsCommand: jest.fn()
15 | };
16 | });
17 |
18 | jest.mock('@aws-sdk/client-ecs', () => {
19 | return {
20 | ECSClient: jest.fn(() => ({
21 | send: jest.fn((command) => {
22 | if (command.constructor.name === 'DescribeServicesCommand') {
23 | return Promise.resolve({ services: [{ status: 'ACTIVE' }] });
24 | } else if (command.constructor.name === 'ListTasksCommand') {
25 | return Promise.resolve({ taskArns: ['task1', 'task2'] });
26 | } else if (command.constructor.name === 'DescribeTasksCommand') {
27 | return Promise.resolve({
28 | tasks: [
29 | { taskArn: 'task1', lastStatus: 'RUNNING' },
30 | { taskArn: 'task2', lastStatus: 'PENDING' }
31 | ]
32 | });
33 | } else if (command.constructor.name === 'ListServicesCommand') {
34 | return Promise.resolve({ serviceArns: ['service1'] });
35 | }
36 | })
37 | })),
38 | DescribeServicesCommand: jest.fn(),
39 | ListTasksCommand: jest.fn(),
40 | DescribeTasksCommand: jest.fn(),
41 | ListServicesCommand: jest.fn()
42 | };
43 | });
44 |
45 | jest.mock('sqlite3', () => {
46 | const mockDbQuery = jest.fn((sql, params, callback) => {
47 | if (sql.includes('SELECT access_key, secret_key FROM Accounts')) {
48 | callback(null, [{ access_key: 'testAccessKey', secret_key: 'testSecretKey' }]);
49 | } else {
50 | callback(null, []);
51 | }
52 | });
53 |
54 | return {
55 | Database: jest.fn(() => ({
56 | all: mockDbQuery,
57 | get: mockDbQuery
58 | }))
59 | };
60 | });
61 | jest.mock('../controllers/redisClient', () => ({
62 | set: jest.fn().mockResolvedValue('OK')
63 | }));
64 |
65 | describe('WebSocket metricController tests', () => {
66 | let server;
67 | let wss;
68 |
69 | beforeAll(done => {
70 | server = require('http').createServer();
71 | wss = new WebSocket.Server({ server });
72 |
73 | wss.on('connection', (ws, req) => {
74 | if (req.url.startsWith('/getMetricData')) {
75 | const urlParams = new URLSearchParams(req.url.split('?')[1]);
76 | const userId = urlParams.get('userId');
77 | const accountName = urlParams.get('accountName');
78 | const region = urlParams.get('region');
79 | const clusterName = urlParams.get('clusterName');
80 | const serviceName = urlParams.get('serviceName');
81 | const metricName = urlParams.get('metricName');
82 | handleMetricRequest(ws, userId, accountName, region, clusterName, serviceName, metricName);
83 | } else {
84 | ws.close();
85 | }
86 | });
87 |
88 | server.listen(3001, done);
89 | });
90 |
91 | afterAll(done => {
92 | wss.close(() => {
93 | server.close(done);
94 | });
95 | });
96 |
97 | test('WebSocket should handle valid metric data requests', done => {
98 | const ws = new WebSocket('ws://localhost:3001/getMetricData?userId=test&accountName=test®ion=test&clusterName=test&serviceName=test&metricName=CPUUtilization');
99 |
100 | ws.on('open', () => {
101 | ws.send(JSON.stringify({ action: 'getMetricData' }));
102 | });
103 |
104 | ws.on('message', message => {
105 | const data = JSON.parse(message);
106 | expect(data).toEqual(expect.any(Array));
107 | expect(data[0]).toHaveProperty('Timestamp');
108 | expect(data[0]).toHaveProperty('Average');
109 | ws.close();
110 | done();
111 | });
112 |
113 | ws.on('error', err => {
114 | done(err);
115 | });
116 | }, 20000);
117 | });
118 |
--------------------------------------------------------------------------------
/server/tests/notificationController.test.js:
--------------------------------------------------------------------------------
1 | const request = require('supertest');
2 | const express = require('express');
3 | const bodyParser = require('body-parser');
4 | const notificationController = require('../controllers/notificationController');
5 | const redisClient = require('../controllers/redisClient');
6 |
7 | jest.mock('sqlite3', () => {
8 | const mockDbRun = jest.fn((sql, params, callback) => {
9 | callback(null);
10 | });
11 |
12 | return {
13 | Database: jest.fn(() => ({
14 | run: mockDbRun,
15 | })),
16 | };
17 | });
18 |
19 | jest.mock('../controllers/redisClient', () => ({
20 | keys: jest.fn(),
21 | get: jest.fn(),
22 | del: jest.fn(),
23 | }));
24 |
25 | const app = express();
26 | app.use(bodyParser.json());
27 | app.post(
28 | '/setNotification',
29 | notificationController.setNotification,
30 | (req, res) => {
31 | res.status(200).send('Notification settings saved');
32 | }
33 | );
34 |
35 | app.use((err, req, res) => {
36 | res.status(err.status || 500).json({ message: err.message });
37 | });
38 |
39 | describe('notificationController tests', () => {
40 | beforeEach(() => {
41 | jest.clearAllMocks();
42 | });
43 |
44 | test('should save notifications if all fields are valid', async () => {
45 | const response = await request(app)
46 | .post('/setNotification')
47 | .query({
48 | userId: 1,
49 | accountName: 'testAccount',
50 | clusterName: 'testCluster',
51 | region: 'us-east-1',
52 | })
53 | .send({
54 | notifications: [
55 | { metric: 'NetworkRxBytes', threshold: 100, operator: 'greaterThan' },
56 | {
57 | metric: 'CPUUtilization',
58 | applyToAllServices: {
59 | threshold: 80,
60 | operator: 'greaterThanOrEqual',
61 | },
62 | },
63 | {
64 | metric: 'MemoryUtilization',
65 | services: { service1: { threshold: 70, operator: 'lessThan' } },
66 | },
67 | ],
68 | });
69 |
70 | expect(response.status).toBe(200);
71 | expect(response.text).toBe('Notification settings saved');
72 | });
73 |
74 | test('should handle notifications check correctly', async () => {
75 | const ws = {
76 | send: jest.fn(),
77 | close: jest.fn(),
78 | };
79 |
80 | redisClient.keys.mockResolvedValue([
81 | 'notification:testUser:1',
82 | 'notification:testUser:2',
83 | ]);
84 | redisClient.get
85 | .mockResolvedValueOnce(
86 | JSON.stringify({ metric: 'CPUUtilization', value: 90 })
87 | )
88 | .mockResolvedValueOnce(
89 | JSON.stringify({ metric: 'MemoryUtilization', value: 80 })
90 | );
91 | redisClient.del.mockResolvedValue(1);
92 |
93 | await notificationController.handleNotificationCheck(ws, 'testUser');
94 |
95 | expect(ws.send).toHaveBeenCalledWith(
96 | JSON.stringify([
97 | { metric: 'CPUUtilization', value: 90 },
98 | { metric: 'MemoryUtilization', value: 80 },
99 | ])
100 | );
101 | });
102 |
103 | test('should handle no new notifications', async () => {
104 | const ws = {
105 | send: jest.fn(),
106 | close: jest.fn(),
107 | };
108 |
109 | redisClient.keys.mockResolvedValue([]);
110 |
111 | await notificationController.handleNotificationCheck(ws, 'testUser');
112 |
113 | expect(ws.send).toHaveBeenCalledWith(
114 | JSON.stringify({ message: 'No new notifications' })
115 | );
116 | });
117 |
118 | test('should handle errors during notifications check', async () => {
119 | const ws = {
120 | send: jest.fn(),
121 | close: jest.fn(),
122 | };
123 |
124 | redisClient.keys.mockRejectedValue(new Error('Redis error'));
125 |
126 | await notificationController.handleNotificationCheck(ws, 'testUser');
127 |
128 | expect(ws.send).toHaveBeenCalledWith(
129 | JSON.stringify({
130 | error: 'Error checking notifications, Error: Redis error',
131 | })
132 | );
133 | expect(ws.close).toHaveBeenCalled();
134 | });
135 | });
136 |
--------------------------------------------------------------------------------
/server/tests/userController.test.js:
--------------------------------------------------------------------------------
1 | const request = require('supertest');
2 | const express = require('express');
3 |
4 | const app = express();
5 | app.use(express.json());
6 |
7 | jest.mock('nodemailer', () => ({
8 | createTransport: jest.fn().mockReturnValue({
9 | sendMail: jest.fn().mockResolvedValue({ messageId: '123' })
10 | }),
11 | }));
12 |
13 | const nodemailer = require('nodemailer');
14 |
15 | const userController = {
16 | createUser: (req, res) => {
17 | if (req.body.username === 'existingUser') {
18 | return res.status(400).json({ message: 'Username already exists' });
19 | }
20 | const transporter = nodemailer.createTransport();
21 | transporter.sendMail({
22 | from: 'test@example.com',
23 | to: req.body.email,
24 | subject: 'Verification email',
25 | text: 'Please verify your email.'
26 | });
27 |
28 | res.status(200).json({ userId: 1, message: 'User created' });
29 | },
30 | verifyUser: (req, res) => {
31 | if (req.body.username === 'AriaLiang' && req.body.password === 'password123') {
32 | return res.status(200).json({ userId: 1 });
33 | }
34 | res.status(401).json({ message: 'Unauthorized' });
35 | }
36 | };
37 |
38 | app.post('/signup', userController.createUser);
39 | app.post('/login', userController.verifyUser);
40 |
41 | describe('UserController tests', () => {
42 | test("should create user and send verification email when data is valid", async () => {
43 | const userData = {
44 | firstname: "Aria",
45 | lastname: "Liang",
46 | username: "AriaLiang",
47 | password: "password123",
48 | email: "arialiang@example.com"
49 | };
50 |
51 | const response = await request(app).post('/signup').send(userData);
52 |
53 | expect(response.status).toBe(200);
54 | expect(response.body).toHaveProperty('userId');
55 | expect(nodemailer.createTransport().sendMail.mock.calls.length).toBe(1);
56 | });
57 |
58 | test("should return an error if username already exists", async () => {
59 | const userData = {
60 | firstname: 'Aria',
61 | lastname: 'Liang',
62 | username: 'existingUser',
63 | password: 'password123',
64 | email: 'arialiang@example.com'
65 | };
66 |
67 | const response = await request(app).post('/signup').send(userData);
68 | expect(response.status).toBe(400);
69 | expect(response.body.message).toBe('Username already exists');
70 | });
71 |
72 | test("should authenticate user with correct username and password", async () => {
73 | const userData = {
74 | username: "AriaLiang",
75 | password: "password123"
76 | };
77 |
78 | const response = await request(app).post('/login').send(userData);
79 | expect(response.status).toBe(200);
80 | expect(response.body).toHaveProperty('userId');
81 | });
82 |
83 | test("should return error for incorrect password", async () => {
84 | const userData = {
85 | username: "AriaLiang",
86 | password: "wrongPassword"
87 | };
88 |
89 | const response = await request(app).post('/login').send(userData);
90 | expect(response.status).toBe(401);
91 | expect(response.body.message).toBe('Unauthorized');
92 | });
93 | });
94 |
--------------------------------------------------------------------------------