Skip to content

feature(redux-devtools-app/ui): add search feature to state #1305

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions extension/src/devpanel/store/panelReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
stateTreeSettings,
StoreAction,
StoreState,
stateFilter,
theme,
} from '@redux-devtools/app';

Expand All @@ -26,6 +27,7 @@ const rootReducer: Reducer<
socket,
theme,
connection,
stateFilter,
stateTreeSettings,
}) as any;

Expand Down
3 changes: 3 additions & 0 deletions packages/redux-devtools-app-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
"javascript-stringify": "^2.1.0",
"jsan": "^3.1.14",
"jsondiffpatch": "^0.6.0",
"jsonpath-plus": "github:80avin/JSONPath#de0566aee8abf14b7f0f57e2711dbea06cf997fa",
"lodash-es": "^4.17.21",
"react-icons": "^5.3.0",
"react-is": "^18.3.1"
},
Expand All @@ -69,6 +71,7 @@
"@types/jest": "^29.5.13",
"@types/jsan": "^3.1.5",
"@types/json-schema": "^7.0.15",
"@types/lodash-es": "^4.17.12",
"@types/node": "^20.16.5",
"@types/react": "^18.3.6",
"@types/react-dom": "^18.3.0",
Expand Down
17 changes: 17 additions & 0 deletions packages/redux-devtools-app-core/src/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
GET_REPORT_SUCCESS,
ERROR,
SET_PERSIST,
SET_STATE_FILTER,
CHANGE_STATE_TREE_SETTINGS,
CLEAR_INSTANCES,
} from '../constants/actionTypes';
Expand All @@ -33,6 +34,7 @@ import { MonitorStateMonitorState } from '../reducers/monitor';
import { LiftedAction } from '@redux-devtools/core';
import { Data } from '../reducers/reports';
import { LiftedState } from '@redux-devtools/core';
import { StateFilterState } from '../reducers/stateFilter';

let monitorReducer: (
monitorProps: unknown,
Expand Down Expand Up @@ -67,6 +69,20 @@ export function changeTheme(data: ChangeThemeData): ChangeThemeAction {
return { type: CHANGE_THEME, ...data.formData! };
}

export interface SetStateFilterAction {
readonly type: typeof SET_STATE_FILTER;
stateFilter: Partial<StateFilterState>;
}

export function setStateFilter(
stateFilter: SetStateFilterAction['stateFilter'],
): SetStateFilterAction {
return {
type: SET_STATE_FILTER,
stateFilter,
};
}

interface ChangeStateTreeSettingsFormData {
readonly sortAlphabetically: boolean;
readonly disableCollection: boolean;
Expand Down Expand Up @@ -487,6 +503,7 @@ export interface ReduxPersistRehydrateAction {
export type CoreStoreActionWithoutUpdateStateOrLiftedAction =
| ChangeSectionAction
| ChangeThemeAction
| SetStateFilterAction
| ChangeStateTreeSettingsAction
| MonitorActionAction
| SelectInstanceAction
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const TOGGLE_DISPATCHER = 'devTools/TOGGLE_DISPATCHER';
export const EXPORT = 'devTools/EXPORT';
export const SHOW_NOTIFICATION = 'devTools/SHOW_NOTIFICATION';
export const CLEAR_NOTIFICATION = 'devTools/CLEAR_NOTIFICATION';
export const SET_STATE_FILTER = 'devTools/SET_STATE_FILTER';

export const UPDATE_REPORTS = 'reports/UPDATE';
export const GET_REPORT_REQUEST = 'reports/GET_REPORT_REQUEST';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ import {
DiffTab,
} from '@redux-devtools/inspector-monitor';
import { Action } from 'redux';
import { selectMonitorTab } from '../../../actions';
import { selectMonitorTab, setStateFilter } from '../../../actions';
import RawTab from './RawTab';
import ChartTab from './ChartTab';
import VisualDiffTab from './VisualDiffTab';
import { CoreStoreState } from '../../../reducers';
import type { Delta } from 'jsondiffpatch';
import { filter } from '../../../utils/searchUtils';
import { StateFilterValue } from '@redux-devtools/ui/lib/types/StateFilter/StateFilter';

type StateProps = ReturnType<typeof mapStateToProps>;
type DispatchProps = ResolveThunks<typeof actionCreators>;
Expand All @@ -35,17 +37,38 @@ class SubTabs extends Component<Props> {
}
}

selector = () => {
switch (this.props.parentTab) {
case 'Action':
return { data: this.props.action };
case 'Diff':
return { data: this.props.delta };
default:
return { data: this.props.nextState };
}
filteredData = () => {
const [data, _error] = filter(
this.props.nextState as object,
this.props.stateFilter,
);
return data;
};

treeSelector = (): Props => {
const props = {
...this.props,
};
if (this.props.nextState) props.nextState = this.filteredData();
return props;
};

selectorCreator =
(parentTab: Props['parentTab'], selected: Props['selected']) => () => {
if (selected === 'Tree')
// FIXME change to (this.props.nextState ? { nextState: data } : {})
return this.treeSelector();
switch (parentTab) {
case 'Action':
return { data: this.props.action };
case 'Diff':
return { data: this.props.delta };
default:
return { data: this.filteredData() };
}
// {a: 1, b: {c: 2}, e: 5, c: {d: 1}, d: 2}
};

updateTabs(props: Props) {
const parentTab = props.parentTab;

Expand All @@ -54,12 +77,17 @@ class SubTabs extends Component<Props> {
{
name: 'Tree',
component: DiffTab,
selector: () => this.props,
selector: this.selectorCreator(
this.props.parentTab,
'Tree',
) as () => Props,
},
{
name: 'Raw',
component: VisualDiffTab,
selector: this.selector as () => { data?: Delta },
selector: this.selectorCreator(this.props.parentTab, 'Raw') as () => {
data?: Delta;
},
},
];
return;
Expand All @@ -69,21 +97,37 @@ class SubTabs extends Component<Props> {
{
name: 'Tree',
component: parentTab === 'Action' ? ActionTab : StateTab,
selector: () => this.props,
selector: this.selectorCreator(
this.props.parentTab,
'Tree',
) as () => Props,
},
{
name: 'Chart',
component: ChartTab,
selector: this.selector,
selector: this.selectorCreator(this.props.parentTab, 'Chart') as () => {
data: unknown;
},
},
{
name: 'Raw',
component: RawTab,
selector: this.selector,
selector: this.selectorCreator(this.props.parentTab, 'Raw') as () => {
data: Delta;
},
},
];
}

setFilter = (value: StateFilterValue) => {
this.setState({
stateFilter: {
isJsonPath: value.isJsonPath,
searchString: value.searchString,
},
});
};

render() {
let selected = this.props.selected;
if (selected === 'Chart' && this.props.parentTab === 'Diff')
Expand All @@ -94,6 +138,8 @@ class SubTabs extends Component<Props> {
tabs={this.tabs! as any}
selected={selected || 'Tree'}
onClick={this.props.selectMonitorTab}
setFilter={this.props.setStateFilter}
stateFilterValue={this.props.stateFilter}
/>
);
}
Expand All @@ -102,10 +148,12 @@ class SubTabs extends Component<Props> {
const mapStateToProps = (state: CoreStoreState) => ({
parentTab: state.monitor.monitorState!.tabName,
selected: state.monitor.monitorState!.subTabName,
stateFilter: state.stateFilter,
});

const actionCreators = {
selectMonitorTab,
setStateFilter,
};

export default connect(mapStateToProps, actionCreators)(SubTabs);
1 change: 1 addition & 0 deletions packages/redux-devtools-app-core/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@ export * from './reducers/notification';
export * from './reducers/reports';
export * from './reducers/section';
export * from './reducers/theme';
export * from './reducers/stateFilter';
export * from './reducers/stateTreeSettings';
export * from './utils/stringifyJSON';
3 changes: 3 additions & 0 deletions packages/redux-devtools-app-core/src/reducers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { notification, NotificationState } from './notification';
import { instances, InstancesState } from './instances';
import { reports, ReportsState } from './reports';
import { theme, ThemeState } from './theme';
import { stateFilter, StateFilterState } from './stateFilter';
import { stateTreeSettings, StateTreeSettings } from './stateTreeSettings';

export interface CoreStoreState {
Expand All @@ -14,6 +15,7 @@ export interface CoreStoreState {
readonly instances: InstancesState;
readonly reports: ReportsState;
readonly notification: NotificationState;
readonly stateFilter: StateFilterState;
}

export const coreReducers = {
Expand All @@ -24,4 +26,5 @@ export const coreReducers = {
instances,
reports,
notification,
stateFilter,
};
18 changes: 18 additions & 0 deletions packages/redux-devtools-app-core/src/reducers/stateFilter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { StateFilterValue } from '@redux-devtools/ui/lib/types/StateFilter/StateFilter';
import { CoreStoreAction } from '../actions';
import { SET_STATE_FILTER } from '../constants/actionTypes';

export type StateFilterState = StateFilterValue;

export function stateFilter(
state: StateFilterState = {
isJsonPath: false,
searchString: '',
},
action: CoreStoreAction,
): StateFilterState {
if (action.type === SET_STATE_FILTER) {
return { ...state, ...action.stateFilter };
}
return state;
}
78 changes: 78 additions & 0 deletions packages/redux-devtools-app-core/src/utils/searchUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { StateFilterValue } from '@redux-devtools/ui/lib/types/StateFilter/StateFilter';
import { JSONPath } from 'jsonpath-plus';
import _ from 'lodash-es';

type Path = string[];

const _filterByPaths = (obj: any, paths: Path[]) => {
if (typeof obj !== 'object' || obj === null) return obj;
if (paths.length === 1 && !paths[0].length) return obj;

// groupBy top level key and,
// remove that key from grouped path values
// [['a', 'b'], ['a', 'c']] => {'a': ['b', 'c']}
const groupedTopPaths = _.mapValues(_.groupBy(paths, 0), (val) =>
val.map((v) => v.slice(1)),
);
const topKeys = Object.keys(groupedTopPaths);

if (Array.isArray(obj)) {
obj = obj.flatMap((_, i) =>
i in groupedTopPaths ? _filterByPaths(obj[i], groupedTopPaths[i]) : [],
);
return obj;
}
if (typeof obj === 'object') obj = _.pick(obj, topKeys);

for (const k of topKeys) {
if (!(k in obj)) continue;
obj[k] = _filterByPaths(obj[k], groupedTopPaths[k]);
}
return obj;
};

const filterByPaths = (obj: any, paths: Path[]) => {
const sortedUniqPaths = _.sortBy(_.uniqBy(paths, JSON.stringify), ['length']);
// remove unnecessary depths
// [['a'], ['a', 'b']] => [['a']]
const filteredPaths = sortedUniqPaths.filter((s, i, arr) => {
if (i === 0) return true;
if (!_.isEqual(arr[i].slice(0, arr[i - 1].length), arr[i - 1])) return true;
return false;
});
return _filterByPaths(obj, filteredPaths);
};

export const filterByJsonPath = (obj: any, jsonpath: string) => {
const paths = JSONPath({
json: obj,
path: jsonpath,
resultType: 'path',
eval: 'safe',
}).map((jp: string) => JSONPath.toPathArray(jp).slice(1)) as Path[];

if (paths.some((path: Path) => !path.length)) return obj;

const result = filterByPaths(obj, paths);

return result;
};

export type FilterType = 'jsonpath' | 'regexp-glob';
export type FilterResult = [any, Error | null];

export const filter = (
obj: any,
stateFilter: StateFilterValue,
): FilterResult => {
try {
const { isJsonPath, searchString } = stateFilter;
if (!searchString) return [obj, null];

if (isJsonPath) return [filterByJsonPath(obj, searchString), null];
else return [filterByJsonPath(obj, `$..${searchString}`), null];
} catch (error) {
console.error('Filter Error', error);
return [{}, error as Error];
}
};
Loading
Loading