feat(rtk-query): complete initial setup of rtk-query

This commit is contained in:
FaberVitale 2021-06-11 18:54:44 +02:00
parent 1449d5a9c2
commit e9f397bd1e
45 changed files with 13823 additions and 1 deletions

View File

@ -38,6 +38,8 @@
}, },
"dependencies": { "dependencies": {
"@redux-devtools/chart-monitor": "^1.9.0", "@redux-devtools/chart-monitor": "^1.9.0",
"@reduxjs/toolkit": "^1.6.0",
"@redux-devtools/rtk-query-inspector-monitor": "^1.0.0",
"@redux-devtools/core": "^3.9.0", "@redux-devtools/core": "^3.9.0",
"@redux-devtools/inspector-monitor": "^1.0.0", "@redux-devtools/inspector-monitor": "^1.0.0",
"@redux-devtools/inspector-monitor-test-tab": "^0.7.2", "@redux-devtools/inspector-monitor-test-tab": "^0.7.2",

View File

@ -0,0 +1,25 @@
import React, { Component } from 'react';
import { connect, ResolveThunks } from 'react-redux';
import RtkQueryInspectorMonitor from '@redux-devtools/rtk-query-inspector-monitor';
import { selectMonitorWithState } from '../../actions';
type DispatchProps = ResolveThunks<typeof actionCreators>;
type Props = DispatchProps;
class RtkQueryInspectorMonitorWrapper extends Component<Props> {
static update = RtkQueryInspectorMonitor.update;
render() {
return (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
<RtkQueryInspectorMonitor defaultIsVisible invertTheme {...this.props} />
);
}
}
const actionCreators = {
selectMonitorWithState: selectMonitorWithState,
};
export default connect(null, actionCreators)(RtkQueryInspectorMonitorWrapper);

View File

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import LogMonitor from '@redux-devtools/log-monitor'; import LogMonitor from '@redux-devtools/log-monitor';
import RtkQueryInspectorMonitorWrapper from '../containers/monitors/RtkQueryMonitorWrapper';
import ChartMonitorWrapper from '../containers/monitors/ChartMonitorWrapper'; import ChartMonitorWrapper from '../containers/monitors/ChartMonitorWrapper';
import InspectorWrapper from '../containers/monitors/InspectorWrapper'; import InspectorWrapper from '../containers/monitors/InspectorWrapper';
@ -7,6 +8,7 @@ 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 <RtkQueryInspectorMonitorWrapper />;
default: default:
return <InspectorWrapper />; return <InspectorWrapper />;
} }

View File

@ -0,0 +1,8 @@
{
"presets": [
"@babel/preset-env",
"@babel/preset-react",
"@babel/preset-typescript"
],
"plugins": ["@babel/plugin-proposal-class-properties"]
}

View 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'],
},
},
],
};

View File

@ -0,0 +1 @@
demo/src/generated-module/

View 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.

View File

@ -0,0 +1,71 @@
# Redux DevTools Chart Monitor
A chart monitor for [Redux DevTools](https://github.com/gaearon/redux-devtools).
Created by [@romseguy](https://github.com/romseguy) and merged from [`reduxjs/redux-devtools-chart-monitor`](https://github.com/reduxjs/redux-devtools-chart-monitor).
It shows a real-time view of the store aka the current state of the app.
:rocket: Now with immutable-js support.
[Demo](http://romseguy.github.io/redux-store-visualizer/) [(Source)](https://github.com/romseguy/redux-store-visualizer)
![Chart Monitor](https://camo.githubusercontent.com/19aebaeba929e97f97225115c49dc994299cb76e/687474703a2f2f692e696d6775722e636f6d2f4d53677655366c2e676966)
### Installation
```
yarn add @redux-devtools/chart-monitor
```
### Usage
You can use `ChartMonitor` as the only monitor in your app:
##### `containers/DevTools.js`
```js
import React from 'react';
import { createDevTools } from '@redux-devtools/core';
import ChartMonitor from '@redux-devtools/chart-monitor';
export default createDevTools(<ChartMonitor />);
```
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.
Consult the [`DockMonitor` README](https://github.com/reduxjs/redux-devtools/tree/master/packages/redux-devtools-dock-monitor) for details of this approach.
[Read how to start using Redux DevTools.](https://github.com/reduxjs/redux-devtools)
### Features
### Props
#### ChartMonitor props
You can read the React component [propTypes](https://github.com/reduxjs/redux-devtools/blob/master/packages/redux-devtools-chart-monitor/src/Chart.js#L11) in addition to the details below:
| Name | Description |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `defaultIsVisible` | By default, set to `true`. |
| `transitionDuration` | By default, set to `750`, in milliseconds. |
| `heightBetweenNodesCoeff` | By default, set to `1`. |
| `widthBetweenNodesCoeff` | By default, set to `1.3`. |
| `isSorted` | By default, set to `false`. |
| `style` | {<br>&nbsp;&nbsp;width: '100%', height: '100%', // i.e fullscreen for [`DockMonitor`](https://github.com/reduxjs/redux-devtools/tree/master/packages/redux-devtools-dock-monitor)<br>&nbsp;&nbsp;text: {<br>&nbsp;&nbsp;&nbsp;&nbsp;colors: {<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'default': `theme.base0D`,<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;hover: `theme.base06`<br>&nbsp;&nbsp;&nbsp;&nbsp;}<br>&nbsp;&nbsp;},<br>&nbsp;&nbsp;node: {<br>&nbsp;&nbsp;&nbsp;&nbsp;colors: {<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;'default': `theme.base0B`,<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;collapsed: `theme.base0B`,<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;parent: `theme.base0E`<br>&nbsp;&nbsp;&nbsp;&nbsp;},<br>&nbsp;&nbsp;&nbsp;&nbsp;radius: 7<br>&nbsp;&nbsp;}<br>} |
| `onClickText` | Function called with a reference to the clicked node as first argument when clicking on the text next to a node. |
| `tooltipOptions` | {<br>&nbsp;&nbsp;disabled: false,<br>&nbsp;&nbsp;indentationSize: 2,<br>&nbsp;&nbsp;style: {<br>&nbsp;&nbsp;&nbsp;&nbsp;'background-color': `theme.base06`,<br>&nbsp;&nbsp;&nbsp;&nbsp;'opacity': '0.7',<br>&nbsp;&nbsp;&nbsp;&nbsp;'border-radius': '5px',<br>&nbsp;&nbsp;&nbsp;&nbsp;'padding': '5px'<br>&nbsp;&nbsp;}<br>}<br>[More info](https://github.com/reduxjs/redux-devtools/tree/master/packages/d3tooltip#api). |
#### 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` |
| `select` | A function that selects the slice of the state for DevTools to show. For example, `state => state.thePart.iCare.about`. Optional. By default, set to `state => state`. |
### License
MIT

View File

@ -0,0 +1,3 @@
.snowpack
build
node_modules

View File

@ -0,0 +1,25 @@
# New Project
> ✨ Bootstrapped with Create Snowpack App (CSA).
## Available Scripts
### npm start
Runs the app in the development mode.
Open http://localhost:8080 to view it in the browser.
The page will reload if you make edits.
You will also see any lint errors in the console.
### npm run build
Builds a static copy of your site to the `build/` folder.
Your app is ready to be deployed!
**For the best production performance:** Add a build bundler plugin like "@snowpack/plugin-webpack" to your `snowpack.config.mjs` config file.
### npm test
Launches the application test runner.
Run with the `--watch` flag (`npm test -- --watch`) to run in interactive watch mode.

View File

@ -0,0 +1,39 @@
{
"name": "rtk-query-imspector-monitor-demo",
"private": true,
"scripts": {
"start": "cross-env SKIP_PREFLIGHT_CHECK=true react-scripts start",
"build": "react-scripts build"
},
"dependencies": {
"@redux-devtools/core": "^3.9.0",
"@redux-devtools/dock-monitor": "^1.4.0",
"@reduxjs/toolkit": "^1.6.0",
"cross-env": "^7.0.3",
"devui": "^1.0.0-8",
"react": "^17.0.2",
"react-base16-styling": "^0.8.0",
"react-dom": "^17.0.2",
"react-redux": "^7.2.1",
"react-scripts": "4.0.2",
"redux": "^4.0.5",
"redux-devtools-themes": "^1.0.0"
},
"devDependencies": {
"@types/react": "17.0.0",
"@types/react-dom": "17.0.0",
"@types/react-redux": "7.1.9",
"typescript": "4.1.3"
},
"eslintConfig": {
"extends": [
"react-app"
]
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="Web site created using create-snowpack-app" />
<title>Snowpack App</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>

View File

@ -0,0 +1,46 @@
.App {
text-align: center;
}
.App code {
background: #fff3;
padding: 4px 8px;
border-radius: 4px;
}
.App p {
margin: 0.4rem;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@ -0,0 +1,32 @@
import * as React from 'react';
import { Pokemon } from './Pokemon';
import { PokemonName, POKEMON_NAMES } from './pokemon.data';
import './styles.css';
const getRandomPokemonName = () =>
POKEMON_NAMES[Math.floor(Math.random() * POKEMON_NAMES.length)];
export default function App() {
const [pokemon, setPokemon] = React.useState<PokemonName[]>(['bulbasaur']);
return (
<div className="App">
<div>
<button
onClick={() =>
setPokemon((prev) => [...prev, getRandomPokemonName()])
}
>
Add random pokemon
</button>
<button onClick={() => setPokemon((prev) => [...prev, 'bulbasaur'])}>
Add bulbasaur
</button>
</div>
{pokemon.map((name, index) => (
<Pokemon key={index} name={name} />
))}
</div>
);
}

View File

@ -0,0 +1,14 @@
import * as React from 'react';
import { createDevTools } from '@redux-devtools/core';
import DockMonitor from '@redux-devtools/dock-monitor';
import RtkQueryInspectorMonitor from './generated-module/RtkQueryInspectorMonitor.js';
export default createDevTools(
<DockMonitor
toggleVisibilityKey="ctrl-h"
changePositionKey="ctrl-q"
changeMonitorKey="ctrl-m"
>
<RtkQueryInspectorMonitor />
</DockMonitor>,
);

View File

@ -0,0 +1,77 @@
import React, { useState } from '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 },
];
const getRandomIntervalValue = () =>
intervalOptions[Math.floor(Math.random() * intervalOptions.length)].value;
export function Pokemon({ name }: { name: PokemonName }) {
const [pollingInterval, setPollingInterval] = useState(60000);
const {
data,
error,
isLoading,
isFetching,
refetch,
} = useGetPokemonByNameQuery(name, {
pollingInterval,
});
return (
<div
style={{
float: 'left',
textAlign: 'center',
...(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 onClick={refetch} disabled={isFetching}>
{isFetching ? 'Loading' : 'Manually refetch'}
</button>
</div>
</>
) : (
'No Data'
)}
</div>
);
}

View File

@ -0,0 +1,13 @@
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;
}

View File

@ -0,0 +1,16 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import App from './App';
import { store } from './store';
import DevTools from './DevTools';
const rootElement = document.getElementById('root');
ReactDOM.render(
<Provider store={store}>
<App />
<DevTools />
</Provider>,
rootElement,
);

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
<g fill="#61DAFB">
<path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4a43.8 43.8 0 00-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9a487.8 487.8 0 00-41.6-50c32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9a467 467 0 00-63.6 11c-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4a44 44 0 0022.5 5.6c27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7a450.4 450.4 0 01-13.5 39.5 473.3 473.3 0 00-27.5-47.4c14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5a532.7 532.7 0 01-24.1 38.2 520.3 520.3 0 01-90.2.1 551.2 551.2 0 01-45-77.8 521.5 521.5 0 0144.8-78.1 520.3 520.3 0 0190.2-.1 551.2 551.2 0 0145 77.8 560 560 0 01-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8a448.8 448.8 0 01-41.2 8 552.4 552.4 0 0027.4-47.8zM421.2 430a412.3 412.3 0 01-27.8-32 619 619 0 0055.3 0c-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9a451.2 451.2 0 01-41-7.9c3.7-12.9 8.3-26.2 13.5-39.5a473.3 473.3 0 0027.5 47.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32a619 619 0 00-55.3 0c9-11.7 18.3-22.4 27.5-32zm-74 58.9a552.4 552.4 0 00-27.4 47.7c-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9a473.5 473.5 0 00-22.2 60.6c-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9a487.8 487.8 0 0041.6 50c-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9a467 467 0 0063.6-11 280 280 0 015.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9a473.5 473.5 0 0022.2-60.6c9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
<circle cx="420.9" cy="296.5" r="45.7"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -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];

View File

@ -0,0 +1,5 @@
/// <reference types="react-scripts" />
declare module '@redux-devtools/app';
declare module 'remote-redux-devtools';

View File

@ -0,0 +1,15 @@
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/' }),
endpoints: (builder) => ({
getPokemonByName: builder.query({
query: (name: PokemonName) => `pokemon/${name}`,
}),
}),
});
// Export hooks for usage in functional components
export const { useGetPokemonByNameQuery } = pokemonApi;

View File

@ -0,0 +1,13 @@
import { configureStore } from '@reduxjs/toolkit';
import { pokemonApi } from './services/pokemon';
import DevTools from './DevTools';
export const store = configureStore({
reducer: {
[pokemonApi.reducerPath]: pokemonApi.reducer,
},
devTools: false,
// adding the api middleware enables caching, invalidation, polling and other features of `rtk-query`
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(pokemonApi.middleware),
enhancers: [DevTools.instrument()]
});

View File

@ -0,0 +1,4 @@
.App {
font-family: sans-serif;
text-align: center;
}

View File

@ -0,0 +1,25 @@
{
"include": [
"./src/**/*"
],
"compilerOptions": {
"strict": true,
"esModuleInterop": true,
"lib": [
"dom",
"es2015"
],
"jsx": "react-jsx",
"target": "es5",
"allowJs": true,
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true
}
}

View File

@ -0,0 +1,59 @@
/* Use this file to declare any custom file extensions for importing */
/* Use this folder to also add/extend a package d.ts file, if needed. */
/* CSS MODULES */
declare module '*.module.css' {
const classes: { [key: string]: string };
export default classes;
}
declare module '*.module.scss' {
const classes: { [key: string]: string };
export default classes;
}
declare module '*.module.sass' {
const classes: { [key: string]: string };
export default classes;
}
declare module '*.module.less' {
const classes: { [key: string]: string };
export default classes;
}
declare module '*.module.styl' {
const classes: { [key: string]: string };
export default classes;
}
/* CSS */
declare module '*.css';
declare module '*.scss';
declare module '*.sass';
declare module '*.less';
declare module '*.styl';
/* IMAGES */
declare module '*.svg' {
const ref: string;
export default ref;
}
declare module '*.bmp' {
const ref: string;
export default ref;
}
declare module '*.gif' {
const ref: string;
export default ref;
}
declare module '*.jpg' {
const ref: string;
export default ref;
}
declare module '*.jpeg' {
const ref: string;
export default ref;
}
declare module '*.png' {
const ref: string;
export default ref;
}
/* CUSTOM: ADD YOUR OWN HERE */

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,64 @@
{
"name": "@redux-devtools/rtk-query-inspector-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/reduxjs/redux-devtools/tree/master/packages/redux-devtools-chart-monitor",
"bugs": {
"url": "https://github.com/reduxjs/redux-devtools/issues"
},
"license": "MIT",
"author": "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": {
"build": "npm run build:types && npm run build:js",
"build:types": "tsc --emitDeclarationOnly",
"start:dev": "tsc -p ./tsconfig.dev.json --watch",
"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": {
"@types/prop-types": "^15.7.3",
"@types/redux-devtools-themes": "^1.0.0",
"@redux-devtools/dock-monitor": "^1.4.0",
"prop-types": "^15.7.2",
"redux-devtools-themes": "^1.0.0",
"devui": "^1.0.0-8"
},
"devDependencies": {
"@redux-devtools/core": "^3.9.0",
"@types/react": "^16.9.46",
"@reduxjs/toolkit": "^1.6.0",
"react": "^16.13.1",
"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"
}
}

View File

@ -0,0 +1,105 @@
import React, { Component, createRef, CSSProperties, ReactNode } from 'react';
import { AnyAction, Dispatch, Action } from 'redux';
import { LiftedAction, LiftedState } from '@redux-devtools/core';
import * as themes from 'redux-devtools-themes';
import { Base16Theme } from 'react-base16-styling';
import { QueryInfo, RtkQueryInspectorMonitorState } from './types';
import { createInspectorSelectors, computeSelectorSource } from './selectors';
import { selectQueryKey } from './reducers';
import { QueryList } from './components/QueryList';
import { StyleUtils } from './styles/createStylingFromTheme';
import { QueryForm } from './components/QueryForm';
const wrapperStyle: CSSProperties = {
width: '100%',
height: '100%',
};
type SelectorsSource<S> = {
currentState: S | null;
monitorState: RtkQueryInspectorMonitorState;
};
export interface RtkQueryInspectorProps<S, A extends Action<unknown>>
extends LiftedState<S, A, RtkQueryInspectorMonitorState> {
dispatch: Dispatch<LiftedAction<S, A, RtkQueryInspectorMonitorState>>;
theme: keyof typeof themes | Base16Theme;
invertTheme: boolean;
state: S | null;
styleUtils: StyleUtils;
}
type RtkQueryInspectorState<S> = { selectorsSource: SelectorsSource<S> };
class RtkQueryInspector<S, A extends Action<unknown>> extends Component<
RtkQueryInspectorProps<S, A>,
RtkQueryInspectorState<S>
> {
divRef = createRef<HTMLDivElement>();
constructor(props: RtkQueryInspectorProps<S, A>) {
super(props);
this.state = {
selectorsSource: computeSelectorSource(props, null),
};
}
static getDerivedStateFromProps(
props: RtkQueryInspectorProps<unknown, Action<unknown>>,
state: RtkQueryInspectorState<unknown>
): null | RtkQueryInspectorState<unknown> {
const selectorsSource = computeSelectorSource<unknown, Action<unknown>>(
props,
state.selectorsSource
);
if (selectorsSource !== state.selectorsSource) {
return {
selectorsSource,
};
}
return null;
}
selectors = createInspectorSelectors<S>();
handleSelectQuery = (queryInfo: QueryInfo) => {
this.props.dispatch(selectQueryKey(queryInfo) as AnyAction);
};
render(): ReactNode {
const { selectorsSource } = this.state;
const {
styleUtils: { styling },
} = this.props;
const apiStates = this.selectors.selectApiStates(selectorsSource);
const allSortedQueries = this.selectors.selectAllSortedQueries(
selectorsSource
);
console.log('inspector', { apiStates, allSortedQueries, selectorsSource });
return (
<div style={wrapperStyle} ref={this.divRef}>
<div {...styling('querySectionWrapper')}>
<QueryForm
dispatch={this.props.dispatch}
queryComparator={selectorsSource.monitorState.queryComparator}
isAscendingQueryComparatorOrder={
selectorsSource.monitorState.isAscendingQueryComparatorOrder
}
/>
<QueryList
onSelectQuery={this.handleSelectQuery}
queryInfos={allSortedQueries}
selectedQueryKey={selectorsSource.monitorState.selectedQueryKey}
/>
</div>
</div>
);
}
}
export default RtkQueryInspector;

View File

@ -0,0 +1,116 @@
import React, { CSSProperties, PureComponent } from 'react';
import PropTypes from 'prop-types';
import * as themes from 'redux-devtools-themes';
import { Action } from 'redux';
import { Base16Theme } from 'react-base16-styling';
import RtkQueryInspector from './RtkQueryInspector';
import reducer from './reducers';
import {
ExternalProps,
RtkQueryInspectorMonitorProps,
RtkQueryInspectorMonitorState,
} from './types';
import {
createThemeState,
StyleUtils,
StyleUtilsContext,
} from './styles/createStylingFromTheme';
const styles: { container: CSSProperties } = {
container: {
fontFamily: 'monaco, Consolas, Lucida Console, monospace',
position: 'relative',
overflowY: 'hidden',
width: '100%',
height: '100%',
minWidth: 300,
},
};
interface DefaultProps<S> {
select: (state: unknown) => unknown;
theme: keyof typeof themes | Base16Theme;
preserveScrollTop: boolean;
expandActionRoot: boolean;
expandStateRoot: boolean;
markStateDiff: boolean;
}
export interface RtkQueryInspectorComponentState {
readonly styleUtils: StyleUtils;
}
class RtkQueryInspectorMonitor<
S,
A extends Action<unknown>
> extends PureComponent<
RtkQueryInspectorMonitorProps<S, A>,
RtkQueryInspectorComponentState
> {
constructor(props: RtkQueryInspectorMonitorProps<S, A>) {
super(props);
this.state = {
styleUtils: createThemeState<S, A>(props),
};
}
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.shape({
initialScrollTop: PropTypes.number,
}),
preserveScrollTop: PropTypes.bool,
select: PropTypes.func.isRequired,
theme: PropTypes.oneOfType([PropTypes.object, PropTypes.string]),
invertTheme: PropTypes.bool,
};
static defaultProps: DefaultProps<unknown> = {
select: (state: unknown) => state,
theme: 'nicinabox',
preserveScrollTop: true,
expandActionRoot: true,
expandStateRoot: true,
markStateDiff: false,
};
render() {
const {
styleUtils: { base16Theme, styling },
} = this.state;
const RtkQueryInspectorAsAny = RtkQueryInspector as any;
return (
<StyleUtilsContext.Provider value={this.state.styleUtils}>
<div {...styling(['inspector'])}>
<RtkQueryInspectorAsAny
{...this.props}
theme={base16Theme}
styleUtils={this.state.styleUtils}
/>
</div>
</StyleUtilsContext.Provider>
);
}
}
export default (RtkQueryInspectorMonitor as unknown) as React.ComponentType<
ExternalProps<unknown, Action<unknown>>
> & {
update(
monitorProps: ExternalProps<unknown, Action<unknown>>,
state: RtkQueryInspectorMonitorState | undefined,
action: Action
): RtkQueryInspectorMonitorState;
defaultProps: DefaultProps<unknown>;
};

View File

@ -0,0 +1,132 @@
import React, { ReactNode, FormEvent, MouseEvent } from 'react';
import { RtkQueryInspectorMonitorState } from '../types';
import { StyleUtilsContext } from '../styles/createStylingFromTheme';
import { Select } from 'devui';
import { AnyAction } from 'redux';
import { sortQueryOptions, QueryComparators } from '../utils/comparators';
import {
changeIsAscendingQueryComparatorOrder,
changeQueryComparator,
} from '../reducers';
export interface QueryFormProps
extends Pick<
RtkQueryInspectorMonitorState,
'isAscendingQueryComparatorOrder' | 'queryComparator'
> {
dispatch: (action: AnyAction) => void;
}
const ascId = 'rtk-query-rb-asc';
const descId = 'rtk-query-rb-desc';
const selectId = 'rtk-query-comp-select';
const searchId = 'rtk-query-search-query';
const searchPlaceholder = 'filter query...';
export class QueryForm extends React.PureComponent<QueryFormProps> {
handleSubmit = (evt: FormEvent<HTMLFormElement>): void => {
evt.preventDefault();
};
handleButtonGroupClick = ({ target }: MouseEvent<HTMLElement>): void => {
const { isAscendingQueryComparatorOrder: isAsc, dispatch } = this.props;
const targetId = (target as HTMLElement)?.id ?? null;
if (targetId === ascId && !isAsc) {
dispatch(changeIsAscendingQueryComparatorOrder(true));
} else if (targetId === descId && isAsc) {
this.props.dispatch(changeIsAscendingQueryComparatorOrder(false));
}
};
handleSelectComparator = (option: { value: string }): void => {
const { dispatch } = this.props;
if (typeof option?.value === 'string') {
dispatch(changeQueryComparator(option.value as QueryComparators));
}
};
getSelectedOption = (option: { value: string }): string => option?.value;
render(): ReactNode {
const {
isAscendingQueryComparatorOrder: isAsc,
queryComparator,
} = this.props;
const isDesc = !isAsc;
return (
<StyleUtilsContext.Consumer>
{({ styling, base16Theme }) => {
return (
<form
action="#"
onSubmit={this.handleSubmit}
{...styling('queryForm')}
>
<div {...styling('queryListHeader')}>
<label htmlFor={searchId} {...styling('srOnly')}>
filter query
</label>
<input
type="search"
placeholder={searchPlaceholder}
{...styling('querySearch')}
/>
</div>
<div {...styling('sortBySection')}>
<label htmlFor={selectId}>Sort by</label>
<Select
id={selectId}
isSearchable={false}
openOuterUp
theme={base16Theme}
value={sortQueryOptions.find(
(opt) => opt?.value === queryComparator
)}
options={sortQueryOptions}
onChange={this.handleSelectComparator}
selectOption={this.getSelectedOption}
/>
<div
tabIndex={0}
role="radiogroup"
aria-activedescendant={isAsc ? ascId : descId}
onClick={this.handleButtonGroupClick}
>
<button
role="radio"
type="button"
id={ascId}
aria-checked={isAsc}
{...styling(
['selectorButton', isAsc && 'selectorButtonSelected'],
isAsc
)}
>
asc
</button>
<button
id={descId}
role="radio"
type="button"
aria-checked={isDesc}
{...styling(
['selectorButton', isDesc && 'selectorButtonSelected'],
isDesc
)}
>
desc
</button>
</div>
</div>
</form>
);
}}
</StyleUtilsContext.Consumer>
);
}
}

View File

@ -0,0 +1,53 @@
import React, { PureComponent, ReactNode, MouseEvent } from 'react';
import { StyleUtilsContext } from '../styles/createStylingFromTheme';
import { QueryInfo, RtkQueryInspectorMonitorState } from '../types';
import { isQuerySelected } from '../utils/rtk-query';
export interface QueryListProps {
queryInfos: QueryInfo[];
selectedQueryKey: RtkQueryInspectorMonitorState['selectedQueryKey'];
onSelectQuery: (query: QueryInfo) => void;
}
export class QueryList extends PureComponent<QueryListProps> {
static isItemSelected(
selectedQueryKey: QueryListProps['selectedQueryKey'],
queryInfo: QueryInfo
): boolean {
return (
!!selectedQueryKey &&
selectedQueryKey.queryKey === queryInfo.queryKey &&
selectedQueryKey.reducerPath === queryInfo.reducerPath
);
}
render(): ReactNode {
const { queryInfos, selectedQueryKey, onSelectQuery } = this.props;
return (
<StyleUtilsContext.Consumer>
{({ styling }) => (
<ul {...styling('queryList')}>
{queryInfos.map((queryInfo) => {
const isSelected = isQuerySelected(selectedQueryKey, queryInfo);
return (
<li
key={queryInfo.queryKey}
onClick={() => onSelectQuery(queryInfo)}
{...styling(
['queryListItem', isSelected && 'queryListItemSelected'],
isSelected
)}
>
<p>{queryInfo.queryKey}</p>
<p {...styling('queryStatus')}>{queryInfo.query.status}</p>
</li>
);
})}
</ul>
)}
</StyleUtilsContext.Consumer>
);
}
}

View File

@ -0,0 +1,2 @@
export { default } from './RtkQueryInspectorMonitor';
export { ExternalProps } from './types';

View File

@ -0,0 +1,52 @@
import { Action, AnyAction } from 'redux';
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { RtkQueryInspectorProps } from './RtkQueryInspector';
import { QueryInfo, RtkQueryInspectorMonitorState } from './types';
import { QueryComparators } from './utils/comparators';
const initialState: RtkQueryInspectorMonitorState = {
queryComparator: QueryComparators.fulfilledTimeStamp,
isAscendingQueryComparatorOrder: false,
selectedQueryKey: null,
};
const monitorSlice = createSlice({
name: 'rtk-query-monitor',
initialState,
reducers: {
changeQueryComparator(state, action: PayloadAction<QueryComparators>) {
state.queryComparator = action.payload;
},
changeIsAscendingQueryComparatorOrder(
state,
action: PayloadAction<boolean>
) {
state.isAscendingQueryComparatorOrder = !!action.payload;
},
selectQueryKey(
state,
action: PayloadAction<Pick<QueryInfo, 'reducerPath' | 'queryKey'>>
) {
state.selectedQueryKey = {
queryKey: action.payload.queryKey,
reducerPath: action.payload.reducerPath,
};
},
},
});
export default function reducer<S, A extends Action<unknown>>(
props: RtkQueryInspectorProps<S, A>,
state: RtkQueryInspectorMonitorState | undefined = initialState,
action: AnyAction
): RtkQueryInspectorMonitorState {
const output = monitorSlice.reducer(state, action);
return output;
}
export const {
changeIsAscendingQueryComparatorOrder,
changeQueryComparator,
selectQueryKey,
} = monitorSlice.actions;

View File

@ -0,0 +1,94 @@
import { Action, createSelector, Selector } from '@reduxjs/toolkit';
import { RtkQueryInspectorProps } from './RtkQueryInspector';
import { QueryInfo, RtkQueryInspectorMonitorState } from './types';
import { Comparator, queryComparators } from './utils/comparators';
import {
getApiStatesOf,
extractAllApiQueries,
flipComparator,
} from './utils/rtk-query';
type SelectorsSource<S> = {
currentState: S | null;
monitorState: RtkQueryInspectorMonitorState;
};
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 } = props;
const currentState =
computedStates.length > 0 ? computedStates[currentStateIndex].state : null;
if (
!previous ||
previous.currentState !== currentState ||
previous.monitorState !== monitorState
) {
return {
currentState,
monitorState,
};
}
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 selectAllSortedQueries: InspectorSelector<S, QueryInfo[]>;
}
export function createInspectorSelectors<S>(): InspectorSelectors<S> {
const selectQueryComparator = ({
monitorState,
}: SelectorsSource<S>): Comparator<QueryInfo> => {
return queryComparators[monitorState.queryComparator];
};
const selectApiStates = createSelector(
({ currentState }: SelectorsSource<S>) => currentState,
getApiStatesOf
);
const selectAllQueries = createSelector(
selectApiStates,
extractAllApiQueries
);
const selectAllSortedQueries = createSelector(
[
selectQueryComparator,
selectAllQueries,
({ monitorState }: SelectorsSource<S>) =>
monitorState.isAscendingQueryComparatorOrder,
],
(comparator, queryList, isAscending) => {
console.log({ comparator, queryList, isAscending });
const computedComparator = isAscending
? comparator
: flipComparator(comparator);
return queryList.slice().sort(computedComparator);
}
);
return {
selectQueryComparator,
selectApiStates,
selectAllQueries,
selectAllSortedQueries,
};
}

View File

@ -0,0 +1,276 @@
import jss, { StyleSheet } from 'jss';
import preset from 'jss-preset-default';
import {
createStyling,
getBase16Theme,
invertTheme,
StylingFunction,
} from 'react-base16-styling';
import rgba from 'hex-rgba';
import { Base16Theme } from 'redux-devtools-themes';
import { rtkInspectorTheme } from './theme';
import * as reduxThemes from 'redux-devtools-themes';
import { Action } from 'redux';
import { RtkQueryInspectorMonitorProps } from '../types';
import { createContext } from 'react';
jss.setup(preset());
export const colorMap = (theme: 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,
});
type Color = keyof ReturnType<typeof colorMap>;
type ColorMap = {
[color in Color]: string;
};
const getSheetFromColorMap = (map: ColorMap) => ({
inspector: {
display: 'flex',
'flex-direction': 'column',
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,
},
querySectionWrapper: {
display: 'flex',
height: '100%',
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',
'& > :first-child': {
whiteSpace: 'nowrap',
overflowX: 'hidden',
maxWidth: 'calc(100% - 70px)',
textOverflow: 'ellipsis',
},
'&:last-child': {
'border-bottom-width': 0,
},
'border-bottom-color': map.BORDER_COLOR,
},
queryListHeader: {
display: 'flex',
flex: '0 0 auto',
'align-items': 'center',
'border-bottom-width': '1px',
'border-bottom-style': 'solid',
'border-color': map.LIST_BORDER_COLOR,
},
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,
'background-color': map.ACTION_TIME_BACK_COLOR,
color: map.ACTION_TIME_COLOR,
},
queryListItemSelected: {
'background-color': map.SELECTED_BACKGROUND_COLOR,
},
tabSelector: {
position: 'relative',
'z-index': 1,
display: 'inline-flex',
float: 'right',
},
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',
padding: '6.5px 10px',
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,
},
selectorButtonSmall: {
padding: '0px 8px',
'font-size': '0.8em',
},
selectorButtonSelected: {
'background-color': map.TAB_BACK_SELECTED_COLOR,
},
queryForm: {
display: 'flex',
flexFlow: 'column nowrap',
},
sortBySection: {
display: 'flex',
padding: '0.4em',
'& > [role="radiogroup"]': {
flex: '0 0 auto',
padding: '0 0 0 0.4em',
},
'& label': {
display: 'flex',
flex: '0 0 auto',
whiteSpace: 'noWrap',
alignItems: 'center',
paddingRight: '0.4em',
},
},
querySearch: {
outline: 'none',
border: 'none',
width: '100%',
padding: '5px 10px',
'font-size': '1em',
'font-family': '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,
},
},
});
let themeSheet: StyleSheet;
const getDefaultThemeStyling = (theme: Base16Theme) => {
if (themeSheet) {
themeSheet.detach();
}
themeSheet = jss
.createStyleSheet(getSheetFromColorMap(colorMap(theme)))
.attach();
return themeSheet.classes;
};
export const base16Themes = { ...reduxThemes };
export const createStylingFromTheme = createStyling(getDefaultThemeStyling, {
defaultBase16: rtkInspectorTheme,
base16Themes,
});
export interface StyleUtils {
base16Theme: Base16Theme;
styling: StylingFunction;
}
export function createThemeState<S, A extends Action<unknown>>(
props: RtkQueryInspectorMonitorProps<S, A>
): StyleUtils {
const base16Theme =
getBase16Theme(props.theme, base16Themes) ?? rtkInspectorTheme;
const theme = props.invertTheme ? invertTheme(props.theme) : props.theme;
const styling = createStylingFromTheme(theme);
return { base16Theme, styling };
}
export const StyleUtilsContext = createContext<StyleUtils>({
base16Theme: rtkInspectorTheme,
styling: (...args: any[]) => ({ className: '', style: {} }),
});

View File

@ -0,0 +1,25 @@
import { Base16Theme } from 'react-base16-styling';
/**
* Lifted from `packages/redux-devtools-inspector-monitor/src/themes/inspector.ts`
*/
export const rtkInspectorTheme: Base16Theme = {
scheme: 'rtk-inspector',
author: 'Alexander Kuznetsov (alexkuz@gmail.com)',
base00: '#181818',
base01: '#282828',
base02: '#383838',
base03: '#585858',
base04: '#b8b8b8',
base05: '#d8d8d8',
base06: '#e8e8e8',
base07: '#FFFFFF',
base08: '#E92F28',
base09: '#dc9656',
base0A: '#f7ca88',
base0B: '#65AD00',
base0C: '#86c1b9',
base0D: '#347BD9',
base0E: '#EC31C0',
base0F: '#a16946',
};

View File

@ -0,0 +1,65 @@
import { LiftedAction, LiftedState } from '@redux-devtools/instrument';
import type { createApi } from '@reduxjs/toolkit/query';
import { Dispatch } from 'react';
import { Base16Theme } from 'react-base16-styling';
import { Action } from 'redux';
import * as themes from 'redux-devtools-themes';
import { QueryComparators } from './utils/comparators';
export interface RtkQueryInspectorMonitorState {
queryComparator: QueryComparators;
isAscendingQueryComparatorOrder: boolean;
selectedQueryKey: Pick<QueryInfo, 'reducerPath' | 'queryKey'> | null;
}
export interface RtkQueryInspectorMonitorProps<S, A extends Action<unknown>>
extends LiftedState<S, A, RtkQueryInspectorMonitorState> {
dispatch: Dispatch<
Action | LiftedAction<S, A, RtkQueryInspectorMonitorState>
>;
preserveScrollTop: boolean;
select: (state: S) => unknown;
theme: keyof typeof themes | Base16Theme;
expandActionRoot: boolean;
expandStateRoot: boolean;
markStateDiff: boolean;
hideMainButtons?: boolean;
invertTheme?: boolean;
}
export type RtkQueryApiState = ReturnType<
ReturnType<typeof createApi>['reducer']
>;
export type RtkQueryState = NonNullable<
RtkQueryApiState['queries'][keyof RtkQueryApiState]
>;
export interface ExternalProps<S, A extends Action<unknown>> {
dispatch: Dispatch<
Action | LiftedAction<S, A, RtkQueryInspectorMonitorState>
>;
preserveScrollTop: boolean;
select: (state: S) => unknown;
theme: keyof typeof themes | Base16Theme;
expandActionRoot: boolean;
expandStateRoot: boolean;
markStateDiff: boolean;
hideMainButtons?: boolean;
invertTheme: boolean;
}
export type AnyExternalProps = ExternalProps<unknown, any>;
export interface QueryInfo {
query: RtkQueryState;
queryKey: string;
reducerPath: string;
}
export interface ApiInfo {
reducerPath: string;
apiState: RtkQueryApiState;
}

View File

@ -0,0 +1,82 @@
import { QueryStatus } from '@reduxjs/toolkit/dist/query';
import { QueryInfo } from '../types';
export interface Comparator<T> {
(a: T, b: T): number;
}
export enum QueryComparators {
fulfilledTimeStamp = 'timestamp',
queryKey = 'key',
status = 'status',
endpointName = 'endpointName',
}
export const sortQueryOptions: { label: string; value: string }[] = [
{ label: 'fulfilledTimeStamp', value: QueryComparators.fulfilledTimeStamp },
{ label: 'query key', value: QueryComparators.queryKey },
{ label: 'status ', value: QueryComparators.status },
{ label: 'endpoint', value: QueryComparators.endpointName },
];
function sortQueryByFulfilled(
thisQueryInfo: QueryInfo,
thatQueryInfo: QueryInfo
): number {
const thisFulfilled = thisQueryInfo.query.fulfilledTimeStamp ?? -1;
const thatFulfilled = thatQueryInfo.query.fulfilledTimeStamp ?? -1;
return thisFulfilled - thatFulfilled;
}
const mapStatusToFactor = {
[QueryStatus.uninitialized]: 1,
[QueryStatus.pending]: 2,
[QueryStatus.rejected]: 3,
[QueryStatus.fulfilled]: 4,
};
function sortQueryByStatus(
thisQueryInfo: QueryInfo,
thatQueryInfo: QueryInfo
): number {
const thisTerm = mapStatusToFactor[thisQueryInfo.query.status] || -1;
const thatTerm = mapStatusToFactor[thatQueryInfo.query.status] || -1;
return thisTerm - thatTerm;
}
function compareStrings(a: string, b: string): number {
if (a === b) {
return 0;
}
return a > b ? 1 : -1;
}
function sortByQueryKey(
thisQueryInfo: QueryInfo,
thatQueryInfo: QueryInfo
): number {
return compareStrings(thisQueryInfo.queryKey, thatQueryInfo.queryKey);
}
function sortQueryByEndpointName(
thisQueryInfo: QueryInfo,
thatQueryInfo: QueryInfo
): number {
const thisEndpointName = thisQueryInfo.query.endpointName ?? '';
const thatEndpointName = thatQueryInfo.query.endpointName ?? '';
return compareStrings(thisEndpointName, thatEndpointName);
}
export const queryComparators: Readonly<Record<
QueryComparators,
Comparator<QueryInfo>
>> = {
[QueryComparators.fulfilledTimeStamp]: sortQueryByFulfilled,
[QueryComparators.status]: sortQueryByStatus,
[QueryComparators.endpointName]: sortQueryByEndpointName,
[QueryComparators.queryKey]: sortByQueryKey,
};

View File

@ -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;
}

View File

@ -0,0 +1,109 @@
import { isPlainObject } from '@reduxjs/toolkit';
import type { createApi } from '@reduxjs/toolkit/query';
import { QueryInfo, RtkQueryInspectorMonitorState } from '../types';
import { Comparator } from './comparators';
import { emptyArray } from './object';
export type RtkQueryApiState = ReturnType<
ReturnType<typeof createApi>['reducer']
>;
const rtkqueryApiStateKeys: ReadonlyArray<keyof RtkQueryApiState> = [
'queries',
'mutations',
'config',
'provided',
'subscriptions',
];
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;
}
export function getApiStatesOf(
state: unknown
): null | Readonly<Record<string, RtkQueryApiState>> {
if (!isPlainObject(state)) {
return null;
}
const output: null | Record<string, RtkQueryApiState> = {};
const keys = Object.keys(state);
for (let i = 0, len = keys.length; i < len; i++) {
const key = keys[i];
const value = (state 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 query = api.queries[queryKey];
if (query) {
output.push({
reducerPath,
queryKey,
query,
});
}
}
}
return output;
}
export function flipComparator<T>(comparator: Comparator<T>): Comparator<T> {
return function flipped(a: T, b: T) {
return comparator(b, a);
};
}
export function isQuerySelected(
selectedQueryKey: RtkQueryInspectorMonitorState['selectedQueryKey'],
queryInfo: QueryInfo
): boolean {
return (
!!selectedQueryKey &&
selectedQueryKey.queryKey === queryInfo.queryKey &&
selectedQueryKey.reducerPath === queryInfo.reducerPath
);
}

View File

@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.react.base.json",
"compilerOptions": {
"outDir": "./demo/src/generated-module",
"module": "ES2015",
"strict": false
},
"include": ["src"],
}

View File

@ -1,5 +1,5 @@
{ {
"extends": "../../tsconfig.base.json", "extends": "../../tsconfig.react.base.json",
"compilerOptions": { "compilerOptions": {
"outDir": "lib" "outDir": "lib"
}, },