├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── __mocks__ ├── fakeNetworkEvent.js ├── fileMock.js ├── schema.js ├── styleMock.js └── themeMock.js ├── babel.config.js ├── jest.setup.js ├── package.json ├── src ├── app │ ├── Events_Tab │ │ ├── ApolloTabResponsive.tsx │ │ ├── Cache.tsx │ │ ├── CacheDetails.tsx │ │ ├── EventDetails.tsx │ │ ├── EventLog.tsx │ │ ├── EventPanel.tsx │ │ └── __tests__ │ │ │ ├── ApolloTab_test.js │ │ │ └── __snapshots__ │ │ │ └── ApolloTab_test.js.snap │ ├── GraphiQL_Tab │ │ ├── GraphiQLPage.tsx │ │ └── GraphiqlPlugin.jsx │ ├── Panel │ │ ├── MainDrawer.tsx │ │ ├── __tests__ │ │ │ ├── MainDrawer_test.js │ │ │ ├── __snapshots__ │ │ │ │ ├── MainDrawer_test.js.snap │ │ │ │ └── app_test.js.snap │ │ │ └── app_test.js │ │ ├── app.tsx │ │ ├── index.css │ │ ├── index.tsx │ │ └── themes │ │ │ ├── ThemeProvider.tsx │ │ │ ├── base.ts │ │ │ ├── dark.ts │ │ │ └── normal.ts │ ├── Performance_Tab │ │ ├── ArrowChip.tsx │ │ ├── Performance_v2.tsx │ │ ├── TracingDetails.tsx │ │ ├── __tests__ │ │ │ ├── Performance_test.js │ │ │ └── __snapshots__ │ │ │ │ └── Performance_test.js.snap │ │ └── progressBar.tsx │ └── utils │ │ ├── helper.ts │ │ ├── managedlog │ │ ├── eventObject.ts │ │ ├── index.ts │ │ ├── lib │ │ │ ├── apollo11types.ts │ │ │ ├── eventLogData.ts │ │ │ ├── eventLogNode.ts │ │ │ ├── networkStatus.ts │ │ │ └── objectDifference.ts │ │ └── other │ │ │ ├── dll.ts │ │ │ └── index.ts │ │ ├── messaging.ts │ │ ├── networking.ts │ │ ├── performanceMetricsCalcs.tsx │ │ ├── tracingTimeFormating.tsx │ │ └── useClientEventlogs.ts ├── assets │ ├── Apollo11-Events-Low.gif │ ├── Apollo11-GraphiQL-Low.gif │ ├── Apollo11-Performance-Low.gif │ ├── ApolloDevQL-01.png │ └── ApolloDevQL-02.png └── extension │ ├── background.ts │ ├── build │ ├── devtools.html │ ├── devtools.ts │ ├── diff.css │ ├── manifest.json │ ├── panel.html │ └── stylesheet.css │ ├── contentScript.ts │ └── hook │ └── apollo.ts ├── tsconfig.json └── webpack.config.js /.eslintignore: -------------------------------------------------------------------------------- 1 | .eslintrc.js 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | webextensions: true, 6 | node: true, 7 | jest: true, 8 | }, 9 | extends: [ 10 | 'airbnb', 11 | 'airbnb/hooks', 12 | 'plugin:react/recommended', 13 | 'prettier', 14 | 'prettier/react', 15 | ], 16 | parser: 'babel-eslint', 17 | parserOptions: { 18 | ecmaFeatures: { 19 | jsx: true, 20 | }, 21 | ecmaVersion: 12, 22 | sourceType: 'module', 23 | }, 24 | plugins: ['react', 'jsx-a11y', 'prettier'], 25 | rules: { 26 | 'prettier/prettier': ['warn'], 27 | 'react/prop-types': 'off', 28 | 'react/jsx-filename-extension': [ 29 | 1, 30 | { 31 | extensions: ['.js', '.jsx', '.ts', '.tsx'], 32 | }, 33 | ], 34 | 'no-underscore-dangle': 'off', 35 | // 'no-useless-computed-key': 'off', 36 | // 'no-param-reassign': 'off', 37 | 'no-console': 'off', 38 | 'import/extensions': [ 39 | 'error', 40 | 'ignorePackages', 41 | { 42 | js: 'never', 43 | mjs: 'never', 44 | jsx: 'never', 45 | ts: 'never', 46 | tsx: 'never', 47 | }, 48 | ], 49 | }, 50 | settings: { 51 | react: { 52 | version: 'detect', 53 | }, 54 | 'import/resolver': { 55 | node: { 56 | extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'], 57 | }, 58 | }, 59 | }, 60 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | .vscode 4 | src/extension/build/bundles 5 | .DS_Store 6 | package/node_modules/ 7 | coverage/ -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .eslintrc.js 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "bracketSpacing": false, 6 | "jsxBracketSameLine": true, 7 | "overrides": [ 8 | { 9 | "files": ["*.js", "*.jsx"], 10 | "options": { 11 | } 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 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 |

4 |

5 | Client Debugger for Apollo GraphQL 6 |

7 | 8 | [![GitHub](https://img.shields.io/github/license/oslabs-beta/ApolloDevQL?style=plastic)](https://github.com/oslabs-beta/ApolloDevQL)[![Vulnerabilities](https://snyk.io/test/github/oslabs-beta/ApolloDevQL/badge.svg)](https://snyk.io/test/github/oslabs-beta/ApolloDevQL)![BabelPresetPrefs](https://img.shields.io/badge/babel%20preset-airbnb-ff69b4)![LintPrefs](https://img.shields.io/badge/linted%20with-eslint-blueviolet) 9 | 10 | ApolloDevQL is a debugging and querying tool for GraphQL developers. It finds the Apollo instance of your application and allows the developer to test queries, debug cache and measure query/resolver performance. 11 | 12 | Currently, ApolloDevQL 1.0 beta supports Apollo Client's 2.0 and 3.0 and transport mechansism to your GraphQL endpoint needs to be a POST request. 13 | 14 | After installing ApolloDevQL in your Chrome browser, you can test its functionalities with the following demo repositories: 15 | 16 | - [Apollo's Fullstack Tutorial](https://github.com/apollographql/fullstack-tutorial) 17 | - [React-Time](http://reactime-demo2.us-east-1.elasticbeanstalk.com) 18 | 19 | ## Installation 20 | 21 | To get started, install the ApolloDevQL [extension](https://chrome.google.com/webstore/detail/kdbhdgkakklkjhcfiighgonefimkpaeh) from Chrome Web Store. 22 | 23 | ## How to Use 24 | 25 | After installing the Chrome extension, just open up your project in the browser. 26 | 27 | Then open up your Chrome DevTools and navigate to the ApolloDevQL panel. 28 | 29 | ## Features 30 | 31 | ### GraphiQL 32 | 33 | - Run independent queries on the graphQL endpoint of the website or a secondary website using the alternative url. Inspect your queries and build queries using the explorer feature in the GraphiQL Tab. 34 | 35 |

36 | 37 |

38 | 39 | ### Events & Cache 40 | 41 | - View all of the events, queries and mutations created in your App 42 | - View the cache and details at the time of the event 43 | - Events are displayed on a timeline allowing you to track changes at each point of interaction with you app 44 | 45 |

46 | 47 |

48 | 49 | ### Performance 50 | 51 | - Displays the performance times fo all resolvers that have been run 52 | - Visually displays the performance data so you can quickly interpret the data 53 | - Performance data is sorted by time taken to track any performance issues 54 | 55 |

56 | 57 |

58 | 59 | ## Authors 60 | 61 | - **Matt Digel** - [@mdigel](https://github.com/mdigel) 62 | - **Lanre Makinde** - [@lanre-mark](https://github.com/lanre-mark) 63 | - **Rob Wise** - [@robcodehub](https://github.com/robcodehub) 64 | - **Steve Dang** - [@sdang-git](https://github.com/sdang-git) 65 | 66 | ## License 67 | 68 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details 69 | -------------------------------------------------------------------------------- /__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /__mocks__/schema.js: -------------------------------------------------------------------------------- 1 | const data = { 2 | __schema: { 3 | queryType: { 4 | name: 'Query', 5 | }, 6 | mutationType: null, 7 | subscriptionType: null, 8 | types: [ 9 | { 10 | kind: 'OBJECT', 11 | name: 'Query', 12 | description: null, 13 | fields: [ 14 | { 15 | name: 'hello', 16 | description: null, 17 | args: [], 18 | type: { 19 | kind: 'SCALAR', 20 | name: 'String', 21 | ofType: null, 22 | }, 23 | isDeprecated: false, 24 | deprecationReason: null, 25 | }, 26 | ], 27 | inputFields: null, 28 | interfaces: [], 29 | enumValues: null, 30 | possibleTypes: null, 31 | }, 32 | { 33 | kind: 'SCALAR', 34 | name: 'String', 35 | description: 36 | 'The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.', 37 | fields: null, 38 | inputFields: null, 39 | interfaces: null, 40 | enumValues: null, 41 | possibleTypes: null, 42 | }, 43 | { 44 | kind: 'ENUM', 45 | name: 'CacheControlScope', 46 | description: null, 47 | fields: null, 48 | inputFields: null, 49 | interfaces: null, 50 | enumValues: [ 51 | { 52 | name: 'PUBLIC', 53 | description: null, 54 | isDeprecated: false, 55 | deprecationReason: null, 56 | }, 57 | { 58 | name: 'PRIVATE', 59 | description: null, 60 | isDeprecated: false, 61 | deprecationReason: null, 62 | }, 63 | ], 64 | possibleTypes: null, 65 | }, 66 | { 67 | kind: 'SCALAR', 68 | name: 'Upload', 69 | description: 'The `Upload` scalar type represents a file upload.', 70 | fields: null, 71 | inputFields: null, 72 | interfaces: null, 73 | enumValues: null, 74 | possibleTypes: null, 75 | }, 76 | { 77 | kind: 'SCALAR', 78 | name: 'Int', 79 | description: 80 | 'The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.', 81 | fields: null, 82 | inputFields: null, 83 | interfaces: null, 84 | enumValues: null, 85 | possibleTypes: null, 86 | }, 87 | { 88 | kind: 'SCALAR', 89 | name: 'Boolean', 90 | description: 'The `Boolean` scalar type represents `true` or `false`.', 91 | fields: null, 92 | inputFields: null, 93 | interfaces: null, 94 | enumValues: null, 95 | possibleTypes: null, 96 | }, 97 | { 98 | kind: 'OBJECT', 99 | name: '__Schema', 100 | description: 101 | 'A GraphQL Schema defines the capabilities of a GraphQL server. It exposes all available types and directives on the server, as well as the entry points for query, mutation, and subscription operations.', 102 | fields: [ 103 | { 104 | name: 'description', 105 | description: null, 106 | args: [], 107 | type: { 108 | kind: 'SCALAR', 109 | name: 'String', 110 | ofType: null, 111 | }, 112 | isDeprecated: false, 113 | deprecationReason: null, 114 | }, 115 | { 116 | name: 'types', 117 | description: 'A list of all types supported by this server.', 118 | args: [], 119 | type: { 120 | kind: 'NON_NULL', 121 | name: null, 122 | ofType: { 123 | kind: 'LIST', 124 | name: null, 125 | ofType: { 126 | kind: 'NON_NULL', 127 | name: null, 128 | ofType: { 129 | kind: 'OBJECT', 130 | name: '__Type', 131 | ofType: null, 132 | }, 133 | }, 134 | }, 135 | }, 136 | isDeprecated: false, 137 | deprecationReason: null, 138 | }, 139 | { 140 | name: 'queryType', 141 | description: 'The type that query operations will be rooted at.', 142 | args: [], 143 | type: { 144 | kind: 'NON_NULL', 145 | name: null, 146 | ofType: { 147 | kind: 'OBJECT', 148 | name: '__Type', 149 | ofType: null, 150 | }, 151 | }, 152 | isDeprecated: false, 153 | deprecationReason: null, 154 | }, 155 | { 156 | name: 'mutationType', 157 | description: 158 | 'If this server supports mutation, the type that mutation operations will be rooted at.', 159 | args: [], 160 | type: { 161 | kind: 'OBJECT', 162 | name: '__Type', 163 | ofType: null, 164 | }, 165 | isDeprecated: false, 166 | deprecationReason: null, 167 | }, 168 | { 169 | name: 'subscriptionType', 170 | description: 171 | 'If this server support subscription, the type that subscription operations will be rooted at.', 172 | args: [], 173 | type: { 174 | kind: 'OBJECT', 175 | name: '__Type', 176 | ofType: null, 177 | }, 178 | isDeprecated: false, 179 | deprecationReason: null, 180 | }, 181 | { 182 | name: 'directives', 183 | description: 'A list of all directives supported by this server.', 184 | args: [], 185 | type: { 186 | kind: 'NON_NULL', 187 | name: null, 188 | ofType: { 189 | kind: 'LIST', 190 | name: null, 191 | ofType: { 192 | kind: 'NON_NULL', 193 | name: null, 194 | ofType: { 195 | kind: 'OBJECT', 196 | name: '__Directive', 197 | ofType: null, 198 | }, 199 | }, 200 | }, 201 | }, 202 | isDeprecated: false, 203 | deprecationReason: null, 204 | }, 205 | ], 206 | inputFields: null, 207 | interfaces: [], 208 | enumValues: null, 209 | possibleTypes: null, 210 | }, 211 | { 212 | kind: 'OBJECT', 213 | name: '__Type', 214 | description: 215 | 'The fundamental unit of any GraphQL Schema is the type. There are many kinds of types in GraphQL as represented by the `__TypeKind` enum.\n\nDepending on the kind of a type, certain fields describe information about that type. Scalar types provide no information beyond a name, description and optional `specifiedByUrl`, while Enum types provide their values. Object and Interface types provide the fields they describe. Abstract types, Union and Interface, provide the Object types possible at runtime. List and NonNull types compose other types.', 216 | fields: [ 217 | { 218 | name: 'kind', 219 | description: null, 220 | args: [], 221 | type: { 222 | kind: 'NON_NULL', 223 | name: null, 224 | ofType: { 225 | kind: 'ENUM', 226 | name: '__TypeKind', 227 | ofType: null, 228 | }, 229 | }, 230 | isDeprecated: false, 231 | deprecationReason: null, 232 | }, 233 | { 234 | name: 'name', 235 | description: null, 236 | args: [], 237 | type: { 238 | kind: 'SCALAR', 239 | name: 'String', 240 | ofType: null, 241 | }, 242 | isDeprecated: false, 243 | deprecationReason: null, 244 | }, 245 | { 246 | name: 'description', 247 | description: null, 248 | args: [], 249 | type: { 250 | kind: 'SCALAR', 251 | name: 'String', 252 | ofType: null, 253 | }, 254 | isDeprecated: false, 255 | deprecationReason: null, 256 | }, 257 | { 258 | name: 'specifiedByUrl', 259 | description: null, 260 | args: [], 261 | type: { 262 | kind: 'SCALAR', 263 | name: 'String', 264 | ofType: null, 265 | }, 266 | isDeprecated: false, 267 | deprecationReason: null, 268 | }, 269 | { 270 | name: 'fields', 271 | description: null, 272 | args: [ 273 | { 274 | name: 'includeDeprecated', 275 | description: null, 276 | type: { 277 | kind: 'SCALAR', 278 | name: 'Boolean', 279 | ofType: null, 280 | }, 281 | defaultValue: 'false', 282 | }, 283 | ], 284 | type: { 285 | kind: 'LIST', 286 | name: null, 287 | ofType: { 288 | kind: 'NON_NULL', 289 | name: null, 290 | ofType: { 291 | kind: 'OBJECT', 292 | name: '__Field', 293 | ofType: null, 294 | }, 295 | }, 296 | }, 297 | isDeprecated: false, 298 | deprecationReason: null, 299 | }, 300 | { 301 | name: 'interfaces', 302 | description: null, 303 | args: [], 304 | type: { 305 | kind: 'LIST', 306 | name: null, 307 | ofType: { 308 | kind: 'NON_NULL', 309 | name: null, 310 | ofType: { 311 | kind: 'OBJECT', 312 | name: '__Type', 313 | ofType: null, 314 | }, 315 | }, 316 | }, 317 | isDeprecated: false, 318 | deprecationReason: null, 319 | }, 320 | { 321 | name: 'possibleTypes', 322 | description: null, 323 | args: [], 324 | type: { 325 | kind: 'LIST', 326 | name: null, 327 | ofType: { 328 | kind: 'NON_NULL', 329 | name: null, 330 | ofType: { 331 | kind: 'OBJECT', 332 | name: '__Type', 333 | ofType: null, 334 | }, 335 | }, 336 | }, 337 | isDeprecated: false, 338 | deprecationReason: null, 339 | }, 340 | { 341 | name: 'enumValues', 342 | description: null, 343 | args: [ 344 | { 345 | name: 'includeDeprecated', 346 | description: null, 347 | type: { 348 | kind: 'SCALAR', 349 | name: 'Boolean', 350 | ofType: null, 351 | }, 352 | defaultValue: 'false', 353 | }, 354 | ], 355 | type: { 356 | kind: 'LIST', 357 | name: null, 358 | ofType: { 359 | kind: 'NON_NULL', 360 | name: null, 361 | ofType: { 362 | kind: 'OBJECT', 363 | name: '__EnumValue', 364 | ofType: null, 365 | }, 366 | }, 367 | }, 368 | isDeprecated: false, 369 | deprecationReason: null, 370 | }, 371 | { 372 | name: 'inputFields', 373 | description: null, 374 | args: [], 375 | type: { 376 | kind: 'LIST', 377 | name: null, 378 | ofType: { 379 | kind: 'NON_NULL', 380 | name: null, 381 | ofType: { 382 | kind: 'OBJECT', 383 | name: '__InputValue', 384 | ofType: null, 385 | }, 386 | }, 387 | }, 388 | isDeprecated: false, 389 | deprecationReason: null, 390 | }, 391 | { 392 | name: 'ofType', 393 | description: null, 394 | args: [], 395 | type: { 396 | kind: 'OBJECT', 397 | name: '__Type', 398 | ofType: null, 399 | }, 400 | isDeprecated: false, 401 | deprecationReason: null, 402 | }, 403 | ], 404 | inputFields: null, 405 | interfaces: [], 406 | enumValues: null, 407 | possibleTypes: null, 408 | }, 409 | { 410 | kind: 'ENUM', 411 | name: '__TypeKind', 412 | description: 413 | 'An enum describing what kind of type a given `__Type` is.', 414 | fields: null, 415 | inputFields: null, 416 | interfaces: null, 417 | enumValues: [ 418 | { 419 | name: 'SCALAR', 420 | description: 'Indicates this type is a scalar.', 421 | isDeprecated: false, 422 | deprecationReason: null, 423 | }, 424 | { 425 | name: 'OBJECT', 426 | description: 427 | 'Indicates this type is an object. `fields` and `interfaces` are valid fields.', 428 | isDeprecated: false, 429 | deprecationReason: null, 430 | }, 431 | { 432 | name: 'INTERFACE', 433 | description: 434 | 'Indicates this type is an interface. `fields`, `interfaces`, and `possibleTypes` are valid fields.', 435 | isDeprecated: false, 436 | deprecationReason: null, 437 | }, 438 | { 439 | name: 'UNION', 440 | description: 441 | 'Indicates this type is a union. `possibleTypes` is a valid field.', 442 | isDeprecated: false, 443 | deprecationReason: null, 444 | }, 445 | { 446 | name: 'ENUM', 447 | description: 448 | 'Indicates this type is an enum. `enumValues` is a valid field.', 449 | isDeprecated: false, 450 | deprecationReason: null, 451 | }, 452 | { 453 | name: 'INPUT_OBJECT', 454 | description: 455 | 'Indicates this type is an input object. `inputFields` is a valid field.', 456 | isDeprecated: false, 457 | deprecationReason: null, 458 | }, 459 | { 460 | name: 'LIST', 461 | description: 462 | 'Indicates this type is a list. `ofType` is a valid field.', 463 | isDeprecated: false, 464 | deprecationReason: null, 465 | }, 466 | { 467 | name: 'NON_NULL', 468 | description: 469 | 'Indicates this type is a non-null. `ofType` is a valid field.', 470 | isDeprecated: false, 471 | deprecationReason: null, 472 | }, 473 | ], 474 | possibleTypes: null, 475 | }, 476 | { 477 | kind: 'OBJECT', 478 | name: '__Field', 479 | description: 480 | 'Object and Interface types are described by a list of Fields, each of which has a name, potentially a list of arguments, and a return type.', 481 | fields: [ 482 | { 483 | name: 'name', 484 | description: null, 485 | args: [], 486 | type: { 487 | kind: 'NON_NULL', 488 | name: null, 489 | ofType: { 490 | kind: 'SCALAR', 491 | name: 'String', 492 | ofType: null, 493 | }, 494 | }, 495 | isDeprecated: false, 496 | deprecationReason: null, 497 | }, 498 | { 499 | name: 'description', 500 | description: null, 501 | args: [], 502 | type: { 503 | kind: 'SCALAR', 504 | name: 'String', 505 | ofType: null, 506 | }, 507 | isDeprecated: false, 508 | deprecationReason: null, 509 | }, 510 | { 511 | name: 'args', 512 | description: null, 513 | args: [], 514 | type: { 515 | kind: 'NON_NULL', 516 | name: null, 517 | ofType: { 518 | kind: 'LIST', 519 | name: null, 520 | ofType: { 521 | kind: 'NON_NULL', 522 | name: null, 523 | ofType: { 524 | kind: 'OBJECT', 525 | name: '__InputValue', 526 | ofType: null, 527 | }, 528 | }, 529 | }, 530 | }, 531 | isDeprecated: false, 532 | deprecationReason: null, 533 | }, 534 | { 535 | name: 'type', 536 | description: null, 537 | args: [], 538 | type: { 539 | kind: 'NON_NULL', 540 | name: null, 541 | ofType: { 542 | kind: 'OBJECT', 543 | name: '__Type', 544 | ofType: null, 545 | }, 546 | }, 547 | isDeprecated: false, 548 | deprecationReason: null, 549 | }, 550 | { 551 | name: 'isDeprecated', 552 | description: null, 553 | args: [], 554 | type: { 555 | kind: 'NON_NULL', 556 | name: null, 557 | ofType: { 558 | kind: 'SCALAR', 559 | name: 'Boolean', 560 | ofType: null, 561 | }, 562 | }, 563 | isDeprecated: false, 564 | deprecationReason: null, 565 | }, 566 | { 567 | name: 'deprecationReason', 568 | description: null, 569 | args: [], 570 | type: { 571 | kind: 'SCALAR', 572 | name: 'String', 573 | ofType: null, 574 | }, 575 | isDeprecated: false, 576 | deprecationReason: null, 577 | }, 578 | ], 579 | inputFields: null, 580 | interfaces: [], 581 | enumValues: null, 582 | possibleTypes: null, 583 | }, 584 | { 585 | kind: 'OBJECT', 586 | name: '__InputValue', 587 | description: 588 | 'Arguments provided to Fields or Directives and the input fields of an InputObject are represented as Input Values which describe their type and optionally a default value.', 589 | fields: [ 590 | { 591 | name: 'name', 592 | description: null, 593 | args: [], 594 | type: { 595 | kind: 'NON_NULL', 596 | name: null, 597 | ofType: { 598 | kind: 'SCALAR', 599 | name: 'String', 600 | ofType: null, 601 | }, 602 | }, 603 | isDeprecated: false, 604 | deprecationReason: null, 605 | }, 606 | { 607 | name: 'description', 608 | description: null, 609 | args: [], 610 | type: { 611 | kind: 'SCALAR', 612 | name: 'String', 613 | ofType: null, 614 | }, 615 | isDeprecated: false, 616 | deprecationReason: null, 617 | }, 618 | { 619 | name: 'type', 620 | description: null, 621 | args: [], 622 | type: { 623 | kind: 'NON_NULL', 624 | name: null, 625 | ofType: { 626 | kind: 'OBJECT', 627 | name: '__Type', 628 | ofType: null, 629 | }, 630 | }, 631 | isDeprecated: false, 632 | deprecationReason: null, 633 | }, 634 | { 635 | name: 'defaultValue', 636 | description: 637 | 'A GraphQL-formatted string representing the default value for this input value.', 638 | args: [], 639 | type: { 640 | kind: 'SCALAR', 641 | name: 'String', 642 | ofType: null, 643 | }, 644 | isDeprecated: false, 645 | deprecationReason: null, 646 | }, 647 | ], 648 | inputFields: null, 649 | interfaces: [], 650 | enumValues: null, 651 | possibleTypes: null, 652 | }, 653 | { 654 | kind: 'OBJECT', 655 | name: '__EnumValue', 656 | description: 657 | 'One possible value for a given Enum. Enum values are unique values, not a placeholder for a string or numeric value. However an Enum value is returned in a JSON response as a string.', 658 | fields: [ 659 | { 660 | name: 'name', 661 | description: null, 662 | args: [], 663 | type: { 664 | kind: 'NON_NULL', 665 | name: null, 666 | ofType: { 667 | kind: 'SCALAR', 668 | name: 'String', 669 | ofType: null, 670 | }, 671 | }, 672 | isDeprecated: false, 673 | deprecationReason: null, 674 | }, 675 | { 676 | name: 'description', 677 | description: null, 678 | args: [], 679 | type: { 680 | kind: 'SCALAR', 681 | name: 'String', 682 | ofType: null, 683 | }, 684 | isDeprecated: false, 685 | deprecationReason: null, 686 | }, 687 | { 688 | name: 'isDeprecated', 689 | description: null, 690 | args: [], 691 | type: { 692 | kind: 'NON_NULL', 693 | name: null, 694 | ofType: { 695 | kind: 'SCALAR', 696 | name: 'Boolean', 697 | ofType: null, 698 | }, 699 | }, 700 | isDeprecated: false, 701 | deprecationReason: null, 702 | }, 703 | { 704 | name: 'deprecationReason', 705 | description: null, 706 | args: [], 707 | type: { 708 | kind: 'SCALAR', 709 | name: 'String', 710 | ofType: null, 711 | }, 712 | isDeprecated: false, 713 | deprecationReason: null, 714 | }, 715 | ], 716 | inputFields: null, 717 | interfaces: [], 718 | enumValues: null, 719 | possibleTypes: null, 720 | }, 721 | { 722 | kind: 'OBJECT', 723 | name: '__Directive', 724 | description: 725 | "A Directive provides a way to describe alternate runtime execution and type validation behavior in a GraphQL document.\n\nIn some cases, you need to provide options to alter GraphQL's execution behavior in ways field arguments will not suffice, such as conditionally including or skipping a field. Directives provide this by describing additional information to the executor.", 726 | fields: [ 727 | { 728 | name: 'name', 729 | description: null, 730 | args: [], 731 | type: { 732 | kind: 'NON_NULL', 733 | name: null, 734 | ofType: { 735 | kind: 'SCALAR', 736 | name: 'String', 737 | ofType: null, 738 | }, 739 | }, 740 | isDeprecated: false, 741 | deprecationReason: null, 742 | }, 743 | { 744 | name: 'description', 745 | description: null, 746 | args: [], 747 | type: { 748 | kind: 'SCALAR', 749 | name: 'String', 750 | ofType: null, 751 | }, 752 | isDeprecated: false, 753 | deprecationReason: null, 754 | }, 755 | { 756 | name: 'isRepeatable', 757 | description: null, 758 | args: [], 759 | type: { 760 | kind: 'NON_NULL', 761 | name: null, 762 | ofType: { 763 | kind: 'SCALAR', 764 | name: 'Boolean', 765 | ofType: null, 766 | }, 767 | }, 768 | isDeprecated: false, 769 | deprecationReason: null, 770 | }, 771 | { 772 | name: 'locations', 773 | description: null, 774 | args: [], 775 | type: { 776 | kind: 'NON_NULL', 777 | name: null, 778 | ofType: { 779 | kind: 'LIST', 780 | name: null, 781 | ofType: { 782 | kind: 'NON_NULL', 783 | name: null, 784 | ofType: { 785 | kind: 'ENUM', 786 | name: '__DirectiveLocation', 787 | ofType: null, 788 | }, 789 | }, 790 | }, 791 | }, 792 | isDeprecated: false, 793 | deprecationReason: null, 794 | }, 795 | { 796 | name: 'args', 797 | description: null, 798 | args: [], 799 | type: { 800 | kind: 'NON_NULL', 801 | name: null, 802 | ofType: { 803 | kind: 'LIST', 804 | name: null, 805 | ofType: { 806 | kind: 'NON_NULL', 807 | name: null, 808 | ofType: { 809 | kind: 'OBJECT', 810 | name: '__InputValue', 811 | ofType: null, 812 | }, 813 | }, 814 | }, 815 | }, 816 | isDeprecated: false, 817 | deprecationReason: null, 818 | }, 819 | ], 820 | inputFields: null, 821 | interfaces: [], 822 | enumValues: null, 823 | possibleTypes: null, 824 | }, 825 | { 826 | kind: 'ENUM', 827 | name: '__DirectiveLocation', 828 | description: 829 | 'A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies.', 830 | fields: null, 831 | inputFields: null, 832 | interfaces: null, 833 | enumValues: [ 834 | { 835 | name: 'QUERY', 836 | description: 'Location adjacent to a query operation.', 837 | isDeprecated: false, 838 | deprecationReason: null, 839 | }, 840 | { 841 | name: 'MUTATION', 842 | description: 'Location adjacent to a mutation operation.', 843 | isDeprecated: false, 844 | deprecationReason: null, 845 | }, 846 | { 847 | name: 'SUBSCRIPTION', 848 | description: 'Location adjacent to a subscription operation.', 849 | isDeprecated: false, 850 | deprecationReason: null, 851 | }, 852 | { 853 | name: 'FIELD', 854 | description: 'Location adjacent to a field.', 855 | isDeprecated: false, 856 | deprecationReason: null, 857 | }, 858 | { 859 | name: 'FRAGMENT_DEFINITION', 860 | description: 'Location adjacent to a fragment definition.', 861 | isDeprecated: false, 862 | deprecationReason: null, 863 | }, 864 | { 865 | name: 'FRAGMENT_SPREAD', 866 | description: 'Location adjacent to a fragment spread.', 867 | isDeprecated: false, 868 | deprecationReason: null, 869 | }, 870 | { 871 | name: 'INLINE_FRAGMENT', 872 | description: 'Location adjacent to an inline fragment.', 873 | isDeprecated: false, 874 | deprecationReason: null, 875 | }, 876 | { 877 | name: 'VARIABLE_DEFINITION', 878 | description: 'Location adjacent to a variable definition.', 879 | isDeprecated: false, 880 | deprecationReason: null, 881 | }, 882 | { 883 | name: 'SCHEMA', 884 | description: 'Location adjacent to a schema definition.', 885 | isDeprecated: false, 886 | deprecationReason: null, 887 | }, 888 | { 889 | name: 'SCALAR', 890 | description: 'Location adjacent to a scalar definition.', 891 | isDeprecated: false, 892 | deprecationReason: null, 893 | }, 894 | { 895 | name: 'OBJECT', 896 | description: 'Location adjacent to an object type definition.', 897 | isDeprecated: false, 898 | deprecationReason: null, 899 | }, 900 | { 901 | name: 'FIELD_DEFINITION', 902 | description: 'Location adjacent to a field definition.', 903 | isDeprecated: false, 904 | deprecationReason: null, 905 | }, 906 | { 907 | name: 'ARGUMENT_DEFINITION', 908 | description: 'Location adjacent to an argument definition.', 909 | isDeprecated: false, 910 | deprecationReason: null, 911 | }, 912 | { 913 | name: 'INTERFACE', 914 | description: 'Location adjacent to an interface definition.', 915 | isDeprecated: false, 916 | deprecationReason: null, 917 | }, 918 | { 919 | name: 'UNION', 920 | description: 'Location adjacent to a union definition.', 921 | isDeprecated: false, 922 | deprecationReason: null, 923 | }, 924 | { 925 | name: 'ENUM', 926 | description: 'Location adjacent to an enum definition.', 927 | isDeprecated: false, 928 | deprecationReason: null, 929 | }, 930 | { 931 | name: 'ENUM_VALUE', 932 | description: 'Location adjacent to an enum value definition.', 933 | isDeprecated: false, 934 | deprecationReason: null, 935 | }, 936 | { 937 | name: 'INPUT_OBJECT', 938 | description: 939 | 'Location adjacent to an input object type definition.', 940 | isDeprecated: false, 941 | deprecationReason: null, 942 | }, 943 | { 944 | name: 'INPUT_FIELD_DEFINITION', 945 | description: 946 | 'Location adjacent to an input object field definition.', 947 | isDeprecated: false, 948 | deprecationReason: null, 949 | }, 950 | ], 951 | possibleTypes: null, 952 | }, 953 | ], 954 | directives: [ 955 | { 956 | name: 'cacheControl', 957 | description: null, 958 | locations: ['FIELD_DEFINITION', 'OBJECT', 'INTERFACE'], 959 | args: [ 960 | { 961 | name: 'maxAge', 962 | description: null, 963 | type: { 964 | kind: 'SCALAR', 965 | name: 'Int', 966 | ofType: null, 967 | }, 968 | defaultValue: null, 969 | }, 970 | { 971 | name: 'scope', 972 | description: null, 973 | type: { 974 | kind: 'ENUM', 975 | name: 'CacheControlScope', 976 | ofType: null, 977 | }, 978 | defaultValue: null, 979 | }, 980 | ], 981 | }, 982 | { 983 | name: 'skip', 984 | description: 985 | 'Directs the executor to skip this field or fragment when the `if` argument is true.', 986 | locations: ['FIELD', 'FRAGMENT_SPREAD', 'INLINE_FRAGMENT'], 987 | args: [ 988 | { 989 | name: 'if', 990 | description: 'Skipped when true.', 991 | type: { 992 | kind: 'NON_NULL', 993 | name: null, 994 | ofType: { 995 | kind: 'SCALAR', 996 | name: 'Boolean', 997 | ofType: null, 998 | }, 999 | }, 1000 | defaultValue: null, 1001 | }, 1002 | ], 1003 | }, 1004 | { 1005 | name: 'include', 1006 | description: 1007 | 'Directs the executor to include this field or fragment only when the `if` argument is true.', 1008 | locations: ['FIELD', 'FRAGMENT_SPREAD', 'INLINE_FRAGMENT'], 1009 | args: [ 1010 | { 1011 | name: 'if', 1012 | description: 'Included when true.', 1013 | type: { 1014 | kind: 'NON_NULL', 1015 | name: null, 1016 | ofType: { 1017 | kind: 'SCALAR', 1018 | name: 'Boolean', 1019 | ofType: null, 1020 | }, 1021 | }, 1022 | defaultValue: null, 1023 | }, 1024 | ], 1025 | }, 1026 | { 1027 | name: 'deprecated', 1028 | description: 1029 | 'Marks an element of a GraphQL schema as no longer supported.', 1030 | locations: ['FIELD_DEFINITION', 'ENUM_VALUE'], 1031 | args: [ 1032 | { 1033 | name: 'reason', 1034 | description: 1035 | 'Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted using the Markdown syntax, as specified by [CommonMark](https://commonmark.org/).', 1036 | type: { 1037 | kind: 'SCALAR', 1038 | name: 'String', 1039 | ofType: null, 1040 | }, 1041 | defaultValue: '"No longer supported"', 1042 | }, 1043 | ], 1044 | }, 1045 | { 1046 | name: 'specifiedBy', 1047 | description: 1048 | 'Exposes a URL that specifies the behaviour of this scalar.', 1049 | locations: ['SCALAR'], 1050 | args: [ 1051 | { 1052 | name: 'url', 1053 | description: 'The URL that specifies the behaviour of this scalar.', 1054 | type: { 1055 | kind: 'NON_NULL', 1056 | name: null, 1057 | ofType: { 1058 | kind: 'SCALAR', 1059 | name: 'String', 1060 | ofType: null, 1061 | }, 1062 | }, 1063 | defaultValue: null, 1064 | }, 1065 | ], 1066 | }, 1067 | ], 1068 | }, 1069 | }; 1070 | 1071 | export default data; 1072 | -------------------------------------------------------------------------------- /__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /__mocks__/themeMock.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {mount} from 'enzyme'; 3 | 4 | import Apollo11ThemeProvider from '../src/app/Panel/themes/ThemeProvider'; 5 | import normal from '../src/app/Panel/themes/normal'; 6 | 7 | /* 8 | import { ThemeProvider } from "styled-components"; 9 | import { yourTheme } from "path-to-your-theme"; 10 | 11 | const getThemeProviderWrappingComponent = theme => ({ children }) => ( 12 | {children} 13 | ); 14 | 15 | export const shallowWithTheme = (tree: React.Node, theme: Object = normal) => { 16 | return shallow(tree, { 17 | wrappingComponent: getThemeProviderWrappingComponent(theme) 18 | }) 19 | .dive() 20 | .dive(); 21 | }; 22 | 23 | export const mountWithTheme = (component: React.Node, theme: Object = normal) => { 24 | const wrapper = mount(component, { 25 | wrappingComponent: getThemeProviderWrappingComponent(theme) 26 | }); 27 | 28 | return wrapper; 29 | }; 30 | 31 | */ 32 | 33 | const getThemeProviderWrappingComponent = () => 34 | function wrappedTheme({children}) { 35 | return ( 36 | {children} 37 | ); 38 | }; 39 | 40 | const mountWithTheme = (component, theme = normal) => { 41 | const wrapper = mount(component, { 42 | wrappingComponent: getThemeProviderWrappingComponent(theme), 43 | }); 44 | 45 | return wrapper; 46 | }; 47 | 48 | export default mountWithTheme; 49 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@babel/preset-env', 4 | '@babel/preset-react', 5 | '@babel/preset-typescript', 6 | ], 7 | plugins: ['@babel/plugin-proposal-class-properties'], 8 | }; 9 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | Object.assign(global, require('jest-chrome')); 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apollodevql", 3 | "version": "1.0.0", 4 | "description": "ApolloDevQL.", 5 | "main": "index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/oslabs-beta/ApolloDevQL" 9 | }, 10 | "scripts": { 11 | "build": "webpack --mode production", 12 | "dev": "webpack --mode development --watch", 13 | "test": "jest --coverage --verbose --silent", 14 | "test-dev": "jest --coverage --verbose", 15 | "snap": "jest --u --coverage --verbose --silent", 16 | "verify:test": "jest --verbose --coverage --silent", 17 | "verify:lint": "eslint '**/*.tsx' '**/*.ts' '**/*.js' --ignore-path .gitignore", 18 | "precommit": "npm-run-all --parallel verify:lint", 19 | "validate": "npm-run-all --parallel verify:*" 20 | }, 21 | "author": "matt rob lanre steve", 22 | "license": "MIT", 23 | "devDependencies": { 24 | "@babel/core": "^7.12.3", 25 | "@babel/plugin-proposal-class-properties": "^7.12.1", 26 | "@babel/preset-env": "^7.12.1", 27 | "@babel/preset-react": "^7.12.1", 28 | "@babel/preset-typescript": "^7.12.1", 29 | "@types/chrome": "0.0.124", 30 | "@types/react": "^16.9.52", 31 | "@types/react-dom": "^16.9.8", 32 | "@typescript-eslint/eslint-plugin": "^4.4.1", 33 | "@typescript-eslint/parser": "^4.4.1", 34 | "babel-eslint": "^10.1.0", 35 | "babel-jest": "^26.6.0", 36 | "babel-loader": "^8.1.0", 37 | "css-loader": "^3.6.0", 38 | "enzyme": "^3.11.0", 39 | "enzyme-adapter-react-16": "^1.15.5", 40 | "enzyme-to-json": "^3.6.1", 41 | "eslint": "^7.11.0", 42 | "eslint-config-airbnb": "^18.2.0", 43 | "eslint-config-prettier": "^6.12.0", 44 | "eslint-plugin-babel": "^5.3.1", 45 | "eslint-plugin-import": "^2.22.1", 46 | "eslint-plugin-jsx-a11y": "^6.3.1", 47 | "eslint-plugin-prettier": "^3.1.4", 48 | "eslint-plugin-react": "^7.21.5", 49 | "eslint-plugin-react-hooks": "^4.2.0", 50 | "ghooks": "^2.0.4", 51 | "jest": "^26.6.0", 52 | "jest-chrome": "^0.7.0", 53 | "npm-run-all": "^4.1.5", 54 | "prettier": "^2.1.2", 55 | "style-loader": "^1.3.0", 56 | "ts-loader": "^7.0.5", 57 | "typescript": "^3.9.7", 58 | "webpack": "^4.44.2", 59 | "webpack-chrome-extension-reloader": "^1.3.0", 60 | "webpack-cli": "^3.3.12" 61 | }, 62 | "dependencies": { 63 | "@emotion/core": "^10.0.35", 64 | "@material-ui/core": "^4.11.0", 65 | "@material-ui/icons": "^4.9.1", 66 | "@material-ui/lab": "^4.0.0-alpha.56", 67 | "clsx": "^1.1.1", 68 | "graphiql": "^1.0.5", 69 | "graphiql-explorer": "^0.6.2", 70 | "graphql": "^14.1.1", 71 | "react": "^16.14.0", 72 | "react-dom": "^16.14.0", 73 | "react-grid-layout": "^1.1.1", 74 | "react-json-view": "^1.19.1", 75 | "react-resizable": "^1.11.0", 76 | "react-spinners": "^0.9.0" 77 | }, 78 | "engines": { 79 | "node": "^10.12.0 || >=12.0.0" 80 | }, 81 | "config": { 82 | "ghooks": {} 83 | }, 84 | "jest": { 85 | "snapshotSerializers": [ 86 | "enzyme-to-json/serializer" 87 | ], 88 | "moduleNameMapper": { 89 | "\\.(css|less|sass|scss)$": "/__mocks__/styleMock.js", 90 | "\\.(gif|ttf|eot|svg)$": "/__mocks__/fileMock.js" 91 | }, 92 | "setupFilesAfterEnv": [ 93 | "./jest.setup.js" 94 | ] 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/app/Events_Tab/ApolloTabResponsive.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import {createStyles, makeStyles, Theme} from '@material-ui/core/styles'; 3 | import AppBar from '@material-ui/core/AppBar'; 4 | import Grid from '@material-ui/core/Grid'; 5 | import Paper from '@material-ui/core/Paper'; 6 | import Tabs from '@material-ui/core/Tabs'; 7 | import Tab from '@material-ui/core/Tab'; 8 | import Toolbar from '@material-ui/core/Toolbar'; 9 | import Typography from '@material-ui/core/Typography'; 10 | 11 | // React Grid Layout 12 | import RGL, {WidthProvider} from 'react-grid-layout'; 13 | 14 | import {ApolloResponsiveTabProps} from '../utils/managedlog/lib/eventLogData'; 15 | import Cache from './Cache'; 16 | import CacheDetails from './CacheDetails'; 17 | import EventLog from './EventLog'; 18 | import EventDetails from './EventDetails'; 19 | import EventPanel from './EventPanel'; 20 | import EventNode from '../utils/managedlog/lib/eventLogNode'; 21 | import {Apollo11ThemeContext} from '../Panel/themes/ThemeProvider'; 22 | 23 | // interface Props extends StyledComponentProps> { 24 | // myProp: string; 25 | // } 26 | 27 | // React Grid Layout 28 | const ReactGridLayout = WidthProvider(RGL); 29 | 30 | const layoutArray = [ 31 | {i: '1', x: 0, y: 0, w: 3, h: 22}, 32 | {i: '2', x: 3, y: 0, w: 9, h: 22}, 33 | ]; 34 | 35 | const useStyles: any = makeStyles((theme: Theme) => 36 | createStyles({ 37 | root: { 38 | flexGrow: 1, 39 | // overflow: 'scroll', 40 | }, 41 | eventGrid: { 42 | width: '100%', 43 | marginLeft: '1%', 44 | boxShadow: '1px 1px 1px #fff', 45 | borderRadius: '1px', 46 | overflow: 'auto', 47 | }, 48 | mainGrid: { 49 | width: '100%', 50 | marginLeft: '1%', 51 | boxShadow: '1px 1px 1px #fff', 52 | borderRadius: '1px', 53 | overflow: 'auto', 54 | }, 55 | grid: { 56 | height: '100vh', 57 | width: '100%', 58 | marginLeft: '1%', 59 | boxShadow: '1px 1px 1px #fff', 60 | borderRadius: '1px', 61 | // overflow: 'auto', 62 | }, 63 | gridContainer: { 64 | display: 'flex', 65 | flexDirection: 'row', 66 | }, 67 | cacheDetails: { 68 | borderStyle: 'solid', 69 | overflow: 'auto', 70 | }, 71 | cacheGrid: { 72 | height: '100vh', 73 | boxShadow: '1px 1px 1px #fff', 74 | borderRadius: '1px', 75 | overflow: 'auto', 76 | }, 77 | paper: { 78 | color: theme.palette.text.secondary, 79 | // height: '100vh', 80 | // 'overflow-y': 'hidden', 81 | // 'overflow-x': 'auto', 82 | }, 83 | paperJson: (props: any) => ({ 84 | color: theme.palette.text.secondary, 85 | height: '100vh', 86 | overflow: 'auto', 87 | backgroundColor: props.isDark ? 'black' : '', 88 | }), 89 | tabPaper: { 90 | flexGrow: 1, 91 | backgroundColor: theme.palette.background.paper, 92 | }, 93 | }), 94 | ); 95 | 96 | function ApolloTabResponsive({ 97 | eventLog, 98 | isDraggable, 99 | isResizable, 100 | items, 101 | rowHeight, 102 | cols, 103 | verticalCompact, 104 | resizeHandles, 105 | compactType, 106 | preventCollision, 107 | autoSize, 108 | margin, 109 | }: ApolloResponsiveTabProps) { 110 | const {isDark} = React.useContext(Apollo11ThemeContext); 111 | const classes = useStyles({isDark}); 112 | // const [cacheDetailsVisible, setCacheDetailsVisible] = useState(() => false); 113 | const [activeEvent, setActiveEvent] = useState( 114 | (): EventNode => { 115 | return eventLog.eventHead; 116 | }, 117 | ); 118 | const [activeCache, setActiveCache] = useState(() => ({})); 119 | 120 | const [tabValue, setTabValue] = useState(() => 0); 121 | 122 | // Function to change the active event key to pass active event to components 123 | const handleEventChange = (e: EventNode) => { 124 | setActiveEvent(e); 125 | }; 126 | 127 | const handleTabChange = (event: any, value: any) => { 128 | setTabValue(value); 129 | }; 130 | 131 | // Function to change the active cache key to pass to components 132 | const handleCacheChange = (e: any) => { 133 | setActiveCache(e); 134 | }; 135 | 136 | return ( 137 |
138 | 153 | {/* */} 154 |
158 | 159 | 160 | 161 | Event Log 162 | 163 | 164 | 165 | 166 | 167 | 171 | 172 |
173 | {/*
*/} 174 | 175 |
179 | {/*
*/} 183 | 184 | 185 |
186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 207 | 208 | 209 | 210 | 211 | 212 | 216 | 217 | 218 | 219 | 220 |
221 |
222 |
223 | 224 |
225 | ); 226 | } 227 | 228 | ApolloTabResponsive.defaultProps = { 229 | isDraggable: true, 230 | isResizable: true, 231 | items: 2, 232 | rowHeight: 22, 233 | cols: 12, 234 | verticalCompact: true, 235 | resizeHandles: ['e', 'ne', 'se'], 236 | autoSize: true, 237 | compactType: 'vertical', 238 | preventCollision: false, 239 | margin: [10, 10], 240 | }; 241 | 242 | export default ApolloTabResponsive; 243 | -------------------------------------------------------------------------------- /src/app/Events_Tab/Cache.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // Material Ui 3 | import List from '@material-ui/core/List'; 4 | import ListItem from '@material-ui/core/ListItem'; 5 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 6 | import ListItemText from '@material-ui/core/ListItemText'; 7 | import StorageIcon from '@material-ui/icons/Storage'; 8 | // Project Files 9 | import {CacheProps} from '../utils/managedlog/lib/eventLogNode'; 10 | 11 | const Cache = ({activeEvent, handleCacheChange}: CacheProps) => { 12 | if (activeEvent === null) return <>; 13 | const { 14 | content: {event, cache}, 15 | } = activeEvent; 16 | 17 | return ( 18 |
19 | {event ? ( 20 | 21 | {cache && 22 | Object.keys(cache).map((cacheItem: any) => { 23 | const cacheString = cacheItem.toString(); 24 | // Check if key is 0 or undefined 25 | if ( 26 | cacheItem === 0 || 27 | cacheItem === undefined || 28 | cacheItem === 'undefined' || 29 | !cacheItem || 30 | cacheItem === '0' || 31 | cacheString === '0' 32 | ) { 33 | // if key is 0 or undefined, just log it to the console and return so the next lines of code don't run 34 | return undefined; // console.log('CACHE === undefined', cacheItem); 35 | } 36 | 37 | return ( 38 | handleCacheChange(cacheItem)}> 42 | 43 | 44 | 45 | 46 | 47 | ); 48 | })} 49 | 50 | ) : ( 51 |
No event selected
52 | )} 53 |
54 | ); 55 | }; 56 | 57 | export default Cache; 58 | -------------------------------------------------------------------------------- /src/app/Events_Tab/CacheDetails.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactJson from 'react-json-view'; 3 | import Typography from '@material-ui/core/Typography'; 4 | 5 | import {CacheDetailsProps} from '../utils/managedlog/lib/eventLogNode'; 6 | import {Apollo11ThemeContext} from '../Panel/themes/ThemeProvider'; 7 | 8 | const CacheDetails = ({activeEvent, activeCache}: CacheDetailsProps) => { 9 | const {isDark} = React.useContext(Apollo11ThemeContext); 10 | 11 | if (activeEvent === null) return <>; 12 | const { 13 | content: {event, cache}, 14 | } = activeEvent; 15 | return ( 16 |
17 | {event ? ( 18 |
19 | 20 | 25 | 26 |
27 | ) : ( 28 |
No cache item selected
29 | )} 30 |
31 | ); 32 | }; 33 | 34 | export default CacheDetails; 35 | -------------------------------------------------------------------------------- /src/app/Events_Tab/EventDetails.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactJson from 'react-json-view'; 3 | import Typography from '@material-ui/core/Typography'; 4 | 5 | import {EventDetailsProps} from '../utils/managedlog/lib/eventLogNode'; 6 | import {Apollo11ThemeContext} from '../Panel/themes/ThemeProvider'; 7 | 8 | const EventDetails = ({activeEvent}: EventDetailsProps) => { 9 | const {isDark} = React.useContext(Apollo11ThemeContext); 10 | 11 | if (activeEvent === null) return <>; 12 | const { 13 | content: {event}, 14 | } = activeEvent; // eventId 15 | const { 16 | request: { 17 | operation: {operationName, query}, 18 | }, 19 | } = event; 20 | const {variables} = event; 21 | return ( 22 |
23 | {event ? ( 24 |
25 | 26 | 35 | 36 | 37 |
38 | ) : ( 39 |
No event selected
40 | )} 41 |
42 | ); 43 | }; 44 | 45 | export default EventDetails; 46 | -------------------------------------------------------------------------------- /src/app/Events_Tab/EventLog.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // Material UI 3 | import {makeStyles, createStyles, Theme} from '@material-ui/core/styles'; 4 | import List from '@material-ui/core/List'; 5 | import ListItem from '@material-ui/core/ListItem'; 6 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 7 | import ListItemText from '@material-ui/core/ListItemText'; 8 | import Avatar from '@material-ui/core/Avatar'; 9 | // Project Props 10 | import {EventLogProps} from '../utils/managedlog/lib/eventLogData'; 11 | 12 | const useStyles = makeStyles((theme: Theme) => 13 | createStyles({ 14 | smallAvatar: { 15 | width: theme.spacing(3), 16 | height: theme.spacing(3), 17 | fontSize: '16px', 18 | fontWeight: 900, 19 | }, 20 | iconDiv: { 21 | minWidth: '30px', 22 | }, 23 | }), 24 | ); 25 | 26 | const EventLog = ({eventLog, handleEventChange}: EventLogProps) => { 27 | const classes = useStyles(); 28 | 29 | return ( 30 |
31 | 32 | {eventLog.eventLength ? ( 33 | eventLog.map((eventNode: any): any => { 34 | // DoubleLinkedList Impementation of thee EventLog 35 | const { 36 | content: {event, eventId}, 37 | } = eventNode; // destructure event and eventID from content node of linkedList 38 | 39 | const eventString = eventId.toString(); 40 | 41 | // if statement to handle key of 0 and undefined 42 | if ( 43 | event === 0 || 44 | event === undefined || 45 | event === 'undefined' || 46 | event === 'queryIdCounter' || 47 | event === 'mutationIdCounter' || 48 | event === 'requestIdCounter' || 49 | event === 'idCounter' || 50 | event === 'lastEventId' || 51 | !event || 52 | event === '0' || 53 | eventString === '0' 54 | // !eventLog[event].request 55 | ) { 56 | // if key is 0 or undefined, just log it to the console and return so the next lines of code don't run 57 | return undefined; // console.log('Event === undefined', event); 58 | } 59 | 60 | return ( 61 | handleEventChange(eventNode)}> 65 | 66 | {/* Show a Q or M based on query or mutation */} 67 | {eventNode.content.type === 'query' ? ( 68 | Q 69 | ) : ( 70 | '' 71 | )} 72 | {eventNode.content.type === 'mutation' ? ( 73 | M 74 | ) : ( 75 | '' 76 | )} 77 | 78 | 79 | 80 | ); 81 | }) 82 | ) : ( 83 | <> 84 | )} 85 | 86 |
87 | ); 88 | }; 89 | 90 | export default EventLog; 91 | -------------------------------------------------------------------------------- /src/app/Events_Tab/EventPanel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {EventPanelType} from '../utils/managedlog/lib/eventLogNode'; 4 | 5 | export default function EventPanel({ 6 | children, 7 | panelValue, 8 | panelIndex, 9 | }: EventPanelType) { 10 | return ( 11 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/app/Events_Tab/__tests__/ApolloTab_test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {configure} from 'enzyme'; 3 | import Adapter from 'enzyme-adapter-react-16'; 4 | 5 | import ApolloTabResponsive from '../ApolloTabResponsive'; 6 | import mountWithTheme from '../../../../__mocks__/themeMock'; 7 | 8 | configure({ 9 | adapter: new Adapter(), 10 | }); 11 | 12 | describe('snapshot tests', () => { 13 | const testEvents = { 14 | eventHead: null, 15 | eventLength: 0, 16 | eventTail: null, 17 | }; 18 | let wrapper; 19 | 20 | it('should mount the app', () => { 21 | wrapper = mountWithTheme(); 22 | expect(wrapper).toMatchSnapshot(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/app/GraphiQL_Tab/GraphiQLPage.tsx: -------------------------------------------------------------------------------- 1 | // Decision to be made about whether a custom endpoint can be entered to run queries on other graphql endpoints. 2 | // This decision should include whether this endpoint would be stored in local state or global state 3 | // Also to consider whether other tabs would require this endpoint 4 | 5 | import React, {FunctionComponent, useState, useEffect} from 'react'; 6 | import 'graphiql/graphiql.min.css'; 7 | import '../Panel/index.css'; 8 | import {createStyles, makeStyles, Theme} from '@material-ui/core/styles'; // Colors for TextField component 9 | import TextField from '@material-ui/core/TextField'; 10 | import Typography from '@material-ui/core/Typography'; 11 | import Radio from '@material-ui/core/Radio'; 12 | import {css} from '@emotion/core'; 13 | import BeatLoader from 'react-spinners/BeatLoader'; 14 | 15 | import GraphiQLPlugin from './GraphiqlPlugin'; 16 | import {Apollo11ThemeContext} from '../Panel/themes/ThemeProvider'; 17 | 18 | // import Apollo11Logo from '-!svg-react-loader!../assets/logo.svg'; 19 | 20 | const defaultQuery = `query{ 21 | launch(id: 98) { 22 | id 23 | site 24 | isBooked 25 | } 26 | }`; // make a default query based on the endpoint 27 | 28 | const useStyles = makeStyles((theme: Theme) => 29 | createStyles({ 30 | root: { 31 | '& > *': { 32 | margin: theme.spacing(2), 33 | width: '90%', 34 | }, 35 | }, 36 | endpointInput: { 37 | minWidth: '90%', 38 | }, 39 | loaderText: { 40 | display: 'flex', 41 | flexDirection: 'column', 42 | alignContent: 'center', 43 | justifyContent: 'center', 44 | marginTop: '10px', 45 | textAlign: 'center', 46 | }, 47 | }), 48 | ); 49 | 50 | // for the react-spinner 51 | const override = css` 52 | display: flex; 53 | justify-content: center; 54 | margin-top: 50px; 55 | margin-right: auto; 56 | margin-left: auto; 57 | `; 58 | 59 | interface GraphiQLProps { 60 | endpointURI: string; 61 | } 62 | 63 | const GraphiQLPage: FunctionComponent = ({endpointURI}) => { 64 | // console.log('GraphiQL endpointURI :>> ', endpointURI); 65 | const classes = useStyles(); 66 | const [loadingEndpoint, setLoadingEndpoint] = useState(true); 67 | const [secondEndpoint, setSecondEndpoint] = useState( 68 | 'https://swapi-graphql.netlify.com/.netlify/functions/index', 69 | ); 70 | const [selectedRadio, setSelectedRadio] = useState(''); 71 | const [endpointForGraphiQL, setEndpointForGraphiql] = useState(''); 72 | const {isDark} = React.useContext(Apollo11ThemeContext); 73 | 74 | useEffect(() => { 75 | // console.log('useEffect endpointURI :>> ', endpointURI); 76 | if (endpointURI) { 77 | // Once the Apolllo endpoint is loaded Display Graphiql 78 | setLoadingEndpoint(false); 79 | 80 | // Radio button starts with endpointURI selected first 81 | setSelectedRadio(endpointURI); 82 | setEndpointForGraphiql(endpointURI); 83 | } 84 | }, [endpointURI]); 85 | 86 | const handleSecondEndpointOnChange = e => { 87 | setSecondEndpoint(e.target.value); 88 | }; 89 | 90 | const handleRadio = (e: React.ChangeEvent) => { 91 | setSelectedRadio(e.target.value); 92 | setEndpointForGraphiql(e.target.value); 93 | }; 94 | 95 | return ( 96 |
97 |
98 |
99 |
100 | 109 | {loadingEndpoint ? null : ( 110 | handleRadio(e)} 113 | value={endpointURI} 114 | name="radio-button-demo" 115 | inputProps={{'aria-label': 'A'}} 116 | /> 117 | )} 118 |
119 |
120 | handleSecondEndpointOnChange(e)} 127 | value={secondEndpoint} 128 | /> 129 | {loadingEndpoint ? null : ( 130 | handleRadio(e)} 133 | value={secondEndpoint} 134 | name="radio-button-demo" 135 | inputProps={{'aria-label': 'B'}} 136 | /> 137 | )} 138 |
139 |
140 |
141 | 142 | {loadingEndpoint ? ( 143 | <> 144 | 150 |
151 | 152 | Loading Apollo GraphQL Endpoint 153 | 154 | 155 | If this doesnt load click 156 | 161 | {' '} 162 | here 163 | 164 | 165 |
166 | 167 | ) : ( 168 | 173 | )} 174 |
175 | ); 176 | }; 177 | 178 | export default GraphiQLPage; 179 | -------------------------------------------------------------------------------- /src/app/GraphiQL_Tab/GraphiqlPlugin.jsx: -------------------------------------------------------------------------------- 1 | // To get Explorer component to work had to move graphql module to 14.1.1 and follow 2 | // this github issue to fix webpack: https://github.com/graphql/graphql-js/issues/1272 3 | 4 | import React, {Component} from 'react'; 5 | import GraphiQL from 'graphiql'; 6 | import GraphiQLExplorer from 'graphiql-explorer'; 7 | import {getIntrospectionQuery, buildClientSchema} from 'graphql/utilities'; 8 | 9 | // Css imports 10 | import 'graphiql/graphiql.min.css'; 11 | import '../Panel/index.css'; 12 | 13 | class GraphiQLPlugin extends Component { 14 | constructor(props) { 15 | super(props); 16 | 17 | this.state = { 18 | endpoint: this.props.endpoint, 19 | // noFetch: false, 20 | query: this.props.query, 21 | variables: this.props.variables, 22 | schema: null, 23 | explorerIsOpen: false, 24 | }; 25 | } 26 | 27 | // eslint-disable-next-line react/sort-comp 28 | graphQLFetcher = (graphQLParms = {}) => { 29 | // console.log('graphQLFetcher this.state.endpoint :>> ', this.state.endpoint); 30 | return fetch(this.state.endpoint, { 31 | method: 'post', 32 | headers: { 33 | Accept: 'application/json', 34 | 'Content-Type': 'application/json', 35 | }, 36 | body: JSON.stringify(graphQLParms), 37 | }) 38 | .then(response => { 39 | return response.json(); 40 | }) 41 | .catch(error => console.log(error)); 42 | }; 43 | 44 | // starts as null then is updated to a fetcher with second endpoint 45 | // eslint-disable-next-line react/sort-comp 46 | graphQLFetcher2 = null; 47 | 48 | componentDidMount() { 49 | // create fetcher with Apollo Endpoint on initial mount 50 | // console.log('GraphiqlPlugin mounted, invoking graphQLFetcher'); 51 | this.graphQLFetcher({ 52 | query: getIntrospectionQuery(), 53 | // noFetch: false, 54 | }) 55 | .then(result => { 56 | // console.log('.then result :>> ', result); 57 | 58 | // If the Apollo Server does not have introspection enabled, it will return a 400 59 | // and an error message stating this, so bail out and throw an error 60 | if ( 61 | result.errors && 62 | Array.isArray(result.errors) && 63 | result.errors[0].message 64 | ) { 65 | throw result.errors[0].message; 66 | } 67 | 68 | this.setState(oldState => { 69 | // build schema from introspection query 70 | return { 71 | schema: buildClientSchema(result.data), 72 | query: oldState.query, 73 | // || this.context.storage.getItem('graphiql:query'), 74 | }; 75 | }); 76 | }) 77 | .catch(error => console.log(error)); 78 | 79 | // if a query exist 80 | if (this.props.query) { 81 | // TBD: if auto run query is true 82 | // if (this.props.automaticallyRunQuery) { 83 | this.graphiql.handleRunQuery(); 84 | // } 85 | } 86 | 87 | // Close History Pane before mounting 88 | if (this.graphiql.state.historyPaneOpen) { 89 | this.graphiql.handleToggleHistory(); 90 | } 91 | 92 | // Close Doc Pane before mounting 93 | if (this.graphiql.state.docExplorerOpen) { 94 | this.graphiql.handleToggleDocs(); 95 | } 96 | } 97 | 98 | componentDidUpdate(prevProps) { 99 | // if props.endpoint updates recreate the fetcher and schema 100 | if (prevProps.endpoint !== this.props.endpoint) { 101 | this.setState({endpoint: this.props.endpoint}); 102 | 103 | // console.log('endpoint in update', this.props.endpoint); 104 | 105 | // create a new fetcher with updated endpoint 106 | this.graphQLFetcher2 = (graphQLParms = {}) => { 107 | return fetch(this.props.endpoint, { 108 | method: 'post', 109 | headers: { 110 | Accept: 'application/json', 111 | 'Content-Type': 'application/json', 112 | }, 113 | body: JSON.stringify(graphQLParms), 114 | }) 115 | .then(response => { 116 | return response.json(); 117 | }) 118 | .catch(error => console.log(error)); 119 | }; 120 | 121 | // recreate the schema 122 | this.graphQLFetcher2({ 123 | query: getIntrospectionQuery(), 124 | // noFetch: false, 125 | }) 126 | .then(result => { 127 | // console.log('result of 2nd introspection:', result); 128 | this.setState(oldState => { 129 | return { 130 | schema: buildClientSchema(result.data), 131 | query: oldState.query, 132 | // || this.context.storage.getItem('graphiql:query'), 133 | }; 134 | }); 135 | }) 136 | .catch(error => console.log(error)); 137 | 138 | // clear query 139 | this.clearDefaultQueryState(''); 140 | } 141 | } 142 | 143 | handleClickHistoryButton = event => { 144 | this.graphiql.handleToggleHistory(); 145 | }; 146 | 147 | // Not sure what this does 148 | handleClickPrettifyButton = event => { 149 | const editor = this.graphiql.getQueryEditor(); 150 | const currentText = editor.getValue(); 151 | const prettyText = print(parse(currentText)); 152 | editor.setValue(prettyText); 153 | }; 154 | 155 | handleToggleExplorer = () => { 156 | this.setState({ 157 | explorerIsOpen: !this.state.explorerIsOpen, 158 | }); 159 | }; 160 | 161 | clearDefaultQueryState = query => { 162 | this.setState({ 163 | query, 164 | variables: undefined, 165 | }); 166 | }; 167 | 168 | render() { 169 | const {noFetch, query, schema} = this.state; 170 | 171 | const graphiql = ( 172 |
173 | this.clearDefaultQueryState(queryEdit)} 177 | explorerIsOpen={this.state.explorerIsOpen} 178 | onToggleExplorer={this.handleToggleExplorer} 179 | />{' '} 180 | { 187 | this.clearDefaultQueryState(queryOnEdit); 188 | }} 189 | onEditVariables={() => { 190 | this.clearDefaultQueryState(); 191 | }} 192 | variables={this.state.variables} 193 | ref={r => { 194 | this.graphiql = r; 195 | }}> 196 | 197 | 201 | 206 | {' '} 210 | {/* 211 | Feature in Apollo Client Dev Tool to consider 212 | */}{' '} 229 | {' '} 230 | {' '} 231 |
232 | ); 233 | 234 | return
{graphiql}
; 235 | } 236 | } 237 | 238 | export default GraphiQLPlugin; 239 | -------------------------------------------------------------------------------- /src/app/Panel/MainDrawer.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | 3 | import clsx from 'clsx'; 4 | import { 5 | createStyles, 6 | makeStyles, 7 | useTheme, 8 | Theme, 9 | } from '@material-ui/core/styles'; 10 | import AppBar from '@material-ui/core/AppBar'; 11 | import ChevronLeftIcon from '@material-ui/icons/ChevronLeft'; 12 | import ChevronRightIcon from '@material-ui/icons/ChevronRight'; 13 | import CssBaseline from '@material-ui/core/CssBaseline'; 14 | import Divider from '@material-ui/core/Divider'; 15 | import Drawer from '@material-ui/core/Drawer'; 16 | import FormControlLabel from '@material-ui/core/FormControlLabel'; 17 | import IconButton from '@material-ui/core/IconButton'; 18 | import List from '@material-ui/core/List'; 19 | import ListItem from '@material-ui/core/ListItem'; 20 | import ListItemIcon from '@material-ui/core/ListItemIcon'; 21 | import ListItemText from '@material-ui/core/ListItemText'; 22 | import HttpIcon from '@material-ui/icons/Http'; 23 | import StorageIcon from '@material-ui/icons/Storage'; 24 | import BarChartIcon from '@material-ui/icons/BarChart'; 25 | import Popper, {PopperPlacementType} from '@material-ui/core/Popper'; 26 | import Fade from '@material-ui/core/Fade'; 27 | import Paper from '@material-ui/core/Paper'; 28 | import MenuIcon from '@material-ui/icons/Menu'; 29 | import SwitchUI from '@material-ui/core/Switch'; 30 | import Toolbar from '@material-ui/core/Toolbar'; 31 | import Typography from '@material-ui/core/Typography'; 32 | 33 | import ApolloTabResponsive from '../Events_Tab/ApolloTabResponsive'; 34 | import {Apollo11ThemeContext} from './themes/ThemeProvider'; 35 | import GraphiQL from '../GraphiQL_Tab/GraphiQLPage'; 36 | import {MainDrawerProps} from '../utils/managedlog/lib/eventLogNode'; 37 | import Performance from '../Performance_Tab/Performance_v2'; 38 | 39 | const drawerWidth = 200; 40 | 41 | const useStyles = makeStyles((theme: Theme) => 42 | createStyles({ 43 | root: { 44 | display: 'flex', 45 | }, 46 | appBar: { 47 | zIndex: theme.zIndex.drawer + 1, 48 | transition: theme.transitions.create(['width', 'margin'], { 49 | easing: theme.transitions.easing.sharp, 50 | duration: theme.transitions.duration.leavingScreen, 51 | }), 52 | }, 53 | appBarShift: { 54 | marginLeft: drawerWidth, 55 | width: `calc(100% - ${drawerWidth}px)`, 56 | transition: theme.transitions.create(['width', 'margin'], { 57 | easing: theme.transitions.easing.sharp, 58 | duration: theme.transitions.duration.enteringScreen, 59 | }), 60 | }, 61 | content: { 62 | flexGrow: 1, 63 | padding: theme.spacing(0), 64 | }, 65 | drawer: { 66 | width: drawerWidth, 67 | flexShrink: 0, 68 | whiteSpace: 'nowrap', 69 | }, 70 | drawerOpen: { 71 | width: drawerWidth, 72 | transition: theme.transitions.create('width', { 73 | easing: theme.transitions.easing.sharp, 74 | duration: theme.transitions.duration.enteringScreen, 75 | }), 76 | }, 77 | drawerClose: { 78 | transition: theme.transitions.create('width', { 79 | easing: theme.transitions.easing.sharp, 80 | duration: theme.transitions.duration.leavingScreen, 81 | }), 82 | overflowX: 'hidden', 83 | width: theme.spacing(7) + 1, 84 | [theme.breakpoints.up('sm')]: { 85 | width: theme.spacing(9) + 1, 86 | }, 87 | }, 88 | hide: { 89 | display: 'none', 90 | }, 91 | labelPlacementStart: { 92 | justifyContent: 'space-between', 93 | }, 94 | menuButton: { 95 | marginRight: 36, 96 | }, 97 | toolbar: { 98 | display: 'flex', 99 | alignItems: 'center', 100 | justifyContent: 'flex-end', 101 | padding: theme.spacing(0, 1), 102 | // necessary for content to be below app bar 103 | ...theme.mixins.toolbar, 104 | }, 105 | popperText: { 106 | padding: theme.spacing(2), 107 | }, 108 | popperPaper: { 109 | opacity: 1, 110 | }, 111 | switchDiv: { 112 | marginLeft: '20px', 113 | }, 114 | }), 115 | ); 116 | 117 | export default function MainDrawer({ 118 | endpointURI, 119 | events, 120 | networkEvents, 121 | networkURI, 122 | }: MainDrawerProps) { 123 | const classes = useStyles(); 124 | const theme = useTheme(); 125 | const [open, setOpen] = useState(false); 126 | const [activeTab, setActiveTab] = useState('GraphiQL'); 127 | 128 | // Hooks for the Popper on hover 129 | const [anchorEl, setAnchorEl] = React.useState(null); 130 | const [openPopper, setOpenPopper] = React.useState(false); 131 | const [placement, setPlacement] = React.useState(); 132 | const [popperContent, setPopperContent] = React.useState(''); 133 | const {setTheme, isDark} = React.useContext(Apollo11ThemeContext); 134 | 135 | const handleThemeChange = event => { 136 | const {checked} = event.target; 137 | if (checked) { 138 | setTheme('dark'); 139 | } else { 140 | setTheme('normal'); 141 | } 142 | }; 143 | 144 | const handleDrawerOpen = () => { 145 | setOpen(true); 146 | }; 147 | 148 | const handleDrawerClose = () => { 149 | setOpen(false); 150 | }; 151 | 152 | const handlePopper = (newPlacement: PopperPlacementType, text: string) => ( 153 | event: React.MouseEvent, 154 | ) => { 155 | setPopperContent(text); 156 | setAnchorEl(event.currentTarget); 157 | setOpenPopper(prev => placement !== newPlacement || !prev); 158 | setPlacement(newPlacement); 159 | }; 160 | 161 | const closePopper = () => { 162 | setOpenPopper(false); 163 | }; 164 | 165 | /** 166 | * 167 | * @param tab Current Selected Tab as String 168 | * Returns and renders Selected Tab's component as a react Element 169 | */ 170 | const renderTab = (tab: string): React.ReactElement => { 171 | switch (tab) { 172 | case 'GraphiQL': 173 | return ; 174 | case 'Events & Cache': 175 | return ; 176 | case 'Performance': 177 | return ; 178 | default: 179 | return ; 180 | } 181 | }; 182 | 183 | return ( 184 |
185 | 191 | {({TransitionProps}) => ( 192 | // eslint-disable-next-line react/jsx-props-no-spreading 193 | 194 | 195 | 196 | {popperContent} 197 | 198 | 199 | 200 | )} 201 | 202 | 203 | 204 | 209 | 210 | 218 | 219 | 220 | 221 | ApolloDevQL 222 | 223 | 231 | } 232 | label={ 233 | 234 | {isDark ? 'Light Mode' : 'Dark Mode'} 235 | 236 | } 237 | classes={{ 238 | labelPlacementStart: classes.labelPlacementStart, 239 | }} 240 | /> 241 | 242 | 243 | 255 |
256 | 257 | {theme.direction === 'rtl' ? ( 258 | 259 | ) : ( 260 | 261 | )} 262 | 263 |
264 | 265 | 266 | {['GraphiQL', 'Events & Cache', 'Performance'].map((text, index) => ( 267 | { 271 | setActiveTab(`${text}`); 272 | }} 273 | onMouseEnter={handlePopper('right', text)} 274 | onMouseLeave={handlePopper('right', text)}> 275 | 276 | {index === 0 ? : null} 277 | {index === 1 ? : null} 278 | {index === 2 ? : null} 279 | 280 | 281 | 282 | ))} 283 | 284 |
285 |
286 |
287 | {/* Should add a loading spinner */} 288 | {renderTab(activeTab) || ( 289 | Loading App....Please wait.... 290 | )} 291 |
292 |
293 | ); 294 | } 295 | -------------------------------------------------------------------------------- /src/app/Panel/__tests__/MainDrawer_test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/display-name */ 2 | import React from 'react'; 3 | import {configure} from 'enzyme'; 4 | import Adapter from 'enzyme-adapter-react-16'; 5 | 6 | import MainDrawer from '../MainDrawer'; 7 | import data from '../../../../__mocks__/schema'; 8 | import mountWithTheme from '../../../../__mocks__/themeMock'; 9 | 10 | document.createRange = () => { 11 | const range = new Range(); 12 | 13 | range.getBoundingClientRect = jest.fn(); 14 | 15 | range.getClientRects = jest.fn(() => ({ 16 | item: () => null, 17 | length: 0, 18 | })); 19 | 20 | return range; 21 | }; 22 | 23 | configure({ 24 | adapter: new Adapter(), 25 | }); 26 | 27 | describe('snapshot tests', () => { 28 | const endpointURI = {}; 29 | const events = {}; 30 | const networkEvents = {}; 31 | const networkURI = {}; 32 | 33 | let wrapper; 34 | 35 | it('should mount the app', () => { 36 | global.fetch = jest.fn(() => 37 | Promise.resolve({ 38 | json: () => ({ 39 | data, 40 | }), 41 | }), 42 | ); 43 | wrapper = mountWithTheme( 44 | , 50 | ); 51 | 52 | // console.log('wrapper :>> ', wrapper.html()); 53 | // console.log('wrapper :>> ', wrapper.text()); 54 | expect(wrapper).toMatchSnapshot(); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /src/app/Panel/__tests__/app_test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {configure} from 'enzyme'; 3 | import Adapter from 'enzyme-adapter-react-16'; 4 | 5 | // import {chrome} from 'jest-chrome'; 6 | import App from '../app'; 7 | import mountWithTheme from '../../../../__mocks__/themeMock'; 8 | 9 | configure({ 10 | adapter: new Adapter(), 11 | }); 12 | 13 | describe('snapshot tests', () => { 14 | let wrapper; 15 | 16 | // it('should shallow render the app', () => { 17 | // wrapper = shallow(); 18 | // console.log('wrapper :>> ', wrapper.html()); 19 | // console.log('wrapper :>> ', wrapper.text()); 20 | // }); 21 | it('should mount the app', () => { 22 | wrapper = mountWithTheme(); 23 | // console.log('wrapper :>> ', wrapper.html()); 24 | // console.log('wrapper :>> ', wrapper.text()); 25 | expect(wrapper).toMatchSnapshot(); 26 | }); 27 | // it('should static render the app', () => { 28 | // wrapper = render(); 29 | // console.log('wrapper :>> ', wrapper.html()); 30 | // console.log('wrapper :>> ', wrapper.text()); 31 | // }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/app/Panel/app.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState, useEffect} from 'react'; 2 | 3 | import createNetworkEventListener from '../utils/networking'; 4 | import createApolloClientListener, {getApolloClient} from '../utils/messaging'; 5 | import EventLogDataObject from '../utils/managedlog/lib/eventLogData'; 6 | import EventLogTreeContainer from '../utils/managedlog/eventObject'; 7 | import MainDrawer from './MainDrawer'; 8 | 9 | type UseEffectListener = { 10 | setupApolloClientListener(): void; 11 | setupApolloClient(): void; 12 | setupNetworkEventListener(): void; 13 | }; 14 | 15 | const App = () => { 16 | const EventList = EventLogTreeContainer(new EventLogDataObject()); 17 | const [apolloURI, setApolloURI] = useState(''); 18 | const [networkURI, setNetworkURI] = useState(''); 19 | const [events, setEvents] = useState(() => 20 | EventList.getDataStore(), 21 | ); 22 | const [networkEvents, setNetworkEvents] = useState({}); 23 | 24 | // need to use a UseRef to resolve ths linting situation 25 | // React Hook useEffect has a missing dependency: 'EventList'. Either include it or remove the dependency array 26 | const useEffectListeners = React.useRef(); 27 | 28 | useEffectListeners.current = { 29 | setupApolloClientListener() { 30 | // Event listener to obtain the GraphQL server endpoint (URI) 31 | // and the cache from the Apollo Client 32 | createApolloClientListener(setApolloURI, EventList, setEvents); 33 | }, 34 | setupApolloClient() { 35 | // Initial load of the App, so send a message to the contentScript to get the cache 36 | getApolloClient(); 37 | }, 38 | setupNetworkEventListener() { 39 | // Listen for network events 40 | createNetworkEventListener(setNetworkURI, setNetworkEvents); 41 | }, 42 | }; 43 | 44 | // Only create the listener when the App is initially mounted 45 | useEffect(() => { 46 | useEffectListeners.current.setupApolloClientListener(); 47 | useEffectListeners.current.setupApolloClient(); 48 | useEffectListeners.current.setupNetworkEventListener(); 49 | }, []); 50 | 51 | return ( 52 |
53 | 59 |
60 | ); 61 | }; 62 | 63 | export default App; 64 | -------------------------------------------------------------------------------- /src/app/Panel/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | 4 | height: 100vh; 5 | } 6 | 7 | main { 8 | height: 100vh; 9 | } 10 | 11 | .graphiql_wrapper { 12 | height: 100vh; 13 | } 14 | 15 | .wrapper-mainql { 16 | height: 100vh; 17 | } 18 | 19 | ​ .graphiql-container { 20 | height: 100vh; 21 | } 22 | 23 | ​ .logo { 24 | height: 50px; 25 | } 26 | 27 | .endpoint-row { 28 | width: 100%; 29 | display: flex; 30 | justify-content: flex-start; 31 | } 32 | 33 | .gridc { 34 | display: flex; 35 | flex-direction: row; 36 | } 37 | .grid { 38 | width: 58%; 39 | margin-left: 1%; 40 | margin-right: 1%; 41 | box-shadow: 1px 1px 1px #fff; 42 | border-radius: 3px; 43 | } 44 | -------------------------------------------------------------------------------- /src/app/Panel/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './app'; 4 | import Apollo11ThemeProvider from './themes/ThemeProvider'; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root'), 11 | ); 12 | -------------------------------------------------------------------------------- /src/app/Panel/themes/ThemeProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import {ThemeProvider} from '@material-ui/core/styles'; 3 | 4 | import {Apollo11ThemeContextType} from '../../utils/managedlog/lib/eventLogNode'; 5 | import getTheme from './base'; 6 | 7 | export const Apollo11ThemeContext = React.createContext< 8 | Apollo11ThemeContextType | undefined 9 | >(undefined); 10 | 11 | type Props = { 12 | children: React.ReactNode; 13 | }; 14 | 15 | const Apollo11ThemeProvider = ({children}: Props) => { 16 | let theme; 17 | let isDark; 18 | // State to hold the selected theme name 19 | const [themeName, _setThemeName] = useState(() => { 20 | // Read current theme from localStorage or maybe from an api 21 | const currentTheme = localStorage.getItem('apollo11AppTheme') || 'normal'; 22 | // Retrieve the theme object of theme name from localStorage once context is rendered 23 | 24 | theme = getTheme(currentTheme); 25 | isDark = Boolean(currentTheme === 'dark'); 26 | return currentTheme; 27 | }); 28 | 29 | // Retrieve the theme object by theme name as safety check 30 | theme = getTheme(themeName || 'normal'); 31 | isDark = Boolean(themeName === 'dark'); 32 | // Wrap _setThemeName to store new theme names in localStorage 33 | const setThemeName = themename => { 34 | localStorage.setItem('apollo11AppTheme', themename); 35 | _setThemeName(themename); 36 | }; 37 | 38 | const themeContext = { 39 | currentTheme: themeName, 40 | setTheme: setThemeName, 41 | isDark, 42 | }; 43 | 44 | return ( 45 | 46 | {children} 47 | 48 | ); 49 | }; 50 | 51 | export default Apollo11ThemeProvider; 52 | -------------------------------------------------------------------------------- /src/app/Panel/themes/base.ts: -------------------------------------------------------------------------------- 1 | import normal from './normal'; 2 | import dark from './dark'; 3 | 4 | const themes = { 5 | normal, 6 | dark, 7 | }; 8 | 9 | export default function getTheme(theme) { 10 | return themes[theme]; 11 | } 12 | -------------------------------------------------------------------------------- /src/app/Panel/themes/dark.ts: -------------------------------------------------------------------------------- 1 | import {createMuiTheme} from '@material-ui/core/styles'; 2 | import {red} from '@material-ui/core/colors'; 3 | 4 | const theme = createMuiTheme({ 5 | palette: { 6 | type: 'dark', 7 | primary: { 8 | main: '#26292C', 9 | light: 'rgb(81, 91, 95)', 10 | dark: 'rgb(26, 35, 39)', 11 | contrastText: '#ffffff', 12 | }, 13 | secondary: { 14 | main: '#FFB74D', 15 | light: 'rgb(255, 197, 112)', 16 | dark: 'rgb(200, 147, 89)', 17 | contrastText: 'rgba(0, 0, 0, 0.87)', 18 | }, 19 | error: { 20 | main: red.A400, 21 | }, 22 | }, 23 | }); 24 | 25 | export default theme; 26 | -------------------------------------------------------------------------------- /src/app/Panel/themes/normal.ts: -------------------------------------------------------------------------------- 1 | import {createMuiTheme} from '@material-ui/core/styles'; 2 | import {red} from '@material-ui/core/colors'; 3 | 4 | const theme = createMuiTheme({ 5 | palette: { 6 | primary: { 7 | main: '#556cd6', 8 | }, 9 | secondary: { 10 | main: '#cc4444', 11 | }, 12 | error: { 13 | main: red.A400, 14 | }, 15 | background: { 16 | default: '#f5f5f5', 17 | }, 18 | }, 19 | }); 20 | 21 | export default theme; 22 | -------------------------------------------------------------------------------- /src/app/Performance_Tab/ArrowChip.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ArrowBackIcon from '@material-ui/icons/ArrowBack'; 3 | import Avatar from '@material-ui/core/Avatar'; 4 | import Chip from '@material-ui/core/Chip'; 5 | import {makeStyles, createStyles, Theme} from '@material-ui/core/styles'; 6 | import {Apollo11ThemeContext} from '../Panel/themes/ThemeProvider'; 7 | 8 | // style for alerts 9 | const useStyles = makeStyles((theme: Theme) => 10 | createStyles({ 11 | chipDiv: { 12 | display: 'flex', 13 | flexDirection: 'row', 14 | justifyContent: 'center', 15 | marginTop: theme.spacing(2), 16 | marginBottom: theme.spacing(2), 17 | }, 18 | chip: () => ({ 19 | // props: any 20 | backgroundColor: 'white', 21 | color: 'black', 22 | }), 23 | }), 24 | ); 25 | 26 | const ArrowChip = () => { 27 | const {isDark} = React.useContext(Apollo11ThemeContext); 28 | const classes = useStyles({isDark}); 29 | 30 | return ( 31 |
32 | 37 | 38 | 39 | } 40 | variant="outlined" 41 | /> 42 |
43 | ); 44 | }; 45 | 46 | export default ArrowChip; 47 | -------------------------------------------------------------------------------- /src/app/Performance_Tab/Performance_v2.tsx: -------------------------------------------------------------------------------- 1 | import 'react-grid-layout/css/styles.css'; 2 | import 'react-resizable/css/styles.css'; 3 | import RGL, {WidthProvider} from 'react-grid-layout'; 4 | 5 | import React, {useEffect} from 'react'; 6 | import {css} from '@emotion/core'; 7 | import PuffLoader from 'react-spinners/PuffLoader'; 8 | 9 | // Material UI Components 10 | import AppBar from '@material-ui/core/AppBar'; 11 | import {createStyles, makeStyles, Theme} from '@material-ui/core/styles'; 12 | import List from '@material-ui/core/List'; 13 | import ListItem from '@material-ui/core/ListItem'; 14 | import ListItemText from '@material-ui/core/ListItemText'; 15 | import Toolbar from '@material-ui/core/Toolbar'; 16 | import Typography from '@material-ui/core/Typography'; 17 | // Project files 18 | import {extractOperationName, transformTimingData} from '../utils/helper'; 19 | import getMaxEventTime from '../utils/performanceMetricsCalcs'; 20 | import progressBarStyle from './progressBar'; 21 | import TracingDetails from './TracingDetails'; 22 | 23 | // React Grid Function 24 | const ReactGridLayout = WidthProvider(RGL); 25 | 26 | interface IPerformanceData { 27 | networkEvents: any; 28 | isDraggable: boolean; 29 | isResizable: boolean; 30 | items: number; 31 | rowHeight: number; 32 | // onLayoutChange: function () {}, 33 | cols: number; 34 | verticalCompact: boolean; 35 | resizeHandles: Array; 36 | compactType: string; 37 | preventCollision: boolean; 38 | autoSize: boolean; 39 | margin: [number, number]; 40 | } 41 | 42 | interface ITimings { 43 | duration: any; 44 | endTime?: any; 45 | key?: any; 46 | startTime?: any; 47 | resolvers?: {[num: number]: any}; 48 | traceInfo: string; 49 | } 50 | 51 | // create event progress bar 52 | const BorderLinearProgress = progressBarStyle('#1876D2'); 53 | 54 | // for the react-spinner 55 | const override = css` 56 | display: block; 57 | margin: 0 auto; 58 | border-color: red; 59 | `; 60 | 61 | // setup component class hook 62 | const useStyles: any = makeStyles((theme: Theme) => 63 | createStyles({ 64 | root: { 65 | flexGrow: 1, 66 | }, 67 | itemCSS: { 68 | flexGrow: 1, 69 | }, 70 | paper: { 71 | padding: theme.spacing(0), 72 | textAlign: 'center', 73 | color: theme.palette.text.primary, 74 | }, 75 | titles: { 76 | textAlign: 'center', 77 | marginTop: '2px', 78 | marginBottom: '2px', 79 | }, 80 | grid: { 81 | 'overflow-x': 'hidden', 82 | 'overflow-y': 'auto', 83 | border: '1px solid lightgrey', 84 | borderRadius: '5px', 85 | }, 86 | }), 87 | ); 88 | 89 | const Performance = ({ 90 | networkEvents, 91 | isDraggable, 92 | isResizable, 93 | items, 94 | rowHeight, 95 | cols, 96 | verticalCompact, 97 | resizeHandles, 98 | compactType, 99 | preventCollision, 100 | autoSize, 101 | margin, 102 | }: IPerformanceData) => { 103 | const componentClass = useStyles(); 104 | const [selectedIndex, setSelectedIndex] = React.useState(() => 0); 105 | const [isAnEventSelected, setIsAnEventSelected] = React.useState(false); 106 | const [maxEventTime, setMaxEventTime] = React.useState( 107 | getMaxEventTime(networkEvents), 108 | ); 109 | const [tracingInfo, setTracingInfo] = React.useState( 110 | (): ITimings => ({ 111 | duration: '', 112 | resolvers: {}, 113 | traceInfo: '', 114 | }), 115 | ); 116 | 117 | // layout is an array of objects 118 | const layoutArray = [ 119 | {i: '1', x: 0, y: 0, w: 3, h: 22}, 120 | {i: '2', x: 3, y: 0, w: 9, h: 22}, 121 | ]; 122 | 123 | useEffect(() => { 124 | console.log('events', networkEvents); 125 | setMaxEventTime(getMaxEventTime(networkEvents)); 126 | }, [networkEvents]); 127 | 128 | const handleListItemClick = (event: any, index: number, key: string) => { 129 | if (networkEvents[key]) { 130 | const payload = networkEvents[key]; 131 | if (payload && payload.response && payload.response.content) { 132 | // first level safety check 133 | 134 | // using destructured assignment 135 | const { 136 | response: {content}, 137 | } = payload; 138 | if (!(content && content.extensions && content.extensions.tracing)) { 139 | // let use know they need to activate Tracing Data when ApolloServer was instantiated on their server 140 | // payload.time 141 | // setTimingsInfo({duration: payload.time}) 142 | setTracingInfo({ 143 | duration: payload.time, 144 | resolvers: {}, 145 | traceInfo: 146 | 'Please enabled tracing and cache in your Apollo Server initialization to show further network/tracing visualization', 147 | }); 148 | } else { 149 | // const {duration, endTime, startTime} = payload.extensions.tracing; 150 | // extract from content using destructured assignment construct 151 | const { 152 | extensions: { 153 | tracing: { 154 | duration, 155 | endTime, 156 | startTime, 157 | execution: {resolvers}, 158 | }, 159 | }, 160 | } = content; 161 | // need to transform resolvers in Array 162 | const tracingData = { 163 | key, 164 | duration, 165 | endTime, 166 | startTime, 167 | resolvers: transformTimingData(resolvers, duration), 168 | traceInfo: '', 169 | }; 170 | 171 | // need to transform resolvers in Array 172 | // TODO: Transform resolvers ordering by startOffset and hopeful format to show in the details list on a waterfall model 173 | 174 | tracingData.traceInfo = 175 | Object.keys(tracingData.resolvers).length === 0 176 | ? 'There is no tracing info available for this operation' 177 | : ''; 178 | // this should be sent to the hook - tracingData 179 | console.log('Tracing Data :: ', tracingData); 180 | 181 | setTracingInfo(tracingData); 182 | } 183 | } 184 | } 185 | setIsAnEventSelected(true); 186 | setSelectedIndex(index); 187 | }; 188 | 189 | return ( 190 | 205 | {/* generateDOM() */} 206 |
210 | {/*

Network Events

*/} 211 | 212 | 213 | 217 | Network Events 218 | 219 | 220 | 221 | 222 | {Object.entries(networkEvents) 223 | .filter(([, obj]: any) => obj && (obj.response || obj.request)) 224 | .map(([key, obj]: any, k: number) => { 225 | const newobj = { 226 | operation: 227 | obj && 228 | obj.request && 229 | obj.request.operation && 230 | obj.request.operation.operationName 231 | ? obj.request.operation.operationName 232 | : extractOperationName(obj), 233 | time: obj.time, 234 | }; 235 | 236 | return ( 237 |
238 | handleListItemClick(event, k, key)}> 243 | 248 | 249 | 253 |
254 | ); 255 | })} 256 | 257 | 261 |
262 |
263 | 264 |
268 | {/*

Resolver Times

*/} 269 | 270 | 271 | 275 | Resolver Times 276 | 277 | 278 | 279 | 283 |
284 |
285 | ); 286 | }; 287 | 288 | Performance.defaultProps = { 289 | isDraggable: true, 290 | isResizable: true, 291 | items: 2, 292 | 293 | rowHeight: 22, 294 | 295 | cols: 12, 296 | verticalCompact: true, 297 | resizeHandles: ['e', 'ne', 'se'], 298 | autoSize: true, 299 | compactType: 'vertical', 300 | preventCollision: false, 301 | margin: [10, 10], 302 | }; 303 | 304 | export default Performance; 305 | -------------------------------------------------------------------------------- /src/app/Performance_Tab/TracingDetails.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // material UI components 4 | import Alert from '@material-ui/lab/Alert'; 5 | import AppBar from '@material-ui/core/AppBar'; 6 | import Card from '@material-ui/core/Card'; 7 | import CardContent from '@material-ui/core/CardContent'; 8 | import Divider from '@material-ui/core/Divider'; 9 | import {createStyles, makeStyles, Theme} from '@material-ui/core/styles'; 10 | import List from '@material-ui/core/List'; 11 | import ListItem from '@material-ui/core/ListItem'; 12 | import ListItemText from '@material-ui/core/ListItemText'; 13 | import Toolbar from '@material-ui/core/Toolbar'; 14 | import Typography from '@material-ui/core/Typography'; 15 | 16 | // project files 17 | import ArrowChip from './ArrowChip'; 18 | import { 19 | createResolversArray, 20 | filterSortResolvers, 21 | formatTime, 22 | formatTimeForProgressBar, 23 | // TimeMagnitude, 24 | } from '../utils/tracingTimeFormating'; 25 | import progressBarStyle from './progressBar'; 26 | import {Apollo11ThemeContext} from '../Panel/themes/ThemeProvider'; 27 | 28 | // styles for each progress bar color 29 | // const BorderLinearProgress = progressBarStyle('#1876D2'); 30 | const BordereLinearProgressTotal = progressBarStyle('#4BAF50'); 31 | const BorderLinearProgressLongest = progressBarStyle('#F44335'); 32 | const BorderLinearProgressMedium = progressBarStyle('#FF9800'); 33 | const BorderLinearProgressShortest = progressBarStyle('#2296F3'); 34 | 35 | // style for alerts 36 | const useStyles = makeStyles((theme: Theme) => 37 | createStyles({ 38 | alerts: { 39 | width: '100%', 40 | '& > * + *': { 41 | marginTop: theme.spacing(2), 42 | }, 43 | }, 44 | 45 | topAlert: { 46 | width: '100%', 47 | paddingLeft: '5px', 48 | paddingRight: '5px', 49 | '& > * + *': { 50 | marginTop: theme.spacing(2), 51 | }, 52 | }, 53 | 54 | titles: { 55 | textAlign: 'center', 56 | marginTop: '2px', 57 | marginBottom: '2px', 58 | }, 59 | cards: (props: any) => ({ 60 | backgroundColor: props.isDark ? '#2C137C' : '#F7E4CE', 61 | color: props.isDark ? 'white' : 'black', 62 | marginLeft: '5px', 63 | marginRight: '5px', 64 | marginTop: '15px', 65 | marginBottom: '15px', 66 | }), 67 | 68 | divider: { 69 | margin: '10px', 70 | }, 71 | defaultStateTopSection: { 72 | display: 'flex', 73 | flexDirection: 'row', 74 | justifyContent: 'center', 75 | marginTop: theme.spacing(0), 76 | marginBottom: theme.spacing(2), 77 | }, 78 | }), 79 | ); 80 | 81 | type TracingDetailProps = { 82 | tracing: any; 83 | eventSelected: boolean; 84 | }; 85 | 86 | // Start of component 87 | const TracingDetails = ({tracing, eventSelected}: TracingDetailProps) => { 88 | const {isDark} = React.useContext(Apollo11ThemeContext); 89 | const classes = useStyles({isDark}); 90 | 91 | const resolversArray = createResolversArray(tracing); 92 | 93 | // Create list of Reducers for each magnitude of time 94 | const nsResolvers = filterSortResolvers(resolversArray, 'ns'); 95 | const µsResolvers = filterSortResolvers(resolversArray, 'µs'); 96 | const msResolvers = filterSortResolvers(resolversArray, 'ms'); 97 | 98 | // Find max of each Magnitude 99 | const nsResolversMax = 100 | nsResolvers.length > 0 101 | ? nsResolvers.reduce((acc, curr) => { 102 | if (acc < curr.duration) { 103 | return curr.duration; 104 | } 105 | 106 | return acc; 107 | }, 0) 108 | : null; 109 | 110 | const µsResolversMax = 111 | µsResolvers.length > 0 112 | ? µsResolvers.reduce((acc, curr) => { 113 | if (acc < curr.duration) { 114 | return curr.duration; 115 | } 116 | 117 | return acc; 118 | }, 0) 119 | : null; 120 | 121 | const msResolversMax = 122 | msResolvers.length > 0 123 | ? msResolvers.reduce((acc, curr) => { 124 | if (acc < curr.duration) { 125 | return curr.duration; 126 | } 127 | 128 | return acc; 129 | }, 0) 130 | : null; 131 | 132 | return ( 133 | 134 | 135 | 136 | {eventSelected ? ( 137 |
138 | 139 | Total Resolver Time: 140 | 141 | 142 | 143 | 146 | 147 |
148 | 149 |
150 |
151 | ) : ( 152 | 153 | )} 154 |
155 |
156 | 157 | {/*

158 | Individual Resolver Times 159 |

*/} 160 | 161 | 162 | 166 | Individual Resolver Times 167 | 168 | 169 | 170 | 171 | {eventSelected ? ( 172 | <> 173 | 174 | 175 |
176 | 177 | Longest: Millisecond Resolvers 10^−3 178 | 179 |
180 | {msResolvers.length !== 0 ? ( 181 | msResolvers.map((resolver: any) => { 182 | return ( 183 |
184 | 185 | 190 | 191 | 197 | 1 198 | ? (formatTimeForProgressBar(resolver.duration) / 199 | formatTimeForProgressBar(msResolversMax)) * 200 | 100 201 | : 1 202 | } 203 | /> 204 |
205 | ); 206 | }) 207 | ) : ( 208 | 209 | )} 210 |
211 |
212 | {/* */} 213 | 214 | 215 |
216 | 217 | Medium: Microsecond Resolvers 10^−6 218 | 219 |
220 | {µsResolvers.length !== 0 ? ( 221 | µsResolvers.map((resolver: any) => { 222 | return ( 223 |
224 | 225 | 230 | 231 | 237 | 1 238 | ? (formatTimeForProgressBar(resolver.duration) / 239 | formatTimeForProgressBar(µsResolversMax)) * 240 | 100 241 | : 1 242 | } 243 | /> 244 |
245 | ); 246 | }) 247 | ) : ( 248 | 249 | )} 250 |
251 |
252 | {/* */} 253 | 254 | 255 |
256 | 257 | Shortest: Nanosecond Resolvers 10^−9 258 | 259 |
260 | {nsResolvers.length !== 0 ? ( 261 | nsResolvers.map((resolver: any) => { 262 | return ( 263 |
264 | 265 | 270 | 271 | 277 | 1 278 | ? (formatTimeForProgressBar(resolver.duration) / 279 | formatTimeForProgressBar(nsResolversMax)) * 280 | 100 281 | : 0.5 282 | } 283 | /> 284 |
285 | ); 286 | }) 287 | ) : ( 288 | 289 | )} 290 |
291 |
292 | 293 | ) : null} 294 |
295 | ); 296 | }; 297 | 298 | export default TracingDetails; 299 | -------------------------------------------------------------------------------- /src/app/Performance_Tab/__tests__/Performance_test.js: -------------------------------------------------------------------------------- 1 | // https://www.smashingmagazine.com/2020/06/practical-guide-testing-react-applications-jest/ 2 | import React from 'react'; 3 | import {configure} from 'enzyme'; 4 | import Adapter from 'enzyme-adapter-react-16'; 5 | 6 | // Project Files 7 | import Performance from '../Performance_v2'; 8 | import mountWithTheme from '../../../../__mocks__/themeMock'; 9 | import fakeNetworkEvents from '../../../../__mocks__/fakeNetworkEvent'; 10 | 11 | configure({ 12 | adapter: new Adapter(), 13 | }); 14 | 15 | describe('snapshot tests', () => { 16 | const networkEvents = {}; 17 | let wrapper; 18 | 19 | it('should mount the app', () => { 20 | wrapper = mountWithTheme(); 21 | expect(wrapper).toMatchSnapshot(); 22 | }); 23 | }); 24 | 25 | describe('Events Log', () => { 26 | it('the first event should be named: GetLaunchList', () => { 27 | // Helpful Blog: https://medium.com/7shifts-engineering-blog/testing-usecontext-react-hook-with-enzyme-shallow-da062140fc83 28 | 29 | const wrapper = mountWithTheme( 30 | , 31 | ); 32 | 33 | const eventListElement = wrapper 34 | .find({ 35 | className: 36 | 'MuiTypography-root MuiListItemText-primary MuiTypography-body2 MuiTypography-displayBlock', 37 | }) 38 | .first() 39 | .find('span') 40 | .text(); 41 | 42 | expect(eventListElement).toContain('GetLaunchList'); 43 | }); 44 | 45 | it('the first event should have speed of 269 ms', () => { 46 | const wrapper = mountWithTheme( 47 | , 48 | ); 49 | 50 | const eventListElement = wrapper 51 | .find({ 52 | className: 53 | 'MuiTypography-root MuiListItemText-primary MuiTypography-body2 MuiTypography-displayBlock', 54 | }) 55 | .first() 56 | .find('span') 57 | .text(); 58 | 59 | expect(eventListElement).toContain('269 ms'); 60 | }); 61 | 62 | it('there should be 6 events + spinner', () => { 63 | const wrapper = mountWithTheme( 64 | , 65 | ); 66 | 67 | const eventListElement = wrapper.find({ 68 | className: 69 | 'MuiTypography-root MuiListItemText-primary MuiTypography-body2 MuiTypography-displayBlock', 70 | }); 71 | 72 | expect(eventListElement).toHaveLength(7); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /src/app/Performance_Tab/progressBar.tsx: -------------------------------------------------------------------------------- 1 | import {createStyles, withStyles, Theme} from '@material-ui/core/styles'; 2 | import LinearProgress from '@material-ui/core/LinearProgress'; 3 | 4 | const progressBar = (color: string) => { 5 | const progressbar = withStyles((theme: Theme) => 6 | createStyles({ 7 | root: { 8 | height: 10, 9 | borderRadius: 5, 10 | }, 11 | colorPrimary: { 12 | backgroundColor: 13 | theme.palette.grey[theme.palette.type === 'light' ? 200 : 700], 14 | }, 15 | bar: { 16 | borderRadius: 5, 17 | backgroundColor: color, 18 | }, 19 | }), 20 | )(LinearProgress); 21 | 22 | return progressbar; 23 | }; 24 | 25 | export default progressBar; 26 | -------------------------------------------------------------------------------- /src/app/utils/helper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * An enumerator for the DIRECTION of item to pluck from an Array mainly FIRST or LAST 3 | */ 4 | enum DIRECTION { 5 | FRONT, 6 | END, 7 | } 8 | 9 | /** 10 | * 11 | * @param inputArray input Array of strings 12 | * @param position the position to pluck 13 | * @param defaultValue a default value to return when position of the array does not exist 14 | */ 15 | export function pluck( 16 | inputArray: string[], 17 | position: DIRECTION = 0, 18 | defaultValue: any = undefined, 19 | ): any { 20 | return inputArray && inputArray.length 21 | ? inputArray[position === DIRECTION.FRONT ? 0 : inputArray.length - 1] 22 | : defaultValue; 23 | } 24 | 25 | export function plucked( 26 | inputArray: Array, 27 | position: DIRECTION = 0, 28 | defaultValue: T | undefined, 29 | ): T | undefined { 30 | return inputArray && inputArray.length 31 | ? inputArray[position === DIRECTION.FRONT ? 0 : inputArray.length - 1] 32 | : defaultValue; 33 | } 34 | 35 | /** 36 | * Extract the name of a query request or mutation using the query passed to the grapqql server 37 | * @param operation The request operation object from the network request object 38 | * Returns a string or null if not avaialable and in some cases a null if the request object has no query passed 39 | */ 40 | export function extractOperationName(operation: any): string { 41 | const operate = 42 | operation && 43 | operation.request && 44 | operation.request.operation && 45 | operation.request.operation.query 46 | ? pluck(operation.request.operation.query.split('(')) // pluck the first item from the array after the query is split using '(' as teh delimter 47 | : null; 48 | if (!operate) return 'Query'; // return a constant response 'Query' if the when the query was split, there was no item(s) in the array 49 | return pluck(operate.split(' '), DIRECTION.END, ''); // pluck the last item from the 'operate' array, this will definitely be the name of the operation form the query passed to the graphql server 50 | } 51 | 52 | // declare global { 53 | // interface Array { 54 | // groupBy(groupKey: string): Array; 55 | // } 56 | // } 57 | 58 | /** 59 | * 60 | * @param {*} key the property/key to use for grouping 61 | * this returns an object with keys being the values for which the groups are summarized 62 | * and the values, an array of the timing data 63 | * 64 | * An error could occur here, right click and choose "declare 'groupBy'" from the contenxt menu 65 | */ 66 | // Array.prototype.groupBy = function (key: string): Array { 67 | // return (>this).reduce((summary: any, timingData: T): Array => { 68 | // return { 69 | // ...summary, 70 | // [timingData[key]]: summary[timingData[key]] 71 | // ? [...summary[timingData[key]], timingData] 72 | // : [timingData], 73 | // }; 74 | // }, {}); 75 | // }; 76 | 77 | /** 78 | * 79 | * @param inputArray the array to be grouped 80 | * @param key the property/key to use for grouping 81 | * this returns an object with keys being the values for which the groups are summarized 82 | * and the values, an array of the timing data 83 | */ 84 | const groupResolverTimingBy = (inputArray: any[], key: string): any[] => { 85 | return inputArray.reduce((summary: any, timingData: any): any => { 86 | // exisitng key in summary summary[timingData[key]] 87 | // the value is the Array stored IN summary[timingData[key]] and a the new timingData 88 | return { 89 | ...summary, 90 | [timingData[key]]: summary[timingData[key]] 91 | ? [...summary[timingData[key]], timingData] 92 | : [timingData], 93 | }; 94 | }, {}); 95 | }; 96 | 97 | /** 98 | * This function returns a number given 'timing' that is a fraction of totalDuration and a ratio of the totalScale 99 | * @param timing the resolver's timeline 100 | * @param totalDuration the response's total duration 101 | * @param totalScale a numeric scale that underscores the length of the graph to be rendered default 100 102 | */ 103 | const timeToScale = ( 104 | timing: number, 105 | totalDuration: number, 106 | totalScale: number, 107 | ): number => { 108 | return (timing / totalDuration) * totalScale; 109 | }; 110 | 111 | /** 112 | * 113 | * @param resolverTimings a grouped array of resolver timmings from the response property/key event log 114 | * @param totalDuration the total duration of the entire response 115 | * @param totalScale a numeric scale that underscores the length of the graph to be rendered default 100 116 | */ 117 | const scaleResolverTiming = ( 118 | resolverTimings: any, 119 | totalDuration: number, 120 | totalScale: number, 121 | ): any => { 122 | return Object.keys(resolverTimings).reduce( 123 | (resolvedTimings: any, timingSet: string): any => ({ 124 | ...resolvedTimings, 125 | [timingSet]: resolverTimings[timingSet].map(timing => ({ 126 | ...timing, 127 | durationScale: timeToScale( 128 | timing.durationScale, 129 | totalDuration, 130 | totalScale, 131 | ), 132 | startOffset: timeToScale(timing.startOffset, totalDuration, totalScale), 133 | })), 134 | }), 135 | {}, 136 | ); 137 | }; 138 | 139 | /** 140 | * 141 | * @param resolverTimings an array of resolver timmings from the response property/key event log 142 | * @param groupPropertyKey a property/key to be used to group the initial resolver timing data received from response of event log 143 | * @param totalDuration the total duration of the entire response 144 | * @param totalScale a numeric scale that underscores the length of the graph to be rendered default 100 145 | */ 146 | export function transformTimingData( 147 | resolverTimings: Array, 148 | totalDuration: number, 149 | groupPropertyKey: string = 'startOffset', 150 | totalScale: number = 100, 151 | ): Array { 152 | return scaleResolverTiming( 153 | groupResolverTimingBy( 154 | resolverTimings 155 | .map((timing: any) => { 156 | const {path, startOffset, duration} = timing; 157 | return { 158 | durationScale: duration, 159 | duration, 160 | startOffset, 161 | path: path.join('.'), 162 | }; 163 | }) 164 | .sort( 165 | (timingA: any, timingB: any) => 166 | timingA.startOffset - timingB.startOffset, 167 | ), 168 | groupPropertyKey, 169 | ), 170 | // .groupBy(groupPropertyKey), 171 | totalDuration, 172 | totalScale, 173 | ); 174 | } 175 | -------------------------------------------------------------------------------- /src/app/utils/managedlog/eventObject.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {EventBase, MutationStoreValue} from './lib/apollo11types'; 3 | import eventLogIsDifferent, {validateOperationName} from './lib/objectDifference'; 4 | import EventLogDataObject from './lib/eventLogData'; 5 | import EventNode from './lib/eventLogNode'; 6 | 7 | export type EventLogStore = { 8 | eventId: string; 9 | queryManager: { 10 | mutationStore: Object; 11 | queriesStore: Object; 12 | }; 13 | cache?: Object; 14 | queries?: Object; 15 | mutations?: Object; 16 | }; 17 | 18 | export class EventLogContainer { 19 | _eventsBase: EventBase | undefined; 20 | 21 | _eventLogData: EventLogDataObject | undefined; 22 | 23 | constructor(srceObj?: EventLogDataObject) { 24 | this._eventLogData = srceObj; 25 | this._eventsBase = { 26 | mutation: {}, 27 | query: {}, 28 | }; 29 | } 30 | 31 | logEvent( 32 | eventNode: EventNode, 33 | baseId: string, 34 | setEvents?: React.Dispatch>, 35 | ) { 36 | this._eventLogData.addEventLog(eventNode); 37 | this._eventsBase[eventNode.content.type][baseId] = eventNode.content.event; 38 | if (setEvents) { 39 | // perform the State Hook 40 | setEvents(() => { 41 | // preEvents: EventLogDataObject 42 | return this._eventLogData; 43 | }); 44 | } 45 | } 46 | 47 | adjustEventId(evtId, entNum) { 48 | return `${evtId}.${'0'.repeat(3 - (entNum - 1).toString().length)}${( 49 | entNum - 1 50 | ).toString()}`; 51 | } 52 | 53 | sequenceApolloLog( 54 | eventLog: EventLogStore, 55 | setEvents?: React.Dispatch>, 56 | ) { 57 | // console.log('Query Log :: ', eventLog); 58 | const { 59 | queryManager: {mutationStore, queriesStore}, 60 | eventId, 61 | cache, 62 | queries, mutations 63 | } = eventLog; 64 | let evtNum = 0; 65 | // perform queriesStore Check 66 | Object.keys(queriesStore).forEach(storeKey => { 67 | const proposedQry: EventNode = new EventNode({ 68 | event: { 69 | ...queriesStore[storeKey], 70 | 'variables': queriesStore[storeKey].variables ? queriesStore[storeKey].variables : queries && queries.hasOwnProperty(storeKey) && queries[storeKey].variables ? queries[storeKey].variables : {}, 71 | request: { 72 | operation: { 73 | operationName: 74 | validateOperationName(queriesStore[storeKey].document.definitions, 'Query'), 75 | query: queriesStore[storeKey].document.loc.source.body, 76 | }, 77 | }, 78 | response: {}, 79 | }, 80 | type: 'query', 81 | eventId: this.adjustEventId(eventId, (evtNum += 1)), 82 | cache, 83 | }); 84 | // console.log('Proposed Query Snapshot :: ', proposedQry); 85 | if (!this._eventsBase.query[storeKey]) { 86 | this.logEvent(proposedQry, storeKey, setEvents); 87 | } else { 88 | // perform the diff 89 | if ( 90 | !eventLogIsDifferent( 91 | { 92 | document: this._eventsBase.query[storeKey].document, 93 | variables: this._eventsBase.query[storeKey].variables, 94 | // diff: null, //this.eventsBase.query[storeKey].diff, 95 | }, 96 | { 97 | document: queriesStore[storeKey].document, 98 | variables: queriesStore[storeKey].variables ? queriesStore[storeKey].variables : queries && queries.hasOwnProperty(storeKey) && queries[storeKey].variables ? queries[storeKey].variables : {}, 99 | // diff: null, //queriesStore[storeKey].diff, 100 | }, 101 | ) 102 | ) { 103 | this.logEvent(proposedQry, storeKey, setEvents); 104 | } 105 | } 106 | }); 107 | // perform mutationStore Check 108 | Object.keys(mutationStore).forEach(storeKey => { 109 | // console.log('Mutation Snapshot :: ', mutationStore[storeKey]); 110 | const proposedMutate: EventNode = new EventNode({ 111 | event: { 112 | ...mutationStore[storeKey], 113 | 'variables': mutationStore[storeKey].variables ? mutationStore[storeKey].variables : mutations && mutations.hasOwnProperty(storeKey) && mutations[storeKey].variables ? mutations[storeKey].variables : {}, 114 | 'loading': mutationStore[storeKey].loading ? mutationStore[storeKey].loading : mutations && mutations.hasOwnProperty(storeKey) && mutations[storeKey].loading ? mutations[storeKey].loading : {}, 115 | 'error': mutationStore[storeKey].variables ? mutationStore[storeKey].error : mutations && mutations.hasOwnProperty(storeKey) && mutations[storeKey].error ? mutations[storeKey].error : {}, 116 | request: { 117 | operation: { 118 | operationName: 119 | validateOperationName(mutationStore[storeKey].mutation.definitions, 'Mutation'), 120 | query: mutationStore[storeKey].mutation.loc.source.body, 121 | }, 122 | }, 123 | response: {}, 124 | }, 125 | type: 'mutation', 126 | eventId: this.adjustEventId(eventId, (evtNum += 1)), 127 | cache, 128 | }); 129 | // console.log('Proposed Mutation Snapshot :: ', proposedMutate); 130 | if ((proposedMutate.content.event as MutationStoreValue).loading) { 131 | return; 132 | } 133 | if (!this._eventsBase.mutation[storeKey]) { 134 | this.logEvent(proposedMutate, storeKey, setEvents); 135 | } else { 136 | // perform the diff 137 | if ( 138 | !eventLogIsDifferent( 139 | { 140 | mutation: this._eventsBase.mutation[storeKey].mutation, 141 | variables: this._eventsBase.mutation[storeKey].variables, 142 | loading: this._eventsBase.mutation[storeKey].loading, 143 | error: this._eventsBase.mutation[storeKey].error, 144 | // diff: null, //this.eventsBase.mutation[storeKey].diff, 145 | }, 146 | { 147 | mutation: mutationStore[storeKey].mutation, 148 | variables: mutationStore[storeKey].variables ? mutationStore[storeKey].variables : mutations && mutations.hasOwnProperty(storeKey) && mutations[storeKey].variables ? mutations[storeKey].variables : {}, 149 | loading: mutationStore[storeKey].loading ? mutationStore[storeKey].loading : mutations && mutations.hasOwnProperty(storeKey) && mutations[storeKey].loading ? mutations[storeKey].loading : {}, 150 | error: mutationStore[storeKey].error ? mutationStore[storeKey].error : mutations && mutations.hasOwnProperty(storeKey) && mutations[storeKey].error ? mutations[storeKey].error : {}, 151 | // diff: null, //mutationStore[storeKey].diff, 152 | }, 153 | ) 154 | ) { 155 | this.logEvent(proposedMutate, storeKey, setEvents); 156 | } 157 | } 158 | }); 159 | } 160 | 161 | getDataStore() { 162 | return this._eventLogData; 163 | } 164 | 165 | getTempStore() { 166 | return this._eventsBase; 167 | } 168 | } 169 | 170 | // export default new EventLogContainer(new eventLogDataObject()); 171 | 172 | export default (LogObject: EventLogDataObject): EventLogContainer => { 173 | return new EventLogContainer(LogObject); 174 | }; 175 | -------------------------------------------------------------------------------- /src/app/utils/managedlog/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ApolloDevQL/2c579086ce1934bede9e50e2e1e2d236018adc8a/src/app/utils/managedlog/index.ts -------------------------------------------------------------------------------- /src/app/utils/managedlog/lib/apollo11types.ts: -------------------------------------------------------------------------------- 1 | import {DocumentNode, GraphQLError} from 'graphql'; 2 | import {NetworkStatus} from './networkStatus'; 3 | 4 | export interface MutationStoreValue { 5 | mutation: DocumentNode; 6 | variables?: Object; 7 | loading?: boolean; 8 | error?: Error | null; 9 | request?: Record; 10 | response?: Record; 11 | } 12 | 13 | export interface QueryStoreValue { 14 | document: DocumentNode | null; 15 | variables?: Record; 16 | networkStatus?: NetworkStatus; 17 | networkError?: Error | null; 18 | graphQLErrors?: ReadonlyArray; 19 | request?: Record; 20 | response?: Record; 21 | } 22 | 23 | // interface EventDesc { 24 | // [k: string]: number | string | undefined | any[] | EventDesc; 25 | // } 26 | 27 | // export interface EventObject { 28 | // [k: string]: QueryStoreValue | MutationStoreValue; 29 | // } 30 | 31 | export interface EventLogObject { 32 | eventId: string; 33 | type: string; 34 | event: QueryStoreValue | MutationStoreValue; 35 | cache?: Object; 36 | } 37 | 38 | export interface EventBase { 39 | mutation: { 40 | [k: string]: { 41 | mutation: MutationStoreValue; 42 | diff?: any; 43 | variables?: Record | Object; 44 | loading?: boolean; 45 | error?: Error | null; 46 | }; 47 | }; 48 | query: { 49 | [k: string]: { 50 | document: QueryStoreValue; 51 | diff?: any; 52 | variables?: Record | Object; 53 | }; 54 | }; 55 | } 56 | 57 | export interface EventStore { 58 | [k: string]: Record | string; 59 | lastEventId?: string; 60 | } 61 | 62 | let eventStore: EventStore = { 63 | lastEventId: '1601084356248', 64 | 1601084335409: {action: {}, cache: {}, queryManager: {}}, 65 | 1601084356248: { 66 | action: {}, 67 | cache: {}, 68 | queryManager: {mutationIdCounter: 3, mutationStore: {}, queriesStore: {}}, 69 | }, 70 | }; 71 | 72 | const storeIdx = eventStore.lastEventId; 73 | 74 | const aStore = { 75 | queryManager: (eventStore[storeIdx] as any).queryManager, 76 | eventId: eventStore.lastEventId, 77 | cache: (eventStore[storeIdx] as any).cache, 78 | }; 79 | 80 | // console.log('aStore :: ', aStore); 81 | 82 | // export type Record = { 83 | // [P in K]: T; 84 | // }; 85 | 86 | // export interface ReadonlyArray { 87 | // /** 88 | // * Determines whether an array includes a certain element, returning true or false as appropriate. 89 | // * @param searchElement The element to search for. 90 | // * @param fromIndex The position in this array at which to begin searching for searchElement. 91 | // */ 92 | // includes(searchElement: T, fromIndex?: number): boolean; 93 | // } 94 | -------------------------------------------------------------------------------- /src/app/utils/managedlog/lib/eventLogData.ts: -------------------------------------------------------------------------------- 1 | import EventNode from './eventLogNode'; 2 | import {EventLogObject} from './apollo11types'; 3 | import eventLogIsDifferent from './objectDifference'; 4 | 5 | export default class EventLogDataObject { 6 | eventHead: EventNode | null; 7 | 8 | eventTail: EventNode | null; 9 | 10 | eventLength: number; 11 | 12 | constructor() { 13 | this.eventHead = null; 14 | this.eventTail = null; 15 | this.eventLength = 0; 16 | } 17 | 18 | setEventHead(content: EventNode) { 19 | if (this.eventHead === null) { 20 | this.eventHead = content; 21 | this.eventTail = content; 22 | } else { 23 | this.insertEventLogBefore(this.eventHead, content); 24 | } 25 | } 26 | 27 | addEventLog(content: EventNode) { 28 | if (this.eventHead === null) { 29 | this.setEventHead(content); 30 | } else { 31 | this.insertEventLogAfter(this.eventTail, content); 32 | } 33 | this.eventLength += 1; 34 | // return this; 35 | } 36 | 37 | insertEventLogBefore(content: EventNode, nodeToInsert: EventNode) { 38 | if (this.isNodeTailAndHead(nodeToInsert)) return; 39 | const [insertContent, insertNode] = [content, nodeToInsert]; // make a copy of mutable argument 40 | insertNode.prev = insertContent.prev; 41 | insertNode.next = insertContent; 42 | if (insertContent.prev === null) { 43 | this.eventHead = insertNode; 44 | } else { 45 | insertContent.prev.next = insertNode; 46 | } 47 | insertContent.prev = insertNode; 48 | } 49 | 50 | insertEventLogAfter(content: EventNode, nodeToInsert: EventNode) { 51 | if (this.isNodeTailAndHead(nodeToInsert)) return; 52 | const [insertContent, insertNode] = [content, nodeToInsert]; // make a copy of mutable argument 53 | insertNode.prev = insertContent; 54 | insertNode.next = insertContent.next; 55 | if (insertContent.next === null) { 56 | insertContent.next = insertNode; 57 | this.eventTail = insertNode; 58 | } else { 59 | insertContent.next = insertNode; 60 | } 61 | // return this; 62 | } 63 | 64 | insertEventLogAtPosition(position: number, nodeToInsert: EventNode) { 65 | if (position === 1) { 66 | this.setEventHead(nodeToInsert); 67 | this.eventLength += 1; 68 | } else { 69 | let eNode = this.eventHead; 70 | let currentPosition = 1; 71 | while (eNode !== null && currentPosition !== position) { 72 | eNode = eNode.next; 73 | currentPosition += 1; 74 | } 75 | if (eNode !== null) { 76 | this.insertEventLogBefore(eNode, nodeToInsert); 77 | this.eventLength += 1; 78 | } else { 79 | this.addEventLog(nodeToInsert); 80 | } 81 | } 82 | } 83 | 84 | removeEventLogNodesWithContent(content: EventLogObject) { 85 | let eNode = this.eventHead; 86 | while (eNode !== null) { 87 | const proposedToRemove = eNode; 88 | eNode = eNode.next; 89 | if (eventLogIsDifferent(eNode.content, content)) 90 | this.removeEventLogNode(proposedToRemove); 91 | } 92 | } 93 | 94 | removeEventLogNode(content: EventNode) { 95 | if (content === this.eventHead) this.eventHead = this.eventHead.next; 96 | if (content === this.eventTail) this.eventTail = this.eventTail.prev; 97 | // cleanup removed EventNode pointers 98 | this.removeEventNodePointers(content); 99 | this.eventLength -= 1; 100 | } 101 | 102 | containsEventNodeWithContent(content: EventLogObject) { 103 | if (!content) { 104 | let eNode = this.eventHead; 105 | while (eNode !== null && !eventLogIsDifferent(eNode.content, content)) { 106 | eNode = eNode.next; 107 | } 108 | return eNode === null; 109 | } 110 | return false; 111 | } 112 | 113 | append(targetObj: EventLogDataObject) { 114 | const tgtObject = targetObj; 115 | if (this.eventHead === null) { 116 | if (tgtObject && tgtObject.eventHead) { 117 | this.eventHead = tgtObject.eventHead; 118 | } 119 | if (tgtObject && tgtObject.eventTail) { 120 | this.eventTail = tgtObject.eventTail; 121 | } 122 | } else if (this.eventHead && tgtObject.eventHead) { 123 | tgtObject.eventHead.prev = this.eventTail; 124 | this.eventTail.next = tgtObject.eventHead; 125 | this.eventTail = tgtObject.eventTail; 126 | 127 | // nDe.head.prev = this.tail; 128 | // this.tail.next = nDe.head; 129 | // this.tail = nDe.tail; 130 | } 131 | this.eventLength += tgtObject.eventLength; 132 | } 133 | 134 | debugPrint(): boolean { 135 | if (this.eventHead === null && this.eventTail === null) { 136 | // console.log('Empty EventLog Object'); 137 | return false; 138 | } 139 | let temp = this.eventHead; 140 | while (temp !== null) { 141 | process.stdout.write(String(temp.content.eventId)); 142 | process.stdout.write(' <-> '); 143 | temp = temp.next; 144 | } 145 | return true; 146 | } 147 | 148 | reverseDebugPrint(): boolean { 149 | if (this.eventTail === null) { 150 | // console.log('Empty EventLog Object'); 151 | return false; 152 | } 153 | let temp = this.eventTail; 154 | while (temp != null) { 155 | process.stdout.write(String(temp.content.eventId)); 156 | process.stdout.write(' <-> '); 157 | temp = temp.prev; 158 | } 159 | return true; 160 | } 161 | 162 | isNodeTailAndHead(nodeToInsert: EventNode) { 163 | return nodeToInsert === this.eventHead && nodeToInsert === this.eventTail; 164 | } 165 | 166 | removeEventNodePointers(content: EventNode) { 167 | const removeNode = content; 168 | if (removeNode.prev !== null) removeNode.prev.next = removeNode.next; 169 | if (removeNode.next !== null) removeNode.next.prev = removeNode.prev; 170 | removeNode.prev = null; 171 | removeNode.next = null; 172 | // line to shut linter 173 | console.debug(this.eventLength); 174 | } 175 | 176 | map(decorator): any[] { 177 | const _extractList = []; 178 | let curr = this.eventHead; 179 | while (curr) { 180 | _extractList.push(decorator(curr)); 181 | curr = curr.next; 182 | } 183 | return _extractList; 184 | } 185 | } 186 | 187 | export type EventLogProps = { 188 | eventLog: EventLogDataObject; 189 | handleEventChange: any; 190 | }; 191 | 192 | export type ApolloTabProps = { 193 | eventLog: EventLogDataObject; 194 | }; 195 | 196 | export type ApolloResponsiveTabProps = { 197 | eventLog: EventLogDataObject; 198 | 199 | isDraggable: boolean; 200 | isResizable: boolean; 201 | items: number; 202 | rowHeight: number; 203 | // onLayoutChange: function () {}, 204 | cols: number; 205 | verticalCompact: boolean; 206 | resizeHandles: Array; 207 | compactType: string; 208 | preventCollision: boolean; 209 | autoSize: boolean; 210 | margin: [number, number]; 211 | }; 212 | -------------------------------------------------------------------------------- /src/app/utils/managedlog/lib/eventLogNode.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {EventLogObject} from './apollo11types'; 3 | 4 | export default class EventNode { 5 | content: EventLogObject; 6 | 7 | prev: EventNode | null; 8 | 9 | next: EventNode | null; 10 | 11 | constructor(content: EventLogObject) { 12 | this.content = content; 13 | this.prev = null; 14 | this.next = null; 15 | } 16 | } 17 | 18 | export type Apollo11ThemeContextType = { 19 | currentTheme: string; 20 | setTheme: (value: string) => void; 21 | isDark: boolean; 22 | }; 23 | 24 | export type CacheDetailsProps = { 25 | activeEvent?: EventNode; 26 | activeCache: any; 27 | }; 28 | 29 | export type CacheProps = { 30 | activeEvent?: EventNode; 31 | toggleCacheDetails?: any; 32 | handleCacheChange: any; 33 | cacheDetailsVisible?: boolean; 34 | }; 35 | 36 | export type EventDetailsProps = { 37 | activeEvent?: EventNode; 38 | }; 39 | 40 | export type EventPanelType = { 41 | children: React.ReactNode; 42 | panelValue: number; 43 | panelIndex: number; 44 | }; 45 | 46 | export type MainDrawerProps = { 47 | endpointURI: string; 48 | events: any; 49 | networkEvents: any; 50 | networkURI: string; 51 | }; 52 | -------------------------------------------------------------------------------- /src/app/utils/managedlog/lib/networkStatus.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The current status of a query’s execution in our system. 3 | */ 4 | export enum NetworkStatus { 5 | /** 6 | * The query has never been run before and the query is now currently running. A query will still 7 | * have this network status even if a partial data result was returned from the cache, but a 8 | * query was dispatched anyway. 9 | */ 10 | Loading = 1, 11 | 12 | /** 13 | * If `setVariables` was called and a query was fired because of that then the network status 14 | * will be `setVariables` until the result of that query comes back. 15 | */ 16 | SetVariables = 2, 17 | 18 | /** 19 | * Indicates that `fetchMore` was called on this query and that the query created is currently in 20 | * flight. 21 | */ 22 | FetchMore = 3, 23 | 24 | /** 25 | * Similar to the `setVariables` network status. It means that `refetch` was called on a query 26 | * and the refetch request is currently in flight. 27 | */ 28 | Refetch = 4, 29 | 30 | /** 31 | * Indicates that a polling query is currently in flight. So for example if you are polling a 32 | * query every 10 seconds then the network status will switch to `poll` every 10 seconds whenever 33 | * a poll request has been sent but not resolved. 34 | */ 35 | Poll = 6, 36 | 37 | /** 38 | * No request is in flight for this query, and no errors happened. Everything is OK. 39 | */ 40 | Ready = 7, 41 | 42 | /** 43 | * No request is in flight for this query, but one or more errors were detected. 44 | */ 45 | Error = 8, 46 | } 47 | 48 | /** 49 | * Returns true if there is currently a network request in flight according to a given network 50 | * status. 51 | */ 52 | export function isNetworkRequestInFlight( 53 | networkStatus?: NetworkStatus, 54 | ): boolean { 55 | return networkStatus ? networkStatus < 7 : false; 56 | } 57 | -------------------------------------------------------------------------------- /src/app/utils/managedlog/lib/objectDifference.ts: -------------------------------------------------------------------------------- 1 | // import {equal} from '@wry/equality'; 2 | 3 | const isLogValuePrimitive = (val: any): boolean => { 4 | return val == null || /^[sbn]/.test(typeof val); 5 | }; 6 | 7 | const eventLogIsDifferent = (a: any, b: any): boolean => { 8 | // return equal(a, b); 9 | return ( 10 | a && 11 | b && 12 | Object.keys(b).every(bKey => { 13 | const bVal = b[bKey]; 14 | const aVal = a[bKey]; 15 | if (typeof bVal === 'function') { 16 | return bVal(aVal); 17 | } 18 | return isLogValuePrimitive(bVal) 19 | ? bVal === aVal 20 | : eventLogIsDifferent(aVal, bVal); 21 | }) 22 | ); 23 | }; 24 | 25 | /** 26 | * @param objStore : ReadonlyArray> 27 | * @param objType : string 28 | * Desc:: processes the document.definitions or mutation.definitions of the query/mutation operation 29 | * this was necessary as some clients do not have a name.value on the document/mutation object 30 | * especially then having to get the operations names form the list of sections that make up the operation 31 | * Returns the operation name of the query or mutation 32 | */ 33 | export const validateOperationName = (objStore: ReadonlyArray>, objType: string,): string => { 34 | if (objStore.length) { 35 | if (objStore[0].name) { 36 | return objStore[0].name.value; 37 | } else { 38 | if (objStore[0].selectionSet && objStore[0].selectionSet.selections) { 39 | if (objStore[0].selectionSet.selections.length) { 40 | return objStore[0].selectionSet.selections.reduce((operationName, selection) => { 41 | // reduce selectionSet.selections Array's name.value to an array of name values 42 | return selection && selection.name && selection.name.value ? [...operationName, selection.name.value.toLowerCase()] : operationName; 43 | }, []).reduce((opera: string, curr: string): string => { 44 | // reduce array of names values to a string padding front and back with ( & ) respectively 45 | if (curr.toLowerCase().trim().length > 0) { 46 | if (opera === objType.toLowerCase()) { 47 | return `${opera} (${curr.toLowerCase().trim()})`; 48 | } else { 49 | return `${opera.substr(0, opera.length - 1)}, ${curr.toLowerCase().trim()})`; 50 | } 51 | } 52 | return opera; 53 | }, `${objType.toLowerCase()}`); 54 | } 55 | } 56 | } 57 | } 58 | return objType.toLowerCase(); 59 | } 60 | 61 | export default eventLogIsDifferent; 62 | -------------------------------------------------------------------------------- /src/app/utils/managedlog/other/dll.ts: -------------------------------------------------------------------------------- 1 | import EventNode from '../lib/eventLogNode'; 2 | import EventLogDataObject from '../lib/eventLogData'; 3 | import {EventLogObject} from '../lib/apollo11types'; 4 | 5 | const dllStructure = new EventLogDataObject(); 6 | const dllStructure2 = new EventLogDataObject(); 7 | 8 | const evtNode1: EventLogObject = { 9 | event: { 10 | mutation: { 11 | kind: 'Document', 12 | definitions: [], 13 | }, 14 | variables: {}, 15 | loading: false, 16 | error: null, 17 | }, 18 | type: 'query', 19 | eventId: '099087779', 20 | }; 21 | const eventNode1 = new EventNode(evtNode1); 22 | 23 | const evtNode2: EventLogObject = { 24 | event: { 25 | mutation: { 26 | kind: 'Document', 27 | definitions: [ 28 | { 29 | kind: 'OperationDefinition', 30 | operation: 'mutation', 31 | name: {kind: 'Name', value: 'newBookTrips'}, 32 | variableDefinitions: [], 33 | directives: [], 34 | selectionSet: { 35 | kind: 'SelectionSet', 36 | selections: [ 37 | { 38 | kind: 'Field', 39 | name: {kind: 'Name', value: 'newbookTrips'}, 40 | arguments: [ 41 | { 42 | kind: 'Argument', 43 | name: {kind: 'Name', value: 'newlaunchIds'}, 44 | value: { 45 | kind: 'Variable', 46 | name: {kind: 'Name', value: 'newlaunchIds'}, 47 | }, 48 | }, 49 | ], 50 | directives: [], 51 | selectionSet: { 52 | kind: 'SelectionSet', 53 | selections: [ 54 | { 55 | kind: 'Field', 56 | name: {kind: 'Name', value: 'success'}, 57 | arguments: [], 58 | directives: [], 59 | }, 60 | { 61 | kind: 'Field', 62 | name: {kind: 'Name', value: 'message'}, 63 | arguments: [], 64 | directives: [], 65 | }, 66 | { 67 | kind: 'Field', 68 | name: {kind: 'Name', value: 'launches'}, 69 | arguments: [], 70 | directives: [], 71 | selectionSet: { 72 | kind: 'SelectionSet', 73 | selections: [ 74 | { 75 | kind: 'Field', 76 | name: {kind: 'Name', value: 'id'}, 77 | arguments: [], 78 | directives: [], 79 | }, 80 | { 81 | kind: 'Field', 82 | name: {kind: 'Name', value: 'isBooked'}, 83 | arguments: [], 84 | directives: [], 85 | }, 86 | { 87 | kind: 'Field', 88 | name: {kind: 'Name', value: '__typename'}, 89 | }, 90 | ], 91 | }, 92 | }, 93 | { 94 | kind: 'Field', 95 | name: {kind: 'Name', value: '__typename'}, 96 | }, 97 | ], 98 | }, 99 | }, 100 | ], 101 | }, 102 | }, 103 | ], 104 | // loc: {start: 0, end: 173}, 105 | }, 106 | variables: {launchIds: ['100']}, 107 | loading: false, 108 | error: null, 109 | }, 110 | type: 'mutation', 111 | eventId: '099087780', 112 | }; 113 | const eventNode2 = new EventNode(evtNode2); 114 | 115 | const evtNode3: EventLogObject = { 116 | event: { 117 | mutation: { 118 | kind: 'Document', 119 | definitions: [ 120 | { 121 | kind: 'OperationDefinition', 122 | operation: 'mutation', 123 | name: {kind: 'Name', value: 'cancel'}, 124 | variableDefinitions: [ 125 | { 126 | kind: 'VariableDefinition', 127 | variable: { 128 | kind: 'Variable', 129 | name: {kind: 'Name', value: 'launchId'}, 130 | }, 131 | type: { 132 | kind: 'NonNullType', 133 | type: { 134 | kind: 'NamedType', 135 | name: {kind: 'Name', value: 'ID'}, 136 | }, 137 | }, 138 | directives: [], 139 | }, 140 | ], 141 | directives: [], 142 | selectionSet: { 143 | kind: 'SelectionSet', 144 | selections: [ 145 | { 146 | kind: 'Field', 147 | name: {kind: 'Name', value: 'cancelTrip'}, 148 | arguments: [ 149 | { 150 | kind: 'Argument', 151 | name: {kind: 'Name', value: 'launchId'}, 152 | value: { 153 | kind: 'Variable', 154 | name: {kind: 'Name', value: 'launchId'}, 155 | }, 156 | }, 157 | ], 158 | directives: [], 159 | selectionSet: { 160 | kind: 'SelectionSet', 161 | selections: [ 162 | { 163 | kind: 'Field', 164 | name: {kind: 'Name', value: 'success'}, 165 | arguments: [], 166 | directives: [], 167 | }, 168 | { 169 | kind: 'Field', 170 | name: {kind: 'Name', value: 'message'}, 171 | arguments: [], 172 | directives: [], 173 | }, 174 | { 175 | kind: 'Field', 176 | name: {kind: 'Name', value: 'launches'}, 177 | arguments: [], 178 | directives: [], 179 | selectionSet: { 180 | kind: 'SelectionSet', 181 | selections: [ 182 | { 183 | kind: 'Field', 184 | name: {kind: 'Name', value: 'id'}, 185 | arguments: [], 186 | directives: [], 187 | }, 188 | { 189 | kind: 'Field', 190 | name: {kind: 'Name', value: 'isBooked'}, 191 | arguments: [], 192 | directives: [], 193 | }, 194 | { 195 | kind: 'Field', 196 | name: {kind: 'Name', value: '__typename'}, 197 | }, 198 | ], 199 | }, 200 | }, 201 | { 202 | kind: 'Field', 203 | name: {kind: 'Name', value: '__typename'}, 204 | }, 205 | ], 206 | }, 207 | }, 208 | ], 209 | }, 210 | }, 211 | ], 212 | }, 213 | variables: {launchId: '101'}, 214 | loading: true, 215 | error: null, 216 | }, 217 | type: 'mutation', 218 | eventId: '099087787', 219 | }; 220 | const eventNode3 = new EventNode(evtNode3); 221 | 222 | const evtNode4: EventLogObject = { 223 | event: { 224 | mutation: { 225 | kind: 'Document', 226 | definitions: [], 227 | }, 228 | variables: {}, 229 | loading: false, 230 | error: null, 231 | }, 232 | type: 'query', 233 | eventId: '099087793', 234 | }; 235 | const eventNode4 = new EventNode(evtNode4); 236 | 237 | const evtNode5: EventLogObject = { 238 | event: { 239 | mutation: { 240 | kind: 'Document', 241 | definitions: [ 242 | { 243 | kind: 'OperationDefinition', 244 | operation: 'mutation', 245 | name: {kind: 'Name', value: 'newBookTrips'}, 246 | variableDefinitions: [], 247 | directives: [], 248 | selectionSet: { 249 | kind: 'SelectionSet', 250 | selections: [ 251 | { 252 | kind: 'Field', 253 | name: {kind: 'Name', value: 'newbookTrips'}, 254 | arguments: [ 255 | { 256 | kind: 'Argument', 257 | name: {kind: 'Name', value: 'newlaunchIds'}, 258 | value: { 259 | kind: 'Variable', 260 | name: {kind: 'Name', value: 'newlaunchIds'}, 261 | }, 262 | }, 263 | ], 264 | directives: [], 265 | selectionSet: { 266 | kind: 'SelectionSet', 267 | selections: [ 268 | { 269 | kind: 'Field', 270 | name: {kind: 'Name', value: 'success'}, 271 | arguments: [], 272 | directives: [], 273 | }, 274 | { 275 | kind: 'Field', 276 | name: {kind: 'Name', value: 'message'}, 277 | arguments: [], 278 | directives: [], 279 | }, 280 | { 281 | kind: 'Field', 282 | name: {kind: 'Name', value: 'launches'}, 283 | arguments: [], 284 | directives: [], 285 | selectionSet: { 286 | kind: 'SelectionSet', 287 | selections: [ 288 | { 289 | kind: 'Field', 290 | name: {kind: 'Name', value: 'id'}, 291 | arguments: [], 292 | directives: [], 293 | }, 294 | { 295 | kind: 'Field', 296 | name: {kind: 'Name', value: 'isBooked'}, 297 | arguments: [], 298 | directives: [], 299 | }, 300 | { 301 | kind: 'Field', 302 | name: {kind: 'Name', value: '__typename'}, 303 | }, 304 | ], 305 | }, 306 | }, 307 | { 308 | kind: 'Field', 309 | name: {kind: 'Name', value: '__typename'}, 310 | }, 311 | ], 312 | }, 313 | }, 314 | ], 315 | }, 316 | }, 317 | ], 318 | // loc: {start: 0, end: 173}, 319 | }, 320 | variables: {launchIds: ['100']}, 321 | loading: false, 322 | error: null, 323 | }, 324 | type: 'mutation', 325 | eventId: '099087796', 326 | }; 327 | const eventNode5 = new EventNode(evtNode5); 328 | 329 | dllStructure.addEventLog(eventNode1); 330 | // console.log(dllStructure); 331 | dllStructure.addEventLog(eventNode2); 332 | // console.log(dllStructure); 333 | dllStructure.addEventLog(eventNode3); 334 | // console.log(dllStructure); 335 | // console.log(dllStructure.map(e => e)); 336 | 337 | dllStructure2.addEventLog(eventNode4); 338 | dllStructure2.addEventLog(eventNode5); 339 | 340 | // eventNode1: '099087779', 341 | // eventNode2: '099087780', 342 | // eventNode3: '099087787', 343 | // eventNode4: '099087793', 344 | // eventNode5: '099087796', 345 | 346 | console.log('First DLL ::', dllStructure); 347 | console.log(dllStructure.debugPrint()); 348 | console.log(dllStructure.reverseDebugPrint()); 349 | console.log('Second DLL ::', dllStructure2); 350 | console.log(dllStructure2.debugPrint()); 351 | console.log(dllStructure2.reverseDebugPrint()); 352 | 353 | dllStructure.append(dllStructure2); 354 | console.log('New DLL ::', dllStructure); 355 | console.log(dllStructure.debugPrint()); 356 | console.log(dllStructure.reverseDebugPrint()); 357 | 358 | // const allStructures = dllStructure.addEventLog(dllStructure2.eventHead); 359 | // const allStructures = dllStructure.insertEventLogAfter( 360 | // dllStructure.eventTail, 361 | // dllStructure2.eventHead, 362 | // ); 363 | // console.log('Addition of BOTH'); 364 | // console.log(allStructures); 365 | 366 | // let nde = allStructures.eventHead; 367 | // while (nde) { 368 | // console.log('NDE :: ', nde); 369 | // console.log(nde.content.eventId); 370 | // nde = nde.next; 371 | // } 372 | -------------------------------------------------------------------------------- /src/app/utils/managedlog/other/index.ts: -------------------------------------------------------------------------------- 1 | // import {EventStore} from '../lib/apollo11types'; 2 | import EventLogDataObject from '../lib/eventLogData'; 3 | import EventLogTreeContainer, {EventLogStore} from '../eventObject'; 4 | // import eventLogIsDifferent from '../lib/objectDifference'; 5 | 6 | // TESTS 7 | const testobj = EventLogTreeContainer(new EventLogDataObject()); 8 | // const testEvtLog: EventBase = { 9 | // mutation: {3: {}, 4: {}}, 10 | // query: {1: {}, 2: {}}, 11 | // }; 12 | const testStore: EventLogStore = { 13 | queryManager: { 14 | mutationStore: {3: {}, 4: {}}, 15 | queriesStore: {1: {}, 2: {}}, 16 | }, 17 | eventId: '1600905405018', 18 | }; 19 | console.log('===== EventContainer Object ===='); 20 | console.log(testobj); 21 | console.log('===== Sample EventLog to Sequence ===='); 22 | console.log(testobj.sequenceApolloLog(testStore)); 23 | console.log('===== EventContainer DataStore ===='); 24 | console.log(testobj.getDataStore()); 25 | console.log('===== EventBase Local Container Store ===='); 26 | console.log(testobj.getTempStore()); 27 | -------------------------------------------------------------------------------- /src/app/utils/messaging.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {EventLogContainer} from './managedlog/eventObject'; 3 | 4 | // Listen for messages from the contentScript 5 | // The contentScript will send to the App: 6 | // - Apollo Client URI 7 | // - Apollo Client cache 8 | export default function createApolloClientListener( 9 | setApolloURI: React.Dispatch>, 10 | eventList: EventLogContainer, 11 | setEvents: React.Dispatch>, 12 | ) { 13 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 14 | const {tabId} = chrome.devtools.inspectedWindow; 15 | 16 | // Ignore any messages from contentScripts that aren't on the same tab 17 | // as the currently open Apollo devtools tab 18 | if (tabId !== sender.tab.id) { 19 | // sendResponse(`App on ${tabId} ignoring message from ${sender.tab.id}`); 20 | return; 21 | } 22 | 23 | sendResponse(`App on ${tabId} accepting message from ${sender.tab.id}`); 24 | 25 | if (request.type === 'INITIAL') { 26 | setApolloURI(request.apolloURI); 27 | } 28 | 29 | // Check if this is the initial message sent by the contentScript 30 | // i.e., received without a pre-generated eventId, 31 | // so set eventId to zero so it is the smallest value key in the events object 32 | // This keeps the first cache sent to be chronologically the first one in the events object 33 | let {eventId} = request; 34 | if (eventId === 'null') { 35 | eventId = '0'; 36 | } 37 | const {cache, queryManager, queries, mutations} = request; 38 | eventList.sequenceApolloLog({queryManager, eventId, cache, queries, mutations}, setEvents); 39 | }); 40 | } 41 | 42 | // Send a message to the contentScript to get the Apollo Client cache 43 | // Need to pass it the pre-generated eventId so it can correlate the cache + data 44 | // with its corresponding network request 45 | export function getApolloClient() { 46 | // Get the active tab and send a message to the contentScript to get the cache 47 | chrome.tabs.query({active: true}, function getClientData(tabs) { 48 | if (tabs.length) { 49 | chrome.tabs.sendMessage(tabs[0].id, { 50 | type: 'GET_APOLLO_CLIENT', 51 | }); 52 | } 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /src/app/utils/networking.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // Parses the network request to find a potentially valid GraphQL operation 4 | // Currently only supports HTTP POST and GET methods 5 | const getGraphQLOperation = (httpReq: any) => { 6 | const {request} = httpReq; 7 | let operation; 8 | 9 | if ( 10 | request.method === 'POST' && 11 | request.postData && 12 | request.postData.text && 13 | request.postData.mimeType && 14 | request.postData.mimeType.includes('json') 15 | ) { 16 | operation = JSON.parse(request.postData.text); 17 | 18 | // In some cases, the operation field is an Array 19 | if (Array.isArray(operation)) { 20 | [operation] = operation; 21 | } 22 | 23 | if (operation) { 24 | if (!operation.query) { 25 | // console.log('Operation has no query object'); 26 | operation = null; 27 | } 28 | 29 | // Previously we required these keywords in the query but now we 30 | // relaxed that restriction and simply log it if we don't see it 31 | // if ( 32 | // !operation.query.startsWith('query') && 33 | // !operation.query.startsWith('mutation') && 34 | // !operation.query.startsWith('fragment') 35 | // ) { 36 | // console.log('Operation does not start with keyword'); 37 | // } 38 | } 39 | } 40 | 41 | if (request.method === 'GET' && request.queryString) { 42 | if (Array.isArray(request.queryString)) { 43 | operation = {}; 44 | request.queryString.forEach(item => { 45 | operation[item.name] = decodeURIComponent(item.value); 46 | }); 47 | const keys = Object.keys(operation); 48 | if (keys.includes('operationName') || keys.includes('query')) { 49 | // console.log('graphQL GET operation', operation, 'for URL', request.url); 50 | } else { 51 | // console.log('graphQL GET has no operationName or query', operation); 52 | operation = null; 53 | } 54 | } 55 | } 56 | 57 | // For debugging only -- log the network request if we can't parse it 58 | // properly but there's a 'graphql' string in the URL. 59 | // if ( 60 | // !operation && 61 | // request.url.includes('graphql') && 62 | // request.method !== 'OPTIONS' 63 | // ) { 64 | // console.log('Ignoring potential graphql request', request); 65 | // } 66 | 67 | return operation; 68 | }; 69 | 70 | // Listens for network events and filters them only for GraphQL requests 71 | // Currently only looks for queries and mutations 72 | export default function createNetworkEventListener( 73 | setNetworkURI: React.Dispatch>, 74 | setNetworkEvents: React.Dispatch>, 75 | ) { 76 | chrome.devtools.network.onRequestFinished.addListener((httpReq: any) => { 77 | const operation = getGraphQLOperation(httpReq); 78 | 79 | if (!operation) return; 80 | 81 | const searchIndex = httpReq.request.url.indexOf('?'); 82 | const baseURL = 83 | searchIndex !== -1 84 | ? httpReq.request.url.slice(0, searchIndex) 85 | : httpReq.request.url; 86 | const {startedDateTime, time, timings} = httpReq; 87 | const request = { 88 | bodySize: httpReq.request.bodySize, 89 | headersSize: httpReq.request.headersSize, 90 | method: httpReq.request.method, 91 | operation, 92 | url: baseURL, 93 | }; 94 | const response = { 95 | bodySize: httpReq.response.bodySize, 96 | headersSize: httpReq.response.headersSize, 97 | }; 98 | 99 | // The eventId will store the Unix epoch time of the GraphQL network request 100 | // It will act as the key in the Events object where the cache and 101 | // related data will be stored 102 | const requestId = new Date(startedDateTime).getTime().toString(); 103 | const eventId = new Date().getTime().toString(); 104 | 105 | if (httpReq.request.method === 'POST') { 106 | // console.log('httpReq.request.url :>> ', httpReq.request.url); 107 | setNetworkURI(httpReq.request.url); 108 | } 109 | 110 | // The response from the GraphQL request is not immediately available 111 | // Have to invoke the getContent() method to obtain this 112 | httpReq.getContent((content: string) => { 113 | const event: any = {}; 114 | event.requestId = requestId; 115 | event.eventId = eventId; 116 | event.request = request; 117 | event.response = response; 118 | event.response.content = JSON.parse(content); 119 | if (httpReq.response.content.size) { 120 | event.response.content.size = httpReq.response.content.size; 121 | } else { 122 | // console.log( 123 | // 'httpReq.getContent has no content.size for event :>>', 124 | // event, 125 | // ); 126 | } 127 | event.startedDateTime = startedDateTime; 128 | event.time = time; 129 | event.timings = timings; 130 | 131 | setNetworkEvents(prevNetworkEvents => { 132 | const newNetworkEvents = {...prevNetworkEvents}; 133 | 134 | if (!newNetworkEvents[eventId]) { 135 | newNetworkEvents[eventId] = {}; 136 | } 137 | 138 | newNetworkEvents[eventId] = {...prevNetworkEvents[eventId], ...event}; 139 | 140 | return newNetworkEvents; 141 | }); 142 | }); 143 | }); 144 | } 145 | -------------------------------------------------------------------------------- /src/app/utils/performanceMetricsCalcs.tsx: -------------------------------------------------------------------------------- 1 | // find the max time btw events that have been tracked so far by Apollo 2 | const getMaxEventTime = (eventsObj: any): number => { 3 | // console.log('in get MaxEventTime'); 4 | // make events into an array of key, value pairs 5 | const eventsObjDuple: Array = Object.entries(eventsObj); 6 | 7 | const eventsArray = eventsObjDuple.map(el => el[1]); 8 | 9 | let max = 0; 10 | 11 | // if there are events 12 | if (eventsArray.length > 0) { 13 | // find max time 14 | max = eventsArray.reduce((acc, curr) => { 15 | if (acc < curr.time) { 16 | return curr.time; 17 | } 18 | 19 | return acc; 20 | 21 | // start acc at 0 22 | }, 0); 23 | } 24 | return max; 25 | }; 26 | 27 | export default getMaxEventTime; 28 | -------------------------------------------------------------------------------- /src/app/utils/tracingTimeFormating.tsx: -------------------------------------------------------------------------------- 1 | const formatTime = (time: number) => { 2 | let formattedTime = time; 3 | if (formattedTime < 1000) return `${formattedTime} ns`; 4 | 5 | formattedTime = Math.floor(formattedTime / 1000); 6 | if (formattedTime < 1000) return `${formattedTime} µs`; 7 | 8 | formattedTime = Math.floor(formattedTime / 1000); 9 | return `${formattedTime} ms`; 10 | }; 11 | 12 | const formatTimeForProgressBar = (time: number): number => { 13 | let formattedTime = time; 14 | if (formattedTime < 1000) return formattedTime; 15 | 16 | formattedTime = Math.floor(formattedTime / 1000); 17 | if (formattedTime < 1000) return formattedTime; 18 | 19 | formattedTime = Math.floor(formattedTime / 1000); 20 | return formattedTime; 21 | }; 22 | 23 | const TimeMagnitude = (time: number): string => { 24 | let formattedTime = time; 25 | if (formattedTime < 1000) return 'ns'; 26 | 27 | formattedTime = Math.floor(formattedTime / 1000); 28 | if (formattedTime < 1000) return 'µs'; 29 | 30 | formattedTime = Math.floor(formattedTime / 1000); 31 | return 'ms'; 32 | }; 33 | 34 | const filterSortResolvers = (resolversArray: Array, magnitude: string) => { 35 | if (resolversArray.length === 0) return []; 36 | 37 | const output = resolversArray 38 | .filter(resolver => TimeMagnitude(resolver.duration) === magnitude) 39 | .sort((a, b) => b.duration - a.duration); 40 | 41 | return output; 42 | }; 43 | 44 | const createResolversArray = (timingInfo: any) => { 45 | const output = []; 46 | 47 | // console.log('timingInfo in create Array1', timingInfo); 48 | 49 | if (!timingInfo.resolvers) return []; 50 | 51 | // console.log('timingInfo in create Array2', timingInfo); 52 | 53 | const resolverObj = timingInfo.resolvers; 54 | const keysArray = Object.keys(resolverObj); 55 | 56 | keysArray.forEach(key => { 57 | resolverObj[key].forEach(resolver => { 58 | output.push(resolver); 59 | }); 60 | }); 61 | 62 | // console.log('output', output); 63 | 64 | return output; 65 | }; 66 | 67 | export { 68 | formatTime, 69 | formatTimeForProgressBar, 70 | TimeMagnitude, 71 | filterSortResolvers, 72 | createResolversArray, 73 | }; 74 | -------------------------------------------------------------------------------- /src/app/utils/useClientEventlogs.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // interface IApolloEventLog { 4 | // [key: string]: any; 5 | // } 6 | 7 | // import React, {createContext, FC, useContext, useEffect, useState} from 'react'; 8 | 9 | // interface IViewport { 10 | // width: number; 11 | // } 12 | 13 | // const ViewportContext = createContext({ 14 | // width: window.innerWidth, 15 | // }); 16 | 17 | // // export const ViewportProvider: FC = ({ children }) => { 18 | // type Props = { 19 | // children: React.ReactNode; 20 | // }; 21 | // export const ViewportProvider = ({children}: Props) => { 22 | // const [width, setWidth] = useState(window.innerWidth); 23 | 24 | // const handleResize = () => setWidth(window.innerWidth); 25 | 26 | // useEffect(() => { 27 | // window.addEventListener('resize', handleResize); 28 | // return () => window.removeEventListener('resize', handleResize); 29 | // }, []); 30 | 31 | // return ( 32 | // 33 | // {children} 34 | // 35 | // ); 36 | // }; 37 | 38 | // export function useViewport() { 39 | // return useContext(ViewportContext); 40 | // } 41 | 42 | // const defaultTheme = 'white'; 43 | // const ThemeContext = React.createContext(defaultTheme); 44 | // type Props = { 45 | // children: React.ReactNode; 46 | // }; 47 | // export const ThemeProvider = ({children}: Props) => { 48 | // const [theme, setTheme] = React.useState(defaultTheme); 49 | 50 | // React.useEffect(() => { 51 | // // We'd get the theme from a web API / local storage in a real app 52 | // // We've hardcoded the theme in our example 53 | // const currentTheme = 'lightblue'; 54 | // setTheme(currentTheme); 55 | // }, []); 56 | 57 | // return ( 58 | // {children} 59 | // ); 60 | // }; 61 | 62 | const useClientEventlogs = () => { 63 | const [eventLogs, updateEventLogs] = React.useState((): any => ({})); 64 | const updateLogs = (evts: any) => { 65 | // console.log('updating hook wt ', evts); 66 | updateEventLogs(() => { 67 | // console.log('Previous Events :: ', prevEvents); 68 | return evts; 69 | }); 70 | }; 71 | return {eventLogs, updateLogs}; 72 | }; 73 | 74 | export default useClientEventlogs; 75 | -------------------------------------------------------------------------------- /src/assets/Apollo11-Events-Low.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ApolloDevQL/2c579086ce1934bede9e50e2e1e2d236018adc8a/src/assets/Apollo11-Events-Low.gif -------------------------------------------------------------------------------- /src/assets/Apollo11-GraphiQL-Low.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ApolloDevQL/2c579086ce1934bede9e50e2e1e2d236018adc8a/src/assets/Apollo11-GraphiQL-Low.gif -------------------------------------------------------------------------------- /src/assets/Apollo11-Performance-Low.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ApolloDevQL/2c579086ce1934bede9e50e2e1e2d236018adc8a/src/assets/Apollo11-Performance-Low.gif -------------------------------------------------------------------------------- /src/assets/ApolloDevQL-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ApolloDevQL/2c579086ce1934bede9e50e2e1e2d236018adc8a/src/assets/ApolloDevQL-01.png -------------------------------------------------------------------------------- /src/assets/ApolloDevQL-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ApolloDevQL/2c579086ce1934bede9e50e2e1e2d236018adc8a/src/assets/ApolloDevQL-02.png -------------------------------------------------------------------------------- /src/extension/background.ts: -------------------------------------------------------------------------------- 1 | // console.log('this is background.ts'); 2 | -------------------------------------------------------------------------------- /src/extension/build/devtools.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | ⚛ ApolloDevQL 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/extension/build/devtools.ts: -------------------------------------------------------------------------------- 1 | // The initial script that loads the extension into the DevTools panel. 2 | chrome.devtools.panels.create( 3 | 'ApolloDevQL', // title of devtool panel 4 | '../assets/covalent-recoil-logo.jpg', // icon of devtool panel 5 | 'panel.html', // html of devtool panel 6 | ); 7 | -------------------------------------------------------------------------------- /src/extension/build/diff.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ApolloDevQL/2c579086ce1934bede9e50e2e1e2d236018adc8a/src/extension/build/diff.css -------------------------------------------------------------------------------- /src/extension/build/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ApolloDevQL", 3 | "version": "1.0.0", 4 | "devtools_page": "devtools.html", 5 | "description": "ApolloDevQL", 6 | "manifest_version": 2, 7 | "content_security_policy": "script-src 'self' 'unsafe-eval' ; object-src 'self'", 8 | "permissions": ["tabs", "activeTab", "storage"], 9 | "background": { 10 | "scripts": ["bundles/background.bundle.js"], 11 | "persistent": false 12 | }, 13 | "web_accessible_resources": ["bundles/apollo.bundle.js"], 14 | "content_scripts": [ 15 | { 16 | "matches": ["http://*/*", "https://*/*"], 17 | "js": ["bundles/content.bundle.js"] 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/extension/build/panel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/extension/build/stylesheet.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/ApolloDevQL/2c579086ce1934bede9e50e2e1e2d236018adc8a/src/extension/build/stylesheet.css -------------------------------------------------------------------------------- /src/extension/contentScript.ts: -------------------------------------------------------------------------------- 1 | // console.log('Executing contentScript.ts...'); 2 | 3 | // Need to inject our script as an IIFE into the DOM 4 | // This will allow us to obtain the __APOLLO_CLIENT__ object 5 | // on the application's window object. 6 | // https://stackoverflow.com/questions/12395722/can-the-window-object-be-modified-from-a-chrome-extension 7 | const injectScript = () => { 8 | if (document instanceof HTMLDocument) { 9 | const s = document.createElement('script'); 10 | s.setAttribute('data-version', chrome.runtime.getManifest().version); 11 | s.src = chrome.extension.getURL('bundles/apollo.bundle.js'); 12 | document.body.appendChild(s); 13 | } 14 | }; 15 | 16 | // Listen for messages from the App 17 | // If a message to get the cache is received, it will inject the detection code 18 | chrome.runtime.onMessage.addListener(request => { 19 | if (request && request.type && request.type === 'GET_APOLLO_CLIENT') { 20 | injectScript(); 21 | } 22 | }); 23 | 24 | // Listen for messages from the injected script 25 | // Once a message is received, it will send a message to the App 26 | // with the Apollo Client URI and cache 27 | window.addEventListener( 28 | 'message', 29 | function sendClientData(event) { 30 | // We only accept messages from ourselves 31 | if (event.source !== window) { 32 | return; 33 | } 34 | 35 | if (!event.data.type || !event.data.eventId) { 36 | return; 37 | } 38 | 39 | const apolloClient = { 40 | type: event.data.type, 41 | eventId: event.data.eventId, 42 | apolloURI: event.data.apolloURI, 43 | cache: event.data.cache, 44 | action: event.data.action, 45 | inspector: event.data.inspector, 46 | queries: event.data.queries, 47 | mutations: event.data.mutations, 48 | queryManager: event.data.queryManager, 49 | }; 50 | chrome.runtime.sendMessage(apolloClient); 51 | }, 52 | false, 53 | ); 54 | 55 | // Immediately inject the detection code once the contentScript is loaded every time 56 | // we navigate to a new website 57 | // This mitigates issues where the App panel has already mounted and sent its initial 58 | // requests for the URI and cache, but there isn't any website loaded yet (i.e. empty tab) 59 | injectScript(); 60 | -------------------------------------------------------------------------------- /src/extension/hook/apollo.ts: -------------------------------------------------------------------------------- 1 | // This callback is injected into the Apollo Client and will be invoked whenever 2 | // there is a query or mutation operation 3 | // 4 | // When invoked, it will collect the Apollo Client cache and related stores to 5 | // send to the contentScript for further processing in our extension 6 | function apollo11Callback( 7 | win: any, 8 | action: any, 9 | queries: any, 10 | mutations: any, 11 | inspector: any, 12 | initial: boolean = false, 13 | ) { 14 | // If the callback is invoked with a special HEARTBEAT action, 15 | // it will simply return the HEARTBEAT to show that it is still 16 | // "alive" -- i.e., injected in the Apollo Client 17 | if (action === 'HEARTBEAT') { 18 | return 'APOLLO11_CALLBACK_HEARTBEAT'; 19 | } 20 | 21 | const queryManager: any = {}; 22 | 23 | const apolloCache = win.__APOLLO_CLIENT__.cache; 24 | let cache: any = {}; 25 | if (apolloCache && apolloCache.data && apolloCache.data.data) { 26 | cache = apolloCache.data.data; 27 | } 28 | 29 | const apolloQM = win.__APOLLO_CLIENT__.queryManager; 30 | if (apolloQM) { 31 | const store: any = {}; 32 | if (apolloQM.queries instanceof Map) { 33 | apolloQM.queries.forEach((info: any, queryId: any) => { 34 | store[queryId] = { 35 | variables: info.variables, 36 | networkStatus: info.networkStatus, 37 | networkError: info.networkError, 38 | graphQLErrors: info.graphQLErrors, 39 | document: info.document, 40 | diff: info.diff, 41 | }; 42 | }); 43 | } 44 | 45 | queryManager.queriesStore = store; 46 | queryManager.mutationStore = apolloQM.mutationStore.store; 47 | 48 | // The counters below were being used to determine when new queries 49 | // or mutations were handled in the Apollo Client. They're no longer 50 | // used as we are now diffing the object structure but are still 51 | // being sent for the time being 52 | 53 | // v3 counters 54 | queryManager.requestIdCounter = apolloQM.requestIdCounter; 55 | queryManager.queryIdCounter = apolloQM.queryIdCounter; 56 | queryManager.mutationIdCounter = apolloQM.mutationIdCounter; 57 | 58 | // v2 counter 59 | queryManager.idCounter = apolloQM.idCounter; 60 | } 61 | 62 | const {link} = win.__APOLLO_CLIENT__; 63 | let apolloURI = ''; 64 | if (link && link.options && link.options.uri) { 65 | apolloURI = link.options.uri; 66 | } 67 | 68 | // If this is the very first (i.e. INITIAL) invocation of the callback, 69 | // set the eventId to 0 so that the app knows how to handle the very first 70 | // cache in the client, which might be empty 71 | const type = initial ? 'INITIAL' : 'APOLLO_CLIENT'; 72 | const eventId = initial ? '0' : new Date().getTime().toString(); 73 | const apolloClient = { 74 | type, 75 | eventId, 76 | apolloURI, 77 | cache, 78 | action, 79 | inspector, 80 | queries, 81 | mutations, 82 | queryManager, 83 | }; 84 | 85 | win.postMessage(apolloClient); 86 | return undefined; 87 | } 88 | 89 | // Injects our callback into the Apollo Client by invoking the built-in hook 90 | // It does not preserve any other callback that might have been injected into 91 | // it previously 92 | const injectApollo11Callback = (win: any) => { 93 | if (win.__APOLLO_CLIENT__) { 94 | win.__APOLLO_CLIENT__.__actionHookForDevTools( 95 | ({ 96 | action, 97 | state: {queries, mutations}, 98 | dataWithOptimisticResults: inspector, 99 | }) => { 100 | const heartbeat = apollo11Callback( 101 | win, 102 | action, 103 | queries, 104 | mutations, 105 | inspector, 106 | ); 107 | return heartbeat; 108 | }, 109 | ); 110 | } 111 | }; 112 | 113 | // This HEARTBEAT "listener" will invoke our injected callback and if it 114 | // doesn't get a HEARTBEAT response, it means our callback has been 115 | // overwritten by another extension. 116 | // 117 | // It will the attempt to re-inject our callback into the devToolsHookCb 118 | const heartbeatListener = () => { 119 | const win: any = window; 120 | const options = { 121 | action: 'HEARTBEAT', 122 | state: {queries: {}, mutations: {}}, 123 | dataWithOptimisticResults: {}, 124 | }; 125 | const heartbeat = win.__APOLLO_CLIENT__.devToolsHookCb(options); 126 | if (heartbeat !== 'APOLLO11_CALLBACK_HEARTBEAT') { 127 | injectApollo11Callback(win); 128 | } 129 | }; 130 | 131 | // This IIFE will be invoked by the contentScript whenever the user navigates 132 | // to a new website. It will set up an interval timer that will wait for the 133 | // presence of an __APOLLO_CLIENT__ object on the window. 134 | // If it finds it, it will invoke and inject our callback into the Apollo Client 135 | // and clear the interval timer previously set. 136 | // Once injected, it will also set up a HEARTBEAT listener to ensure that our 137 | // callback has not been removed or overwritten by another extension. 138 | (function hooked(win: any) { 139 | // eslint-disable-next-line no-undef 140 | let detectionInterval: NodeJS.Timeout; 141 | const findApolloClient = () => { 142 | if (win.__APOLLO_CLIENT__) { 143 | clearInterval(detectionInterval); 144 | 145 | // Immediately invoke the callback to retrieve the initial state of the 146 | // Apollo Client cache 147 | apollo11Callback(win, null, null, null, null, true); 148 | injectApollo11Callback(win); 149 | setInterval(heartbeatListener, 1000); 150 | } 151 | }; 152 | detectionInterval = global.setInterval(findApolloClient, 1000); 153 | })(window); 154 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./build", 4 | "module": "commonjs", 5 | "pretty": true, 6 | "noImplicitAny": false, 7 | "removeComments": true, 8 | "preserveConstEnums": true, 9 | "sourceMap": true, 10 | "allowJs": true, 11 | "jsx": "react", 12 | "target": "esnext", 13 | "esModuleInterop": true, 14 | "lib": ["es2015", "esnext.asynciterable", "dom"] 15 | }, 16 | "include": ["./src/app/**/*", "./index.d.ts", "./src/custom.d.ts"], 17 | "exclude": ["node_modules", ".vscode", "__tests__"] 18 | } 19 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const ChromeExtensionReloader = require('webpack-chrome-extension-reloader'); 3 | 4 | const config = { 5 | entry: { 6 | app: './src/app/Panel/index.tsx', 7 | background: './src/extension/background.ts', 8 | content: './src/extension/contentScript.ts', 9 | apollo: './src/extension/hook/apollo.ts', 10 | }, 11 | output: { 12 | path: path.resolve(__dirname, 'src/extension/build/bundles'), 13 | filename: '[name].bundle.js', 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.tsx?$/, 19 | use: 'ts-loader', 20 | exclude: [/node_modules/, /__tests__/, /__mocks__/], 21 | resolve: { 22 | extensions: ['.ts', '.tsx', '.js', '.jsx'], 23 | }, 24 | }, 25 | { 26 | test: /\.jsx?/, 27 | exclude: [/node_modules/, /__tests__/, /__mocks__/], 28 | resolve: { 29 | extensions: ['.js', '.jsx'], 30 | }, 31 | use: { 32 | loader: 'babel-loader', 33 | options: { 34 | presets: ['@babel/preset-env', '@babel/preset-react'], 35 | }, 36 | }, 37 | }, 38 | { 39 | test: /\.scss$/, 40 | use: ['style-loader', 'css-loader', 'sass-loader'], 41 | }, 42 | { 43 | test: /\.css$/, 44 | use: ['style-loader', 'css-loader'], 45 | }, 46 | { 47 | test: /\.mjs$/, 48 | include: /node_modules/, 49 | type: 'javascript/auto', 50 | }, 51 | ], 52 | }, 53 | resolve: { 54 | extensions: ['.mjs', '.tsx', '.ts', '.js', '.jsx'], 55 | }, 56 | plugins: [], 57 | }; 58 | 59 | module.exports = (env, argv) => { 60 | if (argv.mode === 'development') { 61 | config.plugins.push( 62 | new ChromeExtensionReloader({ 63 | entries: { 64 | contentScript: ['app', 'content'], 65 | background: ['background'], 66 | }, 67 | }), 68 | ); 69 | } 70 | return config; 71 | }; 72 | --------------------------------------------------------------------------------