Added filter UI (but with a lot of $FlowFixMe comments)

This commit is contained in:
Brian Vaughn
2019-05-01 10:45:18 -07:00
parent 0b4bfbc98f
commit 8429220164
19 changed files with 831 additions and 196 deletions
+4 -4
View File
@@ -16,7 +16,7 @@ import type {
RendererID,
RendererInterface,
} from './types';
import type { Bridge, FilterPreferences } from '../types';
import type { Bridge, ComponentFilter } from '../types';
const debug = (methodName, ...args) => {
if (__DEBUG__) {
@@ -118,7 +118,7 @@ export default class Agent extends EventEmitter {
this.syncSelectionFromNativeElementsPanel
);
bridge.addListener('shutdown', this.shutdown);
bridge.addListener('updateFilterPreferences', this.updateFilterPreferences);
bridge.addListener('updateComponentFilters', this.updateComponentFilters);
bridge.addListener('viewElementSource', this.viewElementSource);
if (this._isProfiling) {
@@ -490,12 +490,12 @@ export default class Agent extends EventEmitter {
this._bridge.send('profilingStatus', this._isProfiling);
};
updateFilterPreferences = (filterPreferences: FilterPreferences) => {
updateComponentFilters = (componentFilters: Array<ComponentFilter>) => {
for (let rendererID in this._rendererInterfaces) {
const renderer = ((this._rendererInterfaces[
(rendererID: any)
]: any): RendererInterface);
renderer.updateFilterPreferences(filterPreferences);
renderer.updateComponentFilters(componentFilters);
}
};
+49 -13
View File
@@ -2,6 +2,9 @@
import { gte } from 'semver';
import {
ComponentFilterDisplayName,
ComponentFilterElementType,
ComponentFilterPath,
ElementTypeClass,
ElementTypeContext,
ElementTypeEventComponent,
@@ -17,7 +20,7 @@ import {
} from 'src/types';
import {
getDisplayName,
getSavedFilterPreferences,
getSavedComponentFilters,
getUID,
utfEncodeString,
} from 'src/utils';
@@ -47,7 +50,7 @@ import type {
RendererInterface,
} from './types';
import type { InspectedElement } from 'src/devtools/views/Components/types';
import type { ElementType, FilterPreferences } from 'src/types';
import type { ComponentFilter, ElementType } from 'src/types';
function getInternalReactConstants(version) {
const ReactSymbols = {
@@ -261,17 +264,53 @@ export function attach(
}
};
let {
hideElementsWithTypes,
hideElementsWithDisplayNames,
hideElementsWithPaths,
} = getSavedFilterPreferences();
// Configurable Components tree filters.
const hideElementsWithDisplayNames: Set<RegExp> = new Set();
const hideElementsWithPaths: Set<RegExp> = new Set();
const hideElementsWithTypes: Set<ElementType> = new Set();
function applyComponentFilters(componentFilters: Array<ComponentFilter>) {
hideElementsWithTypes.clear();
hideElementsWithDisplayNames.clear();
hideElementsWithPaths.clear();
componentFilters.forEach(componentFilter => {
if (!componentFilter.isEnabled) {
return;
}
switch (componentFilter.type) {
case ComponentFilterDisplayName:
if (componentFilter.isValid && componentFilter.value !== '') {
hideElementsWithDisplayNames.add(
new RegExp(componentFilter.value, 'i')
);
}
break;
case ComponentFilterElementType:
hideElementsWithTypes.add(componentFilter.value);
break;
case ComponentFilterPath:
if (componentFilter.isValid && componentFilter.value !== '') {
hideElementsWithPaths.add(new RegExp(componentFilter.value, 'i'));
}
break;
default:
console.warn(
`Invalid component filter type "${componentFilter.type}"`
);
break;
}
});
}
applyComponentFilters(getSavedComponentFilters());
// TODO (filter) Should we make this operation more efficient?
// For example, we could add a new recursive unmount tree operation.
// The unmount operations are already significantly smaller than mount opreations though.
// This is something to keep in mind for later.
function updateFilterPreferences(filterPreferences: FilterPreferences) {
function updateComponentFilters(componentFilters: Array<ComponentFilter>) {
if (this._isProfiling) {
// Re-mounting a tree while profiling is in progress might break a lot of assumptions.
// If necessary, we could support this- but it doesn't seem like a necessary use case.
@@ -286,10 +325,7 @@ export function attach(
currentRootID = -1;
});
hideElementsWithTypes = filterPreferences.hideElementsWithTypes;
hideElementsWithDisplayNames =
filterPreferences.hideElementsWithDisplayNames;
hideElementsWithPaths = filterPreferences.hideElementsWithPaths;
applyComponentFilters(componentFilters);
// Recursively re-mount all roots with new filter criteria applied.
hook.getFiberRoots(rendererID).forEach(root => {
@@ -2280,6 +2316,6 @@ export function attach(
setTrackedPath,
startProfiling,
stopProfiling,
updateFilterPreferences,
updateComponentFilters,
};
}
+2 -2
View File
@@ -1,6 +1,6 @@
// @flow
import type { ElementType, FilterPreferences } from 'src/types';
import type { ComponentFilter, ElementType } from 'src/types';
import type { InspectedElement } from 'src/devtools/views/Components/types';
type BundleType =
@@ -139,7 +139,7 @@ export type RendererInterface = {
setTrackedPath: (path: Array<PathFrame> | null) => void,
startProfiling: () => void,
stopProfiling: () => void,
updateFilterPreferences: (filterPreferences: FilterPreferences) => void,
updateComponentFilters: (somponentFilters: Array<ComponentFilter>) => void,
};
export type Handler = (data: any) => void;
+1 -1
View File
@@ -6,7 +6,7 @@ export const TREE_OPERATION_REORDER_CHILDREN = 3;
export const TREE_OPERATION_UPDATE_TREE_BASE_DURATION = 4;
export const LOCAL_STORAGE_FILTER_PREFERENCES_KEY =
'React::DevTools::filterPreferences';
'React::DevTools::componentFilters';
export const LOCAL_STORAGE_RELOAD_AND_PROFILE_KEY =
'React::DevTools::reloadAndProfile';
+12 -12
View File
@@ -11,8 +11,8 @@ import {
} from '../constants';
import { ElementTypeRoot } from '../types';
import {
getSavedFilterPreferences,
saveFilterPreferences,
getSavedComponentFilters,
saveComponentFilters,
utfDecodeString,
} from '../utils';
import { __DEBUG__ } from '../constants';
@@ -24,7 +24,7 @@ import type {
ImportedProfilingData,
ProfilingSnapshotNode,
} from './views/Profiler/types';
import type { Bridge, ElementType, FilterPreferences } from '../types';
import type { Bridge, ComponentFilter, ElementType } from '../types';
const debug = (methodName, ...args) => {
if (__DEBUG__) {
@@ -69,7 +69,7 @@ export default class Store extends EventEmitter {
// Should new nodes be collapsed by default when added to the tree?
_collapseNodesByDefault: boolean = true;
_filterPreferences: FilterPreferences;
_componentFilters: Array<ComponentFilter>;
// At least one of the injected renderers contains (DEV only) owner metadata.
_hasOwnerMetadata: boolean = false;
@@ -143,7 +143,7 @@ export default class Store extends EventEmitter {
localStorage.getItem(LOCAL_STORAGE_COLLAPSE_ROOTS_BY_DEFAULT_KEY) !==
'false';
this._filterPreferences = getSavedFilterPreferences();
this._componentFilters = getSavedComponentFilters();
if (config != null) {
const {
@@ -239,26 +239,26 @@ export default class Store extends EventEmitter {
this.emit('collapseNodesByDefault');
}
get filterPreferences(): FilterPreferences {
return this._filterPreferences;
get componentFilters(): Array<ComponentFilter> {
return this._componentFilters;
}
set filterPreferences(value: FilterPreferences): void {
set componentFilters(value: Array<ComponentFilter>): void {
if (this._isProfiling) {
// Re-mounting a tree while profiling is in progress might break a lot of assumptions.
// If necessary, we could support this- but it doesn't seem like a necessary use case.
throw Error('Cannot modify filter preferences while profiling');
}
this._filterPreferences = value;
this._componentFilters = value;
// Update persisted filter preferences stored in localStorage.
saveFilterPreferences(value);
saveComponentFilters(value);
// Notify the renderer that filter prefernces have changed.
// This is an expensive opreation; it unmounts and remounts the entire tree.
this._bridge.send('updateFilterPreferences', value);
this._bridge.send('updateComponentFilters', value);
this.emit('filterPreferences');
this.emit('componentFilters');
}
get hasOwnerMetadata(): boolean {
+2 -2
View File
@@ -9,13 +9,13 @@ import tooltipStyles from './Tooltip.css';
type Props = {
children: React$Node,
className?: string,
title: string,
title?: string,
};
export default function Button({
children,
className = '',
title,
title = '',
...rest
}: Props) {
let button = (
+47 -3
View File
@@ -6,6 +6,7 @@ import styles from './ButtonIcon.css';
export type IconType =
| 'add'
| 'cancel'
| 'clear'
| 'close'
| 'collapsed'
| 'copy'
@@ -21,7 +22,11 @@ export type IconType =
| 'previous'
| 'record'
| 'reload'
| 'save'
| 'search'
| 'settings'
| 'toggle_off'
| 'toggle_on'
| 'undo'
| 'up'
| 'view-dom'
@@ -41,6 +46,9 @@ export default function ButtonIcon({ className = '', type }: Props) {
case 'cancel':
pathData = PATH_CANCEL;
break;
case 'clear':
pathData = PATH_CLEAR;
break;
case 'close':
pathData = PATH_CLOSE;
break;
@@ -86,9 +94,21 @@ export default function ButtonIcon({ className = '', type }: Props) {
case 'reload':
pathData = PATH_RELOAD;
break;
case 'save':
pathData = PATH_SAVE;
break;
case 'search':
pathData = PATH_SEARCH;
break;
case 'settings':
pathData = PATH_SETTINGS;
break;
case 'toggle_off':
pathData = PATH_TOGGLE_OFF;
break;
case 'toggle_on':
pathData = PATH_TOGGLE_ON;
break;
case 'undo':
pathData = PATH_UNDO;
break;
@@ -120,9 +140,14 @@ export default function ButtonIcon({ className = '', type }: Props) {
);
}
const PATH_ADD = 'M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z';
const PATH_ADD =
'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm5 11h-4v4h-2v-4H7v-2h4V7h2v4h4v2z';
const PATH_CANCEL = `
M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z
`;
const PATH_CLEAR = `
M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zM4 12c0-4.42 3.58-8 8-8 1.85 0 3.55.63 4.9 1.69L5.69
16.9C4.63 15.55 4 13.85 4 12zm8 8c-1.85 0-3.55-.63-4.9-1.69L18.31 7.1C19.37 8.45 20 10.15 20 12c0 4.42-3.58 8-8 8z
`;
@@ -138,8 +163,8 @@ const PATH_COPY = `
`;
const PATH_DELETE = `
M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zm2.46-7.12l1.41-1.41L12 12.59l2.12-2.12 1.41 1.41L13.41 14l2.12
2.12-1.41 1.41L12 15.41l-2.12 2.12-1.41-1.41L10.59 14l-2.13-2.12zM15.5 4l-1-1h-5l-1 1H5v2h14V4z
M12 2C6.47 2 2 6.47 2 12s4.47 10 10 10 10-4.47 10-10S17.53 2 12 2zm5 13.59L15.59 17 12
13.41 8.41 17 7 15.59 10.59 12 7 8.41 8.41 7 12 10.59 15.59 7 17 8.41 13.41 12 17 15.59z
`;
const PATH_DOWN = 'M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z';
@@ -177,11 +202,30 @@ const PATH_RELOAD = `
3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z
`;
const PATH_SAVE = `
M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm-5 16c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm3-10H5V5h10v4z
`;
const PATH_SEARCH = `
M8.5,22H3.7l-1.4-1.5V3.8l1.3-1.5h17.2l1,1.5v4.9h-1.3V4.3l-0.4-0.6H4.2L3.6,4.3V20l0.7,0.7h4.2V22z
M23,13.9l-4.6,3.6l4.6,4.6l-1.1,1.1l-4.7-4.4l-3.3,4.4l-3.2-12.3L23,13.9z
`;
const PATH_SETTINGS = `
M15.95 10.78c.03-.25.05-.51.05-.78s-.02-.53-.06-.78l1.69-1.32c.15-.12.19-.34.1-.51l-1.6-2.77c-.1-.18-.31-.24-.49-.18l-1.99.8c-.42-.32-.86-.58-1.35-.78L12
2.34c-.03-.2-.2-.34-.4-.34H8.4c-.2 0-.36.14-.39.34l-.3 2.12c-.49.2-.94.47-1.35.78l-1.99-.8c-.18-.07-.39
0-.49.18l-1.6 2.77c-.1.18-.06.39.1.51l1.69
1.32c-.04.25-.07.52-.07.78s.02.53.06.78L2.37 12.1c-.15.12-.19.34-.1.51l1.6 2.77c.1.18.31.24.49.18l1.99-.8c.42.32.86.58
1.35.78l.3 2.12c.04.2.2.34.4.34h3.2c.2 0 .37-.14.39-.34l.3-2.12c.49-.2.94-.47 1.35-.78l1.99.8c.18.07.39 0
.49-.18l1.6-2.77c.1-.18.06-.39-.1-.51l-1.67-1.32zM10 13c-1.65 0-3-1.35-3-3s1.35-3 3-3 3 1.35 3 3-1.35 3-3 3z
`;
const PATH_TOGGLE_OFF =
'M17 7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h10c2.76 0 5-2.24 5-5s-2.24-5-5-5zM7 15c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3z';
const PATH_TOGGLE_ON =
'M17 7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h10c2.76 0 5-2.24 5-5s-2.24-5-5-5zm0 8c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3z';
const PATH_UNDO = `
M12.5 8c-2.65 0-5.05.99-6.9 2.6L2 7v9h9l-3.62-3.62c1.39-1.16 3.16-1.88 5.12-1.88
3.54 0 6.55 2.31 7.6 5.5l2.37-.78C21.08 11.03 17.15 8 12.5 8z
@@ -0,0 +1,119 @@
.Background {
position: absolute;
width: 100%;
height: 100%;
display: flex;
align-items: flex-start;
justify-content: center;
padding: 1rem;
background-color: var(--color-modal-background);
overflow: auto;
}
.Modal {
position: relative;
z-index: 3;
min-width: 20rem;
max-width: 100%;
display: inline-block;
background-color: var(--color-background);
padding: 0.5rem;
border: 1px solid var(--color-border);
border-radius: 0.25rem;
}
.LeftRight {
display: flex;
}
.Left {
}
.Right {
flex: 1;
display: flex;
align-items: center;
justify-content: flex-end;
}
.ButtonIcon {
margin-right: 0.25rem;
}
.NoFiltersCell {
padding: 0.25rem 0;
color: var(--color-dim);
}
.Table {
min-width: 20rem;
margin-top: 0.5rem;
border-spacing: 0;
}
.TableRow {
padding-bottom: 0.5rem;
}
.TableCell {
padding: 0;
padding-right: 0.5rem;
}
.TableCell:last-of-type {
text-align: right;
padding-right: 0;
}
.Select {
}
.Input {
border: none;
border-radius: 0.125rem;
padding: 0.125rem;
}
.CancelButton {
margin-right: 0.25rem;
}
.InvalidRegExp,
.InvalidRegExp:active,
.InvalidRegExp:focus,
.InvalidRegExp:hover {
color: var(--color-value-invalid);
}
.ToggleOffInvalid,
.ToggleOnInvalid,
.ToggleOff,
.ToggleOn {
border-radius: 0.75rem;
width: 1rem;
height: 0.625rem;
display: flex;
align-items: center;
padding: 0.125rem;
}
.ToggleOffInvalid {
background-color: var(--color-toggle-background-invalid);
justify-content: flex-start;
}
.ToggleOnInvalid {
background-color: var(--color-toggle-background-invalid);
justify-content: flex-end;
}
.ToggleOff {
background-color: var(--color-toggle-background-off);
justify-content: flex-start;
}
.ToggleOn {
background-color: var(--color-toggle-background-on);
justify-content: flex-end;
}
.ToggleInsideOff,
.ToggleInsideOn {
border-radius: 0.375rem;
width: 0.375rem;
height: 0.375rem;
background-color: var(--color-toggle-text);
}
@@ -0,0 +1,406 @@
// @flow
import React, {
useCallback,
useContext,
useMemo,
useRef,
useState,
} from 'react';
import { useModalDismissSignal, useSubscription } from '../hooks';
import { ComponentFiltersModalContext } from './ComponentFiltersModalContext';
import { StoreContext } from '../context';
import Button from '../Button';
import ButtonIcon from '../ButtonIcon';
import Toggle from '../Toggle';
import Store from 'src/devtools/store';
import {
ComponentFilterElementType,
ComponentFilterDisplayName,
ComponentFilterPath,
ElementTypeClass,
ElementTypeContext,
ElementTypeEventTarget,
ElementTypeFunction,
ElementTypeForwardRef,
ElementTypeHostComponent,
ElementTypeMemo,
ElementTypeOtherOrUnknown,
ElementTypeProfiler,
ElementTypeSuspense,
} from 'src/types';
import styles from './ComponentFiltersModal.css';
import type {
ComponentFilter,
ElementType,
ElementTypeComponentFilter,
FilterType,
RegExpComponentFilter,
} from 'src/types';
export default function ComponentFiltersModalWrapper(_: {||}) {
const store = useContext(StoreContext);
const { isModalShowing, setIsModalShowing } = useContext(
ComponentFiltersModalContext
);
// Re-mounting a tree while profiling is in progress might break a lot of assumptions.
// If necessary, we could support this- but it doesn't seem like a necessary use case.
const isProfilingSubscription = useMemo(
() => ({
getCurrentValue: () => store.isProfiling,
subscribe: (callback: Function) => {
store.addListener('isProfiling', callback);
return () => store.removeListener('isProfiling', callback);
},
}),
[store]
);
const isProfiling = useSubscription<boolean, Store>(isProfilingSubscription);
if (isProfiling && isModalShowing) {
setIsModalShowing(false);
}
return isModalShowing ? (
<ComponentFiltersModal
store={store}
setIsModalShowing={setIsModalShowing}
/>
) : null;
}
type Props = {|
store: Store,
setIsModalShowing: (value: boolean) => void,
|};
function ComponentFiltersModal({ store, setIsModalShowing }: Props) {
const dismissModal = useCallback(() => setIsModalShowing(false), [
setIsModalShowing,
]);
const modalRef = useRef<HTMLDivElement | null>(null);
useModalDismissSignal(modalRef, dismissModal);
const {
addFilter,
changeFilterType,
updateFilterValueElementType,
updateFilterValueRegExp,
componentFilters,
removeFilter,
saveFilters,
toggleFilterIsEnabled,
} = useComponentFilters();
const saveAndClose = useCallback(() => {
saveFilters();
dismissModal();
}, [dismissModal, saveFilters]);
return (
<div className={styles.Background}>
<div className={styles.Modal} ref={modalRef}>
<div className={styles.LeftRight}>
<div className={styles.Left}>Hide components where...</div>
<div className={styles.Right}>
<Button onClick={addFilter}>
<ButtonIcon className={styles.ButtonIcon} type="add" />
Add filter
</Button>
</div>
</div>
<table className={styles.Table}>
<tbody>
{componentFilters.length === 0 && (
<tr className={styles.TableRow}>
<td className={styles.NoFiltersCell}>
No filters have been added.
</td>
</tr>
)}
{componentFilters.map((componentFilter, index) => (
<tr className={styles.TableRow} key={index}>
<td className={styles.TableCell}>
<Toggle
className={
componentFilter.isValid !== false
? ''
: styles.InvalidRegExp
}
isChecked={componentFilter.isEnabled}
onChange={isEnabled =>
toggleFilterIsEnabled(componentFilter, isEnabled)
}
title={
componentFilter.isValid === false
? 'Filter invalid'
: componentFilter.isEnabled
? 'Filter enabled'
: 'Filter disabled'
}
>
<ToggleIcon
isEnabled={componentFilter.isEnabled}
isValid={
componentFilter.isValid == null ||
componentFilter.isValid === true
}
/>
</Toggle>
</td>
<td className={styles.TableCell}>
<select
className={styles.Select}
value={componentFilter.type}
onChange={({ currentTarget }) =>
changeFilterType(
componentFilter,
// $FlowFixMe TODO (filters)
parseInt(currentTarget.value, 10)
)
}
>
<option value={ComponentFilterElementType}>type</option>
<option value={ComponentFilterDisplayName}>name</option>
<option value={ComponentFilterPath}>path</option>
</select>
</td>
<td className={styles.TableCell}>
{componentFilter.type === ComponentFilterElementType
? 'equals'
: 'matches'}
</td>
<td className={styles.TableCell}>
{componentFilter.type === ComponentFilterElementType ? (
<select
className={styles.Select}
value={componentFilter.value}
onChange={({ currentTarget }) =>
updateFilterValueElementType(
componentFilter,
// $FlowFixMe TODO (filters)
parseInt(currentTarget.value, 10)
)
}
>
<option value={ElementTypeClass}>class</option>
<option value={ElementTypeContext}>context</option>
<option value={ElementTypeEventTarget}>event</option>
<option value={ElementTypeFunction}>function</option>
<option value={ElementTypeForwardRef}>forward ref</option>
<option value={ElementTypeHostComponent}>
host (e.g. &lt;div&gt;)
</option>
<option value={ElementTypeMemo}>memo</option>
<option value={ElementTypeOtherOrUnknown}>other</option>
<option value={ElementTypeProfiler}>profiler</option>
<option value={ElementTypeSuspense}>suspense</option>
</select>
) : (
<input
className={styles.Input}
type="text"
placeholder="Regular expression"
onChange={({ currentTarget }) =>
updateFilterValueRegExp(
componentFilter,
currentTarget.value
)
}
value={componentFilter.value}
/>
)}
</td>
<td className={styles.TableCell}>
<Button
onClick={() => removeFilter(index)}
title="Delete filter"
>
<ButtonIcon type="delete" />
</Button>
</td>
</tr>
))}
</tbody>
</table>
<div className={styles.LeftRight}>
<div className={styles.Right}>
<Button className={styles.CancelButton} onClick={dismissModal}>
Cancel
</Button>
<Button onClick={saveAndClose}>Save Changes</Button>
</div>
</div>
</div>
</div>
);
}
type ToggleIconProps = {|
isEnabled: boolean,
isValid: boolean,
|};
function ToggleIcon({ isEnabled, isValid }: ToggleIconProps) {
let className;
if (isValid) {
className = isEnabled ? styles.ToggleOn : styles.ToggleOff;
} else {
className = isEnabled ? styles.ToggleOnInvalid : styles.ToggleOffInvalid;
}
return (
<div className={className}>
<div
className={isEnabled ? styles.ToggleInsideOn : styles.ToggleInsideOff}
/>
</div>
);
}
function useComponentFilters() {
const store = useContext(StoreContext);
const [componentFilters, setComponentFilters] = useState<
Array<ComponentFilter>
>(() => [...store.componentFilters]);
const addFilter = useCallback(() => {
setComponentFilters(componentFilters => {
return [
...componentFilters,
{
type: ComponentFilterElementType,
value: ElementTypeHostComponent,
isEnabled: true,
},
];
});
}, []);
const changeFilterType = useCallback(
(componentFilter: ComponentFilter, type: FilterType) => {
setComponentFilters(componentFilters => {
const cloned: Array<ComponentFilter> = [...componentFilters];
const index = componentFilters.indexOf(componentFilter);
if (index >= 0) {
if (type === ComponentFilterElementType) {
// $FlowFixMe TODO (filters)
cloned[index] = ({
type,
isEnabled: componentFilter.isEnabled,
value: ElementTypeHostComponent,
}: ElementTypeComponentFilter);
} else if (
type === ComponentFilterDisplayName ||
type === ComponentFilterPath
) {
// $FlowFixMe TODO (filters)
cloned[index] = ({
type,
isEnabled: componentFilter.isEnabled,
isValid: true,
value: '',
}: RegExpComponentFilter);
}
}
return cloned;
});
},
[]
);
const updateFilterValueElementType = useCallback(
(componentFilter: ComponentFilter, value: ElementType) => {
if (componentFilter.type !== ComponentFilterElementType) {
throw Error('Invalid value for element type filter');
}
setComponentFilters(componentFilters => {
const cloned: Array<ComponentFilter> = [...componentFilters];
const index = componentFilters.indexOf(componentFilter);
if (index >= 0) {
// $FlowFixMe TODO (filters)
cloned[index] = {
...componentFilter,
value,
};
}
return cloned;
});
},
[]
);
const updateFilterValueRegExp = useCallback(
(componentFilter: ComponentFilter, value: string) => {
if (componentFilter.type === ComponentFilterElementType) {
throw Error('Invalid value for element type filter');
}
setComponentFilters(componentFilters => {
const cloned: Array<ComponentFilter> = [...componentFilters];
const index = componentFilters.indexOf(componentFilter);
if (index >= 0) {
let isValid = true;
try {
new RegExp(value);
} catch (error) {
isValid = false;
}
// $FlowFixMe TODO (filters)
cloned[index] = {
...componentFilter,
isValid,
value,
};
}
return cloned;
});
},
[]
);
const removeFilter = useCallback((index: number) => {
setComponentFilters(componentFilters => {
const cloned: Array<ComponentFilter> = [...componentFilters];
cloned.splice(index, 1);
return cloned;
});
}, []);
const saveFilters = useCallback(() => {
store.componentFilters = [...componentFilters];
}, [componentFilters, store]);
const toggleFilterIsEnabled = useCallback(
(componentFilter: ComponentFilter, isEnabled: boolean) => {
setComponentFilters(componentFilters => {
const cloned: Array<ComponentFilter> = [...componentFilters];
const index = componentFilters.indexOf(componentFilter);
if (index >= 0) {
// $FlowFixMe TODO (filters)
cloned[index] = {
...cloned[index],
isEnabled,
};
}
return cloned;
});
},
[]
);
return {
addFilter,
changeFilterType,
componentFilters,
removeFilter,
saveFilters,
toggleFilterIsEnabled,
updateFilterValueElementType,
updateFilterValueRegExp,
};
}
@@ -0,0 +1,37 @@
// @flow
import React, { createContext, useMemo, useState } from 'react';
type Context = {|
isModalShowing: boolean,
setIsModalShowing: (value: boolean) => void,
|};
const ComponentFiltersModalContext = createContext<Context>(
((null: any): Context)
);
ComponentFiltersModalContext.displayName = 'ComponentFiltersModalContext';
type Props = {|
children: React$Node,
|};
function ComponentFiltersModalContextController({ children }: Props) {
const [isModalShowing, setIsModalShowing] = useState(false);
const value = useMemo(
() => ({
isModalShowing,
setIsModalShowing,
}),
[isModalShowing]
);
return (
<ComponentFiltersModalContext.Provider value={value}>
{children}
</ComponentFiltersModalContext.Provider>
);
}
export { ComponentFiltersModalContext, ComponentFiltersModalContextController };
@@ -0,0 +1,42 @@
// @flow
import React, { useContext, useMemo } from 'react';
import { useSubscription } from '../hooks';
import { ComponentFiltersModalContext } from './ComponentFiltersModalContext';
import { StoreContext } from '../context';
import Toggle from '../Toggle';
import ButtonIcon from '../ButtonIcon';
import Store from 'src/devtools/store';
export default function ToggleCommitFilterModalButton() {
const store = useContext(StoreContext);
const { isModalShowing, setIsModalShowing } = useContext(
ComponentFiltersModalContext
);
// Re-mounting a tree while profiling is in progress might break a lot of assumptions.
// If necessary, we could support this- but it doesn't seem like a necessary use case.
const isProfilingSubscription = useMemo(
() => ({
getCurrentValue: () => store.isProfiling,
subscribe: (callback: Function) => {
store.addListener('isProfiling', callback);
return () => store.removeListener('isProfiling', callback);
},
}),
[store]
);
const isProfiling = useSubscription<boolean, Store>(isProfilingSubscription);
return (
<Toggle
isChecked={isModalShowing}
isDisabled={isProfiling}
onChange={setIsModalShowing}
title="Filter preferences"
>
<ButtonIcon type="filter" />
</Toggle>
);
}
+1
View File
@@ -1,4 +1,5 @@
.Tree {
position: relative;
height: 100%;
width: 100%;
display: flex;
+43 -35
View File
@@ -18,6 +18,9 @@ import ElementView from './Element';
import InspectHostNodesToggle from './InspectHostNodesToggle';
import OwnersStack from './OwnersStack';
import SearchInput from './SearchInput';
import { ComponentFiltersModalContextController } from './ComponentFiltersModalContext';
import ToggleComponentFiltersModalButton from './ToggleComponentFiltersModalButton';
import ComponentFiltersModal from './ComponentFiltersModal';
import styles from './Tree.css';
@@ -269,42 +272,47 @@ export default function Tree(props: Props) {
);
return (
<div className={styles.Tree} ref={treeRef}>
<div className={styles.SearchInput}>
<InspectHostNodesToggle />
<div className={styles.VRule} />
{ownerStack.length > 0 ? <OwnersStack /> : <SearchInput />}
<ComponentFiltersModalContextController>
<div className={styles.Tree} ref={treeRef}>
<div className={styles.SearchInput}>
<InspectHostNodesToggle />
<div className={styles.VRule} />
{ownerStack.length > 0 ? <OwnersStack /> : <SearchInput />}
<div className={styles.VRule} />
<ToggleComponentFiltersModalButton />
</div>
<div
className={styles.AutoSizerWrapper}
onBlur={handleBlur}
onFocus={handleFocus}
onKeyPress={handleKeyPress}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
ref={focusTargetRef}
tabIndex={0}
>
<AutoSizer>
{({ height, width }) => (
// $FlowFixMe https://github.com/facebook/flow/issues/7341
<FixedSizeList
className={styles.List}
height={height}
innerElementType={InnerElementType}
itemCount={numElements}
itemData={itemData}
itemSize={lineHeight}
overscanCount={3}
ref={listRef}
width={width}
>
{ElementView}
</FixedSizeList>
)}
</AutoSizer>
</div>
<ComponentFiltersModal />
</div>
<div
className={styles.AutoSizerWrapper}
onBlur={handleBlur}
onFocus={handleFocus}
onKeyPress={handleKeyPress}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
ref={focusTargetRef}
tabIndex={0}
>
<AutoSizer>
{({ height, width }) => (
// $FlowFixMe https://github.com/facebook/flow/issues/7341
<FixedSizeList
className={styles.List}
height={height}
innerElementType={InnerElementType}
itemCount={numElements}
itemData={itemData}
itemSize={lineHeight}
overscanCount={3}
ref={listRef}
width={width}
>
{ElementView}
</FixedSizeList>
)}
</AutoSizer>
</div>
</div>
</ComponentFiltersModalContextController>
);
}
@@ -18,7 +18,7 @@ export default function ClearProfilingDataButton() {
onClick={clear}
title="Clear profiling data"
>
<ButtonIcon type="cancel" />
<ButtonIcon type="clear" />
</Button>
);
}
-58
View File
@@ -1,7 +1,6 @@
// @flow
import React, { useCallback, useContext, useMemo } from 'react';
import { ElementTypeHostComponent } from 'src/types';
import { useSubscription } from '../hooks';
import { StoreContext } from '../context';
import { SettingsContext } from './SettingsContext';
@@ -10,8 +9,6 @@ import portaledContent from '../portaledContent';
import styles from './Settings.css';
import type { FilterPreferences } from 'src/types';
function Settings(_: {||}) {
const store = useContext(StoreContext);
const { displayDensity, setDisplayDensity, theme, setTheme } = useContext(
@@ -46,49 +43,6 @@ function Settings(_: {||}) {
collapseNodesByDefaultSubscription
);
// Re-mounting a tree while profiling is in progress might break a lot of assumptions.
// If necessary, we could support this- but it doesn't seem like a necessary use case.
const isProfilingSubscription = useMemo(
() => ({
getCurrentValue: () => store.isProfiling,
subscribe: (callback: Function) => {
store.addListener('isProfiling', callback);
return () => store.removeListener('isProfiling', callback);
},
}),
[store]
);
const isProfiling = useSubscription<boolean, Store>(isProfilingSubscription);
const filterPreferencesSubscription = useMemo(
() => ({
getCurrentValue: () => store.filterPreferences,
subscribe: (callback: Function) => {
store.addListener('filterPreferences', callback);
return () => store.removeListener('filterPreferences', callback);
},
}),
[store]
);
const filterPreferences = useSubscription<FilterPreferences, Store>(
filterPreferencesSubscription
);
const updateFilterPreferences = useCallback(
({ currentTarget }) => {
const filterPreferences = store.filterPreferences;
if (currentTarget.checked) {
filterPreferences.hideElementsWithTypes.add(ElementTypeHostComponent);
} else {
filterPreferences.hideElementsWithTypes.delete(
ElementTypeHostComponent
);
}
store.filterPreferences = { ...filterPreferences };
},
[store]
);
const updateDisplayDensity = useCallback(
({ currentTarget }) => {
setDisplayDensity(currentTarget.value);
@@ -189,18 +143,6 @@ function Settings(_: {||}) {
/>{' '}
Collapse newly added components by default
</label>
<label className={styles.CheckboxOption}>
<input
type="checkbox"
checked={filterPreferences.hideElementsWithTypes.has(
ElementTypeHostComponent
)}
disabled={isProfiling}
onChange={updateFilterPreferences}
/>{' '}
Hide host components (e.g. <code>&lt;div&gt;</code>)
</label>
</div>
{store.supportsCaptureScreenshots && (
@@ -247,6 +247,11 @@ function updateThemeVariables(
updateStyleHelper(theme, 'color-tab-selected-border', documentElements);
updateStyleHelper(theme, 'color-text', documentElements);
updateStyleHelper(theme, 'color-text-selected', documentElements);
updateStyleHelper(theme, 'color-toggle-background-invalid', documentElements);
updateStyleHelper(theme, 'color-toggle-background-on', documentElements);
updateStyleHelper(theme, 'color-toggle-background-off', documentElements);
updateStyleHelper(theme, 'color-toggle-text', documentElements);
updateStyleHelper(theme, 'color-toggle-text', documentElements);
updateStyleHelper(theme, 'color-tooltip-background', documentElements);
updateStyleHelper(theme, 'color-tooltip-text', documentElements);
+8
View File
@@ -48,6 +48,10 @@
--light-color-tab-selected-border: #0088fa;
--light-color-text: #000000;
--light-color-text-selected: #ffffff;
--light-color-toggle-background-invalid: #fc3a4b;
--light-color-toggle-background-on: #0088fa;
--light-color-toggle-background-off: #cfd1d5;
--light-color-toggle-text: #ffffff;
--light-color-tooltip-background: rgba(0, 0, 0, 0.9);
--light-color-tooltip-text: #ffffff;
@@ -96,6 +100,10 @@
--dark-color-tab-selected-border: #178fb9;
--dark-color-text: #ffffff;
--dark-color-text-selected: #ffffff;
--dark-color-toggle-background-invalid: #fc3a4b;
--dark-color-toggle-background-on: #178fb9;
--dark-color-toggle-background-off: #777d88;
--dark-color-toggle-text: #ffffff;
--dark-color-tooltip-background: rgba(255, 255, 255, 0.9);
--dark-color-tooltip-text: #000000;
+28 -10
View File
@@ -13,7 +13,7 @@ export type Wall = {|
|};
// WARNING
// The values below are referenced by FilterPreferences (which is saved via localStorage).
// The values below are referenced by ComponentFilters (which are saved via localStorage).
// Do not change them or it will break previously saved user customizations.
// If new element types are added, use new numbers rather than re-ordering existing ones.
export const ElementTypeClass = 1;
@@ -34,15 +34,33 @@ export const ElementTypeSuspense = 12;
// or to enable/disable certain functionality.
export type ElementType = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
export type FilterPreferences = {|
// Hide all elements of types in this Set.
// We hide host components only by default.
hideElementsWithTypes: Set<ElementType>,
// WARNING
// The values below are referenced by ComponentFilters (which are saved via localStorage).
// Do not change them or it will break previously saved user customizations.
// If new filter types are added, use new numbers rather than re-ordering existing ones.
export const ComponentFilterElementType = 1;
export const ComponentFilterDisplayName = 2;
export const ComponentFilterPath = 3;
// Hide all elements with displayNames matching one or more of the RegExps in this Set.
hideElementsWithDisplayNames: Set<RegExp>,
export type FilterType = 1 | 2 | 3;
// Hide all elements within paths matching one or more of the RegExps in this Set.
// This filter is only used for elements that include debug source location.
hideElementsWithPaths: Set<RegExp>,
// Hide all elements of types in this Set.
// We hide host components only by default.
export type ElementTypeComponentFilter = {|
isEnabled: boolean,
type: 1,
value: ElementType,
|};
// Hide all elements with displayNames or paths matching one or more of the RegExps in this Set.
// Path filters are only used when elements include debug source location.
export type RegExpComponentFilter = {|
isEnabled: boolean,
isValid: boolean,
type: 2 | 3,
value: string,
|};
export type ComponentFilter =
| ElementTypeComponentFilter
| RegExpComponentFilter;
+24 -55
View File
@@ -2,9 +2,9 @@
import LRU from 'lru-cache';
import { LOCAL_STORAGE_FILTER_PREFERENCES_KEY } from './constants';
import { ElementTypeHostComponent } from './types';
import { ComponentFilterElementType, ElementTypeHostComponent } from './types';
import type { FilterPreferences } from './types';
import type { ComponentFilter } from './types';
const FB_MODULE_RE = /^(.*) \[from (.*)\]$/;
const cachedDisplayNames: WeakMap<Function, string> = new WeakMap();
@@ -81,65 +81,34 @@ function toCodePoint(string: string) {
return string.codePointAt(0);
}
export function getDefaultFilterPreferences(): FilterPreferences {
return {
hideElementsWithTypes: new Set([ElementTypeHostComponent]),
hideElementsWithDisplayNames: new Set(),
hideElementsWithPaths: new Set(),
};
// TODO (filters) Save the filters as the frontend needs them (an array, with type and "enabled" status)
// Convert the fitlers to Sets for the renderer to consume.
export function getDefaultComponentFilters(): Array<ComponentFilter> {
return [
{
type: ComponentFilterElementType,
value: ElementTypeHostComponent,
isEnabled: true,
},
];
}
function getSavedFilterPreferencesFilter(key, value) {
if (typeof value === 'string' && value.indexOf('__REGEXP__') === 0) {
const match = value.substr(9).match(/\/(.*)\/(.*)?/);
return new RegExp(match[1], match[2] || '');
}
return value;
export function getSavedComponentFilters(): Array<ComponentFilter> {
try {
const raw = localStorage.getItem(LOCAL_STORAGE_FILTER_PREFERENCES_KEY);
if (raw != null) {
return JSON.parse(raw);
}
} catch (error) {}
return getDefaultComponentFilters();
}
export function getSavedFilterPreferences(): FilterPreferences {
const raw = localStorage.getItem(LOCAL_STORAGE_FILTER_PREFERENCES_KEY);
if (raw != null) {
const json = JSON.parse(raw, getSavedFilterPreferencesFilter);
return {
hideElementsWithTypes: new Set(json.hideElementsWithTypes),
hideElementsWithDisplayNames: new Set(
json.hideElementsWithDisplayNames.map(source => new RegExp(source))
),
hideElementsWithPaths: new Set(
json.hideElementsWithPaths.map(source => new RegExp(source))
),
};
} else {
return getDefaultFilterPreferences();
}
}
function saveFilterPreferencesFilter(key, value) {
if (value instanceof RegExp) {
return '__REGEXP__' + value.toString();
}
return value;
}
export function saveFilterPreferences(
filterPreferences: FilterPreferences
export function saveComponentFilters(
componentFilters: Array<ComponentFilter>
): void {
localStorage.setItem(
LOCAL_STORAGE_FILTER_PREFERENCES_KEY,
JSON.stringify(
{
hideElementsWithTypes: Array.from(
filterPreferences.hideElementsWithTypes
),
hideElementsWithDisplayNames: Array.from(
filterPreferences.hideElementsWithDisplayNames
),
hideElementsWithPaths: Array.from(
filterPreferences.hideElementsWithPaths
),
},
saveFilterPreferencesFilter
)
JSON.stringify(componentFilters)
);
}