mirror of
https://github.com/reduxjs/redux-devtools.git
synced 2025-07-22 14:09:46 +03:00
Add SearchPanel component and subcomponents, use it in StateTab
This commit is contained in:
parent
2097b49ffe
commit
7a10825e16
|
@ -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 (
|
||||
<button
|
||||
{...styling('jumpResultButton')}
|
||||
onClick={() => jumpToNewResult()}
|
||||
disabled={buttonDisabled}
|
||||
>
|
||||
<svg
|
||||
{...styling('jumpResultButtonArrow')}
|
||||
viewBox="4 0 14 18"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
>
|
||||
<g>
|
||||
{buttonDirection === BUTTON_DIRECTION.LEFT ? (
|
||||
<path d="M15.41 16.09l-4.58-4.59 4.58-4.59-1.41-1.41-6 6 6 6z" />
|
||||
) : (
|
||||
<path d="M8.59 16.34l4.58-4.59-4.58-4.59 1.41-1.41 6 6-6 6z" />
|
||||
)}
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export default JumpSearchResultButton;
|
|
@ -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 (
|
||||
<div className={`search-bar ${className || ''}`}>
|
||||
<input
|
||||
{...styling('searchInput')}
|
||||
placeholder={'Search'}
|
||||
value={text}
|
||||
type={'text'}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SearchBar;
|
|
@ -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<SearchQuery>(INITIAL_QUERY);
|
||||
const [searchStatus, setSearchStatus] = useState<SearchStatus>('unset');
|
||||
const [results, setResults] = useState<string[][] | undefined>(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 (
|
||||
<div className={'search-panel'} {...styling('searchPanel')}>
|
||||
<SearchBar
|
||||
text={query.queryText}
|
||||
onChange={(text: string) => setQuery({ ...query, queryText: text })}
|
||||
styling={styling}
|
||||
/>
|
||||
<div>
|
||||
<input
|
||||
{...styling('searchPanelParameterSelection')}
|
||||
type={'checkbox'}
|
||||
checked={query.location.keys}
|
||||
onChange={(event) =>
|
||||
setQuery({
|
||||
...query,
|
||||
location: { ...query.location, keys: event.target.checked },
|
||||
})
|
||||
}
|
||||
/>
|
||||
<span>Keys</span>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
{...styling('searchPanelParameterSelection')}
|
||||
type={'checkbox'}
|
||||
checked={query.location.values}
|
||||
onChange={(event) =>
|
||||
setQuery({
|
||||
...query,
|
||||
location: { ...query.location, values: event.target.checked },
|
||||
})
|
||||
}
|
||||
/>
|
||||
<span>Values</span>
|
||||
</div>
|
||||
<button
|
||||
{...styling('searchButton')}
|
||||
onClick={() => handleSubmit()}
|
||||
disabled={
|
||||
(!query.location.keys && !query.location.values) || !query.queryText
|
||||
}
|
||||
>
|
||||
Go
|
||||
</button>
|
||||
{searchStatus === 'pending' && 'Searching...'}
|
||||
{searchStatus === 'done' && (
|
||||
<>
|
||||
<JumpSearchResultButton
|
||||
buttonDirection={BUTTON_DIRECTION.LEFT}
|
||||
buttonDisabled={!results || results.length < 2}
|
||||
styling={styling}
|
||||
jumpToNewResult={() => {
|
||||
if (!results) {
|
||||
return;
|
||||
}
|
||||
const newIndex =
|
||||
resultIndex - 1 < 0 ? results.length - 1 : resultIndex - 1;
|
||||
setResultIndex(newIndex);
|
||||
onSubmit({
|
||||
searchResult: results[newIndex] || [],
|
||||
searchInProgress: true,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<JumpSearchResultButton
|
||||
buttonDirection={BUTTON_DIRECTION.RIGHT}
|
||||
buttonDisabled={!results || results.length < 2}
|
||||
styling={styling}
|
||||
jumpToNewResult={() => {
|
||||
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}`}
|
||||
<button {...styling('searchButton')} onClick={() => reset()}>
|
||||
Reset
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default SearchPanel;
|
|
@ -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<any, Action<unknown>>
|
||||
|
@ -18,9 +19,52 @@ const StateTab: React.FunctionComponent<
|
|||
isWideLayout,
|
||||
sortStateTreeAlphabetically,
|
||||
disableStateTreeCollection,
|
||||
}) => (
|
||||
}) => {
|
||||
interface SearchState {
|
||||
searchResult: string[];
|
||||
searchInProgress: boolean;
|
||||
}
|
||||
const [searchState, setSearchState] = useState<SearchState>({
|
||||
searchResult: [],
|
||||
searchInProgress: false,
|
||||
});
|
||||
|
||||
const displayedResult = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (searchState.searchInProgress && displayedResult.current) {
|
||||
displayedResult.current.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
inline: 'center',
|
||||
});
|
||||
setSearchState({ ...searchState, searchInProgress: false });
|
||||
}
|
||||
}, [searchState, setSearchState]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchPanel
|
||||
onSubmit={setSearchState}
|
||||
onReset={() =>
|
||||
setSearchState({
|
||||
searchResult: [],
|
||||
searchInProgress: false,
|
||||
})
|
||||
}
|
||||
styling={styling}
|
||||
state={nextState}
|
||||
/>
|
||||
<JSONTree
|
||||
labelRenderer={labelRenderer}
|
||||
labelRenderer={(keyPath, nodeType, expanded, expandable) => {
|
||||
return isMatch(searchState.searchResult, [...keyPath].reverse()) ? (
|
||||
<span {...styling('queryResultLabel')} ref={displayedResult}>
|
||||
{labelRenderer(keyPath, nodeType, expanded, expandable)}
|
||||
</span>
|
||||
) : (
|
||||
labelRenderer(keyPath, nodeType, expanded, expandable)
|
||||
);
|
||||
}}
|
||||
theme={getJsonTreeTheme(base16Theme)}
|
||||
data={nextState}
|
||||
getItemString={(type, data) =>
|
||||
|
@ -30,8 +74,28 @@ const StateTab: React.FunctionComponent<
|
|||
hideRoot
|
||||
sortObjectKeys={sortStateTreeAlphabetically}
|
||||
{...(disableStateTreeCollection ? { collectionLimit: 0 } : {})}
|
||||
isSearchInProgress={searchState.searchInProgress}
|
||||
searchResultPath={searchState.searchResult}
|
||||
valueRenderer={(raw, value, ...keyPath) => {
|
||||
return isMatch(searchState.searchResult, [...keyPath].reverse()) ? (
|
||||
<span {...styling('queryResult')} ref={displayedResult}>
|
||||
{raw as string}
|
||||
</span>
|
||||
) : (
|
||||
<span>{raw as string}</span>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
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,
|
||||
|
|
|
@ -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<typeof colorMap>;
|
||||
|
@ -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',
|
||||
|
|
Loading…
Reference in New Issue
Block a user