From 7a10825e16542a782ec055824c4f87add6e94d04 Mon Sep 17 00:00:00 2001 From: Zsofia Vagi Date: Fri, 16 Jun 2023 17:52:25 +0200 Subject: [PATCH] Add SearchPanel component and subcomponents, use it in StateTab --- .../searchPanel/JumpSearchResultButton.tsx | 45 +++++ .../src/searchPanel/SearchBar.tsx | 30 ++++ .../src/searchPanel/SearchPanel.tsx | 161 ++++++++++++++++++ .../src/tabs/StateTab.tsx | 92 ++++++++-- .../src/utils/createStylingFromTheme.ts | 91 ++++++++++ 5 files changed, 405 insertions(+), 14 deletions(-) create mode 100644 packages/redux-devtools-inspector-monitor/src/searchPanel/JumpSearchResultButton.tsx create mode 100644 packages/redux-devtools-inspector-monitor/src/searchPanel/SearchBar.tsx create mode 100644 packages/redux-devtools-inspector-monitor/src/searchPanel/SearchPanel.tsx 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',