├── .gitignore ├── LICENSE ├── README.md ├── gcfPricingStructure.js ├── package.json ├── src ├── client │ ├── App.jsx │ ├── components │ │ ├── AccountMenu.jsx │ │ ├── DeleteAlert.jsx │ │ ├── DrawerHeader.jsx │ │ ├── DropDownField.jsx │ │ ├── FunctionTable.jsx │ │ ├── NavBar.jsx │ │ ├── ProjectsTable.jsx │ │ └── ZoomGraph.jsx │ ├── css │ │ └── dummyGraph.css │ ├── images │ │ └── rabbit_hop.png │ ├── index.html │ ├── index.js │ ├── pages │ │ ├── ForecastPage.jsx │ │ ├── FunctionsPage.jsx │ │ ├── HomePage.jsx │ │ ├── LoginPage.jsx │ │ ├── MetricsPage.jsx │ │ ├── ProfilePage.jsx │ │ ├── ProjectSetupPage.jsx │ │ └── ProjectsPage.jsx │ ├── slicers │ │ ├── projectsSlice.js │ │ └── userSlice.js │ └── store.js ├── db │ └── db.js └── server │ ├── controllers │ ├── authController.js │ ├── bigQuery.js │ ├── cookieController.js │ ├── forecastController.js │ ├── graphController.js │ ├── metrics.js │ ├── projectController.js │ └── userController.js │ ├── gcfFunction_testing.js │ ├── routers │ ├── forecastRouter.js │ ├── projectRouter.js │ └── userRouter.js │ └── server.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | .env 3 | node_modules 4 | package-lock.json 5 | build 6 | google-cloud-sdk* 7 | util/*.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Open Source Labs Beta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RabbitGCF 2 | 3 | ## Table of Contents 4 | 5 | - [About](#about) 6 | - [How to Use](#how-to-use) 7 | - [Existing Features](#existing-features) 8 | - [Future Features](#future-features) 9 | - [Contributions](#contributions) 10 | - [Developers](#developers) 11 | 12 | ## About 13 | 14 | RabbitGCF is a Google Cloud Function visualizer and cost optimization tool designed to help you make informed decisions about your cloud resources. By inputting your Google Cloud project, RabbitGCF provides detailed metrics for each function. Additionally, our forecasting tool allows you to experiment with different configurations to determine the most cost-effective options. 15 | 16 | ## How to Use 17 | 18 | To get started with RabbitGCF, clone the repository: 19 | 20 | `git clone https://github.com/oslabs-beta/RabbitGCF.git` 21 | 22 | Navigate to the project directory and install dev dependencies: 23 | 24 | `npm install` 25 | 26 | Set up your Google Cloud Function configurations and have your Google Cloud project ID available. 27 | 28 | Create a .env file with a `PROJECT_ID` and set it to your Google Cloud project ID. 29 | 30 | Create a Service Account at https://console.cloud.google.com/iam-admin/serviceaccounts. Click your Google Cloud Functions project and click the "Create Service Account" at the top of the page. Give your service account any name and id you would like. Add the next step, give your service account the roles "Cloud Functions Viewer" and "Monitoring Viewer" to enable read only access to your Functions. Click "Continue" and optionally fill out the "Grant user access to this service account" fields or skip it and click "Done". You now have a Functions read only service account. 31 | 32 | Click the blue hyperlink of the service account under the "Email" column. At the top of the page, click on the "Keys" tab. Create a new key with the "Add Key" button. Choose the JSON key type and click "Create". Your browser will download a JSON key file. Take this JSON file and insert it into the "util" folder in your repository. 33 | 34 | Create a `PROJECT_KEY` variable and set it to the name of your access key including the .json extension. 35 | 36 | Your .env file should look something like this: 37 | ``` 38 | PROJECT_ID=[INSERT PROJECT ID HERE] 39 | PROJECT_KEY=[INSERT PROJECT KEY HERE].json 40 | ``` 41 | 42 | Start the application: 43 | 44 | `npm run dev` 45 | 46 | ## Existing Features 47 | 48 | - Functions Page: displays a list of all Google Cloud functions within your project, with navigation buttons directing to the corresponding metrics and forecast pages 49 | - Metrics Page: contains four key graphs - execution count, runtime, memory usage and network egress - over a selected timeframe to give you an in-depth view of each function’s performance 50 | - Forecast Page: lets you experiment with different configurations for a selected function, visualizing potential outcomes to identify the optimal setup 51 | 52 | ## Future Features 53 | 54 | - Launch via VM with Pre-Packaged SDK: to streamline the user experience, we plan to containerize RabbitGCF, allowing the application to come pre-packaged with the Google Cloud SDK & eliminating the need for users to install the SDK on their machines 55 | - Multi-Project Support: implementing support for managing and visualizing metrics across multiple Google Cloud projects under a single user account 56 | - Optimal Configuration Suggestions: building on our existing forecasting tool, we aim to introduce a feature that automatically suggests the optimal configuration combinations for your Google Cloud Functions, balancing cost efficiency with the desired number of invocations 57 | 58 | ## Contributions 59 | 60 | Contributions, suggestions and reports of any encountered issues from the Open Source community are welcome. 61 | 62 | ## Developers 63 | 64 | - Brendan Lam | [Github](https://github.com/gitbrendanlam) | [Linkedin](https://www.linkedin.com/in/brendanlam/) 65 | - Daniel Park | [Github](https://github.com/dpark001) | [Linkedin](https://www.linkedin.com/in/dpark001/) 66 | - Wilson Chen | [Github](https://github.com/Wilson7chen) | [Linkedin](https://www.linkedin.com/in/wilson7chen/) 67 | - Alexandra Thorne | [Github](https://github.com/AlexaThr) | [Linkedin](http://linkedin.com/in/alexathorne) 68 | -------------------------------------------------------------------------------- /gcfPricingStructure.js: -------------------------------------------------------------------------------- 1 | const gcfPricingStructure = {}; 2 | 3 | gcfPricingStructure.gcfComputePricing = { 4 | "Tier 1": { 5 | memoryGbPrice: 0.0000025, 6 | cpuGHzPrice: 0.00001, 7 | }, 8 | "Tier 2": { 9 | memoryGbPrice: 0.0000035, 10 | cpuGHzPrice: 0.000014, 11 | } 12 | }; 13 | 14 | gcfPricingStructure.gcfRegionTiers = { 15 | "asia-east1": { 16 | "1": "Tier 1", 17 | "2": "Tier 1" 18 | }, 19 | "asia-east2": { 20 | "1": "Tier 1", 21 | "2": "Tier 2" 22 | }, 23 | "asia-northeast1": { 24 | "1": "Tier 1", 25 | "2": "Tier 1" 26 | }, 27 | "asia-northeast2": { 28 | "1": "Tier 1", 29 | "2": "Tier 1" 30 | }, 31 | "asia-northeast3": { 32 | "1": "Tier 2", 33 | "2": "Tier 2" 34 | }, 35 | "asia-south1": { 36 | "1": "Tier 2", 37 | "2": "Tier 2" 38 | }, 39 | "asia-south2": { 40 | "2": "Tier 2" 41 | }, 42 | "asia-southeast1": { 43 | "1": "Tier 2", 44 | "2": "Tier 2" 45 | }, 46 | "asia-southeast2": { 47 | "1": "Tier 2", 48 | "2": "Tier 2" 49 | }, 50 | "australia-southeast1": { 51 | "1": "Tier 2", 52 | "2": "Tier 2" 53 | }, 54 | "australia-southeast2": { 55 | "2": "Tier 2" 56 | }, 57 | "europe-central2": { 58 | "1": "Tier 2", 59 | "2": "Tier 2" 60 | }, 61 | "europe-north1": { 62 | "2": "Tier 1" 63 | }, 64 | "europe-southwest1": { 65 | "2": "Tier 1" 66 | }, 67 | "europe-west1": { 68 | "1": "Tier 1", 69 | "2": "Tier 1" 70 | }, 71 | "europe-west10": { 72 | "2": "Tier 2" 73 | }, 74 | "europe-west12": { 75 | "2": "Tier 2" 76 | }, 77 | "europe-west2": { 78 | "1": "Tier 1", 79 | "2": "Tier 2" 80 | }, 81 | "europe-west3": { 82 | "1": "Tier 2", 83 | "2": "Tier 2" 84 | }, 85 | "europe-west4": { 86 | "2": "Tier 1" 87 | }, 88 | "europe-west6": { 89 | "1": "Tier 2", 90 | "2": "Tier 2" 91 | }, 92 | "europe-west8": { 93 | "2": "Tier 1" 94 | }, 95 | "europe-west9": { 96 | "2": "Tier 1" 97 | }, 98 | "me-central1": { 99 | "2": "Tier 2" 100 | }, 101 | "me-central2": { 102 | "2": "Tier 2" 103 | }, 104 | "me-west1": { 105 | "2": "Tier 1" 106 | }, 107 | "northamerica-northeast1": { 108 | "1": "Tier 2", 109 | "2": "Tier 2" 110 | }, 111 | "northamerica-northeast2": { 112 | "2": "Tier 2" 113 | }, 114 | "southamerica-east1": { 115 | "1": "Tier 2", 116 | "2": "Tier 2" 117 | }, 118 | "southamerica-west1": { 119 | "2": "Tier 2" 120 | }, 121 | "us-central1": { 122 | "1": "Tier 1", 123 | "2": "Tier 1" 124 | }, 125 | "us-east1": { 126 | "1": "Tier 1", 127 | "2": "Tier 1" 128 | }, 129 | "us-east4": { 130 | "1": "Tier 1", 131 | "2": "Tier 1" 132 | }, 133 | "us-east5": { 134 | "2": "Tier 1" 135 | }, 136 | "us-south1": { 137 | "2": "Tier 1" 138 | }, 139 | "us-west1": { 140 | "1": "Tier 1", 141 | "2": "Tier 1" 142 | }, 143 | "us-west2": { 144 | "1": "Tier 2", 145 | "2": "Tier 2" 146 | }, 147 | "us-west3": { 148 | "1": "Tier 2", 149 | "2": "Tier 2" 150 | }, 151 | "us-west4": { 152 | "1": "Tier 2", 153 | "2": "Tier 2" 154 | } 155 | }; 156 | 157 | gcfPricingStructure.gcfTypes = { 158 | "Memory: 128MB / CPU: 200MHz": { mb: 128, mhz: 200 }, 159 | "Memory: 256MB / CPU: 400MHz": { mb: 256, mhz: 400 }, 160 | "Memory: 512MB / CPU: 800MHz": { mb: 512, mhz: 800 }, 161 | "Memory: 1GB / CPU: 1.4GHz": { mb: 1024, mhz: 1400 }, 162 | "Memory: 2GB / CPU: 2.8GHz": { mb: 2048, mhz: 2800 }, 163 | "Memory: 4GB / CPU: 4.8GHz": { mb: 4096, mhz: 4800 }, 164 | "Memory: 8GB / CPU: 4.8MHz": { mb: 8192, mhz: 4800 }, 165 | "Memory: 16GB / CPU: 4.8MHz": { mb: 16384, mhz: 4800 }, 166 | "Memory: 32GB / CPU: 4.8MHz": { mb: 32768, mhz: 4800 } 167 | } 168 | 169 | gcfPricingStructure.typeMapping = { 170 | "128Mi": "Memory: 128MB / CPU: 200MHz", 171 | "256Mi": "Memory: 256MB / CPU: 400MHz", 172 | "512Mi": "Memory: 512MB / CPU: 800MHz", 173 | "1Gi": "Memory: 1GB / CPU: 1.4GHz", 174 | "2Gi": "Memory: 2GB / CPU: 2.8GHz", 175 | "4Gi": "Memory: 4GB / CPU: 4.8GHz", 176 | "8Gi": "Memory: 8GB / CPU: 4.8MHz", 177 | "16Gi": "Memory: 16GB / CPU: 4.8MHz", 178 | "32Gi": "Memory: 32GB / CPU: 4.8MHz" 179 | } 180 | 181 | gcfPricingStructure.genMapping = { 182 | "GEN_1": "1", 183 | "GEN_2": "2", 184 | } 185 | 186 | module.exports = gcfPricingStructure; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rabbitgcf", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "nodemon src/server/server.js", 8 | "build": "webpack", 9 | "dev": "concurrently \"webpack serve --open\" \"nodemon src/server/server.js\"", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "author": "", 13 | "license": "MIT", 14 | "dependencies": { 15 | "@emotion/react": "^11.11.4", 16 | "@emotion/styled": "^11.11.5", 17 | "@google-cloud/bigquery": "^7.7.1", 18 | "@google-cloud/functions": "^3.4.0", 19 | "@google-cloud/logging": "^11.0.0", 20 | "@google-cloud/monitoring": "^4.1.0", 21 | "@google-cloud/storage": "^7.11.2", 22 | "@google-cloud/trace-agent": "^8.0.0", 23 | "@mui/icons-material": "^5.15.19", 24 | "@mui/material": "^5.15.19", 25 | "@react-oauth/google": "^0.12.1", 26 | "@reduxjs/toolkit": "^2.2.7", 27 | "cors": "^2.8.5", 28 | "date-fns": "^3.6.0", 29 | "dotenv": "^16.4.5", 30 | "dotenv-webpack": "^8.1.0", 31 | "express": "^4.19.2", 32 | "express-session": "^1.18.0", 33 | "file-loader": "^6.2.0", 34 | "google-auth-library": "^9.11.0", 35 | "nodemon": "^3.1.2", 36 | "passport": "^0.7.0", 37 | "passport-google-oauth2": "^0.2.0", 38 | "path": "^0.12.7", 39 | "postgres": "^3.4.4", 40 | "react": "^18.3.1", 41 | "react-dom": "^18.3.1", 42 | "react-redux": "^9.1.2", 43 | "react-router-dom": "^6.23.1", 44 | "react-select": "^5.8.0" 45 | }, 46 | "devDependencies": { 47 | "@babel/core": "^7.24.6", 48 | "@babel/preset-env": "^7.24.6", 49 | "@babel/preset-react": "^7.24.6", 50 | "babel-loader": "^9.1.3", 51 | "concurrently": "^8.2.2", 52 | "css-loader": "^7.1.2", 53 | "html-webpack-plugin": "^5.6.0", 54 | "recharts": "^2.12.7", 55 | "style-loader": "^4.0.0", 56 | "webpack-cli": "^5.1.4", 57 | "webpack-dev-server": "^5.0.4" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/client/App.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { BrowserRouter, Route, Routes } from 'react-router-dom'; 3 | import HomePage from './pages/HomePage.jsx'; 4 | import MetricsPage from './pages/MetricsPage.jsx'; 5 | import ForecastPage from './pages/ForecastPage.jsx'; 6 | import ProfilePage from './pages/ProfilePage.jsx'; 7 | import LoginPage from './pages/LoginPage.jsx'; 8 | import FunctionsPage from './pages/FunctionsPage.jsx'; 9 | import ProjectsPage from './pages/ProjectsPage.jsx'; 10 | import ProjectSetupPage from './pages/ProjectSetupPage.jsx'; 11 | 12 | const App = () => { 13 | const [functionName, setFunctionName] = useState(""); 14 | const [projectList, setProjectList] = useState([]); 15 | const [selectedProject, setSelectedProject] = useState(null); 16 | 17 | return( 18 |
19 | 20 | 21 | }/> 22 | }/> 23 | }/> 24 | }/> 25 | }/> 26 | }/> 27 | }/> 28 | }/> 29 | 30 | 31 |
32 | ); 33 | }; 34 | 35 | export default App; -------------------------------------------------------------------------------- /src/client/components/AccountMenu.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { Button, Box, Link } from '@mui/material'; 3 | import Menu from '@mui/material/Menu'; 4 | import MenuItem from '@mui/material/MenuItem'; 5 | import Divider from '@mui/material/Divider'; 6 | import IconButton from '@mui/material/IconButton'; 7 | import LogoutIcon from '@mui/icons-material/Logout'; 8 | import AccountCircleIcon from '@mui/icons-material/AccountCircle'; 9 | import ManageAccountsIcon from '@mui/icons-material/ManageAccounts'; 10 | import { useNavigate } from 'react-router-dom'; 11 | import { useSelector, useDispatch } from "react-redux"; 12 | import { login, setProfile, logout } from "../slicers/userSlice.js"; 13 | import { GoogleLogin, googleLogout, useGoogleLogin } from '@react-oauth/google' 14 | 15 | const AccountMenu = () => { 16 | const dispatch = useDispatch(); 17 | const user = useSelector( state => state.user ); 18 | const [ anchorEl, setAnchorEl ] = useState(null); 19 | const open = Boolean(anchorEl); 20 | const navigate = useNavigate(); 21 | 22 | useEffect( 23 | () => { 24 | if (user.authCredentials) { 25 | fetch(`https://www.googleapis.com/oauth2/v1/userinfo?access_token=${user.authCredentials.access_token}`, 26 | { 27 | method: "GET", 28 | headers: { "Content-Type": "application/json" }, 29 | } 30 | ) 31 | .then(res => res.json()) 32 | .then(res => { 33 | dispatch(setProfile(res)); 34 | }) 35 | .catch(err => console.log(err)); 36 | } 37 | }, [user.authCredentials] 38 | ) 39 | 40 | 41 | const loginRequest = () => { 42 | if (user.profile) { 43 | console.log('userProfile useEffect ==>', user.profile); 44 | // Fetch request to backend to store login info 45 | fetch('/api/user/login', { 46 | method: "POST", 47 | headers: { "Content-Type": "application/json" }, 48 | body: JSON.stringify({ 49 | name: user.profile.name, 50 | email: user.profile.email, 51 | profile_id: user.profile.id, 52 | }) 53 | }) 54 | } 55 | } 56 | 57 | function openMenu(e) { 58 | setAnchorEl(e.target); 59 | } 60 | 61 | function closeMenu() { 62 | setAnchorEl(null); 63 | } 64 | 65 | function profileClick() { 66 | navigate('/profile'); 67 | } 68 | 69 | const loginClick = useGoogleLogin({ 70 | onSuccess: (codeResponse) => { 71 | console.log(codeResponse); 72 | dispatch(login(codeResponse)); 73 | loginRequest(); 74 | closeMenu(); 75 | }, 76 | onError: (error) => { 77 | alert('Login failed'); 78 | console.log('Login failed | Error:', error); 79 | } 80 | }) 81 | 82 | const logoutClick = () => { 83 | googleLogout(); 84 | dispatch(logout()); 85 | console.log('logged Out'); 86 | } 87 | 88 | return( 89 |
90 | { user.isLoggedIn && user.profile ? 91 | 92 | {user.profile.name} 93 | 97 | 98 | 99 | 105 | 106 | 107 | My Account 108 | 109 | 110 | 111 | 112 | Log out 113 | 114 | 115 | : 116 | 117 | 123 | 129 | 130 | Sign In with Google 131 | 132 | 133 | 134 | 135 | } 136 |
137 | ); 138 | } 139 | 140 | export default AccountMenu; -------------------------------------------------------------------------------- /src/client/components/DeleteAlert.jsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Button from '@mui/material/Button'; 3 | import Dialog from '@mui/material/Dialog'; 4 | import DialogActions from '@mui/material/DialogActions'; 5 | import DialogContent from '@mui/material/DialogContent'; 6 | import DialogContentText from '@mui/material/DialogContentText'; 7 | import DialogTitle from '@mui/material/DialogTitle'; 8 | import { useDispatch, useSelector } from 'react-redux'; 9 | import { deleteProject, focusProject } from '../slicers/projectsSlice'; 10 | 11 | export default function DeleteAlert({ projectFocusIndex, deleteAlertOpen, setDeleteAlertOpen }) { 12 | const dispatch = useDispatch(); 13 | const projectList = useSelector(state => state.projects.projectList); 14 | 15 | const agree = (e) => { 16 | (() => { 17 | dispatch(deleteProject(projectFocusIndex)); 18 | })() 19 | 20 | setDeleteAlertOpen(false); 21 | } 22 | 23 | const disagree = () => { 24 | setDeleteAlertOpen(false); 25 | }; 26 | 27 | return ( 28 | 29 | 35 | 36 | {"WARNING"} 37 | 38 | 39 | 40 | Are you sure you want to delete this project? 41 | 42 | 43 | 44 | 45 | 48 | 49 | 50 | 51 | ); 52 | } -------------------------------------------------------------------------------- /src/client/components/DrawerHeader.jsx: -------------------------------------------------------------------------------- 1 | import { styled } from '@mui/material/styles'; 2 | 3 | const DrawerHeader = styled('div')(({ theme }) => ({ 4 | display: 'flex', 5 | alignItems: 'center', 6 | justifyContent: 'flex-end', 7 | padding: theme.spacing(0, 1), 8 | // necessary for content to be below app bar 9 | ...theme.mixins.toolbar, 10 | })); 11 | 12 | export default DrawerHeader; -------------------------------------------------------------------------------- /src/client/components/DropDownField.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { FormControl, Select, InputLabel, MenuItem } from "@mui/material"; 3 | 4 | const DropDownField = ({ fieldType, fieldName = fieldType, optionsList, selected, handleOptionChange }) => { 5 | return ( 6 | 7 | {fieldName} 8 | 23 | 24 | ) 25 | } 26 | 27 | export default DropDownField; -------------------------------------------------------------------------------- /src/client/components/FunctionTable.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import Paper from "@mui/material/Paper"; 3 | import Table from "@mui/material/Table"; 4 | import TableBody from "@mui/material/TableBody"; 5 | import TableCell from "@mui/material/TableCell"; 6 | import TableContainer from "@mui/material/TableContainer"; 7 | import TableHead from "@mui/material/TableHead"; 8 | import TablePagination from "@mui/material/TablePagination"; 9 | import TableRow from "@mui/material/TableRow"; 10 | import Button from "@mui/material/Button"; 11 | import Skeleton from "@mui/material/Skeleton"; 12 | import { useNavigate } from "react-router-dom"; 13 | 14 | const columns = [ 15 | { id: "function", label: "Function", minWidth: 170 }, 16 | { id: "metrics", label: "Metrics", minWidth: 100 }, 17 | { id: "forcast", label: "Forcast", minWidth: 100 }, 18 | ]; 19 | 20 | let rows = []; 21 | 22 | export default function FunctionTable(props) { 23 | const [page, setPage] = useState(0); 24 | const [rowsPerPage, setRowsPerPage] = useState(10); 25 | const [loaded, setLoaded] = useState(false); 26 | 27 | const navigate = useNavigate(); 28 | 29 | const handleChangePage = (event, newPage) => { 30 | setPage(newPage); 31 | }; 32 | 33 | const handleChangeRowsPerPage = (event) => { 34 | setRowsPerPage(+event.target.value); 35 | setPage(0); 36 | }; 37 | 38 | const handleMetricClick = (e) => { 39 | props.setFunctionName(e.target.value); 40 | navigate("/metrics"); 41 | }; 42 | 43 | const handleForcastClick = (e) => { 44 | props.setFunctionName(e.target.value); 45 | navigate("/forecast"); 46 | }; 47 | 48 | const [projectId, setProjectId] = useState(''); 49 | 50 | // const getProjectId = async() => { 51 | // try { 52 | // const response = await fetch(`/api/getProjectId`, { 53 | // method: 'GET', 54 | // headers: { 'Content-Type': 'application/json' }, 55 | // }); 56 | // const data = await response.json(); 57 | // console.log(data); 58 | // setProjectId(data); 59 | // } catch (error) { 60 | // console.log('Error in getProjectId: ', error); 61 | // } 62 | // } 63 | 64 | // useEffect(() => { 65 | // getProjectId(); 66 | // }, []); 67 | 68 | const getFunctionList = async () => { 69 | try { 70 | const projectIdResponse = await fetch(`/api/getProjectId`, { 71 | method: 'GET', 72 | headers: { 'Content-Type': 'application/json' }, 73 | }); 74 | const projectIdData = await projectIdResponse.json(); 75 | console.log(projectIdData); 76 | setProjectId(projectId); 77 | const response = await fetch( 78 | `/api/metrics/funcs/${projectIdData}`, 79 | { 80 | method: "GET", 81 | headers: { "Content-Type": "application/json" }, 82 | } 83 | ); 84 | const data = await response.json(); 85 | console.log(data.funcList); 86 | rows = data.funcList; 87 | setLoaded(true); 88 | } catch (error) { 89 | console.log("Error in getFunctionList: ", error); 90 | } 91 | }; 92 | 93 | useEffect(() => { 94 | getFunctionList(); 95 | }, []); 96 | 97 | return ( 98 |
99 | {loaded ? ( 100 | 101 | 102 | 103 | 104 | 105 | {columns.map((column) => ( 106 | 111 | {column.label} 112 | 113 | ))} 114 | 115 | 116 | 117 | {rows 118 | .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) 119 | .map((row) => { 120 | return ( 121 | 122 | {row} 123 | 124 | 131 | 132 | 133 | 140 | 141 | 142 | ); 143 | })} 144 | 145 |
146 |
147 | 156 |
157 | ) : ( 158 | 165 | )} 166 |
167 | ); 168 | } 169 | -------------------------------------------------------------------------------- /src/client/components/NavBar.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { styled, useTheme } from '@mui/material/styles'; 3 | import { useNavigate } from 'react-router-dom'; 4 | import { useSelector, useDispatch } from 'react-redux'; 5 | import { Link } from 'react-router-dom'; 6 | 7 | import DrawerHeader from './DrawerHeader.jsx'; 8 | import AccountMenu from './AccountMenu.jsx'; 9 | import Box from '@mui/material/Box'; 10 | import MuiDrawer from '@mui/material/Drawer'; 11 | import MuiAppBar from '@mui/material/AppBar'; 12 | import Toolbar from '@mui/material/Toolbar'; 13 | import List from '@mui/material/List'; 14 | import CssBaseline from '@mui/material/CssBaseline'; 15 | import Typography from '@mui/material/Typography'; 16 | import Divider from '@mui/material/Divider'; 17 | import IconButton from '@mui/material/IconButton'; 18 | import ListItem from '@mui/material/ListItem'; 19 | import ListItemButton from '@mui/material/ListItemButton'; 20 | import ListItemIcon from '@mui/material/ListItemIcon'; 21 | import ListItemText from '@mui/material/ListItemText'; 22 | 23 | import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; 24 | import MenuIcon from '@mui/icons-material/Menu'; 25 | import HomeIcon from '@mui/icons-material/Home'; 26 | import AssessmentIcon from '@mui/icons-material/Assessment'; 27 | import InsightsIcon from '@mui/icons-material/Insights'; 28 | import DataObjectIcon from '@mui/icons-material/DataObject'; 29 | import CloudQueueIcon from '@mui/icons-material/CloudQueue'; 30 | import { Button } from '@mui/material'; 31 | 32 | import DropDownField from './DropDownField.jsx'; 33 | 34 | const drawerWidth = 240; 35 | 36 | const openedMixin = (theme) => ({ 37 | width: drawerWidth, 38 | transition: theme.transitions.create('width', { 39 | easing: theme.transitions.easing.sharp, 40 | duration: theme.transitions.duration.enteringScreen, 41 | }), 42 | overflowX: 'hidden', 43 | }); 44 | 45 | const closedMixin = (theme) => ({ 46 | transition: theme.transitions.create('width', { 47 | easing: theme.transitions.easing.sharp, 48 | duration: theme.transitions.duration.leavingScreen, 49 | }), 50 | overflowX: 'hidden', 51 | width: `calc(${theme.spacing(7)} + 1px)`, 52 | [theme.breakpoints.up('sm')]: { 53 | width: `calc(${theme.spacing(8)} + 1px)`, 54 | }, 55 | }); 56 | 57 | const AppBar = styled(MuiAppBar, { 58 | shouldForwardProp: (prop) => prop !== 'open', 59 | })(({ theme, open }) => ({ 60 | zIndex: theme.zIndex.drawer + 1, 61 | transition: theme.transitions.create(['width', 'margin'], { 62 | easing: theme.transitions.easing.sharp, 63 | duration: theme.transitions.duration.leavingScreen, 64 | }), 65 | ...(open && { 66 | marginLeft: drawerWidth, 67 | width: `calc(100% - ${drawerWidth}px)`, 68 | transition: theme.transitions.create(['width', 'margin'], { 69 | easing: theme.transitions.easing.sharp, 70 | duration: theme.transitions.duration.enteringScreen, 71 | }), 72 | }), 73 | })); 74 | 75 | const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => prop !== 'open' })( 76 | ({ theme, open }) => ({ 77 | width: drawerWidth, 78 | flexShrink: 0, 79 | whiteSpace: 'nowrap', 80 | boxSizing: 'border-box', 81 | ...(open && { 82 | ...openedMixin(theme), 83 | '& .MuiDrawer-paper': openedMixin(theme), 84 | }), 85 | ...(!open && { 86 | ...closedMixin(theme), 87 | '& .MuiDrawer-paper': closedMixin(theme), 88 | }), 89 | }), 90 | ); 91 | 92 | export default function NavBar() { 93 | const theme = useTheme(); 94 | const [open, setOpen] = useState(false); 95 | const navigate = useNavigate(); 96 | const projectList = useSelector( state => state.projects.projectList ); 97 | 98 | const handleDrawerOpen = () => { 99 | setOpen(true); 100 | }; 101 | 102 | const handleDrawerClose = () => { 103 | setOpen(false); 104 | }; 105 | 106 | function homeClick() { 107 | console.log('home clicked') 108 | navigate('/'); 109 | } 110 | 111 | function metricsClick() { 112 | console.log('metrics clicked') 113 | navigate('/metrics'); 114 | } 115 | 116 | function forecastClick() { 117 | console.log('forecast clicked') 118 | navigate('/forecast'); 119 | } 120 | 121 | function functionsClick() { 122 | console.log('functions clicked') 123 | navigate('/functions'); 124 | } 125 | 126 | function projectsClick() { 127 | console.log('projects clicked') 128 | navigate('/projects'); 129 | } 130 | 131 | return ( 132 | 133 | 134 | 135 | 136 | 146 | 147 | 148 | 149 | RabbitGCF 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 171 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 195 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 216 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 237 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 258 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | ); 274 | } 275 | -------------------------------------------------------------------------------- /src/client/components/ProjectsTable.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { useNavigate } from "react-router-dom"; 3 | import DeleteAlert from "./DeleteAlert.jsx"; 4 | import { useSelector, useDispatch } from "react-redux"; 5 | import { editProject } from "../slicers/projectsSlice.js"; 6 | import { 7 | Box, 8 | Paper, 9 | Table, 10 | TableBody, 11 | TableCell, 12 | TableContainer, 13 | TableHead, 14 | TablePagination, 15 | TableRow, 16 | Button, 17 | Skeleton, 18 | IconButton } from "@mui/material" 19 | 20 | 21 | const columns = [ 22 | { id: "projectName", label: "Project", minWidth: 100 }, 23 | { id: "projectId", label: "ID", minWidth: 100 }, 24 | // { id: "connectionStatus", label: "Status", minWidth: 100 }, 25 | { id: "settings", minWidth: 100 } 26 | ]; 27 | 28 | const projectId = "refined-engine-424416-p7"; 29 | 30 | export default function ProjectsTable() { 31 | const dispatch = useDispatch(); 32 | 33 | const [page, setPage] = useState(0); 34 | const [rowsPerPage, setRowsPerPage] = useState(10); 35 | const [deleteAlertOpen, setDeleteAlertOpen] = useState(false); 36 | const [deleteIndex, setDeleteIndex] = useState(null); 37 | 38 | const navigate = useNavigate(); 39 | const projectList = useSelector((state) => state.projects.projectList); 40 | 41 | const handleChangePage = (event, newPage) => { 42 | setPage(newPage); 43 | }; 44 | 45 | const handleChangeRowsPerPage = (event) => { 46 | setRowsPerPage(+event.target.value); 47 | setPage(0); 48 | }; 49 | 50 | const editProject = (e) => { 51 | console.log('edit project clicked'); 52 | navigate("/projects/setup", { state: { project: projectList[e.target.value], projectListIndex: e.target.value }}); 53 | } 54 | 55 | const deleteProject = (e) => { 56 | setDeleteIndex(e.target.value); 57 | setDeleteAlertOpen(true); 58 | } 59 | 60 | console.log(projectList); 61 | return ( 62 |
63 | 64 | 65 | 66 | 67 | 68 | {columns.map((column) => ( 69 | 74 | {column.label} 75 | 76 | ))} 77 | 78 | 79 | 80 | {projectList 81 | // .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) 82 | .map((project, index) => { 83 | return ( 84 | 85 | {project.projectName} 86 | {project.projectId} 87 | 88 | 89 | 96 | 101 | 102 | 103 | 104 | ); 105 | })} 106 | 107 |
108 |
109 | 118 |
119 | {(deleteAlertOpen && deleteIndex !== null) && } 120 |
121 | ); 122 | } 123 | -------------------------------------------------------------------------------- /src/client/components/ZoomGraph.jsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { 3 | LineChart, 4 | Legend, 5 | Line, 6 | CartesianGrid, 7 | XAxis, 8 | YAxis, 9 | Tooltip, 10 | ReferenceArea, 11 | ResponsiveContainer 12 | } from 'recharts'; 13 | import '../css/dummyGraph.css'; 14 | 15 | const CustomizedDot = (props) => { 16 | const { cx, cy, value } = props; 17 | if (value > 0) { 18 | return ( 19 | 20 | ); 21 | } 22 | return null; 23 | }; 24 | 25 | 26 | class GraphComponent extends PureComponent { 27 | constructor(props) { 28 | super(props); 29 | this.state = { 30 | refAreaLeft: '', 31 | refAreaRight: '', 32 | zoomedData: null, 33 | disableAnimation: false, 34 | filledData: [] 35 | }; 36 | } 37 | 38 | componentDidMount() { 39 | this.updateFilledData(); 40 | } 41 | 42 | componentDidUpdate(prevProps) { 43 | if (prevProps.data !== this.props.data || prevProps.timeRange !== this.props.timeRange) { 44 | this.updateFilledData(); 45 | } 46 | } 47 | 48 | updateFilledData() { 49 | const { data, timeRange } = this.props; 50 | const interval = 60000; 51 | const filledData = this.fillDataGaps(data, interval); 52 | this.setState({ filledData }); 53 | } 54 | 55 | normalizeTimestamp = (timestamp) => { 56 | const date = new Date(timestamp); 57 | date.setSeconds(0); 58 | date.setMilliseconds(0); 59 | return date.toISOString(); 60 | }; 61 | 62 | generateTimeIntervals = (start, end, interval) => { 63 | const timeIntervals = []; 64 | let current = new Date(start); 65 | 66 | while (current <= end) { 67 | timeIntervals.push(this.normalizeTimestamp(current)); 68 | current = new Date(current.getTime() + interval); 69 | } 70 | return timeIntervals; 71 | }; 72 | 73 | fillDataGaps = (data, interval = 60000) => { 74 | if (!data || data.length === 0) { 75 | return []; 76 | } 77 | 78 | const points = data.flatMap(d => d.points || 79 | (d.timestamp && d.value ? [{ timestamp: new Date(d.timestamp).toISOString(), value: d.value }] : []) 80 | ); 81 | 82 | if (points.length === 0) { 83 | return []; 84 | } 85 | 86 | const end = new Date(); 87 | const start = new Date(end.getTime() - this.props.timeRange * 60 * 1000); 88 | 89 | const timeIntervals = this.generateTimeIntervals(start, end, interval); 90 | 91 | return timeIntervals.map(time => { 92 | const existingPoint = points.find(point => new Date(point.timestamp).getTime() === new Date(time).getTime()); 93 | return existingPoint || { timestamp: time, value: 0 }; 94 | }); 95 | }; 96 | 97 | zoom = () => { 98 | const { refAreaLeft, refAreaRight, filledData } = this.state; 99 | 100 | if (!refAreaLeft || !refAreaRight) { 101 | return; 102 | } 103 | 104 | let refAreaLeftTimestamp = new Date(refAreaLeft).getTime(); 105 | let refAreaRightTimestamp = new Date(refAreaRight).getTime(); 106 | 107 | if (refAreaLeftTimestamp > refAreaRightTimestamp) { 108 | [refAreaLeftTimestamp, refAreaRightTimestamp] = [refAreaRightTimestamp, refAreaLeftTimestamp]; 109 | } 110 | 111 | const zoomedData = filledData.filter(d => { 112 | const dataPointTimestamp = new Date(d.timestamp).getTime(); 113 | return dataPointTimestamp >= refAreaLeftTimestamp && dataPointTimestamp <= refAreaRightTimestamp; 114 | }).map(d => ({ 115 | ...d, 116 | timestamp: new Date(d.timestamp).getTime(), 117 | })); 118 | 119 | this.setState({ 120 | refAreaLeft: '', 121 | refAreaRight: '', 122 | zoomedData, 123 | disableAnimation: true, 124 | }); 125 | }; 126 | 127 | zoomOut = () => { 128 | this.setState({ 129 | zoomedData: null, 130 | refAreaLeft: '', 131 | refAreaRight: '', 132 | disableAnimation: false 133 | }); 134 | }; 135 | 136 | formatXAxis = (tickItem) => { 137 | const date = new Date(tickItem); 138 | 139 | const interval = (this.props.timeRange <= 60) ? 5 : (this.props.timeRange <= 1440) ? 30 : 60; 140 | 141 | const roundedMinutes = Math.round(date.getMinutes() / interval) * interval; 142 | date.setMinutes(roundedMinutes); 143 | date.setSeconds(0); 144 | date.setMilliseconds(0); 145 | 146 | if (this.props.timeRange <= 60) { 147 | return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); 148 | } else if (this.props.timeRange <= 1440) { 149 | return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); 150 | } else if (this.props.timeRange <= 2880) { 151 | const checkDate = new Date(date); 152 | checkDate.setHours(0, 0, 0, 0); 153 | const isStartOfDay = checkDate.getTime() === date.getTime(); 154 | if (isStartOfDay){ 155 | return date.toLocaleDateString([], { month: 'short', day: 'numeric'}); 156 | } else { 157 | return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit'}); 158 | } 159 | } else { 160 | return date.toLocaleDateString([], { month: 'short', day: 'numeric'}); 161 | } 162 | }; 163 | 164 | render() { 165 | const { data, dataKey, label, timeRange } = this.props; 166 | const { refAreaLeft, refAreaRight, zoomedData, disableAnimation } = this.state; 167 | 168 | const normalizeToMilliseconds = (data) => { 169 | return data.map(d => ({ 170 | ...d, 171 | timestamp: new Date(d.timestamp).getTime() 172 | })); 173 | }; 174 | 175 | const allDataPoints = normalizeToMilliseconds(data.flatMap(d => d.points || 176 | (d.timestamp && d.value ? [{ timestamp: new Date(d.timestamp).toISOString(), value: d.value }] : []) 177 | )); 178 | 179 | const interval = 60000; 180 | const filledChartData = this.fillDataGaps(zoomedData ? normalizeToMilliseconds(zoomedData) : allDataPoints, interval).map(entry => ({ 181 | ...entry, 182 | timestamp: new Date(entry.timestamp).getTime() 183 | })); 184 | 185 | let startTime, endTime; 186 | 187 | if (zoomedData && zoomedData.length > 0) { 188 | startTime = zoomedData[0].timestamp; 189 | endTime = zoomedData[zoomedData.length - 1].timestamp; 190 | } else { 191 | endTime = Date.now(); 192 | startTime = endTime - timeRange * 60 * 1000; 193 | } 194 | 195 | return ( 196 |
197 | 198 | 199 | { 202 | this.setState({ refAreaLeft: new Date(e.activeLabel).toISOString() }); 203 | }} 204 | onMouseMove={(e) => { 205 | if (this.state.refAreaLeft) { 206 | this.setState({ refAreaRight: new Date(e.activeLabel).toISOString() }); 207 | } 208 | }} 209 | onMouseUp={this.zoom} 210 | > 211 | 212 | 219 | 220 | new Date(label).toString()} /> 221 | 226 | } stroke="#8884d8" animationDuration={disableAnimation ? 0 : 1000} /> 227 | {refAreaLeft && refAreaRight ? ( 228 | 229 | ) : null} 230 | 231 | 232 |
233 | ); 234 | } 235 | } 236 | export default GraphComponent; 237 | -------------------------------------------------------------------------------- /src/client/css/dummyGraph.css: -------------------------------------------------------------------------------- 1 | /* making sure users don't select text on XY axes across all browsers */ 2 | .chart-container { 3 | -webkit-user-select: none; 4 | -moz-user-select: none; 5 | -ms-user-select: none; 6 | user-select: none; 7 | } 8 | -------------------------------------------------------------------------------- /src/client/images/rabbit_hop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/RabbitGCF/63463da8b48f4d672906fdd58647f196b04a80b4/src/client/images/rabbit_hop.png -------------------------------------------------------------------------------- /src/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | RabbitGCF 7 | 8 | 9 |
10 | 11 | -------------------------------------------------------------------------------- /src/client/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { GoogleOAuthProvider } from '@react-oauth/google'; 3 | import App from './App.jsx'; 4 | import { createRoot } from 'react-dom/client'; 5 | import store from './store.js'; 6 | import { Provider } from 'react-redux'; 7 | 8 | const root = createRoot(document.getElementById('root')); 9 | root.render( 10 | 11 | 12 | 13 | 14 | 15 | 16 | ) -------------------------------------------------------------------------------- /src/client/pages/ForecastPage.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import NavBar from '../components/NavBar.jsx'; 3 | import Box from '@mui/material/Box'; 4 | import DrawerHeader from '../components/DrawerHeader.jsx'; 5 | import Typography from '@mui/material/Typography'; 6 | import { FormGroup, FormControlLabel, Checkbox, TextField, Button, CircularProgress, Skeleton } from '@mui/material'; 7 | import { BarChart, Bar, LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, Label } from 'recharts'; 8 | import gcfPricingStructure from '../../../gcfPricingStructure'; 9 | import DropDownField from '../components/DropDownField.jsx'; 10 | 11 | const ForecastPage = (props) => { 12 | const [isLoaded, setLoaded] = useState(false); 13 | const [skeleton, setSkeleton] = useState(true); 14 | const [fetching, setFetching] = useState(false); 15 | 16 | const [invalidIncrements, setInvalidIncrements] = useState(''); 17 | const [invalidMaxInc, setInvalidMaxInc] = useState(''); 18 | const [incrementsInput, setIncrementsInput] = useState(); 19 | const [maxIncInput, setMaxIncInput] = useState(); 20 | 21 | const [selectedFunc, setSelectedFunc] = useState(); 22 | const [selectedType, setSelectedType] = useState(); 23 | const [selectedRegion, setSelectedRegion] = useState(); 24 | const [selectedGen, setSelectedGen] = useState(); 25 | 26 | const [generationOptions, setGenerationOptions] = useState([]); 27 | 28 | const [dataSeries, setDataSeries] = useState([]); 29 | const [filteredDataSeries, setFilteredDataSeries] = useState({}); 30 | const [filteringOptions, setFilteringOptions] = useState({ 31 | cpuGHzCost: true, 32 | cpuRAMCost: true, 33 | invocationCost: true, 34 | invocations: true, 35 | networkBandwidthCost: true, 36 | totalCost: true 37 | }); 38 | 39 | const [funcList, setfuncList] = useState([]); 40 | const [configurations, setConfigurations] = useState(); 41 | const [projectId, setProjectId] = useState(''); 42 | 43 | const getProjectId = async() => { 44 | try { 45 | const response = await fetch(`/api/getProjectId`, { 46 | method: 'GET', 47 | headers: { 'Content-Type': 'application/json' }, 48 | }); 49 | const data = await response.json(); 50 | console.log(data); 51 | setProjectId(data); 52 | } catch (error) { 53 | console.log('Error in getProjectId: ', error); 54 | } 55 | } 56 | 57 | useEffect(() => { 58 | getProjectId(); 59 | }, []); 60 | 61 | const getFunctionList = async () => { 62 | try { 63 | const response = await fetch(`/api/metrics/funcs/${projectId}?timeRange=43200`, { 64 | method: 'GET', 65 | headers: { 'Content-Type': 'application/json' }, 66 | }); 67 | const data = await response.json(); 68 | 69 | setfuncList(data.funcList); 70 | setConfigurations(data.configurations); 71 | if (data.funcList[0] && props.functionName === '') { 72 | props.setFunctionName(data.funcList[0]); 73 | setSelectedFunc(data.funcList[0]); 74 | updateFields('Function', data.funcList[0], data.configurations); 75 | } else if(props.functionName !== '') { 76 | setSelectedFunc(props.functionName); 77 | updateFields('Function', props.functionName, data.configurations); 78 | } 79 | setSkeleton(false); 80 | } catch (error) { 81 | console.log('Error in getFunctionList: ', error); 82 | } 83 | } 84 | 85 | useEffect(() => { 86 | getFunctionList(); 87 | }, [projectId]); 88 | 89 | const updateFields = (optionType, selectedOption, configs = configurations) => { 90 | switch (optionType) { 91 | case 'Function': 92 | setSelectedType(gcfPricingStructure.typeMapping[configs[selectedOption].funcType]); 93 | setSelectedRegion(configs[selectedOption].funcRegion); 94 | updateFields('Region', configs[selectedOption].funcRegion); 95 | updateFields('GCF-Generation', selectedOption, configs); 96 | break; 97 | case 'Region': 98 | setGenerationOptions(Object.keys(gcfPricingStructure.gcfRegionTiers[selectedOption])); 99 | break; 100 | case 'GCF-Generation': 101 | setSelectedGen(gcfPricingStructure.genMapping[configs[selectedOption].funcGeneration]); 102 | break; 103 | default: 104 | break; 105 | } 106 | } 107 | 108 | const handleOptionChange = (e) => { 109 | switch (e.target.name) { 110 | case 'Function': 111 | console.log('switched Functions'); 112 | props.setFunctionName(e.target.name); 113 | setSelectedFunc(e.target.value); 114 | updateFields('Function', e.target.value); 115 | break; 116 | case 'Type': 117 | console.log('switched Types') 118 | setSelectedType(e.target.value); 119 | break; 120 | case 'Region': 121 | console.log('switched Region') 122 | setSelectedRegion(e.target.value); 123 | updateFields('Region', e.target.value); 124 | break; 125 | case 'GCF-Generation': 126 | console.log('switched Generations') 127 | setSelectedGen(e.target.value); 128 | break; 129 | default: 130 | console.log('Error in handleOptionClick in Forecast Page'); 131 | } 132 | } 133 | 134 | const filterData = (e) => { 135 | filteringOptions[e.target.name] = !filteringOptions[e.target.name]; 136 | const filteredData = dataSeries.map(dataPointObj => { 137 | const point = {}; 138 | for (const costType in dataPointObj) { 139 | if(filteringOptions[costType]) { 140 | point[costType] = dataPointObj[costType]; 141 | } 142 | } 143 | return point; 144 | }) 145 | setFilteredDataSeries(filteredData); 146 | } 147 | 148 | const validateInput = (e) => { 149 | switch (e.target.id) { 150 | case 'incrementsInput': 151 | setIncrementsInput(e.target.value); 152 | (isNaN(e.target.value)) ? setInvalidIncrements('Must be a number') : setInvalidIncrements(''); 153 | break; 154 | case 'maxIncInput': 155 | setMaxIncInput(e.target.value); 156 | (isNaN(e.target.value)) ? setInvalidMaxInc('Must be a number') : setInvalidMaxInc(''); 157 | break; 158 | default: 159 | break; 160 | } 161 | 162 | } 163 | 164 | const forecastSubmit = () => { 165 | setFetching(true); 166 | console.log('forecast button clicked'); 167 | const forecastArgs = { 168 | functionName: document.getElementsByName('Function')[0].value, 169 | type: document.getElementsByName('Type')[0].value, 170 | region: document.getElementsByName('Region')[0].value, 171 | generation: document.getElementsByName('GCF-Generation')[0].value, 172 | increments: Number(document.getElementById('incrementsInput').value), 173 | maxIncrements: Number(document.getElementById('maxIncInput').value), 174 | } 175 | if(invalidIncrements || invalidMaxInc) { 176 | alert('Please fix errors in text fields before submitting'); 177 | setFetching(false); 178 | return; 179 | } 180 | try { 181 | fetch(`api/forecast/${projectId}?timeRange=43200`,{ 182 | method: 'POST', 183 | headers: { 184 | 'Content-Type': 'application/json', 185 | }, 186 | body: JSON.stringify(forecastArgs), 187 | }) 188 | .then(response => { 189 | if(response.ok) { 190 | setFetching(false); 191 | setLoaded(true); 192 | return response.json(); 193 | } else { 194 | setLoaded(false); 195 | } 196 | }) 197 | .then(response => { 198 | console.log('fetched data ==>',response); 199 | setDataSeries(response); 200 | setFilteredDataSeries(response); 201 | setFetching(false); 202 | }); 203 | } catch (err) { 204 | console.log('Error in forecast fetch: ', err); 205 | } 206 | } 207 | 208 | return( 209 |
210 | 211 | 212 | 213 |

Forecast Page

214 | 215 | 216 | 217 | {skeleton ? 218 | : 219 | 225 | } 226 | 227 | 228 | {skeleton ? 229 | : 230 | selectedType && 236 | } 237 | 238 | 239 | {skeleton ? 240 | : 241 | selectedType && 247 | } 248 | 249 | 250 | {skeleton ? 251 | : 252 | selectedType && 258 | } 259 | 260 | 261 | 262 | 263 | { invalidIncrements ? : 272 | } 279 | 280 | 281 | {invalidMaxInc ? 282 | : 291 | 292 | } 293 | 294 | 295 | 296 | 297 | 298 | 299 | This is your forecast 300 | 301 | 302 | 303 | } label="Invocation Costs" /> 304 | } label="CPU RAM Costs" /> 305 | } label="CPU GHz Costs" /> 306 | } label="Network Bandwidth Costs" /> 307 | } label="Total Costs" /> 308 | 309 | 310 | {isLoaded ? ( 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | ) : 323 | fetching ? ( 324 | 332 | 338 | 339 | 340 | ) : 341 | ( 350 | 356 | Data not available 357 | 358 | )} 359 | 360 |
361 |
362 | ); 363 | }; 364 | 365 | export default ForecastPage; -------------------------------------------------------------------------------- /src/client/pages/FunctionsPage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import NavBar from '../components/NavBar.jsx'; 3 | import Box from '@mui/material/Box'; 4 | import DrawerHeader from '../components/DrawerHeader.jsx'; 5 | import Typography from '@mui/material/Typography'; 6 | import FunctionTable from '../components/FunctionTable.jsx'; 7 | 8 | const FunctionsPage = (props) => { 9 | 10 | return( 11 |
12 | 13 | 14 | 15 |

Functions Page

16 | 17 | Table: 18 | 19 | 20 |
21 |
22 | ); 23 | }; 24 | 25 | export default FunctionsPage; -------------------------------------------------------------------------------- /src/client/pages/HomePage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import NavBar from '../components/NavBar.jsx'; 3 | import Box from '@mui/material/Box'; 4 | import DrawerHeader from '../components/DrawerHeader.jsx'; 5 | import Typography from '@mui/material/Typography'; 6 | import RabbitLogo from '../images/rabbit_hop.png'; 7 | 8 | const HomePage = () => { 9 | 10 | return( 11 |
12 | 13 | 14 | 15 | 16 | Welcome to RabbitGCF! 17 | 18 | 19 | Optimizing Google Cloud Functions one hop at a time 20 | 21 | 22 | 23 |
24 | ); 25 | }; 26 | 27 | export default HomePage; 28 | -------------------------------------------------------------------------------- /src/client/pages/LoginPage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const LoginPage = () => { 4 | 5 | return( 6 |
7 | 8 |
9 | ); 10 | }; 11 | 12 | export default LoginPage; -------------------------------------------------------------------------------- /src/client/pages/MetricsPage.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import NavBar from "../components/NavBar.jsx"; 3 | import Box from "@mui/material/Box"; 4 | import DrawerHeader from "../components/DrawerHeader.jsx"; 5 | import Typography from "@mui/material/Typography"; 6 | import GraphComponent from "../components/ZoomGraph.jsx"; 7 | import Skeleton from "@mui/material/Skeleton"; 8 | 9 | import InputLabel from "@mui/material/InputLabel"; 10 | import MenuItem from "@mui/material/MenuItem"; 11 | import FormControl from "@mui/material/FormControl"; 12 | import Select from "@mui/material/Select"; 13 | 14 | const MetricsPage = (props) => { 15 | const [functionList, setFunctionList] = useState(); 16 | const [executionCountData, setExecutionCountData] = useState([]); 17 | const [executionTimeData, setExecutionTimeData] = useState([]); 18 | const [memoryData, setMemoryData] = useState([]); 19 | const [networkData, setNetworkData] = useState([]); 20 | const [selected, setSelected] = useState(false); 21 | 22 | const [skeleton, setSkeleton] = useState(true); 23 | const [timeRange, setTimeRange] = useState(60); 24 | 25 | const selectTimeframe = [ 26 | { label: "1 hour", value: 60}, 27 | { label: "12 hours", value: 720}, 28 | { label: "1 day", value: 1440}, 29 | { label: "2 days", value: 2880}, 30 | { label: "7 days", value: 10080 }, 31 | { label: "14 days", value: 20160 }, 32 | { label: "30 days", value: 43200 } 33 | ]; 34 | 35 | function handleFunctionSelect(e) { 36 | props.setFunctionName(e.target.value); 37 | } 38 | 39 | function handleTimeRangeSelect(e) { 40 | setTimeRange(e.target.value); 41 | setSelected(true); 42 | } 43 | 44 | const [projectId, setProjectId] = useState(''); 45 | 46 | const getProjectId = async() => { 47 | try { 48 | const response = await fetch(`/api/getProjectId`, { 49 | method: 'GET', 50 | headers: { 'Content-Type': 'application/json' }, 51 | }); 52 | const data = await response.json(); 53 | console.log(data); 54 | setProjectId(data); 55 | } catch (error) { 56 | console.log('Error in getProjectId: ', error); 57 | } 58 | } 59 | 60 | useEffect(() => { 61 | getProjectId(); 62 | }, []); 63 | 64 | const getFunctionList = async () => { 65 | try { 66 | const response = await fetch( 67 | `/api/metrics/funcs/${projectId}`, 68 | { 69 | method: "GET", 70 | headers: { "Content-Type": "application/json" }, 71 | } 72 | ); 73 | const data = await response.json(); 74 | setFunctionList(data.funcList); 75 | if (data.funcList[0] && props.functionName === '') props.setFunctionName(data.funcList[0]); 76 | } catch (error) { 77 | console.log("Error in getFunctionList: ", error); 78 | } 79 | } 80 | 81 | useEffect(() => { 82 | getFunctionList(); 83 | }, [projectId]) 84 | 85 | // useEffect(() => { // added this useEffect 86 | // if (selected) { 87 | // fetchMetrics(); 88 | // } 89 | // }, [selected, timeRange]); 90 | 91 | // const fetchMetrics = () => { 92 | // getExecutionCounts(); 93 | // getExecutionTimes(); 94 | // getMemoryBytes(); 95 | // getNetworkEgress(); 96 | // setSelected(false); 97 | // } 98 | 99 | const getExecutionCounts = async () => { 100 | try { 101 | const response = await fetch( 102 | `/api/metrics/execution_count/${projectId}?timeRange=${timeRange}`, 103 | { 104 | method: "GET", 105 | headers: { "Content-Type": "application/json" }, 106 | } 107 | ); 108 | const data = await response.json(); 109 | setExecutionCountData(data); 110 | 111 | } catch (error) { 112 | console.log("Error in getExecutionCounts: ", error); 113 | } 114 | }; 115 | 116 | const getExecutionTimes = async () => { 117 | try { 118 | const response = await fetch( 119 | `/api/metrics/execution_times/${projectId}?timeRange=${timeRange}`, 120 | { 121 | method: "GET", 122 | headers: { "Content-Type": "application/json" }, 123 | } 124 | ); 125 | const data = await response.json(); 126 | setExecutionTimeData(data); 127 | } catch (error) { 128 | console.log("Error in getExecutionTimes: ", error); 129 | } 130 | }; 131 | 132 | const getMemoryBytes = async () => { 133 | try { 134 | const response = await fetch( 135 | `/api/metrics/user_memory_bytes/${projectId}?timeRange=${timeRange}`, 136 | { 137 | method: "GET", 138 | headers: { "Content-Type": "application/json" }, 139 | } 140 | ); 141 | const data = await response.json(); 142 | setMemoryData(data); 143 | } catch (error) { 144 | console.log("Error in getMemoryBytes: ", error); 145 | } 146 | }; 147 | 148 | const getNetworkEgress = async () => { 149 | try { 150 | const response = await fetch( 151 | `/api/metrics/network_egress/${projectId}?timeRange=${timeRange}`, 152 | { 153 | method: "GET", 154 | headers: { "Content-Type": "application/json" }, 155 | } 156 | ); 157 | const data = await response.json(); 158 | setNetworkData(data); 159 | } catch (error) { 160 | console.log("Error in getNetworkEgress: ", error); 161 | } 162 | }; 163 | 164 | useEffect(() => { 165 | getExecutionCounts(); 166 | getExecutionTimes(); 167 | getMemoryBytes(); 168 | getNetworkEgress(); 169 | setSkeleton(false); 170 | setSelected(false); 171 | }, [timeRange]); 172 | 173 | return ( 174 |
175 | 176 | 177 | 178 |

Metrics Page

179 |
180 | 183 | 184 | Function 185 | 186 | 206 | 207 | 208 | Timerange 209 | 224 | 225 |
226 | 227 | These are your metrics: 228 | 229 |
230 |
231 | 232 | Execution Count: 233 | 234 | {skeleton ? ( 235 | 242 | ) : ( 243 | 252 | {executionCountData[props.functionName] ? ( 253 | 260 | ) : ( 261 | 271 | 278 | Data not available 279 | 280 | 281 | )} 282 | 283 | )} 284 |
285 |
286 | 287 | Execution Time: 288 | 289 | {skeleton ? ( 290 | 297 | ) : ( 298 | 307 | {executionTimeData[props.functionName] ? ( 308 | 316 | ) : ( 317 | 327 | 334 | Data not available 335 | 336 | 337 | )} 338 | 339 | )} 340 |
341 |
342 | 343 | Memory: 344 | 345 | {skeleton ? ( 346 | 353 | ) : ( 354 | 363 | {memoryData[props.functionName] ? ( 364 | 372 | ) : ( 373 | 383 | 390 | Data not available 391 | 392 | 393 | )} 394 | 395 | )} 396 |
397 |
398 | 399 | Network Egress: 400 | 401 | {skeleton ? ( 402 | 409 | ) : ( 410 | 419 | {networkData[props.functionName] ? ( 420 | 428 | ) : ( 429 | 439 | 446 | Data not available 447 | 448 | 449 | )} 450 | 451 | )} 452 |
453 |
454 |
455 |
456 | ); 457 | }; 458 | 459 | export default MetricsPage; 460 | -------------------------------------------------------------------------------- /src/client/pages/ProfilePage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import NavBar from '../components/NavBar.jsx'; 3 | import Box from '@mui/material/Box'; 4 | import DrawerHeader from '../components/DrawerHeader.jsx'; 5 | import Typography from '@mui/material/Typography'; 6 | 7 | const ProfilePage = () => { 8 | 9 | return( 10 |
11 | 12 | 13 | 14 |

Profile Page

15 | 16 | Coming soon ... 17 | 18 |
19 |
20 | ); 21 | }; 22 | 23 | export default ProfilePage; -------------------------------------------------------------------------------- /src/client/pages/ProjectSetupPage.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import { useNavigate, useLocation } from "react-router-dom"; 3 | import NavBar from '../components/NavBar.jsx'; 4 | import DrawerHeader from "../components/DrawerHeader.jsx"; 5 | import { Box, Typography, Button, FormControl, TextField } from '@mui/material/'; 6 | import { saveProject } from "../slicers/projectsSlice.js"; 7 | import { useDispatch, useSelector } from "react-redux"; 8 | 9 | const ProjectSetupPage = () => { 10 | const navigate = useNavigate(); 11 | const dispatch = useDispatch(); 12 | const location = useLocation(); 13 | 14 | const user = useSelector( state => state.user.profile ); 15 | console.log(user); 16 | 17 | const [projectName, setProjectName] = useState(''); 18 | const [projectId, setProjectId] = useState(''); 19 | const [serviceAccKey, setServiceAccKey] = useState(); 20 | 21 | useEffect(() => { 22 | if (location.state) { 23 | setProjectName(location.state.project.projectName); 24 | setProjectId(location.state.project.projectId); 25 | setServiceAccKey(location.state.project.serviceAccKey); 26 | } else { 27 | setProjectName(`e.g., My Project ${99999 - Math.floor(Math.random()*99999)}`); 28 | setProjectId(`e.g., refined-engine-${99999 - Math.floor(Math.random()*99999)}-e8`); 29 | setServiceAccKey("Paste JSON object key here"); 30 | } 31 | }, []) 32 | 33 | const back = (e) => { 34 | console.log('add project clicked'); 35 | navigate("/projects"); 36 | } 37 | 38 | /** 39 | * Will need to refactor save to save project to database instead of state. 40 | * Refactoring should also encrypt serviceAccKey prior to saving to database 41 | */ 42 | const save = (e) => { 43 | const index = (location.state) ? location.state.projectListIndex : null; 44 | 45 | (() => { 46 | dispatch(saveProject( 47 | { 48 | project: { 49 | projectId, 50 | projectName, 51 | serviceAccKey 52 | }, 53 | index, 54 | }) 55 | ) 56 | })(); 57 | 58 | fetch('/api/project/add', { 59 | method: "POST", 60 | headers: { "Content-Type": "application/json" }, 61 | body: JSON.stringify( 62 | { 63 | profile_id: user.id, 64 | project_id: projectId, 65 | project_name: projectName, 66 | key: serviceAccKey 67 | } 68 | ) 69 | }) 70 | 71 | navigate("/projects"); 72 | } 73 | 74 | return ( 75 |
76 | 77 | 78 | 79 |

Setup Project

80 | 81 | { projectId && projectName && serviceAccKey && 82 | 83 | setProjectName(e.target.value)} 90 | /> 91 | setProjectId(e.target.value)} 98 | /> 99 | setServiceAccKey(e.target.value)} 108 | /> 109 | } 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 |
120 |
121 | ); 122 | } 123 | 124 | export default ProjectSetupPage; -------------------------------------------------------------------------------- /src/client/pages/ProjectsPage.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import NavBar from '../components/NavBar.jsx'; 3 | import DrawerHeader from '../components/DrawerHeader.jsx'; 4 | import ProjectsTable from '../components/ProjectsTable.jsx'; 5 | import { Box, Typography, Button } from '@mui/material/'; 6 | import { useNavigate } from "react-router-dom"; 7 | 8 | const ProjectsPage = () => { 9 | const navigate = useNavigate(); 10 | 11 | const addProject = (e) => { 12 | console.log('add project clicked'); 13 | navigate("/projects/setup"); 14 | } 15 | 16 | return ( 17 |
18 | 19 | 20 | 21 |

Projects Page

22 | 23 | 24 |
25 |
26 | ); 27 | } 28 | 29 | export default ProjectsPage; -------------------------------------------------------------------------------- /src/client/slicers/projectsSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit' 2 | 3 | export const projectsSlice = createSlice({ 4 | name: 'projects', 5 | initialState: { 6 | projectList: [], 7 | setProjectId: '', 8 | }, 9 | reducers: { 10 | saveProject: (state, action) => { 11 | console.log(action.payload); 12 | const { project, index } = action.payload; 13 | if(index) state.projectList[index] = project; 14 | else state.projectList.push(project); 15 | }, 16 | deleteProject: (state, action) => { 17 | state.projectList.splice(action.payload, 1); 18 | }, 19 | }, 20 | }) 21 | 22 | // Action creators are generated for each case reducer function 23 | export const { saveProject, deleteProject, focusProject } = projectsSlice.actions 24 | 25 | export default projectsSlice.reducer -------------------------------------------------------------------------------- /src/client/slicers/userSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit' 2 | 3 | export const userSlice = createSlice({ 4 | name: 'user', 5 | initialState: { 6 | isLoggedIn: false, 7 | authCredentials: null, 8 | profile: null, 9 | }, 10 | reducers: { 11 | login: (state, action) => { 12 | state.isLoggedIn = true; 13 | state.authCredentials = action.payload; 14 | }, 15 | setProfile: (state, action) => { 16 | state.profile = action.payload; 17 | }, 18 | logout: (state, action) => { 19 | state.isLoggedIn = false; 20 | state.authCredentials = null; 21 | state.profile = null; 22 | } 23 | }, 24 | }) 25 | 26 | // Action creators are generated for each case reducer function 27 | export const { login, setProfile, logout } = userSlice.actions 28 | 29 | export default userSlice.reducer -------------------------------------------------------------------------------- /src/client/store.js: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import projectsSlice from './slicers/projectsSlice.js'; 3 | import userSlice from './slicers/userSlice.js'; 4 | 5 | export default configureStore({ 6 | reducer: { 7 | projects: projectsSlice, 8 | user: userSlice, 9 | }, 10 | }) -------------------------------------------------------------------------------- /src/db/db.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | const postgres = require('postgres'); 3 | 4 | const connectionString = process.env.DATABASE_URL; 5 | const sql = postgres(connectionString); 6 | 7 | module.exports = sql; -------------------------------------------------------------------------------- /src/server/controllers/authController.js: -------------------------------------------------------------------------------- 1 | const bcrypt = require('bcrypt'); 2 | const saltRounds = 10; 3 | 4 | const authController = {}; 5 | 6 | 7 | module.exports = authController; -------------------------------------------------------------------------------- /src/server/controllers/bigQuery.js: -------------------------------------------------------------------------------- 1 | const { BigQuery } = require('@google-cloud/bigquery'); 2 | const { Logging } = require('@google-cloud/logging'); 3 | 4 | // this project id for testing purposes 5 | // const projectId = 'refined-engine-424416-p7'; 6 | const bigquery = new BigQuery(); 7 | 8 | // const bigQuery = { 9 | // getDatasets: async (req, res, next) => { 10 | // console.log(`This is req params: ${req.params.projectId}`); 11 | // const { projectId } = req.params; // use later 12 | 13 | // try { 14 | // const [datasets] = await bigquery.getDatasets(projectId); 15 | // console.log(`List of datasets: ${datasets}`); 16 | 17 | // const datasetList = []; 18 | 19 | // if (datasets) { 20 | // datasets.forEach(el => datasetList.push(datasets.id)); 21 | // console.log(`Dataset names only: ${datasetList}`); 22 | // res.locals.datasetList = datasetList; 23 | // return next(); 24 | // } else { 25 | // return next('Could not get dataset list.'); 26 | // } 27 | 28 | // // const mappedList = datasetList.map(async el => { 29 | // // await bigquery.dataset(el).get(); 30 | // // }); 31 | // // console.log(`Full datasets: ${mappedList}`); 32 | 33 | // // if (mappedList) { 34 | // // res.locals.datasets = mappedList; 35 | // // return next(); 36 | // // } else { 37 | // // return next('Could not get mapped datasets.'); 38 | // // } 39 | // } catch (err) { 40 | // return next({ err: `Something went wrong. ${err}` }); 41 | // } 42 | // }, 43 | 44 | // getContents: async (req, res, next) => { 45 | // console.log(`This is res.locals.datasetList: ${res.locals.datasetList}`); 46 | // const mappedList = res.locals.datasetList; 47 | 48 | // try { 49 | // mappedList.map(async el => { 50 | // await bigquery.dataset(el).get(); 51 | // }); 52 | // console.log(`Mapped datasets: ${mappedList}`); 53 | 54 | // if (mappedList) { 55 | // res.locals.datasetContents = mappedList; 56 | // return next(); 57 | // } else { 58 | // return next('Could not get dataset contents.'); 59 | // } 60 | // } catch (err) { 61 | // return next({ err: `Something went wrong. ${err}` }); 62 | // } 63 | // } 64 | // }; 65 | 66 | const bigQuery = { 67 | getDatasets: async (req, res, next) => { 68 | const { projectId } = req.params; 69 | console.log(`This is projectId: ${projectId}`); 70 | const url = `https://bigquery.googleapis.com/bigquery/v2/projects/${projectId}/datasets`; 71 | 72 | try { 73 | const datasetList = await fetch(url); 74 | console.log(`Fetched dataset list: ${datasetList}`); 75 | // console.log(`First dataset in fetched list: ${datasetList.datasets[0].id}`); 76 | 77 | if (datasetList) { 78 | res.locals.datasetList = datasetList; 79 | return next(); 80 | } else { 81 | return next('Could not get dataset list.'); 82 | } 83 | } catch (err) { 84 | return next({ err: `Something went wrong. ${err}` }); 85 | } 86 | } 87 | } 88 | 89 | async function quickstart( 90 | projectId = 'YOUR_PROJECT_ID', // Your Google Cloud Platform project ID 91 | logName = 'my-log' // The name of the log to write to 92 | ) { 93 | // Creates a client 94 | const logging = new Logging({projectId}); 95 | 96 | // Selects the log to write to 97 | const log = logging.log(logName); 98 | 99 | // The data to write to the log 100 | const text = 'Hello, world!'; 101 | 102 | // The metadata associated with the entry 103 | const metadata = { 104 | resource: {type: 'global'}, 105 | // See: https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#logseverity 106 | severity: 'INFO', 107 | }; 108 | 109 | // Prepares a log entry 110 | const entry = log.entry(metadata, text); 111 | 112 | async function writeLog() { 113 | // Writes the log entry 114 | await log.write(entry); 115 | console.log(`Logged: ${text}`); 116 | } 117 | writeLog(); 118 | } 119 | 120 | module.exports = bigQuery; -------------------------------------------------------------------------------- /src/server/controllers/cookieController.js: -------------------------------------------------------------------------------- 1 | const cookieController = {}; 2 | 3 | // To handle setting cookies for OAUTH users 4 | cookieController.setSession = (req, res, next) => { 5 | res.cookie('SSID', req.user._id); 6 | } 7 | 8 | cookieController.clearCookies = (req, res, next) => { 9 | res.clearCookie('SSID'); 10 | res.clearCookie('connect.sid'); 11 | return next(); 12 | } 13 | 14 | module.exports = cookieController; -------------------------------------------------------------------------------- /src/server/controllers/forecastController.js: -------------------------------------------------------------------------------- 1 | // Require dependencies 2 | const gcfPricingStructure = require('../../../gcfPricingStructure'); 3 | 4 | const forecastController = { 5 | /** 6 | * Function retrieves historical data and calulates average historical run time 7 | * and network bandwidth memory utilization 8 | */ 9 | calcHistorical (req, res, next){ 10 | const { functionName } = req.body; 11 | 12 | console.log('calcHistorical middleware invoked ========='); 13 | 14 | // fetch function metrics for the last 30 invocations to get average runtime and memory usage 15 | try { 16 | const hstExecutionTimes = res.locals.execution_times[functionName]; 17 | 18 | if(hstExecutionTimes === undefined) { 19 | console.log('test'); 20 | return next({ 21 | log: 'Error in forecast middleware - no historical invocation data found', 22 | status: 408, 23 | message: { err: 'REQUEST TIMEOUT' }, 24 | }); 25 | } 26 | 27 | let hstExecutionTimeData = (hstExecutionTimes.length > 30) ? hstExecutionTimes.slice(0, 30) : hstExecutionTimes; 28 | 29 | const totalExecTimeMS = hstExecutionTimeData.reduce((acc, dataPoint) => { return acc + dataPoint.value }, 0); 30 | const avgExecTimeMS = totalExecTimeMS / hstExecutionTimeData.length; 31 | res.locals.avgExecTimeMS = avgExecTimeMS / 1000; 32 | 33 | // Calc avg historical memory usage 34 | const hstMemory = res.locals.user_memory_bytes[functionName]; 35 | 36 | let hstMemoryData = (hstMemory.length > 30) ? hstMemory.slice(0, 30) : hstMemory; 37 | 38 | 39 | const totalMemoryKB = hstMemoryData.reduce((acc, dataPoint) => { return acc + dataPoint.value }, 0); 40 | const avgMemoryKB = totalMemoryKB / hstMemoryData.length; 41 | res.locals.avgMemoryKB = avgMemoryKB; 42 | 43 | console.log(`average memory in KB: ${avgMemoryKB} | average execution time in ms: ${avgExecTimeMS}`); 44 | 45 | return next(); 46 | } catch (err) { 47 | return next({ 48 | log: 'Error in calcHistorical middleware - error retrieving historical metrics', 49 | status: 404, 50 | message: { err }, 51 | }); 52 | } 53 | }, 54 | 55 | forecast (req, res, next){ 56 | console.log('forecast controller invoked'); 57 | const { region, generation, type, increments, maxIncrements } = req.body; 58 | 59 | // Retrieve gfc configurations 60 | const tier = gcfPricingStructure.gcfRegionTiers[region][generation]; 61 | const memoryConfig = gcfPricingStructure.gcfTypes[type].mb; 62 | const cpuMHzConfig = gcfPricingStructure.gcfTypes[type].mhz; 63 | 64 | // Create output array 65 | const forecastDataSeries = []; 66 | 67 | const calcInvocationCosts = (increments, maxIncrement) => { 68 | const freeInvocations = 2000000; // number of monthly free invocations allowed by Google 69 | const costPerInvocation = 0.40 / 1000000; // Google's cost per million invocation 70 | 71 | for (let i = 0; i <= maxIncrement; i++) { 72 | const invocations = increments * i; // calc invocations based on arguments 73 | const netInvocations = Math.max(0, invocations - freeInvocations); // less than free invocations provided 74 | const invocationCost = Number((netInvocations * costPerInvocation).toFixed(2)); // calc invocations * unit price 75 | 76 | forecastDataSeries.push({ 77 | invocationCost, 78 | totalCost: invocationCost, 79 | invocations, 80 | }); 81 | }; 82 | } 83 | 84 | const calcComputeRAMCost = (gcfMemoryConfigMb, gcfHistoricalRunTime) => { 85 | const freeGbRAM = 400000; // free monthly RAM allowed by Google 86 | const unitPriceRAM = gcfPricingStructure.gcfComputePricing[tier].memoryGbPrice; // price based on tier 87 | 88 | const gcfGbMemoryConfigGb = gcfMemoryConfigMb / 1024; // conversion from MB to GB 89 | const cpuGbSecond = gcfGbMemoryConfigGb * gcfHistoricalRunTime; // calc GB-seconds based on gcf type 90 | 91 | for (let i = 0; i < forecastDataSeries.length; i++) { 92 | const totalGbRAM = forecastDataSeries[i].invocations * cpuGbSecond; // calc total GB-ram usage based on invocations 93 | const netRAMUsageGb = Math.max(0, totalGbRAM - freeGbRAM); // less any free RAM provided 94 | const cpuRAMCost = Number((netRAMUsageGb * unitPriceRAM).toFixed(2)); // calc ram used * unit price 95 | 96 | forecastDataSeries[i].cpuRAMCost = cpuRAMCost; 97 | forecastDataSeries[i].totalCost += cpuRAMCost; 98 | } 99 | } 100 | 101 | const calcComputeGHzCost = (gcfMHzConfig, gcfHistoricalRunTime) => { 102 | const freeGHz = 200000; // free monthly GHz allowed by Google 103 | const unitPriceGHz = gcfPricingStructure.gcfComputePricing[tier].cpuGHzPrice; // price based on tier 104 | // console.log(gcfGHzConfig, gcfHistoricalRunTime) 105 | const gcfGHzConfig = gcfMHzConfig / 1000; // conversion from MHz to GHz 106 | const cpuGHzSecond = gcfGHzConfig * gcfHistoricalRunTime; // calc GHz-second based on config 107 | 108 | for (let i = 0; i < forecastDataSeries.length; i++) { 109 | const totalGHz = forecastDataSeries[i].invocations * cpuGHzSecond; // calc total GHz cost based on invocations 110 | const netGHzUsage = Math.max(0, totalGHz - freeGHz); // less free GHz provided 111 | const cpuGHzCost = Number((netGHzUsage * unitPriceGHz).toFixed(2)); // calc GHz used * unit price 112 | 113 | forecastDataSeries[i].cpuGHzCost = cpuGHzCost; 114 | forecastDataSeries[i].totalCost += cpuGHzCost; 115 | } 116 | } 117 | 118 | const calcNetworkBandwithCost = (gcfHistoricalBandwidthKb) => { 119 | const freeBandwidthGb = 5; // free monthly bandwidth GB allowed by Google 120 | const unitPriceBandwidthGb = 0.12; // fixed price per Google 121 | 122 | const gcfBandwidthGb = gcfHistoricalBandwidthKb / 1048576; // conversion from KB to GB per invocation 123 | 124 | for (let i = 0; i < forecastDataSeries.length; i++) { 125 | const totalNetworkBandwidthGb = forecastDataSeries[i].invocations * gcfBandwidthGb; // calc total network bandwidth cost 126 | const netNetworkBandwithGb = Math.max(0, totalNetworkBandwidthGb - freeBandwidthGb); // less any free network bandwidth provided 127 | const networkBandwidthCost = Number((netNetworkBandwithGb * unitPriceBandwidthGb).toFixed(2)); // calc networkbandwidth used * unit price 128 | 129 | forecastDataSeries[i].networkBandwidthCost = networkBandwidthCost; 130 | forecastDataSeries[i].totalCost += networkBandwidthCost; 131 | forecastDataSeries[i].totalCost = Number(forecastDataSeries[i].totalCost.toFixed(2)); 132 | } 133 | } 134 | 135 | calcInvocationCosts(increments, maxIncrements); 136 | calcComputeRAMCost(memoryConfig, res.locals.avgExecTimeMS); 137 | calcComputeGHzCost(cpuMHzConfig, res.locals.avgExecTimeMS); 138 | calcNetworkBandwithCost(res.locals.avgMemoryKB); 139 | 140 | res.locals.forecastDataSeries = forecastDataSeries; 141 | return next(); 142 | } 143 | }; 144 | 145 | module.exports = forecastController; -------------------------------------------------------------------------------- /src/server/controllers/graphController.js: -------------------------------------------------------------------------------- 1 | const graphController = {}; 2 | 3 | 4 | module.exports = graphController; -------------------------------------------------------------------------------- /src/server/controllers/metrics.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | require('dotenv').config(); 3 | const monitoring = require('@google-cloud/monitoring'); 4 | const { FunctionServiceClient } = require('@google-cloud/functions').v2; 5 | 6 | const keyFilename = path.join(__dirname,`../../../util/${process.env.PROJECT_KEY}`); 7 | 8 | // create needed clients 9 | const funcsClient = new FunctionServiceClient({ keyFilename }); 10 | const monClient = new monitoring.MetricServiceClient({ keyFilename }); 11 | // const funcsClient = new FunctionServiceClient(); 12 | // const monClient = new monitoring.MetricServiceClient(); 13 | 14 | // const metrics = [ 15 | // 'metric.type="cloudfunctions.googleapis.com/function/execution_count"', 16 | // 'metric.type="cloudfunctions.googleapis.com/function/execution_times"', 17 | // 'metric.type="cloudfunctions.googleapis.com/function/instance_count"', 18 | // 'metric.type="cloudfunctions.googleapis.com/function/user_memory_bytes"' 19 | // ]; 20 | 21 | const metricsController = { 22 | getFuncs: async (req, res, next) => { 23 | const { projectId } = req.params; 24 | const parent = `projects/${projectId}/locations/-`; 25 | const request = { parent }; 26 | 27 | try { 28 | const response = { 29 | funcList: [], 30 | configurations: {}, 31 | }; 32 | const iterable = funcsClient.listFunctionsAsync(request); 33 | for await (const func of iterable) { 34 | const testRegex = /.*?locations\/(.*)\/.*?functions\/(.*)/; 35 | const testTrim = func.name.match(testRegex); 36 | const funcName = testTrim[2]; 37 | response.funcList.push(funcName); 38 | response.configurations[funcName] = { 39 | funcRegion: testTrim[1], 40 | funcType: func.serviceConfig.availableMemory, 41 | funcGeneration: func.environment, 42 | } 43 | } 44 | 45 | res.locals.funcConfigs = response; 46 | 47 | return next(); 48 | } catch (err) { 49 | return next(`Could not get functions list. ERROR: ${err}`); 50 | } 51 | }, 52 | 53 | executionCount: async (req, res, next) => { 54 | const { projectId } = req.params; 55 | const { timeRange } = req.query; 56 | 57 | const execution_count = `metric.type="cloudfunctions.googleapis.com/function/execution_count" AND resource.labels.project_id="${projectId}"`; 58 | const request = { 59 | name: monClient.projectPath(projectId), 60 | filter: execution_count, 61 | interval: { 62 | startTime: { 63 | seconds: Date.now() / 1000 - 60 * timeRange 64 | }, 65 | endTime: { 66 | seconds: Date.now() / 1000 67 | } 68 | } 69 | }; 70 | 71 | try { 72 | const [ timeSeries ] = await monClient.listTimeSeries(request); 73 | const parsedTimeSeries = {}; 74 | timeSeries.forEach(obj => { 75 | if (obj.metric.labels.status === 'ok') { 76 | const newPoints = []; 77 | 78 | obj.points.forEach(point => { 79 | const time = new Date(point.interval.startTime.seconds * 1000).toLocaleString(); 80 | newPoints.push({ 81 | timestamp: time, 82 | value: Number(point.value.int64Value) 83 | }); 84 | }); 85 | 86 | parsedTimeSeries[obj.resource.labels.function_name] = newPoints.sort((a, b) => a.timestamp - b.timestamp); 87 | }; 88 | }); 89 | res.locals.execution_count = parsedTimeSeries; 90 | 91 | return next(); 92 | } catch (err) { 93 | return next(`Could not get execution count data. ERROR: ${err}`); 94 | } 95 | }, 96 | 97 | executionTimes: async (req, res, next) => { 98 | const { projectId } = req.params; 99 | const { timeRange } = req.query; 100 | 101 | const execution_times = `metric.type="cloudfunctions.googleapis.com/function/execution_times" AND resource.labels.project_id="${projectId}"`; 102 | const request = { 103 | name: monClient.projectPath(projectId), 104 | filter: execution_times, 105 | interval: { 106 | startTime: { 107 | seconds: Date.now() / 1000 - 60 * timeRange 108 | }, 109 | endTime: { 110 | seconds: Date.now() / 1000 111 | } 112 | } 113 | }; 114 | 115 | try { 116 | const [ timeSeries ] = await monClient.listTimeSeries(request); 117 | const parsedTimeSeries = {}; 118 | timeSeries.forEach(obj => { 119 | const newPoints = []; 120 | 121 | obj.points.forEach(point => { 122 | const time = new Date(point.interval.startTime.seconds * 1000).toLocaleString(); 123 | newPoints.push({ 124 | timestamp: time, 125 | value: Math.round(point.value.distributionValue.mean / 1000000) 126 | }); 127 | }); 128 | 129 | parsedTimeSeries[obj.resource.labels.function_name] = newPoints.sort((a, b) => a.timestamp - b.timestamp); 130 | 131 | // return newSeries; 132 | }); 133 | 134 | 135 | res.locals.execution_times = parsedTimeSeries; 136 | 137 | return next(); 138 | } catch (err) { 139 | return next(`Could not get execution times data. ERROR: ${err}`); 140 | } 141 | }, 142 | 143 | userMemoryBytes: async (req, res, next) => { 144 | const { projectId } = req.params; 145 | const { timeRange } = req.query; 146 | 147 | const user_memory_bytes = `metric.type="cloudfunctions.googleapis.com/function/user_memory_bytes" AND resource.labels.project_id="${projectId}"`; 148 | const request = { 149 | name: monClient.projectPath(projectId), 150 | filter: user_memory_bytes, 151 | interval: { 152 | startTime: { 153 | seconds: Date.now() / 1000 - 60 * timeRange 154 | }, 155 | endTime: { 156 | seconds: Date.now() / 1000 157 | } 158 | } 159 | }; 160 | 161 | const normalizeTimestamp = (timestamp) => { 162 | const date = new Date(timestamp); 163 | date.setSeconds(0, 0); 164 | return date.toISOString(); 165 | }; 166 | 167 | try { 168 | const [ timeSeries ] = await monClient.listTimeSeries(request); 169 | const parsedTimeSeries = {}; 170 | timeSeries.forEach(obj => { 171 | const newPoints = []; 172 | 173 | obj.points.forEach(point => { 174 | const time = new Date(point.interval.startTime.seconds * 1000).toLocaleString(); 175 | newPoints.push({ 176 | timestamp: normalizeTimestamp(time), 177 | value: Math.round(point.value.distributionValue.mean / 1048576 * 100) / 100 178 | }); 179 | }); 180 | 181 | parsedTimeSeries[obj.resource.labels.function_name] = newPoints.sort((a, b) => a.timestamp - b.timestamp); 182 | 183 | // return newSeries; 184 | }); 185 | 186 | res.locals.user_memory_bytes = parsedTimeSeries; 187 | 188 | return next(); 189 | } catch (err) { 190 | return next(`Could not get user memory bytes data. ERROR: ${err}`); 191 | } 192 | }, 193 | 194 | networkEgress: async (req, res, next) => { 195 | const { projectId } = req.params; 196 | const { timeRange } = req.query; 197 | 198 | const network_egress = `metric.type="cloudfunctions.googleapis.com/function/network_egress" AND resource.labels.project_id="${projectId}"`; 199 | const request = { 200 | name: monClient.projectPath(projectId), 201 | filter: network_egress, 202 | interval: { 203 | startTime: { 204 | seconds: Date.now() / 1000 - 60 * timeRange 205 | }, 206 | endTime: { 207 | seconds: Date.now() / 1000 208 | } 209 | } 210 | }; 211 | 212 | try { 213 | const [ timeSeries ] = await monClient.listTimeSeries(request); 214 | const parsedTimeSeries = {}; 215 | timeSeries.forEach(obj => { 216 | const newPoints = []; 217 | 218 | obj.points.forEach(point => { 219 | const time = new Date(point.interval.startTime.seconds * 1000).toLocaleString(); 220 | newPoints.push({ 221 | timestamp: time, 222 | value: Math.round(point.value.int64Value / 1048576 * 100) / 100 223 | }); 224 | }); 225 | 226 | // newSeries.name = obj.resource.labels.function_name; 227 | parsedTimeSeries[obj.resource.labels.function_name] = newPoints.sort((a, b) => a.timestamp - b.timestamp);; 228 | 229 | // return newSeries; 230 | }); 231 | res.locals.network_egress = parsedTimeSeries; 232 | 233 | return next(); 234 | } catch (err) { 235 | return next(`Could not get network egress data. ERROR: ${err}`); 236 | } 237 | } 238 | }; 239 | 240 | 241 | module.exports = metricsController; -------------------------------------------------------------------------------- /src/server/controllers/projectController.js: -------------------------------------------------------------------------------- 1 | const sql = require('../../db/db'); 2 | 3 | const projectController = {}; 4 | 5 | projectController.addProject = async (req, res, next) => { 6 | try { 7 | console.log('addProject middleware invoked') 8 | const { project_name, project_id, profile_id, key } = req.body; 9 | 10 | if(res.locals.recordExists) return next(); 11 | else { 12 | const response = await sql`INSERT INTO projects (project_name, project_id, profile_id, key) VALUES (${project_name}, ${project_id}, ${profile_id}, ${key}) RETURNING id, project_name`; 13 | 14 | res.locals.project = response; 15 | console.log('addProject sql response ==>', response); 16 | return next(); 17 | } 18 | } catch (error) { 19 | return next({ 20 | log: `Error in addProject middleware: ${error}`, 21 | status: 500, 22 | message: 'An error occured adding a project', 23 | }); 24 | } 25 | } 26 | 27 | projectController.getAllProjects = async (req, res, next) => { 28 | try { 29 | const { profile_id } = req.body; 30 | 31 | const response = await sql`SELECT * FROM projects WHERE profile_id=${profile_id}`; 32 | res.locals.projects = response; 33 | 34 | return next(); 35 | } catch (error) { 36 | return next({ 37 | log: `Error in getAllProjects middleware: ${error}`, 38 | status: 500, 39 | message: 'An error occured getting all projects', 40 | }); 41 | } 42 | } 43 | 44 | projectController.getProject = async (req, res, next) => { 45 | try { 46 | console.log('getProject middleware invoked') 47 | const { profile_id, project_id } = req.body; 48 | 49 | const response = await sql`SELECT * FROM projects WHERE profile_id=${profile_id} AND project_id=${project_id}`; 50 | res.locals.project = response; 51 | 52 | if(response.length === 0) { 53 | console.log('project does not exist in database') 54 | res.locals.recordExists = false; 55 | return next(); 56 | } else { 57 | res.locals.recordExists = true; 58 | return next(); 59 | } 60 | 61 | return next(); 62 | } catch (error) { 63 | return next({ 64 | log: `Error in getProject middleware: ${error}`, 65 | status: 500, 66 | message: 'An error occured getting a project', 67 | }); 68 | } 69 | } 70 | 71 | module.exports = projectController; 72 | -------------------------------------------------------------------------------- /src/server/controllers/userController.js: -------------------------------------------------------------------------------- 1 | const sql = require('../../db/db'); 2 | 3 | const userController = {}; 4 | 5 | userController.createUser = async (req, res, next) => { 6 | try { 7 | if(res.locals.newUser) { 8 | console.log(req.body); 9 | const { name, email, profile_id } = req.body; 10 | 11 | const response = await sql`INSERT INTO users (name, email, profile_id) VALUES (${name}, ${email}, ${profile_id}) RETURNING *`; 12 | res.locals.user = response; 13 | console.log('createUser Response ==>',response); 14 | } 15 | return next(); 16 | } catch (err) { 17 | next({ 18 | log: `Error in createUser middleware: ${err}`, 19 | status: 500, 20 | message: 'An error occured creating a user', 21 | }); 22 | } 23 | } 24 | 25 | userController.getUser = async (req, res, next) => { 26 | try { 27 | const { profile_id } = req.body; 28 | 29 | const response = await sql`SELECT * FROM users WHERE profile_id=${profile_id}`; 30 | 31 | if(response.length !== 0) { 32 | res.locals.newUser = false; 33 | } else { 34 | res.locals.newUser = true; 35 | } 36 | 37 | return next(); 38 | } catch (err) { 39 | next({ 40 | log: `Error in getUser middleware: ${err}`, 41 | status: 500, 42 | message: 'An error occured getting a user', 43 | }); 44 | } 45 | } 46 | 47 | module.exports = userController; -------------------------------------------------------------------------------- /src/server/gcfFunction_testing.js: -------------------------------------------------------------------------------- 1 | const rabbitFunctions = ['addCharacter', 'getSpecies', 'deleteCharacter', 'getHomeworld', 'getFilm', 'getCharacters', 'updateCharacters']; 2 | 3 | function invoke () { 4 | console.log('=========== Invocations STARTED ===========') 5 | const invocations = Math.floor(Math.random() * 30) + 20; 6 | console.log(`Total invocations to fire: ${invocations}`); 7 | let count = 0; 8 | const repeat = setInterval(() => { 9 | if(count === invocations) { 10 | clearInterval(repeat); 11 | console.log('=========== Invocations COMPLETED ==========='); 12 | } 13 | for (const gcfFunc of rabbitFunctions){ 14 | // fetch('https://us-central1-refined-engine-424416-p7.cloudfunctions.net/getCharacters'); 15 | fetch(`https://us-central1-refined-engine-424416-p7.cloudfunctions.net/${gcfFunc}`) 16 | // .then(response => console.log(gcfFunc, response.ok)); 17 | } 18 | 19 | count++; 20 | }, 1000) 21 | 22 | return; 23 | } 24 | 25 | invoke(); 26 | 27 | // process.stdout.write("\r"); 28 | // console.log('test'); 29 | // process.stdout.write("\n"); -------------------------------------------------------------------------------- /src/server/routers/forecastRouter.js: -------------------------------------------------------------------------------- 1 | // Package dependencies 2 | const express = require('express'); 3 | 4 | // Controllers 5 | const forecastController = require('../controllers/forecastController.js'); 6 | const metricsController = require('../controllers/metrics.js'); 7 | 8 | const router = express.Router(); 9 | 10 | router.post('/:projectId', 11 | /*retrieve metrics from metric middleware here*/ 12 | metricsController.executionTimes, 13 | metricsController.userMemoryBytes, 14 | forecastController.calcHistorical, 15 | forecastController.forecast, 16 | (req, res) => { 17 | console.log('forecast calculated'); 18 | res.status(200).json(res.locals.forecastDataSeries); 19 | }); 20 | 21 | 22 | 23 | module.exports = router; -------------------------------------------------------------------------------- /src/server/routers/projectRouter.js: -------------------------------------------------------------------------------- 1 | // Package dependencies 2 | const express = require('express'); 3 | const router = express.Router(); 4 | const { addProject, getAllProjects, getProject } = require('./../controllers/projectController.js') 5 | 6 | router.post('/add', (req, res, next) => { 7 | console.log('invoked add project route'); 8 | console.log(req.body); 9 | return next(); 10 | }, 11 | getProject, addProject, 12 | (req, res) => { 13 | console.log('finished adding project'); 14 | return res.status(200); 15 | }); 16 | 17 | module.exports = router; -------------------------------------------------------------------------------- /src/server/routers/userRouter.js: -------------------------------------------------------------------------------- 1 | // Package dependencies 2 | const express = require('express'); 3 | const router = express.Router(); 4 | const { createUser, getUser } = require('./../controllers/userController.js') 5 | 6 | router.post('/login', getUser, createUser, (req, res) => { 7 | res.status(200).json('successful login'); 8 | }); 9 | 10 | module.exports = router; -------------------------------------------------------------------------------- /src/server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const passport = require('passport'); 3 | const session = require('express-session'); 4 | const path = require('path'); 5 | const dotenv = require('dotenv'); 6 | 7 | dotenv.config({ path: './.env' }); 8 | const PORT = 3000; 9 | const app = express(); 10 | 11 | const metricsController = require('./controllers/metrics'); 12 | const graphController = require('./controllers/graphController'); 13 | // const authController = require('./controllers/authController'); 14 | const bigQuery = require('./controllers/bigQuery'); 15 | 16 | app.use(express.json()); 17 | app.use(express.static(path.join(__dirname, './../client'))); 18 | 19 | // app.use(session({ 20 | // secret: process.env.SESSION_SECRET, 21 | // resave: true, 22 | // saveUninitialized: true 23 | // })); 24 | 25 | // app.post('/bigquery/datasets/:projectId', bigQuery.getDatasets, (req, res) => { 26 | // return res.status(200).send(res.locals); 27 | // }); 28 | 29 | app.get('/api/getProjectId', (req, res) => { 30 | return res.status(200).json(process.env.PROJECT_ID); 31 | }) 32 | 33 | app.get('/api/metrics/funcs/:projectId', metricsController.getFuncs, (req, res) => { 34 | // return res.status(200).send(res.locals.funcNames); 35 | return res.status(200).send(res.locals.funcConfigs); 36 | }) 37 | 38 | app.get('/api/metrics/execution_count/:projectId', metricsController.executionCount, (req, res) => { 39 | return res.status(200).send(res.locals.execution_count); 40 | }); 41 | 42 | app.get('/api/metrics/execution_times/:projectId', metricsController.executionTimes, (req, res) => { 43 | return res.status(200).send(res.locals.execution_times); 44 | }); 45 | 46 | app.get('/api/metrics/user_memory_bytes/:projectId', metricsController.userMemoryBytes, (req, res) => { 47 | return res.status(200).send(res.locals.user_memory_bytes); 48 | }); 49 | 50 | app.get('/api/metrics/network_egress/:projectId', metricsController.networkEgress, (req, res) => { 51 | return res.status(200).send(res.locals.network_egress); 52 | }); 53 | 54 | // routers 55 | app.use('/api/user', require('./routers/userRouter')); 56 | app.use('/api/forecast', require('./routers/forecastRouter')); 57 | app.use('/api/project', require('./routers/projectRouter')); 58 | 59 | // catch-all route handler 60 | app.use('*', (req, res) => { 61 | res.status(404).json('!!Page not found!!'); 62 | }); 63 | 64 | // global error handler 65 | app.use((err, req, res) => { 66 | console.log('ERROR HANDLER INVOKED') 67 | const defaultErr = { 68 | log: 'Express error handler caught unknown middleware error', 69 | status: 500, 70 | message: 'An error occurred' , 71 | }; 72 | const errorObj = Object.assign({}, defaultErr, err); 73 | console.log(errorObj.log); 74 | return res.status(errorObj.status).json(errorObj.message); 75 | }); 76 | 77 | // set up the server to listen for http requests 78 | app.listen(PORT, () => { 79 | console.log(`Server is listening on port ${PORT}`); 80 | }); 81 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = { 5 | entry: './src/client/index.js', 6 | output: { 7 | path: path.resolve(__dirname, 'build'), 8 | filename: 'bundle.js', 9 | }, 10 | mode: 'development', 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.jsx?/, 15 | use: [ 16 | { 17 | loader: 'babel-loader', 18 | options: { 19 | presets: [ 20 | '@babel/preset-env', '@babel/preset-react' 21 | ], 22 | }, 23 | }, 24 | ], 25 | exclude: /node_modules/, 26 | }, 27 | { 28 | test: /\.css$/i, 29 | use: ['style-loader', 'css-loader'], 30 | }, 31 | { 32 | test: /\.(png|jpe?g|gif|svg)$/i, 33 | use: ['file-loader'] 34 | }, 35 | ], 36 | }, 37 | devServer: { 38 | historyApiFallback: true, 39 | static: { 40 | directory: path.resolve(__dirname, 'build'), 41 | publicPath: '/build', 42 | }, 43 | port: 8080, 44 | proxy:[ 45 | { 46 | context: ['/api'], 47 | target: 'http://localhost:3000/' 48 | } 49 | ] 50 | }, 51 | plugins: [ 52 | new HtmlWebpackPlugin({ 53 | title: 'Development', 54 | template: '/src/client/index.html', 55 | }) 56 | ] 57 | }; --------------------------------------------------------------------------------