diff --git a/packages/redux-devtools-inspector-monitor/src/searchPanel/JumpSearchResultButton.tsx b/packages/redux-devtools-inspector-monitor/src/searchPanel/JumpSearchResultButton.tsx
new file mode 100644
index 00000000..b4dce52e
--- /dev/null
+++ b/packages/redux-devtools-inspector-monitor/src/searchPanel/JumpSearchResultButton.tsx
@@ -0,0 +1,45 @@
+import { StylingFunction } from 'react-base16-styling';
+import React, { ReactElement } from 'react';
+
+export const BUTTON_DIRECTION = {
+ LEFT: 'left',
+ RIGHT: 'right',
+} as const;
+
+interface JumpSearchResultButtonProps {
+ buttonDirection: string;
+ buttonDisabled: boolean;
+ styling: StylingFunction;
+ jumpToNewResult: () => void;
+}
+
+function JumpSearchResultButton({
+ buttonDirection,
+ buttonDisabled,
+ styling,
+ jumpToNewResult,
+}: JumpSearchResultButtonProps): ReactElement {
+ return (
+
+ );
+}
+
+export default JumpSearchResultButton;
diff --git a/packages/redux-devtools-inspector-monitor/src/searchPanel/SearchBar.tsx b/packages/redux-devtools-inspector-monitor/src/searchPanel/SearchBar.tsx
new file mode 100644
index 00000000..f08764ca
--- /dev/null
+++ b/packages/redux-devtools-inspector-monitor/src/searchPanel/SearchBar.tsx
@@ -0,0 +1,30 @@
+import React, { ReactElement } from 'react';
+import { StylingFunction } from 'react-base16-styling';
+
+export interface SearchBarProps {
+ onChange: (s: string) => void;
+ text: string;
+ className?: string;
+ styling: StylingFunction;
+}
+
+function SearchBar({
+ onChange,
+ text,
+ className,
+ styling,
+}: SearchBarProps): ReactElement {
+ return (
+
+ onChange(e.target.value)}
+ />
+
+ );
+}
+
+export default SearchBar;
diff --git a/packages/redux-devtools-inspector-monitor/src/searchPanel/SearchPanel.tsx b/packages/redux-devtools-inspector-monitor/src/searchPanel/SearchPanel.tsx
new file mode 100644
index 00000000..b73fed0d
--- /dev/null
+++ b/packages/redux-devtools-inspector-monitor/src/searchPanel/SearchPanel.tsx
@@ -0,0 +1,161 @@
+import React, { ReactElement, useEffect, useState } from 'react';
+import { StylingFunction } from 'react-base16-styling';
+import { searchInObject } from '../utils/objectSearch';
+import { Value } from '../utils/searchWorker';
+import JumpSearchResultButton, {
+ BUTTON_DIRECTION,
+} from './JumpSearchResultButton';
+import SearchBar from './SearchBar';
+
+export interface SearchQuery {
+ queryText: string;
+ location: {
+ keys: boolean;
+ values: boolean;
+ };
+}
+
+const INITIAL_QUERY: SearchQuery = {
+ queryText: '',
+ location: {
+ keys: true,
+ values: true,
+ },
+};
+
+export interface SearchPanelProps {
+ state: Value;
+ onSubmit: (result: {
+ searchResult: string[];
+ searchInProgress: boolean;
+ }) => void;
+ onReset: () => void;
+ styling: StylingFunction;
+}
+
+type SearchStatus = 'done' | 'pending' | 'unset';
+
+function SearchPanel({
+ onSubmit,
+ onReset,
+ styling,
+ state,
+}: SearchPanelProps): ReactElement {
+ const [query, setQuery] = useState(INITIAL_QUERY);
+ const [searchStatus, setSearchStatus] = useState('unset');
+ const [results, setResults] = useState(undefined);
+ const [resultIndex, setResultIndex] = useState(0);
+
+ async function handleSubmit() {
+ setSearchStatus('pending');
+ const result = await searchInObject(state, query);
+ setResults(result.map((r) => r.split('.')));
+ setResultIndex(0);
+ setSearchStatus('done');
+ }
+
+ function reset() {
+ setQuery(INITIAL_QUERY);
+ setSearchStatus('unset');
+ setResults(undefined);
+ setResultIndex(0);
+ onReset();
+ }
+
+ useEffect(() => {
+ results &&
+ onSubmit({ searchResult: results[0] || [], searchInProgress: true });
+ }, [results, onSubmit]);
+
+ return (
+
+
setQuery({ ...query, queryText: text })}
+ styling={styling}
+ />
+
+
+ setQuery({
+ ...query,
+ location: { ...query.location, keys: event.target.checked },
+ })
+ }
+ />
+ Keys
+
+
+
+ setQuery({
+ ...query,
+ location: { ...query.location, values: event.target.checked },
+ })
+ }
+ />
+ Values
+
+
+ {searchStatus === 'pending' && 'Searching...'}
+ {searchStatus === 'done' && (
+ <>
+ {
+ if (!results) {
+ return;
+ }
+ const newIndex =
+ resultIndex - 1 < 0 ? results.length - 1 : resultIndex - 1;
+ setResultIndex(newIndex);
+ onSubmit({
+ searchResult: results[newIndex] || [],
+ searchInProgress: true,
+ });
+ }}
+ />
+ {
+ if (!results) {
+ return;
+ }
+ const newIndex = (resultIndex + 1) % results.length || 0;
+ setResultIndex(newIndex);
+ onSubmit({
+ searchResult: results[newIndex] || [],
+ searchInProgress: true,
+ });
+ }}
+ />
+ {results &&
+ `${results.length ? resultIndex + 1 : 0}/${results.length}`}
+
+ >
+ )}
+
+ );
+}
+
+export default SearchPanel;
diff --git a/packages/redux-devtools-inspector-monitor/src/tabs/StateTab.tsx b/packages/redux-devtools-inspector-monitor/src/tabs/StateTab.tsx
index 94c2807b..244cac77 100644
--- a/packages/redux-devtools-inspector-monitor/src/tabs/StateTab.tsx
+++ b/packages/redux-devtools-inspector-monitor/src/tabs/StateTab.tsx
@@ -1,10 +1,11 @@
-import React from 'react';
+import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { JSONTree } from 'react-json-tree';
import { Action } from 'redux';
import getItemString from './getItemString';
import getJsonTreeTheme from './getJsonTreeTheme';
import { TabComponentProps } from '../ActionPreview';
+import SearchPanel from '../searchPanel/SearchPanel';
const StateTab: React.FunctionComponent<
TabComponentProps>
@@ -18,20 +19,83 @@ const StateTab: React.FunctionComponent<
isWideLayout,
sortStateTreeAlphabetically,
disableStateTreeCollection,
-}) => (
-
- getItemString(styling, type, data, dataTypeKey, isWideLayout)
+}) => {
+ interface SearchState {
+ searchResult: string[];
+ searchInProgress: boolean;
+ }
+ const [searchState, setSearchState] = useState({
+ searchResult: [],
+ searchInProgress: false,
+ });
+
+ const displayedResult = React.useRef(null);
+
+ useEffect(() => {
+ if (searchState.searchInProgress && displayedResult.current) {
+ displayedResult.current.scrollIntoView({
+ behavior: 'smooth',
+ block: 'center',
+ inline: 'center',
+ });
+ setSearchState({ ...searchState, searchInProgress: false });
}
- invertTheme={invertTheme}
- hideRoot
- sortObjectKeys={sortStateTreeAlphabetically}
- {...(disableStateTreeCollection ? { collectionLimit: 0 } : {})}
- />
-);
+ }, [searchState, setSearchState]);
+
+ return (
+ <>
+
+ setSearchState({
+ searchResult: [],
+ searchInProgress: false,
+ })
+ }
+ styling={styling}
+ state={nextState}
+ />
+ {
+ return isMatch(searchState.searchResult, [...keyPath].reverse()) ? (
+
+ {labelRenderer(keyPath, nodeType, expanded, expandable)}
+
+ ) : (
+ labelRenderer(keyPath, nodeType, expanded, expandable)
+ );
+ }}
+ theme={getJsonTreeTheme(base16Theme)}
+ data={nextState}
+ getItemString={(type, data) =>
+ getItemString(styling, type, data, dataTypeKey, isWideLayout)
+ }
+ invertTheme={invertTheme}
+ hideRoot
+ sortObjectKeys={sortStateTreeAlphabetically}
+ {...(disableStateTreeCollection ? { collectionLimit: 0 } : {})}
+ isSearchInProgress={searchState.searchInProgress}
+ searchResultPath={searchState.searchResult}
+ valueRenderer={(raw, value, ...keyPath) => {
+ return isMatch(searchState.searchResult, [...keyPath].reverse()) ? (
+
+ {raw as string}
+
+ ) : (
+ {raw as string}
+ );
+ }}
+ />
+ >
+ );
+};
+
+const isMatch = (resultPath: string[], nodePath: (string | number)[]) => {
+ return (
+ resultPath.length === nodePath.length &&
+ resultPath.every((result, index) => result === nodePath[index].toString())
+ );
+};
StateTab.propTypes = {
nextState: PropTypes.any.isRequired,
diff --git a/packages/redux-devtools-inspector-monitor/src/utils/createStylingFromTheme.ts b/packages/redux-devtools-inspector-monitor/src/utils/createStylingFromTheme.ts
index 9bd45ea3..e71c105b 100644
--- a/packages/redux-devtools-inspector-monitor/src/utils/createStylingFromTheme.ts
+++ b/packages/redux-devtools-inspector-monitor/src/utils/createStylingFromTheme.ts
@@ -17,6 +17,7 @@ const colorMap = (theme: Base16Theme) => ({
SELECTED_BACKGROUND_COLOR: rgba(theme.base03, 20),
SKIPPED_BACKGROUND_COLOR: rgba(theme.base03, 10),
HEADER_BACKGROUND_COLOR: rgba(theme.base03, 30),
+ HEADER_BACKGROUND_COLOR_OPAQUE: rgba(theme.base03, 100),
HEADER_BORDER_COLOR: rgba(theme.base03, 20),
BORDER_COLOR: rgba(theme.base03, 50),
LIST_BORDER_COLOR: rgba(theme.base03, 50),
@@ -34,6 +35,7 @@ const colorMap = (theme: Base16Theme) => ({
LINK_COLOR: rgba(theme.base0E, 90),
LINK_HOVER_COLOR: theme.base0E,
ERROR_COLOR: theme.base08,
+ SEARCH_BUTTON_COLOR: rgba(theme.base00, 50),
});
type Color = keyof ReturnType;
@@ -267,6 +269,95 @@ const getSheetFromColorMap = (map: ColorMap) => ({
'border-bottom-color': map.HEADER_BORDER_COLOR,
},
+ searchPanel: {
+ display: 'flex',
+ position: 'sticky',
+ top: 0,
+ width: '100%',
+ 'z-index': 1,
+ height: '2em',
+ gap: '1em',
+ padding: '5px 10px',
+ 'align-items': 'center',
+ 'border-bottom-width': '1px',
+ 'border-bottom-style': 'solid',
+
+ 'background-color': map.HEADER_BACKGROUND_COLOR_OPAQUE,
+ 'border-bottom-color': map.HEADER_BORDER_COLOR,
+ },
+
+ searchForm: {
+ display: 'flex',
+ gap: '1em',
+ 'align-items': 'center',
+ },
+
+ searchPanelParameterSelection: {
+ display: 'inline-grid',
+ color: map.SEARCH_BUTTON_COLOR,
+ width: '1.15em',
+ height: '1.15em',
+ border: '0.15em solid currentColor',
+ 'border-radius': '0.15em',
+ transform: 'translateY(-0.075em)',
+ 'place-content': 'center',
+ appearance: 'none',
+ 'background-color': map.SEARCH_BUTTON_COLOR,
+
+ '&::before': {
+ 'background-color': map.TEXT_COLOR,
+ 'transform-origin': 'bottom left',
+ 'clip-path':
+ 'polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%)',
+ content: '""',
+ width: '0.65em',
+ height: '0.65em',
+ transform: 'scale(0)',
+ transition: '120ms transform ease-in-out',
+ },
+ '&:checked::before': {
+ transform: 'scale(1)',
+ },
+ },
+
+ searchInput: {
+ 'background-color': 'white',
+ opacity: 0.9,
+ 'border-color': 'transparent',
+ 'border-radius': '4px',
+ height: '1.2rem',
+ },
+
+ searchButton: {
+ 'background-color': map.SEARCH_BUTTON_COLOR,
+ 'border-color': 'transparent',
+ 'border-radius': '4px',
+ height: '1.56rem',
+ color: map.TEXT_COLOR,
+ },
+
+ jumpResultButton: {
+ 'background-color': map.SEARCH_BUTTON_COLOR,
+ 'border-color': 'transparent',
+ 'border-radius': '4px',
+ },
+
+ jumpResultButtonArrow: {
+ cursor: 'hand',
+ fill: map.TEXT_COLOR,
+ width: '0.6rem',
+ height: '1rem',
+ },
+
+ queryResult: {
+ 'background-color': '#FFFF00',
+ },
+
+ queryResultLabel: {
+ 'background-color': '#FFFF00',
+ 'text-indent': 0,
+ },
+
tabSelector: {
position: 'relative',
display: 'inline-flex',