This commit is contained in:
Zsofia Vagi 2023-11-16 18:05:07 +05:30 committed by GitHub
commit 96e8022a82
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 653 additions and 82 deletions

View File

@ -1,6 +1,6 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import JSONArrow from './JSONArrow'; import JSONArrow from './JSONArrow';
import type { CircularCache, CommonInternalProps } from './types'; import type { CircularCache, CommonInternalProps, KeyPath } from './types';
interface Props extends CommonInternalProps { interface Props extends CommonInternalProps {
data: unknown; data: unknown;
@ -13,12 +13,25 @@ interface Props extends CommonInternalProps {
} }
export default function ItemRange(props: Props) { export default function ItemRange(props: Props) {
const { styling, from, to, renderChildNodes, nodeType } = props; const {
styling,
from,
to,
renderChildNodes,
nodeType,
keyPath,
searchResultPath,
level,
} = props;
const [expanded, setExpanded] = useState<boolean>(false); const [userExpanded, setUserExpanded] = useState<boolean>(false);
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
setExpanded(!expanded); setUserExpanded(!userExpanded);
}, [expanded]); }, [userExpanded]);
const expanded =
userExpanded ||
containsSearchResult(keyPath, from, to, searchResultPath, level);
return expanded ? ( return expanded ? (
<div {...styling('itemRange', expanded)}> <div {...styling('itemRange', expanded)}>
@ -37,3 +50,15 @@ export default function ItemRange(props: Props) {
</div> </div>
); );
} }
const containsSearchResult = (
ownKeyPath: KeyPath,
from: number,
to: number,
resultKeyPath: KeyPath,
level: number,
): boolean => {
const searchLevel = level > 1 ? level - 1 : level;
const nextKey = Number(resultKeyPath[searchLevel]);
return !isNaN(nextKey) && nextKey >= from && nextKey <= to;
};

View File

@ -1,9 +1,9 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import JSONArrow from './JSONArrow';
import getCollectionEntries from './getCollectionEntries';
import JSONNode from './JSONNode';
import ItemRange from './ItemRange'; import ItemRange from './ItemRange';
import type { CircularCache, CommonInternalProps } from './types'; import JSONArrow from './JSONArrow';
import JSONNode from './JSONNode';
import getCollectionEntries from './getCollectionEntries';
import type { CircularCache, CommonInternalProps, KeyPath } from './types';
/** /**
* Renders nested values (eg. objects, arrays, lists, etc.) * Renders nested values (eg. objects, arrays, lists, etc.)
@ -110,17 +110,21 @@ export default function JSONNestedNode(props: Props) {
nodeType, nodeType,
nodeTypeIndicator, nodeTypeIndicator,
shouldExpandNodeInitially, shouldExpandNodeInitially,
searchResultPath,
styling, styling,
} = props; } = props;
const [expanded, setExpanded] = useState<boolean>( const [userExpanded, setUserExpanded] = useState<boolean>(
// calculate individual node expansion if necessary // calculate individual node expansion if necessary
isCircular ? false : shouldExpandNodeInitially(keyPath, data, level), isCircular ? false : shouldExpandNodeInitially(keyPath, data, level),
); );
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
if (expandable) setExpanded(!expanded); if (expandable) setUserExpanded(!userExpanded);
}, [expandable, expanded]); }, [expandable, userExpanded]);
const expanded =
userExpanded || containsSearchResult(keyPath, searchResultPath, level);
const renderedChildren = const renderedChildren =
expanded || (hideRoot && level === 0) expanded || (hideRoot && level === 0)
@ -175,3 +179,15 @@ export default function JSONNestedNode(props: Props) {
</li> </li>
); );
} }
const containsSearchResult = (
ownKeyPath: KeyPath,
resultKeyPath: KeyPath,
level: number,
): boolean => {
const searchLevel = level > 0 ? level - 1 : level;
const currKey = [...ownKeyPath].reverse()[searchLevel];
return resultKeyPath && currKey !== undefined
? resultKeyPath[searchLevel] === currKey.toString()
: false;
};

View File

@ -4,10 +4,10 @@
// port by Daniele Zannotti http://www.github.com/dzannotti <dzannotti@me.com> // port by Daniele Zannotti http://www.github.com/dzannotti <dzannotti@me.com>
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import type { StylingValue, Theme } from 'react-base16-styling';
import { invertTheme } from 'react-base16-styling';
import JSONNode from './JSONNode'; import JSONNode from './JSONNode';
import createStylingFromTheme from './createStylingFromTheme'; import createStylingFromTheme from './createStylingFromTheme';
import { invertTheme } from 'react-base16-styling';
import type { StylingValue, Theme } from 'react-base16-styling';
import type { import type {
CommonExternalProps, CommonExternalProps,
GetItemString, GetItemString,
@ -41,6 +41,8 @@ export function JSONTree({
labelRenderer = defaultLabelRenderer, labelRenderer = defaultLabelRenderer,
valueRenderer = identity, valueRenderer = identity,
shouldExpandNodeInitially = expandRootNode, shouldExpandNodeInitially = expandRootNode,
isSearchInProgress = false,
searchResultPath = [],
hideRoot = false, hideRoot = false,
getItemString = defaultItemString, getItemString = defaultItemString,
postprocessValue = identity, postprocessValue = identity,
@ -64,6 +66,8 @@ export function JSONTree({
labelRenderer={labelRenderer} labelRenderer={labelRenderer}
valueRenderer={valueRenderer} valueRenderer={valueRenderer}
shouldExpandNodeInitially={shouldExpandNodeInitially} shouldExpandNodeInitially={shouldExpandNodeInitially}
searchResultPath={searchResultPath}
isSearchInProgress={isSearchInProgress}
hideRoot={hideRoot} hideRoot={hideRoot}
getItemString={getItemString} getItemString={getItemString}
postprocessValue={postprocessValue} postprocessValue={postprocessValue}
@ -75,16 +79,16 @@ export function JSONTree({
} }
export type { export type {
CommonExternalProps,
GetItemString,
IsCustomNode,
Key, Key,
KeyPath, KeyPath,
GetItemString,
LabelRenderer, LabelRenderer,
ValueRenderer,
ShouldExpandNodeInitially,
PostprocessValue, PostprocessValue,
IsCustomNode, ShouldExpandNodeInitially,
SortObjectKeys, SortObjectKeys,
Styling, Styling,
CommonExternalProps, ValueRenderer,
} from './types'; } from './types';
export type { StylingValue }; export type { StylingValue };

View File

@ -47,6 +47,8 @@ export interface CommonExternalProps {
labelRenderer: LabelRenderer; labelRenderer: LabelRenderer;
valueRenderer: ValueRenderer; valueRenderer: ValueRenderer;
shouldExpandNodeInitially: ShouldExpandNodeInitially; shouldExpandNodeInitially: ShouldExpandNodeInitially;
searchResultPath: KeyPath;
isSearchInProgress: boolean;
hideRoot: boolean; hideRoot: boolean;
getItemString: GetItemString; getItemString: GetItemString;
postprocessValue: PostprocessValue; postprocessValue: PostprocessValue;

View File

@ -1,31 +1,33 @@
import { LiftedAction, LiftedState } from '@redux-devtools/core';
import { SchemeName, ThemeName } from '@redux-devtools/ui'; import { SchemeName, ThemeName } from '@redux-devtools/ui';
import { AuthStates, States } from 'socketcluster-client/lib/clientsocket'; import { Action } from 'redux';
import { REHYDRATE } from 'redux-persist'; import { REHYDRATE } from 'redux-persist';
import { AuthStates, States } from 'socketcluster-client/lib/clientsocket';
import { import {
CHANGE_SECTION, CHANGE_SECTION,
CHANGE_STATE_TREE_SETTINGS,
CHANGE_THEME, CHANGE_THEME,
SELECT_INSTANCE, CLEAR_NOTIFICATION,
SELECT_MONITOR, ERROR,
UPDATE_MONITOR_STATE, EXPORT,
GET_REPORT_ERROR,
GET_REPORT_REQUEST,
GET_REPORT_SUCCESS,
LIFTED_ACTION, LIFTED_ACTION,
MONITOR_ACTION, MONITOR_ACTION,
EXPORT, REMOVE_INSTANCE,
TOGGLE_SYNC, SELECT_INSTANCE,
TOGGLE_SLIDER, SELECT_MONITOR,
SET_PERSIST,
SET_STATE,
SHOW_NOTIFICATION,
TOGGLE_DISPATCHER, TOGGLE_DISPATCHER,
TOGGLE_PERSIST, TOGGLE_PERSIST,
GET_REPORT_REQUEST, TOGGLE_SLIDER,
SHOW_NOTIFICATION, TOGGLE_SYNC,
CLEAR_NOTIFICATION, UPDATE_MONITOR_STATE,
UPDATE_STATE,
UPDATE_REPORTS, UPDATE_REPORTS,
REMOVE_INSTANCE, UPDATE_STATE,
SET_STATE,
GET_REPORT_ERROR,
GET_REPORT_SUCCESS,
ERROR,
SET_PERSIST,
CHANGE_STATE_TREE_SETTINGS,
} from '../constants/actionTypes'; } from '../constants/actionTypes';
import { import {
AUTH_ERROR, AUTH_ERROR,
@ -43,12 +45,9 @@ import {
SUBSCRIBE_SUCCESS, SUBSCRIBE_SUCCESS,
UNSUBSCRIBE, UNSUBSCRIBE,
} from '../constants/socketActionTypes'; } from '../constants/socketActionTypes';
import { Action } from 'redux';
import { Features, State } from '../reducers/instances'; import { Features, State } from '../reducers/instances';
import { MonitorStateMonitorState } from '../reducers/monitor'; import { MonitorStateMonitorState } from '../reducers/monitor';
import { LiftedAction } from '@redux-devtools/core';
import { Data } from '../reducers/reports'; import { Data } from '../reducers/reports';
import { LiftedState } from '@redux-devtools/core';
let monitorReducer: ( let monitorReducer: (
monitorProps: unknown, monitorProps: unknown,
@ -86,6 +85,7 @@ export function changeTheme(data: ChangeThemeData): ChangeThemeAction {
interface ChangeStateTreeSettingsFormData { interface ChangeStateTreeSettingsFormData {
readonly sortAlphabetically: boolean; readonly sortAlphabetically: boolean;
readonly disableCollection: boolean; readonly disableCollection: boolean;
readonly enableSearchPanel: boolean;
} }
interface ChangeStateTreeSettingsData { interface ChangeStateTreeSettingsData {
@ -96,6 +96,7 @@ export interface ChangeStateTreeSettingsAction {
readonly type: typeof CHANGE_STATE_TREE_SETTINGS; readonly type: typeof CHANGE_STATE_TREE_SETTINGS;
readonly sortAlphabetically: boolean; readonly sortAlphabetically: boolean;
readonly disableCollection: boolean; readonly disableCollection: boolean;
readonly enableSearchPanel: boolean;
} }
export function changeStateTreeSettings( export function changeStateTreeSettings(

View File

@ -1,6 +1,6 @@
import React, { Component } from 'react';
import { connect, ResolveThunks } from 'react-redux';
import { Container, Form } from '@redux-devtools/ui'; import { Container, Form } from '@redux-devtools/ui';
import React, { Component } from 'react';
import { ResolveThunks, connect } from 'react-redux';
import { changeStateTreeSettings } from '../../actions'; import { changeStateTreeSettings } from '../../actions';
import { StoreState } from '../../reducers'; import { StoreState } from '../../reducers';
@ -14,6 +14,7 @@ export class StateTree extends Component<Props> {
const formData = { const formData = {
sortAlphabetically: stateTree.sortAlphabetically, sortAlphabetically: stateTree.sortAlphabetically,
disableCollection: stateTree.disableCollection, disableCollection: stateTree.disableCollection,
enableSearchPanel: stateTree.enableSearchPanel,
}; };
return ( return (
@ -30,6 +31,10 @@ export class StateTree extends Component<Props> {
title: 'Disable collapsing of nodes', title: 'Disable collapsing of nodes',
type: 'boolean', type: 'boolean',
}, },
enableSearchPanel: {
title: 'Show search panel in State tab',
type: 'boolean',
},
}, },
}} }}
formData={formData} formData={formData}

View File

@ -1,13 +1,13 @@
import React, { Component } from 'react';
import { withTheme } from 'styled-components';
import { LiftedAction, LiftedState } from '@redux-devtools/core'; import { LiftedAction, LiftedState } from '@redux-devtools/core';
import { ThemeFromProvider } from '@redux-devtools/ui';
import React, { Component } from 'react';
import { Action } from 'redux'; import { Action } from 'redux';
import getMonitor from '../utils/getMonitor'; import { withTheme } from 'styled-components';
import { InitMonitorAction } from '../actions'; import { InitMonitorAction } from '../actions';
import { Features, State } from '../reducers/instances'; import { Features, State } from '../reducers/instances';
import { MonitorStateMonitorState } from '../reducers/monitor'; import { MonitorStateMonitorState } from '../reducers/monitor';
import { ThemeFromProvider } from '@redux-devtools/ui';
import { StateTreeSettings } from '../reducers/stateTreeSettings'; import { StateTreeSettings } from '../reducers/stateTreeSettings';
import getMonitor from '../utils/getMonitor';
interface Props { interface Props {
monitor: string; monitor: string;
@ -118,6 +118,7 @@ class DevTools extends Component<Props> {
disableStateTreeCollection={ disableStateTreeCollection={
this.props.stateTreeSettings.disableCollection this.props.stateTreeSettings.disableCollection
} }
enableSearchPanel={this.props.stateTreeSettings.enableSearchPanel}
/> />
</div> </div>
); );

View File

@ -1,15 +1,17 @@
import { CHANGE_STATE_TREE_SETTINGS } from '../constants/actionTypes';
import { StoreAction } from '../actions'; import { StoreAction } from '../actions';
import { CHANGE_STATE_TREE_SETTINGS } from '../constants/actionTypes';
export interface StateTreeSettings { export interface StateTreeSettings {
readonly sortAlphabetically: boolean; readonly sortAlphabetically: boolean;
readonly disableCollection: boolean; readonly disableCollection: boolean;
readonly enableSearchPanel: boolean;
} }
export function stateTreeSettings( export function stateTreeSettings(
state: StateTreeSettings = { state: StateTreeSettings = {
sortAlphabetically: false, sortAlphabetically: false,
disableCollection: false, disableCollection: false,
enableSearchPanel: false,
}, },
action: StoreAction, action: StoreAction,
) { ) {
@ -17,6 +19,7 @@ export function stateTreeSettings(
return { return {
sortAlphabetically: action.sortAlphabetically, sortAlphabetically: action.sortAlphabetically,
disableCollection: action.disableCollection, disableCollection: action.disableCollection,
enableSearchPanel: action.enableSearchPanel,
}; };
} }
return state; return state;

View File

@ -1,15 +1,15 @@
import React, { Component } from 'react';
import { Base16Theme } from 'redux-devtools-themes';
import { Action } from 'redux';
import type { StylingFunction } from 'react-base16-styling';
import type { LabelRenderer } from 'react-json-tree';
import { PerformAction } from '@redux-devtools/core'; import { PerformAction } from '@redux-devtools/core';
import { Delta } from 'jsondiffpatch'; import { Delta } from 'jsondiffpatch';
import { DEFAULT_STATE, DevtoolsInspectorState } from './redux'; import React, { Component } from 'react';
import type { StylingFunction } from 'react-base16-styling';
import type { LabelRenderer } from 'react-json-tree';
import { Action } from 'redux';
import { Base16Theme } from 'redux-devtools-themes';
import ActionPreviewHeader from './ActionPreviewHeader'; import ActionPreviewHeader from './ActionPreviewHeader';
import { DEFAULT_STATE, DevtoolsInspectorState } from './redux';
import ActionTab from './tabs/ActionTab';
import DiffTab from './tabs/DiffTab'; import DiffTab from './tabs/DiffTab';
import StateTab from './tabs/StateTab'; import StateTab from './tabs/StateTab';
import ActionTab from './tabs/ActionTab';
export interface TabComponentProps<S, A extends Action<string>> { export interface TabComponentProps<S, A extends Action<string>> {
labelRenderer: LabelRenderer; labelRenderer: LabelRenderer;
@ -23,6 +23,7 @@ export interface TabComponentProps<S, A extends Action<string>> {
isWideLayout: boolean; isWideLayout: boolean;
sortStateTreeAlphabetically: boolean; sortStateTreeAlphabetically: boolean;
disableStateTreeCollection: boolean; disableStateTreeCollection: boolean;
enableSearchPanel: boolean;
dataTypeKey: string | symbol | undefined; dataTypeKey: string | symbol | undefined;
delta: Delta | null | undefined | false; delta: Delta | null | undefined | false;
action: A; action: A;
@ -74,6 +75,7 @@ interface Props<S, A extends Action<string>> {
onSelectTab: (tabName: string) => void; onSelectTab: (tabName: string) => void;
sortStateTreeAlphabetically: boolean; sortStateTreeAlphabetically: boolean;
disableStateTreeCollection: boolean; disableStateTreeCollection: boolean;
enableSearchPanel: boolean;
} }
class ActionPreview<S, A extends Action<string>> extends Component< class ActionPreview<S, A extends Action<string>> extends Component<
@ -107,6 +109,7 @@ class ActionPreview<S, A extends Action<string>> extends Component<
updateMonitorState, updateMonitorState,
sortStateTreeAlphabetically, sortStateTreeAlphabetically,
disableStateTreeCollection, disableStateTreeCollection,
enableSearchPanel,
} = this.props; } = this.props;
const renderedTabs: Tab<S, A>[] = const renderedTabs: Tab<S, A>[] =
@ -141,6 +144,7 @@ class ActionPreview<S, A extends Action<string>> extends Component<
isWideLayout, isWideLayout,
sortStateTreeAlphabetically, sortStateTreeAlphabetically,
disableStateTreeCollection, disableStateTreeCollection,
enableSearchPanel,
dataTypeKey, dataTypeKey,
delta, delta,
action, action,

View File

@ -1,25 +1,20 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { Base16Theme } from 'redux-devtools-themes';
import {
getBase16Theme,
invertTheme,
StylingFunction,
} from 'react-base16-styling';
import { import {
ActionCreators, ActionCreators,
LiftedAction, LiftedAction,
LiftedState, LiftedState,
} from '@redux-devtools/core'; } from '@redux-devtools/core';
import { Action, Dispatch } from 'redux';
import { Delta, DiffContext } from 'jsondiffpatch'; import { Delta, DiffContext } from 'jsondiffpatch';
import PropTypes from 'prop-types';
import React, { PureComponent } from 'react';
import { import {
createStylingFromTheme, StylingFunction,
base16Themes, getBase16Theme,
} from './utils/createStylingFromTheme'; invertTheme,
} from 'react-base16-styling';
import { Action, Dispatch } from 'redux';
import { Base16Theme } from 'redux-devtools-themes';
import ActionList from './ActionList'; import ActionList from './ActionList';
import ActionPreview, { Tab } from './ActionPreview'; import ActionPreview, { Tab } from './ActionPreview';
import getInspectedState from './utils/getInspectedState';
import createDiffPatcher from './createDiffPatcher'; import createDiffPatcher from './createDiffPatcher';
import { import {
DevtoolsInspectorAction, DevtoolsInspectorAction,
@ -27,6 +22,11 @@ import {
reducer, reducer,
updateMonitorState, updateMonitorState,
} from './redux'; } from './redux';
import {
base16Themes,
createStylingFromTheme,
} from './utils/createStylingFromTheme';
import getInspectedState from './utils/getInspectedState';
const { const {
// eslint-disable-next-line @typescript-eslint/unbound-method // eslint-disable-next-line @typescript-eslint/unbound-method
@ -153,6 +153,7 @@ export interface ExternalProps<S, A extends Action<string>> {
invertTheme: boolean; invertTheme: boolean;
sortStateTreeAlphabetically: boolean; sortStateTreeAlphabetically: boolean;
disableStateTreeCollection: boolean; disableStateTreeCollection: boolean;
enableSearchPanel: boolean;
dataTypeKey?: string | symbol; dataTypeKey?: string | symbol;
tabs: Tab<S, A>[] | ((tabs: Tab<S, A>[]) => Tab<S, A>[]); tabs: Tab<S, A>[] | ((tabs: Tab<S, A>[]) => Tab<S, A>[]);
} }
@ -181,6 +182,7 @@ export interface DevtoolsInspectorProps<S, A extends Action<string>>
hideActionButtons?: boolean; hideActionButtons?: boolean;
sortStateTreeAlphabetically: boolean; sortStateTreeAlphabetically: boolean;
disableStateTreeCollection: boolean; disableStateTreeCollection: boolean;
enableSearchPanel: boolean;
invertTheme: boolean; invertTheme: boolean;
dataTypeKey?: string | symbol; dataTypeKey?: string | symbol;
tabs: Tab<S, A>[] | ((tabs: Tab<S, A>[]) => Tab<S, A>[]); tabs: Tab<S, A>[] | ((tabs: Tab<S, A>[]) => Tab<S, A>[]);
@ -226,6 +228,7 @@ class DevtoolsInspector<S, A extends Action<string>> extends PureComponent<
invertTheme: PropTypes.bool, invertTheme: PropTypes.bool,
sortStateTreeAlphabetically: PropTypes.bool, sortStateTreeAlphabetically: PropTypes.bool,
disableStateTreeCollection: PropTypes.bool, disableStateTreeCollection: PropTypes.bool,
enableSearchPanel: PropTypes.bool,
skippedActionIds: PropTypes.array, skippedActionIds: PropTypes.array,
dataTypeKey: PropTypes.any, dataTypeKey: PropTypes.any,
tabs: PropTypes.oneOfType([PropTypes.array, PropTypes.func]), tabs: PropTypes.oneOfType([PropTypes.array, PropTypes.func]),
@ -313,6 +316,7 @@ class DevtoolsInspector<S, A extends Action<string>> extends PureComponent<
hideActionButtons, hideActionButtons,
sortStateTreeAlphabetically, sortStateTreeAlphabetically,
disableStateTreeCollection, disableStateTreeCollection,
enableSearchPanel,
} = this.props; } = this.props;
const { selectedActionId, startActionId, searchValue, tabName } = const { selectedActionId, startActionId, searchValue, tabName } =
monitorState; monitorState;
@ -374,6 +378,7 @@ class DevtoolsInspector<S, A extends Action<string>> extends PureComponent<
dataTypeKey, dataTypeKey,
sortStateTreeAlphabetically, sortStateTreeAlphabetically,
disableStateTreeCollection, disableStateTreeCollection,
enableSearchPanel,
}} }}
monitorState={this.props.monitorState} monitorState={this.props.monitorState}
updateMonitorState={this.updateMonitorState} updateMonitorState={this.updateMonitorState}

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,163 @@
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' && (
<>
<div {...styling('jumpResultContainer')}>
<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}`}
</div>
<button {...styling('searchButton')} onClick={() => reset()}>
Reset
</button>
</>
)}
</div>
);
}
export default SearchPanel;

View File

@ -1,10 +1,16 @@
import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { useEffect, useState } from 'react';
import { JSONTree } from 'react-json-tree'; import { JSONTree } from 'react-json-tree';
import { Action } from 'redux'; import { Action } from 'redux';
import { TabComponentProps } from '../ActionPreview';
import SearchPanel from '../searchPanel/SearchPanel';
import getItemString from './getItemString'; import getItemString from './getItemString';
import getJsonTreeTheme from './getJsonTreeTheme'; import getJsonTreeTheme from './getJsonTreeTheme';
import { TabComponentProps } from '../ActionPreview';
interface SearchState {
searchResult: string[];
searchInProgress: boolean;
}
const StateTab: React.FunctionComponent< const StateTab: React.FunctionComponent<
TabComponentProps<any, Action<string>> TabComponentProps<any, Action<string>>
@ -18,20 +24,86 @@ const StateTab: React.FunctionComponent<
isWideLayout, isWideLayout,
sortStateTreeAlphabetically, sortStateTreeAlphabetically,
disableStateTreeCollection, disableStateTreeCollection,
}) => ( enableSearchPanel,
<JSONTree }) => {
labelRenderer={labelRenderer} const [searchState, setSearchState] = useState<SearchState>({
theme={getJsonTreeTheme(base16Theme)} searchResult: [],
data={nextState} searchInProgress: false,
getItemString={(type, data) => });
getItemString(styling, type, data, dataTypeKey, isWideLayout)
const displayedResult = React.useRef<HTMLDivElement>(null);
useEffect(() => {
if (
enableSearchPanel &&
searchState.searchInProgress &&
displayedResult.current
) {
displayedResult.current.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'center',
});
setSearchState({ ...searchState, searchInProgress: false });
} }
invertTheme={invertTheme} }, [searchState, setSearchState, enableSearchPanel]);
hideRoot
sortObjectKeys={sortStateTreeAlphabetically} return (
{...(disableStateTreeCollection ? { collectionLimit: 0 } : {})} <>
/> {enableSearchPanel && (
); <SearchPanel
onSubmit={setSearchState}
onReset={() =>
setSearchState({
searchResult: [],
searchInProgress: false,
})
}
styling={styling}
state={nextState}
/>
)}
<JSONTree
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) =>
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()) ? (
<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 = { StateTab.propTypes = {
nextState: PropTypes.any.isRequired, nextState: PropTypes.any.isRequired,

View File

@ -17,6 +17,7 @@ const colorMap = (theme: Base16Theme) => ({
SELECTED_BACKGROUND_COLOR: rgba(theme.base03, 20), SELECTED_BACKGROUND_COLOR: rgba(theme.base03, 20),
SKIPPED_BACKGROUND_COLOR: rgba(theme.base03, 10), SKIPPED_BACKGROUND_COLOR: rgba(theme.base03, 10),
HEADER_BACKGROUND_COLOR: rgba(theme.base03, 30), HEADER_BACKGROUND_COLOR: rgba(theme.base03, 30),
HEADER_BACKGROUND_COLOR_OPAQUE: rgba(theme.base03, 100),
HEADER_BORDER_COLOR: rgba(theme.base03, 20), HEADER_BORDER_COLOR: rgba(theme.base03, 20),
BORDER_COLOR: rgba(theme.base03, 50), BORDER_COLOR: rgba(theme.base03, 50),
LIST_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_COLOR: rgba(theme.base0E, 90),
LINK_HOVER_COLOR: theme.base0E, LINK_HOVER_COLOR: theme.base0E,
ERROR_COLOR: theme.base08, ERROR_COLOR: theme.base08,
SEARCH_BUTTON_COLOR: rgba(theme.base00, 50),
}); });
type Color = keyof ReturnType<typeof colorMap>; type Color = keyof ReturnType<typeof colorMap>;
@ -267,6 +269,95 @@ const getSheetFromColorMap = (map: ColorMap) => ({
'border-bottom-color': map.HEADER_BORDER_COLOR, 'border-bottom-color': map.HEADER_BORDER_COLOR,
}, },
searchPanel: {
display: 'flex',
'flex-wrap': 'wrap',
position: 'sticky',
top: 0,
width: '100%',
'z-index': 1,
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,
},
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,
},
jumpResultContainer: {
display: 'flex',
gap: '1em',
'align-items': 'center',
},
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: { tabSelector: {
position: 'relative', position: 'relative',
display: 'inline-flex', display: 'inline-flex',

View File

@ -0,0 +1,18 @@
import { SearchQuery } from '../searchPanel/SearchPanel';
import { Value } from './searchWorker';
export function searchInObject(
objectToSearch: Value,
query: SearchQuery,
): Promise<string[]> {
return new Promise((resolve) => {
const worker = new Worker(new URL('./searchWorker.js', import.meta.url));
worker.onmessage = (event: MessageEvent<string[]>) => {
resolve(event.data);
worker.terminate();
};
worker.postMessage({ objectToSearch, query });
});
}

View File

@ -0,0 +1,85 @@
import { SearchQuery } from '../searchPanel/SearchPanel';
type PrimitiveValue = string | number | boolean | undefined | null;
export type Value =
| PrimitiveValue
| Value[]
| { [key: string | number]: Value };
function getKeys(object: Value): string[] {
if (object === undefined || object === null) {
return [];
}
if (['string', 'number', 'boolean'].includes(typeof object)) {
return [];
}
if (Array.isArray(object)) {
return [...object.keys()].map((el) => el.toString());
}
return Object.keys(object);
}
function getKeyPaths(object: Value): string[] {
const keys = getKeys(object) as (keyof object)[];
const appendKey = (path: string, branch: string | number): string =>
path ? `${path}.${branch}` : `${branch}`;
return keys.flatMap((key) => [
key,
...getKeyPaths(object?.[key]).map((subKey) => appendKey(key, subKey)),
]);
}
const isMatchable = (value: Value): boolean => {
return (
value === undefined ||
value === null ||
['string', 'number', 'boolean'].includes(typeof value)
);
};
function doSearch(objectToSearch: Value, query: SearchQuery): string[] {
const {
queryText,
location: { keys: matchKeys, values: matchValues },
} = query;
if (!matchKeys && !matchValues) {
return [];
}
const keyPaths = getKeyPaths(objectToSearch);
const getValueFromKeyPath = (obj: Value, path: string): Value => {
return path
.split('.')
.reduce(
(intermediateObj, key) => intermediateObj?.[key as keyof object],
obj,
);
};
const match = (value: Value, searchText: string): boolean => {
return isMatchable(value) && String(value).includes(searchText);
};
return keyPaths.filter((keyPath) => {
const valuesToCheck = [];
matchKeys && valuesToCheck.push(keyPath.split('.').splice(-1)[0]);
matchValues &&
valuesToCheck.push(getValueFromKeyPath(objectToSearch, keyPath));
return valuesToCheck.some((value) => match(value, queryText));
});
}
self.onmessage = (
event: MessageEvent<{ objectToSearch: Value; query: SearchQuery }>,
) => {
const { objectToSearch, query } = event.data;
const result = doSearch(objectToSearch, query);
self.postMessage(result);
};
export {};

View File

@ -1,6 +1,7 @@
{ {
"extends": "../../tsconfig.react.base.json", "extends": "../../tsconfig.react.base.json",
"compilerOptions": { "compilerOptions": {
"module": "ES2022",
"outDir": "lib/types", "outDir": "lib/types",
"resolveJsonModule": true "resolveJsonModule": true
}, },