├── .DS_Store ├── .babelrc ├── .eslintignore ├── .eslintrc.json ├── .github └── pull_request_template.md ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .prettierignore ├── .prettierrc.json ├── .travis-test.yml ├── .travis.yml ├── .vscode └── settings.json ├── @types ├── Image.d.ts ├── Interfaces.d.ts ├── dashboard.d.ts ├── project.d.ts ├── projectQuery.d.ts ├── rateLimiter.d.ts └── user.d.ts ├── Dockerfile ├── Dockerfile-dev ├── Dockerrun.aws.json ├── LICENSE ├── README.md ├── client ├── App.tsx ├── auth │ └── AuthProvider.tsx ├── components │ ├── ChartBox.tsx │ ├── Dashboard.tsx │ ├── Footer.tsx │ ├── Form.tsx │ ├── Loading.tsx │ ├── Login.tsx │ ├── Navbar.tsx │ ├── ProjectItem.tsx │ ├── ProjectView.tsx │ ├── ProjectsPane.tsx │ ├── Queries.tsx │ ├── Query.tsx │ ├── RequireAuth.tsx │ ├── SettingsPane.tsx │ ├── Signup.tsx │ ├── Team.tsx │ └── ToolBar.tsx └── index.tsx ├── docker-compose-dev-hot.yml ├── docker-compose.yml ├── package-lock.json ├── package.json ├── public ├── barchart.png ├── bulboff.png ├── bulbon.png ├── code-snippet.png ├── data.png ├── demogif3.gif ├── favicon.ico ├── gear.png ├── index.html ├── intime.png ├── logo.png ├── settings.png └── styles.css ├── scripts ├── databaseMockQueries.js └── deploy.sh ├── server ├── db.ts ├── index.ts ├── models │ ├── Project.ts │ ├── Query.ts │ └── User.ts ├── schema │ ├── ProjectQueryResolvers.ts │ ├── ProjectResolvers.ts │ ├── RateLimiterConfigResolvers.ts │ ├── TypeDefs.ts │ ├── UserResolvers.ts │ └── errors.ts └── utilities │ ├── RedisMock.js │ ├── rateLimiterAnalysis.ts │ ├── sessions.ts │ └── workerThread.js ├── template.env ├── tsconfig.json └── webpack.config.cjs /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/GraphQL-Gateway/9bd08ec3c3f0f4032dff30820c6de5bcaf0e6ecc/.DS_Store -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | .vscode/* 3 | scripts 4 | build 5 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es2021": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "airbnb", 10 | "airbnb-typescript", 11 | "plugin:@typescript-eslint/recommended", 12 | "plugin:import/typescript", 13 | "prettier" 14 | ], 15 | "ignorePatterns": [ 16 | "webpack.config.cjs" 17 | ], 18 | "parser": "@typescript-eslint/parser", 19 | "parserOptions": { 20 | "ecmaVersion": "latest", 21 | "project": "tsconfig.json" 22 | }, 23 | "plugins": [ 24 | "@typescript-eslint", 25 | "prettier" 26 | ], 27 | "rules": { 28 | "prettier/prettier": [ 29 | "error" 30 | ] 31 | } 32 | } -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Summary 2 | 3 | _Provide a short summary of the changes in this PR_ 4 | 5 | ### Type of Change 6 | 7 | Please delete options that are not relevant. 8 | 9 | - [ ] Bug fix (non-breaking change which fixes an issue) 10 | - [ ] New feature (non-breaking change which adds functionality) 11 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 12 | - [ ] This change requires a documentation update 13 | 14 | ### Issues 15 | 16 | - Link any issues this PR resolves using keywords (resolve, closes, fixed) 17 | 18 | ### Evidence 19 | 20 | - Provide evidence of the the changes functioning as expected or describe your tests. If tests are included in the CI pipeline this may be omitted. 21 | 22 | _(delete this line)_ Prior to submitting the PR assign a reviewer from each team to review this PR. 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | build/ 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # TypeScript v1 declaration files 46 | typings/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Microbundle cache 58 | .rpt2_cache/ 59 | .rts2_cache_cjs/ 60 | .rts2_cache_es/ 61 | .rts2_cache_umd/ 62 | 63 | # Optional REPL history 64 | .node_repl_history 65 | 66 | # Output of 'npm pack' 67 | *.tgz 68 | 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | 72 | # dotenv environment variables file 73 | .env 74 | .env.test 75 | 76 | # parcel-bundler cache (https://parceljs.org/) 77 | .cache 78 | 79 | # Next.js build output 80 | .next 81 | 82 | # Nuxt.js build / generate output 83 | .nuxt 84 | dist 85 | 86 | # Gatsby files 87 | .cache/ 88 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 89 | # https://nextjs.org/blog/next-9-1#public-directory-support 90 | # public 91 | 92 | # vuepress build output 93 | .vuepress/dist 94 | 95 | # Serverless directories 96 | .serverless/ 97 | 98 | # FuseBox cache 99 | .fusebox/ 100 | 101 | # DynamoDB Local files 102 | .dynamodb/ 103 | 104 | # TernJS port file 105 | .tern-port 106 | 107 | build 108 | ts-build 109 | 110 | gateway.zip -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | # Runs all code quality tools prior to each commit. 5 | npx lint-staged 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | .vscode/* -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 4, 4 | "semi": true, 5 | "singleQuote": true, 6 | "printWidth": 100, 7 | "bracketSpacing": true, 8 | "arrowParens": "always", 9 | "proseWrap": "never" 10 | } 11 | -------------------------------------------------------------------------------- /.travis-test.yml: -------------------------------------------------------------------------------- 1 | # FOR DEPLOYMENT TESTING PURPOSES 2 | 3 | # Make a draft PR from feature branch to deploy branch 4 | # Every commit will run necessary scripts 5 | 6 | language: node_js 7 | 8 | node_js: 9 | - 16 10 | 11 | branches: 12 | only: 13 | - dev 14 | - main 15 | - deploy 16 | 17 | script: 18 | - 'npm run lint' 19 | - python3 -VV 20 | - pip install --upgrade pip 21 | - pip -V 22 | 23 | # install the aws cli 24 | - python3 -m pip install --user awscli 25 | # install the elastic beanstalk cli 26 | - python3 -m pip install --user awsebcli 27 | # Append exe location to our PATH 28 | - export PATH=$PATH:$HOME/.local/bin 29 | - sh $TRAVIS_BUILD_DIR/scripts/deploy.sh 30 | 31 | env: PATH=/opt/python/3.7.1/bin:$PATH 32 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 16 5 | 6 | branches: 7 | only: 8 | - dev 9 | - main 10 | 11 | script: 12 | - 'npm run lint' 13 | - 'npm run build' 14 | - python3 -VV 15 | - pip install --upgrade pip 16 | - pip -V 17 | 18 | before_deploy: 19 | # install the aws cli 20 | - python3 -m pip install --user awscli 21 | # install the elastic beanstalk cli 22 | - python3 -m pip install --user awsebcli 23 | # Append exe location to our PATH 24 | - export PATH=$PATH:$HOME/.local/bin 25 | 26 | env: PATH=/opt/python/3.7.1/bin:$PATH 27 | deploy: 28 | provider: script 29 | skip_cleanup: true 30 | on: 31 | branch: main 32 | script: sh $TRAVIS_BUILD_DIR/scripts/deploy.sh 33 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [], 3 | "settings": {}, 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll.eslint": true 6 | }, 7 | "editor.formatOnSave": true 8 | } 9 | -------------------------------------------------------------------------------- /@types/Image.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png' { 2 | const value: any; 3 | export default value; 4 | } 5 | 6 | declare module '*.jpg'; 7 | declare module '*.gif'; 8 | -------------------------------------------------------------------------------- /@types/Interfaces.d.ts: -------------------------------------------------------------------------------- 1 | // interface Data { 2 | // user: User; 3 | // } 4 | 5 | // interface Projects { 6 | // projects: [Project] | undefined; 7 | // } 8 | // interface SelectedProject { 9 | // project: Project | undefined; 10 | // } 11 | // These were added in 12 | // export interface Project { 13 | // id: string; 14 | // userID: string; 15 | // name: string; 16 | // apiKey: string; 17 | // } 18 | // export interface User { 19 | // id: string; 20 | // email: string; 21 | // password: string; 22 | // } 23 | 24 | // export interface Data { 25 | // user: User; 26 | // } 27 | 28 | // export interface Projects { 29 | // projects: [Project] | undefined; 30 | // } 31 | // export interface SelectedProject { 32 | // project: Project | undefined; 33 | // } 34 | -------------------------------------------------------------------------------- /@types/dashboard.d.ts: -------------------------------------------------------------------------------- 1 | type SortOrder = '↑' | '↓' | ''; 2 | 3 | interface SeriesData { 4 | readonly name: string; 5 | data: number[]; 6 | } 7 | interface ChartData { 8 | options: { 9 | readonly chart: { 10 | id: string; 11 | }; 12 | xaxis: { 13 | categories: string[]; 14 | }; 15 | }; 16 | series: SeriesData[]; 17 | } 18 | 19 | type ChartSelectionDays = 1 | 7 | 30 | 365; 20 | interface ProjectPaneProps { 21 | projects: Project[] | undefined; 22 | setSelectedProject: React.Dispatch>; 23 | projectLoading: boolean; 24 | getUserData: any; 25 | } 26 | 27 | interface SettingsPaneProps { 28 | rateLimiterConfig: RateLimiterConfig; 29 | rateLimiterLoading: boolean; 30 | setRateLimiterConfig: (config: RateLimiterConfig, saveConfig: boolean) => void; 31 | onRawQueriesClick: () => void; 32 | showSettings: boolean; 33 | } 34 | 35 | type ToolbarProps = ProjePaneProps & SettingsPaneProps; 36 | -------------------------------------------------------------------------------- /@types/project.d.ts: -------------------------------------------------------------------------------- 1 | type Project = { 2 | id: string; 3 | userID: string; 4 | name: string; 5 | apiKey: string; 6 | queries: Array; 7 | rateLimiterConfig: RateLimiterConfig; 8 | }; 9 | 10 | // export interface Project { 11 | // id: string; 12 | // userID: string; 13 | // name: string; 14 | // apiKey: string; 15 | // } 16 | 17 | // interface Project { 18 | // id: string; 19 | // userID: string; 20 | // name: string; 21 | // apiKey: string; 22 | // rateLimiterConfig: import('./dashboard').RateLimiterConfig; 23 | // // queries: [ProjectQuery]; 24 | // // query: ProjectQuery; // FIXME: is this ever used? 25 | // } 26 | 27 | type CreateProjectArgs = { 28 | project: { 29 | name: string; 30 | userID: string; 31 | }; 32 | }; 33 | 34 | type UpdateProjectArgs = { 35 | // project: { 36 | id: string; 37 | name?: string; 38 | rateLimiterConfig?: RateLimiterUpdateArgs; 39 | // }; 40 | }; 41 | -------------------------------------------------------------------------------- /@types/projectQuery.d.ts: -------------------------------------------------------------------------------- 1 | type ProjectQuery = { 2 | id: string; 3 | userID: string; // FIXME: Why do we need this? gql specific 4 | requestUuid: string; 5 | projectID: string; 6 | number: number; 7 | depth: number; 8 | complexity: number; 9 | tokens: number; 10 | success: boolean; 11 | timestamp: number; 12 | loggedOn: number; // gql specific 13 | latency?: number; 14 | }; 15 | 16 | // interface ProjectQuery { 17 | // id: string; 18 | // number: number; 19 | // projectID: string; 20 | // depth: number; 21 | // complexity: number; 22 | // timestamp: number; 23 | // tokens: number; 24 | // // latency: number; 25 | // success: boolean; 26 | // requestId: string; 27 | // } 28 | 29 | type CreateProjectQueryArgs = { 30 | projectQuery: { 31 | projectID: string; 32 | number: string; 33 | complexity: number; 34 | depth: number; 35 | tokens: number; 36 | success: boolean; 37 | timestamp: number; 38 | loggedOn: number; 39 | latency?: number; 40 | }; 41 | }; 42 | 43 | type QueryByID = { 44 | id: string; 45 | minDate: number; 46 | maxDate: number; 47 | }; 48 | -------------------------------------------------------------------------------- /@types/rateLimiter.d.ts: -------------------------------------------------------------------------------- 1 | type BucketType = 'TOKEN_BUCKET' | 'LEAKY_BUCKET'; 2 | 3 | type WindowType = 'FIXED_WINDOW' | 'SLIDING_WINDOW_LOG' | 'SLIDING_WINDOW_COUNTER'; 4 | 5 | type RateLimiterType = BucketType | WindowType; 6 | 7 | // interface Options { 8 | // capacity: number; 9 | // } 10 | 11 | // interface WindowOptions extends Options { 12 | // windowSize: number; 13 | // } 14 | 15 | // interface BucketOptions extends Options { 16 | // refillRate: number; 17 | // } 18 | 19 | // type BucketRateLimiter = { 20 | // type: BucketType; 21 | // options: BucketOptions; 22 | // }; 23 | 24 | // type WindowRateLimiter = { 25 | // type: WindowType; 26 | // options: WindowOptions; 27 | // }; 28 | 29 | // type RateLimiterConfig = WindowRateLimiter | BucketRateLimiter; 30 | 31 | type RateLimiterConfig = { 32 | type: RateLimiterType; 33 | options: { 34 | capacity: number; 35 | windowSize?: number; 36 | refillRate?: number; 37 | }; 38 | }; 39 | -------------------------------------------------------------------------------- /@types/user.d.ts: -------------------------------------------------------------------------------- 1 | interface AuthUser { 2 | id: string; 3 | email: string; 4 | password: string; 5 | token: string; 6 | } 7 | 8 | interface User extends AuthUser { 9 | projects: Array; 10 | } 11 | // interface User { 12 | // id: string; 13 | // email: string; 14 | // password: string; 15 | // projects: [Project]; 16 | // project: Project; // FIXME: iS this ever used? 17 | // } 18 | 19 | interface GetUserArgs { 20 | user: { 21 | email: string; 22 | password: string; 23 | }; 24 | } 25 | 26 | interface CreateUserArgs { 27 | user: { email: string; password: string; projects: Array }; 28 | } 29 | 30 | interface UpdateUserArgs { 31 | user: { 32 | id: number; 33 | email: string; 34 | password: string; 35 | }; 36 | } 37 | 38 | interface Context { 39 | authenticated: boolean; 40 | user: null | string | JwtPayload; 41 | } 42 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16.14 2 | 3 | RUN npm i webpack -g 4 | 5 | RUN npm i typescript -g 6 | 7 | WORKDIR /usr/src/app 8 | 9 | COPY package.json . 10 | 11 | RUN npm i 12 | 13 | COPY . . 14 | 15 | RUN npm run build 16 | 17 | RUN tsc 18 | 19 | RUN cd ts-build 20 | 21 | RUN rm -rf client/ 22 | 23 | EXPOSE 3000 24 | 25 | CMD ["npm", "run", "start:docker"] -------------------------------------------------------------------------------- /Dockerfile-dev: -------------------------------------------------------------------------------- 1 | FROM node:16.14 2 | 3 | RUN npm i -g webpack 4 | 5 | RUN npm i -g ts-node 6 | 7 | WORKDIR /usr/src/app/ 8 | 9 | COPY package*.json /usr/src/app/ 10 | 11 | RUN npm i 12 | 13 | EXPOSE 3000 -------------------------------------------------------------------------------- /Dockerrun.aws.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSEBDockerrunVersion": "1", 3 | "Image": { 4 | "Name": "346649815440.dkr.ecr.us-west-1.amazonaws.com/gateway:", 5 | "Update": "true" 6 | }, 7 | "Ports": [ 8 | { 9 | "ContainerPort": "3000" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 OSLabs Beta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

GraphQL Gateway Developer Portal

4 | GitHub stars GitHub issues GitHub last commit 5 |
6 |
7 |   8 | 9 | ## 🛡 GraphQL Gateway Developer Portal 10 | 11 | GGDP is designed for offering a visualization of how your GraphQL API endpoints is secured using rate limits and depth limits. With this tool, users can: 12 | 13 | - Visualize API call data and facilitate a tuning of rate limiting algorithm settings 14 | - Seek for query resolution optimizations for GraphQL APIs 15 | - View cached performance metrics 16 | 17 | ## ✏️ Table of Contents 18 | 19 | - [Prerequisites](#prerequisites) 20 | - [Getting Started](#getting-started) 21 | - [Contributions](#contributions) 22 | - [Developers](#developers) 23 | - [More Information](#for-more-information) 24 | 25 | ## 📖 Prerequisites 26 | 27 | 1. Signup/login to the [Gateway developer portal](graphqlgate.io). 28 | 29 | 2. Create a new project to recieve a project ID and API key. 30 | 31 | 3. Import and configure the [GraphQLGate logger package](https://www.npmjs.com/package/gate-logger) 32 | 33 | ``` 34 | npm i gate-logger 35 | ``` 36 | 37 | 4. Import and configure the [GraphQLGate rate-limiting package](https://www.npmjs.com/package/graphql-limiter) 38 | 39 | ``` 40 | npm i graphql-limiter 41 | ``` 42 | 43 | ## 📍 Getting Started 44 | 45 | - Register if you are first-time user, otherwise, login to the portal with your email and password 46 |
47 | 48 | - Select your existing project or create a new project from the toolbar on your left 49 |
50 | 51 | - Use features on the chart and view your cached performance metrics sorted by time periods/algorithms 52 |
53 | 54 | ## 🧠 Contributions 55 | 56 | Contributions to the code, examples, documentation, etc. are very much appreciated🧑‍💻👩‍💻 57 | 58 | - Please report issues and bugs directly in this [GitHub project](https://github.com/oslabs-beta/GraphQL-Gateway/issues). 59 | 60 | ## 💻 Developers 61 | 62 | - [Evan McNeely](https://github.com/evanmcneely) 63 | - [Stephan Halarewicz](https://github.com/shalarewicz) 64 | - [Flora Yufei Wu](https://github.com/feiw101) 65 | - [Jon Dewey](https://github.com/donjewey) 66 | - [Milos Popovic](https://github.com/milos381) 67 | 68 | ## 🔍 For More Information 69 | 70 | GraphQLGate rate-limiting and Logger package documentation: 71 | 72 | - [GraphQLGate rate-limiting package](https://github.com/oslabs-beta/GraphQL-Gate) 73 | - [GraphQLGate logger package](https://github.com/oslabs-beta/Gate-Logger) 74 | -------------------------------------------------------------------------------- /client/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import settings from '../public/settings.png'; 3 | import intime from '../public/intime.png'; 4 | import data from '../public/data.png'; 5 | import barchart from '../public/barchart.png'; 6 | import snippet from '../public/code-snippet.png'; 7 | import logo from '../public/logo.png'; 8 | import project from '../public/demogif3.gif'; 9 | import '../public/styles.css'; 10 | 11 | export default function HomePage() { 12 | const [loopNum, setLoopNum] = useState(0); 13 | const [isDeleting, setIsDeleting] = useState(false); 14 | const [text, setText] = useState(''); 15 | const toRotate = ['Developer Portal']; 16 | const [isCopied, setIsCopied] = useState(false); 17 | 18 | const tick = () => { 19 | const i = loopNum % toRotate.length; 20 | const fullText = toRotate[i]; 21 | const updatedText = isDeleting 22 | ? fullText.substring(0, text.length - 1) 23 | : fullText.substring(0, text.length + 1); 24 | 25 | setText(updatedText); 26 | 27 | if (!isDeleting && updatedText === fullText) { 28 | setIsDeleting(true); 29 | } else if (isDeleting && updatedText === '') { 30 | setIsDeleting(false); 31 | setLoopNum(loopNum + 1); 32 | } 33 | }; 34 | 35 | useEffect(() => { 36 | const ticker = setInterval(() => { 37 | tick(); 38 | }, 400); 39 | 40 | return () => { 41 | clearInterval(ticker); 42 | }; 43 | }, [text]); 44 | 45 | const code = ` 46 | // import package 47 | import gateLogger from 'gate-logger'; 48 | import expressGraphQLRateLimiter from 'graphqlgate'; 49 | 50 | /** 51 | * Import other dependencies 52 | * */ 53 | 54 | //Add the logger middleware into your GraphQL middleware chain 55 | app.use('gql', gateLogger(/*PROJECT ID*/, /*API KEY*/)); 56 | 57 | //Add the rate limiting middleware 58 | app.use( 59 | 'gql', 60 | expressGraphQLRateLimiter(schemaObject, { 61 | rateLimiter: { 62 | type: 'TOKEN_BUCKET', 63 | refillRate: 10, 64 | capacity: 100, 65 | }, 66 | }) /** add GraphQL server here*/ 67 | ) 68 | `; 69 | 70 | function copy(someText: string) { 71 | navigator.clipboard.writeText(someText); 72 | setIsCopied(true); 73 | } 74 | useEffect(() => { 75 | setTimeout(() => { 76 | if (isCopied) setIsCopied(false); 77 | }, 2000); 78 | }); 79 | 80 | return ( 81 |
82 |
83 |
84 |
85 |

GraphQLGate

86 |

87 | 91 | {text} 92 | 93 |

94 |

95 | An Open Source GraphQL rate-limiter with query complexity analysis for 96 | Node.js and Express. Estimate the upper bound of the response size 97 | before resolving the query and optionally log query data to our Gateway 98 | Developer Portal to monitor and tune rate limiting settings. 99 |

100 | 112 |
113 |
114 | {logo} 115 |
116 |
117 |
118 |
119 |

120 | Code Snippet 121 | 124 |

125 | settings 126 |
127 | ./project.gif 128 |
129 |
130 |

Core Capabilities

131 |
132 |
133 | 134 |
135 |
136 |
137 | settings-icon 138 |

139 | Customize your rate-limiting logic with popular algorithms like 140 | token-bucket, fixed-window, sliding-window-counter and 141 | sliding-window-log, and assign custom type weights to your 142 | fields to tailor the solution to your needs. 143 |

144 |
145 |
146 | barchart-icon 147 |

148 | Analyses query request data such as complexity, depth and tokens 149 | with different rate limiting settings applied to see “what would 150 | have happened” to tune your API rate-limits and better protect 151 | your application. 152 |

153 |
154 |
155 |
156 |
157 | intime-icon 158 |

159 | Accurately measure the complexity of a GraphQL query before 160 | executing it to throttle queries by complexity and depth before 161 | they reach your GraphQL API. 162 |

163 |
164 |
165 | data-icon 166 |

167 | Gain insight to your GraphQL API with the Gateway Developer 168 | Portal which allows you to visualize query data to identify 169 | trends over the long term. 170 |

171 |
172 |
173 |
174 |
175 |
176 |
177 | ); 178 | } 179 | -------------------------------------------------------------------------------- /client/auth/AuthProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { gql, useQuery } from '@apollo/client'; 3 | 4 | interface UserContext { 5 | email?: string; 6 | id?: string; 7 | } 8 | 9 | interface AuthContextType { 10 | user: UserContext | null; 11 | // better typed but erroring --> setUser: React.Dispatch> | null 12 | setUser: any; 13 | loading: boolean; 14 | } 15 | 16 | const AuthContext = React.createContext({ 17 | user: null, 18 | setUser: null, 19 | loading: true, 20 | }); 21 | 22 | const CHECK_AUTH_QUERY = gql` 23 | query checkAuthQuery { 24 | checkAuth { 25 | id 26 | email 27 | } 28 | } 29 | `; 30 | 31 | function AuthProvider({ children }: React.PropsWithChildren) { 32 | const [user, setUser] = useState(null); 33 | const [loading, setLoading] = useState(true); 34 | 35 | // query to the server to check if there is a valid session and update state accordingly 36 | useQuery(CHECK_AUTH_QUERY, { 37 | onCompleted: (data: any) => { 38 | if (data.checkAuth === null) { 39 | localStorage.removeItem('session-token'); 40 | } else { 41 | setUser({ 42 | email: data.checkAuth.email, 43 | id: data.checkAuth.id, 44 | }); 45 | } 46 | setLoading(false); 47 | }, 48 | onError: (error) => { 49 | console.error(error); 50 | localStorage.removeItem('session-token'); 51 | setLoading(false); 52 | }, 53 | }); 54 | 55 | // eslint-disable-next-line react/jsx-no-constructed-context-values 56 | const value: AuthContextType = { user, setUser, loading }; 57 | return {children}; 58 | } 59 | 60 | function useAuth() { 61 | const context = React.useContext(AuthContext); 62 | if (context === undefined) throw new Error('useAuth must be used within an Auth provider'); 63 | return context; 64 | } 65 | 66 | export { useAuth, AuthProvider }; 67 | -------------------------------------------------------------------------------- /client/components/ChartBox.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useMemo } from 'react'; 2 | import { 3 | Chart as ChartJS, 4 | LinearScale, 5 | CategoryScale, 6 | BarElement, 7 | PointElement, 8 | LineElement, 9 | Legend, 10 | Tooltip, 11 | } from 'chart.js'; 12 | import { Line, Bar } from 'react-chartjs-2'; 13 | 14 | ChartJS.register( 15 | LinearScale, 16 | CategoryScale, 17 | BarElement, 18 | PointElement, 19 | LineElement, 20 | Legend, 21 | Tooltip 22 | ); 23 | 24 | export const options = { 25 | responsive: true, 26 | plugins: { 27 | legend: { 28 | position: 'top' as const, 29 | }, 30 | title: { 31 | display: true, 32 | text: 'Chart.js Line Chart', 33 | }, 34 | }, 35 | // TODO: if there is no data, the chart axis should start at 0. This is not working 36 | options: { scales: { y: { suggestedMin: 0 } } }, 37 | }; 38 | export interface ISState { 39 | style: { 40 | chartOne: string; 41 | chartTwo: string; 42 | chartThree: string; 43 | chartFour: string; 44 | chartFive: string; 45 | chartSix: string; 46 | }; 47 | } 48 | 49 | export interface IProps { 50 | queries: ProjectQuery[]; 51 | setNumberOfDaysToView: (days: ChartSelectionDays) => void; 52 | numberOfDaysToView: ChartSelectionDays; 53 | } 54 | 55 | // eslint-disable-next-line react/function-component-definition 56 | const ChartBox: React.FC = ({ queries, setNumberOfDaysToView, numberOfDaysToView }) => { 57 | /** create the state required for the chart */ 58 | const [depthData, setDepthData] = useState([]); 59 | const [complexityData, setComplexityData] = useState([]); 60 | const [tokenData, setTokenData] = useState([]); 61 | const [blockedData, setblockedData] = useState([]); 62 | const [volumeData, setVolumeData] = useState([]); 63 | const [labels, setLabels] = useState([]); 64 | const [smoothingFactor, setSmoothingFactor] = useState<1 | 3 | 6 | 12>(12); 65 | // const [timeRangeDays, setTimeRangeDays] = useState(30); 66 | 67 | /** useEffect will create the chart data to display form the query data */ 68 | useMemo(() => { 69 | /** create storage for the */ 70 | // y-axis data 71 | const depthArray: number[] = []; 72 | const complexityArray: number[] = []; 73 | const tokenArray: number[] = []; 74 | const blockedArray: number[] = []; 75 | const volumeArray: number[] = []; 76 | // x-axis data is an array of dates 77 | const labelsArray: string[] = []; 78 | 79 | /** layout the begining of the chart and time increments for each point */ 80 | const currentTime = new Date().valueOf(); 81 | // the time block is determined by: taking the smallest block of 15 minutes and multiplying that by a user conctrolled smoothing factor 82 | const timeBlock = 900000 * smoothingFactor * ((numberOfDaysToView + 30) / 30); 83 | let startTime = currentTime - numberOfDaysToView * 86400000; // 1 day * number of days for the time frame 84 | 85 | /** process time blocks for the chart while the start time of the current time block is less than the current date */ 86 | // The counter i will track the index we are on in the queries array 87 | let i = 0; 88 | while (startTime < currentTime) { 89 | // specify the end time for the current time block 90 | const nextTimeBlock = startTime + timeBlock; 91 | // push the date for the current timeblock into the labels array 92 | const date = new Date(startTime); 93 | labelsArray.push(`${date.toDateString().slice(0, 10)}, ${date.getHours()}:00`); 94 | // intialze the sum of depth, complexity, tokens, etc. to be zero 95 | let count = 0; 96 | let totalDepth = 0; 97 | let totalComplexity = 0; 98 | let totalTokens = 0; 99 | let totalBlocked = 0; 100 | let totalVolume = 0; 101 | 102 | /** process the queries that lie within this time block */ 103 | while (i < queries.length && queries[i].timestamp < nextTimeBlock) { 104 | if (queries[i].timestamp > startTime) { 105 | totalDepth += queries[i].depth; 106 | totalComplexity += queries[i].complexity; 107 | totalTokens += queries[i].tokens; 108 | totalVolume += 1; 109 | if (!queries[i].success) totalBlocked += 1; 110 | count += 1; 111 | } 112 | i += 1; 113 | } 114 | // push the average depth, complexity, tokens into the appropriate array 115 | depthArray.push(Math.round(totalDepth / count) || 0); 116 | complexityArray.push(Math.round(totalComplexity / count) || 0); 117 | tokenArray.push(Math.round(totalTokens / count) || 0); 118 | blockedArray.push(Math.round((totalBlocked / count) * 100) || 0); 119 | const volumePerHour = totalVolume / (timeBlock / 3600000); 120 | volumeArray.push(Number(volumePerHour.toFixed(2))); 121 | // increment the start time for the next timeblock 122 | startTime = nextTimeBlock; 123 | } 124 | 125 | /** set the state of the chart data */ 126 | setDepthData(depthArray); 127 | setComplexityData(complexityArray); 128 | setTokenData(tokenArray); 129 | setblockedData(blockedArray); 130 | setLabels(labelsArray); 131 | setVolumeData(volumeArray); 132 | }, [queries, numberOfDaysToView, smoothingFactor]); 133 | 134 | /** Configure the datasets for Chart.js */ 135 | // apply these prooperties to all datasets 136 | const defaultDatasetProperties = { 137 | type: 'line' as const, 138 | tension: 0.5, 139 | elements: { point: { radius: 0 } }, 140 | }; 141 | 142 | const tokens = { 143 | labels, 144 | datasets: [ 145 | { 146 | ...defaultDatasetProperties, 147 | label: 'Tokens', 148 | borderColor: 'rgba(255, 99, 132, 0.5)', 149 | backgroundColor: 'rgba(255, 99, 132, 0.5)', 150 | data: tokenData, 151 | }, 152 | ], 153 | }; 154 | const volume = { 155 | labels, 156 | datasets: [ 157 | { 158 | ...defaultDatasetProperties, 159 | type: 'bar' as const, 160 | label: 'Volume (query / h)', 161 | borderColor: 'rgba(128, 0, 128, 0.5)', 162 | backgroundColor: 'rgba(128, 0, 128, 0.5)', 163 | data: volumeData, 164 | }, 165 | ], 166 | }; 167 | 168 | const blocked = { 169 | labels, 170 | datasets: [ 171 | { 172 | ...defaultDatasetProperties, 173 | label: '% Blocked', 174 | backgroundColor: 'rgba(53, 162, 235, 0.5)', 175 | borderColor: 'rgba(53, 162, 235, 0.5)', 176 | data: blockedData, 177 | }, 178 | ], 179 | }; 180 | 181 | const depth = { 182 | labels, 183 | datasets: [ 184 | { 185 | ...defaultDatasetProperties, 186 | label: 'Depth', 187 | backgroundColor: 'rgba(75, 192, 192, 0.8)', 188 | data: depthData, 189 | borderColor: 'rgba(75, 192, 192, 0.8)', 190 | }, 191 | ], 192 | }; 193 | 194 | const complexity = { 195 | labels, 196 | datasets: [ 197 | { 198 | ...defaultDatasetProperties, 199 | label: 'Complexity', 200 | backgroundColor: 'rgba(255, 255, 0, 0.5)', 201 | borderColor: 'rgba(255, 255, 0, 0.5)', 202 | data: complexityData, 203 | }, 204 | ], 205 | }; 206 | 207 | const data = { 208 | labels, 209 | datasets: [ 210 | complexity.datasets[0], 211 | depth.datasets[0], 212 | blocked.datasets[0], 213 | tokens.datasets[0], 214 | // volume.datasets[0], 215 | ], 216 | }; 217 | 218 | const [style, setStyle] = useState({ 219 | chartOne: 'none', 220 | chartTwo: 'none', 221 | chartThree: 'none', 222 | chartFour: 'none', 223 | chartFive: 'none', 224 | chartSix: 'block', 225 | }); 226 | const chartOneFn = () => { 227 | setStyle({ 228 | ...style, 229 | chartOne: 'block', 230 | chartTwo: 'none', 231 | chartThree: 'none', 232 | chartFour: 'none', 233 | chartFive: 'none', 234 | chartSix: 'none', 235 | }); 236 | }; 237 | const chartTwoFn = () => { 238 | setStyle({ 239 | ...style, 240 | chartOne: 'none', 241 | chartTwo: 'block', 242 | chartThree: 'none', 243 | chartFour: 'none', 244 | chartFive: 'none', 245 | chartSix: 'none', 246 | }); 247 | }; 248 | const chartThreeFn = () => { 249 | setStyle({ 250 | ...style, 251 | chartOne: 'none', 252 | chartTwo: 'none', 253 | chartThree: 'block', 254 | chartFour: 'none', 255 | chartFive: 'none', 256 | chartSix: 'none', 257 | }); 258 | }; 259 | const chartFourFn = () => { 260 | setStyle({ 261 | ...style, 262 | chartOne: 'none', 263 | chartTwo: 'none', 264 | chartThree: 'none', 265 | chartFour: 'block', 266 | chartFive: 'none', 267 | chartSix: 'none', 268 | }); 269 | }; 270 | const chartFiveFn = () => { 271 | setStyle({ 272 | ...style, 273 | chartOne: 'none', 274 | chartTwo: 'none', 275 | chartThree: 'none', 276 | chartFour: 'none', 277 | chartFive: 'block', 278 | chartSix: 'none', 279 | }); 280 | }; 281 | const chartSixFn = () => { 282 | setStyle({ 283 | ...style, 284 | chartOne: 'none', 285 | chartTwo: 'none', 286 | chartThree: 'none', 287 | chartFour: 'none', 288 | chartFive: 'none', 289 | chartSix: 'block', 290 | }); 291 | }; 292 | 293 | return ( 294 |
295 |
296 | 299 | 302 | 309 | 316 | 319 | 322 | 325 | 328 |
329 |
330 | 331 |
332 |
333 | 334 |
335 |
336 | 337 |
338 |
339 | 340 |
341 |
342 | 343 |
344 |
345 | 346 |
347 |
348 | 351 | 354 | 357 | 360 | 363 | 366 |
367 |
368 | ); 369 | }; 370 | 371 | export default ChartBox; 372 | -------------------------------------------------------------------------------- /client/components/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { useLazyQuery, gql, useMutation } from '@apollo/client'; 3 | import ToolBar from './ToolBar'; 4 | import ProjectView from './ProjectView'; 5 | import { useAuth } from '../auth/AuthProvider'; 6 | 7 | const GET_PROJECT_DATA = gql` 8 | query getUserData($userId: String!) { 9 | user(id: $userId) { 10 | projects { 11 | name 12 | id 13 | userID 14 | apiKey 15 | } 16 | } 17 | } 18 | `; 19 | 20 | const UPDATE_RATE_LIMITER_CONFIG_MUTATION = gql` 21 | mutation udpateRateLimiter($projectId: String!, $rateLimiterConfig: RateLimiterConfigInput) { 22 | updateProject(id: $projectId, rateLimiterConfig: $rateLimiterConfig) { 23 | rateLimiterConfig { 24 | type 25 | options { 26 | ... on WindowOptions { 27 | capacity 28 | windowSize 29 | } 30 | ... on BucketOptions { 31 | capacity 32 | refillRate 33 | } 34 | } 35 | } 36 | } 37 | } 38 | `; 39 | 40 | const GET_RATE_LIMITER_CONFIG_QUERY = gql` 41 | query getRateLimiter($projectId: String!) { 42 | project(id: $projectId) { 43 | rateLimiterConfig { 44 | type 45 | options { 46 | ... on WindowOptions { 47 | capacity 48 | windowSize 49 | } 50 | ... on BucketOptions { 51 | capacity 52 | refillRate 53 | } 54 | } 55 | } 56 | } 57 | } 58 | `; 59 | 60 | interface ProjectResult { 61 | project: Project; 62 | } 63 | interface ProjectVars { 64 | projectId: string; 65 | } 66 | 67 | interface UpdateRateLimiterVars extends ProjectVars { 68 | rateLimiterConfig: RateLimiterConfig; 69 | } 70 | 71 | export default function Dashboard() { 72 | /** Bring the user context into this component */ 73 | const { user } = useAuth(); 74 | 75 | const [selectedProject, setSelectedProject] = useState(); 76 | const [rateLimitedQueries, setRateLimitedQueries] = useState([]); 77 | 78 | // Apollo graphql hooks 79 | /** Send query to get project information for this user */ 80 | const [getUserData, userData] = useLazyQuery<{ user: User }, { userId: string }>( 81 | GET_PROJECT_DATA 82 | ); 83 | 84 | const [getRateLimiterConfig, rateLimitResponse] = useLazyQuery( 85 | GET_RATE_LIMITER_CONFIG_QUERY, 86 | { fetchPolicy: 'network-only' } 87 | ); 88 | 89 | const [udpateRateLimiter, { loading: updateLoading }] = useMutation< 90 | ProjectResult, 91 | UpdateRateLimiterVars 92 | >(UPDATE_RATE_LIMITER_CONFIG_MUTATION); 93 | 94 | const fetchRateLimiterConfig = async (projectId: string) => { 95 | // FIXME: We can conditionally render an error component 96 | const { error } = await getRateLimiterConfig({ 97 | variables: { 98 | projectId, 99 | }, 100 | }); 101 | if (error) { 102 | console.error(error); 103 | } 104 | }; 105 | 106 | // User data whenever the user changes 107 | useEffect(() => { 108 | if (user?.id) { 109 | getUserData({ 110 | variables: { 111 | userId: user.id, 112 | }, 113 | }); 114 | } 115 | }, [user]); 116 | 117 | // RateLimiter Settings whenever the proejct changes 118 | useEffect(() => { 119 | // Fetches Rate Limiter settings whenever project is changed 120 | if (selectedProject?.id) { 121 | fetchRateLimiterConfig(selectedProject.id).then(() => setRateLimitedQueries([])); 122 | } 123 | }, [selectedProject]); 124 | 125 | const handleRateLimiterConfigChange = ( 126 | updatedConfig: RateLimiterConfig, 127 | saveConfig: boolean 128 | ) => { 129 | if (selectedProject) { 130 | if (saveConfig) { 131 | // Save config in database 132 | udpateRateLimiter({ 133 | variables: { 134 | projectId: selectedProject.id, 135 | rateLimiterConfig: updatedConfig, 136 | }, 137 | // Refetch rate limiter data. The rate limiter query bypasses the cache so we can't just update the cache here. 138 | refetchQueries: [ 139 | { 140 | query: GET_RATE_LIMITER_CONFIG_QUERY, 141 | variables: { projectId: selectedProject.id }, 142 | }, 143 | ], 144 | }); 145 | } else { 146 | // eslint-disable-next-line consistent-return 147 | (async () => { 148 | try { 149 | const data: { queries: ProjectQuery[] } = await fetch( 150 | `/api/projects/rateLimit/${selectedProject.id}`, 151 | { 152 | method: 'POST', 153 | headers: { 154 | 'content-type': 'application/json', 155 | authorization: `BEARER: ${localStorage.getItem( 156 | 'session-token' 157 | )}`, 158 | }, 159 | body: JSON.stringify({ 160 | config: updatedConfig, 161 | }), 162 | } 163 | ).then((res) => res.json()); 164 | setRateLimitedQueries(data.queries); 165 | } catch (err) { 166 | return console.error(err); 167 | } 168 | })(); 169 | } 170 | } 171 | }; 172 | 173 | return ( 174 |
175 | setRateLimitedQueries([])} 183 | showSettings={selectedProject != null} 184 | getUserData={getUserData} 185 | /> 186 | 191 |
192 | ); 193 | } 194 | -------------------------------------------------------------------------------- /client/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '../../public/styles.css'; 3 | 4 | export default function Footer() { 5 | return ( 6 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /client/components/Form.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/no-static-element-interactions */ 2 | /* eslint-disable jsx-a11y/click-events-have-key-events */ 3 | /* eslint-disable jsx-a11y/label-has-associated-control */ 4 | import React, { useState } from 'react'; 5 | 6 | export interface IProps { 7 | togglePopup: any; 8 | userID: any; 9 | createProjectMutation: any; 10 | } 11 | // eslint-disable-next-line react/function-component-definition 12 | const Form: React.FC = ({ togglePopup, userID, createProjectMutation }) => { 13 | const [name, setName] = useState(''); 14 | 15 | const handleSubmit = (event: any) => { 16 | event.preventDefault(); 17 | createProjectMutation({ variables: { project: { userID, name } } }); 18 | }; 19 | 20 | return ( 21 |
22 |
23 |
24 | 25 | x 26 | 27 |

New project name:

28 | setName(e.target.value)} /> 29 | 36 |
37 |
38 |
39 | ); 40 | }; 41 | 42 | export default Form; 43 | -------------------------------------------------------------------------------- /client/components/Loading.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function Loading() { 4 | return ( 5 |
6 |
Loading...
7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /client/components/Login.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useNavigate, Link } from 'react-router-dom'; 3 | import { gql, useMutation } from '@apollo/client'; 4 | import { useAuth } from '../auth/AuthProvider'; 5 | 6 | export interface ISState { 7 | user: { 8 | email: string; 9 | password: string; 10 | }; 11 | } 12 | 13 | const LOGIN_MUTATION = gql` 14 | mutation loginMutation($user: UserInput!) { 15 | login(user: $user) { 16 | token 17 | email 18 | id 19 | } 20 | } 21 | `; 22 | 23 | function Login() { 24 | const [user, setUser] = useState({ 25 | email: '', 26 | password: '', 27 | }); 28 | const [loginError, setloginError] = useState(null); 29 | const { setUser: setUserAuth } = useAuth(); 30 | const handleChange = (e: React.ChangeEvent) => { 31 | setUser({ 32 | ...user, 33 | [e.target.name]: e.target.value, 34 | }); 35 | }; 36 | 37 | const navigate = useNavigate(); 38 | 39 | const [loginMutation] = useMutation(LOGIN_MUTATION, { 40 | onCompleted: (data) => { 41 | setUserAuth({ 42 | email: data.login.email, 43 | id: data.login.id, 44 | }); 45 | localStorage.setItem('session-token', data.login.token); 46 | navigate('/dashboard'); 47 | }, 48 | onError: (error) => setloginError(error.message), 49 | }); 50 | 51 | const handleClick = async ( 52 | e: React.MouseEvent, 53 | userData: ISState['user'] 54 | ) => { 55 | e.preventDefault(); 56 | loginMutation({ variables: { user: userData } }); 57 | }; 58 | 59 | return ( 60 |
61 |

Login

62 | {loginError && {loginError}} 63 |
64 | 72 | 80 |
81 | 84 |
85 | 86 | Not a member? 87 | 88 | Register here 89 | 90 | 91 |
92 |
93 | ); 94 | } 95 | 96 | export default Login; 97 | -------------------------------------------------------------------------------- /client/components/Navbar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from 'react'; 2 | import { Link, useNavigate } from 'react-router-dom'; 3 | import '../../public/styles.css'; 4 | import { useAuth } from '../auth/AuthProvider'; 5 | 6 | export default function Navbar() { 7 | const navigate = useNavigate(); 8 | const { user, setUser, loading } = useAuth(); 9 | 10 | const logout = (e: React.MouseEvent) => { 11 | e.preventDefault(); 12 | setUser(null); 13 | localStorage.removeItem('session-token'); 14 | navigate('/'); 15 | }; 16 | 17 | useEffect(() => { 18 | const url = window.location.href.split('/'); 19 | const target = url[url.length - 1].toLowerCase(); 20 | const element = document.getElementById(target); 21 | if (element) element.scrollIntoView({ behavior: 'smooth', block: 'start' }); 22 | }, []); 23 | 24 | return ( 25 | 92 | ); 93 | } 94 | -------------------------------------------------------------------------------- /client/components/ProjectItem.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | interface ProjectItemProps { 4 | project: Project; 5 | setSelectedProject: any; 6 | } 7 | 8 | export default function ProjectItem({ project, setSelectedProject }: ProjectItemProps) { 9 | const [dropdown, setDropdown] = useState(false); 10 | return ( 11 | <> 12 | 20 | {dropdown && ( 21 |
22 |
23 |

Project ID:

{project.id} 24 |
25 |

API Key:

{project.apiKey} 26 |
27 | 37 |
38 | )} 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /client/components/ProjectView.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { useLazyQuery, gql } from '@apollo/client'; 3 | import Queries from './Queries'; 4 | import ChartBox from './ChartBox'; 5 | import Loading from './Loading'; 6 | 7 | export interface ISState { 8 | style: { 9 | time: boolean; // TODO: Change to timestamp 10 | depth: boolean; 11 | complexity: boolean; 12 | }; 13 | arrow: { 14 | timestamp: SortOrder; 15 | depth: SortOrder; 16 | complexity: SortOrder; 17 | }; 18 | } 19 | 20 | interface ProjectViewProps { 21 | selectedProject: Project | undefined; 22 | projectLoading: boolean; 23 | rateLimiterQueries: ProjectQuery[]; 24 | } 25 | 26 | const GET_QUERY_DATA = gql` 27 | query getQueryData($projectId: String!, $minDate: Float, $maxDate: Float) { 28 | projectQueries(id: $projectId, minDate: $minDate, maxDate: $maxDate) { 29 | id 30 | number 31 | latency 32 | complexity 33 | loggedOn 34 | depth 35 | timestamp 36 | tokens 37 | success 38 | } 39 | } 40 | `; 41 | 42 | const DEFAULT_DAYS = 365; // FIXME: Replace with 7 43 | const MS_IN_DAY = 24 * 60 * 60 * 1000; 44 | 45 | export default function ProjectView({ 46 | selectedProject, 47 | projectLoading, 48 | rateLimiterQueries, 49 | }: ProjectViewProps) { 50 | // Lookback perioud for query data. DEFAULT to 1 week 51 | const [numberOfDaysToView, setNumberOfDaysToView] = useState(DEFAULT_DAYS); 52 | // The previous lookback period 53 | const [previousDays, setPreviousDays] = useState(0); 54 | 55 | // Tracks the timestamp of the last time we fetched queries 56 | const [lastFetchDate, setLastFetchDate] = useState(0); 57 | 58 | // Track the earliest date for which we've fetched a query. We only need to fetch historical data once 59 | const [earliestQueryDate, setEarliestQueryDate] = useState(0); 60 | 61 | // These are the queries rendered by the presentational and components 62 | // FIXME: This is redundant state that can be obtained by splicing raw or rateLimitedQueries 63 | const [visibleQueries, setVisibleQueries] = useState([]); 64 | 65 | // Raw query data without updated rate limiter settings or paginated for view 66 | const [rawQueries, setRawQueries] = useState([]); 67 | 68 | const [getProjectQueries, { loading: queriesLoading }] = useLazyQuery(GET_QUERY_DATA); 69 | 70 | const fetchQueryData = async (projectId: string, minDate: number, maxDate: number) => 71 | getProjectQueries({ 72 | variables: { 73 | projectId, 74 | minDate, 75 | maxDate, 76 | }, 77 | }); 78 | 79 | // Fetch query data whenever the project changes and reset the default lookback period. 80 | useEffect(() => { 81 | (async () => { 82 | // FIXME: check if component is mounted prior to state updates 83 | 84 | if (selectedProject) { 85 | const currentTime = new Date().valueOf(); 86 | // Earliest date for which queries should be fetched 87 | const startDate: number = currentTime - DEFAULT_DAYS * MS_IN_DAY; 88 | setNumberOfDaysToView(DEFAULT_DAYS); 89 | setLastFetchDate(currentTime); 90 | setEarliestQueryDate(startDate); 91 | 92 | // The project has changed refetch query data for this project 93 | const { data } = await fetchQueryData(selectedProject.id, startDate, currentTime); 94 | // TODO: Error handling 95 | // Display these queries and clear rate limited queries 96 | setVisibleQueries(data.projectQueries); 97 | setRawQueries(data.projectQueries); 98 | } 99 | })(); 100 | }, [selectedProject]); 101 | 102 | // Fetch new queries and reslice the data whenever the number of days to view changes 103 | // to minimize delay and ease computation costs rateLimiterQueries are only viewed historically 104 | // new queries will not be fectched. The purpose of this is to view the effect on historical data 105 | // and not monitor incoming data. 106 | useEffect(() => { 107 | (async () => { 108 | // FIXME: check if component is mounted prior to state updates 109 | 110 | if (!selectedProject) return; 111 | 112 | const currentTime = new Date().valueOf(); 113 | const cutoffDate = currentTime - numberOfDaysToView * MS_IN_DAY; 114 | 115 | if (rateLimiterQueries.length > 0) { 116 | setVisibleQueries( 117 | // Sort queries by timestamp and filter for current view 118 | rateLimiterQueries 119 | .filter((a: ProjectQuery) => a.timestamp > cutoffDate) 120 | .sort((a: ProjectQuery, b: ProjectQuery) => a.timestamp - b.timestamp) 121 | ); 122 | } else { 123 | // Fetch any queries that have come in since the last time we fetched 124 | const { data: newQueries } = await fetchQueryData( 125 | selectedProject.id, 126 | lastFetchDate, 127 | currentTime 128 | ); 129 | setLastFetchDate(currentTime); 130 | 131 | // If new number of days is greater then we need to fetch older queries as well 132 | if (numberOfDaysToView > previousDays) { 133 | // Only fetch queries if we haven't gotten them already 134 | const earliestQueryNeeded = currentTime - numberOfDaysToView * MS_IN_DAY; 135 | 136 | let olderQueries: ProjectQuery[] = []; 137 | 138 | if (earliestQueryNeeded < earliestQueryDate) { 139 | olderQueries = await fetchQueryData( 140 | selectedProject.id, 141 | earliestQueryNeeded, 142 | lastFetchDate - previousDays * MS_IN_DAY 143 | ).then(({ data }) => data.projectQueries || []); 144 | } 145 | // merge this data into the rawData 146 | // TODO: Toggling this back and forth will add repeats 147 | const updatedQueries = [...olderQueries, ...rawQueries, ...newQueries]; 148 | 149 | // TODO: Merge with Rate limited queries if custom limiter is configured 150 | 151 | setVisibleQueries( 152 | // Sort queries by timestamp and filter for current view 153 | updatedQueries 154 | .filter((a: ProjectQuery) => a.timestamp > cutoffDate) 155 | .sort((a: ProjectQuery, b: ProjectQuery) => a.timestamp - b.timestamp) 156 | ); 157 | setRawQueries(updatedQueries); 158 | } else { 159 | // we add new queries then drop any queries outside the window 160 | 161 | // Otherwise user wants to view less data so just slice the visible data 162 | setVisibleQueries( 163 | [...visibleQueries, ...rawQueries, ...newQueries] 164 | .filter((a: ProjectQuery) => a.timestamp > cutoffDate) 165 | .sort((a: ProjectQuery, b: ProjectQuery) => a.timestamp - b.timestamp) 166 | ); 167 | } 168 | } 169 | })(); 170 | }, [numberOfDaysToView]); 171 | 172 | // Update visible queries whenever rateLimiterQueries changes 173 | useEffect(() => { 174 | const currentTime = new Date().valueOf(); 175 | const cutoffDate = currentTime - numberOfDaysToView * MS_IN_DAY; 176 | 177 | const newVisibleQueries = rateLimiterQueries.length > 0 ? rateLimiterQueries : rawQueries; 178 | 179 | setVisibleQueries( 180 | [...newVisibleQueries] 181 | .filter((a: ProjectQuery) => a.timestamp > cutoffDate) 182 | .sort((a: ProjectQuery, b: ProjectQuery) => a.timestamp - b.timestamp) 183 | ); 184 | }, [rateLimiterQueries]); 185 | 186 | const handleDayChange = (newDays: ChartSelectionDays) => { 187 | setPreviousDays(numberOfDaysToView); 188 | setNumberOfDaysToView(newDays); 189 | }; 190 | 191 | /** 192 | * There are 3 states to the project view 193 | * 1. no project selected - render a shell of the project view with a "Select a Project Prompt" 194 | * 2. projects or queries are loading - render a shell of the project view with a loading component 195 | * 3. project selected and queries loaded - render the querues and chart 196 | * */ 197 | if (!selectedProject || projectLoading) 198 | return ( 199 |
200 |
201 |
202 | {projectLoading ? ( 203 | 204 | ) : ( 205 |
206 |

Select a project

207 |
208 | )} 209 |
210 |
211 | ); 212 | if (queriesLoading || !visibleQueries) 213 | return ( 214 |
215 |
216 |
217 | 218 |
219 | ; 220 |
221 | ); 222 | 223 | return ( 224 |
225 |
226 | 227 |
228 |
229 | 234 |
235 |
236 | ); 237 | } 238 | -------------------------------------------------------------------------------- /client/components/ProjectsPane.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { gql, useMutation } from '@apollo/client'; 3 | import Loading from './Loading'; 4 | import ProjectItem from './ProjectItem'; 5 | import Form from './Form'; 6 | import { useAuth } from '../auth/AuthProvider'; 7 | 8 | const CREATE_PROJECT = gql` 9 | mutation createProjectMutation($project: CreateProjectInput!) { 10 | createProject(project: $project) { 11 | name 12 | id 13 | userID 14 | apiKey 15 | } 16 | } 17 | `; 18 | 19 | export default function ProjectsPane({ 20 | projectLoading, 21 | projects, 22 | setSelectedProject, 23 | getUserData, 24 | }: ProjectPaneProps) { 25 | // this is a state for the form 26 | const [showForm, setShowForm] = useState(false); 27 | 28 | const togglePopup = () => { 29 | setShowForm(!showForm); 30 | }; 31 | 32 | const { user } = useAuth(); 33 | 34 | const [createProjectMutation] = useMutation(CREATE_PROJECT, { 35 | onCompleted: () => { 36 | getUserData({ 37 | variables: { 38 | userId: user?.id, 39 | }, 40 | }); 41 | window.location.reload(); 42 | }, 43 | onError: (err) => console.log(err), 44 | }); 45 | 46 | return ( 47 | <> 48 |

Projects:

49 | {projectLoading ? ( 50 | 51 | ) : ( 52 | <> 53 | {projects?.map((project) => ( 54 | 59 | ))} 60 | 63 | {showForm && ( 64 |
69 | )} 70 | 71 | )} 72 | 73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /client/components/Queries.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import Query, { QueryHeader } from './Query'; 3 | 4 | export interface ISState { 5 | style: { 6 | time: boolean; // TODO: Change to timestamp 7 | depth: boolean; 8 | complexity: boolean; 9 | }; 10 | arrow: { 11 | timestamp: SortOrder; 12 | depth: SortOrder; 13 | complexity: SortOrder; 14 | }; 15 | } 16 | 17 | export interface IProps { 18 | rawQueries: ProjectQuery[]; 19 | } 20 | 21 | // eslint-disable-next-line react/function-component-definition 22 | const Queries: React.FC = ({ rawQueries }) => { 23 | const queryDisplayIncrements = 150; 24 | // "rawQueries" is the raw, unfilter array of queries. "listOfQueries" is the filter list 25 | const [listOfQueries, setListOfQueries] = useState(rawQueries); 26 | 27 | // State for the list 28 | const [list, setList] = useState([...listOfQueries.slice(0, queryDisplayIncrements)]); 29 | 30 | // State to trigger oad more 31 | const [loadMore, setLoadMore] = useState(false); 32 | 33 | // State of whether there is more to load 34 | const [hasMore, setHasMore] = useState(listOfQueries.length > 150); 35 | 36 | // Load more button click 37 | const handleLoadMore = () => { 38 | setLoadMore(true); 39 | }; 40 | /** State requirments for this component */ 41 | useEffect(() => { 42 | setListOfQueries(rawQueries); 43 | }, [rawQueries]); 44 | 45 | useEffect(() => { 46 | setList(listOfQueries.slice(0, queryDisplayIncrements)); 47 | }, [listOfQueries]); 48 | 49 | useEffect(() => { 50 | if (loadMore && hasMore) { 51 | const currentLength = list.length; 52 | const isMore = currentLength < listOfQueries.length; 53 | const nextResults = isMore 54 | ? listOfQueries.slice(currentLength, currentLength + queryDisplayIncrements) 55 | : []; 56 | setList([...list, ...nextResults]); 57 | setLoadMore(false); 58 | } 59 | }, [loadMore, hasMore]); 60 | 61 | useEffect(() => { 62 | const isMore = list.length < listOfQueries.length; 63 | setHasMore(isMore); 64 | }, [list]); 65 | 66 | const [style, setStyle] = useState({ 67 | time: false, 68 | depth: false, 69 | complexity: false, 70 | }); 71 | const [arrow, setArrow] = useState({ 72 | // adjusting arrow based on ascending vs descending 73 | timestamp: '', 74 | depth: '', 75 | complexity: '', 76 | }); 77 | /** Sort/filter the queries in the set the state of the filter arrows */ 78 | const combinedSort = (field: keyof ISState['arrow'], sortOrder: SortOrder): void => { 79 | const newArr = [...listOfQueries]; 80 | newArr.sort((a, b) => { 81 | if (sortOrder === '↑') { 82 | return a[field] - b[field]; 83 | } 84 | if (sortOrder === '↓') { 85 | return b[field] - a[field]; 86 | } 87 | return 0; 88 | }); 89 | setArrow({ 90 | timestamp: '', 91 | depth: '', 92 | complexity: '', 93 | [field]: sortOrder, 94 | }); 95 | setListOfQueries(newArr); 96 | }; 97 | 98 | const setToggle = (arg: string) => { 99 | if (arg === 'time') { 100 | setStyle({ 101 | ...style, 102 | time: true, 103 | depth: false, 104 | complexity: false, 105 | }); 106 | } else if (arg === 'depth') { 107 | setStyle({ 108 | ...style, 109 | time: false, 110 | depth: true, 111 | complexity: false, 112 | }); 113 | } else if (arg === 'complexity') { 114 | setStyle({ 115 | ...style, 116 | time: false, 117 | depth: false, 118 | complexity: true, 119 | }); 120 | } 121 | }; 122 | 123 | return ( 124 |
125 |
126 |
127 | 144 | 160 | 176 |
177 |
178 |
179 |
180 |
181 | 182 | {list?.map((query: ProjectQuery) => ( 183 | 184 | ))} 185 |
186 | {hasMore ? ( 187 | // eslint-disable-next-line react/button-has-type 188 | 191 | ) : ( 192 |

No more results

193 | )} 194 |
195 |
196 | ); 197 | }; 198 | 199 | export default Queries; 200 | -------------------------------------------------------------------------------- /client/components/Query.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface QueryProps { 4 | query: ProjectQuery; 5 | } 6 | 7 | export function QueryHeader() { 8 | return ( 9 | //
10 | <> 11 |
12 |
ID
13 |
14 |
15 |
Tokens
16 |
17 |
18 |
Depth
19 |
20 |
21 |
Complexity
22 |
23 |
24 |
Blocked
25 |
26 | 27 | //
28 | ); 29 | } 30 | export default function Query({ query }: QueryProps) { 31 | const blocked = !query.success ? 'blocked' : ''; 32 | return ( 33 | //
34 | <> 35 |
36 |
{query.number}
37 |
38 |
39 |
{query.tokens}
40 |
41 |
42 |
{query.depth}
43 |
44 |
45 |
{query.complexity}
46 |
47 |
48 |
{(!query.success).toString()}
49 |
50 | 51 | //
52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /client/components/RequireAuth.tsx: -------------------------------------------------------------------------------- 1 | import React, { PropsWithChildren } from 'react'; 2 | import { useLocation, Navigate } from 'react-router-dom'; 3 | import { useAuth } from '../auth/AuthProvider'; 4 | import Loading from './Loading'; 5 | 6 | function RequireAuth({ children }: PropsWithChildren) { 7 | const { user, loading } = useAuth(); 8 | const location = useLocation(); 9 | // if authentication is still being confirmed, return a loading component 10 | if (loading) return ; 11 | // eslint-disable-next-line react/jsx-no-useless-fragment 12 | return user ? <>{children} : ; 13 | } 14 | 15 | export default RequireAuth; 16 | -------------------------------------------------------------------------------- /client/components/SettingsPane.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | /** 4 | * 5 | * Settings Pane allows a dev to change the rate limiting settings to view the effect 6 | * on changing data. 7 | * 8 | * It should have: 9 | * a drop-down to select a rate limiting algo 10 | * a slider for capacity integers 11 | * a slider for refresh rate (bucket algos) tokends / s 12 | * a slider for window size (ms) (displayed as seconds) 13 | * 14 | * Initial settings are passed in (saved in db); 15 | * User can update settings and click an "Update". No live update as this can be 16 | * expensive with the amount of queries. 17 | * button to apply the new settigns 18 | * 19 | */ 20 | 21 | function Slider({ 22 | name, 23 | value, 24 | displayValue, 25 | onChange, 26 | min, 27 | max, 28 | unit, 29 | }: { 30 | name: string; 31 | value: number; 32 | displayValue: number; 33 | onChange: React.ChangeEventHandler; 34 | min: number; 35 | max: number; 36 | unit: string; 37 | }) { 38 | return ( 39 |
40 | {name} : {displayValue} {unit} 41 | 49 |
50 | ); 51 | } 52 | 53 | export default function SettingsPane({ 54 | rateLimiterConfig, 55 | rateLimiterLoading, 56 | setRateLimiterConfig, 57 | onRawQueriesClick, 58 | showSettings, 59 | }: SettingsPaneProps) { 60 | const [rateLimiterType, setRateLimiterType]: [ 61 | RateLimiterType, 62 | React.Dispatch> 63 | ] = useState(rateLimiterConfig?.type || 'None'); 64 | 65 | // Bucket or window capacity in tokens 66 | const [capacity, setCapacity]: [number, React.Dispatch] = useState( 67 | rateLimiterConfig?.options.capacity || 10 68 | ); 69 | 70 | // Rate for bucket algos in tokens / second 71 | const [refillRate, setRefillRate]: [number, React.Dispatch] = useState( 72 | rateLimiterConfig?.options?.refillRate || 1 73 | ); 74 | 75 | // Window size in seconds 76 | const [windowSize, setWindowSize]: [number, React.Dispatch] = useState( 77 | (rateLimiterConfig?.options?.windowSize || 1000) / 1000 78 | ); 79 | 80 | const onCapacityChange = (e: React.ChangeEvent) => { 81 | setCapacity(Number(e.target.value)); 82 | }; 83 | 84 | const onRefillRateChange = (e: React.ChangeEvent) => { 85 | setRefillRate(Number(e.target.value)); 86 | }; 87 | 88 | const onWindowSizeChange = (e: React.ChangeEvent) => { 89 | // convert from ms to seconds 90 | setWindowSize(Number(e.target.value) / 1000); 91 | }; 92 | 93 | const onRateLimiterChange = (e: React.ChangeEvent) => { 94 | setRateLimiterType(e.target.value as RateLimiterType); 95 | }; 96 | 97 | const onUpdate = (e: React.FormEvent, saveSettings = false) => { 98 | e.preventDefault(); 99 | // TODO: Window size math 100 | const updatedConfig: RateLimiterConfig = { 101 | type: rateLimiterType, 102 | options: { 103 | capacity, 104 | ...((rateLimiterType as WindowType) && { windowSize: windowSize * 1000 }), 105 | ...((rateLimiterType as BucketType) && { refillRate }), 106 | }, 107 | }; 108 | setRateLimiterConfig(updatedConfig, saveSettings); 109 | }; 110 | 111 | const onRestoreProjectSettingsClick = () => { 112 | setCapacity(rateLimiterConfig?.options.capacity || 10); 113 | setRefillRate(rateLimiterConfig?.options?.refillRate || 1); 114 | setWindowSize( 115 | rateLimiterConfig?.options?.windowSize ? rateLimiterConfig.options.windowSize / 1000 : 1 116 | ); 117 | setRateLimiterType(rateLimiterConfig?.type || 'None'); 118 | }; 119 | 120 | return ( 121 | <> 122 |

Settings

123 | {!showSettings &&
Select a project
} 124 | {rateLimiterLoading ? ( 125 |
126 | ) : ( 127 | showSettings && ( 128 |
129 | 143 | 152 | {['TOKEN_BUCKET', 'LEAKY_BUCKET'].includes(rateLimiterType) ? ( 153 | 162 | ) : ( 163 | 172 | )} 173 |
174 | 183 | 192 | 201 | {/* TODO: Implement functionality for the below buttons */} 202 | 211 |
212 |
213 | ) 214 | )} 215 | 216 | ); 217 | } 218 | -------------------------------------------------------------------------------- /client/components/Signup.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { useNavigate, Link } from 'react-router-dom'; 3 | import { gql, useMutation } from '@apollo/client'; 4 | import { useAuth } from '../auth/AuthProvider'; 5 | 6 | export interface ISState { 7 | user: { 8 | email: string; 9 | password: string; 10 | }; 11 | style: { 12 | loginBox: string; 13 | signupBox: string; 14 | }; 15 | } 16 | 17 | const SIGNUP_MUTATION = gql` 18 | mutation signupMutation($user: UserInput!) { 19 | signup(user: $user) { 20 | token 21 | email 22 | id 23 | } 24 | } 25 | `; 26 | 27 | function Signup() { 28 | const [user, setUser] = useState({ 29 | email: '', 30 | password: '', 31 | }); 32 | const [signUpError, setSignUpError] = useState(null); 33 | const { setUser: setUserAuth } = useAuth(); 34 | 35 | const handleChange = (e: React.ChangeEvent) => { 36 | setUser({ 37 | ...user, 38 | [e.target.name]: e.target.value, 39 | }); 40 | }; 41 | 42 | const navigate = useNavigate(); 43 | 44 | const [signupMutation] = useMutation(SIGNUP_MUTATION, { 45 | onCompleted: (data) => { 46 | setUserAuth({ 47 | email: data.signup.email, 48 | id: data.signup.id, 49 | }); 50 | localStorage.setItem('session-token', data.signup.token); 51 | navigate('/dashboard'); 52 | }, 53 | onError: (error) => setSignUpError(error.message), 54 | }); 55 | 56 | const handleClick = async ( 57 | e: React.MouseEvent, 58 | userData: ISState['user'] 59 | ) => { 60 | e.preventDefault(); 61 | signupMutation({ variables: { user: userData } }); 62 | }; 63 | 64 | return ( 65 |
66 |

Signup

67 | {signUpError && {signUpError}} 68 |
69 | 77 | 85 |
86 | 89 |
90 | 91 | Already a member? 92 | 93 | Login here 94 | 95 | 96 |
97 |
98 | ); 99 | } 100 | 101 | export default Signup; 102 | -------------------------------------------------------------------------------- /client/components/Team.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function Team() { 4 | return ( 5 |
6 |
7 |
8 |

OUR STORY

9 |

10 | GraphQLGateway was developed 11 | by a group of passionate engineers with the idea of developing a rate 12 | limiter for GraphQL APIs based on accurate complexity analysis. 13 |

14 |

15 | The GraphQLGateway suite 16 | consists of graphql-limiter{' '} 17 | providing rate limiting tools for your API and{' '} 18 | gate-logger establishing the 19 | link between the rate limiter and your{' '} 20 | Developer Portal. Combined 21 | these tools will allow you to monitor your API and fine tune your rate 22 | limiter to meet your specific needs. 23 |

24 |

25 | With the multiple rate-limiting and configuration choices, protecting your 26 | APIs becomes an afterthought. 27 |

28 |

- Evan, Stephan, Milos, Flora and Jon

29 | 30 | ☕ Want to support us? - Buy us a  31 | 35 | coffee 36 | 37 |  ☕ 38 | 39 |
40 |
41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /client/components/ToolBar.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Loading from './Loading'; 3 | import ProjectsPane from './ProjectsPane'; 4 | import SettingsPane from './SettingsPane'; 5 | 6 | export default function ToolBar({ 7 | projects, 8 | setSelectedProject, 9 | projectLoading, 10 | rateLimiterConfig, 11 | rateLimiterLoading, 12 | setRateLimiterConfig, 13 | onRawQueriesClick, 14 | showSettings, 15 | getUserData, 16 | }: ToolbarProps) { 17 | /** State for the component */ 18 | const [showToolbar, setShowToolbar] = useState(false); 19 | const [toolbarContent, setToolbarContent] = useState(''); 20 | 21 | /** render the toolbar 22 | * while the GET_PROJECT_DATA query is loading, render the loading component 23 | * instead of the project list */ 24 | 25 | let selectedPane; 26 | 27 | switch (toolbarContent) { 28 | case 'PROJECTS': { 29 | selectedPane = ( 30 | { 34 | setSelectedProject(project); 35 | setShowToolbar(false); 36 | setToolbarContent(''); 37 | }} 38 | getUserData={getUserData} 39 | /> 40 | ); 41 | break; 42 | } 43 | case 'SETTINGS': { 44 | selectedPane = ( 45 | 52 | ); 53 | break; 54 | } 55 | default: { 56 | selectedPane = null; 57 | } 58 | } 59 | 60 | return ( 61 |
62 |
94 | )} 95 |
{projectLoading ? : selectedPane}
96 |
97 | ); 98 | } 99 | -------------------------------------------------------------------------------- /client/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { BrowserRouter, Routes, Route } from 'react-router-dom'; 3 | import { ApolloClient, InMemoryCache, ApolloProvider, createHttpLink } from '@apollo/client'; 4 | import { setContext } from '@apollo/client/link/context'; 5 | import { render } from 'react-dom'; 6 | import Signup from './components/Signup'; 7 | import Login from './components/Login'; 8 | import Team from './components/Team'; 9 | import Dashboard from './components/Dashboard'; 10 | import { AuthProvider } from './auth/AuthProvider'; 11 | import Navbar from './components/Navbar'; 12 | import RequireAuth from './components/RequireAuth'; 13 | import App from './App'; 14 | import Footer from './components/Footer'; 15 | 16 | const httpLink = createHttpLink({ 17 | uri: '/gql', 18 | }); 19 | // todo: add apollo link to check auth status 20 | const authLink = setContext((request, { headers }) => { 21 | // get the authentication token from local storage if it exists 22 | const token: string | null = localStorage.getItem('session-token'); 23 | // return the headers to the context so httpLink can read them 24 | return { 25 | headers: { 26 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 27 | ...headers, 28 | authorization: token ? `Bearer ${token}` : '', 29 | }, 30 | }; 31 | }); 32 | 33 | const client = new ApolloClient({ 34 | link: authLink.concat(httpLink), 35 | cache: new InMemoryCache(), 36 | }); 37 | 38 | render( 39 | 40 | 41 | 42 | 43 | 44 | 45 | } /> 46 | } /> 47 | } /> 48 | } /> 49 | } /> 50 | } /> 51 | 55 | 56 | 57 | } 58 | /> 59 | } /> 60 | 61 |