mirror of
https://github.com/reduxjs/redux-devtools.git
synced 2024-11-25 11:03:57 +03:00
[feat][monitor] Add rkt-query-inspector-monitor - feat/rtk query monitor (#750)
* chore: copy rtk-query example from toolkit
* feat(rtk-query): complete initial setup of rtk-query
* feat: complete inspector layout and add initial JSONTree setup
* fix: unintentional removal of tsconfig
* feat(search): add search logic and refactor monitor state shape
* fix: inverted monitor theme inside devtoop-app
Othetr changes:
* simplify monitor integration
* fix: rtk monitor reducer not working in app
* refactor(rtk-query): simplify theme config
* feat(rtk-query-monitor): add query preview tabs
* fix: wip
* refactor(examples): add rtk-query-polling to workspace
Other changes:
* docs(rtk-query-polling): add README.md
* chore(rtk-query-inspector): add demo to monorepo
Other changes:
chore: increase isWideScreen polling interval to 300
refactor: add subscription as root node in QueryPreviewSubscriptions
* feat(rtk-query): add multiple filter options
* chore(rtk-queery): rename demo build script and add SKIP_PREFLIGHT_CHECK=true
* feat(rtk-query): display status flags in QueryPreviewInfo
* chore(rtk-query): update typescript versions in rkt-inspector-monitor & its demo
* docs(rtk-query): add proper README
Other changes:
* fix examples/rtk-query-poilling
* docs(rtk-query): improve rtk-query-inspector-monitor demo gif
* docs(rtk-query): clean up demo
* fix(rtk-query): clear button not updating redux state
* docs(rtk-query): add link to rtk-query-inspector-monitor demo site
* chore(rtk-query): run prettier after prettier upgrade (55e2284
)
* docs(rtk.query): clean up readme add features, todo and development section
* docs(rtk-query): fix link href
* chore(rtk-query): clean up rtk-query-imspector-monitor-demo and add post example
* feat(rtk-query): add counters on tags & subs tab buttons
* fix(rtk-query): layering issue between queryPreview tabList and select
Other changes:
* clean up demo styles
* run prettier
* fix: revert accidental changes packages/redux-devtools-serialize/tsconfig.json
* chore: change QueryComparators.fulfilledTimeStamp label
* feat(rtk-query): display api stats
* refactor: remove rtk-query-polling example from monorepo
* chore: regenerate lock file and add @types/react as monorepo devDep
* chore: display apiState
Other changes:
* fix close button overflow
* minor responsive style tweaks
* display reducerPath in query tab
* fix(rtk-query): resolve CI errors
- fix(prettier:check): unformatted file
- fix(lint:all): fix accidentallly removed .eslintignore
* chore(rtk-query): rename package to '@redux-devtools/rtk-query-monitor'
* fix(rtk-query): lint:all error
https://github.com/reduxjs/redux-devtools/runs/2869729048?check_suite_focus=true
* feat(rtk-query): add fallback message if there are no rtk-query apis
* refactor(rtk-query): move Inspector & Monitor to containers clean up typings
Other changes:
* chore: improved type coverage
* chore: do not lint packages/redux-devtools-rtk-query-monitor/demo folder
* refactor: put sort order buttons inside a component
* chore: hopefully resolve mockServiceWorker formatting issues
* fix(rtk-query): incorrect link color
Other changes:
* fix: add missing anchor property value noopener
* refactor(rtk-query): simplify sort order control
* feat(rtk-query): add timings to apiStats sections
* feat(rtk-query): add slowest and fastest in timings section
* feat(rtk-query): improve formatting of timings and display average loading time
* fix(rtk-query): rtk-query imports
* refactor(rtk-query): reduce selector computations
Other changes:
* simplify TreeView props
* feat(rtk-query): add actions tab
* refactor(rtk-query): add custom logic for TreeView shouldExpandNode
Other changes:
* feat: added duration in QueryPreviewInfo tab
* refactor: TreeView component
* chore(rtk-query): improve demo visibility on small devices
* feat(rtk-query): do not display tree node preview
Other changes:
* improve visibility of demo devTools on small devices
* tweak QueryPreviewInfo labels
* chore(rtk-query): improve responsiveness
* refactor(rtk-query): move preview to containers remove unnecessary computation
* feat(rtk-query): display median of timings
Other changes:
* improved shouldExpandNode logic of QueryPreviewActions
* tweaked mean logic
* refactor(rtk-query-monitor): conform demo setup to repo standards
* chore(rtk-query-monitor): add option to select active devtools
* chore(rtk-query-monitor): remove redux-devtools/examples/rtk-query-polling
* refactor(rtk-query): improve UI of api tab
* feat(rtk-query): add regex search
* feat(rtk-query): display mutations in queryList
* refactor(rtk-query): track all fulfilled requests using actions
Other changes:
* refactor(rtk-query): rename tally properties
* chore(rtk-query): update @redux-devtools/rtk-query-monitor dependencies
* fix(rtk-query): demo build failing caused by a typing error
This commit is contained in:
parent
6cdc18f2fa
commit
7d92a5e186
|
@ -9,3 +9,5 @@ node_modules
|
||||||
__snapshots__
|
__snapshots__
|
||||||
dev
|
dev
|
||||||
.yarn/*
|
.yarn/*
|
||||||
|
**/.yarn/*
|
||||||
|
**/demo/public/**
|
|
@ -50,6 +50,9 @@
|
||||||
"webpack-cli": "^3.3.12",
|
"webpack-cli": "^3.3.12",
|
||||||
"webpack-dev-server": "^3.11.2"
|
"webpack-dev-server": "^3.11.2"
|
||||||
},
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"@types/react": "16.14.8"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lerna": "lerna",
|
"lerna": "lerna",
|
||||||
"build:all": "lerna run build",
|
"build:all": "lerna run build",
|
||||||
|
|
|
@ -43,7 +43,9 @@
|
||||||
"@redux-devtools/inspector-monitor-test-tab": "^0.7.2",
|
"@redux-devtools/inspector-monitor-test-tab": "^0.7.2",
|
||||||
"@redux-devtools/inspector-monitor-trace-tab": "^0.2.2",
|
"@redux-devtools/inspector-monitor-trace-tab": "^0.2.2",
|
||||||
"@redux-devtools/log-monitor": "^2.3.0",
|
"@redux-devtools/log-monitor": "^2.3.0",
|
||||||
|
"@redux-devtools/rtk-query-monitor": "^1.0.0",
|
||||||
"@redux-devtools/slider-monitor": "^2.0.0-8",
|
"@redux-devtools/slider-monitor": "^2.0.0-8",
|
||||||
|
"@reduxjs/toolkit": "^1.6.0",
|
||||||
"d3-state-visualizer": "^1.4.0",
|
"d3-state-visualizer": "^1.4.0",
|
||||||
"devui": "^1.0.0-9",
|
"devui": "^1.0.0-9",
|
||||||
"javascript-stringify": "^2.1.0",
|
"javascript-stringify": "^2.1.0",
|
||||||
|
|
|
@ -2,11 +2,13 @@ import React from 'react';
|
||||||
import LogMonitor from '@redux-devtools/log-monitor';
|
import LogMonitor from '@redux-devtools/log-monitor';
|
||||||
import ChartMonitorWrapper from '../containers/monitors/ChartMonitorWrapper';
|
import ChartMonitorWrapper from '../containers/monitors/ChartMonitorWrapper';
|
||||||
import InspectorWrapper from '../containers/monitors/InspectorWrapper';
|
import InspectorWrapper from '../containers/monitors/InspectorWrapper';
|
||||||
|
import RtkQueryMonitor from '@redux-devtools/rtk-query-monitor';
|
||||||
|
|
||||||
export const monitors = [
|
export const monitors = [
|
||||||
{ value: 'InspectorMonitor', name: 'Inspector' },
|
{ value: 'InspectorMonitor', name: 'Inspector' },
|
||||||
{ value: 'LogMonitor', name: 'Log monitor' },
|
{ value: 'LogMonitor', name: 'Log monitor' },
|
||||||
{ value: 'ChartMonitor', name: 'Chart' },
|
{ value: 'ChartMonitor', name: 'Chart' },
|
||||||
|
{ value: 'RtkQueryMonitor', name: 'RTK Query' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function getMonitor({ monitor }: { monitor: string }) {
|
export default function getMonitor({ monitor }: { monitor: string }) {
|
||||||
|
@ -17,6 +19,8 @@ export default function getMonitor({ monitor }: { monitor: string }) {
|
||||||
);
|
);
|
||||||
case 'ChartMonitor':
|
case 'ChartMonitor':
|
||||||
return <ChartMonitorWrapper />;
|
return <ChartMonitorWrapper />;
|
||||||
|
case 'RtkQueryMonitor':
|
||||||
|
return <RtkQueryMonitor />;
|
||||||
default:
|
default:
|
||||||
return <InspectorWrapper />;
|
return <InspectorWrapper />;
|
||||||
}
|
}
|
||||||
|
|
0
packages/redux-devtools-cli/bin/redux-devtools.js
Normal file → Executable file
0
packages/redux-devtools-cli/bin/redux-devtools.js
Normal file → Executable file
8
packages/redux-devtools-rtk-query-monitor/.babelrc
Normal file
8
packages/redux-devtools-rtk-query-monitor/.babelrc
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"presets": [
|
||||||
|
"@babel/preset-env",
|
||||||
|
"@babel/preset-react",
|
||||||
|
"@babel/preset-typescript"
|
||||||
|
],
|
||||||
|
"plugins": ["@babel/plugin-proposal-class-properties"]
|
||||||
|
}
|
2
packages/redux-devtools-rtk-query-monitor/.eslintignore
Normal file
2
packages/redux-devtools-rtk-query-monitor/.eslintignore
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
lib
|
||||||
|
demo/
|
13
packages/redux-devtools-rtk-query-monitor/.eslintrc.js
Normal file
13
packages/redux-devtools-rtk-query-monitor/.eslintrc.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
module.exports = {
|
||||||
|
extends: '../../.eslintrc',
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: ['*.ts', '*.tsx'],
|
||||||
|
extends: '../../eslintrc.ts.react.base.json',
|
||||||
|
parserOptions: {
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
project: ['./tsconfig.json'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
21
packages/redux-devtools-rtk-query-monitor/LICENSE.md
Normal file
21
packages/redux-devtools-rtk-query-monitor/LICENSE.md
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2021 Fabrizio Vitale
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
108
packages/redux-devtools-rtk-query-monitor/README.md
Normal file
108
packages/redux-devtools-rtk-query-monitor/README.md
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
# Redux DevTools RTK Query inspector monitor
|
||||||
|
|
||||||
|
A monitor that displays [RTK query](https://redux-toolkit.js.org/rtk-query/overview) queries and mutations for [Redux DevTools](https://github.com/gaearon/redux-devtools).
|
||||||
|
|
||||||
|
Created by [FaberVitale](https://github.com/FaberVitale), inspired by [react-query devtools](https://github.com/tannerlinsley/react-query/tree/master/devtools).
|
||||||
|
|
||||||
|
## Demo
|
||||||
|
|
||||||
|
- [link](https://rtk-query-monitor-demo.netlify.app/)
|
||||||
|
- [demo source](https://github.com/FaberVitale/redux-devtools/tree/feat/rtk-query-monitor/packages/redux-devtools-rtk-query-monitor/demo)
|
||||||
|
|
||||||
|
## Preview
|
||||||
|
|
||||||
|
![RTK Query inspector monitor demo](./monitor-demo.gif)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### npm
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm i @redux-devtools/rtk-query-monitor --save
|
||||||
|
```
|
||||||
|
|
||||||
|
### yarn
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn add @redux-devtools/rtk-query-monitor
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
You can use `RtkQueryMonitor` as the only monitor in your app:
|
||||||
|
|
||||||
|
##### `containers/DevTools.js`
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import React from 'react';
|
||||||
|
import { createDevTools } from '@redux-devtools/core';
|
||||||
|
import RtkQueryrMonitor from '@redux-devtools/rtk-query-monitor';
|
||||||
|
|
||||||
|
export default createDevTools(<RtkQueryrMonitor />);
|
||||||
|
```
|
||||||
|
|
||||||
|
Then you can render `<DevTools>` to any place inside app or even into a separate popup window.
|
||||||
|
|
||||||
|
Alternatively, you can use it together with [`DockMonitor`](https://github.com/reduxjs/redux-devtools/tree/master/packages/redux-devtools-dock-monitor) to make it dockable.
|
||||||
|
|
||||||
|
See also
|
||||||
|
|
||||||
|
- [`DockMonitor` README](https://github.com/reduxjs/redux-devtools/tree/master/packages/redux-devtools-dock-monitor)
|
||||||
|
|
||||||
|
- [Read how to start using Redux DevTools.](https://github.com/reduxjs/redux-devtools)
|
||||||
|
|
||||||
|
- [Redux Devtools walkthrough](https://github.com/reduxjs/redux-devtools/tree/master/docs/Walkthrough.md)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- sorts queries in ascending or descending order by:
|
||||||
|
- fulfilledTimeStamp
|
||||||
|
- query key
|
||||||
|
- query status
|
||||||
|
- endpoint
|
||||||
|
- api reducerPath
|
||||||
|
- filters queries by:
|
||||||
|
- fulfilledTimeStamp
|
||||||
|
- query key
|
||||||
|
- query status
|
||||||
|
- endpoint
|
||||||
|
- api reducerPath
|
||||||
|
- displays
|
||||||
|
- status flags
|
||||||
|
- query state
|
||||||
|
- tags
|
||||||
|
- subscriptions
|
||||||
|
- api state
|
||||||
|
- api stats
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
|
||||||
|
- [ ] display mutations
|
||||||
|
- [ ] filter by tags types
|
||||||
|
- [ ] download query.data
|
||||||
|
- [ ] upload query.data(?)
|
||||||
|
- [ ] refetch query button(?)
|
||||||
|
- ...suggestions are welcome
|
||||||
|
|
||||||
|
## Redux DevTools props
|
||||||
|
|
||||||
|
| Name | Description |
|
||||||
|
| ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| `theme` | Either a string referring to one of the themes provided by [redux-devtools-themes](https://github.com/gaearon/redux-devtools-themes) (feel free to contribute!) or a custom object of the same format. Optional. By default, set to [`'nicinabox'`](https://github.com/gaearon/redux-devtools-themes/blob/master/src/nicinabox.js). |
|
||||||
|
| `invertTheme` | Boolean value that will invert the colors of the selected theme. Optional. By default, set to `false` |
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
### Development
|
||||||
|
|
||||||
|
#### Start Demo
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn lerna run start --stream --scope @redux-devtools/rtk-query-monitor
|
||||||
|
```
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT](./LICENSE.md)
|
11
packages/redux-devtools-rtk-query-monitor/demo/.babelrc
Normal file
11
packages/redux-devtools-rtk-query-monitor/demo/.babelrc
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"presets": [
|
||||||
|
"@babel/preset-env",
|
||||||
|
"@babel/preset-react",
|
||||||
|
"@babel/preset-typescript"
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
"@babel/plugin-proposal-class-properties",
|
||||||
|
"@babel/plugin-transform-runtime"
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
module.exports = {
|
||||||
|
extends: ['react-app'],
|
||||||
|
};
|
4
packages/redux-devtools-rtk-query-monitor/demo/.gitignore
vendored
Normal file
4
packages/redux-devtools-rtk-query-monitor/demo/.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
.snowpack
|
||||||
|
build
|
||||||
|
node_modules
|
||||||
|
.yarn/*
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"extends": "../../../../tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"resolveJsonModule": true
|
||||||
|
},
|
||||||
|
"include": ["webpack.config.ts"]
|
||||||
|
}
|
|
@ -0,0 +1,88 @@
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as webpack from 'webpack';
|
||||||
|
import HtmlWebpackPlugin from 'html-webpack-plugin';
|
||||||
|
import { CleanWebpackPlugin } from 'clean-webpack-plugin';
|
||||||
|
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
|
||||||
|
import * as pkg from '../../package.json';
|
||||||
|
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||||
|
const isProduction = process.env.NODE_ENV === 'production';
|
||||||
|
|
||||||
|
const demoSrc = path.join(__dirname, '../src');
|
||||||
|
const libSrc = path.join(__dirname, '../../src');
|
||||||
|
const publicDir = path.join(__dirname, '../public');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
mode: process.env.NODE_ENV || 'development',
|
||||||
|
entry: isProduction
|
||||||
|
? ['./demo/src/index']
|
||||||
|
: [
|
||||||
|
'webpack-dev-server/client?http://localhost:3000',
|
||||||
|
'webpack/hot/only-dev-server',
|
||||||
|
'./demo/src/index',
|
||||||
|
],
|
||||||
|
output: {
|
||||||
|
path: path.join(__dirname, '../dist'),
|
||||||
|
filename: isProduction ? '[name].[contenthash:8].js' : 'js/bundle.js',
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.(js|ts)x?$/,
|
||||||
|
loader: 'babel-loader',
|
||||||
|
exclude: /node_modules/,
|
||||||
|
include: [demoSrc, libSrc],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.css?$/,
|
||||||
|
loaders: ['style-loader', 'css-loader'],
|
||||||
|
include: demoSrc,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
modules: ['node_modules', demoSrc],
|
||||||
|
extensions: ['.js', '.jsx', '.ts', '.tsx'],
|
||||||
|
},
|
||||||
|
optimization: {
|
||||||
|
minimize: isProduction,
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new CleanWebpackPlugin(),
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
inject: true,
|
||||||
|
template: 'demo/public/index.html',
|
||||||
|
package: pkg,
|
||||||
|
}),
|
||||||
|
new CopyWebpackPlugin({
|
||||||
|
patterns: [
|
||||||
|
{
|
||||||
|
from: 'demo/public/assets/*.js',
|
||||||
|
to: ({ absoluteFilename }: any) => {
|
||||||
|
return `./${path.basename(absoluteFilename)}`;
|
||||||
|
},
|
||||||
|
globOptions: {
|
||||||
|
ignore: ['*.DS_Store'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
new ForkTsCheckerWebpackPlugin({
|
||||||
|
typescript: {
|
||||||
|
configFile: 'demo/tsconfig.json',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
].concat(isProduction ? [] : [new webpack.HotModuleReplacementPlugin()]),
|
||||||
|
devServer: isProduction
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
quiet: false,
|
||||||
|
port: 3000,
|
||||||
|
hot: true,
|
||||||
|
stats: {
|
||||||
|
chunkModules: false,
|
||||||
|
colors: true,
|
||||||
|
},
|
||||||
|
historyApiFallback: true,
|
||||||
|
},
|
||||||
|
devtool: isProduction ? 'source-map' : 'cheap-module-source-map',
|
||||||
|
};
|
|
@ -0,0 +1,322 @@
|
||||||
|
/**
|
||||||
|
* Mock Service Worker.
|
||||||
|
* @see https://github.com/mswjs/msw
|
||||||
|
* - Please do NOT modify this file.
|
||||||
|
* - Please do NOT serve this file on production.
|
||||||
|
*/
|
||||||
|
/* eslint-disable */
|
||||||
|
/* tslint:disable */
|
||||||
|
|
||||||
|
const INTEGRITY_CHECKSUM = '82ef9b96d8393b6da34527d1d6e19187'
|
||||||
|
const bypassHeaderName = 'x-msw-bypass'
|
||||||
|
const activeClientIds = new Set()
|
||||||
|
|
||||||
|
self.addEventListener('install', function () {
|
||||||
|
return self.skipWaiting()
|
||||||
|
})
|
||||||
|
|
||||||
|
self.addEventListener('activate', async function (event) {
|
||||||
|
return self.clients.claim()
|
||||||
|
})
|
||||||
|
|
||||||
|
self.addEventListener('message', async function (event) {
|
||||||
|
const clientId = event.source.id
|
||||||
|
|
||||||
|
if (!clientId || !self.clients) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = await self.clients.get(clientId)
|
||||||
|
|
||||||
|
if (!client) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const allClients = await self.clients.matchAll()
|
||||||
|
|
||||||
|
switch (event.data) {
|
||||||
|
case 'KEEPALIVE_REQUEST': {
|
||||||
|
sendToClient(client, {
|
||||||
|
type: 'KEEPALIVE_RESPONSE',
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'INTEGRITY_CHECK_REQUEST': {
|
||||||
|
sendToClient(client, {
|
||||||
|
type: 'INTEGRITY_CHECK_RESPONSE',
|
||||||
|
payload: INTEGRITY_CHECKSUM,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'MOCK_ACTIVATE': {
|
||||||
|
activeClientIds.add(clientId)
|
||||||
|
|
||||||
|
sendToClient(client, {
|
||||||
|
type: 'MOCKING_ENABLED',
|
||||||
|
payload: true,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'MOCK_DEACTIVATE': {
|
||||||
|
activeClientIds.delete(clientId)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'CLIENT_CLOSED': {
|
||||||
|
activeClientIds.delete(clientId)
|
||||||
|
|
||||||
|
const remainingClients = allClients.filter((client) => {
|
||||||
|
return client.id !== clientId
|
||||||
|
})
|
||||||
|
|
||||||
|
// Unregister itself when there are no more clients
|
||||||
|
if (remainingClients.length === 0) {
|
||||||
|
self.registration.unregister()
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Resolve the "master" client for the given event.
|
||||||
|
// Client that issues a request doesn't necessarily equal the client
|
||||||
|
// that registered the worker. It's with the latter the worker should
|
||||||
|
// communicate with during the response resolving phase.
|
||||||
|
async function resolveMasterClient(event) {
|
||||||
|
const client = await self.clients.get(event.clientId)
|
||||||
|
|
||||||
|
if (client.frameType === 'top-level') {
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
const allClients = await self.clients.matchAll()
|
||||||
|
|
||||||
|
return allClients
|
||||||
|
.filter((client) => {
|
||||||
|
// Get only those clients that are currently visible.
|
||||||
|
return client.visibilityState === 'visible'
|
||||||
|
})
|
||||||
|
.find((client) => {
|
||||||
|
// Find the client ID that's recorded in the
|
||||||
|
// set of clients that have registered the worker.
|
||||||
|
return activeClientIds.has(client.id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRequest(event, requestId) {
|
||||||
|
const client = await resolveMasterClient(event)
|
||||||
|
const response = await getResponse(event, client, requestId)
|
||||||
|
|
||||||
|
// Send back the response clone for the "response:*" life-cycle events.
|
||||||
|
// Ensure MSW is active and ready to handle the message, otherwise
|
||||||
|
// this message will pend indefinitely.
|
||||||
|
if (client && activeClientIds.has(client.id)) {
|
||||||
|
;(async function () {
|
||||||
|
const clonedResponse = response.clone()
|
||||||
|
sendToClient(client, {
|
||||||
|
type: 'RESPONSE',
|
||||||
|
payload: {
|
||||||
|
requestId,
|
||||||
|
type: clonedResponse.type,
|
||||||
|
ok: clonedResponse.ok,
|
||||||
|
status: clonedResponse.status,
|
||||||
|
statusText: clonedResponse.statusText,
|
||||||
|
body:
|
||||||
|
clonedResponse.body === null ? null : await clonedResponse.text(),
|
||||||
|
headers: serializeHeaders(clonedResponse.headers),
|
||||||
|
redirected: clonedResponse.redirected,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})()
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getResponse(event, client, requestId) {
|
||||||
|
const { request } = event
|
||||||
|
const requestClone = request.clone()
|
||||||
|
const getOriginalResponse = () => fetch(requestClone)
|
||||||
|
|
||||||
|
// Bypass mocking when the request client is not active.
|
||||||
|
if (!client) {
|
||||||
|
return getOriginalResponse()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bypass initial page load requests (i.e. static assets).
|
||||||
|
// The absence of the immediate/parent client in the map of the active clients
|
||||||
|
// means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
|
||||||
|
// and is not ready to handle requests.
|
||||||
|
if (!activeClientIds.has(client.id)) {
|
||||||
|
return await getOriginalResponse()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bypass requests with the explicit bypass header
|
||||||
|
if (requestClone.headers.get(bypassHeaderName) === 'true') {
|
||||||
|
const cleanRequestHeaders = serializeHeaders(requestClone.headers)
|
||||||
|
|
||||||
|
// Remove the bypass header to comply with the CORS preflight check.
|
||||||
|
delete cleanRequestHeaders[bypassHeaderName]
|
||||||
|
|
||||||
|
const originalRequest = new Request(requestClone, {
|
||||||
|
headers: new Headers(cleanRequestHeaders),
|
||||||
|
})
|
||||||
|
|
||||||
|
return fetch(originalRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the request to the client-side MSW.
|
||||||
|
const reqHeaders = serializeHeaders(request.headers)
|
||||||
|
const body = await request.text()
|
||||||
|
|
||||||
|
const clientMessage = await sendToClient(client, {
|
||||||
|
type: 'REQUEST',
|
||||||
|
payload: {
|
||||||
|
id: requestId,
|
||||||
|
url: request.url,
|
||||||
|
method: request.method,
|
||||||
|
headers: reqHeaders,
|
||||||
|
cache: request.cache,
|
||||||
|
mode: request.mode,
|
||||||
|
credentials: request.credentials,
|
||||||
|
destination: request.destination,
|
||||||
|
integrity: request.integrity,
|
||||||
|
redirect: request.redirect,
|
||||||
|
referrer: request.referrer,
|
||||||
|
referrerPolicy: request.referrerPolicy,
|
||||||
|
body,
|
||||||
|
bodyUsed: request.bodyUsed,
|
||||||
|
keepalive: request.keepalive,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
switch (clientMessage.type) {
|
||||||
|
case 'MOCK_SUCCESS': {
|
||||||
|
return delayPromise(
|
||||||
|
() => respondWithMock(clientMessage),
|
||||||
|
clientMessage.payload.delay,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'MOCK_NOT_FOUND': {
|
||||||
|
return getOriginalResponse()
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'NETWORK_ERROR': {
|
||||||
|
const { name, message } = clientMessage.payload
|
||||||
|
const networkError = new Error(message)
|
||||||
|
networkError.name = name
|
||||||
|
|
||||||
|
// Rejecting a request Promise emulates a network error.
|
||||||
|
throw networkError
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'INTERNAL_ERROR': {
|
||||||
|
const parsedBody = JSON.parse(clientMessage.payload.body)
|
||||||
|
|
||||||
|
console.error(
|
||||||
|
`\
|
||||||
|
[MSW] Request handler function for "%s %s" has thrown the following exception:
|
||||||
|
|
||||||
|
${parsedBody.errorType}: ${parsedBody.message}
|
||||||
|
(see more detailed error stack trace in the mocked response body)
|
||||||
|
|
||||||
|
This exception has been gracefully handled as a 500 response, however, it's strongly recommended to resolve this error.
|
||||||
|
If you wish to mock an error response, please refer to this guide: https://mswjs.io/docs/recipes/mocking-error-responses\
|
||||||
|
`,
|
||||||
|
request.method,
|
||||||
|
request.url,
|
||||||
|
)
|
||||||
|
|
||||||
|
return respondWithMock(clientMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return getOriginalResponse()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.addEventListener('fetch', function (event) {
|
||||||
|
const { request } = event
|
||||||
|
|
||||||
|
// Bypass navigation requests.
|
||||||
|
if (request.mode === 'navigate') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Opening the DevTools triggers the "only-if-cached" request
|
||||||
|
// that cannot be handled by the worker. Bypass such requests.
|
||||||
|
if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bypass all requests when there are no active clients.
|
||||||
|
// Prevents the self-unregistered worked from handling requests
|
||||||
|
// after it's been deleted (still remains active until the next reload).
|
||||||
|
if (activeClientIds.size === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestId = uuidv4()
|
||||||
|
|
||||||
|
return event.respondWith(
|
||||||
|
handleRequest(event, requestId).catch((error) => {
|
||||||
|
console.error(
|
||||||
|
'[MSW] Failed to mock a "%s" request to "%s": %s',
|
||||||
|
request.method,
|
||||||
|
request.url,
|
||||||
|
error,
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
function serializeHeaders(headers) {
|
||||||
|
const reqHeaders = {}
|
||||||
|
headers.forEach((value, name) => {
|
||||||
|
reqHeaders[name] = reqHeaders[name]
|
||||||
|
? [].concat(reqHeaders[name]).concat(value)
|
||||||
|
: value
|
||||||
|
})
|
||||||
|
return reqHeaders
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendToClient(client, message) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const channel = new MessageChannel()
|
||||||
|
|
||||||
|
channel.port1.onmessage = (event) => {
|
||||||
|
if (event.data && event.data.error) {
|
||||||
|
return reject(event.data.error)
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(event.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
client.postMessage(JSON.stringify(message), [channel.port2])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function delayPromise(cb, duration) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => resolve(cb()), duration)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function respondWithMock(clientMessage) {
|
||||||
|
return new Response(clientMessage.payload.body, {
|
||||||
|
...clientMessage.payload,
|
||||||
|
headers: clientMessage.payload.headers,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function uuidv4() {
|
||||||
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||||
|
const r = (Math.random() * 16) | 0
|
||||||
|
const v = c == 'x' ? r : (r & 0x3) | 0x8
|
||||||
|
return v.toString(16)
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="@redux-devtools/rtk-query-monitor demo site"
|
||||||
|
/>
|
||||||
|
<title>RTK Query monitor demo</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
<script type="module" src="/dist/index.js"></script>
|
||||||
|
<!--
|
||||||
|
This HTML file is a template.
|
||||||
|
If you open it directly in the browser, you will see an empty page.
|
||||||
|
|
||||||
|
You can add webfonts, meta tags, or analytics to this file.
|
||||||
|
The build step will place the bundled scripts into the <body> tag.
|
||||||
|
|
||||||
|
To begin the development, run `npm start` or `yarn start`.
|
||||||
|
To create a production bundle, use `npm run build` or `yarn build`.
|
||||||
|
-->
|
||||||
|
</body>
|
||||||
|
</html>
|
71
packages/redux-devtools-rtk-query-monitor/demo/src/App.tsx
Normal file
71
packages/redux-devtools-rtk-query-monitor/demo/src/App.tsx
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import PokemonView from 'features/pokemon/PokemonView';
|
||||||
|
import PostsView from 'features/posts/PostsView';
|
||||||
|
import { Box, Flex, Heading } from '@chakra-ui/react';
|
||||||
|
import { Link, UnorderedList, ListItem } from '@chakra-ui/react';
|
||||||
|
import { Code } from '@chakra-ui/react';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { DevToolsSelector } from 'features/DevTools/DevToolsSelector';
|
||||||
|
|
||||||
|
export function App() {
|
||||||
|
return (
|
||||||
|
<main className="rtk-query-demo-app">
|
||||||
|
<Heading as="h1" p="2">
|
||||||
|
RTK Query inspector monitor demo
|
||||||
|
</Heading>
|
||||||
|
<PokemonView />
|
||||||
|
<PostsView />
|
||||||
|
<DevToolsSelector />
|
||||||
|
<Flex p="2" as="section" flexWrap="nowrap" flexDirection="column">
|
||||||
|
<Heading as="h2">Dock controls</Heading>
|
||||||
|
<Box as="pre" p="2" paddingX="4">
|
||||||
|
<Code>
|
||||||
|
{`toggleVisibilityKey="ctrl-h"\nchangePositionKey="ctrl-q"`}
|
||||||
|
</Code>
|
||||||
|
</Box>
|
||||||
|
<Box as="p" p="2" paddingX="4">
|
||||||
|
Drag its border to resize
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
<Flex p="2" as="footer">
|
||||||
|
<UnorderedList p="2">
|
||||||
|
<ListItem>
|
||||||
|
<Link
|
||||||
|
className="link"
|
||||||
|
isExternal
|
||||||
|
href="https://github.com/FaberVitale/redux-devtools/tree/feat/rtk-query-monitor/packages/redux-devtools-rtk-query-monitor/demo"
|
||||||
|
>
|
||||||
|
demo source
|
||||||
|
</Link>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem>
|
||||||
|
<Link
|
||||||
|
className="link"
|
||||||
|
isExternal
|
||||||
|
href="https://github.com/FaberVitale/redux-devtools/tree/feat/rtk-query-monitor/packages/redux-devtools-rtk-query-monitor"
|
||||||
|
>
|
||||||
|
@redux-devtools/rtk-query-monitor source
|
||||||
|
</Link>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem>
|
||||||
|
<Link
|
||||||
|
className="link"
|
||||||
|
isExternal
|
||||||
|
href="https://github.com/reduxjs/redux-toolkit/tree/master/examples/query/react/polling"
|
||||||
|
>
|
||||||
|
polling example
|
||||||
|
</Link>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem>
|
||||||
|
<Link
|
||||||
|
className="link"
|
||||||
|
isExternal
|
||||||
|
href="https://github.com/reduxjs/redux-toolkit/tree/master/examples/query/react/mutations"
|
||||||
|
>
|
||||||
|
mutations example
|
||||||
|
</Link>
|
||||||
|
</ListItem>
|
||||||
|
</UnorderedList>
|
||||||
|
</Flex>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { createDevTools } from '@redux-devtools/core';
|
||||||
|
import DockMonitor from '@redux-devtools/dock-monitor';
|
||||||
|
import RtkQueryMonitor from '../../../../src';
|
||||||
|
|
||||||
|
const largeScreenQuery = window.matchMedia('(min-width: 1024px)');
|
||||||
|
|
||||||
|
export default createDevTools(
|
||||||
|
<DockMonitor
|
||||||
|
toggleVisibilityKey="ctrl-h"
|
||||||
|
changePositionKey="ctrl-q"
|
||||||
|
changeMonitorKey="ctrl-m"
|
||||||
|
fluid
|
||||||
|
defaultSize={largeScreenQuery.matches ? 0.44 : 0.55}
|
||||||
|
>
|
||||||
|
<RtkQueryMonitor />
|
||||||
|
</DockMonitor>
|
||||||
|
);
|
|
@ -0,0 +1,40 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { ButtonGroup, Button } from '@chakra-ui/react';
|
||||||
|
import { isExtensionEnabled, setIsExtensionEnabled } from './helpers';
|
||||||
|
import { Box, Heading } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
export function DevToolsSelector() {
|
||||||
|
const handleClick = (evt: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
setIsExtensionEnabled(evt.currentTarget.dataset.extension === '1');
|
||||||
|
window.location.reload();
|
||||||
|
};
|
||||||
|
|
||||||
|
const extensionEnabled = isExtensionEnabled();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box as="section" p="2">
|
||||||
|
<Heading as="h2">Set active devTools</Heading>
|
||||||
|
<ButtonGroup variant="outline" spacing="4" p="4">
|
||||||
|
<Button
|
||||||
|
aria-selected={!extensionEnabled}
|
||||||
|
colorScheme="blue"
|
||||||
|
selected={!extensionEnabled}
|
||||||
|
data-extension="0"
|
||||||
|
variant={!extensionEnabled ? 'solid' : 'outline'}
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
Dock
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
aria-selected={extensionEnabled}
|
||||||
|
data-extension="1"
|
||||||
|
colorScheme="blue"
|
||||||
|
variant={extensionEnabled ? 'solid' : 'outline'}
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
Extension
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export const isExtensionEnabledKey = 'prefer-extension';
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { isExtensionEnabledKey } from './config';
|
||||||
|
|
||||||
|
export function isExtensionEnabled(): boolean {
|
||||||
|
let extensionEnabled = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
extensionEnabled =
|
||||||
|
window.sessionStorage.getItem(isExtensionEnabledKey) === '1';
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
return extensionEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setIsExtensionEnabled(active: boolean): void {
|
||||||
|
try {
|
||||||
|
window.sessionStorage.setItem(isExtensionEnabledKey, active ? '1' : '0');
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Button, Select } from '@chakra-ui/react';
|
||||||
|
import { useGetPokemonByNameQuery } from '../../services/pokemon';
|
||||||
|
import type { PokemonName } from '../../pokemon.data';
|
||||||
|
|
||||||
|
const intervalOptions = [
|
||||||
|
{ label: 'Off', value: 0 },
|
||||||
|
{ label: '3s', value: 3000 },
|
||||||
|
{ label: '5s', value: 5000 },
|
||||||
|
{ label: '10s', value: 10000 },
|
||||||
|
{ label: '1m', value: 60000 },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function Pokemon({ name }: { name: PokemonName }) {
|
||||||
|
const [pollingInterval, setPollingInterval] = useState(60000);
|
||||||
|
|
||||||
|
const { data, error, isLoading, isFetching, refetch } =
|
||||||
|
useGetPokemonByNameQuery(name, {
|
||||||
|
pollingInterval,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="pokemon"
|
||||||
|
style={{
|
||||||
|
...(isFetching ? { background: '#e6ffe8' } : {}),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{error ? (
|
||||||
|
<>Oh no, there was an error loading {name}</>
|
||||||
|
) : isLoading ? (
|
||||||
|
<>Loading...</>
|
||||||
|
) : data ? (
|
||||||
|
<>
|
||||||
|
<h3>{data.species.name}</h3>
|
||||||
|
<div style={{ minWidth: 96, minHeight: 96 }}>
|
||||||
|
<img
|
||||||
|
src={data.sprites.front_shiny}
|
||||||
|
alt={data.species.name}
|
||||||
|
style={{ ...(isFetching ? { opacity: 0.3 } : {}) }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style={{ display: 'block' }}>Polling interval</label>
|
||||||
|
<Select
|
||||||
|
value={pollingInterval}
|
||||||
|
onChange={({ target: { value } }) =>
|
||||||
|
setPollingInterval(Number(value))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{intervalOptions.map(({ label, value }) => (
|
||||||
|
<option key={value} value={value}>
|
||||||
|
{label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
colorScheme="blue"
|
||||||
|
variant="outline"
|
||||||
|
onClick={refetch}
|
||||||
|
disabled={isFetching}
|
||||||
|
>
|
||||||
|
{isFetching ? 'Loading' : 'Manually refetch'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'No Data'
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { Pokemon } from './Pokemon';
|
||||||
|
import { PokemonName, POKEMON_NAMES } from '../../pokemon.data';
|
||||||
|
import { Flex, Heading, Button } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
const getRandomPokemonName = () =>
|
||||||
|
POKEMON_NAMES[Math.floor(Math.random() * POKEMON_NAMES.length)];
|
||||||
|
|
||||||
|
export default function PokemonView() {
|
||||||
|
const [pokemon, setPokemon] = React.useState<PokemonName[]>(['bulbasaur']);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex p="2" as="section" flexWrap="nowrap" flexDirection="column">
|
||||||
|
<Heading as="h2">Pokemon polling demo</Heading>
|
||||||
|
<Flex p="2" gridGap="0.5em" flexDirection="row" flexWrap="wrap">
|
||||||
|
<Button
|
||||||
|
colorScheme="blue"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() =>
|
||||||
|
setPokemon((prev) => [...prev, getRandomPokemonName()])
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Add random pokemon
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
colorScheme="blue"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setPokemon((prev) => [...prev, 'bulbasaur'])}
|
||||||
|
>
|
||||||
|
Add bulbasaur
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<div className="pokemon-list">
|
||||||
|
{pokemon.map((name, index) => (
|
||||||
|
<Pokemon key={index} name={name} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,155 @@
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useHistory, useParams } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
useDeletePostMutation,
|
||||||
|
useGetPostQuery,
|
||||||
|
useUpdatePostMutation,
|
||||||
|
} from 'services/posts';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
CloseButton,
|
||||||
|
Flex,
|
||||||
|
Heading,
|
||||||
|
Input,
|
||||||
|
Spacer,
|
||||||
|
Stack,
|
||||||
|
useToast,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
|
||||||
|
const EditablePostName = ({
|
||||||
|
name: initialName,
|
||||||
|
onUpdate,
|
||||||
|
onCancel,
|
||||||
|
isLoading = false,
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
onUpdate: (name: string) => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
isLoading?: boolean;
|
||||||
|
}) => {
|
||||||
|
const [name, setName] = useState(initialName);
|
||||||
|
|
||||||
|
const handleChange = ({
|
||||||
|
target: { value },
|
||||||
|
}: React.ChangeEvent<HTMLInputElement>) => setName(value);
|
||||||
|
|
||||||
|
const handleUpdate = () => onUpdate(name);
|
||||||
|
const handleCancel = () => onCancel();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex>
|
||||||
|
<Box flex={10}>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
onChange={handleChange}
|
||||||
|
value={name}
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Spacer />
|
||||||
|
<Box>
|
||||||
|
<Stack spacing={4} direction="row" align="center">
|
||||||
|
<Button onClick={handleUpdate} isLoading={isLoading}>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
<CloseButton bg="red" onClick={handleCancel} disabled={isLoading} />
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const PostJsonDetail = ({ id }: { id: string }) => {
|
||||||
|
const { data: post } = useGetPostQuery(id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box mt={5} bg="#eee">
|
||||||
|
<pre>{JSON.stringify(post, null, 2)}</pre>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PostDetail = () => {
|
||||||
|
const { id } = useParams<{ id: any }>();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
|
||||||
|
const { data: post, isLoading } = useGetPostQuery(id);
|
||||||
|
|
||||||
|
const [updatePost, { isLoading: isUpdating }] = useUpdatePostMutation();
|
||||||
|
|
||||||
|
const [deletePost, { isLoading: isDeleting }] = useDeletePostMutation();
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <div>Loading...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!post) {
|
||||||
|
return (
|
||||||
|
<Center h="200px">
|
||||||
|
<Heading size="md">
|
||||||
|
Post {id} is missing! Try reloading or selecting another post...
|
||||||
|
</Heading>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box p={4}>
|
||||||
|
{isEditing ? (
|
||||||
|
<EditablePostName
|
||||||
|
name={post.name}
|
||||||
|
onUpdate={async (name) => {
|
||||||
|
try {
|
||||||
|
await updatePost({ id, name }).unwrap();
|
||||||
|
} catch {
|
||||||
|
toast({
|
||||||
|
title: 'An error occurred',
|
||||||
|
description: "We couldn't save your changes, try again!",
|
||||||
|
status: 'error',
|
||||||
|
duration: 2000,
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsEditing(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onCancel={() => setIsEditing(false)}
|
||||||
|
isLoading={isUpdating}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Flex>
|
||||||
|
<Box>
|
||||||
|
<Heading size="md">{post.name}</Heading>
|
||||||
|
</Box>
|
||||||
|
<Spacer />
|
||||||
|
<Box>
|
||||||
|
<Stack spacing={4} direction="row" align="center">
|
||||||
|
<Button
|
||||||
|
onClick={() => setIsEditing(true)}
|
||||||
|
disabled={isDeleting || isUpdating}
|
||||||
|
>
|
||||||
|
{isUpdating ? 'Updating...' : 'Edit'}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() =>
|
||||||
|
deletePost(id).then(() => history.push('/posts'))
|
||||||
|
}
|
||||||
|
disabled={isDeleting}
|
||||||
|
colorScheme="red"
|
||||||
|
>
|
||||||
|
{isDeleting ? 'Deleting...' : 'Delete'}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
<PostJsonDetail id={post.id} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,160 @@
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Center,
|
||||||
|
Divider,
|
||||||
|
Flex,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
Heading,
|
||||||
|
Input,
|
||||||
|
List,
|
||||||
|
ListIcon,
|
||||||
|
ListItem,
|
||||||
|
Spacer,
|
||||||
|
Stat,
|
||||||
|
StatLabel,
|
||||||
|
StatNumber,
|
||||||
|
useToast,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { Route, Switch, useHistory } from 'react-router-dom';
|
||||||
|
import { MdBook } from 'react-icons/md';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Post, useAddPostMutation, useGetPostsQuery } from 'services/posts';
|
||||||
|
import { PostDetail } from './PostDetail';
|
||||||
|
|
||||||
|
const AddPost = () => {
|
||||||
|
const initialValue = { name: '' };
|
||||||
|
const [post, setPost] = useState<Pick<Post, 'name'>>(initialValue);
|
||||||
|
const [addPost, { isLoading }] = useAddPostMutation();
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
const handleChange = ({ target }: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setPost((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[target.name]: target.value,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddPost = async () => {
|
||||||
|
try {
|
||||||
|
await addPost(post).unwrap();
|
||||||
|
setPost(initialValue);
|
||||||
|
} catch {
|
||||||
|
toast({
|
||||||
|
title: 'An error occurred',
|
||||||
|
description: "We couldn't save your post, try again!",
|
||||||
|
status: 'error',
|
||||||
|
duration: 2000,
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex p={'5px 0'} flexDirection="row" flexWrap="wrap" maxWidth={'85%'}>
|
||||||
|
<Box flex={'5 0 auto'} padding="0 5px 0 0">
|
||||||
|
<FormControl
|
||||||
|
flexDirection="column"
|
||||||
|
isInvalid={Boolean(post.name.length < 3 && post.name)}
|
||||||
|
>
|
||||||
|
<FormLabel htmlFor="name">Post name</FormLabel>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
placeholder="Enter post name"
|
||||||
|
value={post.name}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Button
|
||||||
|
mt={8}
|
||||||
|
colorScheme="purple"
|
||||||
|
isLoading={isLoading}
|
||||||
|
onClick={handleAddPost}
|
||||||
|
>
|
||||||
|
Add Post
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const PostList = () => {
|
||||||
|
const { data: posts, isLoading } = useGetPostsQuery();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <div>Loading</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!posts) {
|
||||||
|
return <div>No posts :(</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<List spacing={3}>
|
||||||
|
{posts.map(({ id, name }) => (
|
||||||
|
<ListItem key={id} onClick={() => history.push(`/posts/${id}`)}>
|
||||||
|
<ListIcon as={MdBook} color="green.500" /> {name}
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PostsCountStat = () => {
|
||||||
|
const { data: posts } = useGetPostsQuery();
|
||||||
|
|
||||||
|
if (!posts) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stat>
|
||||||
|
<StatLabel>Active Posts</StatLabel>
|
||||||
|
<StatNumber>{posts?.length}</StatNumber>
|
||||||
|
</Stat>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PostsManager = () => {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Flex bg="#011627" p={4} color="white">
|
||||||
|
<Box>
|
||||||
|
<Heading size="xl">Manage Posts</Heading>
|
||||||
|
</Box>
|
||||||
|
<Spacer />
|
||||||
|
<Box>
|
||||||
|
<PostsCountStat />
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
<Divider />
|
||||||
|
<AddPost />
|
||||||
|
<Divider />
|
||||||
|
<Flex wrap="wrap">
|
||||||
|
<Box flex={1} borderRight="1px solid #eee">
|
||||||
|
<Box p={4} borderBottom="1px solid #eee">
|
||||||
|
<Heading size="sm">Posts</Heading>
|
||||||
|
</Box>
|
||||||
|
<Box p={4}>
|
||||||
|
<PostList />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box flex={2}>
|
||||||
|
<Switch>
|
||||||
|
<Route path="/posts/:id" component={PostDetail} />
|
||||||
|
<Route>
|
||||||
|
<Center h="200px">
|
||||||
|
<Heading size="md">Select a post to edit!</Heading>
|
||||||
|
</Center>
|
||||||
|
</Route>
|
||||||
|
</Switch>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PostsManager;
|
|
@ -0,0 +1,17 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { Switch, Route } from 'react-router-dom';
|
||||||
|
import { PostsManager } from 'features/posts/PostsManager';
|
||||||
|
import { Box, Heading } from '@chakra-ui/react';
|
||||||
|
|
||||||
|
function PostsView() {
|
||||||
|
return (
|
||||||
|
<Box as="section" p="2">
|
||||||
|
<Heading as="h2">Posts Demo</Heading>
|
||||||
|
<Switch>
|
||||||
|
<Route path="/" component={PostsManager} />
|
||||||
|
</Switch>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PostsView;
|
41
packages/redux-devtools-rtk-query-monitor/demo/src/index.css
Normal file
41
packages/redux-devtools-rtk-query-monitor/demo/src/index.css
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||||
|
monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
section {
|
||||||
|
display: block;
|
||||||
|
max-width: 65%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pokemon-list {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pokemon {
|
||||||
|
padding: 0.2em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre code {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
article {
|
||||||
|
padding: 0 0 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link.link {
|
||||||
|
color: #805ad5;
|
||||||
|
}
|
28
packages/redux-devtools-rtk-query-monitor/demo/src/index.tsx
Normal file
28
packages/redux-devtools-rtk-query-monitor/demo/src/index.tsx
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import { ChakraProvider } from '@chakra-ui/react';
|
||||||
|
import './index.css';
|
||||||
|
import { store } from './store';
|
||||||
|
import DevTools from './features/DevTools/DevTools';
|
||||||
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
|
import { App } from 'App';
|
||||||
|
import { worker } from './mocks/browser';
|
||||||
|
|
||||||
|
function renderApp() {
|
||||||
|
const rootElement = document.getElementById('root');
|
||||||
|
|
||||||
|
ReactDOM.render(
|
||||||
|
<Provider store={store}>
|
||||||
|
<ChakraProvider>
|
||||||
|
<BrowserRouter>
|
||||||
|
<App />
|
||||||
|
<DevTools />
|
||||||
|
</BrowserRouter>
|
||||||
|
</ChakraProvider>
|
||||||
|
</Provider>,
|
||||||
|
rootElement
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
worker.start({ quiet: true }).then(renderApp, renderApp);
|
|
@ -0,0 +1,4 @@
|
||||||
|
import { setupWorker } from 'msw';
|
||||||
|
import { handlers } from './db';
|
||||||
|
|
||||||
|
export const worker = setupWorker(...handlers);
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { factory, primaryKey } from '@mswjs/data';
|
||||||
|
import { nanoid } from '@reduxjs/toolkit';
|
||||||
|
import { rest } from 'msw';
|
||||||
|
import { Post } from '../services/posts';
|
||||||
|
|
||||||
|
const db = factory({
|
||||||
|
post: {
|
||||||
|
id: primaryKey(String),
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
[
|
||||||
|
'A sample post',
|
||||||
|
'A post about RTK Query',
|
||||||
|
'How to randomly throw errors, a novella',
|
||||||
|
].forEach((name) => {
|
||||||
|
db.post.create({ id: nanoid(), name });
|
||||||
|
});
|
||||||
|
|
||||||
|
export const handlers = [
|
||||||
|
rest.post('/posts', async (req, res, ctx) => {
|
||||||
|
const { name } = req.body as Partial<Post>;
|
||||||
|
|
||||||
|
if (Math.random() < 0.3) {
|
||||||
|
return res(
|
||||||
|
ctx.json({ error: 'Oh no, there was an error, try again.' }),
|
||||||
|
ctx.status(500),
|
||||||
|
ctx.delay(300)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const post = db.post.create({
|
||||||
|
id: nanoid(),
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res(ctx.json(post), ctx.delay(300));
|
||||||
|
}),
|
||||||
|
rest.put('/posts/:id', (req, res, ctx) => {
|
||||||
|
const { name } = req.body as Partial<Post>;
|
||||||
|
|
||||||
|
if (Math.random() < 0.3) {
|
||||||
|
return res(
|
||||||
|
ctx.json({ error: 'Oh no, there was an error, try again.' }),
|
||||||
|
ctx.status(500),
|
||||||
|
ctx.delay(300)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const post = db.post.update({
|
||||||
|
where: { id: { equals: req.params.id } },
|
||||||
|
data: { name },
|
||||||
|
});
|
||||||
|
|
||||||
|
return res(ctx.json(post), ctx.delay(300));
|
||||||
|
}),
|
||||||
|
...db.post.toHandlers('rest'),
|
||||||
|
] as const;
|
|
@ -0,0 +1,155 @@
|
||||||
|
export const POKEMON_NAMES = [
|
||||||
|
'bulbasaur',
|
||||||
|
'ivysaur',
|
||||||
|
'venusaur',
|
||||||
|
'charmander',
|
||||||
|
'charmeleon',
|
||||||
|
'charizard',
|
||||||
|
'squirtle',
|
||||||
|
'wartortle',
|
||||||
|
'blastoise',
|
||||||
|
'caterpie',
|
||||||
|
'metapod',
|
||||||
|
'butterfree',
|
||||||
|
'weedle',
|
||||||
|
'kakuna',
|
||||||
|
'beedrill',
|
||||||
|
'pidgey',
|
||||||
|
'pidgeotto',
|
||||||
|
'pidgeot',
|
||||||
|
'rattata',
|
||||||
|
'raticate',
|
||||||
|
'spearow',
|
||||||
|
'fearow',
|
||||||
|
'ekans',
|
||||||
|
'arbok',
|
||||||
|
'pikachu',
|
||||||
|
'raichu',
|
||||||
|
'sandshrew',
|
||||||
|
'sandslash',
|
||||||
|
'nidoran',
|
||||||
|
'nidorina',
|
||||||
|
'nidoqueen',
|
||||||
|
'nidoran',
|
||||||
|
'nidorino',
|
||||||
|
'nidoking',
|
||||||
|
'clefairy',
|
||||||
|
'clefable',
|
||||||
|
'vulpix',
|
||||||
|
'ninetales',
|
||||||
|
'jigglypuff',
|
||||||
|
'wigglytuff',
|
||||||
|
'zubat',
|
||||||
|
'golbat',
|
||||||
|
'oddish',
|
||||||
|
'gloom',
|
||||||
|
'vileplume',
|
||||||
|
'paras',
|
||||||
|
'parasect',
|
||||||
|
'venonat',
|
||||||
|
'venomoth',
|
||||||
|
'diglett',
|
||||||
|
'dugtrio',
|
||||||
|
'meowth',
|
||||||
|
'persian',
|
||||||
|
'psyduck',
|
||||||
|
'golduck',
|
||||||
|
'mankey',
|
||||||
|
'primeape',
|
||||||
|
'growlithe',
|
||||||
|
'arcanine',
|
||||||
|
'poliwag',
|
||||||
|
'poliwhirl',
|
||||||
|
'poliwrath',
|
||||||
|
'abra',
|
||||||
|
'kadabra',
|
||||||
|
'alakazam',
|
||||||
|
'machop',
|
||||||
|
'machoke',
|
||||||
|
'machamp',
|
||||||
|
'bellsprout',
|
||||||
|
'weepinbell',
|
||||||
|
'victreebel',
|
||||||
|
'tentacool',
|
||||||
|
'tentacruel',
|
||||||
|
'geodude',
|
||||||
|
'graveler',
|
||||||
|
'golem',
|
||||||
|
'ponyta',
|
||||||
|
'rapidash',
|
||||||
|
'slowpoke',
|
||||||
|
'slowbro',
|
||||||
|
'magnemite',
|
||||||
|
'magneton',
|
||||||
|
"farfetch'd",
|
||||||
|
'doduo',
|
||||||
|
'dodrio',
|
||||||
|
'seel',
|
||||||
|
'dewgong',
|
||||||
|
'grimer',
|
||||||
|
'muk',
|
||||||
|
'shellder',
|
||||||
|
'cloyster',
|
||||||
|
'gastly',
|
||||||
|
'haunter',
|
||||||
|
'gengar',
|
||||||
|
'onix',
|
||||||
|
'drowzee',
|
||||||
|
'hypno',
|
||||||
|
'krabby',
|
||||||
|
'kingler',
|
||||||
|
'voltorb',
|
||||||
|
'electrode',
|
||||||
|
'exeggcute',
|
||||||
|
'exeggutor',
|
||||||
|
'cubone',
|
||||||
|
'marowak',
|
||||||
|
'hitmonlee',
|
||||||
|
'hitmonchan',
|
||||||
|
'lickitung',
|
||||||
|
'koffing',
|
||||||
|
'weezing',
|
||||||
|
'rhyhorn',
|
||||||
|
'rhydon',
|
||||||
|
'chansey',
|
||||||
|
'tangela',
|
||||||
|
'kangaskhan',
|
||||||
|
'horsea',
|
||||||
|
'seadra',
|
||||||
|
'goldeen',
|
||||||
|
'seaking',
|
||||||
|
'staryu',
|
||||||
|
'starmie',
|
||||||
|
'mr. mime',
|
||||||
|
'scyther',
|
||||||
|
'jynx',
|
||||||
|
'electabuzz',
|
||||||
|
'magmar',
|
||||||
|
'pinsir',
|
||||||
|
'tauros',
|
||||||
|
'magikarp',
|
||||||
|
'gyarados',
|
||||||
|
'lapras',
|
||||||
|
'ditto',
|
||||||
|
'eevee',
|
||||||
|
'vaporeon',
|
||||||
|
'jolteon',
|
||||||
|
'flareon',
|
||||||
|
'porygon',
|
||||||
|
'omanyte',
|
||||||
|
'omastar',
|
||||||
|
'kabuto',
|
||||||
|
'kabutops',
|
||||||
|
'aerodactyl',
|
||||||
|
'snorlax',
|
||||||
|
'articuno',
|
||||||
|
'zapdos',
|
||||||
|
'moltres',
|
||||||
|
'dratini',
|
||||||
|
'dragonair',
|
||||||
|
'dragonite',
|
||||||
|
'mewtwo',
|
||||||
|
'mew',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type PokemonName = typeof POKEMON_NAMES[number];
|
5
packages/redux-devtools-rtk-query-monitor/demo/src/react-app-env.d.ts
vendored
Normal file
5
packages/redux-devtools-rtk-query-monitor/demo/src/react-app-env.d.ts
vendored
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
/// <reference types="react-scripts" />
|
||||||
|
|
||||||
|
declare module '@redux-devtools/app';
|
||||||
|
|
||||||
|
declare module 'remote-redux-devtools';
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
|
||||||
|
import type { PokemonName } from '../pokemon.data';
|
||||||
|
|
||||||
|
export const pokemonApi = createApi({
|
||||||
|
reducerPath: 'pokemonApi',
|
||||||
|
baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }),
|
||||||
|
tagTypes: ['pokemon'],
|
||||||
|
endpoints: (builder) => ({
|
||||||
|
getPokemonByName: builder.query({
|
||||||
|
query: (name: PokemonName) => `pokemon/${name}`,
|
||||||
|
providesTags: (result, error, name: PokemonName) => [
|
||||||
|
{ type: 'pokemon' },
|
||||||
|
{ type: 'pokemon', id: name },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Export hooks for usage in functional components
|
||||||
|
export const { useGetPokemonByNameQuery } = pokemonApi;
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
|
||||||
|
|
||||||
|
export interface Post {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type PostsResponse = Post[];
|
||||||
|
|
||||||
|
export const postsApi = createApi({
|
||||||
|
reducerPath: 'postsApi',
|
||||||
|
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
|
||||||
|
tagTypes: ['Post'],
|
||||||
|
endpoints: (build) => ({
|
||||||
|
getPosts: build.query<PostsResponse, void>({
|
||||||
|
query: () => 'posts',
|
||||||
|
providesTags: (result) =>
|
||||||
|
result
|
||||||
|
? [
|
||||||
|
...result.map(({ id }) => ({ type: 'Post' as const, id })),
|
||||||
|
{ type: 'Post', id: 'LIST' },
|
||||||
|
]
|
||||||
|
: [{ type: 'Post', id: 'LIST' }],
|
||||||
|
}),
|
||||||
|
addPost: build.mutation<Post, Partial<Post>>({
|
||||||
|
query: (body) => ({
|
||||||
|
url: `posts`,
|
||||||
|
method: 'POST',
|
||||||
|
body,
|
||||||
|
}),
|
||||||
|
invalidatesTags: [{ type: 'Post', id: 'LIST' }],
|
||||||
|
}),
|
||||||
|
getPost: build.query<Post, string>({
|
||||||
|
query: (id) => `posts/${id}`,
|
||||||
|
providesTags: (result, error, id) => [{ type: 'Post', id }],
|
||||||
|
}),
|
||||||
|
updatePost: build.mutation<void, Pick<Post, 'id'> & Partial<Post>>({
|
||||||
|
query: ({ id, ...patch }) => ({
|
||||||
|
url: `posts/${id}`,
|
||||||
|
method: 'PUT',
|
||||||
|
body: patch,
|
||||||
|
}),
|
||||||
|
invalidatesTags: (result, error, { id }) => [{ type: 'Post', id }],
|
||||||
|
}),
|
||||||
|
deletePost: build.mutation<{ success: boolean; id: number }, number>({
|
||||||
|
query(id) {
|
||||||
|
return {
|
||||||
|
url: `posts/${id}`,
|
||||||
|
method: 'DELETE',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
invalidatesTags: (result, error, id) => [{ type: 'Post', id }],
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const {
|
||||||
|
useGetPostQuery,
|
||||||
|
useGetPostsQuery,
|
||||||
|
useAddPostMutation,
|
||||||
|
useUpdatePostMutation,
|
||||||
|
useDeletePostMutation,
|
||||||
|
} = postsApi;
|
29
packages/redux-devtools-rtk-query-monitor/demo/src/store.ts
Normal file
29
packages/redux-devtools-rtk-query-monitor/demo/src/store.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import {
|
||||||
|
configureStore,
|
||||||
|
Middleware,
|
||||||
|
combineReducers,
|
||||||
|
EnhancedStore,
|
||||||
|
} from '@reduxjs/toolkit';
|
||||||
|
import { pokemonApi } from './services/pokemon';
|
||||||
|
import { postsApi } from 'services/posts';
|
||||||
|
import DevTools from './features/DevTools/DevTools';
|
||||||
|
import { isExtensionEnabled } from 'features/DevTools/helpers';
|
||||||
|
|
||||||
|
const devTools = isExtensionEnabled();
|
||||||
|
|
||||||
|
const reducer = combineReducers({
|
||||||
|
[pokemonApi.reducerPath]: pokemonApi.reducer,
|
||||||
|
[postsApi.reducerPath]: postsApi.reducer,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const store: EnhancedStore<ReturnType<typeof reducer>> = configureStore({
|
||||||
|
reducer,
|
||||||
|
devTools,
|
||||||
|
// adding the api middleware enables caching, invalidation, polling and other features of `rtk-query`
|
||||||
|
middleware: (getDefaultMiddleware) =>
|
||||||
|
getDefaultMiddleware().concat([
|
||||||
|
pokemonApi.middleware,
|
||||||
|
postsApi.middleware,
|
||||||
|
]) as Middleware[],
|
||||||
|
enhancers: (devTools ? [] : [DevTools.instrument()]) as any,
|
||||||
|
});
|
11
packages/redux-devtools-rtk-query-monitor/demo/tsconfig.json
Normal file
11
packages/redux-devtools-rtk-query-monitor/demo/tsconfig.json
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"extends": "../../../tsconfig.react.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"baseUrl": "./src",
|
||||||
|
"target": "esNext",
|
||||||
|
"module": "es6",
|
||||||
|
"moduleResolution": "node"
|
||||||
|
},
|
||||||
|
"include": ["../src", "src"]
|
||||||
|
}
|
17887
packages/redux-devtools-rtk-query-monitor/demo/yarn.lock
Normal file
17887
packages/redux-devtools-rtk-query-monitor/demo/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
BIN
packages/redux-devtools-rtk-query-monitor/monitor-demo.gif
Normal file
BIN
packages/redux-devtools-rtk-query-monitor/monitor-demo.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.4 MiB |
88
packages/redux-devtools-rtk-query-monitor/package.json
Normal file
88
packages/redux-devtools-rtk-query-monitor/package.json
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
{
|
||||||
|
"name": "@redux-devtools/rtk-query-monitor",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "rtk-query monitor for Redux DevTools",
|
||||||
|
"keywords": [
|
||||||
|
"redux",
|
||||||
|
"devtools",
|
||||||
|
"flux",
|
||||||
|
"react",
|
||||||
|
"redux-toolkit",
|
||||||
|
"rtk-query"
|
||||||
|
],
|
||||||
|
"homepage": "https://github.com/FaberVitale/redux-devtools/tree/feat/rtk-query-monitor/packages/redux-devtools-rtk-query-monitor",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/reduxjs/redux-devtools/issues"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"author": {
|
||||||
|
"name": "FaberVitale",
|
||||||
|
"url": "https://github.com/FaberVitale"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"lib",
|
||||||
|
"src"
|
||||||
|
],
|
||||||
|
"main": "lib/index.js",
|
||||||
|
"types": "lib/index.d.ts",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/reduxjs/redux-devtools.git"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"start": "webpack-dev-server --config demo/config/webpack.config.ts",
|
||||||
|
"build": "npm run build:types && npm run build:js",
|
||||||
|
"stats": "webpack --profile --json > stats.json",
|
||||||
|
"build:demo": "cross-env NODE_ENV=production webpack -p --config demo/config/webpack.config.ts",
|
||||||
|
"build:types": "tsc --emitDeclarationOnly",
|
||||||
|
"build:js": "babel src --out-dir lib --extensions \".ts,.tsx\" --source-maps inline",
|
||||||
|
"clean": "rimraf lib",
|
||||||
|
"lint": "eslint . --ext .ts,.tsx",
|
||||||
|
"lint:fix": "eslint . --ext .ts,.tsx --fix",
|
||||||
|
"type-check": "tsc --noEmit",
|
||||||
|
"type-check:watch": "npm run type-check -- --watch",
|
||||||
|
"preversion": "npm run type-check && npm run lint",
|
||||||
|
"prepublishOnly": "npm run clean && npm run build"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@redux-devtools/dock-monitor": "^1.4.0",
|
||||||
|
"@types/prop-types": "^15.7.4",
|
||||||
|
"@types/redux-devtools-themes": "^1.0.0",
|
||||||
|
"devui": "^1.0.0-9",
|
||||||
|
"lodash.debounce": "^4.0.8",
|
||||||
|
"prop-types": "^15.7.2",
|
||||||
|
"react-json-tree": "^0.15.0",
|
||||||
|
"redux-devtools-themes": "^1.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@chakra-ui/react": "^1.6.5",
|
||||||
|
"@emotion/react": "^11",
|
||||||
|
"@emotion/styled": "^11",
|
||||||
|
"@mswjs/data": "^0.3.0",
|
||||||
|
"@redux-devtools/core": "^3.9.0",
|
||||||
|
"@redux-devtools/dock-monitor": "^1.4.0",
|
||||||
|
"@reduxjs/toolkit": "^1.6.0",
|
||||||
|
"@types/react": "^17.0.2",
|
||||||
|
"@types/react-dom": "17.0.0",
|
||||||
|
"@types/react-redux": "7.1.9",
|
||||||
|
"@types/react-router-dom": "5.1.6",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"framer-motion": "^4",
|
||||||
|
"msw": "0.28.2",
|
||||||
|
"react": "^16.14.0",
|
||||||
|
"react-dom": "^16.14.0",
|
||||||
|
"react-redux": "^7.2.1",
|
||||||
|
"react-router-dom": "^5.2.0",
|
||||||
|
"redux": "^4.0.5"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@redux-devtools/core": "^3.7.0",
|
||||||
|
"@reduxjs/toolkit": "^1.6.0",
|
||||||
|
"@types/react": "^16.3.0 || ^17.0.0",
|
||||||
|
"react": "^16.3.0 || ^17.0.0",
|
||||||
|
"redux": "^3.4.0 || ^4.0.0"
|
||||||
|
},
|
||||||
|
"msw": {
|
||||||
|
"workerDirectory": "demo/public"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
import React, { HTMLAttributes } from 'react';
|
||||||
|
|
||||||
|
export type ArrowUpIconProps = Omit<
|
||||||
|
HTMLAttributes<SVGElement>,
|
||||||
|
'xmlns' | 'children' | 'viewBox'
|
||||||
|
>;
|
||||||
|
|
||||||
|
/* eslint-disable max-len */
|
||||||
|
/**
|
||||||
|
* @see https://icons.getbootstrap.com/icons/arrow-up/
|
||||||
|
*/
|
||||||
|
export function ArrowUpIcon(props: ArrowUpIconProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
fill="currentColor"
|
||||||
|
{...props}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M8 15a.5.5 0 0 0 .5-.5V2.707l3.146 3.147a.5.5 0 0 0 .708-.708l-4-4a.5.5 0 0 0-.708 0l-4 4a.5.5 0 1 0 .708.708L7.5 2.707V14.5a.5.5 0 0 0 .5.5z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
/* eslint-enable max-len */
|
|
@ -0,0 +1,24 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { StyleUtilsContext } from '../styles/createStylingFromTheme';
|
||||||
|
|
||||||
|
export function NoRtkQueryApi(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<StyleUtilsContext.Consumer>
|
||||||
|
{({ styling }) => (
|
||||||
|
<div {...styling('noApiFound')}>
|
||||||
|
No rtk-query api found.
|
||||||
|
<br />
|
||||||
|
Make sure to follow{' '}
|
||||||
|
<a
|
||||||
|
href="https://redux-toolkit.js.org/rtk-query/overview#basic-usage"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
>
|
||||||
|
the instructions
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</StyleUtilsContext.Consumer>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,205 @@
|
||||||
|
import React, { ReactNode, FormEvent, MouseEvent, ChangeEvent } from 'react';
|
||||||
|
import { QueryFormValues } from '../types';
|
||||||
|
import { StyleUtilsContext } from '../styles/createStylingFromTheme';
|
||||||
|
import { Select } from 'devui';
|
||||||
|
import { SelectOption } from '../types';
|
||||||
|
import debounce from 'lodash.debounce';
|
||||||
|
import { sortQueryOptions, QueryComparators } from '../utils/comparators';
|
||||||
|
import { QueryFilters, filterQueryOptions } from '../utils/filters';
|
||||||
|
import { SortOrderButton } from './SortOrderButton';
|
||||||
|
import { RegexIcon } from './RegexIcon';
|
||||||
|
|
||||||
|
export interface QueryFormProps {
|
||||||
|
values: QueryFormValues;
|
||||||
|
searchQueryRegex: RegExp | null;
|
||||||
|
onFormValuesChange: (values: Partial<QueryFormValues>) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QueryFormState {
|
||||||
|
searchValue: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectId = 'rtk-query-comp-select';
|
||||||
|
const searchId = 'rtk-query-search-query';
|
||||||
|
const filterSelectId = 'rtk-query-search-query-select';
|
||||||
|
const searchPlaceholder = 'filter query by...';
|
||||||
|
|
||||||
|
const labels = {
|
||||||
|
regexToggle: {
|
||||||
|
info: 'Use regular expression search',
|
||||||
|
error: 'Invalid regular expression provided',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export class QueryForm extends React.PureComponent<
|
||||||
|
QueryFormProps,
|
||||||
|
QueryFormState
|
||||||
|
> {
|
||||||
|
constructor(props: QueryFormProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
searchValue: props.values.searchValue,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
inputSearchRef = React.createRef<HTMLInputElement>();
|
||||||
|
|
||||||
|
handleSubmit = (evt: FormEvent<HTMLFormElement>): void => {
|
||||||
|
evt.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
handleButtonGroupClick = (isAsc: boolean): void => {
|
||||||
|
this.props.onFormValuesChange({ isAscendingQueryComparatorOrder: isAsc });
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSelectComparatorChange = (
|
||||||
|
option: SelectOption<QueryComparators> | undefined | null
|
||||||
|
): void => {
|
||||||
|
if (typeof option?.value === 'string') {
|
||||||
|
this.props.onFormValuesChange({ queryComparator: option.value });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSelectFilterChange = (
|
||||||
|
option: SelectOption<QueryFilters> | undefined | null
|
||||||
|
): void => {
|
||||||
|
if (typeof option?.value === 'string') {
|
||||||
|
this.props.onFormValuesChange({ queryFilter: option.value });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleRegexSearchClick = (): void => {
|
||||||
|
this.props.onFormValuesChange({
|
||||||
|
isRegexSearch: !this.props.values.isRegexSearch,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
restoreCaretPosition = (start: number | null, end: number | null): void => {
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
if (this.inputSearchRef.current) {
|
||||||
|
this.inputSearchRef.current.selectionStart = start;
|
||||||
|
this.inputSearchRef.current.selectionEnd = end;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
invalidateSearchValueFromProps = debounce(() => {
|
||||||
|
this.props.onFormValuesChange({
|
||||||
|
searchValue: this.state.searchValue,
|
||||||
|
});
|
||||||
|
}, 150);
|
||||||
|
|
||||||
|
handleSearchChange = (evt: ChangeEvent<HTMLInputElement>): void => {
|
||||||
|
const searchValue = evt.target.value.trim();
|
||||||
|
this.setState({ searchValue });
|
||||||
|
this.invalidateSearchValueFromProps();
|
||||||
|
};
|
||||||
|
|
||||||
|
handleClearSearchClick = (evt: MouseEvent<HTMLButtonElement>): void => {
|
||||||
|
evt.preventDefault();
|
||||||
|
|
||||||
|
if (this.state.searchValue) {
|
||||||
|
this.setState({ searchValue: '' });
|
||||||
|
this.invalidateSearchValueFromProps();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render(): ReactNode {
|
||||||
|
const {
|
||||||
|
searchQueryRegex,
|
||||||
|
values: {
|
||||||
|
isAscendingQueryComparatorOrder: isAsc,
|
||||||
|
queryComparator,
|
||||||
|
searchValue,
|
||||||
|
queryFilter,
|
||||||
|
isRegexSearch,
|
||||||
|
},
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const isRegexInvalid =
|
||||||
|
isRegexSearch && searchValue.length > 0 && searchQueryRegex == null;
|
||||||
|
const regexToggleType = isRegexInvalid ? 'error' : 'info';
|
||||||
|
const regexToggleLabel = labels.regexToggle[regexToggleType];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyleUtilsContext.Consumer>
|
||||||
|
{({ styling, base16Theme }) => {
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
action="#"
|
||||||
|
onSubmit={this.handleSubmit}
|
||||||
|
{...styling('queryForm')}
|
||||||
|
>
|
||||||
|
<div {...styling('queryListHeader')}>
|
||||||
|
<label htmlFor={searchId} {...styling('srOnly')}>
|
||||||
|
filter query
|
||||||
|
</label>
|
||||||
|
<div {...styling('querySearch')}>
|
||||||
|
<input
|
||||||
|
ref={this.inputSearchRef}
|
||||||
|
type="search"
|
||||||
|
value={this.state.searchValue}
|
||||||
|
onChange={this.handleSearchChange}
|
||||||
|
placeholder={searchPlaceholder}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="reset"
|
||||||
|
aria-label="clear search"
|
||||||
|
data-invisible={
|
||||||
|
+(this.state.searchValue.length === 0) || undefined
|
||||||
|
}
|
||||||
|
onClick={this.handleClearSearchClick}
|
||||||
|
{...styling('closeButton')}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label={regexToggleLabel}
|
||||||
|
title={regexToggleLabel}
|
||||||
|
data-type={regexToggleType}
|
||||||
|
aria-pressed={isRegexSearch}
|
||||||
|
onClick={this.handleRegexSearchClick}
|
||||||
|
{...styling('toggleButton')}
|
||||||
|
>
|
||||||
|
<RegexIcon />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<label htmlFor={selectId} {...styling('srOnly')}>
|
||||||
|
filter by
|
||||||
|
</label>
|
||||||
|
<Select<SelectOption<QueryFilters>>
|
||||||
|
id={filterSelectId}
|
||||||
|
isSearchable={false}
|
||||||
|
options={filterQueryOptions}
|
||||||
|
theme={base16Theme as any}
|
||||||
|
value={filterQueryOptions.find(
|
||||||
|
(opt) => opt?.value === queryFilter
|
||||||
|
)}
|
||||||
|
onChange={this.handleSelectFilterChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div {...styling('sortBySection')}>
|
||||||
|
<label htmlFor={selectId}>Sort by</label>
|
||||||
|
<Select<SelectOption<QueryComparators>>
|
||||||
|
id={selectId}
|
||||||
|
isSearchable={false}
|
||||||
|
theme={base16Theme as any}
|
||||||
|
value={sortQueryOptions.find(
|
||||||
|
(opt) => opt?.value === queryComparator
|
||||||
|
)}
|
||||||
|
options={sortQueryOptions}
|
||||||
|
onChange={this.handleSelectComparatorChange}
|
||||||
|
/>
|
||||||
|
<SortOrderButton
|
||||||
|
id={'rtk-query-sort-order-button'}
|
||||||
|
isAsc={isAsc}
|
||||||
|
onChange={this.handleButtonGroupClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</StyleUtilsContext.Consumer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
import React, { PureComponent, ReactNode } from 'react';
|
||||||
|
import { StyleUtilsContext } from '../styles/createStylingFromTheme';
|
||||||
|
import { RtkResourceInfo, RtkQueryMonitorState } from '../types';
|
||||||
|
import { isQuerySelected } from '../utils/rtk-query';
|
||||||
|
|
||||||
|
export interface QueryListProps {
|
||||||
|
resInfos: RtkResourceInfo[];
|
||||||
|
selectedQueryKey: RtkQueryMonitorState['selectedQueryKey'];
|
||||||
|
onSelectQuery: (query: RtkResourceInfo) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class QueryList extends PureComponent<QueryListProps> {
|
||||||
|
static isItemSelected(
|
||||||
|
selectedQueryKey: QueryListProps['selectedQueryKey'],
|
||||||
|
queryInfo: RtkResourceInfo
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
!!selectedQueryKey &&
|
||||||
|
selectedQueryKey.queryKey === queryInfo.queryKey &&
|
||||||
|
selectedQueryKey.reducerPath === queryInfo.reducerPath
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static formatQuery(resInfo: RtkResourceInfo): string {
|
||||||
|
const key =
|
||||||
|
resInfo.type === 'query'
|
||||||
|
? resInfo.queryKey
|
||||||
|
: `${resInfo.state.endpointName ?? ''} ${resInfo.queryKey}`;
|
||||||
|
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): ReactNode {
|
||||||
|
const { resInfos, selectedQueryKey, onSelectQuery } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyleUtilsContext.Consumer>
|
||||||
|
{({ styling }) => (
|
||||||
|
<ul {...styling('queryList')}>
|
||||||
|
{resInfos.map((resInfo) => {
|
||||||
|
const isSelected = isQuerySelected(selectedQueryKey, resInfo);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={resInfo.queryKey}
|
||||||
|
onClick={() => onSelectQuery(resInfo)}
|
||||||
|
{...styling(
|
||||||
|
['queryListItem', isSelected && 'queryListItemSelected'],
|
||||||
|
isSelected
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<p {...styling('queryListItemKey')}>
|
||||||
|
{QueryList.formatQuery(resInfo)}
|
||||||
|
</p>
|
||||||
|
<div {...styling('queryStatusWrapper')}>
|
||||||
|
<strong {...styling(['queryStatus', 'queryType'])}>
|
||||||
|
{resInfo.type === 'query' ? 'Q' : 'M'}
|
||||||
|
</strong>
|
||||||
|
<p {...styling('queryStatus')}>{resInfo.state.status}</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</StyleUtilsContext.Consumer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import React, { ReactNode, PureComponent } from 'react';
|
||||||
|
import { Action, AnyAction } from 'redux';
|
||||||
|
import { emptyRecord, identity } from '../utils/object';
|
||||||
|
import { TreeView } from './TreeView';
|
||||||
|
|
||||||
|
export interface QueryPreviewActionsProps {
|
||||||
|
isWideLayout: boolean;
|
||||||
|
actionsOfQuery: AnyAction[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const keySep = ' - ';
|
||||||
|
|
||||||
|
export class QueryPreviewActions extends PureComponent<QueryPreviewActionsProps> {
|
||||||
|
selectFormattedActions = createSelector<
|
||||||
|
AnyAction[],
|
||||||
|
AnyAction[],
|
||||||
|
Record<string, AnyAction>
|
||||||
|
>(identity, (actions) => {
|
||||||
|
const output: Record<string, AnyAction> = {};
|
||||||
|
|
||||||
|
if (actions.length === 0) {
|
||||||
|
return emptyRecord;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0, len = actions.length; i < len; i++) {
|
||||||
|
const action = actions[i];
|
||||||
|
const key = `${i}${keySep}${(action as Action<string>)?.type ?? ''}`;
|
||||||
|
output[key] = action;
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
});
|
||||||
|
|
||||||
|
isLastActionNode = (keyPath: (string | number)[], layer: number): boolean => {
|
||||||
|
if (layer >= 1) {
|
||||||
|
const len = this.props.actionsOfQuery.length;
|
||||||
|
const actionKey = keyPath[keyPath.length - 1];
|
||||||
|
|
||||||
|
if (typeof actionKey === 'string') {
|
||||||
|
const index = Number(actionKey.split(keySep)[0]);
|
||||||
|
|
||||||
|
return len > 0 && len - index < 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
shouldExpandNode = (
|
||||||
|
keyPath: (string | number)[],
|
||||||
|
value: unknown,
|
||||||
|
layer: number
|
||||||
|
): boolean => {
|
||||||
|
if (layer === 1) {
|
||||||
|
return this.isLastActionNode(keyPath, layer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layer === 2) {
|
||||||
|
return (
|
||||||
|
this.isLastActionNode(keyPath, layer) &&
|
||||||
|
(keyPath[0] === 'meta' || keyPath[0] === 'error')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return layer <= 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
render(): ReactNode {
|
||||||
|
const { isWideLayout, actionsOfQuery } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TreeView
|
||||||
|
data={this.selectFormattedActions(actionsOfQuery)}
|
||||||
|
isWideLayout={isWideLayout}
|
||||||
|
shouldExpandNode={this.shouldExpandNode}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
import React, { ReactNode, PureComponent } from 'react';
|
||||||
|
import { ApiStats, RtkQueryApiState } from '../types';
|
||||||
|
import { StyleUtilsContext } from '../styles/createStylingFromTheme';
|
||||||
|
import { TreeView } from './TreeView';
|
||||||
|
|
||||||
|
export interface QueryPreviewApiProps {
|
||||||
|
apiStats: ApiStats | null;
|
||||||
|
apiState: RtkQueryApiState | null;
|
||||||
|
isWideLayout: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class QueryPreviewApi extends PureComponent<QueryPreviewApiProps> {
|
||||||
|
shouldExpandApiStateNode = (
|
||||||
|
keyPath: (string | number)[],
|
||||||
|
value: unknown,
|
||||||
|
layer: number
|
||||||
|
): boolean => {
|
||||||
|
const lastKey = keyPath[keyPath.length - 1];
|
||||||
|
|
||||||
|
return layer <= 1 && lastKey !== 'config';
|
||||||
|
};
|
||||||
|
|
||||||
|
render(): ReactNode {
|
||||||
|
const { apiStats, isWideLayout, apiState } = this.props;
|
||||||
|
|
||||||
|
if (!apiState) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasMutations = Object.keys(apiState.mutations).length > 0;
|
||||||
|
const hasQueries = Object.keys(apiState.queries).length > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyleUtilsContext.Consumer>
|
||||||
|
{({ styling }) => (
|
||||||
|
<article {...styling('tabContent')}>
|
||||||
|
<h2>{apiState.config.reducerPath}</h2>
|
||||||
|
<TreeView
|
||||||
|
before={<h3>State</h3>}
|
||||||
|
data={apiState}
|
||||||
|
shouldExpandNode={this.shouldExpandApiStateNode}
|
||||||
|
isWideLayout={isWideLayout}
|
||||||
|
/>
|
||||||
|
{apiStats && (
|
||||||
|
<>
|
||||||
|
<TreeView
|
||||||
|
before={<h3>Tally</h3>}
|
||||||
|
data={apiStats.tally}
|
||||||
|
isWideLayout={isWideLayout}
|
||||||
|
/>
|
||||||
|
{hasQueries && (
|
||||||
|
<TreeView
|
||||||
|
before={<h3>Queries Timings</h3>}
|
||||||
|
data={apiStats.timings.queries}
|
||||||
|
isWideLayout={isWideLayout}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{hasMutations && (
|
||||||
|
<TreeView
|
||||||
|
before={<h3>Mutations Timings</h3>}
|
||||||
|
data={apiStats.timings.mutations}
|
||||||
|
isWideLayout={isWideLayout}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</article>
|
||||||
|
)}
|
||||||
|
</StyleUtilsContext.Consumer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import { StyleUtilsContext } from '../styles/createStylingFromTheme';
|
||||||
|
import { QueryPreviewTabs, TabOption } from '../types';
|
||||||
|
import { emptyArray } from '../utils/object';
|
||||||
|
|
||||||
|
export interface QueryPreviewHeaderProps {
|
||||||
|
tabs: ReadonlyArray<
|
||||||
|
TabOption<QueryPreviewTabs, unknown, 'query' | 'mutation'>
|
||||||
|
>;
|
||||||
|
onTabChange: (tab: QueryPreviewTabs) => void;
|
||||||
|
selectedTab: QueryPreviewTabs;
|
||||||
|
renderTabLabel?: (tab: QueryPreviewHeaderProps['tabs'][number]) => ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class QueryPreviewHeader extends React.Component<QueryPreviewHeaderProps> {
|
||||||
|
handleTabClick = (tab: QueryPreviewHeaderProps['tabs'][number]): void => {
|
||||||
|
if (this.props.selectedTab !== tab.value) {
|
||||||
|
this.props.onTabChange(tab.value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render(): ReactNode {
|
||||||
|
const { tabs, selectedTab, renderTabLabel } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyleUtilsContext.Consumer>
|
||||||
|
{({ styling }) => (
|
||||||
|
<div {...styling('previewHeader')}>
|
||||||
|
<div {...styling('tabSelector')}>
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<div
|
||||||
|
onClick={() => this.handleTabClick(tab)}
|
||||||
|
key={tab.value}
|
||||||
|
{...styling(
|
||||||
|
[
|
||||||
|
'selectorButton',
|
||||||
|
tab.value === selectedTab && 'selectorButtonSelected',
|
||||||
|
],
|
||||||
|
tab.value === selectedTab
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{renderTabLabel ? renderTabLabel(tab) : tab.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</StyleUtilsContext.Consumer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
tabs: emptyArray,
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
import { createSelector, Selector } from '@reduxjs/toolkit';
|
||||||
|
import { QueryStatus } from '@reduxjs/toolkit/dist/query';
|
||||||
|
import React, { ReactNode, PureComponent } from 'react';
|
||||||
|
import { RtkResourceInfo, RTKStatusFlags } from '../types';
|
||||||
|
import { formatMs } from '../utils/formatters';
|
||||||
|
import { identity } from '../utils/object';
|
||||||
|
import { getQueryStatusFlags } from '../utils/rtk-query';
|
||||||
|
import { TreeView } from './TreeView';
|
||||||
|
|
||||||
|
type QueryTimings = {
|
||||||
|
startedAt: string;
|
||||||
|
loadedAt: string;
|
||||||
|
duration: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type FormattedQuery = {
|
||||||
|
key: string;
|
||||||
|
reducerPath: string;
|
||||||
|
timings: QueryTimings;
|
||||||
|
statusFlags: RTKStatusFlags;
|
||||||
|
} & (
|
||||||
|
| { mutation: RtkResourceInfo['state'] }
|
||||||
|
| { query: RtkResourceInfo['state'] }
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface QueryPreviewInfoProps {
|
||||||
|
resInfo: RtkResourceInfo;
|
||||||
|
isWideLayout: boolean;
|
||||||
|
}
|
||||||
|
export class QueryPreviewInfo extends PureComponent<QueryPreviewInfoProps> {
|
||||||
|
shouldExpandNode = (
|
||||||
|
keyPath: (string | number)[],
|
||||||
|
value: unknown,
|
||||||
|
layer: number
|
||||||
|
): boolean => {
|
||||||
|
const lastKey = keyPath[keyPath.length - 1];
|
||||||
|
|
||||||
|
return layer <= 1 && lastKey !== 'query' && lastKey !== 'mutation';
|
||||||
|
};
|
||||||
|
|
||||||
|
selectFormattedQuery: Selector<RtkResourceInfo, FormattedQuery> =
|
||||||
|
createSelector(identity, (resInfo: RtkResourceInfo): FormattedQuery => {
|
||||||
|
const { state, queryKey, reducerPath } = resInfo;
|
||||||
|
|
||||||
|
const startedAt = state.startedTimeStamp
|
||||||
|
? new Date(state.startedTimeStamp).toISOString()
|
||||||
|
: '-';
|
||||||
|
|
||||||
|
const loadedAt = state.fulfilledTimeStamp
|
||||||
|
? new Date(state.fulfilledTimeStamp).toISOString()
|
||||||
|
: '-';
|
||||||
|
|
||||||
|
const statusFlags = getQueryStatusFlags(state);
|
||||||
|
|
||||||
|
const timings = {
|
||||||
|
startedAt,
|
||||||
|
loadedAt,
|
||||||
|
duration: '-',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
state.fulfilledTimeStamp &&
|
||||||
|
state.startedTimeStamp &&
|
||||||
|
state.status !== QueryStatus.pending &&
|
||||||
|
state.startedTimeStamp <= state.fulfilledTimeStamp
|
||||||
|
) {
|
||||||
|
timings.duration = formatMs(
|
||||||
|
state.fulfilledTimeStamp - state.startedTimeStamp
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resInfo.type === 'query') {
|
||||||
|
return {
|
||||||
|
key: queryKey,
|
||||||
|
reducerPath,
|
||||||
|
query: resInfo.state,
|
||||||
|
statusFlags,
|
||||||
|
timings,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: queryKey,
|
||||||
|
reducerPath,
|
||||||
|
mutation: resInfo.state,
|
||||||
|
statusFlags,
|
||||||
|
timings,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
render(): ReactNode {
|
||||||
|
const { resInfo, isWideLayout } = this.props;
|
||||||
|
const formattedQuery = this.selectFormattedQuery(resInfo);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TreeView
|
||||||
|
data={formattedQuery}
|
||||||
|
isWideLayout={isWideLayout}
|
||||||
|
shouldExpandNode={this.shouldExpandNode}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
import React, { ReactNode, PureComponent } from 'react';
|
||||||
|
import { RtkQueryApiState } from '../types';
|
||||||
|
import { TreeView } from './TreeView';
|
||||||
|
|
||||||
|
export interface QueryPreviewSubscriptionsProps {
|
||||||
|
subscriptions: RtkQueryApiState['subscriptions'][keyof RtkQueryApiState['subscriptions']];
|
||||||
|
isWideLayout: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class QueryPreviewSubscriptions extends PureComponent<QueryPreviewSubscriptionsProps> {
|
||||||
|
render(): ReactNode {
|
||||||
|
const { subscriptions } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TreeView data={subscriptions} isWideLayout={this.props.isWideLayout} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
import React, { ReactNode, PureComponent } from 'react';
|
||||||
|
import { RtkQueryTag } from '../types';
|
||||||
|
import { TreeView } from './TreeView';
|
||||||
|
|
||||||
|
interface QueryPreviewTagsState {
|
||||||
|
data: { tags: RtkQueryTag[] };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QueryPreviewTagsProps {
|
||||||
|
tags: RtkQueryTag[];
|
||||||
|
isWideLayout: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class QueryPreviewTags extends PureComponent<
|
||||||
|
QueryPreviewTagsProps,
|
||||||
|
QueryPreviewTagsState
|
||||||
|
> {
|
||||||
|
constructor(props: QueryPreviewTagsProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
data: { tags: props.tags },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): ReactNode {
|
||||||
|
const { isWideLayout, tags } = this.props;
|
||||||
|
|
||||||
|
return <TreeView data={tags} isWideLayout={isWideLayout} />;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
export type RegexIconProps = Omit<
|
||||||
|
React.HTMLAttributes<SVGElement>,
|
||||||
|
'viewBox' | 'children'
|
||||||
|
>;
|
||||||
|
|
||||||
|
// `OOjs_UI_icon_regular-expression.svg` (MIT License)
|
||||||
|
// from https://commons.wikimedia.org/wiki/File:OOjs_UI_icon_regular-expression.svg
|
||||||
|
export function RegexIcon(
|
||||||
|
props: React.HTMLAttributes<SVGElement>
|
||||||
|
): JSX.Element {
|
||||||
|
return (
|
||||||
|
<svg fill="currentColor" {...props} viewBox="0 0 24 24">
|
||||||
|
<g>
|
||||||
|
<path d="M3 12.045c0-.99.15-1.915.45-2.777A6.886 6.886 0 0 1 4.764 7H6.23a7.923 7.923 0 0 0-1.25 2.374 8.563 8.563 0 0 0 .007 5.314c.29.85.7 1.622 1.23 2.312h-1.45a6.53 6.53 0 0 1-1.314-2.223 8.126 8.126 0 0 1-.45-2.732" />
|
||||||
|
<path id="dot" d="M10 16a1 1 0 1 1-2 0 1 1 0 0 1 2 0z" />
|
||||||
|
<path d="M14.25 7.013l-.24 2.156 2.187-.61.193 1.47-1.992.14 1.307 1.74-1.33.71-.914-1.833-.8 1.822-1.38-.698 1.296-1.74-1.98-.152.23-1.464 2.14.61-.24-2.158h1.534" />
|
||||||
|
<path d="M21 12.045c0 .982-.152 1.896-.457 2.744A6.51 6.51 0 0 1 19.236 17h-1.453a8.017 8.017 0 0 0 1.225-2.31c.29-.855.434-1.74.434-2.66 0-.91-.14-1.797-.422-2.66a7.913 7.913 0 0 0-1.248-2.374h1.465a6.764 6.764 0 0 1 1.313 2.28c.3.86.45 1.782.45 2.764" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
import React, { CSSProperties } from 'react';
|
||||||
|
import { ArrowUpIcon } from './ArrowUpIcon';
|
||||||
|
import { StyleUtilsContext } from '../styles/createStylingFromTheme';
|
||||||
|
|
||||||
|
export interface SortOrderButtonProps {
|
||||||
|
readonly isAsc?: boolean;
|
||||||
|
readonly onChange: (isAsc: boolean) => void;
|
||||||
|
id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SortOrderButton({
|
||||||
|
isAsc,
|
||||||
|
onChange,
|
||||||
|
id,
|
||||||
|
}: SortOrderButtonProps): JSX.Element {
|
||||||
|
const handleButtonClick = (): void => {
|
||||||
|
if (!isAsc) {
|
||||||
|
onChange(true);
|
||||||
|
} else onChange(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const buttonLabel = isAsc ? 'asc' : 'desc';
|
||||||
|
|
||||||
|
const arrowStyles: CSSProperties = {
|
||||||
|
width: '1em',
|
||||||
|
height: '1em',
|
||||||
|
transform: !isAsc ? 'scaleY(-1)' : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyleUtilsContext.Consumer>
|
||||||
|
{({ styling }) => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
id={id}
|
||||||
|
onClick={handleButtonClick}
|
||||||
|
aria-pressed={isAsc}
|
||||||
|
{...styling(['sortButton'])}
|
||||||
|
>
|
||||||
|
<ArrowUpIcon style={arrowStyles} />
|
||||||
|
{buttonLabel}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</StyleUtilsContext.Consumer>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,100 @@
|
||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import React, { ComponentProps, ReactNode } from 'react';
|
||||||
|
import JSONTree from 'react-json-tree';
|
||||||
|
import { Base16Theme, StylingFunction } from 'react-base16-styling';
|
||||||
|
import { DATA_TYPE_KEY } from '../monitor-config';
|
||||||
|
import {
|
||||||
|
getJsonTreeTheme,
|
||||||
|
StyleUtilsContext,
|
||||||
|
} from '../styles/createStylingFromTheme';
|
||||||
|
import { createTreeItemLabelRenderer, getItemString } from '../styles/tree';
|
||||||
|
import { identity } from '../utils/object';
|
||||||
|
|
||||||
|
export interface TreeViewProps
|
||||||
|
extends Partial<
|
||||||
|
Pick<
|
||||||
|
ComponentProps<typeof JSONTree>,
|
||||||
|
'keyPath' | 'shouldExpandNode' | 'hideRoot'
|
||||||
|
>
|
||||||
|
> {
|
||||||
|
data: unknown;
|
||||||
|
isWideLayout: boolean;
|
||||||
|
before?: ReactNode;
|
||||||
|
after?: ReactNode;
|
||||||
|
children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TreeView extends React.PureComponent<TreeViewProps> {
|
||||||
|
static defaultProps = {
|
||||||
|
hideRoot: true,
|
||||||
|
shouldExpandNode: (
|
||||||
|
keyPath: (string | number)[],
|
||||||
|
value: unknown,
|
||||||
|
layer: number
|
||||||
|
): boolean => {
|
||||||
|
return layer < 2;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
readonly selectLabelRenderer = createSelector<
|
||||||
|
StylingFunction,
|
||||||
|
StylingFunction,
|
||||||
|
ReturnType<typeof createTreeItemLabelRenderer>
|
||||||
|
>(identity, createTreeItemLabelRenderer);
|
||||||
|
|
||||||
|
readonly selectGetItemString = createSelector<
|
||||||
|
StylingFunction,
|
||||||
|
StylingFunction,
|
||||||
|
(type: string, data: unknown) => ReactNode
|
||||||
|
>(
|
||||||
|
identity,
|
||||||
|
(styling) => (type, data) =>
|
||||||
|
getItemString(styling, type, data, DATA_TYPE_KEY, false)
|
||||||
|
);
|
||||||
|
|
||||||
|
readonly selectTheme = createSelector<
|
||||||
|
Base16Theme,
|
||||||
|
Base16Theme,
|
||||||
|
ReturnType<typeof getJsonTreeTheme>
|
||||||
|
>(identity, getJsonTreeTheme);
|
||||||
|
|
||||||
|
constructor(props: TreeViewProps) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
render(): ReactNode {
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
before,
|
||||||
|
after,
|
||||||
|
children,
|
||||||
|
keyPath,
|
||||||
|
shouldExpandNode,
|
||||||
|
hideRoot,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyleUtilsContext.Consumer>
|
||||||
|
{({ styling, invertTheme, base16Theme }) => {
|
||||||
|
return (
|
||||||
|
<div {...styling('treeWrapper')}>
|
||||||
|
{before}
|
||||||
|
<JSONTree
|
||||||
|
keyPath={keyPath}
|
||||||
|
shouldExpandNode={shouldExpandNode}
|
||||||
|
data={data}
|
||||||
|
labelRenderer={this.selectLabelRenderer(styling)}
|
||||||
|
theme={this.selectTheme(base16Theme)}
|
||||||
|
invertTheme={invertTheme}
|
||||||
|
getItemString={this.selectGetItemString(styling)}
|
||||||
|
hideRoot={hideRoot}
|
||||||
|
/>
|
||||||
|
{after}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</StyleUtilsContext.Consumer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { StyleUtilsContext } from '../styles/createStylingFromTheme';
|
||||||
|
|
||||||
|
export type UListProps = React.HTMLAttributes<HTMLUListElement>;
|
||||||
|
|
||||||
|
export function UList(props: UListProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<StyleUtilsContext.Consumer>
|
||||||
|
{({ styling }) => <ul {...props} {...styling('uList')} />}
|
||||||
|
</StyleUtilsContext.Consumer>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,246 @@
|
||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import { StyleUtilsContext } from '../styles/createStylingFromTheme';
|
||||||
|
import { createTreeItemLabelRenderer } from '../styles/tree';
|
||||||
|
import {
|
||||||
|
QueryPreviewTabs,
|
||||||
|
RtkResourceInfo,
|
||||||
|
SelectorsSource,
|
||||||
|
TabOption,
|
||||||
|
} from '../types';
|
||||||
|
import { QueryPreviewHeader } from '../components/QueryPreviewHeader';
|
||||||
|
import {
|
||||||
|
QueryPreviewInfo,
|
||||||
|
QueryPreviewInfoProps,
|
||||||
|
} from '../components/QueryPreviewInfo';
|
||||||
|
import {
|
||||||
|
QueryPreviewApi,
|
||||||
|
QueryPreviewApiProps,
|
||||||
|
} from '../components/QueryPreviewApi';
|
||||||
|
import {
|
||||||
|
QueryPreviewSubscriptions,
|
||||||
|
QueryPreviewSubscriptionsProps,
|
||||||
|
} from '../components/QueryPreviewSubscriptions';
|
||||||
|
import {
|
||||||
|
QueryPreviewTags,
|
||||||
|
QueryPreviewTagsProps,
|
||||||
|
} from '../components/QueryPreviewTags';
|
||||||
|
import { NoRtkQueryApi } from '../components/NoRtkQueryApi';
|
||||||
|
import { InspectorSelectors } from '../selectors';
|
||||||
|
import { StylingFunction } from 'react-base16-styling';
|
||||||
|
import { mapProps } from './mapProps';
|
||||||
|
import {
|
||||||
|
QueryPreviewActions,
|
||||||
|
QueryPreviewActionsProps,
|
||||||
|
} from '../components/QueryPreviewActions';
|
||||||
|
import { isTabVisible } from '../utils/tabs';
|
||||||
|
|
||||||
|
export interface QueryPreviewProps<S = unknown> {
|
||||||
|
readonly selectedTab: QueryPreviewTabs;
|
||||||
|
readonly hasNoApis: boolean;
|
||||||
|
readonly onTabChange: (tab: QueryPreviewTabs) => void;
|
||||||
|
readonly resInfo: RtkResourceInfo | null;
|
||||||
|
readonly styling: StylingFunction;
|
||||||
|
readonly isWideLayout: boolean;
|
||||||
|
readonly selectorsSource: SelectorsSource<S>;
|
||||||
|
readonly selectors: InspectorSelectors<S>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tab content is not rendered if there's no selected query.
|
||||||
|
*/
|
||||||
|
type QueryPreviewTabProps = Omit<QueryPreviewProps<unknown>, 'resInfo'> & {
|
||||||
|
resInfo: RtkResourceInfo;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MappedQueryPreviewTags = mapProps<
|
||||||
|
QueryPreviewTabProps,
|
||||||
|
QueryPreviewTagsProps
|
||||||
|
>(({ selectors, selectorsSource, isWideLayout, resInfo }) => ({
|
||||||
|
resInfo,
|
||||||
|
tags: selectors.selectCurrentQueryTags(selectorsSource),
|
||||||
|
isWideLayout,
|
||||||
|
}))(QueryPreviewTags);
|
||||||
|
|
||||||
|
const MappedQueryPreviewInfo = mapProps<
|
||||||
|
QueryPreviewTabProps,
|
||||||
|
QueryPreviewInfoProps
|
||||||
|
>(({ resInfo, isWideLayout }) => ({ resInfo, isWideLayout }))(QueryPreviewInfo);
|
||||||
|
|
||||||
|
const MappedQuerySubscriptipns = mapProps<
|
||||||
|
QueryPreviewTabProps,
|
||||||
|
QueryPreviewSubscriptionsProps
|
||||||
|
>(({ selectors, selectorsSource, isWideLayout }) => ({
|
||||||
|
isWideLayout,
|
||||||
|
subscriptions: selectors.selectSubscriptionsOfCurrentQuery(selectorsSource),
|
||||||
|
}))(QueryPreviewSubscriptions);
|
||||||
|
|
||||||
|
const MappedApiPreview = mapProps<QueryPreviewTabProps, QueryPreviewApiProps>(
|
||||||
|
({ isWideLayout, selectors, selectorsSource }) => ({
|
||||||
|
isWideLayout,
|
||||||
|
apiState: selectors.selectApiOfCurrentQuery(selectorsSource),
|
||||||
|
apiStats: selectors.selectApiStatsOfCurrentQuery(selectorsSource),
|
||||||
|
})
|
||||||
|
)(QueryPreviewApi);
|
||||||
|
|
||||||
|
const MappedQueryPreviewActions = mapProps<
|
||||||
|
QueryPreviewTabProps,
|
||||||
|
QueryPreviewActionsProps
|
||||||
|
>(({ isWideLayout, selectorsSource, selectors }) => ({
|
||||||
|
isWideLayout,
|
||||||
|
actionsOfQuery: selectors.selectActionsOfCurrentQuery(selectorsSource),
|
||||||
|
}))(QueryPreviewActions);
|
||||||
|
|
||||||
|
const tabs: ReadonlyArray<
|
||||||
|
TabOption<QueryPreviewTabs, QueryPreviewTabProps, RtkResourceInfo['type']>
|
||||||
|
> = [
|
||||||
|
{
|
||||||
|
label: 'query',
|
||||||
|
value: QueryPreviewTabs.queryinfo,
|
||||||
|
component: MappedQueryPreviewInfo,
|
||||||
|
visible: {
|
||||||
|
query: true,
|
||||||
|
mutation: true,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'actions',
|
||||||
|
value: QueryPreviewTabs.actions,
|
||||||
|
component: MappedQueryPreviewActions,
|
||||||
|
visible: {
|
||||||
|
query: true,
|
||||||
|
mutation: true,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'tags',
|
||||||
|
value: QueryPreviewTabs.queryTags,
|
||||||
|
component: MappedQueryPreviewTags,
|
||||||
|
visible: {
|
||||||
|
query: true,
|
||||||
|
mutation: false,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'subs',
|
||||||
|
value: QueryPreviewTabs.querySubscriptions,
|
||||||
|
component: MappedQuerySubscriptipns,
|
||||||
|
visible: {
|
||||||
|
query: true,
|
||||||
|
mutation: false,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'api',
|
||||||
|
value: QueryPreviewTabs.apiConfig,
|
||||||
|
component: MappedApiPreview,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export class QueryPreview<S> extends React.PureComponent<QueryPreviewProps<S>> {
|
||||||
|
readonly labelRenderer: ReturnType<typeof createTreeItemLabelRenderer>;
|
||||||
|
|
||||||
|
constructor(props: QueryPreviewProps<S>) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.labelRenderer = createTreeItemLabelRenderer(this.props.styling);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderLabelWithCounter = (
|
||||||
|
label: React.ReactText,
|
||||||
|
counter: number
|
||||||
|
): string => {
|
||||||
|
let counterAsString = counter.toFixed(0);
|
||||||
|
|
||||||
|
if (counterAsString.length > 3) {
|
||||||
|
counterAsString = counterAsString.slice(0, 2) + '...';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${label}(${counterAsString})`;
|
||||||
|
};
|
||||||
|
|
||||||
|
renderTabLabel = (
|
||||||
|
tab: TabOption<QueryPreviewTabs, unknown, 'query' | 'mutation'>
|
||||||
|
): ReactNode => {
|
||||||
|
const { selectors, selectorsSource, resInfo } = this.props;
|
||||||
|
const tabCount = selectors.selectTabCounters(selectorsSource)[tab.value];
|
||||||
|
|
||||||
|
let tabLabel = tab.label;
|
||||||
|
|
||||||
|
if (tabLabel === 'query' && resInfo?.type === 'mutation') {
|
||||||
|
tabLabel = resInfo.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tabCount > 0) {
|
||||||
|
return this.renderLabelWithCounter(tabLabel, tabCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tabLabel;
|
||||||
|
};
|
||||||
|
|
||||||
|
render(): ReactNode {
|
||||||
|
const { resInfo, selectedTab, onTabChange, hasNoApis } = this.props;
|
||||||
|
|
||||||
|
const { component: TabComponent } =
|
||||||
|
tabs.find((tab) => tab.value === selectedTab) || tabs[0];
|
||||||
|
|
||||||
|
if (!resInfo) {
|
||||||
|
return (
|
||||||
|
<StyleUtilsContext.Consumer>
|
||||||
|
{({ styling }) => (
|
||||||
|
<div {...styling('queryPreview')}>
|
||||||
|
<QueryPreviewHeader
|
||||||
|
selectedTab={selectedTab}
|
||||||
|
onTabChange={onTabChange}
|
||||||
|
tabs={
|
||||||
|
tabs.filter((tab) =>
|
||||||
|
isTabVisible(tab, 'default')
|
||||||
|
) as ReadonlyArray<
|
||||||
|
TabOption<
|
||||||
|
QueryPreviewTabs,
|
||||||
|
unknown,
|
||||||
|
RtkResourceInfo['type']
|
||||||
|
>
|
||||||
|
>
|
||||||
|
}
|
||||||
|
renderTabLabel={this.renderTabLabel}
|
||||||
|
/>
|
||||||
|
{hasNoApis && <NoRtkQueryApi />}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</StyleUtilsContext.Consumer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyleUtilsContext.Consumer>
|
||||||
|
{({ styling }) => {
|
||||||
|
return (
|
||||||
|
<div {...styling('queryPreview')}>
|
||||||
|
<QueryPreviewHeader
|
||||||
|
selectedTab={selectedTab}
|
||||||
|
onTabChange={onTabChange}
|
||||||
|
tabs={
|
||||||
|
tabs.filter((tab) =>
|
||||||
|
isTabVisible(tab, resInfo.type)
|
||||||
|
) as ReadonlyArray<
|
||||||
|
TabOption<
|
||||||
|
QueryPreviewTabs,
|
||||||
|
unknown,
|
||||||
|
RtkResourceInfo['type']
|
||||||
|
>
|
||||||
|
>
|
||||||
|
}
|
||||||
|
renderTabLabel={this.renderTabLabel}
|
||||||
|
/>
|
||||||
|
<TabComponent {...(this.props as QueryPreviewTabProps)} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</StyleUtilsContext.Consumer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,166 @@
|
||||||
|
import React, { PureComponent, createRef, ReactNode } from 'react';
|
||||||
|
import type { AnyAction, Dispatch, Action } from '@reduxjs/toolkit';
|
||||||
|
import type { LiftedAction, LiftedState } from '@redux-devtools/core';
|
||||||
|
import {
|
||||||
|
QueryFormValues,
|
||||||
|
QueryPreviewTabs,
|
||||||
|
RtkQueryMonitorState,
|
||||||
|
StyleUtils,
|
||||||
|
SelectorsSource,
|
||||||
|
RtkResourceInfo,
|
||||||
|
} from '../types';
|
||||||
|
import { createInspectorSelectors, computeSelectorSource } from '../selectors';
|
||||||
|
import {
|
||||||
|
changeQueryFormValues,
|
||||||
|
selectedPreviewTab,
|
||||||
|
selectQueryKey,
|
||||||
|
} from '../reducers';
|
||||||
|
import { QueryList } from '../components/QueryList';
|
||||||
|
import { QueryForm } from '../components/QueryForm';
|
||||||
|
import { QueryPreview } from './QueryPreview';
|
||||||
|
|
||||||
|
type ForwardedMonitorProps<S, A extends Action<unknown>> = Pick<
|
||||||
|
LiftedState<S, A, RtkQueryMonitorState>,
|
||||||
|
'monitorState' | 'currentStateIndex' | 'computedStates' | 'actionsById'
|
||||||
|
>;
|
||||||
|
|
||||||
|
export interface RtkQueryInspectorProps<S, A extends Action<unknown>>
|
||||||
|
extends ForwardedMonitorProps<S, A> {
|
||||||
|
dispatch: Dispatch<LiftedAction<S, A, RtkQueryMonitorState>>;
|
||||||
|
styleUtils: StyleUtils;
|
||||||
|
}
|
||||||
|
|
||||||
|
type RtkQueryInspectorState<S> = {
|
||||||
|
selectorsSource: SelectorsSource<S>;
|
||||||
|
isWideLayout: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
class RtkQueryInspector<S, A extends Action<unknown>> extends PureComponent<
|
||||||
|
RtkQueryInspectorProps<S, A>,
|
||||||
|
RtkQueryInspectorState<S>
|
||||||
|
> {
|
||||||
|
inspectorRef = createRef<HTMLDivElement>();
|
||||||
|
|
||||||
|
isWideIntervalRef: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
|
constructor(props: RtkQueryInspectorProps<S, A>) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isWideLayout: true,
|
||||||
|
selectorsSource: computeSelectorSource(props, null),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static wideLayout = 600;
|
||||||
|
|
||||||
|
static getDerivedStateFromProps(
|
||||||
|
props: RtkQueryInspectorProps<unknown, Action<unknown>>,
|
||||||
|
state: RtkQueryInspectorState<unknown>
|
||||||
|
): null | Partial<RtkQueryInspectorState<unknown>> {
|
||||||
|
const selectorsSource = computeSelectorSource<unknown, Action<unknown>>(
|
||||||
|
props,
|
||||||
|
state.selectorsSource
|
||||||
|
);
|
||||||
|
|
||||||
|
if (selectorsSource !== state.selectorsSource) {
|
||||||
|
return {
|
||||||
|
selectorsSource,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectors = createInspectorSelectors<S>();
|
||||||
|
|
||||||
|
updateSizeMode = (): void => {
|
||||||
|
if (this.inspectorRef.current) {
|
||||||
|
const isWideLayout =
|
||||||
|
this.inspectorRef.current.offsetWidth >= RtkQueryInspector.wideLayout;
|
||||||
|
|
||||||
|
if (isWideLayout !== this.state.isWideLayout) {
|
||||||
|
this.setState({ isWideLayout });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount(): void {
|
||||||
|
this.updateSizeMode();
|
||||||
|
|
||||||
|
this.isWideIntervalRef = setInterval(this.updateSizeMode, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount(): void {
|
||||||
|
if (this.isWideIntervalRef) {
|
||||||
|
clearTimeout(this.isWideIntervalRef);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleQueryFormValuesChange = (values: Partial<QueryFormValues>): void => {
|
||||||
|
this.props.dispatch(changeQueryFormValues(values) as AnyAction);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSelectQuery = (queryInfo: RtkResourceInfo): void => {
|
||||||
|
this.props.dispatch(selectQueryKey(queryInfo) as AnyAction);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleTabChange = (tab: QueryPreviewTabs): void => {
|
||||||
|
this.props.dispatch(selectedPreviewTab(tab) as AnyAction);
|
||||||
|
};
|
||||||
|
|
||||||
|
render(): ReactNode {
|
||||||
|
const { selectorsSource, isWideLayout } = this.state;
|
||||||
|
const {
|
||||||
|
styleUtils: { styling },
|
||||||
|
} = this.props;
|
||||||
|
const allVisibleRtkResourceInfos =
|
||||||
|
this.selectors.selectAllVisbileQueries(selectorsSource);
|
||||||
|
|
||||||
|
const currentResInfo =
|
||||||
|
this.selectors.selectCurrentQueryInfo(selectorsSource);
|
||||||
|
|
||||||
|
const apiStates = this.selectors.selectApiStates(selectorsSource);
|
||||||
|
|
||||||
|
const hasNoApi = apiStates == null;
|
||||||
|
|
||||||
|
const searchQueryRegex =
|
||||||
|
this.selectors.selectSearchQueryRegex(selectorsSource);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={this.inspectorRef}
|
||||||
|
data-wide-layout={+this.state.isWideLayout}
|
||||||
|
{...styling('inspector')}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
{...styling('querySectionWrapper')}
|
||||||
|
data-wide-layout={+this.state.isWideLayout}
|
||||||
|
>
|
||||||
|
<QueryForm
|
||||||
|
searchQueryRegex={searchQueryRegex}
|
||||||
|
values={selectorsSource.monitorState.queryForm.values}
|
||||||
|
onFormValuesChange={this.handleQueryFormValuesChange}
|
||||||
|
/>
|
||||||
|
<QueryList
|
||||||
|
onSelectQuery={this.handleSelectQuery}
|
||||||
|
resInfos={allVisibleRtkResourceInfos}
|
||||||
|
selectedQueryKey={selectorsSource.monitorState.selectedQueryKey}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<QueryPreview<S>
|
||||||
|
selectorsSource={this.state.selectorsSource}
|
||||||
|
selectors={this.selectors}
|
||||||
|
resInfo={currentResInfo}
|
||||||
|
selectedTab={selectorsSource.monitorState.selectedPreviewTab}
|
||||||
|
onTabChange={this.handleTabChange}
|
||||||
|
styling={styling}
|
||||||
|
isWideLayout={isWideLayout}
|
||||||
|
hasNoApis={hasNoApi}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RtkQueryInspector;
|
|
@ -0,0 +1,90 @@
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Action, AnyAction } from 'redux';
|
||||||
|
import RtkQueryInspector from './RtkQueryInspector';
|
||||||
|
import { reducer } from '../reducers';
|
||||||
|
import {
|
||||||
|
ExternalProps,
|
||||||
|
RtkQueryMonitorProps,
|
||||||
|
RtkQueryMonitorState,
|
||||||
|
StyleUtils,
|
||||||
|
} from '../types';
|
||||||
|
import {
|
||||||
|
createThemeState,
|
||||||
|
StyleUtilsContext,
|
||||||
|
} from '../styles/createStylingFromTheme';
|
||||||
|
|
||||||
|
interface DefaultProps {
|
||||||
|
theme: string;
|
||||||
|
invertTheme: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RtkQueryComponentState {
|
||||||
|
readonly styleUtils: StyleUtils;
|
||||||
|
}
|
||||||
|
|
||||||
|
class RtkQueryMonitor<S, A extends Action<unknown>> extends Component<
|
||||||
|
RtkQueryMonitorProps<S, A>,
|
||||||
|
RtkQueryComponentState
|
||||||
|
> {
|
||||||
|
static update = reducer;
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
dispatch: PropTypes.func,
|
||||||
|
computedStates: PropTypes.array,
|
||||||
|
currentStateIndex: PropTypes.number,
|
||||||
|
actionsById: PropTypes.object,
|
||||||
|
stagedActionIds: PropTypes.array,
|
||||||
|
skippedActionIds: PropTypes.array,
|
||||||
|
monitorState: PropTypes.object,
|
||||||
|
theme: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
|
||||||
|
invertTheme: PropTypes.bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
static defaultProps: DefaultProps = {
|
||||||
|
theme: 'nicinabox',
|
||||||
|
invertTheme: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props: RtkQueryMonitorProps<S, A>) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
styleUtils: createThemeState<S, A>(props),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
currentStateIndex,
|
||||||
|
computedStates,
|
||||||
|
monitorState,
|
||||||
|
dispatch,
|
||||||
|
actionsById,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyleUtilsContext.Provider value={this.state.styleUtils}>
|
||||||
|
<RtkQueryInspector<S, AnyAction>
|
||||||
|
computedStates={computedStates}
|
||||||
|
currentStateIndex={currentStateIndex}
|
||||||
|
monitorState={monitorState}
|
||||||
|
dispatch={dispatch}
|
||||||
|
styleUtils={this.state.styleUtils}
|
||||||
|
actionsById={actionsById}
|
||||||
|
/>
|
||||||
|
</StyleUtilsContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RtkQueryMonitor as unknown as React.ComponentType<
|
||||||
|
ExternalProps<unknown, Action<unknown>>
|
||||||
|
> & {
|
||||||
|
update(
|
||||||
|
monitorProps: ExternalProps<unknown, Action<unknown>>,
|
||||||
|
state: RtkQueryMonitorState | undefined,
|
||||||
|
action: Action
|
||||||
|
): RtkQueryMonitorState;
|
||||||
|
defaultProps: DefaultProps;
|
||||||
|
};
|
|
@ -0,0 +1,29 @@
|
||||||
|
import React, { ComponentType, ReactNode, Component } from 'react';
|
||||||
|
|
||||||
|
interface Mapper<In, Out> {
|
||||||
|
(inProps: In): Out;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MapPropsOutput<In, Out> {
|
||||||
|
(comp: ComponentType<Out>): ComponentType<In>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapProps<In, Out>(
|
||||||
|
mapper: Mapper<In, Out>
|
||||||
|
): MapPropsOutput<In, Out> {
|
||||||
|
return function mapPropsHoc(Comp) {
|
||||||
|
class MapPropsHoc extends Component<In> {
|
||||||
|
render(): ReactNode {
|
||||||
|
const mappedProps = mapper(this.props);
|
||||||
|
|
||||||
|
return <Comp {...mappedProps} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
static displayName = `mapProps(${
|
||||||
|
Comp.displayName || Comp.name || 'Component'
|
||||||
|
})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return MapPropsHoc;
|
||||||
|
};
|
||||||
|
}
|
2
packages/redux-devtools-rtk-query-monitor/src/index.ts
Normal file
2
packages/redux-devtools-rtk-query-monitor/src/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export { default } from './containers/RtkQueryMonitor';
|
||||||
|
export type { ExternalProps } from './types';
|
|
@ -0,0 +1,6 @@
|
||||||
|
export const DATA_TYPE_KEY = Symbol.for('__serializedType__');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see https://github.com/reduxjs/redux-toolkit/blob/b718e01d323d3ab4b913e5d88c9b90aa790bb975/src/query/core/buildSlice.ts#L259
|
||||||
|
*/
|
||||||
|
export const missingTagId = '__internal_without_id';
|
65
packages/redux-devtools-rtk-query-monitor/src/reducers.ts
Normal file
65
packages/redux-devtools-rtk-query-monitor/src/reducers.ts
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import { Action, AnyAction } from 'redux';
|
||||||
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
import {
|
||||||
|
QueryInfo,
|
||||||
|
RtkQueryMonitorState,
|
||||||
|
QueryFormValues,
|
||||||
|
RtkQueryMonitorProps,
|
||||||
|
QueryPreviewTabs,
|
||||||
|
} from './types';
|
||||||
|
import { QueryComparators } from './utils/comparators';
|
||||||
|
import { QueryFilters } from './utils/filters';
|
||||||
|
|
||||||
|
const initialState: RtkQueryMonitorState = {
|
||||||
|
queryForm: {
|
||||||
|
values: {
|
||||||
|
queryComparator: QueryComparators.fulfilledTimeStamp,
|
||||||
|
isAscendingQueryComparatorOrder: false,
|
||||||
|
searchValue: '',
|
||||||
|
isRegexSearch: false,
|
||||||
|
queryFilter: QueryFilters.queryKey,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
selectedPreviewTab: QueryPreviewTabs.queryinfo,
|
||||||
|
selectedQueryKey: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const monitorSlice = createSlice({
|
||||||
|
/**
|
||||||
|
* `@@` prefix is mandatory.
|
||||||
|
* @see lifedAction @ `packages/redux-devtools-app/src/actions/index.ts`
|
||||||
|
*/
|
||||||
|
name: '@@rtk-query-monitor',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
changeQueryFormValues(
|
||||||
|
state,
|
||||||
|
action: PayloadAction<Partial<QueryFormValues>>
|
||||||
|
) {
|
||||||
|
state.queryForm.values = { ...state.queryForm.values, ...action.payload };
|
||||||
|
},
|
||||||
|
selectQueryKey(
|
||||||
|
state,
|
||||||
|
action: PayloadAction<Pick<QueryInfo, 'reducerPath' | 'queryKey'>>
|
||||||
|
) {
|
||||||
|
state.selectedQueryKey = {
|
||||||
|
queryKey: action.payload.queryKey,
|
||||||
|
reducerPath: action.payload.reducerPath,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
selectedPreviewTab(state, action: PayloadAction<QueryPreviewTabs>) {
|
||||||
|
state.selectedPreviewTab = action.payload;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export function reducer<S, A extends Action<unknown>>(
|
||||||
|
props: RtkQueryMonitorProps<S, A>,
|
||||||
|
state: RtkQueryMonitorState | undefined,
|
||||||
|
action: AnyAction
|
||||||
|
): RtkQueryMonitorState {
|
||||||
|
return monitorSlice.reducer(state, action);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const { selectQueryKey, changeQueryFormValues, selectedPreviewTab } =
|
||||||
|
monitorSlice.actions;
|
283
packages/redux-devtools-rtk-query-monitor/src/selectors.ts
Normal file
283
packages/redux-devtools-rtk-query-monitor/src/selectors.ts
Normal file
|
@ -0,0 +1,283 @@
|
||||||
|
import { Action, createSelector, Selector } from '@reduxjs/toolkit';
|
||||||
|
import { RtkQueryInspectorProps } from './containers/RtkQueryInspector';
|
||||||
|
import {
|
||||||
|
ApiStats,
|
||||||
|
QueryInfo,
|
||||||
|
RtkQueryApiState,
|
||||||
|
RtkQueryTag,
|
||||||
|
SelectorsSource,
|
||||||
|
RtkQueryProvided,
|
||||||
|
QueryPreviewTabs,
|
||||||
|
RtkResourceInfo,
|
||||||
|
} from './types';
|
||||||
|
import { Comparator, queryComparators } from './utils/comparators';
|
||||||
|
import { FilterList, queryListFilters } from './utils/filters';
|
||||||
|
import { emptyRecord } from './utils/object';
|
||||||
|
import { escapeRegExpSpecialCharacter } from './utils/regexp';
|
||||||
|
import {
|
||||||
|
getApiStatesOf,
|
||||||
|
extractAllApiQueries,
|
||||||
|
flipComparator,
|
||||||
|
getQueryTagsOf,
|
||||||
|
generateApiStatsOfCurrentQuery,
|
||||||
|
getActionsOfCurrentQuery,
|
||||||
|
extractAllApiMutations,
|
||||||
|
} from './utils/rtk-query';
|
||||||
|
|
||||||
|
type InspectorSelector<S, Output> = Selector<SelectorsSource<S>, Output>;
|
||||||
|
|
||||||
|
export function computeSelectorSource<S, A extends Action<unknown>>(
|
||||||
|
props: RtkQueryInspectorProps<S, A>,
|
||||||
|
previous: SelectorsSource<S> | null = null
|
||||||
|
): SelectorsSource<S> {
|
||||||
|
const { computedStates, currentStateIndex, monitorState, actionsById } =
|
||||||
|
props;
|
||||||
|
|
||||||
|
const userState =
|
||||||
|
computedStates.length > 0 ? computedStates[currentStateIndex].state : null;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!previous ||
|
||||||
|
previous.userState !== userState ||
|
||||||
|
previous.monitorState !== monitorState ||
|
||||||
|
previous.actionsById !== actionsById
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
userState,
|
||||||
|
monitorState,
|
||||||
|
currentStateIndex,
|
||||||
|
actionsById,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return previous;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InspectorSelectors<S> {
|
||||||
|
readonly selectQueryComparator: InspectorSelector<S, Comparator<QueryInfo>>;
|
||||||
|
readonly selectApiStates: InspectorSelector<
|
||||||
|
S,
|
||||||
|
ReturnType<typeof getApiStatesOf>
|
||||||
|
>;
|
||||||
|
readonly selectAllQueries: InspectorSelector<
|
||||||
|
S,
|
||||||
|
ReturnType<typeof extractAllApiQueries>
|
||||||
|
>;
|
||||||
|
readonly selectAllVisbileQueries: InspectorSelector<S, RtkResourceInfo[]>;
|
||||||
|
readonly selectCurrentQueryInfo: InspectorSelector<S, RtkResourceInfo | null>;
|
||||||
|
readonly selectSearchQueryRegex: InspectorSelector<S, RegExp | null>;
|
||||||
|
readonly selectCurrentQueryTags: InspectorSelector<S, RtkQueryTag[]>;
|
||||||
|
readonly selectApiStatsOfCurrentQuery: InspectorSelector<S, ApiStats | null>;
|
||||||
|
readonly selectApiOfCurrentQuery: InspectorSelector<
|
||||||
|
S,
|
||||||
|
RtkQueryApiState | null
|
||||||
|
>;
|
||||||
|
readonly selectTabCounters: InspectorSelector<
|
||||||
|
S,
|
||||||
|
Record<QueryPreviewTabs, number>
|
||||||
|
>;
|
||||||
|
readonly selectSubscriptionsOfCurrentQuery: InspectorSelector<
|
||||||
|
S,
|
||||||
|
RtkQueryApiState['subscriptions'][string]
|
||||||
|
>;
|
||||||
|
readonly selectActionsOfCurrentQuery: InspectorSelector<
|
||||||
|
S,
|
||||||
|
ReturnType<typeof getActionsOfCurrentQuery>
|
||||||
|
>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createInspectorSelectors<S>(): InspectorSelectors<S> {
|
||||||
|
const selectQueryComparator = ({
|
||||||
|
monitorState,
|
||||||
|
}: SelectorsSource<S>): Comparator<RtkResourceInfo> => {
|
||||||
|
return queryComparators[monitorState.queryForm.values.queryComparator];
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectQueryListFilter = ({
|
||||||
|
monitorState,
|
||||||
|
}: SelectorsSource<S>): FilterList<RtkResourceInfo> => {
|
||||||
|
return queryListFilters[monitorState.queryForm.values.queryFilter];
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectActionsById = ({ actionsById }: SelectorsSource<S>) =>
|
||||||
|
actionsById;
|
||||||
|
|
||||||
|
const selectApiStates = createSelector(
|
||||||
|
({ userState }: SelectorsSource<S>) => userState,
|
||||||
|
getApiStatesOf
|
||||||
|
);
|
||||||
|
const selectAllQueries = createSelector(
|
||||||
|
selectApiStates,
|
||||||
|
extractAllApiQueries
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectAllMutations = createSelector(
|
||||||
|
selectApiStates,
|
||||||
|
extractAllApiMutations
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectSearchQueryRegex = createSelector(
|
||||||
|
({ monitorState }: SelectorsSource<S>) =>
|
||||||
|
monitorState.queryForm.values.searchValue,
|
||||||
|
({ monitorState }: SelectorsSource<S>) =>
|
||||||
|
monitorState.queryForm.values.isRegexSearch,
|
||||||
|
(searchValue, isRegexSearch) => {
|
||||||
|
if (searchValue) {
|
||||||
|
try {
|
||||||
|
const regexPattern = isRegexSearch
|
||||||
|
? searchValue
|
||||||
|
: escapeRegExpSpecialCharacter(searchValue);
|
||||||
|
|
||||||
|
return new RegExp(regexPattern, 'i');
|
||||||
|
} catch (err) {
|
||||||
|
// We notify that the search regex provided is not valid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectComparatorOrder = ({ monitorState }: SelectorsSource<S>) =>
|
||||||
|
monitorState.queryForm.values.isAscendingQueryComparatorOrder;
|
||||||
|
|
||||||
|
const selectAllVisbileQueries = createSelector(
|
||||||
|
[
|
||||||
|
selectQueryComparator,
|
||||||
|
selectQueryListFilter,
|
||||||
|
selectAllQueries,
|
||||||
|
selectAllMutations,
|
||||||
|
selectComparatorOrder,
|
||||||
|
selectSearchQueryRegex,
|
||||||
|
],
|
||||||
|
(
|
||||||
|
comparator,
|
||||||
|
queryListFilter,
|
||||||
|
queryList,
|
||||||
|
mutationsList,
|
||||||
|
isAscending,
|
||||||
|
searchRegex
|
||||||
|
) => {
|
||||||
|
const filteredList = queryListFilter(
|
||||||
|
searchRegex,
|
||||||
|
(queryList as RtkResourceInfo[]).concat(mutationsList)
|
||||||
|
);
|
||||||
|
|
||||||
|
const computedComparator = isAscending
|
||||||
|
? comparator
|
||||||
|
: flipComparator(comparator);
|
||||||
|
|
||||||
|
return filteredList.slice().sort(computedComparator);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectCurrentQueryInfo = createSelector(
|
||||||
|
selectAllQueries,
|
||||||
|
selectAllMutations,
|
||||||
|
({ monitorState }: SelectorsSource<S>) => monitorState.selectedQueryKey,
|
||||||
|
(allQueries, allMutations, selectedQueryKey) => {
|
||||||
|
if (!selectedQueryKey) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentQueryInfo: null | RtkResourceInfo =
|
||||||
|
allQueries.find(
|
||||||
|
(query) =>
|
||||||
|
query.queryKey === selectedQueryKey.queryKey &&
|
||||||
|
selectedQueryKey.reducerPath === query.reducerPath
|
||||||
|
) || null;
|
||||||
|
|
||||||
|
if (!currentQueryInfo) {
|
||||||
|
currentQueryInfo =
|
||||||
|
allMutations.find(
|
||||||
|
(mutation) =>
|
||||||
|
mutation.queryKey === selectedQueryKey.queryKey &&
|
||||||
|
selectedQueryKey.reducerPath === mutation.reducerPath
|
||||||
|
) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentQueryInfo;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectApiOfCurrentQuery: InspectorSelector<S, null | RtkQueryApiState> =
|
||||||
|
(selectorsSource: SelectorsSource<S>) => {
|
||||||
|
const apiStates = selectApiStates(selectorsSource);
|
||||||
|
const currentQueryInfo = selectCurrentQueryInfo(selectorsSource);
|
||||||
|
|
||||||
|
if (!apiStates || !currentQueryInfo) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiStates[currentQueryInfo.reducerPath] ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectProvidedOfCurrentQuery: InspectorSelector<
|
||||||
|
S,
|
||||||
|
null | RtkQueryProvided
|
||||||
|
> = (selectorsSource: SelectorsSource<S>) => {
|
||||||
|
return selectApiOfCurrentQuery(selectorsSource)?.provided ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectSubscriptionsOfCurrentQuery = createSelector(
|
||||||
|
[selectApiOfCurrentQuery, selectCurrentQueryInfo],
|
||||||
|
(apiState, queryInfo) => {
|
||||||
|
if (!queryInfo || !apiState) {
|
||||||
|
return emptyRecord;
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiState.subscriptions[queryInfo.queryKey];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectCurrentQueryTags = createSelector(
|
||||||
|
[selectCurrentQueryInfo, selectProvidedOfCurrentQuery],
|
||||||
|
getQueryTagsOf
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectApiStatsOfCurrentQuery = createSelector(
|
||||||
|
selectApiOfCurrentQuery,
|
||||||
|
(selectorsSource) => selectorsSource.actionsById,
|
||||||
|
(selectorsSource) => selectorsSource.currentStateIndex,
|
||||||
|
generateApiStatsOfCurrentQuery
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectActionsOfCurrentQuery = createSelector(
|
||||||
|
selectCurrentQueryInfo,
|
||||||
|
selectActionsById,
|
||||||
|
getActionsOfCurrentQuery
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectTabCounters = createSelector(
|
||||||
|
[
|
||||||
|
selectSubscriptionsOfCurrentQuery,
|
||||||
|
selectActionsOfCurrentQuery,
|
||||||
|
selectCurrentQueryTags,
|
||||||
|
],
|
||||||
|
(subscriptions, actions, tags) => {
|
||||||
|
return {
|
||||||
|
[QueryPreviewTabs.queryTags]: tags.length,
|
||||||
|
[QueryPreviewTabs.querySubscriptions]: Object.keys(subscriptions ?? {})
|
||||||
|
.length,
|
||||||
|
[QueryPreviewTabs.apiConfig]: 0,
|
||||||
|
[QueryPreviewTabs.queryinfo]: 0,
|
||||||
|
[QueryPreviewTabs.actions]: actions.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
selectQueryComparator,
|
||||||
|
selectApiStates,
|
||||||
|
selectAllQueries,
|
||||||
|
selectAllVisbileQueries,
|
||||||
|
selectSearchQueryRegex,
|
||||||
|
selectCurrentQueryInfo,
|
||||||
|
selectCurrentQueryTags,
|
||||||
|
selectApiStatsOfCurrentQuery,
|
||||||
|
selectSubscriptionsOfCurrentQuery,
|
||||||
|
selectApiOfCurrentQuery,
|
||||||
|
selectTabCounters,
|
||||||
|
selectActionsOfCurrentQuery,
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,559 @@
|
||||||
|
import jss, { StyleSheet } from 'jss';
|
||||||
|
import preset from 'jss-preset-default';
|
||||||
|
import {
|
||||||
|
createStyling,
|
||||||
|
getBase16Theme,
|
||||||
|
invertTheme,
|
||||||
|
StylingConfig,
|
||||||
|
} from 'react-base16-styling';
|
||||||
|
import rgba from 'hex-rgba';
|
||||||
|
import * as reduxThemes from 'redux-devtools-themes';
|
||||||
|
import { Action } from 'redux';
|
||||||
|
import { RtkQueryMonitorProps, StyleUtils } from '../types';
|
||||||
|
import { createContext } from 'react';
|
||||||
|
|
||||||
|
jss.setup(preset());
|
||||||
|
|
||||||
|
export const colorMap = (theme: reduxThemes.Base16Theme) =>
|
||||||
|
({
|
||||||
|
TEXT_COLOR: theme.base06,
|
||||||
|
TEXT_PLACEHOLDER_COLOR: rgba(theme.base06, 60),
|
||||||
|
BACKGROUND_COLOR: theme.base00,
|
||||||
|
SELECTED_BACKGROUND_COLOR: rgba(theme.base03, 20),
|
||||||
|
SKIPPED_BACKGROUND_COLOR: rgba(theme.base03, 10),
|
||||||
|
HEADER_BACKGROUND_COLOR: rgba(theme.base03, 30),
|
||||||
|
HEADER_BORDER_COLOR: rgba(theme.base03, 20),
|
||||||
|
BORDER_COLOR: rgba(theme.base03, 50),
|
||||||
|
LIST_BORDER_COLOR: rgba(theme.base03, 50),
|
||||||
|
ACTION_TIME_BACK_COLOR: rgba(theme.base03, 20),
|
||||||
|
ACTION_TIME_COLOR: theme.base04,
|
||||||
|
PIN_COLOR: theme.base04,
|
||||||
|
ITEM_HINT_COLOR: rgba(theme.base0F, 90),
|
||||||
|
TAB_BACK_SELECTED_COLOR: rgba(theme.base03, 20),
|
||||||
|
TAB_BACK_COLOR: rgba(theme.base00, 70),
|
||||||
|
TAB_BACK_HOVER_COLOR: rgba(theme.base03, 40),
|
||||||
|
TAB_BORDER_COLOR: rgba(theme.base03, 50),
|
||||||
|
DIFF_ADD_COLOR: rgba(theme.base0B, 40),
|
||||||
|
DIFF_REMOVE_COLOR: rgba(theme.base08, 40),
|
||||||
|
DIFF_ARROW_COLOR: theme.base0E,
|
||||||
|
LINK_COLOR: rgba(theme.base0E, 90),
|
||||||
|
LINK_HOVER_COLOR: theme.base0E,
|
||||||
|
ERROR_COLOR: theme.base08,
|
||||||
|
ULIST_DISC_COLOR: theme.base0D,
|
||||||
|
ULIST_COLOR: rgba(theme.base06, 60),
|
||||||
|
ULIST_STRONG_COLOR: theme.base0B,
|
||||||
|
TAB_CONTENT_COLOR: rgba(theme.base06, 60),
|
||||||
|
TOGGLE_BUTTON_BACKGROUND: rgba(theme.base00, 70),
|
||||||
|
TOGGLE_BUTTON_SELECTED_BACKGROUND: theme.base04,
|
||||||
|
TOGGLE_BUTTON_ERROR: rgba(theme.base08, 40),
|
||||||
|
} as const);
|
||||||
|
|
||||||
|
type Color = keyof ReturnType<typeof colorMap>;
|
||||||
|
type ColorMap = {
|
||||||
|
[color in Color]: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSheetFromColorMap = (map: ColorMap) => {
|
||||||
|
const appearanceNone = {
|
||||||
|
'-webkit-appearance': 'none',
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
inspector: {
|
||||||
|
display: 'flex',
|
||||||
|
flexFlow: 'column nowrap',
|
||||||
|
overflow: 'hidden',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
'font-family': 'monaco, Consolas, "Lucida Console", monospace',
|
||||||
|
'font-size': '12px',
|
||||||
|
'font-smoothing': 'antialiased',
|
||||||
|
'line-height': '1.5em',
|
||||||
|
|
||||||
|
'background-color': map.BACKGROUND_COLOR,
|
||||||
|
color: map.TEXT_COLOR,
|
||||||
|
|
||||||
|
'&[data-wide-layout="1"]': {
|
||||||
|
flexFlow: 'row nowrap',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
querySectionWrapper: {
|
||||||
|
display: 'flex',
|
||||||
|
flex: '0 0 auto',
|
||||||
|
height: '50%',
|
||||||
|
width: '100%',
|
||||||
|
borderColor: map.TAB_BORDER_COLOR,
|
||||||
|
|
||||||
|
'&[data-wide-layout="0"]': {
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderStyle: 'solid',
|
||||||
|
},
|
||||||
|
|
||||||
|
'&[data-wide-layout="1"]': {
|
||||||
|
height: '100%',
|
||||||
|
width: '44%',
|
||||||
|
borderRightWidth: 1,
|
||||||
|
borderStyle: 'solid',
|
||||||
|
},
|
||||||
|
flexFlow: 'column nowrap',
|
||||||
|
'& > :first-child': {
|
||||||
|
flex: '0 0 auto',
|
||||||
|
'border-bottom-width': '1px',
|
||||||
|
'border-bottom-style': 'solid',
|
||||||
|
'border-color': map.LIST_BORDER_COLOR,
|
||||||
|
},
|
||||||
|
'& > :nth-child(n + 2)': {
|
||||||
|
flex: '1 1 auto',
|
||||||
|
overflowX: 'hidden',
|
||||||
|
overflowY: 'auto',
|
||||||
|
maxHeight: 'calc(100% - 70px)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
queryList: {
|
||||||
|
listStyle: 'none',
|
||||||
|
margin: '0',
|
||||||
|
padding: '0',
|
||||||
|
},
|
||||||
|
|
||||||
|
queryListItem: {
|
||||||
|
'border-bottom-width': '1px',
|
||||||
|
'border-bottom-style': 'solid',
|
||||||
|
display: 'flex',
|
||||||
|
'justify-content': 'space-between',
|
||||||
|
padding: '5px 10px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
'user-select': 'none',
|
||||||
|
'&:last-child': {
|
||||||
|
'border-bottom-width': 0,
|
||||||
|
},
|
||||||
|
overflow: 'hidden',
|
||||||
|
maxHeight: 47,
|
||||||
|
'border-bottom-color': map.BORDER_COLOR,
|
||||||
|
},
|
||||||
|
|
||||||
|
queryListItemKey: {
|
||||||
|
display: '-webkit-box',
|
||||||
|
boxOrient: 'vertical',
|
||||||
|
'-webkit-line-clamp': 2,
|
||||||
|
whiteSpace: 'normal',
|
||||||
|
overflow: 'hidden',
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: 'calc(100% - 70px)',
|
||||||
|
wordBreak: 'break-all',
|
||||||
|
margin: 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
queryListHeader: {
|
||||||
|
display: 'flex',
|
||||||
|
padding: 4,
|
||||||
|
flex: '0 0 auto',
|
||||||
|
'align-items': 'center',
|
||||||
|
'border-bottom-width': '1px',
|
||||||
|
'border-bottom-style': 'solid',
|
||||||
|
|
||||||
|
'border-color': map.LIST_BORDER_COLOR,
|
||||||
|
},
|
||||||
|
|
||||||
|
queryStatusWrapper: {
|
||||||
|
display: 'flex',
|
||||||
|
width: 'auto',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
margin: 0,
|
||||||
|
flex: '0 0 auto',
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
|
|
||||||
|
queryType: {
|
||||||
|
marginRight: 4,
|
||||||
|
},
|
||||||
|
|
||||||
|
queryStatus: {
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
height: 22,
|
||||||
|
padding: '0 6px',
|
||||||
|
'border-radius': '3px',
|
||||||
|
'font-size': '0.7em',
|
||||||
|
'line-height': '1em',
|
||||||
|
'flex-shrink': 0,
|
||||||
|
fontWeight: 700,
|
||||||
|
'background-color': map.ACTION_TIME_BACK_COLOR,
|
||||||
|
color: map.ACTION_TIME_COLOR,
|
||||||
|
},
|
||||||
|
|
||||||
|
queryListItemSelected: {
|
||||||
|
'background-color': map.SELECTED_BACKGROUND_COLOR,
|
||||||
|
},
|
||||||
|
|
||||||
|
tabSelector: {
|
||||||
|
display: 'flex',
|
||||||
|
width: '100%',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
overflow: 'hidden',
|
||||||
|
'& > *': {
|
||||||
|
flex: '0 1 auto',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
srOnly: {
|
||||||
|
position: 'absolute',
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
padding: 0,
|
||||||
|
margin: '-1px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
clip: 'rect(0,0,0,0)',
|
||||||
|
border: 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
selectorButton: {
|
||||||
|
cursor: 'pointer',
|
||||||
|
position: 'relative',
|
||||||
|
height: '33px',
|
||||||
|
padding: '0 8px',
|
||||||
|
display: 'inline-flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
color: map.TEXT_COLOR,
|
||||||
|
'border-style': 'solid',
|
||||||
|
'border-width': '1px',
|
||||||
|
'border-left-width': 0,
|
||||||
|
|
||||||
|
'&:first-child': {
|
||||||
|
'border-left-width': '1px',
|
||||||
|
'border-top-left-radius': '3px',
|
||||||
|
'border-bottom-left-radius': '3px',
|
||||||
|
},
|
||||||
|
|
||||||
|
'&:last-child': {
|
||||||
|
'border-top-right-radius': '3px',
|
||||||
|
'border-bottom-right-radius': '3px',
|
||||||
|
},
|
||||||
|
|
||||||
|
'background-color': map.TAB_BACK_COLOR,
|
||||||
|
|
||||||
|
'&:hover': {
|
||||||
|
'background-color': map.TAB_BACK_HOVER_COLOR,
|
||||||
|
},
|
||||||
|
|
||||||
|
'border-color': map.TAB_BORDER_COLOR,
|
||||||
|
|
||||||
|
'& > *': {
|
||||||
|
display: '-webkit-box',
|
||||||
|
boxOrient: 'vertical',
|
||||||
|
'-webkit-line-clamp': 1,
|
||||||
|
overflow: 'hidden',
|
||||||
|
wordBreak: 'break-all',
|
||||||
|
'-webkit-box-pack': 'end',
|
||||||
|
paddingBottom: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
selectorButtonSmall: {
|
||||||
|
padding: '0px 8px',
|
||||||
|
'font-size': '0.8em',
|
||||||
|
},
|
||||||
|
|
||||||
|
selectorButtonSelected: {
|
||||||
|
'background-color': map.TAB_BACK_SELECTED_COLOR,
|
||||||
|
},
|
||||||
|
|
||||||
|
sortButton: {
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
flexFlow: 'row nowrap',
|
||||||
|
cursor: 'pointer',
|
||||||
|
position: 'relative',
|
||||||
|
padding: '0 8px',
|
||||||
|
color: map.TEXT_COLOR,
|
||||||
|
borderStyle: 'solid',
|
||||||
|
borderWidth: '1px',
|
||||||
|
borderRadius: '3px',
|
||||||
|
backgroundColor: map.TAB_BACK_COLOR,
|
||||||
|
borderColor: map.TAB_BORDER_COLOR,
|
||||||
|
height: 30,
|
||||||
|
fontSize: 12,
|
||||||
|
width: 64,
|
||||||
|
|
||||||
|
'&:active': {
|
||||||
|
backgroundColor: map.TAB_BACK_SELECTED_COLOR,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleButton: {
|
||||||
|
width: '24px',
|
||||||
|
height: '24px',
|
||||||
|
display: 'inline-block',
|
||||||
|
flex: '0 0 auto',
|
||||||
|
color: map.TEXT_PLACEHOLDER_COLOR,
|
||||||
|
cursor: 'pointer',
|
||||||
|
padding: 0,
|
||||||
|
fontSize: '0.7em',
|
||||||
|
letterSpacing: '-0.7px',
|
||||||
|
outline: 'none',
|
||||||
|
boxShadow: 'none',
|
||||||
|
fontWeight: '700',
|
||||||
|
border: 'none',
|
||||||
|
|
||||||
|
'&:hover': {
|
||||||
|
color: map.TEXT_COLOR,
|
||||||
|
},
|
||||||
|
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
'&[aria-pressed="true"]': {
|
||||||
|
color: map.BACKGROUND_COLOR,
|
||||||
|
backgroundColor: map.TEXT_COLOR,
|
||||||
|
},
|
||||||
|
|
||||||
|
'&[data-type="error"]': {
|
||||||
|
color: map.TEXT_COLOR,
|
||||||
|
backgroundColor: map.TOGGLE_BUTTON_ERROR,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
queryForm: {
|
||||||
|
display: 'flex',
|
||||||
|
flexFlow: 'column nowrap',
|
||||||
|
},
|
||||||
|
sortBySection: {
|
||||||
|
display: 'flex',
|
||||||
|
padding: '0.4em',
|
||||||
|
'& label': {
|
||||||
|
display: 'flex',
|
||||||
|
flex: '0 0 auto',
|
||||||
|
whiteSpace: 'noWrap',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingRight: '0.4em',
|
||||||
|
},
|
||||||
|
|
||||||
|
'& > :last-child': {
|
||||||
|
flex: '0 0 auto',
|
||||||
|
marginLeft: '0.4em',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
querySearch: {
|
||||||
|
maxWidth: '65%',
|
||||||
|
'background-color': map.BACKGROUND_COLOR,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
flexFlow: 'row nowrap',
|
||||||
|
flex: '1 1 auto',
|
||||||
|
paddingRight: 6,
|
||||||
|
'& input': {
|
||||||
|
outline: 'none',
|
||||||
|
border: 'none',
|
||||||
|
width: '100%',
|
||||||
|
flex: '1 1 auto',
|
||||||
|
padding: '5px 10px',
|
||||||
|
'font-size': '1em',
|
||||||
|
position: 'relative',
|
||||||
|
fontFamily: 'monaco, Consolas, "Lucida Console", monospace',
|
||||||
|
|
||||||
|
'background-color': map.BACKGROUND_COLOR,
|
||||||
|
color: map.TEXT_COLOR,
|
||||||
|
|
||||||
|
'&::-webkit-input-placeholder': {
|
||||||
|
color: map.TEXT_PLACEHOLDER_COLOR,
|
||||||
|
},
|
||||||
|
|
||||||
|
'&::-moz-placeholder': {
|
||||||
|
color: map.TEXT_PLACEHOLDER_COLOR,
|
||||||
|
},
|
||||||
|
'&::-webkit-search-cancel-button': appearanceNone,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
closeButton: {
|
||||||
|
...appearanceNone,
|
||||||
|
border: 'none',
|
||||||
|
outline: 'none',
|
||||||
|
boxShadow: 'none',
|
||||||
|
display: 'block',
|
||||||
|
flex: '0 0 auto',
|
||||||
|
cursor: 'pointer',
|
||||||
|
background: 'transparent',
|
||||||
|
position: 'relative',
|
||||||
|
fontSize: 'inherit',
|
||||||
|
'&[data-invisible="1"]': {
|
||||||
|
visibility: 'hidden !important',
|
||||||
|
},
|
||||||
|
'&::after': {
|
||||||
|
content: '"\u00d7"',
|
||||||
|
display: 'block',
|
||||||
|
padding: 4,
|
||||||
|
fontSize: '1.2em',
|
||||||
|
color: map.TEXT_PLACEHOLDER_COLOR,
|
||||||
|
background: 'transparent',
|
||||||
|
},
|
||||||
|
'&:hover::after': {
|
||||||
|
color: map.TEXT_COLOR,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
noApiFound: {
|
||||||
|
width: '100%',
|
||||||
|
textAlign: 'center',
|
||||||
|
color: map.TEXT_COLOR,
|
||||||
|
padding: '1.4em',
|
||||||
|
'& a': {
|
||||||
|
fontSize: 'inherit',
|
||||||
|
color: map.TEXT_COLOR,
|
||||||
|
textDecoration: 'underline',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
searchSelectLabel: {
|
||||||
|
display: 'inline-block',
|
||||||
|
padding: 4,
|
||||||
|
borderLeft: '1px solid currentColor',
|
||||||
|
},
|
||||||
|
|
||||||
|
queryPreview: {
|
||||||
|
flex: '1 1 50%',
|
||||||
|
overflowX: 'hidden',
|
||||||
|
oveflowY: 'auto',
|
||||||
|
display: 'flex',
|
||||||
|
'flex-direction': 'column',
|
||||||
|
'overflow-y': 'hidden',
|
||||||
|
'& pre': {
|
||||||
|
border: 'inherit',
|
||||||
|
'border-radius': '3px',
|
||||||
|
'line-height': 'inherit',
|
||||||
|
color: 'inherit',
|
||||||
|
},
|
||||||
|
|
||||||
|
'background-color': map.BACKGROUND_COLOR,
|
||||||
|
},
|
||||||
|
|
||||||
|
previewHeader: {
|
||||||
|
flex: '0 0 30px',
|
||||||
|
padding: '5px 4px',
|
||||||
|
'align-items': 'center',
|
||||||
|
'border-bottom-width': '1px',
|
||||||
|
'border-bottom-style': 'solid',
|
||||||
|
|
||||||
|
'background-color': map.HEADER_BACKGROUND_COLOR,
|
||||||
|
'border-bottom-color': map.HEADER_BORDER_COLOR,
|
||||||
|
},
|
||||||
|
|
||||||
|
treeItemPin: {
|
||||||
|
'font-size': '0.7em',
|
||||||
|
'padding-left': '5px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
'&:hover': {
|
||||||
|
'text-decoration': 'underline',
|
||||||
|
},
|
||||||
|
|
||||||
|
color: map.PIN_COLOR,
|
||||||
|
},
|
||||||
|
|
||||||
|
treeItemKey: {
|
||||||
|
color: map.TEXT_PLACEHOLDER_COLOR,
|
||||||
|
},
|
||||||
|
|
||||||
|
treeWrapper: {
|
||||||
|
overflowX: 'auto',
|
||||||
|
overflowY: 'auto',
|
||||||
|
padding: '0.5em 1em',
|
||||||
|
},
|
||||||
|
|
||||||
|
tabContent: {
|
||||||
|
display: 'block',
|
||||||
|
overflowY: 'auto',
|
||||||
|
padding: '0.5em 0',
|
||||||
|
color: map.TAB_CONTENT_COLOR,
|
||||||
|
'& h2': {
|
||||||
|
color: map.ULIST_STRONG_COLOR,
|
||||||
|
padding: '0.5em 1em',
|
||||||
|
fontWeight: 700,
|
||||||
|
},
|
||||||
|
'& h3': {
|
||||||
|
color: map.ULIST_STRONG_COLOR,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
uList: {
|
||||||
|
listStyle: 'none',
|
||||||
|
padding: '0 0 0 1em',
|
||||||
|
color: map.ULIST_COLOR,
|
||||||
|
'& > li': {
|
||||||
|
listStyle: 'none',
|
||||||
|
},
|
||||||
|
'& > li::before': {
|
||||||
|
content: '"\\2022"',
|
||||||
|
display: 'inline-block',
|
||||||
|
paddingRight: '0.5em',
|
||||||
|
color: map.ULIST_DISC_COLOR,
|
||||||
|
fontSize: '0.8em',
|
||||||
|
},
|
||||||
|
|
||||||
|
'& strong': {
|
||||||
|
color: map.ULIST_STRONG_COLOR,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
let themeSheet: StyleSheet;
|
||||||
|
|
||||||
|
const getDefaultThemeStyling = (theme: reduxThemes.Base16Theme) => {
|
||||||
|
if (themeSheet) {
|
||||||
|
themeSheet.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
themeSheet = jss
|
||||||
|
.createStyleSheet(getSheetFromColorMap(colorMap(theme)))
|
||||||
|
.attach();
|
||||||
|
|
||||||
|
return themeSheet.classes;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createStylingFromTheme = createStyling(getDefaultThemeStyling, {
|
||||||
|
defaultBase16: reduxThemes.nicinabox,
|
||||||
|
base16Themes: { ...reduxThemes },
|
||||||
|
});
|
||||||
|
|
||||||
|
export function createThemeState<S, A extends Action<unknown>>(
|
||||||
|
props: RtkQueryMonitorProps<S, A>
|
||||||
|
): StyleUtils {
|
||||||
|
const base16Theme =
|
||||||
|
getBase16Theme(props.theme, { ...reduxThemes }) ?? reduxThemes.nicinabox;
|
||||||
|
|
||||||
|
const theme = props.invertTheme ? invertTheme(props.theme) : props.theme;
|
||||||
|
const styling = createStylingFromTheme(theme);
|
||||||
|
|
||||||
|
return { base16Theme, styling, invertTheme: !!props.invertTheme };
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockStyling = () => ({ className: '', style: {} });
|
||||||
|
|
||||||
|
export const StyleUtilsContext = createContext<StyleUtils>({
|
||||||
|
base16Theme: reduxThemes.nicinabox,
|
||||||
|
invertTheme: false,
|
||||||
|
styling: mockStyling,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function getJsonTreeTheme(
|
||||||
|
base16Theme: reduxThemes.Base16Theme
|
||||||
|
): StylingConfig {
|
||||||
|
return {
|
||||||
|
extend: base16Theme,
|
||||||
|
nestedNode: ({ style }, keyPath, nodeType, expanded) => ({
|
||||||
|
style: {
|
||||||
|
...style,
|
||||||
|
whiteSpace: expanded ? 'inherit' : 'nowrap',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
nestedNodeItemString: ({ style }, keyPath, nodeType, expanded) => ({
|
||||||
|
style: {
|
||||||
|
...style,
|
||||||
|
display: expanded ? 'none' : 'inline',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
106
packages/redux-devtools-rtk-query-monitor/src/styles/tree.tsx
Normal file
106
packages/redux-devtools-rtk-query-monitor/src/styles/tree.tsx
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import { StylingFunction } from 'react-base16-styling';
|
||||||
|
import { isCollection, isIndexed, isKeyed } from 'immutable';
|
||||||
|
import isIterable from '../utils/isIterable';
|
||||||
|
|
||||||
|
const IS_IMMUTABLE_KEY = '@@__IS_IMMUTABLE__@@';
|
||||||
|
|
||||||
|
function isImmutable(value: unknown) {
|
||||||
|
return isKeyed(value) || isIndexed(value) || isCollection(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getShortTypeString(val: unknown, diff: boolean | undefined) {
|
||||||
|
if (diff && Array.isArray(val)) {
|
||||||
|
val = val[val.length === 2 ? 1 : 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isIterable(val) && !isImmutable(val)) {
|
||||||
|
return '(…)';
|
||||||
|
} else if (Array.isArray(val)) {
|
||||||
|
return val.length > 0 ? '[…]' : '[]';
|
||||||
|
} else if (val === null) {
|
||||||
|
return 'null';
|
||||||
|
} else if (val === undefined) {
|
||||||
|
return 'undef';
|
||||||
|
} else if (typeof val === 'object') {
|
||||||
|
return Object.keys(val as Record<string, unknown>).length > 0
|
||||||
|
? '{…}'
|
||||||
|
: '{}';
|
||||||
|
} else if (typeof val === 'function') {
|
||||||
|
return 'fn';
|
||||||
|
} else if (typeof val === 'string') {
|
||||||
|
return `"${val.substr(0, 10) + (val.length > 10 ? '…' : '')}"`;
|
||||||
|
} else if (typeof val === 'symbol') {
|
||||||
|
return 'symbol';
|
||||||
|
} else {
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getText(
|
||||||
|
type: string,
|
||||||
|
data: any,
|
||||||
|
previewContent: boolean,
|
||||||
|
isDiff: boolean | undefined
|
||||||
|
) {
|
||||||
|
if (type === 'Object') {
|
||||||
|
const keys = Object.keys(data);
|
||||||
|
if (!previewContent) return keys.length ? '{…}' : '{}';
|
||||||
|
|
||||||
|
const str = keys
|
||||||
|
.slice(0, 3)
|
||||||
|
.map(
|
||||||
|
(key) => `${key}: ${getShortTypeString(data[key], isDiff) as string}`
|
||||||
|
)
|
||||||
|
.concat(keys.length > 3 ? ['…'] : [])
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
|
return `{ ${str} }`;
|
||||||
|
} else if (type === 'Array') {
|
||||||
|
if (!previewContent) return data.length ? '[…]' : '[]';
|
||||||
|
|
||||||
|
const str = data
|
||||||
|
.slice(0, 4)
|
||||||
|
.map((val: any) => getShortTypeString(val, isDiff))
|
||||||
|
.concat(data.length > 4 ? ['…'] : [])
|
||||||
|
.join(', ');
|
||||||
|
|
||||||
|
return `[${str as string}]`;
|
||||||
|
} else {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getItemString(
|
||||||
|
styling: StylingFunction,
|
||||||
|
type: string,
|
||||||
|
data: any,
|
||||||
|
dataTypeKey: string | symbol | undefined,
|
||||||
|
previewContent: boolean,
|
||||||
|
isDiff?: boolean
|
||||||
|
): ReactNode {
|
||||||
|
return (
|
||||||
|
<span {...styling('treeItemHint')}>
|
||||||
|
{data[IS_IMMUTABLE_KEY] ? 'Immutable' : ''}
|
||||||
|
{dataTypeKey && data[dataTypeKey]
|
||||||
|
? `${data[dataTypeKey] as string} `
|
||||||
|
: ''}
|
||||||
|
{getText(type, data, previewContent, isDiff)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createTreeItemLabelRenderer(styling: StylingFunction) {
|
||||||
|
return function labelRenderer(
|
||||||
|
[key]: (string | number)[],
|
||||||
|
nodeType: string,
|
||||||
|
expanded: boolean
|
||||||
|
): ReactNode {
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
<span {...styling('treeItemKey')}>{key}</span>
|
||||||
|
{!expanded && ': '}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
179
packages/redux-devtools-rtk-query-monitor/src/types.ts
Normal file
179
packages/redux-devtools-rtk-query-monitor/src/types.ts
Normal file
|
@ -0,0 +1,179 @@
|
||||||
|
import type { LiftedAction, LiftedState } from '@redux-devtools/instrument';
|
||||||
|
import type { createApi, QueryStatus } from '@reduxjs/toolkit/query';
|
||||||
|
import type { Action, AnyAction, Dispatch } from '@reduxjs/toolkit';
|
||||||
|
import type { ComponentType } from 'react';
|
||||||
|
import type { Base16Theme, StylingFunction } from 'react-base16-styling';
|
||||||
|
import type * as themes from 'redux-devtools-themes';
|
||||||
|
import type { QueryComparators } from './utils/comparators';
|
||||||
|
import type { QueryFilters } from './utils/filters';
|
||||||
|
|
||||||
|
export enum QueryPreviewTabs {
|
||||||
|
queryinfo,
|
||||||
|
apiConfig,
|
||||||
|
querySubscriptions,
|
||||||
|
queryTags,
|
||||||
|
actions,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QueryFormValues {
|
||||||
|
queryComparator: QueryComparators;
|
||||||
|
isAscendingQueryComparatorOrder: boolean;
|
||||||
|
searchValue: string;
|
||||||
|
isRegexSearch: boolean;
|
||||||
|
queryFilter: QueryFilters;
|
||||||
|
}
|
||||||
|
export interface RtkQueryMonitorState {
|
||||||
|
readonly queryForm: {
|
||||||
|
values: QueryFormValues;
|
||||||
|
};
|
||||||
|
readonly selectedQueryKey: Pick<QueryInfo, 'reducerPath' | 'queryKey'> | null;
|
||||||
|
readonly selectedPreviewTab: QueryPreviewTabs;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RtkQueryMonitorProps<S, A extends Action<unknown>>
|
||||||
|
extends LiftedState<S, A, RtkQueryMonitorState> {
|
||||||
|
dispatch: Dispatch<Action | LiftedAction<S, A, RtkQueryMonitorState>>;
|
||||||
|
theme: keyof typeof themes | Base16Theme;
|
||||||
|
invertTheme?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RtkQueryApiState = ReturnType<
|
||||||
|
ReturnType<typeof createApi>['reducer']
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type RtkQueryState = NonNullable<
|
||||||
|
RtkQueryApiState['queries'][keyof RtkQueryApiState]
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type RtkMutationState = NonNullable<
|
||||||
|
RtkQueryApiState['mutations'][keyof RtkQueryApiState]
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type RtkQueryApiConfig = RtkQueryApiState['config'];
|
||||||
|
|
||||||
|
export type RtkQueryProvided = RtkQueryApiState['provided'];
|
||||||
|
|
||||||
|
export interface ExternalProps<S, A extends Action<unknown>> {
|
||||||
|
dispatch: Dispatch<Action | LiftedAction<S, A, RtkQueryMonitorState>>;
|
||||||
|
theme: keyof typeof themes | Base16Theme;
|
||||||
|
hideMainButtons?: boolean;
|
||||||
|
invertTheme: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QueryInfo {
|
||||||
|
type: 'query';
|
||||||
|
state: RtkQueryState;
|
||||||
|
queryKey: string;
|
||||||
|
reducerPath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MutationInfo {
|
||||||
|
type: 'mutation';
|
||||||
|
state: RtkMutationState;
|
||||||
|
queryKey: string;
|
||||||
|
reducerPath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RtkResourceInfo = QueryInfo | MutationInfo;
|
||||||
|
|
||||||
|
export interface ApiInfo {
|
||||||
|
reducerPath: string;
|
||||||
|
apiState: RtkQueryApiState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectOption<
|
||||||
|
T = string,
|
||||||
|
VisConfig extends string = 'default'
|
||||||
|
> {
|
||||||
|
label: string;
|
||||||
|
value: T;
|
||||||
|
visible?: Record<VisConfig | 'default', boolean> | boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectorsSource<S> {
|
||||||
|
userState: S | null;
|
||||||
|
monitorState: RtkQueryMonitorState;
|
||||||
|
currentStateIndex: number;
|
||||||
|
actionsById: LiftedState<unknown, AnyAction, unknown>['actionsById'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StyleUtils {
|
||||||
|
readonly base16Theme: Base16Theme;
|
||||||
|
readonly styling: StylingFunction;
|
||||||
|
readonly invertTheme: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RTKQuerySubscribers = NonNullable<
|
||||||
|
RtkQueryApiState['subscriptions'][keyof RtkQueryApiState['subscriptions']]
|
||||||
|
>;
|
||||||
|
|
||||||
|
export interface RtkQueryTag {
|
||||||
|
type: string;
|
||||||
|
id?: number | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Tally {
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type QueryTally = {
|
||||||
|
[key in QueryStatus]?: number;
|
||||||
|
} &
|
||||||
|
Tally;
|
||||||
|
|
||||||
|
export interface RtkRequestTiming {
|
||||||
|
requestId: string;
|
||||||
|
queryKey: string;
|
||||||
|
endpointName: string;
|
||||||
|
startedAt: string;
|
||||||
|
completedAt: string;
|
||||||
|
duration: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QueryTimings {
|
||||||
|
readonly oldest: RtkRequestTiming | null;
|
||||||
|
readonly latest: RtkRequestTiming | null;
|
||||||
|
readonly slowest: RtkRequestTiming | null;
|
||||||
|
readonly fastest: RtkRequestTiming | null;
|
||||||
|
readonly average: string;
|
||||||
|
readonly median: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiTimings {
|
||||||
|
readonly queries: QueryTimings;
|
||||||
|
readonly mutations: QueryTimings;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiStats {
|
||||||
|
readonly timings: ApiTimings;
|
||||||
|
readonly tally: Readonly<{
|
||||||
|
subscriptions: number;
|
||||||
|
cachedQueries: QueryTally;
|
||||||
|
tagTypes: number;
|
||||||
|
cachedMutations: QueryTally;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TabOption<S, P, V extends string = 'default'>
|
||||||
|
extends SelectOption<S, V> {
|
||||||
|
component: ComponentType<P>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* It is Omit<RequestStatusFlags, 'status'> & { isFetching: boolean; }
|
||||||
|
*/
|
||||||
|
export interface RTKStatusFlags {
|
||||||
|
readonly isUninitialized: boolean;
|
||||||
|
readonly isFetching: boolean;
|
||||||
|
readonly isSuccess: boolean;
|
||||||
|
readonly isError: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RtkRequest = {
|
||||||
|
status: QueryStatus;
|
||||||
|
queryKey: string;
|
||||||
|
requestId: string;
|
||||||
|
endpointName: string;
|
||||||
|
startedTimeStamp?: number;
|
||||||
|
fulfilledTimeStamp?: number;
|
||||||
|
};
|
|
@ -0,0 +1,96 @@
|
||||||
|
import { QueryStatus } from '@reduxjs/toolkit/query';
|
||||||
|
import { RtkResourceInfo, SelectOption } from '../types';
|
||||||
|
|
||||||
|
export interface Comparator<T> {
|
||||||
|
(a: T, b: T): number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum QueryComparators {
|
||||||
|
fulfilledTimeStamp = 'timestamp',
|
||||||
|
queryKey = 'key',
|
||||||
|
status = 'status',
|
||||||
|
endpointName = 'endpointName',
|
||||||
|
apiReducerPath = 'apiReducerPath',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sortQueryOptions: SelectOption<QueryComparators>[] = [
|
||||||
|
{ label: 'last updated', value: QueryComparators.fulfilledTimeStamp },
|
||||||
|
{ label: 'query key', value: QueryComparators.queryKey },
|
||||||
|
{ label: 'status', value: QueryComparators.status },
|
||||||
|
{ label: 'endpoint', value: QueryComparators.endpointName },
|
||||||
|
{ label: 'reducerPath', value: QueryComparators.apiReducerPath },
|
||||||
|
];
|
||||||
|
|
||||||
|
function sortQueryByFulfilled(
|
||||||
|
thisQueryInfo: RtkResourceInfo,
|
||||||
|
thatQueryInfo: RtkResourceInfo
|
||||||
|
): number {
|
||||||
|
const thisFulfilled = thisQueryInfo.state.fulfilledTimeStamp ?? -1;
|
||||||
|
const thatFulfilled = thatQueryInfo.state.fulfilledTimeStamp ?? -1;
|
||||||
|
|
||||||
|
return thisFulfilled - thatFulfilled;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStatusToFactor = {
|
||||||
|
[QueryStatus.uninitialized]: 1,
|
||||||
|
[QueryStatus.pending]: 2,
|
||||||
|
[QueryStatus.rejected]: 3,
|
||||||
|
[QueryStatus.fulfilled]: 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
function sortQueryByStatus(
|
||||||
|
thisQueryInfo: RtkResourceInfo,
|
||||||
|
thatQueryInfo: RtkResourceInfo
|
||||||
|
): number {
|
||||||
|
const thisTerm = mapStatusToFactor[thisQueryInfo.state.status] || -1;
|
||||||
|
const thatTerm = mapStatusToFactor[thatQueryInfo.state.status] || -1;
|
||||||
|
|
||||||
|
return thisTerm - thatTerm;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function compareJSONPrimitive<
|
||||||
|
T extends string | number | boolean | null
|
||||||
|
>(a: T, b: T): number {
|
||||||
|
if (a === b) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return a > b ? 1 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortByQueryKey(
|
||||||
|
thisQueryInfo: RtkResourceInfo,
|
||||||
|
thatQueryInfo: RtkResourceInfo
|
||||||
|
): number {
|
||||||
|
return compareJSONPrimitive(thisQueryInfo.queryKey, thatQueryInfo.queryKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortQueryByEndpointName(
|
||||||
|
thisQueryInfo: RtkResourceInfo,
|
||||||
|
thatQueryInfo: RtkResourceInfo
|
||||||
|
): number {
|
||||||
|
const thisEndpointName = thisQueryInfo.state.endpointName ?? '';
|
||||||
|
const thatEndpointName = thatQueryInfo.state.endpointName ?? '';
|
||||||
|
|
||||||
|
return compareJSONPrimitive(thisEndpointName, thatEndpointName);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortByApiReducerPath(
|
||||||
|
thisQueryInfo: RtkResourceInfo,
|
||||||
|
thatQueryInfo: RtkResourceInfo
|
||||||
|
): number {
|
||||||
|
return compareJSONPrimitive(
|
||||||
|
thisQueryInfo.reducerPath,
|
||||||
|
thatQueryInfo.reducerPath
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const queryComparators: Readonly<
|
||||||
|
Record<QueryComparators, Comparator<RtkResourceInfo>>
|
||||||
|
> = {
|
||||||
|
[QueryComparators.fulfilledTimeStamp]: sortQueryByFulfilled,
|
||||||
|
[QueryComparators.status]: sortQueryByStatus,
|
||||||
|
[QueryComparators.endpointName]: sortQueryByEndpointName,
|
||||||
|
[QueryComparators.queryKey]: sortByQueryKey,
|
||||||
|
[QueryComparators.apiReducerPath]: sortByApiReducerPath,
|
||||||
|
};
|
|
@ -0,0 +1,78 @@
|
||||||
|
import { RtkResourceInfo, SelectOption } from '../types';
|
||||||
|
|
||||||
|
export interface FilterList<T> {
|
||||||
|
(regex: RegExp | null, list: T[]): T[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum QueryFilters {
|
||||||
|
queryKey = 'query key',
|
||||||
|
reducerPath = 'reducerPath',
|
||||||
|
endpointName = 'endpoint',
|
||||||
|
status = 'status',
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterByQueryKey(
|
||||||
|
regex: RegExp | null,
|
||||||
|
list: RtkResourceInfo[]
|
||||||
|
): RtkResourceInfo[] {
|
||||||
|
if (!regex) {
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
return list.filter((RtkResourceInfo) => regex.test(RtkResourceInfo.queryKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterByReducerPath(
|
||||||
|
regex: RegExp | null,
|
||||||
|
list: RtkResourceInfo[]
|
||||||
|
): RtkResourceInfo[] {
|
||||||
|
if (!regex) {
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
return list.filter((RtkResourceInfo) =>
|
||||||
|
regex.test(RtkResourceInfo.reducerPath)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterByEndpointName(
|
||||||
|
regex: RegExp | null,
|
||||||
|
list: RtkResourceInfo[]
|
||||||
|
): RtkResourceInfo[] {
|
||||||
|
if (!regex) {
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
return list.filter((RtkResourceInfo) =>
|
||||||
|
regex.test(RtkResourceInfo.state.endpointName ?? 'undefined')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterByStatus(
|
||||||
|
regex: RegExp | null,
|
||||||
|
list: RtkResourceInfo[]
|
||||||
|
): RtkResourceInfo[] {
|
||||||
|
if (!regex) {
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
return list.filter((RtkResourceInfo) =>
|
||||||
|
regex.test(RtkResourceInfo.state.status)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const filterQueryOptions: SelectOption<QueryFilters>[] = [
|
||||||
|
{ label: 'query key', value: QueryFilters.queryKey },
|
||||||
|
{ label: 'reducerPath', value: QueryFilters.reducerPath },
|
||||||
|
{ label: 'status', value: QueryFilters.status },
|
||||||
|
{ label: 'endpoint', value: QueryFilters.endpointName },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const queryListFilters: Readonly<
|
||||||
|
Record<QueryFilters, FilterList<RtkResourceInfo>>
|
||||||
|
> = {
|
||||||
|
[QueryFilters.queryKey]: filterByQueryKey,
|
||||||
|
[QueryFilters.endpointName]: filterByEndpointName,
|
||||||
|
[QueryFilters.reducerPath]: filterByReducerPath,
|
||||||
|
[QueryFilters.status]: filterByStatus,
|
||||||
|
};
|
|
@ -0,0 +1,31 @@
|
||||||
|
export function formatMs(milliseconds: number): string {
|
||||||
|
if (!Number.isFinite(milliseconds)) {
|
||||||
|
return 'NaN';
|
||||||
|
}
|
||||||
|
|
||||||
|
const absInput = Math.abs(Math.round(milliseconds));
|
||||||
|
let millis = (absInput % 1000).toString();
|
||||||
|
|
||||||
|
if (millis.length < 3) {
|
||||||
|
if (millis.length === 2) {
|
||||||
|
millis = '0' + millis;
|
||||||
|
} else {
|
||||||
|
millis = '00' + millis;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const seconds = Math.floor(absInput / 1_000) % 60;
|
||||||
|
const minutes = Math.floor(absInput / 60_000);
|
||||||
|
|
||||||
|
let output = `${seconds}.${millis}s`;
|
||||||
|
|
||||||
|
if (minutes > 0) {
|
||||||
|
output = `${minutes}m${output}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (milliseconds < 0) {
|
||||||
|
output = `-${output}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
export default function isIterable(obj: unknown): boolean {
|
||||||
|
return (
|
||||||
|
obj !== null &&
|
||||||
|
typeof obj === 'object' &&
|
||||||
|
!Array.isArray(obj) &&
|
||||||
|
typeof (obj as Record<string | typeof Symbol.iterator, unknown>)[
|
||||||
|
window.Symbol.iterator
|
||||||
|
] === 'function'
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { freeze } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
export const emptyArray = freeze([]);
|
||||||
|
|
||||||
|
export const emptyRecord = freeze({});
|
||||||
|
|
||||||
|
export function identity<T>(val: T): T {
|
||||||
|
return val;
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
// https://stackoverflow.com/a/9310752
|
||||||
|
export function escapeRegExpSpecialCharacter(text: string): string {
|
||||||
|
return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
|
||||||
|
}
|
666
packages/redux-devtools-rtk-query-monitor/src/utils/rtk-query.ts
Normal file
666
packages/redux-devtools-rtk-query-monitor/src/utils/rtk-query.ts
Normal file
|
@ -0,0 +1,666 @@
|
||||||
|
import { Action, AnyAction, isAllOf, isPlainObject } from '@reduxjs/toolkit';
|
||||||
|
import { QueryStatus } from '@reduxjs/toolkit/query';
|
||||||
|
import {
|
||||||
|
QueryInfo,
|
||||||
|
RtkQueryMonitorState,
|
||||||
|
RtkQueryApiState,
|
||||||
|
RTKQuerySubscribers,
|
||||||
|
RtkQueryTag,
|
||||||
|
RTKStatusFlags,
|
||||||
|
RtkQueryState,
|
||||||
|
MutationInfo,
|
||||||
|
ApiStats,
|
||||||
|
QueryTally,
|
||||||
|
RtkQueryProvided,
|
||||||
|
ApiTimings,
|
||||||
|
QueryTimings,
|
||||||
|
SelectorsSource,
|
||||||
|
RtkMutationState,
|
||||||
|
RtkResourceInfo,
|
||||||
|
RtkRequest,
|
||||||
|
RtkRequestTiming,
|
||||||
|
} from '../types';
|
||||||
|
import { missingTagId } from '../monitor-config';
|
||||||
|
import { Comparator, compareJSONPrimitive } from './comparators';
|
||||||
|
import { emptyArray } from './object';
|
||||||
|
import { formatMs } from './formatters';
|
||||||
|
import * as statistics from './statistics';
|
||||||
|
|
||||||
|
const rtkqueryApiStateKeys: ReadonlyArray<keyof RtkQueryApiState> = [
|
||||||
|
'queries',
|
||||||
|
'mutations',
|
||||||
|
'config',
|
||||||
|
'provided',
|
||||||
|
'subscriptions',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard used to select apis from the user store state.
|
||||||
|
* @param val
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function isApiSlice(val: unknown): val is RtkQueryApiState {
|
||||||
|
if (!isPlainObject(val)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0, len = rtkqueryApiStateKeys.length; i < len; i++) {
|
||||||
|
if (
|
||||||
|
!isPlainObject((val as Record<string, unknown>)[rtkqueryApiStateKeys[i]])
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indexes api states by their `reducerPath`.
|
||||||
|
*
|
||||||
|
* Returns `null` if there are no api slice or `reduxStoreState`
|
||||||
|
* is not an object.
|
||||||
|
*
|
||||||
|
* @param reduxStoreState
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function getApiStatesOf(
|
||||||
|
reduxStoreState: unknown
|
||||||
|
): null | Readonly<Record<string, RtkQueryApiState>> {
|
||||||
|
if (!isPlainObject(reduxStoreState)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const output: null | Record<string, RtkQueryApiState> = {};
|
||||||
|
const keys = Object.keys(reduxStoreState);
|
||||||
|
|
||||||
|
for (let i = 0, len = keys.length; i < len; i++) {
|
||||||
|
const key = keys[i];
|
||||||
|
const value = (reduxStoreState as Record<string, unknown>)[key];
|
||||||
|
|
||||||
|
if (isApiSlice(value)) {
|
||||||
|
output[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(output).length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractAllApiQueries(
|
||||||
|
apiStatesByReducerPath: null | Readonly<Record<string, RtkQueryApiState>>
|
||||||
|
): ReadonlyArray<QueryInfo> {
|
||||||
|
if (!apiStatesByReducerPath) {
|
||||||
|
return emptyArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reducerPaths = Object.keys(apiStatesByReducerPath);
|
||||||
|
|
||||||
|
const output: QueryInfo[] = [];
|
||||||
|
|
||||||
|
for (let i = 0, len = reducerPaths.length; i < len; i++) {
|
||||||
|
const reducerPath = reducerPaths[i];
|
||||||
|
const api = apiStatesByReducerPath[reducerPath];
|
||||||
|
const queryKeys = Object.keys(api.queries);
|
||||||
|
|
||||||
|
for (let j = 0, qKeysLen = queryKeys.length; j < qKeysLen; j++) {
|
||||||
|
const queryKey = queryKeys[j];
|
||||||
|
const state = api.queries[queryKey];
|
||||||
|
|
||||||
|
if (state) {
|
||||||
|
output.push({
|
||||||
|
type: 'query',
|
||||||
|
reducerPath,
|
||||||
|
queryKey,
|
||||||
|
state,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractAllApiMutations(
|
||||||
|
apiStatesByReducerPath: null | Readonly<Record<string, RtkQueryApiState>>
|
||||||
|
): ReadonlyArray<MutationInfo> {
|
||||||
|
if (!apiStatesByReducerPath) {
|
||||||
|
return emptyArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reducerPaths = Object.keys(apiStatesByReducerPath);
|
||||||
|
const output: MutationInfo[] = [];
|
||||||
|
|
||||||
|
for (let i = 0, len = reducerPaths.length; i < len; i++) {
|
||||||
|
const reducerPath = reducerPaths[i];
|
||||||
|
const api = apiStatesByReducerPath[reducerPath];
|
||||||
|
const mutationKeys = Object.keys(api.mutations);
|
||||||
|
|
||||||
|
for (let j = 0, mKeysLen = mutationKeys.length; j < mKeysLen; j++) {
|
||||||
|
const queryKey = mutationKeys[j];
|
||||||
|
const state = api.mutations[queryKey];
|
||||||
|
|
||||||
|
if (state) {
|
||||||
|
output.push({
|
||||||
|
type: 'mutation',
|
||||||
|
reducerPath,
|
||||||
|
queryKey,
|
||||||
|
state,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeQueryTallyOf(
|
||||||
|
queryState: RtkQueryApiState['queries'] | RtkQueryApiState['mutations']
|
||||||
|
): QueryTally {
|
||||||
|
const queries = Object.values(queryState);
|
||||||
|
|
||||||
|
const output: QueryTally = {
|
||||||
|
count: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let i = 0, len = queries.length; i < len; i++) {
|
||||||
|
const query = queries[i];
|
||||||
|
|
||||||
|
if (query) {
|
||||||
|
output.count++;
|
||||||
|
|
||||||
|
if (!output[query.status]) {
|
||||||
|
output[query.status] = 1;
|
||||||
|
} else {
|
||||||
|
(output[query.status] as number)++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
function tallySubscriptions(
|
||||||
|
subsState: RtkQueryApiState['subscriptions']
|
||||||
|
): number {
|
||||||
|
const subsOfQueries = Object.values(subsState);
|
||||||
|
|
||||||
|
let output = 0;
|
||||||
|
|
||||||
|
for (let i = 0, len = subsOfQueries.length; i < len; i++) {
|
||||||
|
const subsOfQuery = subsOfQueries[i];
|
||||||
|
|
||||||
|
if (subsOfQuery) {
|
||||||
|
output += Object.keys(subsOfQuery).length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeRtkQueryRequests(
|
||||||
|
type: 'queries' | 'mutations',
|
||||||
|
api: RtkQueryApiState,
|
||||||
|
sortedActions: AnyAction[],
|
||||||
|
currentStateIndex: SelectorsSource<unknown>['currentStateIndex']
|
||||||
|
): Readonly<Record<string, RtkRequest>> {
|
||||||
|
const requestById: Record<string, RtkRequest> = {};
|
||||||
|
|
||||||
|
const matcher =
|
||||||
|
type === 'queries'
|
||||||
|
? matchesExecuteQuery(api.config.reducerPath)
|
||||||
|
: matchesExecuteMutation(api.config.reducerPath);
|
||||||
|
|
||||||
|
for (
|
||||||
|
let i = 0, len = sortedActions.length;
|
||||||
|
i < len && i <= currentStateIndex;
|
||||||
|
i++
|
||||||
|
) {
|
||||||
|
const action = sortedActions[i];
|
||||||
|
|
||||||
|
if (matcher(action)) {
|
||||||
|
let requestRecord: RtkRequest | undefined =
|
||||||
|
requestById[action.meta.requestId];
|
||||||
|
|
||||||
|
if (!requestRecord) {
|
||||||
|
const queryCacheKey: string | undefined = (
|
||||||
|
action.meta as Record<string, any>
|
||||||
|
)?.arg?.queryCacheKey;
|
||||||
|
|
||||||
|
const queryKey =
|
||||||
|
typeof queryCacheKey === 'string'
|
||||||
|
? queryCacheKey
|
||||||
|
: action.meta.requestId;
|
||||||
|
|
||||||
|
const endpointName: string =
|
||||||
|
(action.meta as any)?.arg?.endpointName ?? '-';
|
||||||
|
|
||||||
|
requestById[action.meta.requestId] = requestRecord = {
|
||||||
|
queryKey,
|
||||||
|
requestId: action.meta.requestId,
|
||||||
|
endpointName,
|
||||||
|
status: action.meta.requestStatus,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
requestRecord.status = action.meta.requestStatus;
|
||||||
|
|
||||||
|
if (
|
||||||
|
action.meta.requestStatus === QueryStatus.pending &&
|
||||||
|
typeof (action.meta as any).startedTimeStamp === 'number'
|
||||||
|
) {
|
||||||
|
requestRecord.startedTimeStamp = (action.meta as any).startedTimeStamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
action.meta.requestStatus === QueryStatus.fulfilled &&
|
||||||
|
typeof (action.meta as any).fulfilledTimeStamp === 'number'
|
||||||
|
) {
|
||||||
|
requestRecord.fulfilledTimeStamp = (
|
||||||
|
action.meta as any
|
||||||
|
).fulfilledTimeStamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestIds = Object.keys(requestById);
|
||||||
|
|
||||||
|
// Patch queries that have pending actions that are committed
|
||||||
|
for (let i = 0, len = requestIds.length; i < len; i++) {
|
||||||
|
const requestId = requestIds[i];
|
||||||
|
const request = requestById[requestId];
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof request.startedTimeStamp === 'undefined' &&
|
||||||
|
typeof request.fulfilledTimeStamp === 'number'
|
||||||
|
) {
|
||||||
|
const startedTimeStampFromCache =
|
||||||
|
api[type][request.queryKey]?.startedTimeStamp;
|
||||||
|
|
||||||
|
if (typeof startedTimeStampFromCache === 'number') {
|
||||||
|
request.startedTimeStamp = startedTimeStampFromCache;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add queries that have pending and fulfilled actions committed
|
||||||
|
const queryCacheEntries = Object.entries(api[type] ?? {});
|
||||||
|
|
||||||
|
for (let i = 0, len = queryCacheEntries.length; i < len; i++) {
|
||||||
|
const [queryCacheKey, queryCache] = queryCacheEntries[i];
|
||||||
|
const requestId: string =
|
||||||
|
type === 'queries'
|
||||||
|
? (queryCache as typeof api['queries'][string])?.requestId ?? ''
|
||||||
|
: queryCacheKey;
|
||||||
|
if (
|
||||||
|
queryCache &&
|
||||||
|
!Object.prototype.hasOwnProperty.call(requestById, requestId)
|
||||||
|
) {
|
||||||
|
const startedTimeStamp = queryCache?.startedTimeStamp;
|
||||||
|
const fulfilledTimeStamp = queryCache?.fulfilledTimeStamp;
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof startedTimeStamp === 'number' &&
|
||||||
|
typeof fulfilledTimeStamp === 'number'
|
||||||
|
) {
|
||||||
|
requestById[requestId] = {
|
||||||
|
queryKey: queryCacheKey,
|
||||||
|
requestId,
|
||||||
|
endpointName: queryCache.endpointName ?? '',
|
||||||
|
startedTimeStamp,
|
||||||
|
fulfilledTimeStamp,
|
||||||
|
status: queryCache.status,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestById;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatRtkRequest(
|
||||||
|
rtkRequest: RtkRequest | null
|
||||||
|
): RtkRequestTiming | null {
|
||||||
|
if (!rtkRequest) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fulfilledTimeStamp = rtkRequest.fulfilledTimeStamp;
|
||||||
|
const startedTimeStamp = rtkRequest.startedTimeStamp;
|
||||||
|
|
||||||
|
const output: RtkRequestTiming = {
|
||||||
|
queryKey: rtkRequest.queryKey,
|
||||||
|
requestId: rtkRequest.requestId,
|
||||||
|
endpointName: rtkRequest.endpointName,
|
||||||
|
startedAt: '-',
|
||||||
|
completedAt: '-',
|
||||||
|
duration: '-',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof fulfilledTimeStamp === 'number' &&
|
||||||
|
typeof startedTimeStamp === 'number'
|
||||||
|
) {
|
||||||
|
output.startedAt = new Date(startedTimeStamp).toISOString();
|
||||||
|
output.completedAt = new Date(fulfilledTimeStamp).toISOString();
|
||||||
|
output.duration = formatMs(fulfilledTimeStamp - startedTimeStamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeQueryApiTimings(
|
||||||
|
requestById: Readonly<Record<string, RtkRequest>>
|
||||||
|
): QueryTimings {
|
||||||
|
const requests = Object.values(requestById);
|
||||||
|
|
||||||
|
let latestRequest: RtkRequest | null = null;
|
||||||
|
let oldestRequest: null | RtkRequest = null;
|
||||||
|
let slowestRequest: RtkRequest | null = null;
|
||||||
|
let fastestRequest: RtkRequest | null = null;
|
||||||
|
let slowestDuration = 0;
|
||||||
|
let fastestDuration = Number.MAX_SAFE_INTEGER;
|
||||||
|
|
||||||
|
const pendingDurations: number[] = [];
|
||||||
|
|
||||||
|
for (let i = 0, len = requests.length; i < len; i++) {
|
||||||
|
const request = requests[i];
|
||||||
|
const { fulfilledTimeStamp, startedTimeStamp } = request;
|
||||||
|
|
||||||
|
if (typeof fulfilledTimeStamp === 'number') {
|
||||||
|
const latestFulfilledTimeStamp = latestRequest?.fulfilledTimeStamp || 0;
|
||||||
|
const oldestFulfilledTimeStamp =
|
||||||
|
oldestRequest?.fulfilledTimeStamp || Number.MAX_SAFE_INTEGER;
|
||||||
|
|
||||||
|
if (fulfilledTimeStamp > latestFulfilledTimeStamp) {
|
||||||
|
latestRequest = request;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fulfilledTimeStamp < oldestFulfilledTimeStamp) {
|
||||||
|
oldestRequest = request;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof startedTimeStamp === 'number' &&
|
||||||
|
startedTimeStamp <= fulfilledTimeStamp
|
||||||
|
) {
|
||||||
|
const pendingDuration = fulfilledTimeStamp - startedTimeStamp;
|
||||||
|
pendingDurations.push(pendingDuration);
|
||||||
|
|
||||||
|
if (pendingDuration > slowestDuration) {
|
||||||
|
slowestDuration = pendingDuration;
|
||||||
|
slowestRequest = request;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pendingDuration < fastestDuration) {
|
||||||
|
fastestDuration = pendingDuration;
|
||||||
|
fastestRequest = request;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const average =
|
||||||
|
pendingDurations.length > 0
|
||||||
|
? formatMs(statistics.mean(pendingDurations))
|
||||||
|
: '-';
|
||||||
|
|
||||||
|
const median =
|
||||||
|
pendingDurations.length > 0
|
||||||
|
? formatMs(statistics.median(pendingDurations))
|
||||||
|
: '-';
|
||||||
|
|
||||||
|
return {
|
||||||
|
latest: formatRtkRequest(latestRequest),
|
||||||
|
oldest: formatRtkRequest(oldestRequest),
|
||||||
|
slowest: formatRtkRequest(slowestRequest),
|
||||||
|
fastest: formatRtkRequest(fastestRequest),
|
||||||
|
average,
|
||||||
|
median,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeApiTimings(
|
||||||
|
api: RtkQueryApiState,
|
||||||
|
actionsById: SelectorsSource<unknown>['actionsById'],
|
||||||
|
currentStateIndex: SelectorsSource<unknown>['currentStateIndex']
|
||||||
|
): ApiTimings {
|
||||||
|
const sortedActions = Object.entries(actionsById)
|
||||||
|
.sort((thisAction, thatAction) =>
|
||||||
|
compareJSONPrimitive(Number(thisAction[0]), Number(thatAction[0]))
|
||||||
|
)
|
||||||
|
.map((entry) => entry[1].action);
|
||||||
|
|
||||||
|
const queryRequestsById = computeRtkQueryRequests(
|
||||||
|
'queries',
|
||||||
|
api,
|
||||||
|
sortedActions,
|
||||||
|
currentStateIndex
|
||||||
|
);
|
||||||
|
|
||||||
|
const mutationRequestsById = computeRtkQueryRequests(
|
||||||
|
'mutations',
|
||||||
|
api,
|
||||||
|
sortedActions,
|
||||||
|
currentStateIndex
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
queries: computeQueryApiTimings(queryRequestsById),
|
||||||
|
mutations: computeQueryApiTimings(mutationRequestsById),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateApiStatsOfCurrentQuery(
|
||||||
|
api: RtkQueryApiState | null,
|
||||||
|
actionsById: SelectorsSource<unknown>['actionsById'],
|
||||||
|
currentStateIndex: SelectorsSource<unknown>['currentStateIndex']
|
||||||
|
): ApiStats | null {
|
||||||
|
if (!api) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
timings: computeApiTimings(api, actionsById, currentStateIndex),
|
||||||
|
tally: {
|
||||||
|
cachedQueries: computeQueryTallyOf(api.queries),
|
||||||
|
cachedMutations: computeQueryTallyOf(api.mutations),
|
||||||
|
tagTypes: Object.keys(api.provided).length,
|
||||||
|
subscriptions: tallySubscriptions(api.subscriptions),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function flipComparator<T>(comparator: Comparator<T>): Comparator<T> {
|
||||||
|
return function flipped(a: T, b: T) {
|
||||||
|
return comparator(b, a);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isQuerySelected(
|
||||||
|
selectedQueryKey: RtkQueryMonitorState['selectedQueryKey'],
|
||||||
|
queryInfo: RtkResourceInfo
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
!!selectedQueryKey &&
|
||||||
|
selectedQueryKey.queryKey === queryInfo.queryKey &&
|
||||||
|
selectedQueryKey.reducerPath === queryInfo.reducerPath
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getApiStateOf(
|
||||||
|
queryInfo: RtkResourceInfo | null,
|
||||||
|
apiStates: ReturnType<typeof getApiStatesOf>
|
||||||
|
): RtkQueryApiState | null {
|
||||||
|
if (!apiStates || !queryInfo) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiStates[queryInfo.reducerPath] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getQuerySubscriptionsOf(
|
||||||
|
queryInfo: QueryInfo | null,
|
||||||
|
apiStates: ReturnType<typeof getApiStatesOf>
|
||||||
|
): RTKQuerySubscribers | null {
|
||||||
|
if (!apiStates || !queryInfo) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
apiStates[queryInfo.reducerPath]?.subscriptions?.[queryInfo.queryKey] ??
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getProvidedOf(
|
||||||
|
queryInfo: QueryInfo | null,
|
||||||
|
apiStates: ReturnType<typeof getApiStatesOf>
|
||||||
|
): RtkQueryApiState['provided'] | null {
|
||||||
|
if (!apiStates || !queryInfo) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiStates[queryInfo.reducerPath]?.provided ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getQueryTagsOf(
|
||||||
|
resInfo: RtkResourceInfo | null,
|
||||||
|
provided: RtkQueryProvided | null
|
||||||
|
): RtkQueryTag[] {
|
||||||
|
if (!resInfo || resInfo.type === 'mutation' || !provided) {
|
||||||
|
return emptyArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagTypes = Object.keys(provided);
|
||||||
|
|
||||||
|
if (tagTypes.length < 1) {
|
||||||
|
return emptyArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
const output: RtkQueryTag[] = [];
|
||||||
|
|
||||||
|
for (const [type, tagIds] of Object.entries(provided)) {
|
||||||
|
if (tagIds) {
|
||||||
|
for (const [id, queryKeys] of Object.entries(tagIds)) {
|
||||||
|
if ((queryKeys as unknown[]).includes(resInfo.queryKey)) {
|
||||||
|
const tag: RtkQueryTag = { type };
|
||||||
|
|
||||||
|
if (id !== missingTagId) {
|
||||||
|
tag.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
output.push(tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes query status flags.
|
||||||
|
* @param status
|
||||||
|
* @see https://redux-toolkit.js.org/rtk-query/usage/queries#frequently-used-query-hook-return-values
|
||||||
|
* @see https://github.com/reduxjs/redux-toolkit/blob/b718e01d323d3ab4b913e5d88c9b90aa790bb975/src/query/core/apiState.ts#L63
|
||||||
|
*/
|
||||||
|
export function getQueryStatusFlags({
|
||||||
|
status,
|
||||||
|
data,
|
||||||
|
}: RtkQueryState | RtkMutationState): RTKStatusFlags {
|
||||||
|
return {
|
||||||
|
isUninitialized: status === QueryStatus.uninitialized,
|
||||||
|
isFetching: status === QueryStatus.pending,
|
||||||
|
isSuccess: status === QueryStatus.fulfilled && !!data,
|
||||||
|
isError: status === QueryStatus.rejected,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* endpoint matcher
|
||||||
|
* @param endpointName
|
||||||
|
* @see https://github.com/reduxjs/redux-toolkit/blob/b718e01d323d3ab4b913e5d88c9b90aa790bb975/src/query/core/buildThunks.ts#L415
|
||||||
|
*/
|
||||||
|
export function matchesEndpoint(endpointName: unknown) {
|
||||||
|
return (action: any): action is Action =>
|
||||||
|
endpointName != null && action?.meta?.arg?.endpointName === endpointName;
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesQueryKey(queryKey: string) {
|
||||||
|
return (action: any): action is Action =>
|
||||||
|
action?.meta?.arg?.queryCacheKey === queryKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
function macthesRequestId(requestId: string) {
|
||||||
|
return (action: any): action is Action =>
|
||||||
|
action?.meta?.requestId === requestId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesReducerPath(reducerPath: string) {
|
||||||
|
return (action: any): action is Action<string> =>
|
||||||
|
typeof action?.type === 'string' && action.type.startsWith(reducerPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesExecuteQuery(reducerPath: string) {
|
||||||
|
return (
|
||||||
|
action: any
|
||||||
|
): action is Action<string> & {
|
||||||
|
meta: { requestId: string; requestStatus: QueryStatus };
|
||||||
|
} => {
|
||||||
|
return (
|
||||||
|
typeof action?.type === 'string' &&
|
||||||
|
action.type.startsWith(`${reducerPath}/executeQuery`) &&
|
||||||
|
typeof action.meta?.requestId === 'string' &&
|
||||||
|
typeof action.meta?.requestStatus === 'string'
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesExecuteMutation(reducerPath: string) {
|
||||||
|
return (
|
||||||
|
action: any
|
||||||
|
): action is Action<string> & {
|
||||||
|
meta: { requestId: string; requestStatus: QueryStatus };
|
||||||
|
} =>
|
||||||
|
typeof action?.type === 'string' &&
|
||||||
|
action.type.startsWith(`${reducerPath}/executeMutation`) &&
|
||||||
|
typeof action.meta?.requestId === 'string' &&
|
||||||
|
typeof action.meta?.requestStatus === 'string';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getActionsOfCurrentQuery(
|
||||||
|
currentQuery: RtkResourceInfo | null,
|
||||||
|
actionById: SelectorsSource<unknown>['actionsById']
|
||||||
|
): Action[] {
|
||||||
|
if (!currentQuery) {
|
||||||
|
return emptyArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
let matcher: ReturnType<typeof macthesRequestId>;
|
||||||
|
|
||||||
|
if (currentQuery.type === 'mutation') {
|
||||||
|
matcher = isAllOf(
|
||||||
|
matchesReducerPath(currentQuery.reducerPath),
|
||||||
|
macthesRequestId(currentQuery.queryKey)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
matcher = isAllOf(
|
||||||
|
matchesReducerPath(currentQuery.reducerPath),
|
||||||
|
matchesQueryKey(currentQuery.queryKey)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const output: AnyAction[] = [];
|
||||||
|
|
||||||
|
for (const [, liftedAction] of Object.entries(actionById)) {
|
||||||
|
if (matcher(liftedAction?.action)) {
|
||||||
|
output.push(liftedAction.action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output.length === 0 ? emptyArray : output;
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
/**
|
||||||
|
* An implementation of `Kahan-Babuska algorithm`
|
||||||
|
* that reduces numerical floating point errors.
|
||||||
|
* @param {number[]} nums
|
||||||
|
* @returns {number}
|
||||||
|
* @see https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.582.288&rep=rep1&type=pdf
|
||||||
|
*/
|
||||||
|
function sum(nums: number[]): number {
|
||||||
|
if (nums.length === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let t;
|
||||||
|
let correction = 0;
|
||||||
|
let output = nums[0];
|
||||||
|
|
||||||
|
for (let i = 1, len = nums.length; i < len; i++) {
|
||||||
|
t = output + nums[i];
|
||||||
|
|
||||||
|
if (Math.abs(output) >= Math.abs(nums[i])) {
|
||||||
|
correction += output - t + nums[i];
|
||||||
|
} else {
|
||||||
|
correction += nums[i] - t + output;
|
||||||
|
}
|
||||||
|
|
||||||
|
output = t;
|
||||||
|
}
|
||||||
|
|
||||||
|
return output + correction;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns mean, also known as average, of numerical sequences.
|
||||||
|
* @param nums
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function mean(nums: number[]): number {
|
||||||
|
if (nums.length === 0) {
|
||||||
|
return NaN;
|
||||||
|
}
|
||||||
|
|
||||||
|
return +(sum(nums) / nums.length).toFixed(6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns median value of a numeric sequence.
|
||||||
|
* @param nums
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function median(nums: number[]): number {
|
||||||
|
const len = nums.length;
|
||||||
|
|
||||||
|
if (len === 0) {
|
||||||
|
return NaN;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len === 1) {
|
||||||
|
return nums[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const sorted = nums.slice().sort();
|
||||||
|
|
||||||
|
if (len % 2 === 1) {
|
||||||
|
return sorted[(len + 1) / 2 - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return +(0.5 * (sorted[len / 2 - 1] + sorted[len / 2])).toFixed(6);
|
||||||
|
}
|
16
packages/redux-devtools-rtk-query-monitor/src/utils/tabs.ts
Normal file
16
packages/redux-devtools-rtk-query-monitor/src/utils/tabs.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { TabOption } from '../types';
|
||||||
|
|
||||||
|
export function isTabVisible<St, Props, Vis extends string>(
|
||||||
|
tab: TabOption<St, Props, Vis>,
|
||||||
|
visKey: Vis | 'default'
|
||||||
|
): boolean {
|
||||||
|
if (typeof tab.visible === 'boolean') {
|
||||||
|
return tab.visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof tab.visible === 'object' && tab.visible) {
|
||||||
|
return !!tab.visible[visKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"extends": "../../tsconfig.react.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./demo/src/build",
|
||||||
|
"module": "ES2015",
|
||||||
|
"strict": false
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
8
packages/redux-devtools-rtk-query-monitor/tsconfig.json
Normal file
8
packages/redux-devtools-rtk-query-monitor/tsconfig.json
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"extends": "../../tsconfig.react.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "lib",
|
||||||
|
"resolveJsonModule": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user