@samitouri / QOS-React-2 / commits / 8429220164

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

Brian Vaughn committed May 1, 2019 at 10:43 UTC 84292201644143dd3f2aace2753f10caec339840
19 files changed +831 -196
src/backend/agent.js
+4 -4
@@ -16,7 +16,7 @@ import type {
16 RendererID,
17 RendererInterface,
18 } from './types';
19 -import type { Bridge, FilterPreferences } from '../types';
19 +import type { Bridge, ComponentFilter } from '../types';
20
21 const debug = (methodName, ...args) => {
22 if (__DEBUG__) {
@@ -118,7 +118,7 @@ export default class Agent extends EventEmitter {
118 this.syncSelectionFromNativeElementsPanel
119 );
120 bridge.addListener('shutdown', this.shutdown);
121 - bridge.addListener('updateFilterPreferences', this.updateFilterPreferences);
121 + bridge.addListener('updateComponentFilters', this.updateComponentFilters);
122 bridge.addListener('viewElementSource', this.viewElementSource);
123
124 if (this._isProfiling) {
@@ -490,12 +490,12 @@ export default class Agent extends EventEmitter {
490 this._bridge.send('profilingStatus', this._isProfiling);
491 };
492
493 - updateFilterPreferences = (filterPreferences: FilterPreferences) => {
493 + updateComponentFilters = (componentFilters: Array<ComponentFilter>) => {
494 for (let rendererID in this._rendererInterfaces) {
495 const renderer = ((this._rendererInterfaces[
496 (rendererID: any)
497 ]: any): RendererInterface);
498 - renderer.updateFilterPreferences(filterPreferences);
498 + renderer.updateComponentFilters(componentFilters);
499 }
500 };
501
src/backend/renderer.js
+49 -13
@@ -2,6 +2,9 @@
2
3 import { gte } from 'semver';
4 import {
5 + ComponentFilterDisplayName,
6 + ComponentFilterElementType,
7 + ComponentFilterPath,
8 ElementTypeClass,
9 ElementTypeContext,
10 ElementTypeEventComponent,
@@ -17,7 +20,7 @@ import {
20 } from 'src/types';
21 import {
22 getDisplayName,
20 - getSavedFilterPreferences,
23 + getSavedComponentFilters,
24 getUID,
25 utfEncodeString,
26 } from 'src/utils';
@@ -47,7 +50,7 @@ import type {
50 RendererInterface,
51 } from './types';
52 import type { InspectedElement } from 'src/devtools/views/Components/types';
50 -import type { ElementType, FilterPreferences } from 'src/types';
53 +import type { ComponentFilter, ElementType } from 'src/types';
54
55 function getInternalReactConstants(version) {
56 const ReactSymbols = {
@@ -261,17 +264,53 @@ export function attach(
264 }
265 };
266
264 - let {
265 - hideElementsWithTypes,
266 - hideElementsWithDisplayNames,
267 - hideElementsWithPaths,
268 - } = getSavedFilterPreferences();
267 + // Configurable Components tree filters.
268 + const hideElementsWithDisplayNames: Set<RegExp> = new Set();
269 + const hideElementsWithPaths: Set<RegExp> = new Set();
270 + const hideElementsWithTypes: Set<ElementType> = new Set();
271 +
272 + function applyComponentFilters(componentFilters: Array<ComponentFilter>) {
273 + hideElementsWithTypes.clear();
274 + hideElementsWithDisplayNames.clear();
275 + hideElementsWithPaths.clear();
276 +
277 + componentFilters.forEach(componentFilter => {
278 + if (!componentFilter.isEnabled) {
279 + return;
280 + }
281 +
282 + switch (componentFilter.type) {
283 + case ComponentFilterDisplayName:
284 + if (componentFilter.isValid && componentFilter.value !== '') {
285 + hideElementsWithDisplayNames.add(
286 + new RegExp(componentFilter.value, 'i')
287 + );
288 + }
289 + break;
290 + case ComponentFilterElementType:
291 + hideElementsWithTypes.add(componentFilter.value);
292 + break;
293 + case ComponentFilterPath:
294 + if (componentFilter.isValid && componentFilter.value !== '') {
295 + hideElementsWithPaths.add(new RegExp(componentFilter.value, 'i'));
296 + }
297 + break;
298 + default:
299 + console.warn(
300 + `Invalid component filter type "${componentFilter.type}"`
301 + );
302 + break;
303 + }
304 + });
305 + }
306 +
307 + applyComponentFilters(getSavedComponentFilters());
308
309 // TODO (filter) Should we make this operation more efficient?
310 // For example, we could add a new recursive unmount tree operation.
311 // The unmount operations are already significantly smaller than mount opreations though.
312 // This is something to keep in mind for later.
274 - function updateFilterPreferences(filterPreferences: FilterPreferences) {
313 + function updateComponentFilters(componentFilters: Array<ComponentFilter>) {
314 if (this._isProfiling) {
315 // Re-mounting a tree while profiling is in progress might break a lot of assumptions.
316 // If necessary, we could support this- but it doesn't seem like a necessary use case.
@@ -286,10 +325,7 @@ export function attach(
325 currentRootID = -1;
326 });
327
289 - hideElementsWithTypes = filterPreferences.hideElementsWithTypes;
290 - hideElementsWithDisplayNames =
291 - filterPreferences.hideElementsWithDisplayNames;
292 - hideElementsWithPaths = filterPreferences.hideElementsWithPaths;
328 + applyComponentFilters(componentFilters);
329
330 // Recursively re-mount all roots with new filter criteria applied.
331 hook.getFiberRoots(rendererID).forEach(root => {
@@ -2280,6 +2316,6 @@ export function attach(
2316 setTrackedPath,
2317 startProfiling,
2318 stopProfiling,
2283 - updateFilterPreferences,
2319 + updateComponentFilters,
2320 };
2321 }
src/backend/types.js
+2 -2
@@ -1,6 +1,6 @@
1 // @flow
2
3 -import type { ElementType, FilterPreferences } from 'src/types';
3 +import type { ComponentFilter, ElementType } from 'src/types';
4 import type { InspectedElement } from 'src/devtools/views/Components/types';
5
6 type BundleType =
@@ -139,7 +139,7 @@ export type RendererInterface = {
139 setTrackedPath: (path: Array<PathFrame> | null) => void,
140 startProfiling: () => void,
141 stopProfiling: () => void,
142 - updateFilterPreferences: (filterPreferences: FilterPreferences) => void,
142 + updateComponentFilters: (somponentFilters: Array<ComponentFilter>) => void,
143 };
144
145 export type Handler = (data: any) => void;
src/constants.js
+1 -1
@@ -6,7 +6,7 @@ export const TREE_OPERATION_REORDER_CHILDREN = 3;
6 export const TREE_OPERATION_UPDATE_TREE_BASE_DURATION = 4;
7
8 export const LOCAL_STORAGE_FILTER_PREFERENCES_KEY =
9 - 'React::DevTools::filterPreferences';
9 + 'React::DevTools::componentFilters';
10
11 export const LOCAL_STORAGE_RELOAD_AND_PROFILE_KEY =
12 'React::DevTools::reloadAndProfile';
src/devtools/store.js
+12 -12
@@ -11,8 +11,8 @@ import {
11 } from '../constants';
12 import { ElementTypeRoot } from '../types';
13 import {
14 - getSavedFilterPreferences,
15 - saveFilterPreferences,
14 + getSavedComponentFilters,
15 + saveComponentFilters,
16 utfDecodeString,
17 } from '../utils';
18 import { __DEBUG__ } from '../constants';
@@ -24,7 +24,7 @@ import type {
24 ImportedProfilingData,
25 ProfilingSnapshotNode,
26 } from './views/Profiler/types';
27 -import type { Bridge, ElementType, FilterPreferences } from '../types';
27 +import type { Bridge, ComponentFilter, ElementType } from '../types';
28
29 const debug = (methodName, ...args) => {
30 if (__DEBUG__) {
@@ -69,7 +69,7 @@ export default class Store extends EventEmitter {
69 // Should new nodes be collapsed by default when added to the tree?
70 _collapseNodesByDefault: boolean = true;
71
72 - _filterPreferences: FilterPreferences;
72 + _componentFilters: Array<ComponentFilter>;
73
74 // At least one of the injected renderers contains (DEV only) owner metadata.
75 _hasOwnerMetadata: boolean = false;
@@ -143,7 +143,7 @@ export default class Store extends EventEmitter {
143 localStorage.getItem(LOCAL_STORAGE_COLLAPSE_ROOTS_BY_DEFAULT_KEY) !==
144 'false';
145
146 - this._filterPreferences = getSavedFilterPreferences();
146 + this._componentFilters = getSavedComponentFilters();
147
148 if (config != null) {
149 const {
@@ -239,26 +239,26 @@ export default class Store extends EventEmitter {
239 this.emit('collapseNodesByDefault');
240 }
241
242 - get filterPreferences(): FilterPreferences {
243 - return this._filterPreferences;
242 + get componentFilters(): Array<ComponentFilter> {
243 + return this._componentFilters;
244 }
245 - set filterPreferences(value: FilterPreferences): void {
245 + set componentFilters(value: Array<ComponentFilter>): void {
246 if (this._isProfiling) {
247 // Re-mounting a tree while profiling is in progress might break a lot of assumptions.
248 // If necessary, we could support this- but it doesn't seem like a necessary use case.
249 throw Error('Cannot modify filter preferences while profiling');
250 }
251
252 - this._filterPreferences = value;
252 + this._componentFilters = value;
253
254 // Update persisted filter preferences stored in localStorage.
255 - saveFilterPreferences(value);
255 + saveComponentFilters(value);
256
257 // Notify the renderer that filter prefernces have changed.
258 // This is an expensive opreation; it unmounts and remounts the entire tree.
259 - this._bridge.send('updateFilterPreferences', value);
259 + this._bridge.send('updateComponentFilters', value);
260
261 - this.emit('filterPreferences');
261 + this.emit('componentFilters');
262 }
263
264 get hasOwnerMetadata(): boolean {
src/devtools/views/Button.js
+2 -2
@@ -9,13 +9,13 @@ import tooltipStyles from './Tooltip.css';
9 type Props = {
10 children: React$Node,
11 className?: string,
12 - title: string,
12 + title?: string,
13 };
14
15 export default function Button({
16 children,
17 className = '',
18 - title,
18 + title = '',
19 ...rest
20 }: Props) {
21 let button = (
src/devtools/views/ButtonIcon.js
+47 -3
@@ -6,6 +6,7 @@ import styles from './ButtonIcon.css';
6 export type IconType =
7 | 'add'
8 | 'cancel'
9 + | 'clear'
10 | 'close'
11 | 'collapsed'
12 | 'copy'
@@ -21,7 +22,11 @@ export type IconType =
22 | 'previous'
23 | 'record'
24 | 'reload'
25 + | 'save'
26 | 'search'
27 + | 'settings'
28 + | 'toggle_off'
29 + | 'toggle_on'
30 | 'undo'
31 | 'up'
32 | 'view-dom'
@@ -41,6 +46,9 @@ export default function ButtonIcon({ className = '', type }: Props) {
46 case 'cancel':
47 pathData = PATH_CANCEL;
48 break;
49 + case 'clear':
50 + pathData = PATH_CLEAR;
51 + break;
52 case 'close':
53 pathData = PATH_CLOSE;
54 break;
@@ -86,9 +94,21 @@ export default function ButtonIcon({ className = '', type }: Props) {
94 case 'reload':
95 pathData = PATH_RELOAD;
96 break;
97 + case 'save':
98 + pathData = PATH_SAVE;
99 + break;
100 case 'search':
101 pathData = PATH_SEARCH;
102 break;
103 + case 'settings':
104 + pathData = PATH_SETTINGS;
105 + break;
106 + case 'toggle_off':
107 + pathData = PATH_TOGGLE_OFF;
108 + break;
109 + case 'toggle_on':
110 + pathData = PATH_TOGGLE_ON;
111 + break;
112 case 'undo':
113 pathData = PATH_UNDO;
114 break;
@@ -120,9 +140,14 @@ export default function ButtonIcon({ className = '', type }: Props) {
140 );
141 }
142
123 -const PATH_ADD = 'M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z';
143 +const PATH_ADD =
144 + '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';
145
146 const PATH_CANCEL = `
147 + 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
148 +`;
149 +
150 +const PATH_CLEAR = `
151 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
152 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
153 `;
@@ -138,8 +163,8 @@ const PATH_COPY = `
163 `;
164
165 const PATH_DELETE = `
141 - 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
142 - 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
166 + 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
167 + 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
168 `;
169
170 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 = `
202 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z
203 `;
204
205 +const PATH_SAVE = `
206 + 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
207 +`;
208 +
209 const PATH_SEARCH = `
210 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
211 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
212 `;
213
214 +const PATH_SETTINGS = `
215 + 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
216 + 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
217 + 0-.49.18l-1.6 2.77c-.1.18-.06.39.1.51l1.69
218 + 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
219 + 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
220 + .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
221 +`;
222 +
223 +const PATH_TOGGLE_OFF =
224 + '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';
225 +
226 +const PATH_TOGGLE_ON =
227 + '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';
228 +
229 const PATH_UNDO = `
230 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
231 3.54 0 6.55 2.31 7.6 5.5l2.37-.78C21.08 11.03 17.15 8 12.5 8z
src/devtools/views/Components/ComponentFiltersModal.css new
+119
@@ -0,0 +1,119 @@
1 +.Background {
2 + position: absolute;
3 + width: 100%;
4 + height: 100%;
5 + display: flex;
6 + align-items: flex-start;
7 + justify-content: center;
8 + padding: 1rem;
9 + background-color: var(--color-modal-background);
10 + overflow: auto;
11 +}
12 +
13 +.Modal {
14 + position: relative;
15 + z-index: 3;
16 + min-width: 20rem;
17 + max-width: 100%;
18 + display: inline-block;
19 + background-color: var(--color-background);
20 + padding: 0.5rem;
21 + border: 1px solid var(--color-border);
22 + border-radius: 0.25rem;
23 +}
24 +
25 +.LeftRight {
26 + display: flex;
27 +}
28 +.Left {
29 +}
30 +.Right {
31 + flex: 1;
32 + display: flex;
33 + align-items: center;
34 + justify-content: flex-end;
35 +}
36 +
37 +.ButtonIcon {
38 + margin-right: 0.25rem;
39 +}
40 +
41 +.NoFiltersCell {
42 + padding: 0.25rem 0;
43 + color: var(--color-dim);
44 +}
45 +
46 +.Table {
47 + min-width: 20rem;
48 + margin-top: 0.5rem;
49 + border-spacing: 0;
50 +}
51 +
52 +.TableRow {
53 + padding-bottom: 0.5rem;
54 +}
55 +
56 +.TableCell {
57 + padding: 0;
58 + padding-right: 0.5rem;
59 +}
60 +.TableCell:last-of-type {
61 + text-align: right;
62 + padding-right: 0;
63 +}
64 +
65 +.Select {
66 +}
67 +
68 +.Input {
69 + border: none;
70 + border-radius: 0.125rem;
71 + padding: 0.125rem;
72 +}
73 +
74 +.CancelButton {
75 + margin-right: 0.25rem;
76 +}
77 +
78 +.InvalidRegExp,
79 +.InvalidRegExp:active,
80 +.InvalidRegExp:focus,
81 +.InvalidRegExp:hover {
82 + color: var(--color-value-invalid);
83 +}
84 +
85 +.ToggleOffInvalid,
86 +.ToggleOnInvalid,
87 +.ToggleOff,
88 +.ToggleOn {
89 + border-radius: 0.75rem;
90 + width: 1rem;
91 + height: 0.625rem;
92 + display: flex;
93 + align-items: center;
94 + padding: 0.125rem;
95 +}
96 +.ToggleOffInvalid {
97 + background-color: var(--color-toggle-background-invalid);
98 + justify-content: flex-start;
99 +}
100 +.ToggleOnInvalid {
101 + background-color: var(--color-toggle-background-invalid);
102 + justify-content: flex-end;
103 +}
104 +.ToggleOff {
105 + background-color: var(--color-toggle-background-off);
106 + justify-content: flex-start;
107 +}
108 +.ToggleOn {
109 + background-color: var(--color-toggle-background-on);
110 + justify-content: flex-end;
111 +}
112 +
113 +.ToggleInsideOff,
114 +.ToggleInsideOn {
115 + border-radius: 0.375rem;
116 + width: 0.375rem;
117 + height: 0.375rem;
118 + background-color: var(--color-toggle-text);
119 +}
src/devtools/views/Components/ComponentFiltersModal.js new
+406
@@ -0,0 +1,406 @@
1 +// @flow
2 +
3 +import React, {
4 + useCallback,
5 + useContext,
6 + useMemo,
7 + useRef,
8 + useState,
9 +} from 'react';
10 +import { useModalDismissSignal, useSubscription } from '../hooks';
11 +import { ComponentFiltersModalContext } from './ComponentFiltersModalContext';
12 +import { StoreContext } from '../context';
13 +import Button from '../Button';
14 +import ButtonIcon from '../ButtonIcon';
15 +import Toggle from '../Toggle';
16 +import Store from 'src/devtools/store';
17 +import {
18 + ComponentFilterElementType,
19 + ComponentFilterDisplayName,
20 + ComponentFilterPath,
21 + ElementTypeClass,
22 + ElementTypeContext,
23 + ElementTypeEventTarget,
24 + ElementTypeFunction,
25 + ElementTypeForwardRef,
26 + ElementTypeHostComponent,
27 + ElementTypeMemo,
28 + ElementTypeOtherOrUnknown,
29 + ElementTypeProfiler,
30 + ElementTypeSuspense,
31 +} from 'src/types';
32 +import styles from './ComponentFiltersModal.css';
33 +
34 +import type {
35 + ComponentFilter,
36 + ElementType,
37 + ElementTypeComponentFilter,
38 + FilterType,
39 + RegExpComponentFilter,
40 +} from 'src/types';
41 +
42 +export default function ComponentFiltersModalWrapper(_: {||}) {
43 + const store = useContext(StoreContext);
44 +
45 + const { isModalShowing, setIsModalShowing } = useContext(
46 + ComponentFiltersModalContext
47 + );
48 +
49 + // Re-mounting a tree while profiling is in progress might break a lot of assumptions.
50 + // If necessary, we could support this- but it doesn't seem like a necessary use case.
51 + const isProfilingSubscription = useMemo(
52 + () => ({
53 + getCurrentValue: () => store.isProfiling,
54 + subscribe: (callback: Function) => {
55 + store.addListener('isProfiling', callback);
56 + return () => store.removeListener('isProfiling', callback);
57 + },
58 + }),
59 + [store]
60 + );
61 + const isProfiling = useSubscription<boolean, Store>(isProfilingSubscription);
62 + if (isProfiling && isModalShowing) {
63 + setIsModalShowing(false);
64 + }
65 +
66 + return isModalShowing ? (
67 + <ComponentFiltersModal
68 + store={store}
69 + setIsModalShowing={setIsModalShowing}
70 + />
71 + ) : null;
72 +}
73 +
74 +type Props = {|
75 + store: Store,
76 + setIsModalShowing: (value: boolean) => void,
77 +|};
78 +
79 +function ComponentFiltersModal({ store, setIsModalShowing }: Props) {
80 + const dismissModal = useCallback(() => setIsModalShowing(false), [
81 + setIsModalShowing,
82 + ]);
83 +
84 + const modalRef = useRef<HTMLDivElement | null>(null);
85 +
86 + useModalDismissSignal(modalRef, dismissModal);
87 +
88 + const {
89 + addFilter,
90 + changeFilterType,
91 + updateFilterValueElementType,
92 + updateFilterValueRegExp,
93 + componentFilters,
94 + removeFilter,
95 + saveFilters,
96 + toggleFilterIsEnabled,
97 + } = useComponentFilters();
98 +
99 + const saveAndClose = useCallback(() => {
100 + saveFilters();
101 + dismissModal();
102 + }, [dismissModal, saveFilters]);
103 +
104 + return (
105 + <div className={styles.Background}>
106 + <div className={styles.Modal} ref={modalRef}>
107 + <div className={styles.LeftRight}>
108 + <div className={styles.Left}>Hide components where...</div>
109 + <div className={styles.Right}>
110 + <Button onClick={addFilter}>
111 + <ButtonIcon className={styles.ButtonIcon} type="add" />
112 + Add filter
113 + </Button>
114 + </div>
115 + </div>
116 + <table className={styles.Table}>
117 + <tbody>
118 + {componentFilters.length === 0 && (
119 + <tr className={styles.TableRow}>
120 + <td className={styles.NoFiltersCell}>
121 + No filters have been added.
122 + </td>
123 + </tr>
124 + )}
125 + {componentFilters.map((componentFilter, index) => (
126 + <tr className={styles.TableRow} key={index}>
127 + <td className={styles.TableCell}>
128 + <Toggle
129 + className={
130 + componentFilter.isValid !== false
131 + ? ''
132 + : styles.InvalidRegExp
133 + }
134 + isChecked={componentFilter.isEnabled}
135 + onChange={isEnabled =>
136 + toggleFilterIsEnabled(componentFilter, isEnabled)
137 + }
138 + title={
139 + componentFilter.isValid === false
140 + ? 'Filter invalid'
141 + : componentFilter.isEnabled
142 + ? 'Filter enabled'
143 + : 'Filter disabled'
144 + }
145 + >
146 + <ToggleIcon
147 + isEnabled={componentFilter.isEnabled}
148 + isValid={
149 + componentFilter.isValid == null ||
150 + componentFilter.isValid === true
151 + }
152 + />
153 + </Toggle>
154 + </td>
155 + <td className={styles.TableCell}>
156 + <select
157 + className={styles.Select}
158 + value={componentFilter.type}
159 + onChange={({ currentTarget }) =>
160 + changeFilterType(
161 + componentFilter,
162 + // $FlowFixMe TODO (filters)
163 + parseInt(currentTarget.value, 10)
164 + )
165 + }
166 + >
167 + <option value={ComponentFilterElementType}>type</option>
168 + <option value={ComponentFilterDisplayName}>name</option>
169 + <option value={ComponentFilterPath}>path</option>
170 + </select>
171 + </td>
172 + <td className={styles.TableCell}>
173 + {componentFilter.type === ComponentFilterElementType
174 + ? 'equals'
175 + : 'matches'}
176 + </td>
177 + <td className={styles.TableCell}>
178 + {componentFilter.type === ComponentFilterElementType ? (
179 + <select
180 + className={styles.Select}
181 + value={componentFilter.value}
182 + onChange={({ currentTarget }) =>
183 + updateFilterValueElementType(
184 + componentFilter,
185 + // $FlowFixMe TODO (filters)
186 + parseInt(currentTarget.value, 10)
187 + )
188 + }
189 + >
190 + <option value={ElementTypeClass}>class</option>
191 + <option value={ElementTypeContext}>context</option>
192 + <option value={ElementTypeEventTarget}>event</option>
193 + <option value={ElementTypeFunction}>function</option>
194 + <option value={ElementTypeForwardRef}>forward ref</option>
195 + <option value={ElementTypeHostComponent}>
196 + host (e.g. &lt;div&gt;)
197 + </option>
198 + <option value={ElementTypeMemo}>memo</option>
199 + <option value={ElementTypeOtherOrUnknown}>other</option>
200 + <option value={ElementTypeProfiler}>profiler</option>
201 + <option value={ElementTypeSuspense}>suspense</option>
202 + </select>
203 + ) : (
204 + <input
205 + className={styles.Input}
206 + type="text"
207 + placeholder="Regular expression"
208 + onChange={({ currentTarget }) =>
209 + updateFilterValueRegExp(
210 + componentFilter,
211 + currentTarget.value
212 + )
213 + }
214 + value={componentFilter.value}
215 + />
216 + )}
217 + </td>
218 + <td className={styles.TableCell}>
219 + <Button
220 + onClick={() => removeFilter(index)}
221 + title="Delete filter"
222 + >
223 + <ButtonIcon type="delete" />
224 + </Button>
225 + </td>
226 + </tr>
227 + ))}
228 + </tbody>
229 + </table>
230 + <div className={styles.LeftRight}>
231 + <div className={styles.Right}>
232 + <Button className={styles.CancelButton} onClick={dismissModal}>
233 + Cancel
234 + </Button>
235 + <Button onClick={saveAndClose}>Save Changes</Button>
236 + </div>
237 + </div>
238 + </div>
239 + </div>
240 + );
241 +}
242 +
243 +type ToggleIconProps = {|
244 + isEnabled: boolean,
245 + isValid: boolean,
246 +|};
247 +function ToggleIcon({ isEnabled, isValid }: ToggleIconProps) {
248 + let className;
249 + if (isValid) {
250 + className = isEnabled ? styles.ToggleOn : styles.ToggleOff;
251 + } else {
252 + className = isEnabled ? styles.ToggleOnInvalid : styles.ToggleOffInvalid;
253 + }
254 + return (
255 + <div className={className}>
256 + <div
257 + className={isEnabled ? styles.ToggleInsideOn : styles.ToggleInsideOff}
258 + />
259 + </div>
260 + );
261 +}
262 +
263 +function useComponentFilters() {
264 + const store = useContext(StoreContext);
265 +
266 + const [componentFilters, setComponentFilters] = useState<
267 + Array<ComponentFilter>
268 + >(() => [...store.componentFilters]);
269 +
270 + const addFilter = useCallback(() => {
271 + setComponentFilters(componentFilters => {
272 + return [
273 + ...componentFilters,
274 + {
275 + type: ComponentFilterElementType,
276 + value: ElementTypeHostComponent,
277 + isEnabled: true,
278 + },
279 + ];
280 + });
281 + }, []);
282 +
283 + const changeFilterType = useCallback(
284 + (componentFilter: ComponentFilter, type: FilterType) => {
285 + setComponentFilters(componentFilters => {
286 + const cloned: Array<ComponentFilter> = [...componentFilters];
287 + const index = componentFilters.indexOf(componentFilter);
288 + if (index >= 0) {
289 + if (type === ComponentFilterElementType) {
290 + // $FlowFixMe TODO (filters)
291 + cloned[index] = ({
292 + type,
293 + isEnabled: componentFilter.isEnabled,
294 + value: ElementTypeHostComponent,
295 + }: ElementTypeComponentFilter);
296 + } else if (
297 + type === ComponentFilterDisplayName ||
298 + type === ComponentFilterPath
299 + ) {
300 + // $FlowFixMe TODO (filters)
301 + cloned[index] = ({
302 + type,
303 + isEnabled: componentFilter.isEnabled,
304 + isValid: true,
305 + value: '',
306 + }: RegExpComponentFilter);
307 + }
308 + }
309 + return cloned;
310 + });
311 + },
312 + []
313 + );
314 +
315 + const updateFilterValueElementType = useCallback(
316 + (componentFilter: ComponentFilter, value: ElementType) => {
317 + if (componentFilter.type !== ComponentFilterElementType) {
318 + throw Error('Invalid value for element type filter');
319 + }
320 +
321 + setComponentFilters(componentFilters => {
322 + const cloned: Array<ComponentFilter> = [...componentFilters];
323 + const index = componentFilters.indexOf(componentFilter);
324 + if (index >= 0) {
325 + // $FlowFixMe TODO (filters)
326 + cloned[index] = {
327 + ...componentFilter,
328 + value,
329 + };
330 + }
331 + return cloned;
332 + });
333 + },
334 + []
335 + );
336 +
337 + const updateFilterValueRegExp = useCallback(
338 + (componentFilter: ComponentFilter, value: string) => {
339 + if (componentFilter.type === ComponentFilterElementType) {
340 + throw Error('Invalid value for element type filter');
341 + }
342 +
343 + setComponentFilters(componentFilters => {
344 + const cloned: Array<ComponentFilter> = [...componentFilters];
345 + const index = componentFilters.indexOf(componentFilter);
346 + if (index >= 0) {
347 + let isValid = true;
348 + try {
349 + new RegExp(value);
350 + } catch (error) {
351 + isValid = false;
352 + }
353 + // $FlowFixMe TODO (filters)
354 + cloned[index] = {
355 + ...componentFilter,
356 + isValid,
357 + value,
358 + };
359 + }
360 + return cloned;
361 + });
362 + },
363 + []
364 + );
365 +
366 + const removeFilter = useCallback((index: number) => {
367 + setComponentFilters(componentFilters => {
368 + const cloned: Array<ComponentFilter> = [...componentFilters];
369 + cloned.splice(index, 1);
370 + return cloned;
371 + });
372 + }, []);
373 +
374 + const saveFilters = useCallback(() => {
375 + store.componentFilters = [...componentFilters];
376 + }, [componentFilters, store]);
377 +
378 + const toggleFilterIsEnabled = useCallback(
379 + (componentFilter: ComponentFilter, isEnabled: boolean) => {
380 + setComponentFilters(componentFilters => {
381 + const cloned: Array<ComponentFilter> = [...componentFilters];
382 + const index = componentFilters.indexOf(componentFilter);
383 + if (index >= 0) {
384 + // $FlowFixMe TODO (filters)
385 + cloned[index] = {
386 + ...cloned[index],
387 + isEnabled,
388 + };
389 + }
390 + return cloned;
391 + });
392 + },
393 + []
394 + );
395 +
396 + return {
397 + addFilter,
398 + changeFilterType,
399 + componentFilters,
400 + removeFilter,
401 + saveFilters,
402 + toggleFilterIsEnabled,
403 + updateFilterValueElementType,
404 + updateFilterValueRegExp,
405 + };
406 +}
src/devtools/views/Components/ComponentFiltersModalContext.js new
+37
@@ -0,0 +1,37 @@
1 +// @flow
2 +
3 +import React, { createContext, useMemo, useState } from 'react';
4 +
5 +type Context = {|
6 + isModalShowing: boolean,
7 + setIsModalShowing: (value: boolean) => void,
8 +|};
9 +
10 +const ComponentFiltersModalContext = createContext<Context>(
11 + ((null: any): Context)
12 +);
13 +ComponentFiltersModalContext.displayName = 'ComponentFiltersModalContext';
14 +
15 +type Props = {|
16 + children: React$Node,
17 +|};
18 +
19 +function ComponentFiltersModalContextController({ children }: Props) {
20 + const [isModalShowing, setIsModalShowing] = useState(false);
21 +
22 + const value = useMemo(
23 + () => ({
24 + isModalShowing,
25 + setIsModalShowing,
26 + }),
27 + [isModalShowing]
28 + );
29 +
30 + return (
31 + <ComponentFiltersModalContext.Provider value={value}>
32 + {children}
33 + </ComponentFiltersModalContext.Provider>
34 + );
35 +}
36 +
37 +export { ComponentFiltersModalContext, ComponentFiltersModalContextController };
src/devtools/views/Components/ToggleComponentFiltersModalButton.js new
+42
@@ -0,0 +1,42 @@
1 +// @flow
2 +
3 +import React, { useContext, useMemo } from 'react';
4 +import { useSubscription } from '../hooks';
5 +import { ComponentFiltersModalContext } from './ComponentFiltersModalContext';
6 +import { StoreContext } from '../context';
7 +import Toggle from '../Toggle';
8 +import ButtonIcon from '../ButtonIcon';
9 +import Store from 'src/devtools/store';
10 +
11 +export default function ToggleCommitFilterModalButton() {
12 + const store = useContext(StoreContext);
13 +
14 + const { isModalShowing, setIsModalShowing } = useContext(
15 + ComponentFiltersModalContext
16 + );
17 +
18 + // Re-mounting a tree while profiling is in progress might break a lot of assumptions.
19 + // If necessary, we could support this- but it doesn't seem like a necessary use case.
20 + const isProfilingSubscription = useMemo(
21 + () => ({
22 + getCurrentValue: () => store.isProfiling,
23 + subscribe: (callback: Function) => {
24 + store.addListener('isProfiling', callback);
25 + return () => store.removeListener('isProfiling', callback);
26 + },
27 + }),
28 + [store]
29 + );
30 + const isProfiling = useSubscription<boolean, Store>(isProfilingSubscription);
31 +
32 + return (
33 + <Toggle
34 + isChecked={isModalShowing}
35 + isDisabled={isProfiling}
36 + onChange={setIsModalShowing}
37 + title="Filter preferences"
38 + >
39 + <ButtonIcon type="filter" />
40 + </Toggle>
41 + );
42 +}
src/devtools/views/Components/Tree.css
+1
@@ -1,4 +1,5 @@
1 .Tree {
2 + position: relative;
3 height: 100%;
4 width: 100%;
5 display: flex;
src/devtools/views/Components/Tree.js
+43 -35
@@ -18,6 +18,9 @@ import ElementView from './Element';
18 import InspectHostNodesToggle from './InspectHostNodesToggle';
19 import OwnersStack from './OwnersStack';
20 import SearchInput from './SearchInput';
21 +import { ComponentFiltersModalContextController } from './ComponentFiltersModalContext';
22 +import ToggleComponentFiltersModalButton from './ToggleComponentFiltersModalButton';
23 +import ComponentFiltersModal from './ComponentFiltersModal';
24
25 import styles from './Tree.css';
26
@@ -269,42 +272,47 @@ export default function Tree(props: Props) {
272 );
273
274 return (
272 - <div className={styles.Tree} ref={treeRef}>
273 - <div className={styles.SearchInput}>
274 - <InspectHostNodesToggle />
275 - <div className={styles.VRule} />
276 - {ownerStack.length > 0 ? <OwnersStack /> : <SearchInput />}
275 + <ComponentFiltersModalContextController>
276 + <div className={styles.Tree} ref={treeRef}>
277 + <div className={styles.SearchInput}>
278 + <InspectHostNodesToggle />
279 + <div className={styles.VRule} />
280 + {ownerStack.length > 0 ? <OwnersStack /> : <SearchInput />}
281 + <div className={styles.VRule} />
282 + <ToggleComponentFiltersModalButton />
283 + </div>
284 + <div
285 + className={styles.AutoSizerWrapper}
286 + onBlur={handleBlur}
287 + onFocus={handleFocus}
288 + onKeyPress={handleKeyPress}
289 + onMouseMove={handleMouseMove}
290 + onMouseLeave={handleMouseLeave}
291 + ref={focusTargetRef}
292 + tabIndex={0}
293 + >
294 + <AutoSizer>
295 + {({ height, width }) => (
296 + // $FlowFixMe https://github.com/facebook/flow/issues/7341
297 + <FixedSizeList
298 + className={styles.List}
299 + height={height}
300 + innerElementType={InnerElementType}
301 + itemCount={numElements}
302 + itemData={itemData}
303 + itemSize={lineHeight}
304 + overscanCount={3}
305 + ref={listRef}
306 + width={width}
307 + >
308 + {ElementView}
309 + </FixedSizeList>
310 + )}
311 + </AutoSizer>
312 + </div>
313 + <ComponentFiltersModal />
314 </div>
278 - <div
279 - className={styles.AutoSizerWrapper}
280 - onBlur={handleBlur}
281 - onFocus={handleFocus}
282 - onKeyPress={handleKeyPress}
283 - onMouseMove={handleMouseMove}
284 - onMouseLeave={handleMouseLeave}
285 - ref={focusTargetRef}
286 - tabIndex={0}
287 - >
288 - <AutoSizer>
289 - {({ height, width }) => (
290 - // $FlowFixMe https://github.com/facebook/flow/issues/7341
291 - <FixedSizeList
292 - className={styles.List}
293 - height={height}
294 - innerElementType={InnerElementType}
295 - itemCount={numElements}
296 - itemData={itemData}
297 - itemSize={lineHeight}
298 - overscanCount={3}
299 - ref={listRef}
300 - width={width}
301 - >
302 - {ElementView}
303 - </FixedSizeList>
304 - )}
305 - </AutoSizer>
306 - </div>
307 - </div>
315 + </ComponentFiltersModalContextController>
316 );
317 }
318
src/devtools/views/Profiler/ClearProfilingDataButton.js
+1 -1
@@ -18,7 +18,7 @@ export default function ClearProfilingDataButton() {
18 onClick={clear}
19 title="Clear profiling data"
20 >
21 - <ButtonIcon type="cancel" />
21 + <ButtonIcon type="clear" />
22 </Button>
23 );
24 }
src/devtools/views/Settings/Settings.js
-58
@@ -1,7 +1,6 @@
1 // @flow
2
3 import React, { useCallback, useContext, useMemo } from 'react';
4 -import { ElementTypeHostComponent } from 'src/types';
4 import { useSubscription } from '../hooks';
5 import { StoreContext } from '../context';
6 import { SettingsContext } from './SettingsContext';
@@ -10,8 +9,6 @@ import portaledContent from '../portaledContent';
9
10 import styles from './Settings.css';
11
13 -import type { FilterPreferences } from 'src/types';
14 -
12 function Settings(_: {||}) {
13 const store = useContext(StoreContext);
14 const { displayDensity, setDisplayDensity, theme, setTheme } = useContext(
@@ -46,49 +43,6 @@ function Settings(_: {||}) {
43 collapseNodesByDefaultSubscription
44 );
45
49 - // Re-mounting a tree while profiling is in progress might break a lot of assumptions.
50 - // If necessary, we could support this- but it doesn't seem like a necessary use case.
51 - const isProfilingSubscription = useMemo(
52 - () => ({
53 - getCurrentValue: () => store.isProfiling,
54 - subscribe: (callback: Function) => {
55 - store.addListener('isProfiling', callback);
56 - return () => store.removeListener('isProfiling', callback);
57 - },
58 - }),
59 - [store]
60 - );
61 - const isProfiling = useSubscription<boolean, Store>(isProfilingSubscription);
62 -
63 - const filterPreferencesSubscription = useMemo(
64 - () => ({
65 - getCurrentValue: () => store.filterPreferences,
66 - subscribe: (callback: Function) => {
67 - store.addListener('filterPreferences', callback);
68 - return () => store.removeListener('filterPreferences', callback);
69 - },
70 - }),
71 - [store]
72 - );
73 - const filterPreferences = useSubscription<FilterPreferences, Store>(
74 - filterPreferencesSubscription
75 - );
76 -
77 - const updateFilterPreferences = useCallback(
78 - ({ currentTarget }) => {
79 - const filterPreferences = store.filterPreferences;
80 - if (currentTarget.checked) {
81 - filterPreferences.hideElementsWithTypes.add(ElementTypeHostComponent);
82 - } else {
83 - filterPreferences.hideElementsWithTypes.delete(
84 - ElementTypeHostComponent
85 - );
86 - }
87 - store.filterPreferences = { ...filterPreferences };
88 - },
89 - [store]
90 - );
91 -
46 const updateDisplayDensity = useCallback(
47 ({ currentTarget }) => {
48 setDisplayDensity(currentTarget.value);
@@ -189,18 +143,6 @@ function Settings(_: {||}) {
143 />{' '}
144 Collapse newly added components by default
145 </label>
192 -
193 - <label className={styles.CheckboxOption}>
194 - <input
195 - type="checkbox"
196 - checked={filterPreferences.hideElementsWithTypes.has(
197 - ElementTypeHostComponent
198 - )}
199 - disabled={isProfiling}
200 - onChange={updateFilterPreferences}
201 - />{' '}
202 - Hide host components (e.g. <code>&lt;div&gt;</code>)
203 - </label>
146 </div>
147
148 {store.supportsCaptureScreenshots && (
src/devtools/views/Settings/SettingsContext.js
+5
@@ -247,6 +247,11 @@ function updateThemeVariables(
247 updateStyleHelper(theme, 'color-tab-selected-border', documentElements);
248 updateStyleHelper(theme, 'color-text', documentElements);
249 updateStyleHelper(theme, 'color-text-selected', documentElements);
250 + updateStyleHelper(theme, 'color-toggle-background-invalid', documentElements);
251 + updateStyleHelper(theme, 'color-toggle-background-on', documentElements);
252 + updateStyleHelper(theme, 'color-toggle-background-off', documentElements);
253 + updateStyleHelper(theme, 'color-toggle-text', documentElements);
254 + updateStyleHelper(theme, 'color-toggle-text', documentElements);
255 updateStyleHelper(theme, 'color-tooltip-background', documentElements);
256 updateStyleHelper(theme, 'color-tooltip-text', documentElements);
257
src/devtools/views/root.css
+8
@@ -48,6 +48,10 @@
48 --light-color-tab-selected-border: #0088fa;
49 --light-color-text: #000000;
50 --light-color-text-selected: #ffffff;
51 + --light-color-toggle-background-invalid: #fc3a4b;
52 + --light-color-toggle-background-on: #0088fa;
53 + --light-color-toggle-background-off: #cfd1d5;
54 + --light-color-toggle-text: #ffffff;
55 --light-color-tooltip-background: rgba(0, 0, 0, 0.9);
56 --light-color-tooltip-text: #ffffff;
57
@@ -96,6 +100,10 @@
100 --dark-color-tab-selected-border: #178fb9;
101 --dark-color-text: #ffffff;
102 --dark-color-text-selected: #ffffff;
103 + --dark-color-toggle-background-invalid: #fc3a4b;
104 + --dark-color-toggle-background-on: #178fb9;
105 + --dark-color-toggle-background-off: #777d88;
106 + --dark-color-toggle-text: #ffffff;
107 --dark-color-tooltip-background: rgba(255, 255, 255, 0.9);
108 --dark-color-tooltip-text: #000000;
109
src/types.js
+28 -10
@@ -13,7 +13,7 @@ export type Wall = {|
13 |};
14
15 // WARNING
16 -// The values below are referenced by FilterPreferences (which is saved via localStorage).
16 +// The values below are referenced by ComponentFilters (which are saved via localStorage).
17 // Do not change them or it will break previously saved user customizations.
18 // If new element types are added, use new numbers rather than re-ordering existing ones.
19 export const ElementTypeClass = 1;
@@ -34,15 +34,33 @@ export const ElementTypeSuspense = 12;
34 // or to enable/disable certain functionality.
35 export type ElementType = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
36
37 -export type FilterPreferences = {|
38 - // Hide all elements of types in this Set.
39 - // We hide host components only by default.
40 - hideElementsWithTypes: Set<ElementType>,
37 +// WARNING
38 +// The values below are referenced by ComponentFilters (which are saved via localStorage).
39 +// Do not change them or it will break previously saved user customizations.
40 +// If new filter types are added, use new numbers rather than re-ordering existing ones.
41 +export const ComponentFilterElementType = 1;
42 +export const ComponentFilterDisplayName = 2;
43 +export const ComponentFilterPath = 3;
44
42 - // Hide all elements with displayNames matching one or more of the RegExps in this Set.
43 - hideElementsWithDisplayNames: Set<RegExp>,
45 +export type FilterType = 1 | 2 | 3;
46
45 - // Hide all elements within paths matching one or more of the RegExps in this Set.
46 - // This filter is only used for elements that include debug source location.
47 - hideElementsWithPaths: Set<RegExp>,
47 +// Hide all elements of types in this Set.
48 +// We hide host components only by default.
49 +export type ElementTypeComponentFilter = {|
50 + isEnabled: boolean,
51 + type: 1,
52 + value: ElementType,
53 |};
54 +
55 +// Hide all elements with displayNames or paths matching one or more of the RegExps in this Set.
56 +// Path filters are only used when elements include debug source location.
57 +export type RegExpComponentFilter = {|
58 + isEnabled: boolean,
59 + isValid: boolean,
60 + type: 2 | 3,
61 + value: string,
62 +|};
63 +
64 +export type ComponentFilter =
65 + | ElementTypeComponentFilter
66 + | RegExpComponentFilter;
src/utils.js
+24 -55
@@ -2,9 +2,9 @@
2
3 import LRU from 'lru-cache';
4 import { LOCAL_STORAGE_FILTER_PREFERENCES_KEY } from './constants';
5 -import { ElementTypeHostComponent } from './types';
5 +import { ComponentFilterElementType, ElementTypeHostComponent } from './types';
6
7 -import type { FilterPreferences } from './types';
7 +import type { ComponentFilter } from './types';
8
9 const FB_MODULE_RE = /^(.*) \[from (.*)\]$/;
10 const cachedDisplayNames: WeakMap<Function, string> = new WeakMap();
@@ -81,65 +81,34 @@ function toCodePoint(string: string) {
81 return string.codePointAt(0);
82 }
83
84 -export function getDefaultFilterPreferences(): FilterPreferences {
85 - return {
86 - hideElementsWithTypes: new Set([ElementTypeHostComponent]),
87 - hideElementsWithDisplayNames: new Set(),
88 - hideElementsWithPaths: new Set(),
89 - };
84 +// TODO (filters) Save the filters as the frontend needs them (an array, with type and "enabled" status)
85 +// Convert the fitlers to Sets for the renderer to consume.
86 +
87 +export function getDefaultComponentFilters(): Array<ComponentFilter> {
88 + return [
89 + {
90 + type: ComponentFilterElementType,
91 + value: ElementTypeHostComponent,
92 + isEnabled: true,
93 + },
94 + ];
95 }
96
92 -function getSavedFilterPreferencesFilter(key, value) {
93 - if (typeof value === 'string' && value.indexOf('__REGEXP__') === 0) {
94 - const match = value.substr(9).match(/\/(.*)\/(.*)?/);
95 - return new RegExp(match[1], match[2] || '');
96 - }
97 - return value;
98 -}
99 -
100 -export function getSavedFilterPreferences(): FilterPreferences {
101 - const raw = localStorage.getItem(LOCAL_STORAGE_FILTER_PREFERENCES_KEY);
102 - if (raw != null) {
103 - const json = JSON.parse(raw, getSavedFilterPreferencesFilter);
104 - return {
105 - hideElementsWithTypes: new Set(json.hideElementsWithTypes),
106 - hideElementsWithDisplayNames: new Set(
107 - json.hideElementsWithDisplayNames.map(source => new RegExp(source))
108 - ),
109 - hideElementsWithPaths: new Set(
110 - json.hideElementsWithPaths.map(source => new RegExp(source))
111 - ),
112 - };
113 - } else {
114 - return getDefaultFilterPreferences();
115 - }
116 -}
117 -
118 -function saveFilterPreferencesFilter(key, value) {
119 - if (value instanceof RegExp) {
120 - return '__REGEXP__' + value.toString();
121 - }
122 - return value;
97 +export function getSavedComponentFilters(): Array<ComponentFilter> {
98 + try {
99 + const raw = localStorage.getItem(LOCAL_STORAGE_FILTER_PREFERENCES_KEY);
100 + if (raw != null) {
101 + return JSON.parse(raw);
102 + }
103 + } catch (error) {}
104 + return getDefaultComponentFilters();
105 }
106
125 -export function saveFilterPreferences(
126 - filterPreferences: FilterPreferences
107 +export function saveComponentFilters(
108 + componentFilters: Array<ComponentFilter>
109 ): void {
110 localStorage.setItem(
111 LOCAL_STORAGE_FILTER_PREFERENCES_KEY,
130 - JSON.stringify(
131 - {
132 - hideElementsWithTypes: Array.from(
133 - filterPreferences.hideElementsWithTypes
134 - ),
135 - hideElementsWithDisplayNames: Array.from(
136 - filterPreferences.hideElementsWithDisplayNames
137 - ),
138 - hideElementsWithPaths: Array.from(
139 - filterPreferences.hideElementsWithPaths
140 - ),
141 - },
142 - saveFilterPreferencesFilter
143 - )
112 + JSON.stringify(componentFilters)
113 );
114 }