├── .eslintrc.base.js ├── .github ├── CODEOWNERS ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ └── branch-naming-check.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── custom.d.ts ├── package-lock.json ├── package.json ├── postcss.config.js ├── server ├── assets │ ├── timeFunctions.js │ └── validRegions.js ├── controllers │ ├── cloudwatchController.js │ ├── dataController.js │ ├── lambdaController.js │ ├── metricsController.js │ ├── priceController.js │ └── regionController.js ├── routes │ ├── logRouter.js │ ├── mainRouter.js │ ├── metricRouter.js │ ├── permissionRouter.js │ └── priceRouter.js └── server.js ├── src ├── .eslintrc.js ├── __test__ │ └── application.spec.ts ├── components │ ├── CreateGraph.tsx │ ├── CreateGraphLoader.tsx │ ├── FunctionDetails.tsx │ ├── GraphComponent.tsx │ ├── GraphLoader.tsx │ ├── Home.tsx │ ├── LambdaFuncComponent.tsx │ ├── LambdaFuncList.tsx │ ├── PermissionsDetails.tsx │ ├── PriceLoader.tsx │ ├── PricingDetails.tsx │ ├── RegionComponent.tsx │ ├── UserComponent.tsx │ └── charts │ │ ├── Bar.tsx │ │ ├── DoubleLine.tsx │ │ ├── Pie.tsx │ │ └── ScatterPlot.tsx ├── container │ ├── SidebarContainer.tsx │ ├── TopBarContainer.tsx │ └── mainContainer.tsx ├── context │ ├── DarkModeHooks.tsx │ ├── FunctionContext.tsx │ ├── GraphContext.tsx │ └── MainPageContext.tsx ├── electron.js ├── electron.ts ├── images │ ├── ghost.PNG │ ├── metrics.gif │ ├── permissions.gif │ └── pricing.gif ├── index.css ├── index.html └── react.tsx ├── tailwind.config.js ├── tsconfig.json └── webpack.config.js /.eslintrc.base.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es2021: true, 4 | }, 5 | extends: ['standard-with-typescript', 'plugin:prettier/recommended'], 6 | overrides: [], 7 | parserOptions: { 8 | ecmaVersion: 'latest', 9 | }, 10 | rules: {}, 11 | }; 12 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @oslabs-beta/kattl 2 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Pull Request Template 2 | 3 | ## Description 4 | 5 | Please include a summary of the changes and issues that are fixed. Also list any relevant details and dependencies required for this change. 6 | 7 | Fixes # (issue) (Note: Always tie PRs to user stories, NOT features) 8 | 9 | ## Type of change 10 | 11 | Please delete options that are not relevant and check the box of the option that is relevant. 12 | 13 | - [ ] Bug fix (non-breaking change which fixes an issue) 14 | - [ ] New feature (non-breaking change which adds functionality) 15 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 16 | - [ ] This change requires a documentation update 17 | 18 | ## Steps to test 19 | 20 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration 21 | 22 | - [ ] Test A 23 | - [ ] Test B 24 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/branch-naming-check.yml: -------------------------------------------------------------------------------- 1 | name: "Branch Naming Convention Check" 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize] 6 | 7 | jobs: 8 | check-branch-name: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Check branch name 12 | run: | 13 | if [[ "${{ github.actor }}" != "dependabot[bot]" && "${{ github.actor }}" != "dependabot-preview[bot]" ]]; then 14 | BRANCH_NAME=$(echo ${GITHUB_HEAD_REF} | grep -E "^(feature|bug|chore)/[a-zA-Z0-9_-]+$") 15 | if [[ ! $BRANCH_NAME ]]; then 16 | echo "Invalid branch name!" 17 | exit 1 18 | fi 19 | else 20 | echo "Dependabot PR, skipping branch name check." 21 | fi 22 | 23 | - name: Leave a comment if branch name is invalid 24 | if: failure() 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | run: | 28 | COMMENT_BODY="🚨 Invalid branch name! Branch names in this repo must adhere to the pattern: '(feature|bug|chore)/'. Please correct it and open a new PR. 🚨" 29 | PR_API_URL="${{ github.event.pull_request.comments_url }}" 30 | 31 | curl \ 32 | -X POST \ 33 | -H "Authorization: token $GITHUB_TOKEN" \ 34 | -H "Accept: application/vnd.github.v3+json" \ 35 | "$PR_API_URL" \ 36 | -d "{\"body\": \"$COMMENT_BODY\"}" 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /dist/ 3 | package-lock.json -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | build 3 | coverage 4 | node_modules 5 | dist -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": true, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 ghost 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | # ghost 4 | 5 | ## Getting Started 6 | 7 | First, please log into Amazon Web Services (AWS). Ghost works best when the AWS Command Line Interface (CLI) is installed on your computer. 8 | 9 | [Log into AWS](https://signin.aws.amazon.com/signin?redirect_uri=https%3A%2F%2Fconsole.aws.amazon.com%2Fconsole%2Fhome%3FhashArgs%3D%2523%26isauthcode%3Dtrue%26state%3DhashArgsFromTB_us-west-2_3def78f93219f346&client_id=arn%3Aaws%3Asignin%3A%3A%3Aconsole%2Fcanvas&forceMobileApp=0&code_challenge=8I-LvSUOJq5oXg_UEBENvX3DmGuddz2I9ScmMDvYY64&code_challenge_method=SHA-256) 10 | 11 | [Install AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) 12 | 13 |
14 | 15 | ## Configuration 16 | 17 | Configure the AWS CLI with your user profile in your terminal of choice. 18 | 19 | ``` 20 | aws configure 21 | ``` 22 | 23 | You will need your security credentials (Access Key ID and Secret Access Key), AWS Region, and output format. You can just press 'enter' if the default values match yours. 24 | 25 | ``` 26 | AWS Access Key ID: 27 | AWS Secret Access Key: 28 | Default region name [us-west-1]: 29 | Default output format [json]: 30 | ``` 31 | 32 |
33 | 34 | ## Run the Server 35 | 36 | To run the server, clone the GitHub repository to your computer. Navigate to the local directory and install the required Node modules. 37 | 38 | ``` 39 | git clone https://github.com/oslabs-beta/ghost.git 40 | cd ghost 41 | npm install 42 | ``` 43 | 44 | Once completed, run the server. 45 | 46 | ``` 47 | npm run server 48 | ``` 49 | 50 |
51 | 52 | ## Download and Launch 53 | 54 | Download ghost from the GitHub repository under 'Releases.' Currently, there are releases for [Mac OS](https://github.com/oslabs-beta/ghost/releases/download/v1.0.0/ghost-macos.zip) and [Windows](https://github.com/oslabs-beta/ghost/releases/download/v1.0.0/ghost-windows.zip). 55 | 56 | On MacOS: 57 | 58 |
  • Unzip the downloaded file
  • 59 |
  • Double click to open the 'ghost-darwin-x64' file
  • 60 |
  • Right click on the ghost app (do not double click)
  • 61 |
  • Click 'Open' on the popup to launch the app
  • 62 | 63 | On Windows: 64 | 65 |
  • Unzip the downloaded file
  • 66 |
  • Double click on the ghost app
  • 67 |
  • Click 'More Info', then a 'Run Anyways' button should appear
  • 68 |
  • Click 'Run Anyways' to launch the app
  • 69 | 70 | Now that ghost is ready to go, let's get started! 71 | 72 |
    73 | 74 | ## Metrics 75 | 76 | To view any graphs or data, you need to first select the Lambda Function you would like to view metrics for. Press 'Your Lambda Functions' on the left menu, then click the 'METRICS' button under the Lambda Function. Here are some basic metrics of your Lambda Function. 77 | 78 | For customized graphs and to see more metrics, select the Lambda Function you would like to create a custom graph for and view its metrics. Then click the orange 'CREATE GRAPH' button in the top right corner to display the graph creation user interface. Enter a title, select a metric, graph type, date/time range (end time must be within 24 hours from the start time), and then hit the 'SUBMIT' button. 79 | 80 | 81 | 82 |
    83 | 84 | ## Pricing 85 | 86 | To view the pricing calculator and previous billing history, first select the Lambda Function you would like to view pricing data for from the left menu. Click the 'PRICING' button under the specific Lambda Function. This will bring up the pricing calculator. Select type, memory size, storage size, billed duration, and total invocations. Click the 'CALCULATE PRICE' button when you are ready. 87 | 88 | To view past billing history, click the 'HISTORY' tab when you are in the pricing calculator user interface for that specific Lambda function. Select your month and year then click 'SUBMIT'. Your previous total cost for that month will be displayed. 89 | 90 | 91 | 92 |
    93 | 94 | ## Permissions 95 | 96 | Select the Lambda Function you would like to view/edit permissions for in the left menu. Under the selected Lambda Function, click the 'PERMISSIONS' button. This will show the permissions UI and 'LIST OF PERMISSIONS' is the default tab. Here you can view all your permissions' information. If you want to delete any, simply click on the 'DELETE PERMISSION' button under the specific permission you wish to delete. 97 | 98 | To add permissions, click on the 'ADD NEW PERMISSIONS' tab on top. Enter a Statement ID (cannot contain spaces), select an action, add a Principal, Principal Organization ID (optional), and then click 'ADD PERMISSION'. Your new permission has been added and can be seen on the 'LIST OF PERMISSIONS' tab now! 99 | 100 | 101 | 102 |
    103 | 104 | ## Technologies Used 105 | 106 | - Electron 107 | - TypeScript 108 | - React 109 | - React Router 110 | - Node.js 111 | - Express 112 | - Chart.js 113 | - MaterialUI 114 | - Tailwind CSS 115 | - Jest 116 | - Supertest 117 | 118 |
    119 | 120 | ## Report an Issue 121 | 122 | Encountered a problem with our application? [Submit a ticket](https://github.com/oslabs-beta/ghost/issues) to our GitHub under 'Issues.' Please be as descriptive as possible. 123 | 124 |
    125 | 126 | ## Contribute 127 | 128 | Interested in contributing to ghost or the Open Source community? The following is a list of features that the ghost team has either started or would like to implement. If you also have additional ideas, feel free to iterate off of ghost and implement those features! 129 | 130 | - Additional testing 131 | - Alerts 132 | - Search for your function 133 | 134 |
    135 | 136 | To contribute: 137 | 138 | - Fork the repository to your GitHub account. 139 | - Clone the project on your machine. 140 | - Create a branch for the issue you would like to work on. 141 | - Once completed, submit a pull request. A member of our team will review it as soon as we can! 142 | 143 |
    144 | 145 | ## Meet the Team 146 | 147 | - Krisette Odegard - [LinkedIn](https://www.linkedin.com/in/krisette) | [GitHub](https://github.com/krisette) 148 | - Akash Patel - [LinkedIn](https://www.linkedin.com/in/akashpatel1198/) | [GitHub](https://github.com/akashpatel1198) 149 | - Tim Kang - [LinkedIn](https://www.linkedin.com/in/tkkang/) | [GitHub](https://github.com/tkang611) 150 | - Tracy Chang - [LinkedIn](https://www.linkedin.com/in/tracycchang/) | [GitHub](https://github.com/tracycchang) 151 | - Lisa Tian - [LinkedIn](https://www.linkedin.com/in/lisatian-/) | [GitHub](https://github.com/lisatiann) 152 | -------------------------------------------------------------------------------- /custom.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.jpg'; 2 | declare module '*.png' { 3 | const value: any; 4 | export = value; 5 | } 6 | declare module '*.jpeg'; 7 | declare module '*.gif'; 8 | declare module '*.PNG' { 9 | const value: any; 10 | export = value; 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ghost", 3 | "version": "1.0.0", 4 | "description": "An open-source AWS Lambda metrics visualizer.", 5 | "contributors": [ 6 | { 7 | "name": "Krisette Odegard" 8 | }, 9 | { 10 | "name": "Akash Patel" 11 | }, 12 | { 13 | "name": "Tim Kang" 14 | }, 15 | { 16 | "name": "Tracy Chang" 17 | }, 18 | { 19 | "name": "Lisa Tian" 20 | } 21 | ], 22 | "main": "index.js", 23 | "scripts": { 24 | "build": "webpack --config ./webpack.config.js", 25 | "electron": "electron ./dist/electron.js", 26 | "start": "npm run build && electron ./dist/electron.js", 27 | "startMon": "npm run build && electronmon ./dist/electron.js", 28 | "server": "nodemon server/server.js", 29 | "lint": "prettier --write . && eslint --ext .js,.jsx,.ts,.tsx --fix ./src" 30 | }, 31 | "repository": { 32 | "type": "git", 33 | "url": "git+https://github.com/oslabs-beta/ghost.git" 34 | }, 35 | "keywords": [], 36 | "author": "", 37 | "license": "MIT", 38 | "bugs": { 39 | "url": "https://github.com/oslabs-beta/ghost/issues" 40 | }, 41 | "homepage": "https://github.com/oslabs-beta/ghost#readme", 42 | "devDependencies": { 43 | "@testing-library/react": "^13.4.0", 44 | "@types/react": "^18.0.21", 45 | "@types/react-dom": "^18.0.6", 46 | "@typescript-eslint/eslint-plugin": "^6.9.0", 47 | "@typescript-eslint/parser": "^6.9.0", 48 | "@wdio/cli": "^7.25.4", 49 | "autoprefixer": "^10.4.12", 50 | "css-loader": "^6.7.1", 51 | "electron": "^27.0.2", 52 | "eslint": "^8.52.0", 53 | "eslint-config-airbnb": "^19.0.4", 54 | "eslint-config-prettier": "^9.0.0", 55 | "eslint-config-standard-with-typescript": "^39.1.1", 56 | "eslint-plugin-import": "^2.29.0", 57 | "eslint-plugin-jsx-a11y": "^6.6.1", 58 | "eslint-plugin-n": "^16.2.0", 59 | "eslint-plugin-prettier": "^5.0.1", 60 | "eslint-plugin-promise": "^6.1.1", 61 | "eslint-plugin-react": "^7.33.2", 62 | "eslint-plugin-react-hooks": "^4.6.0", 63 | "html-webpack-plugin": "^5.5.0", 64 | "postcss": "^8.4.18", 65 | "postcss-loader": "^7.0.1", 66 | "prettier": "^3.0.3", 67 | "react": "^18.2.0", 68 | "react-dom": "^18.2.0", 69 | "style-loader": "^3.3.1", 70 | "tailwindcss": "^3.2.0", 71 | "ts-loader": "^9.4.1", 72 | "typescript": "^4.9.5", 73 | "webpack": "^5.74.0", 74 | "webpack-cli": "^4.10.0" 75 | }, 76 | "dependencies": { 77 | "@aws-sdk/client-cloudwatch": "^3.192.0", 78 | "@aws-sdk/client-cloudwatch-logs": "^3.192.0", 79 | "@aws-sdk/client-lambda": "^3.192.0", 80 | "@emotion/react": "^11.10.4", 81 | "@emotion/styled": "^11.10.4", 82 | "@mui/icons-material": "^5.10.9", 83 | "@mui/material": "^5.10.10", 84 | "@mui/x-date-pickers": "^5.0.7", 85 | "@types/express": "^4.17.14", 86 | "aws-sdk": "^2.1236.0", 87 | "chart.js": "^3.9.1", 88 | "dayjs": "^1.11.6", 89 | "express": "^4.18.2", 90 | "file-loader": "^6.2.0", 91 | "material-ui-popup-state": "^4.1.0", 92 | "nodemon": "^3.0.1", 93 | "react-chartjs-2": "^4.3.1", 94 | "react-spinners": "^0.13.6", 95 | "react-toggle-dark-mode": "^1.1.0" 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /server/assets/timeFunctions.js: -------------------------------------------------------------------------------- 1 | function convertTime(timestamp) { 2 | const dateObject = new Date(timestamp); // declare new data object 3 | const humanDataFormat = dateObject.toLocaleString('en-US', { 4 | timeZone: 'UTC', 5 | }); // convert to human-readable string 6 | return humanDataFormat; 7 | } 8 | 9 | function convertToUnix(date) { 10 | const dateObject = new Date((date += ' UTC')); // declare new date 11 | const unix = dateObject.getTime(); // convert to unix 12 | return unix; 13 | } 14 | 15 | module.exports = { 16 | convertTime, 17 | convertToUnix, 18 | }; 19 | -------------------------------------------------------------------------------- /server/assets/validRegions.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | 'us-east-1', 3 | 'us-east-2', 4 | 'us-west-1', 5 | 'us-west-2', 6 | 'af-south-1', 7 | 'ap-east-1', 8 | 'ap-southeast-3', 9 | 'ap-south-1', 10 | 'ap-northeast-3', 11 | 'ap-northeast-2', 12 | 'ap-southeast-1', 13 | 'ap-southeast-2', 14 | 'ap-northeast-1', 15 | 'ca-central-1', 16 | 'eu-central-1', 17 | 'eu-west-1', 18 | 'eu-west-2', 19 | 'eu-south-1', 20 | 'eu-west-3', 21 | 'eu-north-1', 22 | 'me-south-1', 23 | 'me-central-1', 24 | 'sa-east-1', 25 | ]; 26 | -------------------------------------------------------------------------------- /server/controllers/cloudwatchController.js: -------------------------------------------------------------------------------- 1 | const { 2 | CloudWatchLogsClient, 3 | DescribeLogStreamsCommand, 4 | GetLogEventsCommand, 5 | } = require('@aws-sdk/client-cloudwatch-logs'); 6 | const regionController = require('./regionController'); 7 | 8 | const cloudwatchController = {}; 9 | 10 | cloudwatchController.getLogStreams = (req, res, next) => { 11 | const client = new CloudWatchLogsClient(regionController.currentRegion); // req.body.region (object with key/value pair) 12 | 13 | const input = { 14 | logGroupName: `/aws/lambda/${req.body.functionName}`, 15 | descending: true, 16 | }; 17 | 18 | req.body.date ? (input.logStreamNamePrefix = req.body.date) : null; 19 | 20 | const command = new DescribeLogStreamsCommand(input); 21 | 22 | client 23 | .send(command) 24 | .then((data) => { 25 | const logStreams = data.logStreams.map((streamObj) => { 26 | const streamData = {}; 27 | streamData.arn = streamObj.arn; 28 | streamData.streamName = streamObj.logStreamName; 29 | return streamData; 30 | }); 31 | res.locals.logStreams = [...logStreams]; 32 | return next(); 33 | }) 34 | .catch((err) => { 35 | console.log('error in getLogStreams: ', err); 36 | return next('error in cw.getLogStreams'); 37 | }); 38 | }; 39 | 40 | cloudwatchController.getAllLogStreams = async (req, res, next) => { 41 | const client = new CloudWatchLogsClient(regionController.currentRegion); // req.body.region (object with key/value pair) 42 | 43 | const input = { 44 | logGroupName: `/aws/lambda/${req.body.functionName}`, 45 | descending: true, 46 | }; 47 | req.body.date ? (input.logStreamNamePrefix = req.body.date) : null; 48 | 49 | res.locals.logStreams = []; 50 | 51 | let data = null; 52 | 53 | do { 54 | data ? (input.nextToken = data.nextToken) : null; 55 | const command = new DescribeLogStreamsCommand(input); 56 | data = await client.send(command); 57 | const logStreams = data.logStreams.map((streamObj) => { 58 | const streamData = {}; 59 | streamData.arn = streamObj.arn; 60 | streamData.streamName = streamObj.logStreamName; 61 | return streamData; 62 | }); 63 | res.locals.logStreams = res.locals.logStreams.concat(logStreams); 64 | } while (data.nextToken); 65 | 66 | return next(); 67 | }; 68 | 69 | cloudwatchController.getRawLogs = async (req, res, next) => { 70 | const client = new CloudWatchLogsClient(regionController.currentRegion); 71 | 72 | const input = { 73 | logGroupName: `/aws/lambda/${req.body.functionName}`, 74 | logStreamName: req.body.streamName || res.locals.logStreams[0].streamName, 75 | // limit: 10 76 | }; 77 | 78 | res.locals.rawLogs = []; 79 | 80 | let data = null; 81 | 82 | do { 83 | data ? (input.nextToken = data.nextBackwardToken) : null; 84 | const command = new GetLogEventsCommand(input); 85 | data = await client.send(command); 86 | res.locals.rawLogs = res.locals.rawLogs.concat(data.events); 87 | } while (data.events.length); 88 | 89 | return next(); 90 | }; 91 | 92 | cloudwatchController.iterateStreamsForLogs = async (req, res, next) => { 93 | res.locals.rawLogs = []; 94 | 95 | const client = new CloudWatchLogsClient(regionController.currentRegion); 96 | 97 | for (const stream of res.locals.logStreams) { 98 | const input = { 99 | logGroupName: `/aws/lambda/${req.body.functionName}`, 100 | logStreamName: stream.streamName, 101 | }; 102 | 103 | let data = null; 104 | 105 | do { 106 | data ? (input.nextToken = data.nextBackwardToken) : null; 107 | const command = new GetLogEventsCommand(input); 108 | data = await client.send(command); 109 | res.locals.rawLogs = res.locals.rawLogs.concat(data.events); 110 | } while (data.events.length); 111 | } 112 | 113 | return next(); 114 | }; 115 | 116 | module.exports = cloudwatchController; 117 | -------------------------------------------------------------------------------- /server/controllers/dataController.js: -------------------------------------------------------------------------------- 1 | const { convertTime } = require('../assets/timeFunctions'); 2 | 3 | const dataController = {}; 4 | 5 | dataController.parseBasic = (req, res, next) => { 6 | try { 7 | // res.locals.rawLogs is an array of objects 8 | let basicData = res.locals.rawLogs.map((logObj) => { 9 | const basicObj = {}; 10 | if (!logObj.message.startsWith('REPORT')) return undefined; 11 | const messageStringArr = logObj.message.trim().split('\t'); 12 | messageStringArr[0] = messageStringArr[0].slice(7); 13 | for (const message of messageStringArr) { 14 | const prop = message.split(':'); 15 | 16 | let key = prop[0]; 17 | key = key[0].toLowerCase() + key.slice(1); 18 | key = key.replaceAll(' ', ''); 19 | 20 | const value = prop[1].trim(); 21 | 22 | basicObj[key] = value; 23 | } 24 | const timestamp = convertTime(logObj.timestamp); 25 | basicObj.timestamp = timestamp; 26 | return basicObj; 27 | }); 28 | 29 | // now basicData is an array of objects representing a single log 30 | // filter out all undefined values from the extraneous logs 31 | basicData = basicData.filter((ele) => ele); 32 | 33 | // iterate through rawLogs again, looking for any error messages, and adding them to 34 | // the appropriate logObject 35 | for (const errObj of res.locals.rawLogs) { 36 | if (errObj.message.includes('ERROR')) { 37 | const errorArr = errObj.message.split('\t'); 38 | const requestId = errorArr[1].trim(); 39 | for (const logObj of basicData) { 40 | if (logObj.requestId === requestId) logObj.error = true; 41 | } 42 | } 43 | } 44 | 45 | res.locals.basicData = basicData; 46 | return next(); 47 | } catch (err) { 48 | console.log('error in parseBasic', err); 49 | return next(err); 50 | } 51 | }; 52 | 53 | dataController.parsePrice = (req, res, next) => { 54 | // res.locals.basicData is an array of basic Data objects 55 | let totalBilledDuration = 0; 56 | let invocations = 0; 57 | 58 | for (const logEvent of res.locals.basicData) { 59 | invocations++; 60 | totalBilledDuration += parseInt(logEvent.billedDuration, 10); 61 | } 62 | 63 | res.locals.priceMetrics = { 64 | durationTotal: totalBilledDuration, 65 | invocationsTotal: invocations, 66 | }; 67 | 68 | return next(); 69 | }; 70 | 71 | dataController.parseColdStarts = (req, res, next) => { 72 | // res.locals.basicData is an array of basic Data objects 73 | // initDuration 74 | 75 | const coldMetrics = []; 76 | 77 | for (const logEvent of res.locals.basicData) { 78 | if (logEvent.hasOwnProperty('initDuration')) { 79 | const coldStart = { 80 | initDuration: logEvent.initDuration, 81 | timestamp: logEvent.timestamp, 82 | }; 83 | coldMetrics.push(coldStart); 84 | } 85 | } 86 | 87 | res.locals.coldMetrics = coldMetrics; 88 | 89 | return next(); 90 | }; 91 | 92 | module.exports = dataController; 93 | -------------------------------------------------------------------------------- /server/controllers/lambdaController.js: -------------------------------------------------------------------------------- 1 | const { 2 | LambdaClient, 3 | ListFunctionsCommand, 4 | GetFunctionConfigurationCommand, 5 | GetPolicyCommand, 6 | AddPermissionCommand, 7 | RemovePermissionCommand, 8 | } = require('@aws-sdk/client-lambda'); 9 | const regionController = require('./regionController'); 10 | 11 | const lambdaController = {}; 12 | 13 | lambdaController.getFunctions = (req, res, next) => { 14 | const client = new LambdaClient(regionController.currentRegion); 15 | 16 | const input = {}; 17 | 18 | const command = new ListFunctionsCommand(input); 19 | 20 | client 21 | .send(command) 22 | .then((data) => { 23 | const functions = data.Functions.map((fnObj) => { 24 | const funcData = {}; 25 | funcData.functionName = fnObj.FunctionName; 26 | funcData.functionARN = fnObj.FunctionArn; 27 | return funcData; 28 | }); 29 | res.locals.functions = functions; 30 | return next(); 31 | }) 32 | .catch((err) => { 33 | console.log('error in getFunctions: ', err); 34 | return next(err); 35 | }); 36 | }; 37 | 38 | lambdaController.functionConfig = (req, res, next) => { 39 | const client = new LambdaClient(regionController.currentRegion); 40 | 41 | const input = { 42 | FunctionName: req.body.functionName, 43 | }; 44 | 45 | const command = new GetFunctionConfigurationCommand(input); 46 | 47 | client 48 | .send(command) 49 | .then((data) => { 50 | res.locals.functionConfig = { 51 | type: data.Architectures[0], 52 | memorySize: data.MemorySize, 53 | storage: data.EphemeralStorage.Size, 54 | runtime: data.Runtime, 55 | }; 56 | return next(); 57 | }) 58 | .catch((err) => { 59 | console.log('error in functionConfig: ', err); 60 | return next(err); 61 | }); 62 | }; 63 | 64 | lambdaController.getPolicies = (req, res, next) => { 65 | const client = new LambdaClient(regionController.currentRegion); 66 | 67 | const input = { 68 | FunctionName: req.body.functionName, 69 | }; 70 | 71 | const command = new GetPolicyCommand(input); 72 | // hover on principal explains that principle represents the services, arns, etc that have permissions to use your lambda function 73 | client 74 | .send(command) 75 | .then((data) => { 76 | const policies = JSON.parse(data.Policy).Statement.map((policyObj) => { 77 | const policyData = { 78 | statementId: policyObj.Sid, 79 | action: policyObj.Action, 80 | resource: policyObj.Resource, 81 | }; 82 | if (typeof policyObj.Principal === 'object') { 83 | for (const key in policyObj.Principal) { 84 | policyData.principal = policyObj.Principal[key]; 85 | } 86 | } else { 87 | policyData.principal = policyObj.Principal; 88 | } 89 | if (policyObj?.Condition?.StringEquals?.['aws:PrincipalOrgID']) { 90 | policyData.principalOrgId = 91 | policyObj.Condition.StringEquals['aws:PrincipalOrgID']; 92 | } 93 | 94 | return policyData; 95 | }); 96 | res.locals.policies = policies; 97 | return next(); 98 | }) 99 | .catch((err) => { 100 | console.log('error in getPolicies: ', err); 101 | return next(err); 102 | }); 103 | }; 104 | 105 | lambdaController.addPermission = (req, res, next) => { 106 | const client = new LambdaClient(regionController.currentRegion); 107 | 108 | const input = { 109 | Action: req.body.action, 110 | FunctionName: req.body.functionName, 111 | Principal: req.body.principal, 112 | StatementId: req.body.statementId, 113 | }; 114 | 115 | req.body.principalOrgId 116 | ? (input.PrincipalOrgID = req.body.principalOrgId) 117 | : null; 118 | 119 | const command = new AddPermissionCommand(input); 120 | 121 | client 122 | .send(command) 123 | .then((data) => { 124 | res.locals.addedPermission = data; 125 | return next(); 126 | }) 127 | .catch((err) => { 128 | console.log('error in addPermissions: ', err); 129 | return next(err); 130 | }); 131 | }; 132 | 133 | lambdaController.removePermission = (req, res, next) => { 134 | const client = new LambdaClient(regionController.currentRegion); 135 | 136 | const input = { 137 | FunctionName: req.body.functionName, 138 | StatementId: req.body.statementId, 139 | }; 140 | 141 | const command = new RemovePermissionCommand(input); 142 | 143 | client 144 | .send(command) 145 | .then((data) => { 146 | res.locals.removedPermission = data; 147 | return next(); 148 | }) 149 | .catch((err) => { 150 | console.log('error in removePermissions: ', err); 151 | return next(err); 152 | }); 153 | }; 154 | 155 | module.exports = lambdaController; 156 | -------------------------------------------------------------------------------- /server/controllers/metricsController.js: -------------------------------------------------------------------------------- 1 | const { 2 | CloudWatchClient, 3 | GetMetricStatisticsCommand, 4 | } = require('@aws-sdk/client-cloudwatch'); 5 | const { convertToUnix, convertTime } = require('../assets/timeFunctions'); 6 | const regionController = require('./regionController'); 7 | 8 | const metricsController = {}; 9 | 10 | metricsController.getMetrics = (req, res, next) => { 11 | const client = new CloudWatchClient(regionController.currentRegion); // req.body.region (object with key/value pair) 12 | const input = { 13 | StartTime: new Date(convertToUnix(req.body.startTime)), // "10/27/2022, 12:00:00 AM" 14 | EndTime: new Date(convertToUnix(req.body.endTime)), // "10/27/2022, 11:59:59 PM" 15 | MetricName: req.body.metricName, 16 | Namespace: 'AWS/Lambda', 17 | Period: 60, // req.body.period (60, 300, 3600) 18 | Statistics: ['Sum', 'Maximum', 'Minimum', 'Average'], // req.body.statistics (should be an array) 19 | Dimensions: [ 20 | { 21 | Name: 'FunctionName', 22 | Value: req.body.functionName, 23 | }, 24 | { 25 | Name: 'Resource', 26 | Value: req.body.functionName, 27 | }, 28 | ], 29 | }; 30 | 31 | const command = new GetMetricStatisticsCommand(input); 32 | 33 | client 34 | .send(command) 35 | .then((data) => { 36 | const metric = {}; 37 | metric.Label = data.Label; 38 | metric.Datapoints = data.Datapoints.map((datapoint) => { 39 | datapoint.Timestamp = convertTime(datapoint.Timestamp); 40 | return datapoint; 41 | }); 42 | res.locals.metricStats = metric; 43 | return next(); 44 | }) 45 | .catch((error) => { 46 | console.log('error in get Metric Stats: ', error.name); 47 | return next(error); 48 | }); 49 | }; 50 | 51 | module.exports = metricsController; 52 | -------------------------------------------------------------------------------- /server/controllers/priceController.js: -------------------------------------------------------------------------------- 1 | const pricingController = {}; 2 | 3 | pricingController.getEstimate = (req, res, next) => { 4 | // req.body will have these props: 5 | // type: "x86_64" or "Arm" 6 | // memorySize: 128 //must be b/w 128 and 10240 (10gb) 7 | // storage: 512 (number) //must be between 512 to 10240 8 | // billedDurationAvg: Number //must be b/w 1 to 900000 9 | // invocationsTotal: Number //must be b/w 1 to 1e+21 10 | console.log(req.body); 11 | const typeKey = req.body.type || res.locals.functionConfig.type; 12 | const memKey = req.body.memorySize || res.locals.functionConfig.memorySize; 13 | const invocations = 14 | req.body.invocationsTotal || res.locals.priceMetrics.invocationsTotal; 15 | const storage = req.body.storage || res.locals.functionConfig.storage; 16 | const totalDuration = req.body.billedDurationAvg 17 | ? req.body.billedDurationAvg * invocations 18 | : res.locals.priceMetrics.durationTotal; 19 | 20 | // unit conversions: 21 | const memoryGb = memKey * 0.0009765625; 22 | const totalDurationSec = totalDuration * 0.001; 23 | const storageGb = storage * 0.0009765625; 24 | 25 | // gb-sec calculation 26 | let totalGbSec = memoryGb * totalDurationSec; 27 | 28 | // inflate history cost for demo 29 | res.locals.functionConfig ? (totalGbSec *= 10000000) : null; 30 | 31 | // calculating cost 32 | let firstTier; 33 | let middleTier; 34 | let lastTier; 35 | 36 | let cost = 0; 37 | 38 | function round(num) { 39 | return Math.round(num * 100) / 100; 40 | } 41 | 42 | // tier breakdown calculation 43 | if (typeKey === 'x86_64') { 44 | if (totalGbSec <= 6000000000) { 45 | firstTier = totalGbSec; 46 | } 47 | if (totalGbSec > 6000000000 && totalGbSec <= 15000000000) { 48 | firstTier = 6000000000; 49 | middleTier = totalGbSec - 6000000000; 50 | } 51 | if (totalGbSec > 15000000000) { 52 | firstTier = 6000000000; 53 | middleTier = 9000000000; 54 | lastTier = totalGbSec - 15000000000; 55 | } 56 | 57 | // calc for firstTier 58 | cost += firstTier * 0.0000166667; 59 | // calc for middleTier 60 | if (middleTier) cost += middleTier * 0.000015; 61 | // calc for lastTier 62 | if (lastTier) cost += lastTier * 0.0000133334; 63 | } else if (typeKey === 'Arm') { 64 | if (totalGbSec <= 7500000000) { 65 | firstTier = totalGbSec; 66 | } 67 | if (totalGbSec > 7500000000 && totalGbSec <= 18750000000) { 68 | firstTier = 7500000000; 69 | middleTier = totalGbSec - 7500000000; 70 | } 71 | if (totalGbSec > 18750000000) { 72 | firstTier = 7500000000; 73 | middleTier = 11250000000; 74 | lastTier = totalGbSec - 18750000000; 75 | } 76 | 77 | // calc for firstTier 78 | cost += firstTier * 0.0000133334; 79 | // calc for middleTier 80 | if (middleTier) cost += middleTier * 0.0000120001; 81 | // calc for lastTier 82 | if (lastTier) cost += lastTier * 0.0000106667; 83 | } 84 | 85 | // add the cost for invocations 86 | cost += round(invocations * 0.0000002); 87 | 88 | // billable portion of storage 89 | 90 | const storageBill = round(storageGb - 0.5); 91 | // get total storage 92 | const storageTotal = round(storageBill * totalDuration); 93 | // get total cost 94 | cost += storageTotal * 0.000000037 > 0 ? storageTotal * 0.000000037 : 0; 95 | 96 | // respond with cost 97 | res.locals.cost = round(cost); 98 | return next(); 99 | }; 100 | 101 | module.exports = pricingController; 102 | -------------------------------------------------------------------------------- /server/controllers/regionController.js: -------------------------------------------------------------------------------- 1 | const validRegions = require('../assets/validRegions'); 2 | 3 | const regionController = { 4 | currentRegion: { region: 'us-west-1' }, 5 | }; 6 | 7 | regionController.changeRegion = (req, res, next) => { 8 | try { 9 | const newRegion = req.body.region; 10 | if (validRegions.includes(newRegion)) { 11 | regionController.currentRegion.region = req.body.region; 12 | res.locals.response = 'region changed'; 13 | } else { 14 | res.locals.response = 'invalid region'; 15 | } 16 | return next(); 17 | } catch (err) { 18 | console.log('error in changeRegion', err); 19 | return next(err); 20 | } 21 | }; 22 | 23 | module.exports = regionController; 24 | -------------------------------------------------------------------------------- /server/routes/logRouter.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const cloudwatchController = require('../controllers/cloudwatchController'); 3 | const dataController = require('../controllers/dataController'); 4 | 5 | const router = express.Router(); 6 | 7 | router.post( 8 | '/logStreams', 9 | cloudwatchController.getAllLogStreams, 10 | (req, res) => { 11 | res.status(200).json(res.locals.logStreams); 12 | } 13 | ); 14 | 15 | router.post('/rawLogs', cloudwatchController.getRawLogs, (req, res) => { 16 | res.status(200).json(res.locals.rawLogs); 17 | }); 18 | 19 | router.post( 20 | '/parsedLogs', 21 | cloudwatchController.getRawLogs, 22 | dataController.parseBasic, 23 | (req, res) => { 24 | res.status(200).json(res.locals.basicData); 25 | } 26 | ); 27 | 28 | module.exports = router; 29 | -------------------------------------------------------------------------------- /server/routes/mainRouter.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const lambdaController = require('../controllers/lambdaController'); 3 | const regionController = require('../controllers/regionController'); 4 | 5 | const router = express.Router(); 6 | 7 | router.get('/functions', lambdaController.getFunctions, (req, res) => { 8 | res.status(200).json(res.locals.functions); 9 | }); 10 | 11 | router.post('/changeRegion', regionController.changeRegion, (req, res) => { 12 | res.status(200).json(res.locals.response); 13 | }); 14 | 15 | module.exports = router; 16 | -------------------------------------------------------------------------------- /server/routes/metricRouter.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const cloudwatchController = require('../controllers/cloudwatchController'); 3 | const dataController = require('../controllers/dataController'); 4 | const metricsController = require('../controllers/metricsController'); 5 | 6 | const router = express.Router(); 7 | 8 | router.post( 9 | '/recent', 10 | cloudwatchController.getLogStreams, 11 | cloudwatchController.getRawLogs, 12 | dataController.parseBasic, 13 | (req, res) => { 14 | res.status(200).json(res.locals.basicData); 15 | } 16 | ); 17 | 18 | router.post('/custom', metricsController.getMetrics, (req, res) => { 19 | res.status(200).json(res.locals.metricStats); 20 | }); 21 | 22 | router.post( 23 | '/cold', 24 | cloudwatchController.getAllLogStreams, 25 | cloudwatchController.iterateStreamsForLogs, 26 | dataController.parseBasic, 27 | dataController.parseColdStarts, 28 | (req, res) => { 29 | res.status(200).json(res.locals.coldMetrics); 30 | } 31 | ); 32 | 33 | module.exports = router; 34 | -------------------------------------------------------------------------------- /server/routes/permissionRouter.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const lambdaController = require('../controllers/lambdaController'); 3 | 4 | const router = express.Router(); 5 | 6 | router.post('/list', lambdaController.getPolicies, (req, res) => { 7 | res.status(200).json(res.locals.policies); 8 | }); 9 | 10 | router.post('/add', lambdaController.addPermission, (req, res) => { 11 | res.status(200).json('permission added'); 12 | }); 13 | 14 | router.post('/remove', lambdaController.removePermission, (req, res) => { 15 | res.status(200).json('permission removed'); 16 | }); 17 | 18 | module.exports = router; 19 | -------------------------------------------------------------------------------- /server/routes/priceRouter.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const lambdaController = require('../controllers/lambdaController'); 3 | const priceController = require('../controllers/priceController'); 4 | const cloudwatchController = require('../controllers/cloudwatchController'); 5 | const dataController = require('../controllers/dataController'); 6 | 7 | // controllers import here 8 | 9 | const router = express.Router(); 10 | 11 | router.post('/defaultConfig', lambdaController.functionConfig, (req, res) => { 12 | res.status(200).json(res.locals.functionConfig); 13 | }); 14 | 15 | router.post('/calc', priceController.getEstimate, (req, res) => { 16 | res.status(200).json(res.locals.cost); 17 | }); 18 | 19 | router.post( 20 | '/history', 21 | cloudwatchController.getAllLogStreams, 22 | cloudwatchController.iterateStreamsForLogs, 23 | dataController.parseBasic, 24 | dataController.parsePrice, 25 | lambdaController.functionConfig, 26 | priceController.getEstimate, 27 | (req, res) => { 28 | res.status(200).json(res.locals.cost); 29 | } 30 | ); 31 | 32 | module.exports = router; 33 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const app = express(); 4 | 5 | app.use(express.json()); 6 | 7 | // import routes 8 | const mainRouter = require('./routes/mainRouter'); 9 | const metricRouter = require('./routes/metricRouter'); 10 | const priceRouter = require('./routes/priceRouter'); 11 | const permissionRouter = require('./routes/permissionRouter'); 12 | const logRouter = require('./routes/logRouter'); 13 | 14 | // define routes 15 | app.use('/main', mainRouter); 16 | 17 | app.use('/metric', metricRouter); 18 | 19 | app.use('/price', priceRouter); 20 | 21 | app.use('/permission', permissionRouter); 22 | 23 | app.use('/log', logRouter); 24 | 25 | // undefined route handler 26 | app.use('/', (req, res) => { 27 | res.status(404).send('Invalid route endpoint'); 28 | }); 29 | 30 | // global error handler 31 | app.use((err, req, res, next) => { 32 | const errObj = { 33 | log: 'global error handler invoked', 34 | status: 400, 35 | message: err, 36 | }; 37 | if (err.name === 'InvalidParameterCombinationException') { 38 | errObj.tooManyDatapoints = true; 39 | } 40 | return res.status(errObj.status).json(errObj); 41 | }); 42 | 43 | // listen on port 44 | app.listen(3000, () => { 45 | console.log('listening on 3000 on server R'); 46 | }); 47 | -------------------------------------------------------------------------------- /src/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | '../.eslintrc.base.js', // Path to your base configuration 4 | 'plugin:react/recommended', 5 | ], 6 | env: { 7 | browser: true, 8 | node: true, 9 | }, 10 | plugins: ['react'], 11 | parserOptions: { 12 | ecmaFeatures: { 13 | jsx: true, 14 | }, 15 | }, 16 | settings: { 17 | react: { 18 | version: 'detect', 19 | }, 20 | }, 21 | rules: {}, 22 | }; 23 | -------------------------------------------------------------------------------- /src/__test__/application.spec.ts: -------------------------------------------------------------------------------- 1 | // describe('application loading', () => { 2 | // describe('App', () => { 3 | // it('should launch the application', async () => { 4 | // // await browser.waitUntilTextExists('html', 'Hello'); 5 | // const title = await browser.getTitle(); 6 | // expect(title).toEqual('ghost - an aws lambda metrics visualizer'); 7 | // }); 8 | // }); 9 | // }); 10 | 11 | // npx wdio run ./wdio.conf.ts 12 | -------------------------------------------------------------------------------- /src/components/CreateGraph.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Select, Button, TextField, MenuItem, InputLabel } from '@mui/material'; 3 | import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; 4 | import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; 5 | import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; 6 | import { DatePicker } from '@mui/x-date-pickers/DatePicker'; 7 | import * as dayjs from 'dayjs'; 8 | import { useMainPageContext } from '../context/MainPageContext'; 9 | import CreateGraphLoader from './CreateGraphLoader'; 10 | import { useGraphContext } from '../context/GraphContext'; 11 | import { useFunctionContext } from '../context/FunctionContext'; 12 | 13 | const CreateGraph = (): JSX.Element => { 14 | // pull relevant state out of context 15 | const { functionName } = useFunctionContext(); 16 | const { 17 | setCustomGraphs, 18 | graphType, 19 | setGraphType, 20 | metricName, 21 | setMetricName, 22 | graphName, 23 | setGraphName, 24 | startTime, 25 | setStartTime, 26 | endTime, 27 | setEndTime, 28 | datapointType, 29 | setDatapointType, 30 | } = useGraphContext(); 31 | const { createLoading, setCreateLoading } = useMainPageContext(); 32 | const [coldStartDate, setColdStartDate] = React.useState(''); 33 | const [errorNoData, setErrorNoData] = React.useState(false); 34 | const [errorTooMuchData, setErrorTooMuchData] = React.useState(false); 35 | 36 | // store list of metrics and graphtypes in an array 37 | const graphTypeNames = ['Line', 'Bar', 'Pie', 'MultiLine']; 38 | const metricNames = [ 39 | 'Errors', 40 | 'ConcurrentExecutions', 41 | 'Invocations', 42 | 'Duration', 43 | 'Throttles', 44 | 'UrlRequestCount', 45 | 'ColdStarts', 46 | ]; 47 | const datapointTypeNames = ['Average', 'Sum', 'Minimum', 'Maximum']; 48 | 49 | // on submit, send the data to the backend 50 | async function handleSubmit(): Promise { 51 | setErrorTooMuchData(false); 52 | setErrorNoData(false); 53 | setCreateLoading?.(true); 54 | // call custom metric API 55 | const res = await fetch('http://localhost:3000/metric/custom', { 56 | method: 'POST', 57 | headers: { 'Content-Type': 'application/json' }, 58 | body: JSON.stringify({ 59 | functionName, 60 | metricName, 61 | startTime, 62 | endTime, 63 | }), 64 | }); 65 | const data = await res.json(); 66 | setCreateLoading?.(false); 67 | 68 | // error handling to display to the user 69 | if (data.tooManyDatapoints) { 70 | setErrorTooMuchData(true); 71 | } else if (data.Datapoints.length === 0) { 72 | setErrorNoData(true); 73 | } else { 74 | // save the graph setup to the state, in addition to all the previous graphs 75 | const newCustomGraph = { 76 | functionName, 77 | graphName, 78 | graphType, 79 | metricName, 80 | date: coldStartDate, 81 | metricData: data, 82 | datapointType, 83 | }; 84 | setCustomGraphs?.((prev: any) => [...prev, newCustomGraph]); 85 | } 86 | } 87 | 88 | // if selected metric is coldstarts, this function will fire on submit 89 | async function handleSubmitColdStarts(): Promise { 90 | setErrorTooMuchData(false); 91 | setErrorNoData(false); 92 | setCreateLoading?.(true); 93 | const res = await fetch('http://localhost:3000/metric/cold', { 94 | method: 'POST', 95 | headers: { 'Content-Type': 'application/json' }, 96 | body: JSON.stringify({ 97 | functionName, 98 | date: coldStartDate, 99 | }), 100 | }); 101 | const data = await res.json(); 102 | setCreateLoading?.(false); 103 | 104 | // error handling to display to the user 105 | if (data.tooManyDatapoints) { 106 | setErrorTooMuchData(true); 107 | } else if (data.length === 0) { 108 | setErrorNoData(true); 109 | } else { 110 | // save the graph setup to the state, in addition to all the previous graphs 111 | const newCustomGraph = { 112 | functionName, 113 | graphName, 114 | graphType, 115 | metricName, 116 | date: coldStartDate, 117 | metricData: data, 118 | datapointType, 119 | }; 120 | setCustomGraphs?.((prev: any) => [...prev, newCustomGraph]); 121 | } 122 | } 123 | 124 | return ( 125 |
    126 |

    127 | Create Graph for 128 | {functionName} 129 |

    130 |
    131 | Graph Name 132 | setGraphName?.(e.target.value)} 138 | /> 139 |
    140 | 141 | Metrics 142 | 153 |
    154 | 155 | {metricName === 'ColdStarts' ? null : ( 156 | <> 157 | Graph Type 158 | 169 |
    170 | 171 | )} 172 | 173 | {(metricName !== 'ColdStarts' && graphType === 'Line') || 174 | graphType === 'Bar' || 175 | graphType === 'Pie' ? ( 176 | <> 177 | Datapoints Type 178 | 189 |
    190 | 191 | ) : null} 192 | 193 | {metricName === 'ColdStarts' ? ( 194 | <> 195 | Select Date 196 | 197 | { 201 | const newDate = dayjs(newValue).format('YYYY/MM/DD'); 202 | console.log(newDate); 203 | setColdStartDate(newDate); 204 | }} 205 | renderInput={(params) => } 206 | /> 207 | 208 |
    209 | 210 | ) : ( 211 | <> 212 | Start Date & Time 213 | 214 | { 218 | const newDate = new Date(newValue).toLocaleString(); 219 | setStartTime?.(newDate); 220 | }} 221 | renderInput={(params) => } 222 | /> 223 | 224 |
    225 | 226 | End Date & Time 227 | 228 | { 232 | // only allow the date and time to be set, if the time within 24 hours time difference 233 | const newDate = new Date(newValue).toLocaleString(); 234 | dayjs(newDate).isAfter(dayjs(startTime).add(24, 'hour')) 235 | ? alert( 236 | 'Please select a time within 24 hours of the start time' 237 | ) 238 | : setEndTime?.(newDate); 239 | }} 240 | renderInput={(params) => } 241 | /> 242 | 243 |
    244 | 245 | )} 246 | 247 | 272 |
    273 | {errorNoData ? ( 274 |

    275 | Error: No datapoints available for this time range. 276 |

    277 | ) : null} 278 | {errorTooMuchData ? ( 279 |

    280 | Error: Too many datapoints available for this time range. Please 281 | select a smaller time range. 282 |

    283 | ) : null} 284 | {createLoading ? ( 285 |
    286 | 287 |
    288 | ) : null} 289 |
    290 | ); 291 | }; 292 | 293 | export default CreateGraph; 294 | -------------------------------------------------------------------------------- /src/components/CreateGraphLoader.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { BeatLoader } from 'react-spinners'; 3 | import { useMainPageContext } from '../context/MainPageContext'; 4 | 5 | const override: React.CSSProperties = { 6 | display: 'block', 7 | margin: '0 auto', 8 | borderColor: 'white', 9 | }; 10 | 11 | function PriceLoader() { 12 | const { createLoading } = useMainPageContext(); 13 | const [color, setColor] = React.useState('#ffffff'); 14 | 15 | return createLoading ? ( 16 |
    17 |
    18 | 26 |
    27 |
    28 | ) : null; 29 | } 30 | 31 | export default PriceLoader; 32 | -------------------------------------------------------------------------------- /src/components/FunctionDetails.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useFunctionContext } from '../context/FunctionContext'; 3 | import { useMainPageContext } from '../context/MainPageContext'; 4 | import { useGraphContext } from '../context/GraphContext'; 5 | import GraphComponent from './GraphComponent'; 6 | import GraphLoader from './GraphLoader'; 7 | 8 | function FunctionDetails() { 9 | const [error, showError] = React.useState(false); 10 | const { functionName } = useFunctionContext(); 11 | const { loading, setLoading } = useMainPageContext(); 12 | const { defaultMetrics, setDefaultMetrics } = useGraphContext(); 13 | 14 | // fetch most recent metrics & initialize skeleton 15 | React.useEffect(() => { 16 | setLoading?.(true); 17 | fetch('http://localhost:3000/metric/recent', { 18 | method: 'POST', 19 | headers: { 20 | 'Content-Type': 'application/json', 21 | }, 22 | body: JSON.stringify({ functionName }), 23 | }) 24 | .then(async (response) => await response.json()) 25 | .then((data) => { 26 | // if backend returns an error message, do not render data or else it will crash 27 | if (!Array.isArray(data)) { 28 | setLoading?.(false); 29 | showError(true); 30 | } else { 31 | showError(false); 32 | setDefaultMetrics?.(data); 33 | setLoading?.(false); 34 | } 35 | }); 36 | }, [functionName]); 37 | 38 | return ( 39 |
    40 |

    41 | Viewing metrics for: 42 |

    43 |

    44 | {functionName} 45 |

    46 | {error ? ( 47 |

    48 | There was an error retrieving metrics for this function. Please try 49 | again or try a different function. 50 |

    51 | ) : null} 52 | {loading ? : !error && } 53 |
    54 | ); 55 | } 56 | 57 | export default FunctionDetails; 58 | -------------------------------------------------------------------------------- /src/components/GraphComponent.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Chart as ChartJS, registerables } from 'chart.js'; 3 | import { Bar, Pie, Line } from 'react-chartjs-2'; 4 | import { useGraphContext } from '../context/GraphContext'; 5 | import { useFunctionContext } from '../context/FunctionContext'; 6 | 7 | // initialize chartjs 8 | ChartJS.register(...registerables); 9 | 10 | function GraphComponent() { 11 | // pull out custom graphs and function name from context 12 | const { customGraphs, defaultMetrics } = useGraphContext(); 13 | const { functionName } = useFunctionContext(); 14 | 15 | // pull out timestamps, durations, memory from basicMetrics 16 | const timestamps: string[] = defaultMetrics.map((item: any) => 17 | item.timestamp.slice(-11) 18 | ); 19 | const durations: number[] = defaultMetrics.map((item: any) => 20 | parseInt(item.duration.replace(/\D/g, '')) 21 | ); 22 | const memory: number[] = defaultMetrics.map((item: any) => 23 | parseInt(item.maxMemoryUsed.replace(/\D/g, '')) 24 | ); 25 | const date: string[] = defaultMetrics.map((item: any) => 26 | item.timestamp.slice(0, 10) 27 | ); 28 | 29 | // manually counting invocations 30 | const invocationObj: any = {}; 31 | for (let i = 0; i < timestamps.length; i++) { 32 | if (invocationObj[timestamps[i]]) { 33 | invocationObj[timestamps[i]] += 1; 34 | } else { 35 | invocationObj[timestamps[i]] = 1; 36 | } 37 | } 38 | const invocations = Object.values(invocationObj); 39 | const singleTime = Object.keys(invocationObj); 40 | 41 | // state for the default graphs 42 | const durationBarState = { 43 | labels: timestamps, 44 | datasets: [ 45 | { 46 | label: 'Duration', 47 | backgroundColor: [ 48 | '#B2CAB3', 49 | '#B8E8FC', 50 | '#EDC09E', 51 | '#9cb59d', 52 | '#FFCACA', 53 | '#D2DAFF', 54 | ], 55 | borderWidth: 0, 56 | borderColor: 'black', 57 | data: durations, 58 | showLine: true, 59 | }, 60 | ], 61 | }; 62 | 63 | const memoryState = { 64 | labels: timestamps, 65 | datasets: [ 66 | { 67 | label: 'Memory', 68 | data: memory, 69 | backgroundColor: [ 70 | '#B2CAB3', 71 | '#B8E8FC', 72 | '#EDC09E', 73 | '#9cb59d', 74 | '#FFCACA', 75 | '#D2DAFF', 76 | ], 77 | borderColor: '#9cb59d', 78 | fill: false, 79 | showLine: true, 80 | borderWidth: 1, 81 | }, 82 | ], 83 | }; 84 | 85 | const invocationState = { 86 | labels: singleTime, 87 | datasets: [ 88 | { 89 | label: 'Invocations', 90 | data: invocations, 91 | backgroundColor: [ 92 | '#B2CAB3', 93 | '#B8E8FC', 94 | '#EDC09E', 95 | '#9cb59d', 96 | '#FFCACA', 97 | '#D2DAFF', 98 | ], 99 | borderColor: '#9cb59d', 100 | fill: false, 101 | showLine: true, 102 | borderWidth: 1, 103 | }, 104 | ], 105 | }; 106 | 107 | return ( 108 |
    109 |

    110 | 168 |

    169 | 170 |

    171 | 230 |

    231 | 232 |

    233 | 292 |

    293 | 294 | {customGraphs 295 | ? customGraphs 296 | .filter((graph: any) => graph.functionName === functionName) 297 | .map((graph: any, index: number) => { 298 | // if datapoints array is empty, do not render or else it will break react 299 | if (graph.metricName === 'ColdStarts') { 300 | if ( 301 | graph.metricData === undefined || 302 | graph.metricData.length === 0 303 | ) { 304 | return null; 305 | } 306 | 307 | // if metric is cold starts, manually count the cold starts 308 | // sort the data by timestamp 309 | graph.metricData.sort((a: any, b: any) => 310 | a.timestamp.localeCompare(b.timestamp) 311 | ); 312 | const coldStartObj: any = {}; 313 | let coldStartDate = ''; 314 | for (let i = 0; i < graph.metricData.length; i++) { 315 | if (coldStartObj[graph.metricData[i]]) { 316 | coldStartObj[graph.metricData[i].timestamp.slice(-11)] += 1; 317 | } else { 318 | coldStartObj[graph.metricData[i].timestamp.slice(-11)] = 1; 319 | } 320 | coldStartDate = graph.metricData[i].timestamp.slice(0, 10); 321 | } 322 | const coldStarts = Object.values(coldStartObj); 323 | const coldStartTimes = Object.keys(coldStartObj); 324 | const coldStartTotal = coldStarts.reduce( 325 | (a: any, b: any) => a + b, 326 | 0 327 | ); 328 | const pluralizeColdStarts = 329 | coldStartTotal === 1 ? 'cold start' : 'cold starts'; 330 | 331 | const coldStartState = { 332 | labels: coldStartTimes, 333 | datasets: [ 334 | { 335 | label: 'Counts', 336 | data: coldStarts, 337 | backgroundColor: [ 338 | '#B2CAB3', 339 | '#B8E8FC', 340 | '#EDC09E', 341 | '#FDFDBD', 342 | '#9cb59d', 343 | '#FFCACA', 344 | '#D2DAFF', 345 | ], 346 | borderColor: '#9cb59d', 347 | fill: false, 348 | showLine: true, 349 | borderWidth: 1, 350 | }, 351 | ], 352 | }; 353 | 354 | return ( 355 |
    356 | 415 |
    416 | ); 417 | } 418 | if (graph.metricData.Datapoints.length === 0) { 419 | return null; 420 | } 421 | 422 | // extract the label and selected datapoint from the metric data 423 | const label = graph.metricData.Label; 424 | const datapoint = graph.datapointType; 425 | 426 | // sort the datapoints by time 427 | const sortedGraph = graph.metricData.Datapoints.sort( 428 | (a: any, b: any) => a.Timestamp.localeCompare(b.Timestamp) 429 | ); 430 | 431 | // extract time & data from each datapoint 432 | const date = graph.metricData.Datapoints[0].Timestamp.slice( 433 | 0, 434 | 10 435 | ); // grabs date from string 436 | const timestamps = sortedGraph.map((item: any) => 437 | item.Timestamp.slice(-11) 438 | ); // removes date from string 439 | const sums = sortedGraph.map((item: any) => item.Sum); 440 | const average = sortedGraph.map((item: any) => item.Average); 441 | const max = sortedGraph.map((item: any) => item.Maximum); 442 | const min = sortedGraph.map((item: any) => item.Minimum); 443 | const units = graph.metricData.Datapoints[0].Unit; 444 | 445 | // states for most graphs 446 | const state = { 447 | labels: timestamps, 448 | datasets: [ 449 | { 450 | label: units, 451 | backgroundColor: [ 452 | '#B2CAB3', 453 | '#B8E8FC', 454 | '#EDC09E', 455 | '#FFCACA', 456 | '#D2DAFF', 457 | ], 458 | borderWidth: 1, 459 | borderColor: '#B2CAB3', 460 | data: 461 | datapoint === 'Sum' 462 | ? sums 463 | : datapoint === 'Average' 464 | ? average 465 | : datapoint === 'Maximum' 466 | ? max 467 | : min, 468 | showLine: true, 469 | spanGaps: true, 470 | }, 471 | ], 472 | }; 473 | 474 | // MULTI LINE GRAPH STATE 475 | const multiState = { 476 | labels: timestamps, 477 | datasets: [ 478 | { 479 | label: 'Maximum', 480 | data: max, 481 | borderColor: '#B2CAB3', 482 | backgroundColor: '#B2CAB3', 483 | }, 484 | { 485 | label: 'Minimum', 486 | data: min, 487 | borderColor: '#B8E8FC', 488 | backgroundColor: '#B8E8FC', 489 | }, 490 | { 491 | label: 'Average', 492 | data: average, 493 | borderColor: '#EDC09E', 494 | backgroundColor: '#EDC09E', 495 | }, 496 | { 497 | label: 'Sum', 498 | data: sums, 499 | borderColor: '#D2DAFF', 500 | backgroundColor: '#D2DAFF', 501 | }, 502 | ], 503 | }; 504 | 505 | // if conditionals for each graph type 506 | if (graph.graphType === 'Bar') { 507 | return ( 508 |
    509 | 568 |
    569 | ); 570 | } 571 | 572 | if (graph.graphType === 'Line') { 573 | return ( 574 |
    575 | 636 |
    637 | ); 638 | } 639 | 640 | if (graph.graphType === 'Pie') { 641 | return ( 642 |
    643 | 686 |
    687 | ); 688 | } 689 | 690 | if (graph.graphType === 'MultiLine') { 691 | return ( 692 |
    693 | 754 |
    755 | ); 756 | } 757 | }) 758 | : null} 759 |
    760 | ); 761 | } 762 | 763 | export default GraphComponent; 764 | -------------------------------------------------------------------------------- /src/components/GraphLoader.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { PropagateLoader } from 'react-spinners'; 3 | import { MainPageContext } from '../context/MainPageContext'; 4 | 5 | const override: React.CSSProperties = { 6 | display: 'block', 7 | margin: '0 auto', 8 | borderColor: 'white', 9 | }; 10 | 11 | function GraphLoader() { 12 | const { loading } = React.useContext(MainPageContext); 13 | const [color, setColor] = React.useState('#ffffff'); 14 | 15 | return loading ? ( 16 |
    17 |
    18 | 26 |
    27 |
    28 | ) : null; 29 | } 30 | 31 | export default GraphLoader; 32 | -------------------------------------------------------------------------------- /src/components/Home.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | function Home() { 4 | return ( 5 |
    6 |
    7 | Let's get started! View your Lambda functions on the left. 8 |
    9 |
    10 | 14 |
    15 |
    16 | ); 17 | } 18 | 19 | export default Home; 20 | -------------------------------------------------------------------------------- /src/components/LambdaFuncComponent.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Button from '@mui/material/Button'; 3 | import List from '@mui/material/List'; 4 | import ListItem from '@mui/material/ListItem'; 5 | import ListItemButton from '@mui/material/ListItemButton'; 6 | import ListItemText from '@mui/material/ListItemText'; 7 | import ExpandLess from '@mui/icons-material/ExpandLess'; 8 | import ExpandMore from '@mui/icons-material/ExpandMore'; 9 | import { useGraphContext } from '../context/GraphContext'; 10 | import { useFunctionContext } from '../context/FunctionContext'; 11 | 12 | // pass down lambdaFuncList by declaring it as interface first 13 | interface Props { 14 | func: any; 15 | } 16 | 17 | const LambdaFuncComponent: React.FC = ({ func }) => { 18 | // when a function is clicked, set the function in context 19 | const { 20 | setFunctionName, 21 | setIsMetricsEnabled, 22 | setIsPricingEnabled, 23 | setIsHomeEnabled, 24 | setIsPermissionsEnabled, 25 | setShowPricing, 26 | setShowHistory, 27 | } = useFunctionContext(); 28 | const { setCreateGraphIsShown } = useGraphContext(); 29 | const [openOptions, setOpenOptions] = React.useState(false); 30 | const handleOpenOptions = () => { 31 | setOpenOptions(!openOptions); 32 | }; 33 | 34 | const handleMetricsClick = (funcName: string) => { 35 | setFunctionName?.(funcName); 36 | setIsMetricsEnabled?.(true); 37 | setIsPricingEnabled?.(false); 38 | setIsHomeEnabled?.(false); 39 | setCreateGraphIsShown?.(false); 40 | setIsPermissionsEnabled?.(false); 41 | }; 42 | const handlePricingClick = (funcName: string) => { 43 | setFunctionName?.(funcName); 44 | setIsPricingEnabled?.(true); 45 | setIsMetricsEnabled?.(false); 46 | setIsHomeEnabled?.(false); 47 | setCreateGraphIsShown?.(false); 48 | setIsPermissionsEnabled?.(false); 49 | setShowPricing?.(false); 50 | setShowHistory?.(false); 51 | }; 52 | 53 | const handlePermissionsClick = (funcName: string) => { 54 | setFunctionName?.(funcName); 55 | setIsPricingEnabled?.(false); 56 | setIsMetricsEnabled?.(false); 57 | setIsHomeEnabled?.(false); 58 | setCreateGraphIsShown?.(false); 59 | setIsPermissionsEnabled?.(true); 60 | }; 61 | 62 | return ( 63 |
    64 | 65 | 66 | 67 | 74 | {openOptions ? ( 75 | 76 | ) : ( 77 | 78 | )} 79 | 80 | 81 | 82 | 83 | {openOptions && ( 84 |
    85 | 107 | 130 | 153 |
    154 | )} 155 |
    156 | ); 157 | }; 158 | 159 | export default LambdaFuncComponent; 160 | -------------------------------------------------------------------------------- /src/components/LambdaFuncList.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import LambdaFuncComponent from './LambdaFuncComponent'; 3 | 4 | // pass down list of lambda functions 5 | interface Props { 6 | list: object[]; 7 | } 8 | 9 | const LambdaFuncList: React.FC = ({ list }) => ( 10 |
    11 | {list.map((func: any, index: number) => ( 12 | 13 | ))} 14 |
    15 | ); 16 | 17 | export default LambdaFuncList; 18 | -------------------------------------------------------------------------------- /src/components/PermissionsDetails.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { 3 | Box, 4 | Typography, 5 | styled, 6 | Button, 7 | FormControl, 8 | TextField, 9 | FormHelperText, 10 | Select, 11 | Tabs, 12 | Tab, 13 | } from '@mui/material'; 14 | import { useFunctionContext } from '../context/FunctionContext'; 15 | 16 | interface PermissionsDetailsProps { 17 | permissionList: any; 18 | setPermissionList: (arg0: any) => void; 19 | } 20 | 21 | interface TabPanelProps { 22 | children?: React.ReactNode; 23 | index: number; 24 | value: number; 25 | } 26 | 27 | const StyledTab = styled(Tab)({ 28 | '&.Mui-selected': { 29 | color: '#7f9f80', 30 | }, 31 | }); 32 | 33 | function TabPanel(props: TabPanelProps) { 34 | const { children, value, index, ...other } = props; 35 | 36 | return ( 37 | 50 | ); 51 | } 52 | 53 | function a11yProps(index: number) { 54 | return { 55 | id: `simple-tab-${index}`, 56 | 'aria-controls': `simple-tabpanel-${index}`, 57 | }; 58 | } 59 | 60 | export default function PermissionsDetails({ 61 | permissionList, 62 | setPermissionList, 63 | }: PermissionsDetailsProps) { 64 | // pull current function from context 65 | const { functionName, functionARN } = useFunctionContext(); 66 | const [value, setValue] = React.useState(0); 67 | const [statementId, setStatementId] = React.useState(''); 68 | const [action, setAction] = React.useState(''); 69 | const [principal, setPrincipal] = React.useState(''); 70 | const [principalOrgId, setPrincipalOrgId] = React.useState(''); 71 | const [errorText, setErrorText] = React.useState(false); 72 | const [successText, setSuccessText] = React.useState(false); 73 | 74 | const handleChange = (event: React.SyntheticEvent, newValue: number) => { 75 | setValue(newValue); 76 | }; 77 | 78 | const addPermission = () => { 79 | // set error text and success text to false to clear any previous helper texts 80 | setErrorText(false); 81 | setSuccessText(false); 82 | 83 | const body = { 84 | functionName, 85 | statementId, 86 | action, 87 | resource: functionARN, 88 | principal, 89 | principalOrgId, 90 | }; 91 | 92 | fetch('http://localhost:3000/permission/add', { 93 | method: 'POST', 94 | headers: { 95 | 'Content-Type': 'application/json', 96 | }, 97 | body: JSON.stringify(body), 98 | }) 99 | .then(async (res) => await res.json()) 100 | .then((data) => { 101 | if (data.status === 400) { 102 | // if error, display error message 103 | setErrorText(true); 104 | } else { 105 | // if successful, add the request body to the permissionList state 106 | setPermissionList([...permissionList, body]); 107 | // display success message 108 | setSuccessText(true); 109 | } 110 | }) 111 | .catch(() => { 112 | // if error, display error message 113 | setErrorText(true); 114 | }); 115 | }; 116 | 117 | const removePermission = (statementId: string, index: number) => { 118 | // user needs to confirm to remove permission 119 | const answer = confirm( 120 | `Are you sure you want to remove this permission?\n${statementId}` 121 | ); 122 | if (answer) { 123 | fetch('http://localhost:3000/permission/remove', { 124 | method: 'POST', 125 | headers: { 126 | 'Content-Type': 'application/json', 127 | }, 128 | body: JSON.stringify({ 129 | functionName, 130 | statementId, 131 | }), 132 | }) 133 | .then(async (res) => await res.json()) 134 | .then((data) => { 135 | console.log(data); 136 | }); 137 | // set a new array to state with the permission removed 138 | setPermissionList( 139 | permissionList.filter((permission: any, i: number) => i !== index) 140 | ); 141 | } 142 | }; 143 | 144 | return ( 145 |
    146 | 147 | 152 | 153 | 154 | 155 | 156 | 157 | {/* PERMISSIONS LIST */} 158 | 159 |

    160 | Viewing permissions for: 161 |

    162 |

    163 | {functionName} 164 |

    165 |
    166 | 167 | {permissionList && permissionList.length > 0 ? ( 168 |
    169 | {permissionList.map((permission: any, index: number) => ( 170 |
    171 |

    172 | Statement ID: {permission.statementId} 173 |

    174 |

    175 | Action: {permission.action} 176 |

    177 |

    178 | Resource: {permission.resource} 179 |

    180 |

    181 | Principal: {permission.principal} 182 |

    183 | {/* if principalOrgId exists, display it */} 184 | {permission.principalOrgId && ( 185 |

    186 | Principal Organization ID:{' '} 187 | {permission.principalOrgId} 188 |

    189 | )} 190 | 212 |
    213 | ))} 214 |
    215 | ) : ( 216 |

    217 | No permissions found. 218 |

    219 | )} 220 |
    221 | 222 | {/* ADD PERMISSIONS */} 223 | 224 |

    225 | Add new permission for: 226 |

    227 |

    228 | {functionName} 229 |

    230 |
    231 | 232 | { 240 | setStatementId(e.target.value); 241 | }} 242 | />{' '} 243 |
    244 | 245 | 300 | 301 | 302 | { 310 | setPrincipal(e.target.value); 311 | }} 312 | /> 313 | 314 | { 321 | setPrincipalOrgId(e.target.value); 322 | }} 323 | /> 324 | 325 | Optional: If principal is part of an AWS Organization, enter the 326 | organization ID. 327 | 328 |
    329 | 350 |
    351 | {errorText ? ( 352 | 353 | There was an error adding the permission. Please try again. 354 | 355 | ) : null} 356 | {successText ? ( 357 | 358 | Permission added successfully. 359 | 360 | ) : null} 361 |
    362 |
    363 |
    364 | ); 365 | } 366 | -------------------------------------------------------------------------------- /src/components/PriceLoader.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { BeatLoader } from 'react-spinners'; 3 | import { useMainPageContext } from '../context/MainPageContext'; 4 | 5 | const override: React.CSSProperties = { 6 | display: 'block', 7 | margin: '0 auto', 8 | borderColor: 'white', 9 | }; 10 | 11 | function PriceLoader() { 12 | const { priceLoading } = useMainPageContext(); 13 | const [color, setColor] = React.useState('#ffffff'); 14 | 15 | return priceLoading ? ( 16 |
    17 |
    18 | 26 |
    27 |
    28 | ) : null; 29 | } 30 | 31 | export default PriceLoader; 32 | -------------------------------------------------------------------------------- /src/components/PricingDetails.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { 3 | Box, 4 | Slider, 5 | Typography, 6 | styled, 7 | Button, 8 | Radio, 9 | FormControl, 10 | RadioGroup, 11 | FormControlLabel, 12 | TextField, 13 | Tabs, 14 | Tab, 15 | Stack, 16 | } from '@mui/material'; 17 | import { LocalizationProvider, DatePicker } from '@mui/x-date-pickers'; 18 | import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; 19 | import * as dayjs from 'dayjs'; 20 | import { useMainPageContext } from '../context/MainPageContext'; 21 | import PriceLoader from './PriceLoader'; 22 | import { useFunctionContext } from '../context/FunctionContext'; 23 | 24 | interface PricingDetailsProps { 25 | defaultFunctionConfig: any; 26 | } 27 | 28 | interface TabPanelProps { 29 | children?: React.ReactNode; 30 | index: number; 31 | value: number; 32 | } 33 | 34 | const StyledTab = styled(Tab)({ 35 | '&.Mui-selected': { 36 | color: '#7f9f80', 37 | }, 38 | }); 39 | 40 | function TabPanel(props: TabPanelProps) { 41 | const { children, value, index, ...other } = props; 42 | 43 | return ( 44 | 57 | ); 58 | } 59 | 60 | function a11yProps(index: number) { 61 | return { 62 | id: `simple-tab-${index}`, 63 | 'aria-controls': `simple-tabpanel-${index}`, 64 | }; 65 | } 66 | 67 | function PricingDetails({ defaultFunctionConfig }: PricingDetailsProps) { 68 | const { 69 | functionName, 70 | showPricing, 71 | setShowPricing, 72 | showHistory, 73 | setShowHistory, 74 | } = useFunctionContext(); 75 | const { priceLoading, setPriceLoading } = useMainPageContext(); 76 | const [type, setType] = React.useState(defaultFunctionConfig.type); 77 | const [memorySize, setMemorySize] = React.useState( 78 | defaultFunctionConfig.memorySize 79 | ); 80 | const [storage, setStorage] = React.useState(defaultFunctionConfig.storage); 81 | const [billedDurationAvg, setBilledDurationAvg] = React.useState(1); 82 | const [invocationsTotal, setInvocationsTotal] = React.useState(1); 83 | const [pricing, setPricing] = React.useState(0); 84 | const [value, setValue] = React.useState(0); 85 | const [date, setDate] = React.useState(new Date()); 86 | const [displayDate, setDisplayDate] = React.useState(''); 87 | const [priceHistory, setPriceHistory] = React.useState([]); 88 | // const [showHistory, setShowHistory] = React.useState(false); 89 | 90 | const handleChange = (event: React.SyntheticEvent, newValue: number) => { 91 | setValue(newValue); 92 | }; 93 | 94 | const handleCalcSubmit = (event: React.SyntheticEvent) => { 95 | event.preventDefault(); 96 | setShowPricing?.(false); 97 | // post request to backend for pricing calculator 98 | const body = { 99 | functionName, 100 | type, 101 | memorySize, 102 | storage, 103 | billedDurationAvg, 104 | invocationsTotal, 105 | }; 106 | fetch('http://localhost:3000/price/calc', { 107 | method: 'POST', 108 | headers: { 'Content-Type': 'application/json' }, 109 | body: JSON.stringify(body), 110 | }) 111 | .then(async (res) => await res.json()) 112 | .then((data) => { 113 | setPricing(data); 114 | setShowPricing?.(true); 115 | }) 116 | .catch((err) => { 117 | console.log('Error fetching pricing calc:', err); 118 | }); 119 | }; 120 | 121 | const handleHistorySubmit = (event: React.SyntheticEvent) => { 122 | event.preventDefault(); 123 | // turn off showing the price history if it was previously calculated 124 | setShowHistory?.(false); 125 | // turn on loading animation 126 | setPriceLoading?.(true); 127 | // post request to backend for pricing history 128 | setDisplayDate(dayjs(date).format('MMMM YYYY')); 129 | fetch('http://localhost:3000/price/history', { 130 | method: 'POST', 131 | headers: { 'Content-Type': 'application/json' }, 132 | body: JSON.stringify({ 133 | functionName, 134 | date: dayjs(date).format('YYYY/MM/'), 135 | }), 136 | }) 137 | .then(async (res) => await res.json()) 138 | .then((data) => { 139 | setPriceHistory(data); 140 | setPriceLoading?.(false); 141 | setShowHistory?.(true); 142 | }) 143 | .catch((err) => { 144 | console.log('Error fetching pricing history:', err); 145 | }); 146 | }; 147 | 148 | // convert returned number to currency 149 | const financial = (num: any) => new Intl.NumberFormat().format(num); 150 | 151 | return ( 152 |
    153 | {/* PRICING CALCULATOR */} 154 | 155 | 160 | 161 | 162 | 163 | 164 | 165 |

    166 | Viewing price calculator for: 167 |

    168 |

    169 | {functionName} 170 |

    171 |

    172 | Configure your function below to estimate how much it will cost you 173 | per month. 174 |
    175 | The default values are your function's current configuration. 176 |

    177 | 178 |
    179 | 180 | 181 | 182 | Type: 183 | { 189 | setType(e.target.value); 190 | }} 191 | > 192 | 202 | } 203 | label="Arm" 204 | /> 205 | 215 | } 216 | label="x86_64" 217 | /> 218 | 219 | 220 | 221 | 222 | Memory Size (MB): 223 | { 231 | setMemorySize(value as number); 232 | }} 233 | /> 234 | 235 | 236 | Storage Size (MB): 237 | { 245 | setStorage(value as number); 246 | }} 247 | /> 248 | 249 | 250 | Billed Duration: 251 | { 259 | setBilledDurationAvg(value as number); 260 | }} 261 | /> 262 | 263 | 264 | Total Invocations: 265 | { 273 | setInvocationsTotal(Number(e.target.value)); 274 | }} 275 | /> 276 | 277 | 278 |
    279 | 299 |

    300 | {showPricing && ( 301 |

    302 | {' '} 303 | ${financial(pricing)} 304 |

    305 | )} 306 |

    307 |
    308 |
    309 | 310 | {/* PRICING HISTORY */} 311 | 312 |

    313 | Viewing price history for: 314 |

    315 |

    316 | {functionName} 317 |

    318 |
    319 | 320 | 321 | 322 | Billed month: 323 | 324 | 325 | 326 | { 330 | setDate(newValue); 331 | }} 332 | renderInput={(params) => ( 333 | 334 | )} 335 | /> 336 | 337 | 338 |
    339 | 340 |
    341 | 361 |
    362 |
    363 |
    364 | 365 | {priceLoading ? ( 366 | 367 | ) : ( 368 | showHistory && ( 369 |
    370 |

    371 | Your total costs for 372 | {displayDate} were: 373 |

    374 |

    375 | ${financial(priceHistory)} 376 |

    377 |
    378 | ) 379 | )} 380 |
    381 |
    382 | ); 383 | } 384 | 385 | export default PricingDetails; 386 | -------------------------------------------------------------------------------- /src/components/RegionComponent.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Button from '@mui/material/Button'; 3 | import Menu from '@mui/material/Menu'; 4 | import MenuItem from '@mui/material/MenuItem'; 5 | import PublicIcon from '@mui/icons-material/Public'; 6 | import PopupState, { bindTrigger, bindMenu } from 'material-ui-popup-state'; 7 | 8 | interface RegionProps { 9 | currentRegion: string; 10 | setCurrentRegion: (region: string) => void; 11 | } 12 | 13 | export default function RegionComponent({ 14 | currentRegion, 15 | setCurrentRegion, 16 | }: RegionProps) { 17 | // list of AWS regions 18 | const awsRegions = [ 19 | 'us-west-1', 20 | 'us-west-2', 21 | 'us-east-1', 22 | 'us-east-2', 23 | 'af-south-1', 24 | 'ap-east-1', 25 | 'ap-south-1', 26 | 'ap-northeast-1', 27 | 'ap-northeast-2', 28 | 'ap-northeast-3', 29 | 'ap-southeast-1', 30 | 'ap-southeast-2', 31 | 'ca-central-1', 32 | 'cn-north-1', 33 | 'cn-northwest-1', 34 | 'eu-central-1', 35 | 'eu-west-1', 36 | 'eu-west-2', 37 | 'eu-west-3', 38 | 'eu-south-1', 39 | 'eu-north-1', 40 | 'me-south-1', 41 | 'sa-east-1', 42 | 'us-gov-east-1', 43 | 'us-gov-west-1', 44 | ]; 45 | 46 | const ITEM_HEIGHT = 48; 47 | 48 | // to capitalize the country code in the region 49 | const displayCapitalizedRegion = (region: string) => { 50 | const firstTwoLetters = region.slice(0, 2).toUpperCase(); 51 | const restOfRegion = region.slice(2); 52 | return firstTwoLetters + restOfRegion; 53 | }; 54 | 55 | const handleRegionClick = (region: any) => { 56 | fetch('http://localhost:3000/main/changeRegion', { 57 | method: 'POST', 58 | headers: { 59 | 'Content-Type': 'application/json', 60 | }, 61 | body: JSON.stringify({ region }), 62 | }) 63 | .then(async (response) => await response.json()) 64 | .then((data) => { 65 | if (data === 'region changed') { 66 | setCurrentRegion(region); 67 | } else alert(data); 68 | }) 69 | .catch((err) => { 70 | console.log('Error changing region:', err); 71 | }); 72 | }; 73 | 74 | return ( 75 | 76 | {(popupState) => ( 77 | <> 78 |
    79 | 104 |
    105 | 116 | {awsRegions.map((region) => ( 117 | { 119 | handleRegionClick(region); 120 | popupState.close(); 121 | }} 122 | value={region} 123 | > 124 | 125 | {displayCapitalizedRegion(region)} 126 | 127 | 128 | ))} 129 | 130 | 131 | )} 132 |
    133 | ); 134 | } 135 | -------------------------------------------------------------------------------- /src/components/UserComponent.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Button from '@mui/material/Button'; 3 | import AccessibilityIcon from '@mui/icons-material/Accessibility'; 4 | import PopupState, { bindTrigger } from 'material-ui-popup-state'; 5 | 6 | export default function UserComponent() { 7 | return ( 8 | 9 | {(popupState) => ( 10 |
    11 | 34 |
    35 | )} 36 |
    37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/components/charts/Bar.tsx: -------------------------------------------------------------------------------- 1 | // import * as React from 'react' 2 | // import { Chart as ChartJS, registerables } from 'chart.js' 3 | // import { Bar, Scatter, Pie, Line } from 'react-chartjs-2' 4 | // import { useGraphContext } from '../../context/GraphContext' 5 | 6 | // ChartJS.register(...registerables) 7 | 8 | // interface Props { 9 | // graphName: string, 10 | // chartState: any 11 | // } 12 | 13 | // const BarChart = ({ chartState, graphName }) => { 14 | // return 15 | // ( 16 | //

    17 | // 59 | //

    60 | // ) 61 | 62 | // } 63 | 64 | // export default BarChart; 65 | -------------------------------------------------------------------------------- /src/components/charts/DoubleLine.tsx: -------------------------------------------------------------------------------- 1 | // import * as React from 'react' 2 | // import { Chart as ChartJS, registerables } from 'chart.js' 3 | // import { Bar, Scatter, Pie, Line } from 'react-chartjs-2' 4 | // import { useGraphContext } from '../../context/GraphContext' 5 | 6 | // ChartJS.register(...registerables) 7 | 8 | // DOUBLE LINE GRAPH STATE 9 | // const multiState = { 10 | // labels: timestamps, 11 | // datasets: [ 12 | // { 13 | // label: 'Memory Used', 14 | // data: memory, 15 | // borderColor: '#B2CAB3', 16 | // backgroundColor: '#B2CAB3', 17 | // }, 18 | // { 19 | // label: 'Duration', 20 | // data: durations, 21 | // borderColor: '#B8E8FC', 22 | // backgroundColor: '#B8E8FC', 23 | // } 24 | // ] 25 | // } 26 | 27 | // DOUBLE LINE GRAPH 28 | //

    29 | // 72 | //

    73 | -------------------------------------------------------------------------------- /src/components/charts/Pie.tsx: -------------------------------------------------------------------------------- 1 | // import * as React from 'react' 2 | import { Chart as ChartJS, registerables } from 'chart.js'; 3 | // import { Bar, Scatter, Pie, Line } from 'react-chartjs-2' 4 | // import { useGraphContext } from '../../context/GraphContext' 5 | 6 | ChartJS.register(...registerables); 7 | 8 | /* 9 | 10 | const BarChart = ({ chartState, graphName }) => { 11 | return 12 | ( 13 |
    14 | 40 |
    41 | )} 42 | 43 | */ 44 | -------------------------------------------------------------------------------- /src/components/charts/ScatterPlot.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | // SCATTER PLOT 4 |

    5 | 30 |

    31 | 32 | // MULTI LINE GRAPH STATE 33 | const multiState = { 34 | labels: timestamps, 35 | datasets: [ 36 | { 37 | label: 'Maximum', 38 | data: memory, 39 | borderColor: '#B2CAB3', 40 | backgroundColor: '#B2CAB3', 41 | }, 42 | { 43 | label: 'Minimum', 44 | data: durations, 45 | borderColor: '#B8E8FC', 46 | backgroundColor: '#B8E8FC', 47 | }, 48 | { 49 | label: 'Average', 50 | data: durations, 51 | borderColor: '#B8E8FC', 52 | backgroundColor: '#B8E8FC', 53 | } 54 | ] 55 | } 56 | 57 | // DOUBLE LINE GRAPH 58 |

    59 | 102 |

    103 | 104 | */ 105 | -------------------------------------------------------------------------------- /src/container/SidebarContainer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Box from '@mui/material/Box'; 3 | import Drawer from '@mui/material/Drawer'; 4 | import List from '@mui/material/List'; 5 | import Divider from '@mui/material/Divider'; 6 | import ListItem from '@mui/material/ListItem'; 7 | import ListItemButton from '@mui/material/ListItemButton'; 8 | import ListItemText from '@mui/material/ListItemText'; 9 | import ExpandLess from '@mui/icons-material/ExpandLess'; 10 | import ExpandMore from '@mui/icons-material/ExpandMore'; 11 | import RegionComponent from '../components/RegionComponent'; 12 | import LambdaFuncList from '../components/LambdaFuncList'; 13 | import { useFunctionContext } from '../context/FunctionContext'; 14 | import { useGraphContext } from '../context/GraphContext'; 15 | 16 | const drawerWidth = 255; 17 | 18 | export default function SidebarContainer() { 19 | // opens the menu drawers on click & changes the color 20 | const [openMenu, setOpenMenu] = React.useState(false); 21 | const [currentRegion, setCurrentRegion] = React.useState('us-west-1'); 22 | const handleOpenMenu = () => { 23 | setOpenMenu(!openMenu); 24 | !openMenu 25 | ? document 26 | .querySelector('#list-select') 27 | ?.classList.add('bg-[#B2CAB3]', 'dark:bg-[#7f9f80]') 28 | : document 29 | .querySelector('#list-select') 30 | ?.classList.remove('dark:bg-[#7f9f80]', 'bg-[#B2CAB3]'); 31 | }; 32 | 33 | // home sends you to the home page 34 | const { 35 | setIsHomeEnabled, 36 | setIsPricingEnabled, 37 | setIsMetricsEnabled, 38 | setIsPermissionsEnabled, 39 | } = useFunctionContext(); 40 | const { setCreateGraphIsShown } = useGraphContext(); 41 | const handleHomeClick = () => { 42 | setIsHomeEnabled?.(true); 43 | setIsMetricsEnabled?.(false); 44 | setIsPricingEnabled?.(false); 45 | setCreateGraphIsShown?.(false); 46 | setIsPermissionsEnabled?.(false); 47 | }; 48 | 49 | // fetch list of lambda functions 50 | const [lambdaFuncList, setLambdaFuncList] = React.useState([]); 51 | React.useEffect(() => { 52 | fetch('http://localhost:3000/main/functions') 53 | .then(async (res) => await res.json()) 54 | .then((data) => { 55 | setLambdaFuncList(data); 56 | }) 57 | .catch((err) => { 58 | console.log('Error fetching lambda functions:', err); 59 | }); 60 | }, [currentRegion]); 61 | 62 | /* default region will be US-West-1 for now 63 | until there is a way to retrieve user's region based on their AWS CLI config 64 | it will auto-change region to US-West-1 on component mount */ 65 | React.useEffect(() => { 66 | fetch('http://localhost:3000/main/changeRegion', { 67 | method: 'POST', 68 | headers: { 69 | 'Content-Type': 'application/json', 70 | }, 71 | body: JSON.stringify({ region: currentRegion }), 72 | }) 73 | .then(async (response) => await response.json()) 74 | .then((data) => { 75 | if (data === 'region changed') setCurrentRegion(currentRegion); 76 | else alert('data'); 77 | }) 78 | .catch((err) => { 79 | console.log('Error changing region:', err); 80 | }); 81 | }, [currentRegion]); 82 | 83 | return ( 84 |
    85 | 86 | 100 |
    101 | 105 |
    106 | 107 | 108 | {['Home'].map((text, index) => ( 109 | 110 | 111 | 112 | 113 | 114 | ))} 115 | 116 | 117 | 118 | {['Your Lambda Functions'].map((text, index) => ( 119 | 120 | 121 | 127 | {openMenu ? : } 128 | 129 | 130 | ))} 131 | 132 | 133 | {openMenu && } 134 | 135 | 136 |
    137 |
    138 |
    139 | ); 140 | } 141 | -------------------------------------------------------------------------------- /src/container/TopBarContainer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Button from '@mui/material/Button'; 3 | import { DarkModeSwitch } from 'react-toggle-dark-mode'; 4 | import AddchartIcon from '@mui/icons-material/Addchart'; 5 | import { useDarkMode } from '../context/DarkModeHooks'; 6 | import { useGraphContext } from '../context/GraphContext'; 7 | import { useFunctionContext } from '../context/FunctionContext'; 8 | 9 | interface TopBarProps { 10 | changeMuiTheme: () => void; 11 | } 12 | 13 | function TopBarContainer({ changeMuiTheme }: TopBarProps) { 14 | const { isHomeEnabled } = useFunctionContext(); 15 | const [isDark, setIsDark] = useDarkMode(); 16 | const toggleDarkMode = (checked: boolean) => { 17 | setIsDark(checked); 18 | changeMuiTheme(); 19 | }; 20 | 21 | const { createGraphIsShown, setCreateGraphIsShown } = useGraphContext(); 22 | const handleCreateGraph = () => { 23 | setCreateGraphIsShown?.(!createGraphIsShown); 24 | }; 25 | 26 | const createGraphButton = () => ( 27 |
    28 | 52 |
    53 | ); 54 | 55 | const ghostIcon = () => ( 56 |
    57 | 61 |
    62 | ); 63 | 64 | return ( 65 |
    66 | {!isHomeEnabled ? createGraphButton() : null} 67 | 68 |
    69 | 89 |
    90 | 91 | {!isHomeEnabled ? ghostIcon() : null} 92 |
    93 | ); 94 | } 95 | 96 | export default TopBarContainer; 97 | -------------------------------------------------------------------------------- /src/container/mainContainer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import FunctionDetails from '../components/FunctionDetails'; 3 | import { useFunctionContext } from '../context/FunctionContext'; 4 | import Home from '../components/Home'; 5 | import { useGraphContext } from '../context/GraphContext'; 6 | import CreateGraph from '../components/CreateGraph'; 7 | import PricingDetails from '../components/PricingDetails'; 8 | import PermissionsDetails from '../components/PermissionsDetails'; 9 | 10 | function MainContainer() { 11 | const { 12 | isMetricsEnabled, 13 | isPricingEnabled, 14 | isHomeEnabled, 15 | isPermissionsEnabled, 16 | } = useFunctionContext(); 17 | const { createGraphIsShown } = useGraphContext(); 18 | const { functionName } = useFunctionContext(); 19 | 20 | const [defaultFunctionConfig, setDefaultFunctionConfig] = React.useState({}); 21 | const [permissionList, setPermissionList] = React.useState([]); 22 | 23 | React.useEffect(() => { 24 | Promise.all([ 25 | fetch('http://localhost:3000/price/defaultConfig', { 26 | method: 'POST', 27 | headers: { 28 | 'Content-Type': 'application/json', 29 | }, 30 | body: JSON.stringify({ functionName }), 31 | }), 32 | fetch('http://localhost:3000/permission/list', { 33 | method: 'POST', 34 | headers: { 35 | 'Content-Type': 'application/json', 36 | }, 37 | body: JSON.stringify({ functionName }), 38 | }), 39 | ]) 40 | .then( 41 | async ([defaultConfigResponse, permissionListResponse]) => 42 | await Promise.all([ 43 | defaultConfigResponse.json(), 44 | permissionListResponse.json(), 45 | ]) 46 | ) 47 | .then(([defaultConfig, permissionList]) => { 48 | setDefaultFunctionConfig(defaultConfig); 49 | setPermissionList(permissionList); 50 | }); 51 | }, [functionName]); 52 | 53 | return ( 54 |
    55 | {createGraphIsShown ? : null} 56 | {isMetricsEnabled ? : null} 57 | {isPricingEnabled ? ( 58 | 59 | ) : null} 60 | {isPermissionsEnabled ? ( 61 | 65 | ) : null} 66 | {isHomeEnabled ? : null} 67 |
    68 | ); 69 | } 70 | 71 | export default MainContainer; 72 | -------------------------------------------------------------------------------- /src/context/DarkModeHooks.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | export function usePrefersDarkMode() { 4 | const [value, setValue] = useState(true); 5 | 6 | useEffect(() => { 7 | const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); 8 | setValue(mediaQuery.matches); 9 | 10 | const handler = () => { 11 | setValue(mediaQuery.matches); 12 | }; 13 | mediaQuery.addEventListener('change', handler); 14 | return () => { 15 | mediaQuery.removeEventListener('change', handler); 16 | }; 17 | }, []); 18 | 19 | return value; 20 | } 21 | 22 | export function useSafeLocalStorage(key: string, initialValue: any) { 23 | const [valueProxy, setValueProxy] = useState(() => { 24 | try { 25 | const value = window.localStorage.getItem(key); 26 | return value ? JSON.parse(value) : initialValue; 27 | } catch { 28 | return initialValue; 29 | } 30 | }); 31 | 32 | const setValue = (value: any) => { 33 | try { 34 | window.localStorage.setItem(key, value); 35 | setValueProxy(value); 36 | } catch { 37 | setValueProxy(value); 38 | } 39 | }; 40 | 41 | return [valueProxy, setValue]; 42 | } 43 | 44 | export function useDarkMode() { 45 | const prefersDarkMode = usePrefersDarkMode(); 46 | const [isEnabled, setIsEnabled] = useSafeLocalStorage('dark-mode', undefined); 47 | 48 | const enabled = isEnabled === undefined ? prefersDarkMode : isEnabled; 49 | 50 | useEffect(() => { 51 | if (window === undefined) return; 52 | const root = window.document.documentElement; 53 | root.classList.remove(enabled ? 'light' : 'dark'); 54 | root.classList.add(enabled ? 'dark' : 'light'); 55 | }, [enabled]); 56 | 57 | return [enabled, setIsEnabled]; 58 | } 59 | -------------------------------------------------------------------------------- /src/context/FunctionContext.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | // declare data types for states and hooks being passed to context in an interface 4 | interface FunctionContextProps { 5 | functionName: string | undefined; 6 | streamName: string | undefined; 7 | functionARN: string | undefined; 8 | showPricing: boolean; 9 | showHistory: boolean; 10 | isMetricsEnabled: boolean; 11 | isPricingEnabled: boolean; 12 | isHomeEnabled: boolean; 13 | isPermissionsEnabled: boolean; 14 | setFunctionName?: (name: string) => void; 15 | setStreamName?: (name: string) => void; 16 | setFunctionARN?: (name: string) => void; 17 | setShowPricing?: (bool: boolean) => void; 18 | setShowHistory?: (bool: boolean) => void; 19 | setIsMetricsEnabled?: (isMetricsEnabled: boolean) => void; 20 | setIsPricingEnabled?: (isPricingEnabled: boolean) => void; 21 | setIsHomeEnabled?: (isHomeEnabled: boolean) => void; 22 | setIsPermissionsEnabled?: (isPermissionsEnabled: boolean) => void; 23 | 24 | children?: React.ReactNode; 25 | } 26 | 27 | // declare default values for states being passed to context 28 | const defaultState = { 29 | functionName: '', 30 | streamName: '', 31 | functionARN: '', 32 | showPricing: false, 33 | showHistory: false, 34 | isMetricsEnabled: false, 35 | isPricingEnabled: false, 36 | isHomeEnabled: true, 37 | isPermissionsEnabled: false, 38 | }; 39 | 40 | // use createContext to create a context object 41 | export const FunctionContext = 42 | React.createContext(defaultState); 43 | 44 | // create a provider component to wrap around components that need access to context 45 | // pass in children as props to provider component 46 | // children = all the components that need access to context 47 | function FunctionContextProvider({ children }: { children: React.ReactNode }) { 48 | const [functionName, setFunctionName] = React.useState(''); 49 | const [streamName, setStreamName] = React.useState(''); 50 | const [showPricing, setShowPricing] = React.useState(false); 51 | const [showHistory, setShowHistory] = React.useState(false); 52 | const [isMetricsEnabled, setIsMetricsEnabled] = React.useState(false); 53 | const [isPricingEnabled, setIsPricingEnabled] = React.useState(false); 54 | const [isHomeEnabled, setIsHomeEnabled] = React.useState(true); 55 | const [isPermissionsEnabled, setIsPermissionsEnabled] = React.useState(false); 56 | const [functionARN, setFunctionARN] = React.useState(''); 57 | 58 | return ( 59 | 81 | {children} 82 | 83 | ); 84 | } 85 | 86 | export const useFunctionContext = () => React.useContext(FunctionContext); 87 | 88 | export default FunctionContextProvider; 89 | -------------------------------------------------------------------------------- /src/context/GraphContext.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | // declare data types for states and hooks being passed to context in an interface 4 | interface GraphContextProps { 5 | createGraphIsShown: boolean; 6 | customGraphs: any; 7 | graphName: string; 8 | graphType: string; 9 | metricName: string; 10 | dataset1: string; 11 | dataset2: string; 12 | errors: string; 13 | concurrent: string; 14 | startTime: any; 15 | endTime: any; 16 | datapointType: string | null; 17 | defaultMetrics: any; 18 | 19 | setGraphName?: (name: string) => void; 20 | setGraphType?: (type: string) => void; 21 | setMetricName?: (metric: string) => void; 22 | setDataset1?: (data: string) => void; 23 | setDataset2?: (data: string) => void; 24 | setErrors?: (data: string) => void; 25 | setConcurrent?: (data: string) => void; 26 | setStartTime?: (data: any) => void; 27 | setEndTime?: (date: any) => void; 28 | setCreateGraphIsShown?: (value: boolean) => void; 29 | setCustomGraphs?: (value: any) => any | void; 30 | setDatapointType?: (value: any) => void; 31 | setDefaultMetrics?: (value: any) => void; 32 | 33 | children?: React.ReactNode; 34 | } 35 | 36 | // declare default values for states being passed to context 37 | const defaultState = { 38 | createGraphIsShown: false, 39 | graphName: '', 40 | graphType: '', 41 | metricName: '', 42 | dataset1: '', 43 | dataset2: '', 44 | errors: '', 45 | concurrent: '', 46 | startTime: '', 47 | endTime: '', 48 | customGraphs: [], 49 | datapointType: null, 50 | defaultMetrics: [], 51 | }; 52 | 53 | // use createContext to create a context object 54 | export const GraphContext = 55 | React.createContext(defaultState); 56 | 57 | // create a provider component to wrap around components that need access to context 58 | // pass in children as props to provider component 59 | // children = all the components that need access to context 60 | function GraphContextProvider({ children }: { children: React.ReactNode }) { 61 | const [graphName, setGraphName] = React.useState(''); 62 | const [graphType, setGraphType] = React.useState(''); 63 | const [dataset1, setDataset1] = React.useState(''); 64 | const [dataset2, setDataset2] = React.useState(''); 65 | const [errors, setErrors] = React.useState(''); 66 | const [concurrent, setConcurrent] = React.useState(''); 67 | const [startTime, setStartTime] = React.useState(''); 68 | const [endTime, setEndTime] = React.useState(''); 69 | const [createGraphIsShown, setCreateGraphIsShown] = React.useState(false); 70 | const [customGraphs, setCustomGraphs] = React.useState([]); 71 | const [metricName, setMetricName] = React.useState(''); 72 | const [datapointType, setDatapointType] = React.useState(null); 73 | const [defaultMetrics, setDefaultMetrics] = React.useState([]); 74 | 75 | return ( 76 | 106 | {children} 107 | 108 | ); 109 | } 110 | 111 | export const useGraphContext = () => React.useContext(GraphContext); 112 | 113 | export default GraphContextProvider; 114 | -------------------------------------------------------------------------------- /src/context/MainPageContext.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | // declare data types for states and hooks being passed to context in an interface 4 | interface MainPageContextProps { 5 | loading: boolean; 6 | priceLoading: boolean; 7 | createLoading: boolean; 8 | setLoading?: (loading: boolean) => void; 9 | setPriceLoading?: (priceLoading: boolean) => void; 10 | setCreateLoading?: (createLoading: boolean) => void; 11 | children?: React.ReactNode; 12 | } 13 | 14 | // declare default values for states being passed to context 15 | const defaultState = { 16 | loading: false, 17 | priceLoading: false, 18 | createLoading: false, 19 | }; 20 | 21 | // use createContext to create a context object 22 | export const MainPageContext = 23 | React.createContext(defaultState); 24 | 25 | // create a provider component to wrap around components that need access to context 26 | // pass in children as props to provider component 27 | // children = all the components that need access to context 28 | function MainPageContextProvider({ children }: { children: React.ReactNode }) { 29 | const [loading, setLoading] = React.useState(false); 30 | const [priceLoading, setPriceLoading] = React.useState(false); 31 | const [createLoading, setCreateLoading] = React.useState(false); 32 | 33 | return ( 34 | 44 | {children} 45 | 46 | ); 47 | } 48 | 49 | export const useMainPageContext = () => React.useContext(MainPageContext); 50 | 51 | export default MainPageContextProvider; 52 | -------------------------------------------------------------------------------- /src/electron.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development"). 3 | * This devtool is neither made for production nor for readable output files. 4 | * It uses "eval()" calls to create a separate source file in the browser devtools. 5 | * If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/) 6 | * or disable the default devtool with "devtool: false". 7 | * If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/). 8 | */ 9 | /** *** */ (() => { 10 | // webpackBootstrap 11 | /** *** */ const __webpack_modules__ = { 12 | /***/ './src/electron.ts': 13 | /*! *************************!*\ 14 | !*** ./src/electron.ts ***! 15 | \************************ */ 16 | /***/ ( 17 | __unused_webpack_module, 18 | __unused_webpack_exports, 19 | __webpack_require__ 20 | ) => { 21 | eval( 22 | "var _a = __webpack_require__(/*! electron */ \"electron\"), app = _a.app, BrowserWindow = _a.BrowserWindow;\nfunction createWindow() {\n // Create the browser window\n var win = new BrowserWindow({\n width: 800,\n height: 600,\n webPreferences: {\n nodeIntegration: true\n }\n });\n // and load the index.html of the app\n win.loadFile('index.html');\n}\napp.on('ready', createWindow);\n\n\n//# sourceURL=webpack://osp-aws-lambda-metrics/./src/electron.ts?" 23 | ); 24 | /***/ 25 | }, 26 | 27 | /***/ electron: 28 | /*! ***************************!*\ 29 | !*** external "electron" ***! 30 | \************************** */ 31 | /***/ (module) => { 32 | module.exports = require('electron'); 33 | /***/ 34 | }, 35 | 36 | /** *** */ 37 | }; 38 | /** ********************************************************************* */ 39 | /** *** */ // The module cache 40 | /** *** */ const __webpack_module_cache__ = {}; 41 | /** *** */ 42 | /** *** */ // The require function 43 | /** *** */ function __webpack_require__(moduleId) { 44 | /** *** */ // Check if module is in cache 45 | /** *** */ const cachedModule = __webpack_module_cache__[moduleId]; 46 | /** *** */ if (cachedModule !== undefined) { 47 | /** *** */ return cachedModule.exports; 48 | /** *** */ 49 | } 50 | /** *** */ // Create a new module (and put it into the cache) 51 | /** *** */ const module = (__webpack_module_cache__[moduleId] = { 52 | /** *** */ // no module.id needed 53 | /** *** */ // no module.loaded needed 54 | /** *** */ exports: {}, 55 | /** *** */ 56 | }); 57 | /** *** */ 58 | /** *** */ // Execute the module function 59 | /** *** */ __webpack_modules__[moduleId]( 60 | module, 61 | module.exports, 62 | __webpack_require__ 63 | ); 64 | /** *** */ 65 | /** *** */ // Return the exports of the module 66 | /** *** */ return module.exports; 67 | /** *** */ 68 | } 69 | /** *** */ 70 | /** ********************************************************************* */ 71 | /** *** */ 72 | /** *** */ // startup 73 | /** *** */ // Load entry module and return exports 74 | /** *** */ // This entry module can't be inlined because the eval devtool is used. 75 | /** *** */ const __webpack_exports__ = 76 | __webpack_require__('./src/electron.ts'); 77 | /** *** */ 78 | /** *** */ 79 | })(); 80 | -------------------------------------------------------------------------------- /src/electron.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow } from 'electron'; 2 | 3 | async function createWindow(): Promise { 4 | // Create the browser window 5 | const win = new BrowserWindow({ 6 | width: 1400, 7 | height: 850, 8 | webPreferences: { 9 | nodeIntegration: true, 10 | }, 11 | }); 12 | 13 | // and load the index.html of the app 14 | await win.loadFile('index.html'); 15 | } 16 | 17 | app.on('ready', () => { 18 | createWindow().catch((error) => { 19 | console.error('Failed to create window:', error); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/images/ghost.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ghost/d5100a0e49217b0a813de3e4c337cb9be7e2fd14/src/images/ghost.PNG -------------------------------------------------------------------------------- /src/images/metrics.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ghost/d5100a0e49217b0a813de3e4c337cb9be7e2fd14/src/images/metrics.gif -------------------------------------------------------------------------------- /src/images/permissions.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ghost/d5100a0e49217b0a813de3e4c337cb9be7e2fd14/src/images/permissions.gif -------------------------------------------------------------------------------- /src/images/pricing.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ghost/d5100a0e49217b0a813de3e4c337cb9be7e2fd14/src/images/pricing.gif -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | /* :root { 2 | @apply .light; 3 | } 4 | 5 | .dark { 6 | --color-bg-primary: #2d3748; 7 | --color-bg-secondary: #283141; 8 | --color-text-primary: #f7fafc; 9 | --color-text-secondary: #e2e8f0; 10 | --color-text-accent: #81e6d9; 11 | } 12 | 13 | .light { 14 | --color-bg-primary: #ffffff; 15 | --color-bg-secondary: #edf2f7; 16 | --color-text-primary: #2d3748; 17 | --color-text-secondary: #4a5568; 18 | --color-text-accent: #2b6cb0; 19 | } */ 20 | 21 | @tailwind base; 22 | @tailwind components; 23 | @tailwind utilities; 24 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ghost - an aws lambda metrics visualizer 8 | 9 | 10 |
    11 | 12 | 13 | -------------------------------------------------------------------------------- /src/react.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import MainContainer from './container/mainContainer'; 5 | import SidebarContainer from './container/SidebarContainer'; 6 | import FunctionContextProvider from './context/FunctionContext'; 7 | import GraphContextProvider from './context/GraphContext'; 8 | import TopBarContainer from './container/TopBarContainer'; 9 | import useMediaQuery from '@mui/material/useMediaQuery'; 10 | import { ThemeProvider, createTheme } from '@mui/material/styles'; 11 | import { LocalizationProvider } from '@mui/x-date-pickers'; 12 | import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; 13 | import MainPageContextProvider from './context/MainPageContext'; 14 | 15 | const light: any = { 16 | palette: { 17 | mode: 'light', 18 | text: { 19 | primary: '#000000', 20 | }, 21 | primary: { 22 | main: '#F5F5F5', 23 | }, 24 | secondary: { 25 | main: '#e6e6e6', 26 | }, 27 | button: '#9cb59d', 28 | }, 29 | }; 30 | 31 | const dark: any = { 32 | palette: { 33 | mode: 'dark', 34 | text: { 35 | primary: '#F5F5F5', 36 | }, 37 | primary: { 38 | main: '#242424', 39 | }, 40 | secondary: { 41 | main: '#636262', 42 | }, 43 | button: '#7f9f80', 44 | }, 45 | }; 46 | 47 | const App = () => { 48 | const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)'); 49 | 50 | const [isDarkMui, setIsDarkMui] = React.useState(prefersDarkMode); // this should hopefully work 51 | const changeMuiTheme = () => { 52 | setIsDarkMui(!isDarkMui); 53 | }; 54 | 55 | return ( 56 |
    57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 |
    71 | ); 72 | }; 73 | 74 | ReactDOM.render(, document.getElementById('app')); 75 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: 'class', 4 | content: ['./src/index.html', './src/**/*.{js,ts,jsx,tsx}'], 5 | theme: { 6 | extend: { 7 | colors: { 8 | 'light-green': '#A6D6A8', 9 | gray1: '#D9D9D9', 10 | gray2: '#828282', 11 | }, 12 | keyframes: { 13 | 'bounce-ghost': {}, 14 | wiggle: { 15 | '0%, 100%': { transform: 'rotate(-3deg) translateY(-15%)' }, 16 | '50%': { transform: 'rotate(3deg) translateY(0)' }, 17 | }, 18 | }, 19 | }, 20 | }, 21 | plugins: [], 22 | }; 23 | 24 | // green '#B2CAB3', 25 | // darker green '#9cb59d', 26 | // blue '#B8E8FC', 27 | // orange '#EDC09E', 28 | // yellow '#FDFDBD', 29 | // pink '#FFCACA', 30 | // purple '#D2DAFF' 31 | // bg gray EBEBEB 32 | // lighter gray F5F5F5 33 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "outDir": "./dist/", 5 | "noImplicitAny": true, 6 | "module": "es6", 7 | "target": "es5", 8 | "allowJs": true, 9 | "moduleResolution": "node", 10 | "lib": ["es6", "dom"], 11 | "noImplicitThis": true, 12 | "strictNullChecks": true 13 | }, 14 | "include": ["src/**/*.ts", "src/**/*.tsx", "./custom.d.ts"], 15 | "exclude": ["/node_modules"] 16 | } 17 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | // webpack.config.js 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | 4 | module.exports = [ 5 | { 6 | mode: 'development', 7 | entry: './src/electron.ts', 8 | target: 'electron-main', 9 | module: { 10 | rules: [ 11 | { 12 | test: /\.ts$/, 13 | include: /src/, 14 | exclude: /node_modules/, 15 | use: [{ loader: 'ts-loader' }], 16 | }, 17 | ], 18 | }, 19 | resolve: { 20 | extensions: ['.ts', '.js', '.tsx'], 21 | }, 22 | output: { 23 | path: `${__dirname}/dist`, 24 | filename: 'electron.js', 25 | }, 26 | }, 27 | { 28 | mode: 'development', 29 | entry: './src/react.tsx', 30 | target: 'electron-renderer', 31 | devtool: 'source-map', 32 | module: { 33 | rules: [ 34 | { 35 | test: /\.ts(x?)$/, 36 | include: /src/, 37 | exclude: /node_modules/, 38 | use: [{ loader: 'ts-loader' }], 39 | }, 40 | { 41 | test: /css$/, 42 | exclude: /node_modules/, 43 | use: ['style-loader', 'css-loader', 'postcss-loader'], 44 | }, 45 | { 46 | test: /\.(png|jpg|jpeg|gif)$/i, 47 | type: 'asset/resource', 48 | use: [{ loader: 'file-loader' }], 49 | }, 50 | ], 51 | }, 52 | resolve: { 53 | extensions: ['.tsx', '.ts', '.js'], 54 | }, 55 | output: { 56 | path: `${__dirname}/dist`, 57 | filename: 'react.js', 58 | }, 59 | plugins: [ 60 | new HtmlWebpackPlugin({ 61 | template: './src/index.html', 62 | }), 63 | ], 64 | }, 65 | ]; 66 | --------------------------------------------------------------------------------