mirror of
https://github.com/reduxjs/redux-devtools.git
synced 2025-07-25 15:40:06 +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 { connect } from 'react-redux';
|
||||
import pkg from '../../../package.json';
|
||||
import pkg from '../../../../package.json';
|
||||
import Button from 'react-bootstrap/Button';
|
||||
import FormGroup from 'react-bootstrap/FormGroup';
|
||||
import FormControl from 'react-bootstrap/FormControl';
|
||||
|
@ -11,9 +11,10 @@ import InputGroup from 'react-bootstrap/InputGroup';
|
|||
import Row from 'react-bootstrap/Row';
|
||||
import * as base16 from 'base16';
|
||||
import { push as pushRoute } from 'connected-react-router';
|
||||
import { PokemonView } from '../components/PokemonView';
|
||||
import { Path } from 'history';
|
||||
import * as inspectorThemes from '../../../src/themes';
|
||||
import getOptions, { Options } from './getOptions';
|
||||
import * as inspectorThemes from '../../../../src/themes';
|
||||
import getOptions, { Options } from '../getOptions';
|
||||
import {
|
||||
AddFunctionAction,
|
||||
AddHugeObjectAction,
|
||||
|
@ -34,7 +35,7 @@ import {
|
|||
ShuffleArrayAction,
|
||||
TimeoutUpdateAction,
|
||||
ToggleTimeoutUpdateAction,
|
||||
} from './reducers';
|
||||
} from '../reducers';
|
||||
|
||||
const styles: {
|
||||
wrapper: CSSProperties;
|
||||
|
@ -56,9 +57,10 @@ const styles: {
|
|||
header: {},
|
||||
content: {
|
||||
display: 'flex',
|
||||
flexFlow: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '50%',
|
||||
justifyContent: 'space-evenly',
|
||||
padding: 8,
|
||||
},
|
||||
buttons: {
|
||||
display: 'flex',
|
||||
|
@ -251,6 +253,7 @@ class DemoApp extends React.Component<Props> {
|
|||
Shuffle Array
|
||||
</Button>
|
||||
</div>
|
||||
<PokemonView />
|
||||
</div>
|
||||
<div style={styles.links}>
|
||||
<a onClick={this.toggleExtension} style={styles.link}>
|
|
@ -3,10 +3,10 @@ import { connect } from 'react-redux';
|
|||
import { createDevTools } from '@redux-devtools/core';
|
||||
import DockMonitor from '@redux-devtools/dock-monitor';
|
||||
import { Location } from 'history';
|
||||
import DevtoolsInspector from '../../../src/DevtoolsInspector';
|
||||
import getOptions from './getOptions';
|
||||
import { base16Themes } from '../../../src/utils/createStylingFromTheme';
|
||||
import { DemoAppState } from './reducers';
|
||||
import DevtoolsInspector from '../../../../src/DevtoolsInspector';
|
||||
import getOptions from '../getOptions';
|
||||
import { base16Themes } from '../../../../src/utils/createStylingFromTheme';
|
||||
import { DemoAppState } from '../reducers';
|
||||
|
||||
const CustomComponent = () => (
|
||||
<div
|
|
@ -1,8 +1,9 @@
|
|||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import { pokemonApi } from '../js/rtk-query/pokemonApi';
|
||||
import {
|
||||
createStore,
|
||||
applyMiddleware,
|
||||
compose,
|
||||
StoreEnhancerStoreCreator,
|
||||
|
@ -13,10 +14,10 @@ import { Route } from 'react-router';
|
|||
import { createBrowserHistory } from 'history';
|
||||
import { ConnectedRouter, routerMiddleware } from 'connected-react-router';
|
||||
import { persistState } from '@redux-devtools/core';
|
||||
import DemoApp from './DemoApp';
|
||||
import DemoApp from './containers/DemoApp';
|
||||
import createRootReducer from './reducers';
|
||||
import getOptions from './getOptions';
|
||||
import { ConnectedDevTools, getDevTools } from './DevTools';
|
||||
import { ConnectedDevTools, getDevTools } from './containers/DevTools';
|
||||
|
||||
function getDebugSessionKey() {
|
||||
const matches = /[?&]debug_session=([^&#]+)\b/.exec(window.location.href);
|
||||
|
@ -51,7 +52,16 @@ const enhancer = compose(
|
|||
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(
|
||||
<Provider store={store}>
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
RouterState,
|
||||
} from 'connected-react-router';
|
||||
import { History } from 'history';
|
||||
import { pokemonApi } from './rtk-query/pokemonApi';
|
||||
|
||||
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;
|
||||
addSymbol: { s: symbol; error: Error } | null;
|
||||
shuffleArray: unknown[];
|
||||
[pokemonApi.reducerPath]: ReturnType<typeof pokemonApi.reducer>;
|
||||
}
|
||||
|
||||
const createRootReducer = (
|
||||
|
@ -259,6 +261,7 @@ const createRootReducer = (
|
|||
: state,
|
||||
shuffleArray: (state = DEFAULT_SHUFFLE_ARRAY, action) =>
|
||||
action.type === 'SHUFFLE_ARRAY' ? shuffle(state) : state,
|
||||
[pokemonApi.reducerPath]: pokemonApi.reducer,
|
||||
});
|
||||
|
||||
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-dragula": "^1.1.17",
|
||||
"react-json-tree": "^0.15.0",
|
||||
"redux-devtools-themes": "^1.0.0"
|
||||
"redux-devtools-themes": "^1.0.0",
|
||||
"reselect": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@redux-devtools/core": "^3.9.0",
|
||||
"@redux-devtools/dock-monitor": "^1.4.0",
|
||||
"@reduxjs/toolkit": "^1.6.0",
|
||||
"@types/dateformat": "^3.0.1",
|
||||
"@types/hex-rgba": "^1.0.0",
|
||||
"@types/history": "^4.7.8",
|
||||
|
|
|
@ -6,7 +6,6 @@ import { PerformAction } from '@redux-devtools/core';
|
|||
import { StylingFunction } from 'react-base16-styling';
|
||||
import ActionListRow from './ActionListRow';
|
||||
import ActionListHeader from './ActionListHeader';
|
||||
import { filterActions } from './utils/filters';
|
||||
import { ActionForm } from './redux';
|
||||
|
||||
function getTimestamps<A extends Action<unknown>>(
|
||||
|
@ -30,9 +29,9 @@ interface Props<S, A extends Action<unknown>> {
|
|||
selectedActionId: number | null;
|
||||
startActionId: number | null;
|
||||
skippedActionIds: number[];
|
||||
computedStates: { state: S; error?: string }[];
|
||||
draggableActions: boolean;
|
||||
actionForm: ActionForm;
|
||||
filteredActionIds: number[];
|
||||
hideMainButtons: boolean | undefined;
|
||||
hideActionButtons: boolean | undefined;
|
||||
styling: StylingFunction;
|
||||
|
@ -129,12 +128,11 @@ export default class ActionList<
|
|||
hideActionButtons,
|
||||
onCommit,
|
||||
onSweep,
|
||||
actionForm,
|
||||
onActionFormChange,
|
||||
onJumpToState,
|
||||
actionForm,
|
||||
filteredActionIds,
|
||||
} = this.props;
|
||||
const filteredActionIds = filterActions<unknown, A>(this.props);
|
||||
|
||||
return (
|
||||
<div
|
||||
key="actionList"
|
||||
|
@ -155,7 +153,7 @@ export default class ActionList<
|
|||
hasStagedActions={actionIds.length > 1}
|
||||
/>
|
||||
<div {...styling('actionListRows')} ref={this.getRef}>
|
||||
{filteredActionIds.map((actionId) => (
|
||||
{filteredActionIds.map((actionId: number) => (
|
||||
<ActionListRow
|
||||
key={actionId}
|
||||
styling={styling}
|
||||
|
|
|
@ -32,7 +32,7 @@ const ActionListHeader: FunctionComponent<Props> = ({
|
|||
actionForm,
|
||||
onActionFormChange,
|
||||
}) => {
|
||||
const { isNoopFilterActive } = actionForm;
|
||||
const { isNoopFilterActive, isRtkQueryFilterActive } = actionForm;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -42,7 +42,37 @@ const ActionListHeader: FunctionComponent<Props> = ({
|
|||
onChange={(e) => onSearch(e.target.value)}
|
||||
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')}>
|
||||
<RightSlider shown={hasStagedActions} styling={styling}>
|
||||
<div {...styling('actionListHeaderSelector')}>
|
||||
|
@ -67,25 +97,8 @@ const ActionListHeader: FunctionComponent<Props> = ({
|
|||
</div>
|
||||
</RightSlider>
|
||||
</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>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -101,6 +114,7 @@ ActionListHeader.propTypes = {
|
|||
actionForm: PropTypes.shape({
|
||||
searchValue: PropTypes.string.isRequired,
|
||||
isNoopFilterActive: PropTypes.bool.isRequired,
|
||||
isRtkQueryFilterActive: PropTypes.bool.isRequired,
|
||||
}).isRequired,
|
||||
onActionFormChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
|
|
@ -30,6 +30,8 @@ import {
|
|||
reducer,
|
||||
updateMonitorState,
|
||||
} from './redux';
|
||||
import { makeSelectFilteredActions } from './utils/filters';
|
||||
import { computeSelectorSource } from './utils/selectors';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
const {
|
||||
|
@ -236,6 +238,8 @@ class DevtoolsInspector<S, A extends Action<unknown>> extends PureComponent<
|
|||
updateSizeTimeout?: number;
|
||||
inspectorRef?: HTMLDivElement | null;
|
||||
|
||||
selectorsSource = computeSelectorSource(this.props);
|
||||
|
||||
componentDidMount() {
|
||||
this.updateSizeMode();
|
||||
this.updateSizeTimeout = window.setInterval(
|
||||
|
@ -293,6 +297,8 @@ class DevtoolsInspector<S, A extends Action<unknown>> extends PureComponent<
|
|||
this.inspectorRef = node;
|
||||
};
|
||||
|
||||
selectFilteredActions = makeSelectFilteredActions();
|
||||
|
||||
render() {
|
||||
const {
|
||||
stagedActionIds: actionIds,
|
||||
|
@ -316,6 +322,12 @@ class DevtoolsInspector<S, A extends Action<unknown>> extends PureComponent<
|
|||
this.state;
|
||||
const { base16Theme, styling } = themeState;
|
||||
|
||||
this.selectorsSource = computeSelectorSource(
|
||||
this.props,
|
||||
this.selectorsSource
|
||||
);
|
||||
const filteredActionIds = this.selectFilteredActions(this.selectorsSource);
|
||||
|
||||
return (
|
||||
<div
|
||||
key="inspector"
|
||||
|
@ -339,7 +351,7 @@ class DevtoolsInspector<S, A extends Action<unknown>> extends PureComponent<
|
|||
styling,
|
||||
}}
|
||||
actionForm={actionForm}
|
||||
computedStates={computedStates}
|
||||
filteredActionIds={filteredActionIds}
|
||||
onSearch={this.handleSearch}
|
||||
onActionFormChange={this.handleActionFormChange}
|
||||
onSelect={this.handleSelectAction}
|
||||
|
|
|
@ -10,6 +10,7 @@ const ACTION_FORM_VALUE_CHANGE =
|
|||
export interface ActionForm {
|
||||
searchValue: string;
|
||||
isNoopFilterActive: boolean;
|
||||
isRtkQueryFilterActive: boolean;
|
||||
}
|
||||
export interface UpdateMonitorStateAction {
|
||||
type: typeof UPDATE_MONITOR_STATE;
|
||||
|
@ -55,6 +56,7 @@ export const DEFAULT_STATE: DevtoolsInspectorState = {
|
|||
actionForm: {
|
||||
searchValue: '',
|
||||
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 { createStyling } from 'react-base16-styling';
|
||||
import rgba from 'hex-rgba';
|
||||
|
@ -33,6 +33,8 @@ const colorMap = (theme: Base16Theme) => ({
|
|||
LINK_COLOR: rgba(theme.base0E, 90),
|
||||
LINK_HOVER_COLOR: theme.base0E,
|
||||
ERROR_COLOR: theme.base08,
|
||||
TOGGLE_BUTTON_BACKGROUND: rgba(theme.base00, 70),
|
||||
TOGGLE_BUTTON_SELECTED_BACKGROUND: theme.base04,
|
||||
});
|
||||
|
||||
type Color = keyof ReturnType<typeof colorMap>;
|
||||
|
@ -85,6 +87,7 @@ const getSheetFromColorMap = (map: ColorMap) => ({
|
|||
|
||||
actionListHeaderSecondRow: {
|
||||
padding: '5px 10px',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
|
||||
actionListRows: {
|
||||
|
@ -110,7 +113,6 @@ const getSheetFromColorMap = (map: ColorMap) => ({
|
|||
|
||||
actionListHeaderSelector: {
|
||||
display: 'inline-flex',
|
||||
'margin-right': '10px',
|
||||
},
|
||||
|
||||
actionListWide: {
|
||||
|
@ -334,6 +336,55 @@ const getSheetFromColorMap = (map: ColorMap) => ({
|
|||
'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: {
|
||||
padding: '2px 3px',
|
||||
'border-radius': '3px',
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import type { Action } from 'redux';
|
||||
import type { LiftedState, PerformAction } from '@redux-devtools/core';
|
||||
import { ActionForm } from '../redux';
|
||||
import { makeSelectRtkQueryActionRegex } from './rtk-query';
|
||||
import { createShallowEqualSelector, SelectorsSource } from './selectors';
|
||||
|
||||
type ComputedStates<S> = LiftedState<
|
||||
S,
|
||||
|
@ -34,7 +36,7 @@ function filterActionsBySearchValue<A extends Action<unknown>>(
|
|||
): number[] {
|
||||
const lowerSearchValue = searchValue && searchValue.toLowerCase();
|
||||
|
||||
if (!lowerSearchValue) {
|
||||
if (!lowerSearchValue || !actionIds.length) {
|
||||
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>> {
|
||||
readonly actionIds: number[];
|
||||
readonly actions: Record<number, PerformAction<A>>;
|
||||
readonly computedStates: ComputedStates<S>;
|
||||
readonly actionForm: ActionForm;
|
||||
readonly rtkQueryRegex: RegExp | null;
|
||||
}
|
||||
|
||||
export function filterActions<S, A extends Action<unknown>>({
|
||||
function filterActions<S, A extends Action<unknown>>({
|
||||
actionIds,
|
||||
actions,
|
||||
computedStates,
|
||||
actionForm,
|
||||
rtkQueryRegex,
|
||||
}: FilterActionsPayload<S, A>): number[] {
|
||||
let output = filterActionsBySearchValue(
|
||||
actionForm.searchValue,
|
||||
|
@ -68,8 +87,44 @@ export function filterActions<S, A extends Action<unknown>>({
|
|||
);
|
||||
|
||||
if (actionForm.isNoopFilterActive) {
|
||||
output = filterStateChangingAction(actionIds, computedStates);
|
||||
output = filterStateChangingAction(output, computedStates);
|
||||
}
|
||||
|
||||
if (actionForm.isRtkQueryFilterActive && rtkQueryRegex) {
|
||||
output = filterOutRtkQueryActions(output, actions, rtkQueryRegex);
|
||||
}
|
||||
|
||||
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:
|
||||
"@redux-devtools/core": ^3.9.0
|
||||
"@redux-devtools/dock-monitor": ^1.4.0
|
||||
"@reduxjs/toolkit": ^1.6.0
|
||||
"@types/dateformat": ^3.0.1
|
||||
"@types/dragula": ^3.7.0
|
||||
"@types/hex-rgba": ^1.0.0
|
||||
|
@ -3632,6 +3633,7 @@ __metadata:
|
|||
redux: ^4.1.0
|
||||
redux-devtools-themes: ^1.0.0
|
||||
redux-logger: ^3.0.6
|
||||
reselect: ^4.0.0
|
||||
seamless-immutable: ^7.1.4
|
||||
peerDependencies:
|
||||
"@redux-devtools/core": ^3.7.0
|
||||
|
@ -3730,6 +3732,26 @@ __metadata:
|
|||
languageName: unknown
|
||||
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":
|
||||
version: 2.1.4
|
||||
resolution: "@restart/context@npm:2.1.4"
|
||||
|
@ -14909,6 +14931,13 @@ fsevents@^1.2.7:
|
|||
languageName: node
|
||||
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":
|
||||
version: 4.0.0-rc.12
|
||||
resolution: "immutable@npm:4.0.0-rc.12"
|
||||
|
@ -23416,6 +23445,13 @@ fsevents@^1.2.7:
|
|||
languageName: node
|
||||
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":
|
||||
version: 2.0.0
|
||||
resolution: "resolve-cwd@npm:2.0.0"
|
||||
|
|
Loading…
Reference in New Issue
Block a user