mirror of
https://github.com/reduxjs/redux-devtools.git
synced 2025-04-19 07:51:59 +03:00
Merge e012515e13
into 04858cd514
This commit is contained in:
commit
b30cb9ea5e
|
@ -10,6 +10,7 @@ import {
|
|||
stateTreeSettings,
|
||||
StoreAction,
|
||||
StoreState,
|
||||
stateFilter,
|
||||
theme,
|
||||
} from '@redux-devtools/app';
|
||||
|
||||
|
@ -26,6 +27,7 @@ const rootReducer: Reducer<
|
|||
socket,
|
||||
theme,
|
||||
connection,
|
||||
stateFilter,
|
||||
stateTreeSettings,
|
||||
}) as any;
|
||||
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
@ -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",
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
GET_REPORT_SUCCESS,
|
||||
ERROR,
|
||||
SET_PERSIST,
|
||||
SET_STATE_FILTER,
|
||||
CHANGE_STATE_TREE_SETTINGS,
|
||||
CLEAR_INSTANCES,
|
||||
} from '../constants/actionTypes';
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -487,6 +503,7 @@ export interface ReduxPersistRehydrateAction {
|
|||
export type CoreStoreActionWithoutUpdateStateOrLiftedAction =
|
||||
| ChangeSectionAction
|
||||
| ChangeThemeAction
|
||||
| SetStateFilterAction
|
||||
| ChangeStateTreeSettingsAction
|
||||
| MonitorActionAction
|
||||
| SelectInstanceAction
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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>;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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')
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -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);
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 {
|
||||
|
@ -14,6 +15,7 @@ export interface CoreStoreState {
|
|||
readonly instances: InstancesState;
|
||||
readonly reports: ReportsState;
|
||||
readonly notification: NotificationState;
|
||||
readonly stateFilter: StateFilterState;
|
||||
}
|
||||
|
||||
export const coreReducers = {
|
||||
|
@ -24,4 +26,5 @@ export const coreReducers = {
|
|||
instances,
|
||||
reports,
|
||||
notification,
|
||||
stateFilter,
|
||||
};
|
||||
|
|
18
packages/redux-devtools-app-core/src/reducers/stateFilter.ts
Normal file
18
packages/redux-devtools-app-core/src/reducers/stateFilter.ts
Normal file
|
@ -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
packages/redux-devtools-app-core/src/utils/searchUtils.ts
Normal file
78
packages/redux-devtools-app-core/src/utils/searchUtils.ts
Normal file
|
@ -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];
|
||||
}
|
||||
};
|
61
packages/redux-devtools-ui/src/StateFilter/StateFilter.tsx
Normal file
61
packages/redux-devtools-ui/src/StateFilter/StateFilter.tsx
Normal file
|
@ -0,0 +1,61 @@
|
|||
import React, {
|
||||
ChangeEvent,
|
||||
MouseEventHandler,
|
||||
PureComponent,
|
||||
ReactNode,
|
||||
SyntheticEvent,
|
||||
} from 'react';
|
||||
import createStyledComponent from '../utils/createStyledComponent';
|
||||
import styles from './styles';
|
||||
|
||||
export interface StateFilterValue {
|
||||
searchString: string;
|
||||
isJsonPath: boolean;
|
||||
}
|
||||
|
||||
export interface StateFilterProps {
|
||||
value: StateFilterValue;
|
||||
onChange: (value: Partial<StateFilterValue>) => void;
|
||||
}
|
||||
|
||||
const FormContainer = createStyledComponent(styles, 'form');
|
||||
|
||||
const searchPlaceholder = 'filter state...';
|
||||
|
||||
export class StateFilter extends PureComponent<StateFilterProps> {
|
||||
handleSubmit = (e: SyntheticEvent<HTMLFormElement, SubmitEvent>) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
this.props.onChange({
|
||||
searchString: e.currentTarget.value,
|
||||
});
|
||||
};
|
||||
toggleJSONPath: MouseEventHandler<HTMLButtonElement> = (e) => {
|
||||
this.props.onChange({
|
||||
isJsonPath: !this.props.value.isJsonPath,
|
||||
});
|
||||
};
|
||||
render(): ReactNode {
|
||||
const { searchString, isJsonPath } = this.props.value;
|
||||
return (
|
||||
<FormContainer onSubmit={this.handleSubmit}>
|
||||
<input
|
||||
type="search"
|
||||
onChange={this.handleChange}
|
||||
value={searchString}
|
||||
placeholder={searchPlaceholder}
|
||||
/>
|
||||
{/* <button type="reset" onClick={this.handleClear}>x</button> */}
|
||||
<button
|
||||
aria-pressed={isJsonPath}
|
||||
title="Use JSONPath to filter state"
|
||||
type="button"
|
||||
onClick={this.toggleJSONPath}
|
||||
>
|
||||
{'{;}'}
|
||||
</button>
|
||||
</FormContainer>
|
||||
);
|
||||
}
|
||||
}
|
30
packages/redux-devtools-ui/src/StateFilter/styles/index.ts
Normal file
30
packages/redux-devtools-ui/src/StateFilter/styles/index.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { css, ThemedStyledProps } from 'styled-components';
|
||||
|
||||
import { Theme } from '../../themes/default';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
export default ({ theme }: ThemedStyledProps<{}, Theme>) => css`
|
||||
align-self: center;
|
||||
height: 100%;
|
||||
border-bottom: 0.5px solid;
|
||||
|
||||
input {
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: ${theme.base06};
|
||||
}
|
||||
input::-webkit-input-placeholder {
|
||||
color: ${theme.base06};
|
||||
opacity: 0.6;
|
||||
}
|
||||
input::-moz-placeholder {
|
||||
color: ${theme.base06};
|
||||
opacity: 0.6;
|
||||
}
|
||||
button[aria-pressed='true'] {
|
||||
filter: invert();
|
||||
}
|
||||
button:focus {
|
||||
}
|
||||
`;
|
|
@ -1,6 +1,7 @@
|
|||
import React, { Component } from 'react';
|
||||
import TabsHeader, { ReactButtonElement, Tab } from './TabsHeader';
|
||||
import { TabsContainer } from './styles/common';
|
||||
import { StateFilterValue } from '../StateFilter/StateFilter';
|
||||
|
||||
export type Position = 'left' | 'right' | 'center';
|
||||
|
||||
|
@ -11,6 +12,8 @@ export interface TabsProps<P> {
|
|||
onClick: (value: string) => void;
|
||||
collapsible?: boolean;
|
||||
position: Position;
|
||||
setFilter?: (value: Partial<StateFilterValue>) => void;
|
||||
stateFilterValue?: StateFilterValue;
|
||||
}
|
||||
|
||||
export default class Tabs<P extends object> extends Component<TabsProps<P>> {
|
||||
|
@ -63,6 +66,8 @@ export default class Tabs<P extends object> extends Component<TabsProps<P>> {
|
|||
onClick={this.props.onClick}
|
||||
selected={this.props.selected}
|
||||
position={this.props.position}
|
||||
setFilter={this.props.setFilter}
|
||||
stateFilterValue={this.props.stateFilterValue}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import { FaAngleDoubleRight } from 'react-icons/fa';
|
|||
import ContextMenu from '../ContextMenu';
|
||||
import createStyledComponent from '../utils/createStyledComponent';
|
||||
import * as styles from './styles';
|
||||
import { StateFilter, StateFilterValue } from '../StateFilter/StateFilter';
|
||||
|
||||
const TabsWrapper = createStyledComponent(styles);
|
||||
|
||||
|
@ -19,7 +20,7 @@ export interface Tab<P> {
|
|||
selector?: (tab: this) => P;
|
||||
}
|
||||
|
||||
interface Props<P> {
|
||||
interface PropsWithoutFilter<P> {
|
||||
tabs: ReactButtonElement[];
|
||||
items: Tab<P>[];
|
||||
main: boolean | undefined;
|
||||
|
@ -29,6 +30,13 @@ interface Props<P> {
|
|||
selected: string | undefined;
|
||||
}
|
||||
|
||||
interface PropsWithFilter<P> extends PropsWithoutFilter<P> {
|
||||
setFilter: (value: Partial<StateFilterValue>) => void;
|
||||
stateFilterValue: StateFilterValue;
|
||||
}
|
||||
|
||||
type Props<P> = PropsWithoutFilter<P> | PropsWithFilter<P>;
|
||||
|
||||
interface State {
|
||||
visibleTabs: ReactButtonElement[];
|
||||
hiddenTabs: ReactButtonElement[];
|
||||
|
@ -220,6 +228,12 @@ export default class TabsHeader<P> extends Component<Props<P>, State> {
|
|||
<FaAngleDoubleRight />
|
||||
</button>
|
||||
)}
|
||||
{'setFilter' in this.props && this.props.setFilter ? (
|
||||
<StateFilter
|
||||
value={this.props.stateFilterValue}
|
||||
onChange={this.props.setFilter}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
{this.props.collapsible && contextMenu && (
|
||||
<ContextMenu
|
||||
|
|
4072
pnpm-lock.yaml
4072
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user