├── .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 |
115 | :
116 |
117 |
123 |
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 |
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 | };
--------------------------------------------------------------------------------