├── .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 | [](https://github.com/oslabs-beta/ApolloDevQL)[](https://snyk.io/test/github/oslabs-beta/ApolloDevQL)
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 |
16 | {panelValue === panelIndex && <>{children}>}
17 |
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 |
141 |
142 | {loadingEndpoint ? (
143 | <>
144 |
150 |
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 |
213 | {
220 | this.setState({
221 | noFetch: !noFetch,
222 | query: undefined,
223 | variables: undefined,
224 | });
225 | }}
226 | />
227 | Load from cache
228 | */}{' '}
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 |
--------------------------------------------------------------------------------