From 9c63c712fc03bd912299434a0c703b0471da50db Mon Sep 17 00:00:00 2001 From: FaberVitale Date: Sat, 24 Jul 2021 12:56:08 +0200 Subject: [PATCH 1/6] feat(inspector-monitor): add noop action toggle button --- .../src/ActionList.tsx | 28 +++--- .../src/ActionListHeader.tsx | 97 +++++++++++++------ .../src/DevtoolsInspector.tsx | 14 ++- .../src/redux.ts | 54 +++++++++-- .../src/utils/createStylingFromTheme.ts | 4 + .../src/utils/filters.ts | 75 ++++++++++++++ 6 files changed, 214 insertions(+), 58 deletions(-) create mode 100644 packages/redux-devtools-inspector-monitor/src/utils/filters.ts diff --git a/packages/redux-devtools-inspector-monitor/src/ActionList.tsx b/packages/redux-devtools-inspector-monitor/src/ActionList.tsx index 22085b0e..4bfbd2af 100644 --- a/packages/redux-devtools-inspector-monitor/src/ActionList.tsx +++ b/packages/redux-devtools-inspector-monitor/src/ActionList.tsx @@ -6,6 +6,8 @@ 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>( actions: { [actionId: number]: PerformAction }, @@ -21,15 +23,16 @@ function getTimestamps>( }; } -interface Props> { +interface Props> { actions: { [actionId: number]: PerformAction }; actionIds: number[]; isWideLayout: boolean; - searchValue: string | undefined; selectedActionId: number | null; startActionId: number | null; skippedActionIds: number[]; + computedStates: { state: S; error?: string }[]; draggableActions: boolean; + actionForm: ActionForm; hideMainButtons: boolean | undefined; hideActionButtons: boolean | undefined; styling: StylingFunction; @@ -40,18 +43,20 @@ interface Props> { onCommit: () => void; onSweep: () => void; onReorderAction: (actionId: number, beforeActionId: number) => void; + onActionFormChange: (formValues: Partial) => void; currentActionId: number; lastActionId: number; } export default class ActionList< + S, A extends Action -> extends PureComponent> { +> extends PureComponent> { node?: HTMLDivElement | null; scrollDown?: boolean; drake?: Drake; - UNSAFE_componentWillReceiveProps(nextProps: Props) { + UNSAFE_componentWillReceiveProps(nextProps: Props) { const node = this.node; if (!node) { this.scrollDown = true; @@ -119,23 +124,16 @@ export default class ActionList< startActionId, onSelect, onSearch, - searchValue, currentActionId, hideMainButtons, hideActionButtons, onCommit, onSweep, + actionForm, + onActionFormChange, onJumpToState, } = this.props; - const lowerSearchValue = searchValue && searchValue.toLowerCase(); - const filteredActionIds = searchValue - ? actionIds.filter( - (id) => - (actions[id].action.type as string) - .toLowerCase() - .indexOf(lowerSearchValue as string) !== -1 - ) - : actionIds; + const filteredActionIds = filterActions(this.props); return (
0} hasStagedActions={actionIds.length > 1} diff --git a/packages/redux-devtools-inspector-monitor/src/ActionListHeader.tsx b/packages/redux-devtools-inspector-monitor/src/ActionListHeader.tsx index f913b2ed..de9be742 100644 --- a/packages/redux-devtools-inspector-monitor/src/ActionListHeader.tsx +++ b/packages/redux-devtools-inspector-monitor/src/ActionListHeader.tsx @@ -2,6 +2,7 @@ import React, { FunctionComponent } from 'react'; import PropTypes from 'prop-types'; import { StylingFunction } from 'react-base16-styling'; import RightSlider from './RightSlider'; +import { ActionForm } from './redux'; const getActiveButtons = (hasSkippedActions: boolean): ('Sweep' | 'Commit')[] => [hasSkippedActions && 'Sweep', 'Commit'].filter( @@ -16,6 +17,8 @@ interface Props { hideMainButtons: boolean | undefined; hasSkippedActions: boolean; hasStagedActions: boolean; + actionForm: ActionForm; + onActionFormChange: (formValues: Partial) => void; } const ActionListHeader: FunctionComponent = ({ @@ -26,41 +29,66 @@ const ActionListHeader: FunctionComponent = ({ onCommit, onSweep, hideMainButtons, -}) => ( -
- onSearch(e.target.value)} - placeholder="filter..." - /> - {!hideMainButtons && ( -
- -
- {getActiveButtons(hasSkippedActions).map((btn) => ( -
- ({ - Commit: onCommit, - Sweep: onSweep, - }[btn]()) - } - {...styling( - ['selectorButton', 'selectorButtonSmall'], - false, - true - )} - > - {btn} + actionForm, + onActionFormChange, +}) => { + const { isNoopFilterActive } = actionForm; + + return ( + <> +
+ onSearch(e.target.value)} + placeholder="filter..." + /> + {!hideMainButtons && ( +
+ +
+ {getActiveButtons(hasSkippedActions).map((btn) => ( +
+ ({ + Commit: onCommit, + Sweep: onSweep, + }[btn]()) + } + {...styling( + ['selectorButton', 'selectorButtonSmall'], + false, + true + )} + > + {btn} +
+ ))}
- ))} +
- + )}
- )} -
-); +
+ +
+ + ); +}; ActionListHeader.propTypes = { styling: PropTypes.func.isRequired, @@ -70,6 +98,11 @@ ActionListHeader.propTypes = { hideMainButtons: PropTypes.bool, hasSkippedActions: PropTypes.bool.isRequired, hasStagedActions: PropTypes.bool.isRequired, + actionForm: PropTypes.shape({ + searchValue: PropTypes.string.isRequired, + isNoopFilterActive: PropTypes.bool.isRequired, + }).isRequired, + onActionFormChange: PropTypes.func.isRequired, }; export default ActionListHeader; diff --git a/packages/redux-devtools-inspector-monitor/src/DevtoolsInspector.tsx b/packages/redux-devtools-inspector-monitor/src/DevtoolsInspector.tsx index aa9803fa..3681fc5a 100644 --- a/packages/redux-devtools-inspector-monitor/src/DevtoolsInspector.tsx +++ b/packages/redux-devtools-inspector-monitor/src/DevtoolsInspector.tsx @@ -22,6 +22,8 @@ import ActionPreview, { Tab } from './ActionPreview'; import getInspectedState from './utils/getInspectedState'; import createDiffPatcher from './createDiffPatcher'; import { + ActionForm, + changeActionFormValues, DevtoolsInspectorAction, DevtoolsInspectorState, reducer, @@ -249,6 +251,10 @@ class DevtoolsInspector> extends PureComponent< this.props.dispatch(updateMonitorState(monitorState)); }; + handleActionFormChange = (formValues: Partial) => { + this.props.dispatch(changeActionFormValues(formValues)); + }; + updateSizeMode() { const isWideLayout = this.inspectorRef!.offsetWidth > 500; @@ -301,7 +307,7 @@ class DevtoolsInspector> extends PureComponent< hideMainButtons, hideActionButtons, } = this.props; - const { selectedActionId, startActionId, searchValue, tabName } = + const { selectedActionId, startActionId, actionForm, tabName } = monitorState; const inspectedPathType = tabName === 'Action' ? 'inspectedActionPath' : 'inspectedStatePath'; @@ -323,7 +329,6 @@ class DevtoolsInspector> extends PureComponent< actions, actionIds, isWideLayout, - searchValue, selectedActionId, startActionId, skippedActionIds, @@ -332,7 +337,10 @@ class DevtoolsInspector> extends PureComponent< hideActionButtons, styling, }} + actionForm={actionForm} + computedStates={computedStates} onSearch={this.handleSearch} + onActionFormChange={this.handleActionFormChange} onSelect={this.handleSelectAction} onToggleAction={this.handleToggleAction} onJumpToState={this.handleJumpToState} @@ -400,7 +408,7 @@ class DevtoolsInspector> extends PureComponent< }; handleSearch = (val: string) => { - this.updateMonitorState({ searchValue: val }); + this.handleActionFormChange({ searchValue: val }); }; handleSelectAction = ( diff --git a/packages/redux-devtools-inspector-monitor/src/redux.ts b/packages/redux-devtools-inspector-monitor/src/redux.ts index 93b2835d..b82c742b 100644 --- a/packages/redux-devtools-inspector-monitor/src/redux.ts +++ b/packages/redux-devtools-inspector-monitor/src/redux.ts @@ -4,17 +4,38 @@ import { DevtoolsInspectorProps } from './DevtoolsInspector'; const UPDATE_MONITOR_STATE = '@@redux-devtools-inspector-monitor/UPDATE_MONITOR_STATE'; +const ACTION_FORM_VALUE_CHANGE = + '@@redux-devtools-inspector-monitor/ACTION_FORM_VALUE_CHANGE'; + +export interface ActionForm { + searchValue: string; + isNoopFilterActive: boolean; +} export interface UpdateMonitorStateAction { type: typeof UPDATE_MONITOR_STATE; monitorState: Partial; } + +export interface ChangeActionFormAction { + type: typeof ACTION_FORM_VALUE_CHANGE; + formValues: Partial; +} + export function updateMonitorState( monitorState: Partial ): UpdateMonitorStateAction { return { type: UPDATE_MONITOR_STATE, monitorState }; } -export type DevtoolsInspectorAction = UpdateMonitorStateAction; +export function changeActionFormValues( + formValues: Partial +): ChangeActionFormAction { + return { type: ACTION_FORM_VALUE_CHANGE, formValues }; +} + +export type DevtoolsInspectorAction = + | UpdateMonitorStateAction + | ChangeActionFormAction; export interface DevtoolsInspectorState { selectedActionId: number | null; @@ -22,7 +43,7 @@ export interface DevtoolsInspectorState { inspectedActionPath: (string | number)[]; inspectedStatePath: (string | number)[]; tabName: string; - searchValue?: string; + actionForm: ActionForm; } export const DEFAULT_STATE: DevtoolsInspectorState = { @@ -31,18 +52,33 @@ export const DEFAULT_STATE: DevtoolsInspectorState = { inspectedActionPath: [], inspectedStatePath: [], tabName: 'Diff', + actionForm: { + searchValue: '', + isNoopFilterActive: false, + }, }; -function reduceUpdateState( +function internalMonitorActionReducer( state: DevtoolsInspectorState, action: DevtoolsInspectorAction -) { - return action.type === UPDATE_MONITOR_STATE - ? { +): DevtoolsInspectorState { + switch (action.type) { + case UPDATE_MONITOR_STATE: + return { ...state, ...action.monitorState, - } - : state; + }; + case ACTION_FORM_VALUE_CHANGE: + return { + ...state, + actionForm: { + ...state.actionForm, + ...action.formValues, + }, + }; + default: + return state; + } } export function reducer>( @@ -51,6 +87,6 @@ export function reducer>( action: DevtoolsInspectorAction ) { return { - ...reduceUpdateState(state, action), + ...internalMonitorActionReducer(state, action), }; } diff --git a/packages/redux-devtools-inspector-monitor/src/utils/createStylingFromTheme.ts b/packages/redux-devtools-inspector-monitor/src/utils/createStylingFromTheme.ts index a8e5f88f..0ef71768 100644 --- a/packages/redux-devtools-inspector-monitor/src/utils/createStylingFromTheme.ts +++ b/packages/redux-devtools-inspector-monitor/src/utils/createStylingFromTheme.ts @@ -83,6 +83,10 @@ const getSheetFromColorMap = (map: ColorMap) => ({ 'border-color': map.LIST_BORDER_COLOR, }, + actionListHeaderSecondRow: { + padding: '5px 10px', + }, + actionListRows: { overflow: 'auto', diff --git a/packages/redux-devtools-inspector-monitor/src/utils/filters.ts b/packages/redux-devtools-inspector-monitor/src/utils/filters.ts new file mode 100644 index 00000000..a4cfe451 --- /dev/null +++ b/packages/redux-devtools-inspector-monitor/src/utils/filters.ts @@ -0,0 +1,75 @@ +import type { Action } from 'redux'; +import type { LiftedState, PerformAction } from '@redux-devtools/core'; +import { ActionForm } from '../redux'; + +type ComputedStates = LiftedState< + S, + Action, + unknown +>['computedStates']; + +function isNoopAction( + actionId: number, + computedStates: ComputedStates +): boolean { + return ( + actionId === 0 || + computedStates[actionId]?.state === computedStates[actionId - 1]?.state + ); +} + +function filterStateChangingAction( + actionIds: number[], + computedStates: ComputedStates +): number[] { + return actionIds.filter( + (actionId) => !isNoopAction(actionId, computedStates) + ); +} + +function filterActionsBySearchValue
>( + searchValue: string | undefined, + actionIds: number[], + actions: Record> +): number[] { + const lowerSearchValue = searchValue && searchValue.toLowerCase(); + + if (!lowerSearchValue) { + return actionIds; + } + + return actionIds.filter((id) => { + const type = actions[id].action.type; + + return ( + type != null && + `${type as string}`.toLowerCase().includes(lowerSearchValue) + ); + }); +} + +export interface FilterActionsPayload> { + readonly actionIds: number[]; + readonly actions: Record>; + readonly computedStates: ComputedStates; + readonly actionForm: ActionForm; +} + +export function filterActions>({ + actionIds, + actions, + computedStates, + actionForm, +}: FilterActionsPayload): number[] { + let output = filterActionsBySearchValue( + actionForm.searchValue, + actionIds, + actions + ); + + if (actionForm.isNoopFilterActive) { + output = filterStateChangingAction(actionIds, computedStates); + } + + return output; +} From 229869698b86dc744c0ecc49835f3310fad34f91 Mon Sep 17 00:00:00 2001 From: FaberVitale Date: Sat, 24 Jul 2021 17:29:51 +0200 Subject: [PATCH 2/6] refactor(inspector-monitor): debounce input search updates --- .../src/DevtoolsInspector.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/redux-devtools-inspector-monitor/src/DevtoolsInspector.tsx b/packages/redux-devtools-inspector-monitor/src/DevtoolsInspector.tsx index 3681fc5a..4fbdae9d 100644 --- a/packages/redux-devtools-inspector-monitor/src/DevtoolsInspector.tsx +++ b/packages/redux-devtools-inspector-monitor/src/DevtoolsInspector.tsx @@ -21,6 +21,7 @@ import ActionList from './ActionList'; import ActionPreview, { Tab } from './ActionPreview'; import getInspectedState from './utils/getInspectedState'; import createDiffPatcher from './createDiffPatcher'; +import debounce from 'lodash.debounce'; import { ActionForm, changeActionFormValues, @@ -407,9 +408,9 @@ class DevtoolsInspector> extends PureComponent< this.props.dispatch(sweep()); }; - handleSearch = (val: string) => { + handleSearch = debounce((val: string) => { this.handleActionFormChange({ searchValue: val }); - }; + }, 200); handleSelectAction = ( e: React.MouseEvent, From 0af1d2b30197b1f5a3b7daa6bfe1d91e4d6bc1b6 Mon Sep 17 00:00:00 2001 From: FaberVitale Date: Sat, 24 Jul 2021 17:38:09 +0200 Subject: [PATCH 3/6] feat(inspector-monitor): add rtk-query toggle button #753 --- .../demo/src/js/components/Pokemon.tsx | 69 ++++++++ .../demo/src/js/components/PokemonView.tsx | 31 ++++ .../demo/src/js/{ => containers}/DemoApp.tsx | 15 +- .../demo/src/js/{ => containers}/DevTools.tsx | 8 +- .../demo/src/js/index.tsx | 18 +- .../demo/src/js/reducers.ts | 3 + .../demo/src/js/rtk-query/pokemon.data.ts | 155 ++++++++++++++++++ .../demo/src/js/rtk-query/pokemonApi.ts | 20 +++ .../package.json | 4 +- .../src/ActionList.tsx | 10 +- .../src/ActionListHeader.tsx | 56 ++++--- .../src/DevtoolsInspector.tsx | 14 +- .../src/redux.ts | 2 + .../src/utils/createStylingFromTheme.ts | 55 ++++++- .../src/utils/filters.ts | 61 ++++++- .../src/utils/object.ts | 28 ++++ .../src/utils/regexp.ts | 17 ++ .../src/utils/rtk-query.ts | 107 ++++++++++++ .../src/utils/selectors.ts | 55 +++++++ .../src/utils/shallowEqual.ts | 47 ++++++ yarn.lock | 36 ++++ 21 files changed, 763 insertions(+), 48 deletions(-) create mode 100644 packages/redux-devtools-inspector-monitor/demo/src/js/components/Pokemon.tsx create mode 100644 packages/redux-devtools-inspector-monitor/demo/src/js/components/PokemonView.tsx rename packages/redux-devtools-inspector-monitor/demo/src/js/{ => containers}/DemoApp.tsx (96%) rename packages/redux-devtools-inspector-monitor/demo/src/js/{ => containers}/DevTools.tsx (86%) create mode 100644 packages/redux-devtools-inspector-monitor/demo/src/js/rtk-query/pokemon.data.ts create mode 100644 packages/redux-devtools-inspector-monitor/demo/src/js/rtk-query/pokemonApi.ts create mode 100644 packages/redux-devtools-inspector-monitor/src/utils/object.ts create mode 100644 packages/redux-devtools-inspector-monitor/src/utils/regexp.ts create mode 100644 packages/redux-devtools-inspector-monitor/src/utils/rtk-query.ts create mode 100644 packages/redux-devtools-inspector-monitor/src/utils/selectors.ts create mode 100644 packages/redux-devtools-inspector-monitor/src/utils/shallowEqual.ts diff --git a/packages/redux-devtools-inspector-monitor/demo/src/js/components/Pokemon.tsx b/packages/redux-devtools-inspector-monitor/demo/src/js/components/Pokemon.tsx new file mode 100644 index 00000000..a263d502 --- /dev/null +++ b/packages/redux-devtools-inspector-monitor/demo/src/js/components/Pokemon.tsx @@ -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 ( +
+ {error ? ( + <>Oh no, there was an error loading {name} + ) : isLoading ? ( + <>Loading... + ) : data ? ( + <> +

{data.species.name}

+
+ {data.species.name} +
+
+ + +
+
+ +
+ + ) : ( + 'No Data' + )} +
+ ); +}; diff --git a/packages/redux-devtools-inspector-monitor/demo/src/js/components/PokemonView.tsx b/packages/redux-devtools-inspector-monitor/demo/src/js/components/PokemonView.tsx new file mode 100644 index 00000000..67314f61 --- /dev/null +++ b/packages/redux-devtools-inspector-monitor/demo/src/js/components/PokemonView.tsx @@ -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(['bulbasaur']); + + return ( +
+
+ + +
+ + {pokemon.map((name, index) => ( + + ))} +
+ ); +} diff --git a/packages/redux-devtools-inspector-monitor/demo/src/js/DemoApp.tsx b/packages/redux-devtools-inspector-monitor/demo/src/js/containers/DemoApp.tsx similarity index 96% rename from packages/redux-devtools-inspector-monitor/demo/src/js/DemoApp.tsx rename to packages/redux-devtools-inspector-monitor/demo/src/js/containers/DemoApp.tsx index 83903219..e242e95b 100644 --- a/packages/redux-devtools-inspector-monitor/demo/src/js/DemoApp.tsx +++ b/packages/redux-devtools-inspector-monitor/demo/src/js/containers/DemoApp.tsx @@ -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 { Shuffle Array
+
diff --git a/packages/redux-devtools-inspector-monitor/demo/src/js/DevTools.tsx b/packages/redux-devtools-inspector-monitor/demo/src/js/containers/DevTools.tsx similarity index 86% rename from packages/redux-devtools-inspector-monitor/demo/src/js/DevTools.tsx rename to packages/redux-devtools-inspector-monitor/demo/src/js/containers/DevTools.tsx index 2d5b7c84..658e3054 100644 --- a/packages/redux-devtools-inspector-monitor/demo/src/js/DevTools.tsx +++ b/packages/redux-devtools-inspector-monitor/demo/src/js/containers/DevTools.tsx @@ -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 = () => (
+ getDefaultMiddleware({ + immutableCheck: false, + serializableCheck: false, + }).concat([pokemonApi.middleware]), + enhancers: [enhancer], +}); // createStore(createRootReducer(history), enhancer); render( diff --git a/packages/redux-devtools-inspector-monitor/demo/src/js/reducers.ts b/packages/redux-devtools-inspector-monitor/demo/src/js/reducers.ts index 3fdbe4fc..20d87ad5 100644 --- a/packages/redux-devtools-inspector-monitor/demo/src/js/reducers.ts +++ b/packages/redux-devtools-inspector-monitor/demo/src/js/reducers.ts @@ -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; } 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; diff --git a/packages/redux-devtools-inspector-monitor/demo/src/js/rtk-query/pokemon.data.ts b/packages/redux-devtools-inspector-monitor/demo/src/js/rtk-query/pokemon.data.ts new file mode 100644 index 00000000..83150cb7 --- /dev/null +++ b/packages/redux-devtools-inspector-monitor/demo/src/js/rtk-query/pokemon.data.ts @@ -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]; diff --git a/packages/redux-devtools-inspector-monitor/demo/src/js/rtk-query/pokemonApi.ts b/packages/redux-devtools-inspector-monitor/demo/src/js/rtk-query/pokemonApi.ts new file mode 100644 index 00000000..22dbe3d4 --- /dev/null +++ b/packages/redux-devtools-inspector-monitor/demo/src/js/rtk-query/pokemonApi.ts @@ -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; diff --git a/packages/redux-devtools-inspector-monitor/package.json b/packages/redux-devtools-inspector-monitor/package.json index 00a52500..0bcceefc 100644 --- a/packages/redux-devtools-inspector-monitor/package.json +++ b/packages/redux-devtools-inspector-monitor/package.json @@ -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", diff --git a/packages/redux-devtools-inspector-monitor/src/ActionList.tsx b/packages/redux-devtools-inspector-monitor/src/ActionList.tsx index 4bfbd2af..1f545ad9 100644 --- a/packages/redux-devtools-inspector-monitor/src/ActionList.tsx +++ b/packages/redux-devtools-inspector-monitor/src/ActionList.tsx @@ -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>( @@ -30,9 +29,9 @@ interface Props> { 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(this.props); - return (
1} />
- {filteredActionIds.map((actionId) => ( + {filteredActionIds.map((actionId: number) => ( = ({ actionForm, onActionFormChange, }) => { - const { isNoopFilterActive } = actionForm; + const { isNoopFilterActive, isRtkQueryFilterActive } = actionForm; return ( <> @@ -42,7 +42,37 @@ const ActionListHeader: FunctionComponent = ({ onChange={(e) => onSearch(e.target.value)} placeholder="filter..." /> - {!hideMainButtons && ( +
+ + +
+
+ {!hideMainButtons && ( +
@@ -67,25 +97,8 @@ const ActionListHeader: FunctionComponent = ({
- )} -
-
- -
+
+ )} ); }; @@ -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, }; diff --git a/packages/redux-devtools-inspector-monitor/src/DevtoolsInspector.tsx b/packages/redux-devtools-inspector-monitor/src/DevtoolsInspector.tsx index 4fbdae9d..a2f815f2 100644 --- a/packages/redux-devtools-inspector-monitor/src/DevtoolsInspector.tsx +++ b/packages/redux-devtools-inspector-monitor/src/DevtoolsInspector.tsx @@ -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> extends PureComponent< updateSizeTimeout?: number; inspectorRef?: HTMLDivElement | null; + selectorsSource = computeSelectorSource(this.props); + componentDidMount() { this.updateSizeMode(); this.updateSizeTimeout = window.setInterval( @@ -293,6 +297,8 @@ class DevtoolsInspector> extends PureComponent< this.inspectorRef = node; }; + selectFilteredActions = makeSelectFilteredActions(); + render() { const { stagedActionIds: actionIds, @@ -316,6 +322,12 @@ class DevtoolsInspector> extends PureComponent< this.state; const { base16Theme, styling } = themeState; + this.selectorsSource = computeSelectorSource( + this.props, + this.selectorsSource + ); + const filteredActionIds = this.selectFilteredActions(this.selectorsSource); + return (
> extends PureComponent< styling, }} actionForm={actionForm} - computedStates={computedStates} + filteredActionIds={filteredActionIds} onSearch={this.handleSearch} onActionFormChange={this.handleActionFormChange} onSelect={this.handleSelectAction} diff --git a/packages/redux-devtools-inspector-monitor/src/redux.ts b/packages/redux-devtools-inspector-monitor/src/redux.ts index b82c742b..7e3616fb 100644 --- a/packages/redux-devtools-inspector-monitor/src/redux.ts +++ b/packages/redux-devtools-inspector-monitor/src/redux.ts @@ -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, }, }; diff --git a/packages/redux-devtools-inspector-monitor/src/utils/createStylingFromTheme.ts b/packages/redux-devtools-inspector-monitor/src/utils/createStylingFromTheme.ts index 0ef71768..f38f59c8 100644 --- a/packages/redux-devtools-inspector-monitor/src/utils/createStylingFromTheme.ts +++ b/packages/redux-devtools-inspector-monitor/src/utils/createStylingFromTheme.ts @@ -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; @@ -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', diff --git a/packages/redux-devtools-inspector-monitor/src/utils/filters.ts b/packages/redux-devtools-inspector-monitor/src/utils/filters.ts index a4cfe451..37c1ce80 100644 --- a/packages/redux-devtools-inspector-monitor/src/utils/filters.ts +++ b/packages/redux-devtools-inspector-monitor/src/utils/filters.ts @@ -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 = LiftedState< S, @@ -34,7 +36,7 @@ function filterActionsBySearchValue>( ): number[] { const lowerSearchValue = searchValue && searchValue.toLowerCase(); - if (!lowerSearchValue) { + if (!lowerSearchValue || !actionIds.length) { return actionIds; } @@ -48,18 +50,35 @@ function filterActionsBySearchValue>( }); } +function filterOutRtkQueryActions( + actionIds: number[], + actions: Record>>, + 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> { readonly actionIds: number[]; readonly actions: Record>; readonly computedStates: ComputedStates; readonly actionForm: ActionForm; + readonly rtkQueryRegex: RegExp | null; } -export function filterActions>({ +function filterActions>({ actionIds, actions, computedStates, actionForm, + rtkQueryRegex, }: FilterActionsPayload): number[] { let output = filterActionsBySearchValue( actionForm.searchValue, @@ -68,8 +87,44 @@ export function filterActions>({ ); 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> { + (selectorsSource: SelectorsSource): number[]; +} + +/** + * Creates a selector that given `SelectorsSource` returns + * a list of filtered `actionsIds`. + * @returns {number[]} + */ +export function makeSelectFilteredActions< + S, + A extends Action +>(): SelectFilteredActions { + const selectRegex = makeSelectRtkQueryActionRegex(); + + return createShallowEqualSelector( + (selectorsSource: SelectorsSource): FilterActionsPayload => { + const actionForm = selectorsSource.monitorState.actionForm; + const { actionIds, actions, computedStates } = selectorsSource; + + return { + actionIds, + actions, + computedStates, + actionForm, + rtkQueryRegex: selectRegex(selectorsSource), + }; + }, + filterActions + ); +} diff --git a/packages/redux-devtools-inspector-monitor/src/utils/object.ts b/packages/redux-devtools-inspector-monitor/src/utils/object.ts new file mode 100644 index 00000000..98b886f6 --- /dev/null +++ b/packages/redux-devtools-inspector-monitor/src/utils/object.ts @@ -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 { + 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(val: T): T { + return val; +} diff --git a/packages/redux-devtools-inspector-monitor/src/utils/regexp.ts b/packages/redux-devtools-inspector-monitor/src/utils/regexp.ts new file mode 100644 index 00000000..5901ecb2 --- /dev/null +++ b/packages/redux-devtools-inspector-monitor/src/utils/regexp.ts @@ -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('|')})`; +} diff --git a/packages/redux-devtools-inspector-monitor/src/utils/rtk-query.ts b/packages/redux-devtools-inspector-monitor/src/utils/rtk-query.ts new file mode 100644 index 00000000..ca9ce37a --- /dev/null +++ b/packages/redux-devtools-inspector-monitor/src/utils/rtk-query.ts @@ -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; + mutations: Record; + config: Record; + provided: Record; + subscriptions: Record; +} + +const rtkqueryApiStateKeys: ReadonlyArray = [ + '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> { + (selectorsSource: SelectorsSource): RegExp | null; +} + +export function makeSelectRtkQueryActionRegex< + S, + A extends Action +>(): SelectRTKQueryActionRegex { + const selectApiReducerPaths = createSelector( + (source: SelectorsSource) => + source.computedStates[source.currentStateIndex].state, + getApiReducerPaths + ); + + const selectRtkQueryActionRegex = createShallowEqualSelector( + selectApiReducerPaths, + generateRtkQueryActionRegex + ); + + return selectRtkQueryActionRegex; +} diff --git a/packages/redux-devtools-inspector-monitor/src/utils/selectors.ts b/packages/redux-devtools-inspector-monitor/src/utils/selectors.ts new file mode 100644 index 00000000..790e83dc --- /dev/null +++ b/packages/redux-devtools-inspector-monitor/src/utils/selectors.ts @@ -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 = LiftedState< + S, + Action, + unknown +>['computedStates']; + +export interface SelectorsSource> { + readonly actionIds: number[]; + readonly actions: Record>; + readonly computedStates: ComputedStates; + readonly monitorState: DevtoolsInspectorState; + readonly currentStateIndex: number; +} + +export function computeSelectorSource>( + props: DevtoolsInspectorProps, + previous: SelectorsSource | null = null +): SelectorsSource { + const { + computedStates, + currentStateIndex, + monitorState, + stagedActionIds, + actionsById, + } = props; + + const next: SelectorsSource = { + currentStateIndex, + monitorState, + computedStates, + actions: actionsById, + actionIds: stagedActionIds, + }; + + if (previous && shallowEqual(next, previous)) { + return previous; + } + + return next; +} diff --git a/packages/redux-devtools-inspector-monitor/src/utils/shallowEqual.ts b/packages/redux-devtools-inspector-monitor/src/utils/shallowEqual.ts new file mode 100644 index 00000000..536ffd1d --- /dev/null +++ b/packages/redux-devtools-inspector-monitor/src/utils/shallowEqual.ts @@ -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)[keysA[i]], + (objB as Record)[keysA[i]] + ) + ) { + return false; + } + } + + return true; +} diff --git a/yarn.lock b/yarn.lock index 90a9dbce..c2be60a7 100644 --- a/yarn.lock +++ b/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" From 4c8a6c928abbaec14b88776d78206f123f86d792 Mon Sep 17 00:00:00 2001 From: FaberVitale Date: Sun, 25 Jul 2021 12:22:19 +0200 Subject: [PATCH 4/6] feat(inspector-monitor): add inverse search button See also #476 --- .../src/ActionListHeader.tsx | 44 ++++++++++++++++--- .../src/redux.ts | 8 ++-- .../src/utils/filters.ts | 22 ++++++++++ 3 files changed, 64 insertions(+), 10 deletions(-) diff --git a/packages/redux-devtools-inspector-monitor/src/ActionListHeader.tsx b/packages/redux-devtools-inspector-monitor/src/ActionListHeader.tsx index 961eaf36..70333c0c 100644 --- a/packages/redux-devtools-inspector-monitor/src/ActionListHeader.tsx +++ b/packages/redux-devtools-inspector-monitor/src/ActionListHeader.tsx @@ -9,6 +9,20 @@ const getActiveButtons = (hasSkippedActions: boolean): ('Sweep' | 'Commit')[] => (a): a is 'Sweep' | 'Commit' => !!a ); +const toggleButton = { + label: { + rtkq(val: boolean) { + return val ? 'Show rtk-query actions' : 'Hide rtk-query actions'; + }, + noop(val: boolean) { + return val ? 'Show noop actions' : 'Hide noop actions'; + }, + invertSearch(val: boolean) { + return val ? 'Disable inverse search' : 'Activate inverse search'; + }, + }, +}; + interface Props { styling: StylingFunction; onSearch: (value: string) => void; @@ -32,7 +46,8 @@ const ActionListHeader: FunctionComponent = ({ actionForm, onActionFormChange, }) => { - const { isNoopFilterActive, isRtkQueryFilterActive } = actionForm; + const { isNoopFilterActive, isRtkQueryFilterActive, isInvertSearchActive } = + actionForm; return ( <> @@ -44,9 +59,9 @@ const ActionListHeader: FunctionComponent = ({ />
+
{!hideMainButtons && ( @@ -115,6 +144,7 @@ ActionListHeader.propTypes = { searchValue: PropTypes.string.isRequired, isNoopFilterActive: PropTypes.bool.isRequired, isRtkQueryFilterActive: PropTypes.bool.isRequired, + isInvertSearchActive: PropTypes.bool.isRequired, }).isRequired, onActionFormChange: PropTypes.func.isRequired, }; diff --git a/packages/redux-devtools-inspector-monitor/src/redux.ts b/packages/redux-devtools-inspector-monitor/src/redux.ts index 7e3616fb..2553042d 100644 --- a/packages/redux-devtools-inspector-monitor/src/redux.ts +++ b/packages/redux-devtools-inspector-monitor/src/redux.ts @@ -8,9 +8,10 @@ const ACTION_FORM_VALUE_CHANGE = '@@redux-devtools-inspector-monitor/ACTION_FORM_VALUE_CHANGE'; export interface ActionForm { - searchValue: string; - isNoopFilterActive: boolean; - isRtkQueryFilterActive: boolean; + readonly searchValue: string; + readonly isNoopFilterActive: boolean; + readonly isRtkQueryFilterActive: boolean; + readonly isInvertSearchActive: boolean; } export interface UpdateMonitorStateAction { type: typeof UPDATE_MONITOR_STATE; @@ -57,6 +58,7 @@ export const DEFAULT_STATE: DevtoolsInspectorState = { searchValue: '', isNoopFilterActive: false, isRtkQueryFilterActive: false, + isInvertSearchActive: false, }, }; diff --git a/packages/redux-devtools-inspector-monitor/src/utils/filters.ts b/packages/redux-devtools-inspector-monitor/src/utils/filters.ts index 37c1ce80..ec3f8008 100644 --- a/packages/redux-devtools-inspector-monitor/src/utils/filters.ts +++ b/packages/redux-devtools-inspector-monitor/src/utils/filters.ts @@ -65,6 +65,24 @@ function filterOutRtkQueryActions( return typeof type !== 'string' || !rtkQueryRegex.test(type); }); } + +function invertSearchResults( + actionIds: number[], + filteredActionIds: number[] +): number[] { + if ( + actionIds.length === 0 || + actionIds.length === filteredActionIds.length || + filteredActionIds.length === 0 + ) { + return actionIds; + } + + const filteredSet = new Set(filteredActionIds); + + return actionIds.filter((actionId) => !filteredSet.has(actionId)); +} + export interface FilterActionsPayload> { readonly actionIds: number[]; readonly actions: Record>; @@ -94,6 +112,10 @@ function filterActions>({ output = filterOutRtkQueryActions(output, actions, rtkQueryRegex); } + if (actionForm.isInvertSearchActive) { + output = invertSearchResults(actionIds, output); + } + return output; } From 7a579532bf81c98b5675dd6478901e15b34d2948 Mon Sep 17 00:00:00 2001 From: FaberVitale Date: Sun, 25 Jul 2021 12:47:28 +0200 Subject: [PATCH 5/6] chore(inspector-monitor): improve demo webpack config --- .../demo/config/webpack.config.ts | 4 ++-- .../redux-devtools-inspector-monitor/demo/src/js/index.tsx | 5 +---- packages/redux-devtools-inspector-monitor/package.json | 2 +- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/redux-devtools-inspector-monitor/demo/config/webpack.config.ts b/packages/redux-devtools-inspector-monitor/demo/config/webpack.config.ts index c7b08800..b4999104 100644 --- a/packages/redux-devtools-inspector-monitor/demo/config/webpack.config.ts +++ b/packages/redux-devtools-inspector-monitor/demo/config/webpack.config.ts @@ -17,7 +17,7 @@ module.exports = { './demo/src/js/index', ], output: { - path: path.join(__dirname, 'demo/dist'), + path: path.join(__dirname, './dist'), filename: 'js/bundle.js', }, module: { @@ -60,5 +60,5 @@ module.exports = { }, historyApiFallback: true, }, - devtool: 'eval-source-map', + devtool: isProduction ? 'sourcemap' : 'eval-source-map', }; diff --git a/packages/redux-devtools-inspector-monitor/demo/src/js/index.tsx b/packages/redux-devtools-inspector-monitor/demo/src/js/index.tsx index 9b90cc1c..17a02733 100644 --- a/packages/redux-devtools-inspector-monitor/demo/src/js/index.tsx +++ b/packages/redux-devtools-inspector-monitor/demo/src/js/index.tsx @@ -24,10 +24,7 @@ function getDebugSessionKey() { return matches && matches.length > 0 ? matches[1] : null; } -const ROOT = - process.env.NODE_ENV === 'production' - ? '/redux-devtools-inspector-monitor/' - : '/'; +const ROOT = '/'; const DevTools = getDevTools(window.location); diff --git a/packages/redux-devtools-inspector-monitor/package.json b/packages/redux-devtools-inspector-monitor/package.json index 0bcceefc..420d0af7 100644 --- a/packages/redux-devtools-inspector-monitor/package.json +++ b/packages/redux-devtools-inspector-monitor/package.json @@ -24,7 +24,7 @@ "scripts": { "start": "webpack-dev-server --config demo/config/webpack.config.ts", "stats": "webpack --profile --json > stats.json", - "build:demo": "NODE_ENV=production webpack -p", + "build:demo": "cross-env NODE_ENV=production webpack -p --config demo/config/webpack.config.ts", "build": "npm run build:types && npm run build:js", "build:types": "tsc --emitDeclarationOnly", "build:js": "babel src --out-dir lib --extensions \".ts,.tsx\" --source-maps inline", From b315404534e059e7edc7ffeb8b18a1bd3b2e14ef Mon Sep 17 00:00:00 2001 From: FaberVitale Date: Sun, 25 Jul 2021 13:08:25 +0200 Subject: [PATCH 6/6] fix(CI): broken test in /app --- .../redux-devtools-inspector-monitor/src/utils/rtk-query.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/redux-devtools-inspector-monitor/src/utils/rtk-query.ts b/packages/redux-devtools-inspector-monitor/src/utils/rtk-query.ts index ca9ce37a..de4cefcd 100644 --- a/packages/redux-devtools-inspector-monitor/src/utils/rtk-query.ts +++ b/packages/redux-devtools-inspector-monitor/src/utils/rtk-query.ts @@ -94,7 +94,7 @@ export function makeSelectRtkQueryActionRegex< >(): SelectRTKQueryActionRegex { const selectApiReducerPaths = createSelector( (source: SelectorsSource) => - source.computedStates[source.currentStateIndex].state, + source.computedStates[source.currentStateIndex]?.state, getApiReducerPaths );