mirror of
https://github.com/reduxjs/redux-devtools.git
synced 2025-07-26 07:59:48 +03:00
feat(inspector-monitor): add rtk-query toggle button #753
This commit is contained in:
parent
229869698b
commit
0af1d2b301
|
@ -0,0 +1,69 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { useGetPokemonByNameQuery } from '../rtk-query/pokemonApi';
|
||||||
|
import type { PokemonName } from '../rtk-query/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 const Pokemon = ({ name }: { name: PokemonName }) => {
|
||||||
|
const [pollingInterval, setPollingInterval] = React.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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,31 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import { PokemonName, POKEMON_NAMES } from '../rtk-query/pokemon.data';
|
||||||
|
import { Pokemon } from './Pokemon';
|
||||||
|
|
||||||
|
const getRandomPokemonName = () =>
|
||||||
|
POKEMON_NAMES[Math.floor(Math.random() * POKEMON_NAMES.length)];
|
||||||
|
|
||||||
|
export function PokemonView() {
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { CSSProperties } from 'react';
|
import React, { CSSProperties } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import pkg from '../../../package.json';
|
import pkg from '../../../../package.json';
|
||||||
import Button from 'react-bootstrap/Button';
|
import Button from 'react-bootstrap/Button';
|
||||||
import FormGroup from 'react-bootstrap/FormGroup';
|
import FormGroup from 'react-bootstrap/FormGroup';
|
||||||
import FormControl from 'react-bootstrap/FormControl';
|
import FormControl from 'react-bootstrap/FormControl';
|
||||||
|
@ -11,9 +11,10 @@ import InputGroup from 'react-bootstrap/InputGroup';
|
||||||
import Row from 'react-bootstrap/Row';
|
import Row from 'react-bootstrap/Row';
|
||||||
import * as base16 from 'base16';
|
import * as base16 from 'base16';
|
||||||
import { push as pushRoute } from 'connected-react-router';
|
import { push as pushRoute } from 'connected-react-router';
|
||||||
|
import { PokemonView } from '../components/PokemonView';
|
||||||
import { Path } from 'history';
|
import { Path } from 'history';
|
||||||
import * as inspectorThemes from '../../../src/themes';
|
import * as inspectorThemes from '../../../../src/themes';
|
||||||
import getOptions, { Options } from './getOptions';
|
import getOptions, { Options } from '../getOptions';
|
||||||
import {
|
import {
|
||||||
AddFunctionAction,
|
AddFunctionAction,
|
||||||
AddHugeObjectAction,
|
AddHugeObjectAction,
|
||||||
|
@ -34,7 +35,7 @@ import {
|
||||||
ShuffleArrayAction,
|
ShuffleArrayAction,
|
||||||
TimeoutUpdateAction,
|
TimeoutUpdateAction,
|
||||||
ToggleTimeoutUpdateAction,
|
ToggleTimeoutUpdateAction,
|
||||||
} from './reducers';
|
} from '../reducers';
|
||||||
|
|
||||||
const styles: {
|
const styles: {
|
||||||
wrapper: CSSProperties;
|
wrapper: CSSProperties;
|
||||||
|
@ -56,9 +57,10 @@ const styles: {
|
||||||
header: {},
|
header: {},
|
||||||
content: {
|
content: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
flexFlow: 'column',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'space-evenly',
|
||||||
height: '50%',
|
padding: 8,
|
||||||
},
|
},
|
||||||
buttons: {
|
buttons: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
@ -251,6 +253,7 @@ class DemoApp extends React.Component<Props> {
|
||||||
Shuffle Array
|
Shuffle Array
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<PokemonView />
|
||||||
</div>
|
</div>
|
||||||
<div style={styles.links}>
|
<div style={styles.links}>
|
||||||
<a onClick={this.toggleExtension} style={styles.link}>
|
<a onClick={this.toggleExtension} style={styles.link}>
|
|
@ -3,10 +3,10 @@ import { connect } from 'react-redux';
|
||||||
import { createDevTools } from '@redux-devtools/core';
|
import { createDevTools } from '@redux-devtools/core';
|
||||||
import DockMonitor from '@redux-devtools/dock-monitor';
|
import DockMonitor from '@redux-devtools/dock-monitor';
|
||||||
import { Location } from 'history';
|
import { Location } from 'history';
|
||||||
import DevtoolsInspector from '../../../src/DevtoolsInspector';
|
import DevtoolsInspector from '../../../../src/DevtoolsInspector';
|
||||||
import getOptions from './getOptions';
|
import getOptions from '../getOptions';
|
||||||
import { base16Themes } from '../../../src/utils/createStylingFromTheme';
|
import { base16Themes } from '../../../../src/utils/createStylingFromTheme';
|
||||||
import { DemoAppState } from './reducers';
|
import { DemoAppState } from '../reducers';
|
||||||
|
|
||||||
const CustomComponent = () => (
|
const CustomComponent = () => (
|
||||||
<div
|
<div
|
|
@ -1,8 +1,9 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render } from 'react-dom';
|
import { render } from 'react-dom';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
|
import { configureStore } from '@reduxjs/toolkit';
|
||||||
|
import { pokemonApi } from '../js/rtk-query/pokemonApi';
|
||||||
import {
|
import {
|
||||||
createStore,
|
|
||||||
applyMiddleware,
|
applyMiddleware,
|
||||||
compose,
|
compose,
|
||||||
StoreEnhancerStoreCreator,
|
StoreEnhancerStoreCreator,
|
||||||
|
@ -13,10 +14,10 @@ import { Route } from 'react-router';
|
||||||
import { createBrowserHistory } from 'history';
|
import { createBrowserHistory } from 'history';
|
||||||
import { ConnectedRouter, routerMiddleware } from 'connected-react-router';
|
import { ConnectedRouter, routerMiddleware } from 'connected-react-router';
|
||||||
import { persistState } from '@redux-devtools/core';
|
import { persistState } from '@redux-devtools/core';
|
||||||
import DemoApp from './DemoApp';
|
import DemoApp from './containers/DemoApp';
|
||||||
import createRootReducer from './reducers';
|
import createRootReducer from './reducers';
|
||||||
import getOptions from './getOptions';
|
import getOptions from './getOptions';
|
||||||
import { ConnectedDevTools, getDevTools } from './DevTools';
|
import { ConnectedDevTools, getDevTools } from './containers/DevTools';
|
||||||
|
|
||||||
function getDebugSessionKey() {
|
function getDebugSessionKey() {
|
||||||
const matches = /[?&]debug_session=([^&#]+)\b/.exec(window.location.href);
|
const matches = /[?&]debug_session=([^&#]+)\b/.exec(window.location.href);
|
||||||
|
@ -51,7 +52,16 @@ const enhancer = compose(
|
||||||
persistState(getDebugSessionKey())
|
persistState(getDebugSessionKey())
|
||||||
);
|
);
|
||||||
|
|
||||||
const store = createStore(createRootReducer(history), enhancer);
|
const store = configureStore({
|
||||||
|
reducer: createRootReducer(history),
|
||||||
|
devTools: false,
|
||||||
|
middleware: (getDefaultMiddleware) =>
|
||||||
|
getDefaultMiddleware({
|
||||||
|
immutableCheck: false,
|
||||||
|
serializableCheck: false,
|
||||||
|
}).concat([pokemonApi.middleware]),
|
||||||
|
enhancers: [enhancer],
|
||||||
|
}); // createStore(createRootReducer(history), enhancer);
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
RouterState,
|
RouterState,
|
||||||
} from 'connected-react-router';
|
} from 'connected-react-router';
|
||||||
import { History } from 'history';
|
import { History } from 'history';
|
||||||
|
import { pokemonApi } from './rtk-query/pokemonApi';
|
||||||
|
|
||||||
type Nested = { long: { nested: { path: { to: { a: string } } }[] } };
|
type Nested = { long: { nested: { path: { to: { a: string } } }[] } };
|
||||||
|
|
||||||
|
@ -187,6 +188,7 @@ export interface DemoAppState {
|
||||||
addFunction: { f: (a: number, b: number, c: number) => number } | null;
|
addFunction: { f: (a: number, b: number, c: number) => number } | null;
|
||||||
addSymbol: { s: symbol; error: Error } | null;
|
addSymbol: { s: symbol; error: Error } | null;
|
||||||
shuffleArray: unknown[];
|
shuffleArray: unknown[];
|
||||||
|
[pokemonApi.reducerPath]: ReturnType<typeof pokemonApi.reducer>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const createRootReducer = (
|
const createRootReducer = (
|
||||||
|
@ -259,6 +261,7 @@ const createRootReducer = (
|
||||||
: state,
|
: state,
|
||||||
shuffleArray: (state = DEFAULT_SHUFFLE_ARRAY, action) =>
|
shuffleArray: (state = DEFAULT_SHUFFLE_ARRAY, action) =>
|
||||||
action.type === 'SHUFFLE_ARRAY' ? shuffle(state) : state,
|
action.type === 'SHUFFLE_ARRAY' ? shuffle(state) : state,
|
||||||
|
[pokemonApi.reducerPath]: pokemonApi.reducer,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default createRootReducer;
|
export default createRootReducer;
|
||||||
|
|
|
@ -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];
|
|
@ -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;
|
|
@ -51,11 +51,13 @@
|
||||||
"react-base16-styling": "^0.8.0",
|
"react-base16-styling": "^0.8.0",
|
||||||
"react-dragula": "^1.1.17",
|
"react-dragula": "^1.1.17",
|
||||||
"react-json-tree": "^0.15.0",
|
"react-json-tree": "^0.15.0",
|
||||||
"redux-devtools-themes": "^1.0.0"
|
"redux-devtools-themes": "^1.0.0",
|
||||||
|
"reselect": "^4.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@redux-devtools/core": "^3.9.0",
|
"@redux-devtools/core": "^3.9.0",
|
||||||
"@redux-devtools/dock-monitor": "^1.4.0",
|
"@redux-devtools/dock-monitor": "^1.4.0",
|
||||||
|
"@reduxjs/toolkit": "^1.6.0",
|
||||||
"@types/dateformat": "^3.0.1",
|
"@types/dateformat": "^3.0.1",
|
||||||
"@types/hex-rgba": "^1.0.0",
|
"@types/hex-rgba": "^1.0.0",
|
||||||
"@types/history": "^4.7.8",
|
"@types/history": "^4.7.8",
|
||||||
|
|
|
@ -6,7 +6,6 @@ import { PerformAction } from '@redux-devtools/core';
|
||||||
import { StylingFunction } from 'react-base16-styling';
|
import { StylingFunction } from 'react-base16-styling';
|
||||||
import ActionListRow from './ActionListRow';
|
import ActionListRow from './ActionListRow';
|
||||||
import ActionListHeader from './ActionListHeader';
|
import ActionListHeader from './ActionListHeader';
|
||||||
import { filterActions } from './utils/filters';
|
|
||||||
import { ActionForm } from './redux';
|
import { ActionForm } from './redux';
|
||||||
|
|
||||||
function getTimestamps<A extends Action<unknown>>(
|
function getTimestamps<A extends Action<unknown>>(
|
||||||
|
@ -30,9 +29,9 @@ interface Props<S, A extends Action<unknown>> {
|
||||||
selectedActionId: number | null;
|
selectedActionId: number | null;
|
||||||
startActionId: number | null;
|
startActionId: number | null;
|
||||||
skippedActionIds: number[];
|
skippedActionIds: number[];
|
||||||
computedStates: { state: S; error?: string }[];
|
|
||||||
draggableActions: boolean;
|
draggableActions: boolean;
|
||||||
actionForm: ActionForm;
|
actionForm: ActionForm;
|
||||||
|
filteredActionIds: number[];
|
||||||
hideMainButtons: boolean | undefined;
|
hideMainButtons: boolean | undefined;
|
||||||
hideActionButtons: boolean | undefined;
|
hideActionButtons: boolean | undefined;
|
||||||
styling: StylingFunction;
|
styling: StylingFunction;
|
||||||
|
@ -129,12 +128,11 @@ export default class ActionList<
|
||||||
hideActionButtons,
|
hideActionButtons,
|
||||||
onCommit,
|
onCommit,
|
||||||
onSweep,
|
onSweep,
|
||||||
actionForm,
|
|
||||||
onActionFormChange,
|
onActionFormChange,
|
||||||
onJumpToState,
|
onJumpToState,
|
||||||
|
actionForm,
|
||||||
|
filteredActionIds,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const filteredActionIds = filterActions<unknown, A>(this.props);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key="actionList"
|
key="actionList"
|
||||||
|
@ -155,7 +153,7 @@ export default class ActionList<
|
||||||
hasStagedActions={actionIds.length > 1}
|
hasStagedActions={actionIds.length > 1}
|
||||||
/>
|
/>
|
||||||
<div {...styling('actionListRows')} ref={this.getRef}>
|
<div {...styling('actionListRows')} ref={this.getRef}>
|
||||||
{filteredActionIds.map((actionId) => (
|
{filteredActionIds.map((actionId: number) => (
|
||||||
<ActionListRow
|
<ActionListRow
|
||||||
key={actionId}
|
key={actionId}
|
||||||
styling={styling}
|
styling={styling}
|
||||||
|
|
|
@ -32,7 +32,7 @@ const ActionListHeader: FunctionComponent<Props> = ({
|
||||||
actionForm,
|
actionForm,
|
||||||
onActionFormChange,
|
onActionFormChange,
|
||||||
}) => {
|
}) => {
|
||||||
const { isNoopFilterActive } = actionForm;
|
const { isNoopFilterActive, isRtkQueryFilterActive } = actionForm;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -42,7 +42,37 @@ const ActionListHeader: FunctionComponent<Props> = ({
|
||||||
onChange={(e) => onSearch(e.target.value)}
|
onChange={(e) => onSearch(e.target.value)}
|
||||||
placeholder="filter..."
|
placeholder="filter..."
|
||||||
/>
|
/>
|
||||||
{!hideMainButtons && (
|
<div {...styling('toggleButtonWrapper')}>
|
||||||
|
<button
|
||||||
|
title="Toggle visibility of noop actions"
|
||||||
|
aria-label="Toggle visibility of noop actions"
|
||||||
|
aria-pressed={!isNoopFilterActive}
|
||||||
|
onClick={() =>
|
||||||
|
onActionFormChange({ isNoopFilterActive: !isNoopFilterActive })
|
||||||
|
}
|
||||||
|
type="button"
|
||||||
|
{...styling('toggleButton')}
|
||||||
|
>
|
||||||
|
noop
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
title="Toggle visibility of rtk-query actions"
|
||||||
|
aria-label="Toggle visibility of rtk-query actions"
|
||||||
|
aria-pressed={!isRtkQueryFilterActive}
|
||||||
|
onClick={() =>
|
||||||
|
onActionFormChange({
|
||||||
|
isRtkQueryFilterActive: !isRtkQueryFilterActive,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
type="button"
|
||||||
|
{...styling('toggleButton')}
|
||||||
|
>
|
||||||
|
RTKQ
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{!hideMainButtons && (
|
||||||
|
<div {...styling(['actionListHeader', 'actionListHeaderSecondRow'])}>
|
||||||
<div {...styling('actionListHeaderWrapper')}>
|
<div {...styling('actionListHeaderWrapper')}>
|
||||||
<RightSlider shown={hasStagedActions} styling={styling}>
|
<RightSlider shown={hasStagedActions} styling={styling}>
|
||||||
<div {...styling('actionListHeaderSelector')}>
|
<div {...styling('actionListHeaderSelector')}>
|
||||||
|
@ -67,25 +97,8 @@ const ActionListHeader: FunctionComponent<Props> = ({
|
||||||
</div>
|
</div>
|
||||||
</RightSlider>
|
</RightSlider>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
<div {...styling(['actionListHeader', 'actionListHeaderSecondRow'])}>
|
|
||||||
<button
|
|
||||||
title="Toggle visibility of noop actions"
|
|
||||||
aria-label="Toggle visibility of noop actions"
|
|
||||||
aria-pressed={!isNoopFilterActive}
|
|
||||||
onClick={() =>
|
|
||||||
onActionFormChange({ isNoopFilterActive: !isNoopFilterActive })
|
|
||||||
}
|
|
||||||
type="button"
|
|
||||||
{...styling(
|
|
||||||
['selectorButton', 'selectorButtonSmall', !isNoopFilterActive && 'selectorButtonSelected'],
|
|
||||||
isNoopFilterActive
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
noop
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -101,6 +114,7 @@ ActionListHeader.propTypes = {
|
||||||
actionForm: PropTypes.shape({
|
actionForm: PropTypes.shape({
|
||||||
searchValue: PropTypes.string.isRequired,
|
searchValue: PropTypes.string.isRequired,
|
||||||
isNoopFilterActive: PropTypes.bool.isRequired,
|
isNoopFilterActive: PropTypes.bool.isRequired,
|
||||||
|
isRtkQueryFilterActive: PropTypes.bool.isRequired,
|
||||||
}).isRequired,
|
}).isRequired,
|
||||||
onActionFormChange: PropTypes.func.isRequired,
|
onActionFormChange: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
|
@ -30,6 +30,8 @@ import {
|
||||||
reducer,
|
reducer,
|
||||||
updateMonitorState,
|
updateMonitorState,
|
||||||
} from './redux';
|
} from './redux';
|
||||||
|
import { makeSelectFilteredActions } from './utils/filters';
|
||||||
|
import { computeSelectorSource } from './utils/selectors';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||||
const {
|
const {
|
||||||
|
@ -236,6 +238,8 @@ class DevtoolsInspector<S, A extends Action<unknown>> extends PureComponent<
|
||||||
updateSizeTimeout?: number;
|
updateSizeTimeout?: number;
|
||||||
inspectorRef?: HTMLDivElement | null;
|
inspectorRef?: HTMLDivElement | null;
|
||||||
|
|
||||||
|
selectorsSource = computeSelectorSource(this.props);
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.updateSizeMode();
|
this.updateSizeMode();
|
||||||
this.updateSizeTimeout = window.setInterval(
|
this.updateSizeTimeout = window.setInterval(
|
||||||
|
@ -293,6 +297,8 @@ class DevtoolsInspector<S, A extends Action<unknown>> extends PureComponent<
|
||||||
this.inspectorRef = node;
|
this.inspectorRef = node;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
selectFilteredActions = makeSelectFilteredActions();
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
stagedActionIds: actionIds,
|
stagedActionIds: actionIds,
|
||||||
|
@ -316,6 +322,12 @@ class DevtoolsInspector<S, A extends Action<unknown>> extends PureComponent<
|
||||||
this.state;
|
this.state;
|
||||||
const { base16Theme, styling } = themeState;
|
const { base16Theme, styling } = themeState;
|
||||||
|
|
||||||
|
this.selectorsSource = computeSelectorSource(
|
||||||
|
this.props,
|
||||||
|
this.selectorsSource
|
||||||
|
);
|
||||||
|
const filteredActionIds = this.selectFilteredActions(this.selectorsSource);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key="inspector"
|
key="inspector"
|
||||||
|
@ -339,7 +351,7 @@ class DevtoolsInspector<S, A extends Action<unknown>> extends PureComponent<
|
||||||
styling,
|
styling,
|
||||||
}}
|
}}
|
||||||
actionForm={actionForm}
|
actionForm={actionForm}
|
||||||
computedStates={computedStates}
|
filteredActionIds={filteredActionIds}
|
||||||
onSearch={this.handleSearch}
|
onSearch={this.handleSearch}
|
||||||
onActionFormChange={this.handleActionFormChange}
|
onActionFormChange={this.handleActionFormChange}
|
||||||
onSelect={this.handleSelectAction}
|
onSelect={this.handleSelectAction}
|
||||||
|
|
|
@ -10,6 +10,7 @@ const ACTION_FORM_VALUE_CHANGE =
|
||||||
export interface ActionForm {
|
export interface ActionForm {
|
||||||
searchValue: string;
|
searchValue: string;
|
||||||
isNoopFilterActive: boolean;
|
isNoopFilterActive: boolean;
|
||||||
|
isRtkQueryFilterActive: boolean;
|
||||||
}
|
}
|
||||||
export interface UpdateMonitorStateAction {
|
export interface UpdateMonitorStateAction {
|
||||||
type: typeof UPDATE_MONITOR_STATE;
|
type: typeof UPDATE_MONITOR_STATE;
|
||||||
|
@ -55,6 +56,7 @@ export const DEFAULT_STATE: DevtoolsInspectorState = {
|
||||||
actionForm: {
|
actionForm: {
|
||||||
searchValue: '',
|
searchValue: '',
|
||||||
isNoopFilterActive: false,
|
isNoopFilterActive: false,
|
||||||
|
isRtkQueryFilterActive: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import jss, { Styles, StyleSheet } from 'jss';
|
import jss, { StyleSheet } from 'jss';
|
||||||
import preset from 'jss-preset-default';
|
import preset from 'jss-preset-default';
|
||||||
import { createStyling } from 'react-base16-styling';
|
import { createStyling } from 'react-base16-styling';
|
||||||
import rgba from 'hex-rgba';
|
import rgba from 'hex-rgba';
|
||||||
|
@ -33,6 +33,8 @@ const colorMap = (theme: Base16Theme) => ({
|
||||||
LINK_COLOR: rgba(theme.base0E, 90),
|
LINK_COLOR: rgba(theme.base0E, 90),
|
||||||
LINK_HOVER_COLOR: theme.base0E,
|
LINK_HOVER_COLOR: theme.base0E,
|
||||||
ERROR_COLOR: theme.base08,
|
ERROR_COLOR: theme.base08,
|
||||||
|
TOGGLE_BUTTON_BACKGROUND: rgba(theme.base00, 70),
|
||||||
|
TOGGLE_BUTTON_SELECTED_BACKGROUND: theme.base04,
|
||||||
});
|
});
|
||||||
|
|
||||||
type Color = keyof ReturnType<typeof colorMap>;
|
type Color = keyof ReturnType<typeof colorMap>;
|
||||||
|
@ -85,6 +87,7 @@ const getSheetFromColorMap = (map: ColorMap) => ({
|
||||||
|
|
||||||
actionListHeaderSecondRow: {
|
actionListHeaderSecondRow: {
|
||||||
padding: '5px 10px',
|
padding: '5px 10px',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
},
|
},
|
||||||
|
|
||||||
actionListRows: {
|
actionListRows: {
|
||||||
|
@ -110,7 +113,6 @@ const getSheetFromColorMap = (map: ColorMap) => ({
|
||||||
|
|
||||||
actionListHeaderSelector: {
|
actionListHeaderSelector: {
|
||||||
display: 'inline-flex',
|
display: 'inline-flex',
|
||||||
'margin-right': '10px',
|
|
||||||
},
|
},
|
||||||
|
|
||||||
actionListWide: {
|
actionListWide: {
|
||||||
|
@ -334,6 +336,55 @@ const getSheetFromColorMap = (map: ColorMap) => ({
|
||||||
'background-color': map.TAB_BACK_SELECTED_COLOR,
|
'background-color': map.TAB_BACK_SELECTED_COLOR,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
toggleButtonWrapper: {
|
||||||
|
display: 'flex',
|
||||||
|
height: 20,
|
||||||
|
margin: 0,
|
||||||
|
padding: '0 10px 0 0',
|
||||||
|
'& > *': {
|
||||||
|
height: '100%',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleButton: {
|
||||||
|
color: 'inherit',
|
||||||
|
cursor: 'pointer',
|
||||||
|
position: 'relative',
|
||||||
|
padding: '0px 4px',
|
||||||
|
fontSize: '0.7em',
|
||||||
|
letterSpacing: '-0.7px',
|
||||||
|
outline: 'none',
|
||||||
|
boxShadow: 'none',
|
||||||
|
fontWeight: '700',
|
||||||
|
'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',
|
||||||
|
},
|
||||||
|
|
||||||
|
'&:hover': {
|
||||||
|
'background-color': map.TAB_BACK_SELECTED_COLOR,
|
||||||
|
},
|
||||||
|
|
||||||
|
'background-color': map.TOGGLE_BUTTON_BACKGROUND,
|
||||||
|
|
||||||
|
'border-color': map.TAB_BORDER_COLOR,
|
||||||
|
|
||||||
|
'&[aria-pressed="true"]': {
|
||||||
|
color: map.BACKGROUND_COLOR,
|
||||||
|
backgroundColor: map.TOGGLE_BUTTON_SELECTED_BACKGROUND,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
diff: {
|
diff: {
|
||||||
padding: '2px 3px',
|
padding: '2px 3px',
|
||||||
'border-radius': '3px',
|
'border-radius': '3px',
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import type { Action } from 'redux';
|
import type { Action } from 'redux';
|
||||||
import type { LiftedState, PerformAction } from '@redux-devtools/core';
|
import type { LiftedState, PerformAction } from '@redux-devtools/core';
|
||||||
import { ActionForm } from '../redux';
|
import { ActionForm } from '../redux';
|
||||||
|
import { makeSelectRtkQueryActionRegex } from './rtk-query';
|
||||||
|
import { createShallowEqualSelector, SelectorsSource } from './selectors';
|
||||||
|
|
||||||
type ComputedStates<S> = LiftedState<
|
type ComputedStates<S> = LiftedState<
|
||||||
S,
|
S,
|
||||||
|
@ -34,7 +36,7 @@ function filterActionsBySearchValue<A extends Action<unknown>>(
|
||||||
): number[] {
|
): number[] {
|
||||||
const lowerSearchValue = searchValue && searchValue.toLowerCase();
|
const lowerSearchValue = searchValue && searchValue.toLowerCase();
|
||||||
|
|
||||||
if (!lowerSearchValue) {
|
if (!lowerSearchValue || !actionIds.length) {
|
||||||
return actionIds;
|
return actionIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,18 +50,35 @@ function filterActionsBySearchValue<A extends Action<unknown>>(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function filterOutRtkQueryActions(
|
||||||
|
actionIds: number[],
|
||||||
|
actions: Record<number, PerformAction<Action<unknown>>>,
|
||||||
|
rtkQueryRegex: RegExp | null
|
||||||
|
) {
|
||||||
|
if (!rtkQueryRegex || actionIds.length === 0) {
|
||||||
|
return actionIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
return actionIds.filter((actionId) => {
|
||||||
|
const type = actions[actionId].action.type;
|
||||||
|
|
||||||
|
return typeof type !== 'string' || !rtkQueryRegex.test(type);
|
||||||
|
});
|
||||||
|
}
|
||||||
export interface FilterActionsPayload<S, A extends Action<unknown>> {
|
export interface FilterActionsPayload<S, A extends Action<unknown>> {
|
||||||
readonly actionIds: number[];
|
readonly actionIds: number[];
|
||||||
readonly actions: Record<number, PerformAction<A>>;
|
readonly actions: Record<number, PerformAction<A>>;
|
||||||
readonly computedStates: ComputedStates<S>;
|
readonly computedStates: ComputedStates<S>;
|
||||||
readonly actionForm: ActionForm;
|
readonly actionForm: ActionForm;
|
||||||
|
readonly rtkQueryRegex: RegExp | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function filterActions<S, A extends Action<unknown>>({
|
function filterActions<S, A extends Action<unknown>>({
|
||||||
actionIds,
|
actionIds,
|
||||||
actions,
|
actions,
|
||||||
computedStates,
|
computedStates,
|
||||||
actionForm,
|
actionForm,
|
||||||
|
rtkQueryRegex,
|
||||||
}: FilterActionsPayload<S, A>): number[] {
|
}: FilterActionsPayload<S, A>): number[] {
|
||||||
let output = filterActionsBySearchValue(
|
let output = filterActionsBySearchValue(
|
||||||
actionForm.searchValue,
|
actionForm.searchValue,
|
||||||
|
@ -68,8 +87,44 @@ export function filterActions<S, A extends Action<unknown>>({
|
||||||
);
|
);
|
||||||
|
|
||||||
if (actionForm.isNoopFilterActive) {
|
if (actionForm.isNoopFilterActive) {
|
||||||
output = filterStateChangingAction(actionIds, computedStates);
|
output = filterStateChangingAction(output, computedStates);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actionForm.isRtkQueryFilterActive && rtkQueryRegex) {
|
||||||
|
output = filterOutRtkQueryActions(output, actions, rtkQueryRegex);
|
||||||
}
|
}
|
||||||
|
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SelectFilteredActions<S, A extends Action<unknown>> {
|
||||||
|
(selectorsSource: SelectorsSource<S, A>): number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a selector that given `SelectorsSource` returns
|
||||||
|
* a list of filtered `actionsIds`.
|
||||||
|
* @returns {number[]}
|
||||||
|
*/
|
||||||
|
export function makeSelectFilteredActions<
|
||||||
|
S,
|
||||||
|
A extends Action<unknown>
|
||||||
|
>(): SelectFilteredActions<S, A> {
|
||||||
|
const selectRegex = makeSelectRtkQueryActionRegex();
|
||||||
|
|
||||||
|
return createShallowEqualSelector(
|
||||||
|
(selectorsSource: SelectorsSource<S, A>): FilterActionsPayload<S, A> => {
|
||||||
|
const actionForm = selectorsSource.monitorState.actionForm;
|
||||||
|
const { actionIds, actions, computedStates } = selectorsSource;
|
||||||
|
|
||||||
|
return {
|
||||||
|
actionIds,
|
||||||
|
actions,
|
||||||
|
computedStates,
|
||||||
|
actionForm,
|
||||||
|
rtkQueryRegex: selectRegex(selectorsSource),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
filterActions
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
/**
|
||||||
|
* Borrowed from `react-redux`.
|
||||||
|
* @param {unknown} obj The object to inspect.
|
||||||
|
* @returns {boolean} True if the argument appears to be a plain object.
|
||||||
|
* @see https://github.com/reduxjs/react-redux/blob/2c7ef25a0704efcf10e41112d88ae9867e946d10/src/utils/isPlainObject.js
|
||||||
|
*/
|
||||||
|
export function isPlainObject(obj: unknown): obj is Record<string, unknown> {
|
||||||
|
if (typeof obj !== 'object' || obj === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const proto = Object.getPrototypeOf(obj);
|
||||||
|
|
||||||
|
if (proto === null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let baseProto = proto;
|
||||||
|
while (Object.getPrototypeOf(baseProto) !== null) {
|
||||||
|
baseProto = Object.getPrototypeOf(baseProto);
|
||||||
|
}
|
||||||
|
|
||||||
|
return proto === baseProto;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function identity<T>(val: T): T {
|
||||||
|
return val;
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
// https://stackoverflow.com/a/9310752
|
||||||
|
export function escapeRegExpSpecialCharacter(text: string): string {
|
||||||
|
return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ```ts
|
||||||
|
* const entries = ['a', 'b', 'c', 'd.'];
|
||||||
|
*
|
||||||
|
* oneOfGroup(entries) // returns "(a|b|c|d\\.)"
|
||||||
|
* ```
|
||||||
|
* @param onOf
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function oneOfGroup(onOf: string[]): string {
|
||||||
|
return `(${onOf.map(escapeRegExpSpecialCharacter).join('|')})`;
|
||||||
|
}
|
107
packages/redux-devtools-inspector-monitor/src/utils/rtk-query.ts
Normal file
107
packages/redux-devtools-inspector-monitor/src/utils/rtk-query.ts
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
import { Action } from 'redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { isPlainObject } from './object';
|
||||||
|
import { oneOfGroup } from './regexp';
|
||||||
|
import { createShallowEqualSelector, SelectorsSource } from './selectors';
|
||||||
|
|
||||||
|
interface RtkQueryApiState {
|
||||||
|
queries: Record<string, unknown>;
|
||||||
|
mutations: Record<string, unknown>;
|
||||||
|
config: Record<string, unknown>;
|
||||||
|
provided: Record<string, unknown>;
|
||||||
|
subscriptions: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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[rtkqueryApiStateKeys[i]])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getApiReducerPaths(currentUserState: unknown): string[] | null {
|
||||||
|
if (!isPlainObject(currentUserState)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userStateKeys = Object.keys(currentUserState);
|
||||||
|
const output: string[] = [];
|
||||||
|
|
||||||
|
for (const key of userStateKeys) {
|
||||||
|
if (isApiSlice(currentUserState[key])) {
|
||||||
|
output.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
const knownRtkQueryActionPrefixes = oneOfGroup([
|
||||||
|
'executeQuery',
|
||||||
|
'executeMutation',
|
||||||
|
'config',
|
||||||
|
'subscriptions',
|
||||||
|
'invalidation',
|
||||||
|
'mutations',
|
||||||
|
'queries',
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a regex that matches rtk query actions from an array of api
|
||||||
|
* `reducerPaths`.
|
||||||
|
* @param reducerPaths list of rtkQuery reducerPaths in user state.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function generateRtkQueryActionRegex(
|
||||||
|
reducerPaths: string[] | null
|
||||||
|
): RegExp | null {
|
||||||
|
if (!reducerPaths?.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new RegExp(
|
||||||
|
`^${oneOfGroup(reducerPaths)}/${knownRtkQueryActionPrefixes}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectRTKQueryActionRegex<S, A extends Action<unknown>> {
|
||||||
|
(selectorsSource: SelectorsSource<S, A>): RegExp | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeSelectRtkQueryActionRegex<
|
||||||
|
S,
|
||||||
|
A extends Action<unknown>
|
||||||
|
>(): SelectRTKQueryActionRegex<S, A> {
|
||||||
|
const selectApiReducerPaths = createSelector(
|
||||||
|
(source: SelectorsSource<S, A>) =>
|
||||||
|
source.computedStates[source.currentStateIndex].state,
|
||||||
|
getApiReducerPaths
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectRtkQueryActionRegex = createShallowEqualSelector(
|
||||||
|
selectApiReducerPaths,
|
||||||
|
generateRtkQueryActionRegex
|
||||||
|
);
|
||||||
|
|
||||||
|
return selectRtkQueryActionRegex;
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { shallowEqual } from 'react-redux';
|
||||||
|
import { Action } from 'redux';
|
||||||
|
import type { LiftedState, PerformAction } from '@redux-devtools/core';
|
||||||
|
import { createSelectorCreator, defaultMemoize } from 'reselect';
|
||||||
|
import { DevtoolsInspectorState } from '../redux';
|
||||||
|
import { DevtoolsInspectorProps } from '../DevtoolsInspector';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see https://github.com/reduxjs/reselect#customize-equalitycheck-for-defaultmemoize
|
||||||
|
*/
|
||||||
|
export const createShallowEqualSelector = createSelectorCreator(
|
||||||
|
defaultMemoize,
|
||||||
|
shallowEqual
|
||||||
|
);
|
||||||
|
|
||||||
|
type ComputedStates<S> = LiftedState<
|
||||||
|
S,
|
||||||
|
Action<unknown>,
|
||||||
|
unknown
|
||||||
|
>['computedStates'];
|
||||||
|
|
||||||
|
export interface SelectorsSource<S, A extends Action<unknown>> {
|
||||||
|
readonly actionIds: number[];
|
||||||
|
readonly actions: Record<number, PerformAction<A>>;
|
||||||
|
readonly computedStates: ComputedStates<S>;
|
||||||
|
readonly monitorState: DevtoolsInspectorState;
|
||||||
|
readonly currentStateIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function computeSelectorSource<S, A extends Action<unknown>>(
|
||||||
|
props: DevtoolsInspectorProps<S, A>,
|
||||||
|
previous: SelectorsSource<S, A> | null = null
|
||||||
|
): SelectorsSource<S, A> {
|
||||||
|
const {
|
||||||
|
computedStates,
|
||||||
|
currentStateIndex,
|
||||||
|
monitorState,
|
||||||
|
stagedActionIds,
|
||||||
|
actionsById,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const next: SelectorsSource<S, A> = {
|
||||||
|
currentStateIndex,
|
||||||
|
monitorState,
|
||||||
|
computedStates,
|
||||||
|
actions: actionsById,
|
||||||
|
actionIds: stagedActionIds,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (previous && shallowEqual(next, previous)) {
|
||||||
|
return previous;
|
||||||
|
}
|
||||||
|
|
||||||
|
return next;
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
function is(x: unknown, y: unknown): boolean {
|
||||||
|
if (x === y) {
|
||||||
|
return x !== 0 || y !== 0 || 1 / x === 1 / y;
|
||||||
|
} else {
|
||||||
|
return x !== x && y !== y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shallow equal algorithm borrowed from react-redux.
|
||||||
|
* @see https://github.com/reduxjs/react-redux/blob/2c7ef25a0704efcf10e41112d88ae9867e946d10/src/utils/shallowEqual.js
|
||||||
|
*/
|
||||||
|
export default function shallowEqual(objA: unknown, objB: unknown): boolean {
|
||||||
|
if (is(objA, objB)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof objA !== 'object' ||
|
||||||
|
objA === null ||
|
||||||
|
typeof objB !== 'object' ||
|
||||||
|
objB === null
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const keysA = Object.keys(objA);
|
||||||
|
const keysB = Object.keys(objB);
|
||||||
|
|
||||||
|
if (keysA.length !== keysB.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < keysA.length; i++) {
|
||||||
|
if (
|
||||||
|
!Object.prototype.hasOwnProperty.call(objB, keysA[i]) ||
|
||||||
|
!is(
|
||||||
|
(objA as Record<string, unknown>)[keysA[i]],
|
||||||
|
(objB as Record<string, unknown>)[keysA[i]]
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
36
yarn.lock
36
yarn.lock
|
@ -3598,6 +3598,7 @@ __metadata:
|
||||||
dependencies:
|
dependencies:
|
||||||
"@redux-devtools/core": ^3.9.0
|
"@redux-devtools/core": ^3.9.0
|
||||||
"@redux-devtools/dock-monitor": ^1.4.0
|
"@redux-devtools/dock-monitor": ^1.4.0
|
||||||
|
"@reduxjs/toolkit": ^1.6.0
|
||||||
"@types/dateformat": ^3.0.1
|
"@types/dateformat": ^3.0.1
|
||||||
"@types/dragula": ^3.7.0
|
"@types/dragula": ^3.7.0
|
||||||
"@types/hex-rgba": ^1.0.0
|
"@types/hex-rgba": ^1.0.0
|
||||||
|
@ -3632,6 +3633,7 @@ __metadata:
|
||||||
redux: ^4.1.0
|
redux: ^4.1.0
|
||||||
redux-devtools-themes: ^1.0.0
|
redux-devtools-themes: ^1.0.0
|
||||||
redux-logger: ^3.0.6
|
redux-logger: ^3.0.6
|
||||||
|
reselect: ^4.0.0
|
||||||
seamless-immutable: ^7.1.4
|
seamless-immutable: ^7.1.4
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
"@redux-devtools/core": ^3.7.0
|
"@redux-devtools/core": ^3.7.0
|
||||||
|
@ -3730,6 +3732,26 @@ __metadata:
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
|
"@reduxjs/toolkit@npm:^1.6.0":
|
||||||
|
version: 1.6.1
|
||||||
|
resolution: "@reduxjs/toolkit@npm:1.6.1"
|
||||||
|
dependencies:
|
||||||
|
immer: ^9.0.1
|
||||||
|
redux: ^4.1.0
|
||||||
|
redux-thunk: ^2.3.0
|
||||||
|
reselect: ^4.0.0
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.14.0 || ^17.0.0
|
||||||
|
react-redux: ^7.2.1
|
||||||
|
peerDependenciesMeta:
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
react-redux:
|
||||||
|
optional: true
|
||||||
|
checksum: fc7f8211a74e4ccb246870e9f3dddbd2f9a79ce50ed3c3bb68a59af2b279712e0cba0690479416ef3fea6cfcc1e8d257da8e6a4a49a306d38d83d80182329cfb
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@restart/context@npm:^2.1.4":
|
"@restart/context@npm:^2.1.4":
|
||||||
version: 2.1.4
|
version: 2.1.4
|
||||||
resolution: "@restart/context@npm:2.1.4"
|
resolution: "@restart/context@npm:2.1.4"
|
||||||
|
@ -14909,6 +14931,13 @@ fsevents@^1.2.7:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"immer@npm:^9.0.1":
|
||||||
|
version: 9.0.5
|
||||||
|
resolution: "immer@npm:9.0.5"
|
||||||
|
checksum: a7fa984fa1887a33ce6d44a7a505fd5ac76009336d8b1c99d34f59aaefc28aadf93ab1e5db27513acd15be454a8a89d8151e915d9b0b6e86e72acbd28218410b
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"immutable@npm:^3.8.1 || ^4.0.0-rc.1, immutable@npm:^4.0.0-rc.12":
|
"immutable@npm:^3.8.1 || ^4.0.0-rc.1, immutable@npm:^4.0.0-rc.12":
|
||||||
version: 4.0.0-rc.12
|
version: 4.0.0-rc.12
|
||||||
resolution: "immutable@npm:4.0.0-rc.12"
|
resolution: "immutable@npm:4.0.0-rc.12"
|
||||||
|
@ -23416,6 +23445,13 @@ fsevents@^1.2.7:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"reselect@npm:^4.0.0":
|
||||||
|
version: 4.0.0
|
||||||
|
resolution: "reselect@npm:4.0.0"
|
||||||
|
checksum: 3480930929f673f12962cdde140dce48ea8ba171cd428bb2c7639672e41770bd6b64e935bc0400f47cfa960f617c7ac068c4309527373825d11e27262f08c0a3
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"resolve-cwd@npm:^2.0.0":
|
"resolve-cwd@npm:^2.0.0":
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
resolution: "resolve-cwd@npm:2.0.0"
|
resolution: "resolve-cwd@npm:2.0.0"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user