Removed Settings panel in favor of new shared Settings modal
Brian Vaughn committed
Jun 10, 2019 at 14:57 UTC
19ed98d0c25fc4aeb3d96656c3f171497d623e29
27 files changed
+1004
-1168
shells/browser/shared/src/main.js
-17
@@ -269,23 +269,6 @@ function createPanelIfReactLoaded() {
269
});
270
});
271
272
- chrome.devtools.panels.create('⚛ Settings', '', 'panel.html', panel => {
273
- panel.onShown.addListener(panel => {
274
- if (currentPanel === panel) {
275
- return;
276
- }
277
-
278
- currentPanel = panel;
279
- settingsPortalContainer = panel.container;
280
-
281
- if (settingsPortalContainer != null) {
282
- ensureInitialHTMLIsCleared(settingsPortalContainer);
283
- render('settings');
284
- panel.injectStyles(cloneStyleTags);
285
- }
286
- });
287
- });
288
-
272
chrome.devtools.network.onNavigated.removeListener(checkPageForReact);
273
274
// Shutdown bridge before a new page is loaded.
src/devtools/views/Components/ComponentFiltersModal.js
deleted
-445
@@ -1,445 +0,0 @@
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
- ComponentFilterDisplayName,
19
- ComponentFilterElementType,
20
- ComponentFilterHOC,
21
- ComponentFilterLocation,
22
- ElementTypeClass,
23
- ElementTypeContext,
24
- ElementTypeEventTarget,
25
- ElementTypeFunction,
26
- ElementTypeForwardRef,
27
- ElementTypeHostComponent,
28
- ElementTypeMemo,
29
- ElementTypeOtherOrUnknown,
30
- ElementTypeProfiler,
31
- ElementTypeSuspense,
32
-} from 'src/types';
33
-import styles from './ComponentFiltersModal.css';
34
-
35
-import type {
36
- BooleanComponentFilter,
37
- ComponentFilter,
38
- ComponentFilterType,
39
- ElementType,
40
- ElementTypeComponentFilter,
41
- RegExpComponentFilter,
42
-} from 'src/types';
43
-
44
-export default function ComponentFiltersModalWrapper(_: {||}) {
45
- const store = useContext(StoreContext);
46
- const { profilerStore } = store;
47
-
48
- const { isModalShowing, setIsModalShowing } = useContext(
49
- ComponentFiltersModalContext
50
- );
51
-
52
- // Re-mounting a tree while profiling is in progress might break a lot of assumptions.
53
- // If necessary, we could support this- but it doesn't seem like a necessary use case.
54
- const isProfilingSubscription = useMemo(
55
- () => ({
56
- getCurrentValue: () => profilerStore.isProfiling,
57
- subscribe: (callback: Function) => {
58
- profilerStore.addListener('isProfiling', callback);
59
- return () => profilerStore.removeListener('isProfiling', callback);
60
- },
61
- }),
62
- [profilerStore]
63
- );
64
- const isProfiling = useSubscription<boolean, Store>(isProfilingSubscription);
65
- if (isProfiling && isModalShowing) {
66
- setIsModalShowing(false);
67
- }
68
-
69
- return isModalShowing ? (
70
- <ComponentFiltersModal
71
- store={store}
72
- setIsModalShowing={setIsModalShowing}
73
- />
74
- ) : null;
75
-}
76
-
77
-type Props = {|
78
- store: Store,
79
- setIsModalShowing: (value: boolean) => void,
80
-|};
81
-
82
-function ComponentFiltersModal({ store, setIsModalShowing }: Props) {
83
- const dismissModal = useCallback(() => setIsModalShowing(false), [
84
- setIsModalShowing,
85
- ]);
86
-
87
- const modalRef = useRef<HTMLDivElement | null>(null);
88
-
89
- useModalDismissSignal(modalRef, dismissModal);
90
-
91
- const {
92
- addFilter,
93
- changeFilterType,
94
- updateFilterValueElementType,
95
- updateFilterValueRegExp,
96
- componentFilters,
97
- removeFilter,
98
- saveFilters,
99
- toggleFilterIsEnabled,
100
- } = useComponentFilters();
101
-
102
- const saveAndClose = useCallback(() => {
103
- saveFilters();
104
- dismissModal();
105
- }, [dismissModal, saveFilters]);
106
-
107
- return (
108
- <div className={styles.Background}>
109
- <div className={styles.Modal} ref={modalRef}>
110
- <div className={styles.LeftRight}>
111
- <div className={styles.Left}>Hide components where...</div>
112
- <div className={styles.Right}>
113
- <Button onClick={addFilter}>
114
- <ButtonIcon className={styles.ButtonIcon} type="add" />
115
- Add filter
116
- </Button>
117
- </div>
118
- </div>
119
- <table className={styles.Table}>
120
- <tbody>
121
- {componentFilters.length === 0 && (
122
- <tr className={styles.TableRow}>
123
- <td className={styles.NoFiltersCell}>
124
- No filters have been added.
125
- </td>
126
- </tr>
127
- )}
128
- {componentFilters.map((componentFilter, index) => (
129
- <tr className={styles.TableRow} key={index}>
130
- <td className={styles.TableCell}>
131
- <Toggle
132
- className={
133
- componentFilter.isValid !== false
134
- ? ''
135
- : styles.InvalidRegExp
136
- }
137
- isChecked={componentFilter.isEnabled}
138
- onChange={isEnabled =>
139
- toggleFilterIsEnabled(componentFilter, isEnabled)
140
- }
141
- title={
142
- componentFilter.isValid === false
143
- ? 'Filter invalid'
144
- : componentFilter.isEnabled
145
- ? 'Filter enabled'
146
- : 'Filter disabled'
147
- }
148
- >
149
- <ToggleIcon
150
- isEnabled={componentFilter.isEnabled}
151
- isValid={
152
- componentFilter.isValid == null ||
153
- componentFilter.isValid === true
154
- }
155
- />
156
- </Toggle>
157
- </td>
158
- <td className={styles.TableCell}>
159
- <select
160
- className={styles.Select}
161
- value={componentFilter.type}
162
- onChange={({ currentTarget }) =>
163
- changeFilterType(
164
- componentFilter,
165
- ((parseInt(
166
- currentTarget.value,
167
- 10
168
- ): any): ComponentFilterType)
169
- )
170
- }
171
- >
172
- <option value={ComponentFilterLocation}>location</option>
173
- <option value={ComponentFilterDisplayName}>name</option>
174
- <option value={ComponentFilterElementType}>type</option>
175
- <option value={ComponentFilterHOC}>hoc</option>
176
- </select>
177
- </td>
178
- <td className={styles.TableCell}>
179
- {componentFilter.type === ComponentFilterElementType &&
180
- 'equals'}
181
- {(componentFilter.type === ComponentFilterLocation ||
182
- componentFilter.type === ComponentFilterDisplayName) &&
183
- 'matches'}
184
- </td>
185
- <td className={styles.TableCell}>
186
- {componentFilter.type === ComponentFilterElementType && (
187
- <select
188
- className={styles.Select}
189
- value={componentFilter.value}
190
- onChange={({ currentTarget }) =>
191
- updateFilterValueElementType(
192
- componentFilter,
193
- ((parseInt(
194
- currentTarget.value,
195
- 10
196
- ): any): ElementType)
197
- )
198
- }
199
- >
200
- <option value={ElementTypeClass}>class</option>
201
- <option value={ElementTypeContext}>context</option>
202
- <option value={ElementTypeEventTarget}>event</option>
203
- <option value={ElementTypeFunction}>function</option>
204
- <option value={ElementTypeForwardRef}>forward ref</option>
205
- <option value={ElementTypeHostComponent}>
206
- host (e.g. <div>)
207
- </option>
208
- <option value={ElementTypeMemo}>memo</option>
209
- <option value={ElementTypeOtherOrUnknown}>other</option>
210
- <option value={ElementTypeProfiler}>profiler</option>
211
- <option value={ElementTypeSuspense}>suspense</option>
212
- </select>
213
- )}
214
- {(componentFilter.type === ComponentFilterLocation ||
215
- componentFilter.type === ComponentFilterDisplayName) && (
216
- <input
217
- className={styles.Input}
218
- type="text"
219
- placeholder="Regular expression"
220
- onChange={({ currentTarget }) =>
221
- updateFilterValueRegExp(
222
- componentFilter,
223
- currentTarget.value
224
- )
225
- }
226
- value={componentFilter.value}
227
- />
228
- )}
229
- </td>
230
- <td className={styles.TableCell}>
231
- <Button
232
- onClick={() => removeFilter(index)}
233
- title="Delete filter"
234
- >
235
- <ButtonIcon type="delete" />
236
- </Button>
237
- </td>
238
- </tr>
239
- ))}
240
- </tbody>
241
- </table>
242
- <div className={styles.LeftRight}>
243
- <div className={styles.Right}>
244
- <Button className={styles.CancelButton} onClick={dismissModal}>
245
- Cancel
246
- </Button>
247
- <Button onClick={saveAndClose}>Save Changes</Button>
248
- </div>
249
- </div>
250
- </div>
251
- </div>
252
- );
253
-}
254
-
255
-type ToggleIconProps = {|
256
- isEnabled: boolean,
257
- isValid: boolean,
258
-|};
259
-function ToggleIcon({ isEnabled, isValid }: ToggleIconProps) {
260
- let className;
261
- if (isValid) {
262
- className = isEnabled ? styles.ToggleOn : styles.ToggleOff;
263
- } else {
264
- className = isEnabled ? styles.ToggleOnInvalid : styles.ToggleOffInvalid;
265
- }
266
- return (
267
- <div className={className}>
268
- <div
269
- className={isEnabled ? styles.ToggleInsideOn : styles.ToggleInsideOff}
270
- />
271
- </div>
272
- );
273
-}
274
-
275
-function useComponentFilters() {
276
- const store = useContext(StoreContext);
277
-
278
- const [componentFilters, setComponentFilters] = useState<
279
- Array<ComponentFilter>
280
- >(() => [...store.componentFilters]);
281
-
282
- const addFilter = useCallback(() => {
283
- setComponentFilters(componentFilters => {
284
- return [
285
- ...componentFilters,
286
- {
287
- type: ComponentFilterElementType,
288
- value: ElementTypeHostComponent,
289
- isEnabled: true,
290
- },
291
- ];
292
- });
293
- }, []);
294
-
295
- const changeFilterType = useCallback(
296
- (componentFilter: ComponentFilter, type: ComponentFilterType) => {
297
- setComponentFilters(componentFilters => {
298
- const cloned: Array<ComponentFilter> = [...componentFilters];
299
- const index = componentFilters.indexOf(componentFilter);
300
- if (index >= 0) {
301
- if (type === ComponentFilterElementType) {
302
- cloned[index] = {
303
- type: ComponentFilterElementType,
304
- isEnabled: componentFilter.isEnabled,
305
- value: ElementTypeHostComponent,
306
- };
307
- } else if (type === ComponentFilterDisplayName) {
308
- cloned[index] = {
309
- type: ComponentFilterDisplayName,
310
- isEnabled: componentFilter.isEnabled,
311
- isValid: true,
312
- value: '',
313
- };
314
- } else if (type === ComponentFilterLocation) {
315
- cloned[index] = {
316
- type: ComponentFilterLocation,
317
- isEnabled: componentFilter.isEnabled,
318
- isValid: true,
319
- value: '',
320
- };
321
- } else if (type === ComponentFilterHOC) {
322
- cloned[index] = {
323
- type: ComponentFilterHOC,
324
- isEnabled: componentFilter.isEnabled,
325
- isValid: true,
326
- };
327
- }
328
- }
329
- return cloned;
330
- });
331
- },
332
- []
333
- );
334
-
335
- const updateFilterValueElementType = useCallback(
336
- (componentFilter: ComponentFilter, value: ElementType) => {
337
- if (componentFilter.type !== ComponentFilterElementType) {
338
- throw Error('Invalid value for element type filter');
339
- }
340
-
341
- setComponentFilters(componentFilters => {
342
- const cloned: Array<ComponentFilter> = [...componentFilters];
343
- if (componentFilter.type === ComponentFilterElementType) {
344
- const index = componentFilters.indexOf(componentFilter);
345
- if (index >= 0) {
346
- cloned[index] = {
347
- ...componentFilter,
348
- value,
349
- };
350
- }
351
- }
352
- return cloned;
353
- });
354
- },
355
- []
356
- );
357
-
358
- const updateFilterValueRegExp = useCallback(
359
- (componentFilter: ComponentFilter, value: string) => {
360
- if (componentFilter.type === ComponentFilterElementType) {
361
- throw Error('Invalid value for element type filter');
362
- }
363
-
364
- setComponentFilters(componentFilters => {
365
- const cloned: Array<ComponentFilter> = [...componentFilters];
366
- if (
367
- componentFilter.type === ComponentFilterDisplayName ||
368
- componentFilter.type === ComponentFilterLocation
369
- ) {
370
- const index = componentFilters.indexOf(componentFilter);
371
- if (index >= 0) {
372
- let isValid = true;
373
- try {
374
- new RegExp(value);
375
- } catch (error) {
376
- isValid = false;
377
- }
378
- cloned[index] = {
379
- ...componentFilter,
380
- isValid,
381
- value,
382
- };
383
- }
384
- }
385
- return cloned;
386
- });
387
- },
388
- []
389
- );
390
-
391
- const removeFilter = useCallback((index: number) => {
392
- setComponentFilters(componentFilters => {
393
- const cloned: Array<ComponentFilter> = [...componentFilters];
394
- cloned.splice(index, 1);
395
- return cloned;
396
- });
397
- }, []);
398
-
399
- const saveFilters = useCallback(() => {
400
- store.componentFilters = [...componentFilters];
401
- }, [componentFilters, store]);
402
-
403
- const toggleFilterIsEnabled = useCallback(
404
- (componentFilter: ComponentFilter, isEnabled: boolean) => {
405
- setComponentFilters(componentFilters => {
406
- const cloned: Array<ComponentFilter> = [...componentFilters];
407
- const index = componentFilters.indexOf(componentFilter);
408
- if (index >= 0) {
409
- if (componentFilter.type === ComponentFilterElementType) {
410
- cloned[index] = {
411
- ...((cloned[index]: any): ElementTypeComponentFilter),
412
- isEnabled,
413
- };
414
- } else if (
415
- componentFilter.type === ComponentFilterDisplayName ||
416
- componentFilter.type === ComponentFilterLocation
417
- ) {
418
- cloned[index] = {
419
- ...((cloned[index]: any): RegExpComponentFilter),
420
- isEnabled,
421
- };
422
- } else if (componentFilter.type === ComponentFilterHOC) {
423
- cloned[index] = {
424
- ...((cloned[index]: any): BooleanComponentFilter),
425
- isEnabled,
426
- };
427
- }
428
- }
429
- return cloned;
430
- });
431
- },
432
- []
433
- );
434
-
435
- return {
436
- addFilter,
437
- changeFilterType,
438
- componentFilters,
439
- removeFilter,
440
- saveFilters,
441
- toggleFilterIsEnabled,
442
- updateFilterValueElementType,
443
- updateFilterValueRegExp,
444
- };
445
-}
src/devtools/views/Components/ComponentFiltersModalContext.js
deleted
-37
@@ -1,37 +0,0 @@
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/Components.js
+19
-14
@@ -7,27 +7,32 @@ import { InspectedElementContextController } from './InspectedElementContext';
7
import { OwnersListContextController } from './OwnersListContext';
8
import portaledContent from '../portaledContent';
9
import { ModalDialog } from '../ModalDialog';
10
+import SettingsModal from 'src/devtools/views/Settings/SettingsModal';
11
+import { SettingsModalContextController } from 'src/devtools/views/Settings/SettingsModalContext';
12
13
import styles from './Components.css';
14
15
function Components(_: {||}) {
16
// TODO Flex wrappers below should be user resizable.
17
return (
16
- <OwnersListContextController>
17
- <InspectedElementContextController>
18
- <div className={styles.Components}>
19
- <div className={styles.TreeWrapper}>
20
- <Tree />
18
+ <SettingsModalContextController>
19
+ <OwnersListContextController>
20
+ <InspectedElementContextController>
21
+ <div className={styles.Components}>
22
+ <div className={styles.TreeWrapper}>
23
+ <Tree />
24
+ </div>
25
+ <div className={styles.SelectedElementWrapper}>
26
+ <Suspense fallback={<Loading />}>
27
+ <SelectedElement />
28
+ </Suspense>
29
+ </div>
30
+ <ModalDialog />
31
+ <SettingsModal defaultTabID="components" />
32
</div>
22
- <div className={styles.SelectedElementWrapper}>
23
- <Suspense fallback={<Loading />}>
24
- <SelectedElement />
25
- </Suspense>
26
- </div>
27
- <ModalDialog />
28
- </div>
29
- </InspectedElementContextController>
30
- </OwnersListContextController>
33
+ </InspectedElementContextController>
34
+ </OwnersListContextController>
35
+ </SettingsModalContextController>
36
);
37
}
38
src/devtools/views/Components/ToggleComponentFiltersModalButton.css
deleted
-8
@@ -1,8 +0,0 @@
1
-.Wrapper {
2
- display: flex;
3
-}
4
-
5
-.Label {
6
- font-size: var(--font-size-sans-small);
7
- font-family: var(--font-family-sans);
8
-}
src/devtools/views/Components/ToggleComponentFiltersModalButton.js
deleted
-80
@@ -1,80 +0,0 @@
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
-import styles from './ToggleComponentFiltersModalButton.css';
12
-
13
-import type { ComponentFilter } from 'src/types';
14
-
15
-export default function ToggleComponentFiltersModalButton() {
16
- const store = useContext(StoreContext);
17
- const { profilerStore } = store;
18
-
19
- const { isModalShowing, setIsModalShowing } = useContext(
20
- ComponentFiltersModalContext
21
- );
22
-
23
- // Re-mounting a tree while profiling is in progress might break a lot of assumptions.
24
- // If necessary, we could support this- but it doesn't seem like a necessary use case.
25
- const isProfilingSubscription = useMemo(
26
- () => ({
27
- getCurrentValue: () => profilerStore.isProfiling,
28
- subscribe: (callback: Function) => {
29
- profilerStore.addListener('isProfiling', callback);
30
- return () => profilerStore.removeListener('isProfiling', callback);
31
- },
32
- }),
33
- [profilerStore]
34
- );
35
- const isProfiling = useSubscription<boolean, Store>(isProfilingSubscription);
36
-
37
- const componentFiltersSubscription = useMemo(
38
- () => ({
39
- getCurrentValue: () => store.componentFilters,
40
- subscribe: (callback: Function) => {
41
- store.addListener('componentFilters', callback);
42
- return () => store.removeListener('componentFilters', callback);
43
- },
44
- }),
45
- [store]
46
- );
47
- const componentFilters = useSubscription<Array<ComponentFilter>, Store>(
48
- componentFiltersSubscription
49
- );
50
-
51
- const enabledCount = useMemo(
52
- () =>
53
- componentFilters.reduce(
54
- (count, componentFilter) =>
55
- componentFilter.isEnabled ? count + 1 : count,
56
- 0
57
- ),
58
- [componentFilters]
59
- );
60
-
61
- return (
62
- <Toggle
63
- isChecked={isModalShowing}
64
- isDisabled={isProfiling}
65
- onChange={setIsModalShowing}
66
- title={
67
- enabledCount > 0
68
- ? `Filter preferences (${enabledCount} enabled)`
69
- : 'Filter preferences'
70
- }
71
- >
72
- <div className={styles.Wrapper}>
73
- <ButtonIcon type="filter" />
74
- {enabledCount > 0 && (
75
- <span className={styles.Label}>{enabledCount}</span>
76
- )}
77
- </div>
78
- </Toggle>
79
- );
80
-}
src/devtools/views/Components/Tree.js
+40
-45
@@ -19,9 +19,7 @@ import ElementView from './Element';
19
import InspectHostNodesToggle from './InspectHostNodesToggle';
20
import OwnersStack from './OwnersStack';
21
import SearchInput from './SearchInput';
22
-import { ComponentFiltersModalContextController } from './ComponentFiltersModalContext';
23
-import ToggleComponentFiltersModalButton from './ToggleComponentFiltersModalButton';
24
-import ComponentFiltersModal from './ComponentFiltersModal';
22
+import SettingsModalContextToggle from 'src/devtools/views/Settings/SettingsModalContextToggle';
23
import SelectedTreeHighlight from './SelectedTreeHighlight';
24
import TreeFocusedContext from './TreeFocusedContext';
25
@@ -284,49 +282,46 @@ export default function Tree(props: Props) {
282
283
return (
284
<TreeFocusedContext.Provider value={treeFocused}>
287
- <ComponentFiltersModalContextController>
288
- <div className={styles.Tree} ref={treeRef}>
289
- <div className={styles.SearchInput}>
290
- <InspectHostNodesToggle />
291
- <div className={styles.VRule} />
292
- <Suspense fallback={<Loading />}>
293
- {ownerID !== null ? <OwnersStack /> : <SearchInput />}
294
- </Suspense>
295
- <div className={styles.VRule} />
296
- <ToggleComponentFiltersModalButton />
297
- </div>
298
- <div
299
- className={styles.AutoSizerWrapper}
300
- onBlur={handleBlur}
301
- onFocus={handleFocus}
302
- onKeyPress={handleKeyPress}
303
- onMouseMove={handleMouseMove}
304
- onMouseLeave={handleMouseLeave}
305
- ref={focusTargetRef}
306
- tabIndex={0}
307
- >
308
- <AutoSizer>
309
- {({ height, width }) => (
310
- // $FlowFixMe https://github.com/facebook/flow/issues/7341
311
- <FixedSizeList
312
- className={styles.List}
313
- height={height}
314
- innerElementType={InnerElementType}
315
- itemCount={numElements}
316
- itemData={itemData}
317
- itemKey={itemKey}
318
- itemSize={lineHeight}
319
- ref={listRef}
320
- width={width}
321
- >
322
- {ElementView}
323
- </FixedSizeList>
324
- )}
325
- </AutoSizer>
326
- </div>
327
- <ComponentFiltersModal />
285
+ <div className={styles.Tree} ref={treeRef}>
286
+ <div className={styles.SearchInput}>
287
+ <InspectHostNodesToggle />
288
+ <div className={styles.VRule} />
289
+ <Suspense fallback={<Loading />}>
290
+ {ownerID !== null ? <OwnersStack /> : <SearchInput />}
291
+ </Suspense>
292
+ <div className={styles.VRule} />
293
+ <SettingsModalContextToggle />
294
</div>
329
- </ComponentFiltersModalContextController>
295
+ <div
296
+ className={styles.AutoSizerWrapper}
297
+ onBlur={handleBlur}
298
+ onFocus={handleFocus}
299
+ onKeyPress={handleKeyPress}
300
+ onMouseMove={handleMouseMove}
301
+ onMouseLeave={handleMouseLeave}
302
+ ref={focusTargetRef}
303
+ tabIndex={0}
304
+ >
305
+ <AutoSizer>
306
+ {({ height, width }) => (
307
+ // $FlowFixMe https://github.com/facebook/flow/issues/7341
308
+ <FixedSizeList
309
+ className={styles.List}
310
+ height={height}
311
+ innerElementType={InnerElementType}
312
+ itemCount={numElements}
313
+ itemData={itemData}
314
+ itemKey={itemKey}
315
+ itemSize={lineHeight}
316
+ ref={listRef}
317
+ width={width}
318
+ >
319
+ {ElementView}
320
+ </FixedSizeList>
321
+ )}
322
+ </AutoSizer>
323
+ </div>
324
+ </div>
325
</TreeFocusedContext.Provider>
326
);
327
}
src/devtools/views/DevTools.js
+1
-14
@@ -11,7 +11,6 @@ import Store from '../store';
11
import { BridgeContext, StoreContext } from './context';
12
import Components from './Components/Components';
13
import Profiler from './Profiler/Profiler';
14
-import Settings from './Settings/Settings';
14
import TabBar from './TabBar';
15
import { SettingsContextController } from './Settings/SettingsContext';
16
import { TreeContextController } from './Components/TreeContext';
@@ -63,14 +62,8 @@ const profilerTab = {
62
label: 'Profiler',
63
title: 'React Profiler',
64
};
66
-const settingsTab = {
67
- id: ('settings': TabID),
68
- icon: 'settings',
69
- label: 'Settings',
70
- title: 'React Settings',
71
-};
65
73
-const tabs = [componentsTab, profilerTab, settingsTab];
66
+const tabs = [componentsTab, profilerTab];
67
68
export default function DevTools({
69
bridge,
@@ -132,12 +125,6 @@ export default function DevTools({
125
>
126
<Profiler portalContainer={profilerPortalContainer} />
127
</div>
135
- <div
136
- className={styles.TabContent}
137
- hidden={tab !== 'settings'}
138
- >
139
- <Settings portalContainer={settingsPortalContainer} />
140
- </div>
128
</div>
129
</ProfilerContextController>
130
</TreeContextController>
src/devtools/views/Profiler/CommitFilterModal.css
deleted
-31
@@ -1,31 +0,0 @@
1
-.Background {
2
- position: absolute;
3
- width: 100%;
4
- height: 100%;
5
- background-color: var(--color-modal-background);
6
-}
7
-
8
-.Modal {
9
- position: absolute;
10
- top: 5px;
11
- right: 5px;
12
- display: inline-block;
13
- background-color: var(--color-background);
14
- padding: 1rem;
15
- border: 1px solid var(--color-border);
16
- border-radius: 0.25rem;
17
-}
18
-
19
-.Input {
20
- width: 40px;
21
- background: none;
22
- border: 1px solid transparent;
23
- color: var(--color-attribute-editable-value);
24
- border-radius: 0.125rem;
25
- font-family: var(--font-family-monospace);
26
- font-size: var(--font-size-monospace-normal);
27
-}
28
-.Input:focus {
29
- background-color: var(--color-button-background-focus);
30
- outline: none;
31
-}
src/devtools/views/Profiler/CommitFilterModal.js
deleted
-89
@@ -1,89 +0,0 @@
1
-// @flow
2
-
3
-import React, { useCallback, useContext, useEffect, useRef } from 'react';
4
-import { CommitFilterModalContext } from './CommitFilterModalContext';
5
-import { ProfilerContext } from './ProfilerContext';
6
-import { useModalDismissSignal } from '../hooks';
7
-
8
-import styles from './CommitFilterModal.css';
9
-
10
-export default function FilterModal(_: {||}) {
11
- const { isModalShowing } = useContext(CommitFilterModalContext);
12
-
13
- if (!isModalShowing) {
14
- return null;
15
- }
16
-
17
- return <FilterModalImpl />;
18
-}
19
-
20
-function FilterModalImpl(_: {||}) {
21
- const {
22
- isCommitFilterEnabled,
23
- minCommitDuration,
24
- setIsCommitFilterEnabled,
25
- setMinCommitDuration,
26
- } = useContext(ProfilerContext);
27
-
28
- const { setIsModalShowing } = useContext(CommitFilterModalContext);
29
- const dismissModal = useCallback(() => setIsModalShowing(false), [
30
- setIsModalShowing,
31
- ]);
32
-
33
- const handleNumberChange = useCallback(
34
- (event: SyntheticEvent<HTMLInputElement>) => {
35
- const newValue = parseFloat(event.currentTarget.value);
36
- setMinCommitDuration(
37
- Number.isNaN(newValue) || newValue <= 0 ? 0 : newValue
38
- );
39
- },
40
- [setMinCommitDuration]
41
- );
42
-
43
- const handleEnabledChange = useCallback(
44
- (event: SyntheticEvent<HTMLInputElement>) => {
45
- const checked = event.currentTarget.checked;
46
- setIsCommitFilterEnabled(checked);
47
- if (checked) {
48
- if (inputRef.current !== null) {
49
- inputRef.current.focus();
50
- }
51
- }
52
- },
53
- [setIsCommitFilterEnabled]
54
- );
55
-
56
- const inputRef = useRef<HTMLInputElement | null>(null);
57
- const modalRef = useRef<HTMLDivElement | null>(null);
58
-
59
- useModalDismissSignal(modalRef, dismissModal);
60
-
61
- useEffect(() => {
62
- if (inputRef.current !== null) {
63
- inputRef.current.focus();
64
- }
65
- }, [inputRef]);
66
-
67
- return (
68
- <div className={styles.Background}>
69
- <div className={styles.Modal} ref={modalRef}>
70
- <label>
71
- <input
72
- checked={isCommitFilterEnabled}
73
- onChange={handleEnabledChange}
74
- type="checkbox"
75
- />{' '}
76
- Hide commits below
77
- </label>{' '}
78
- <input
79
- className={styles.Input}
80
- onChange={handleNumberChange}
81
- ref={inputRef}
82
- type="number"
83
- value={minCommitDuration}
84
- />{' '}
85
- (ms)
86
- </div>
87
- </div>
88
- );
89
-}
src/devtools/views/Profiler/CommitFilterModalContext.js
deleted
-35
@@ -1,35 +0,0 @@
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 CommitFilterModalContext = createContext<Context>(((null: any): Context));
11
-CommitFilterModalContext.displayName = 'CommitFilterModalContext';
12
-
13
-type Props = {|
14
- children: React$Node,
15
-|};
16
-
17
-function CommitFilterModalContextController({ children }: Props) {
18
- const [isModalShowing, setIsModalShowing] = useState(false);
19
-
20
- const value = useMemo(
21
- () => ({
22
- isModalShowing,
23
- setIsModalShowing,
24
- }),
25
- [isModalShowing]
26
- );
27
-
28
- return (
29
- <CommitFilterModalContext.Provider value={value}>
30
- {children}
31
- </CommitFilterModalContext.Provider>
32
- );
33
-}
34
-
35
-export { CommitFilterModalContext, CommitFilterModalContextController };
src/devtools/views/Profiler/NoCommitData.js
+1
-2
@@ -1,7 +1,6 @@
1
// @flow
2
3
import React from 'react';
4
-import ToggleCommitFilterModalButton from './ToggleCommitFilterModalButton';
4
5
import styles from './NoCommitData.css';
6
@@ -12,7 +11,7 @@ export default function NoCommitData(_: {||}) {
11
There is no data matching the current filter criteria.
12
</div>
13
<div className={styles.FilterMessage}>
15
- Try adjusting the commit filter <ToggleCommitFilterModalButton />
14
+ Try adjusting the commit filter in Profiler settings.
15
</div>
16
</div>
17
);
src/devtools/views/Profiler/Profiler.css
+2
-1
@@ -1,6 +1,7 @@
1
.Profiler {
2
width: 100%;
3
height: 100%;
4
+ position: relative;
5
display: flex;
6
flex-direction: row;
7
align-items: stretch;
@@ -57,7 +58,7 @@
58
59
.Toolbar {
60
height: 2.25rem;
60
- padding: 0 0.5rem;
61
+ padding: 0 0.25rem;
62
flex: 0 0 auto;
63
display: flex;
64
align-items: center;
src/devtools/views/Profiler/Profiler.js
+7
-7
@@ -1,14 +1,12 @@
1
// @flow
2
3
import React, { Fragment, useContext } from 'react';
4
-import { CommitFilterModalContextController } from './CommitFilterModalContext';
4
import { ModalDialog } from '../ModalDialog';
5
import { ProfilerContext } from './ProfilerContext';
6
import TabBar from '../TabBar';
7
import ClearProfilingDataButton from './ClearProfilingDataButton';
8
import CommitFlamegraph from './CommitFlamegraph';
9
import CommitRanked from './CommitRanked';
11
-import CommitFilterModal from './CommitFilterModal';
10
import Interactions from './Interactions';
11
import RootSelector from './RootSelector';
12
import RecordToggle from './RecordToggle';
@@ -18,7 +16,9 @@ import SnapshotSelector from './SnapshotSelector';
16
import SidebarCommitInfo from './SidebarCommitInfo';
17
import SidebarInteractions from './SidebarInteractions';
18
import SidebarSelectedFiberInfo from './SidebarSelectedFiberInfo';
21
-import ToggleCommitFilterModalButton from './ToggleCommitFilterModalButton';
19
+import SettingsModal from 'src/devtools/views/Settings/SettingsModal';
20
+import SettingsModalContextToggle from 'src/devtools/views/Settings/SettingsModalContextToggle';
21
+import { SettingsModalContextController } from 'src/devtools/views/Settings/SettingsModalContext';
22
import portaledContent from '../portaledContent';
23
24
import styles from './Profiler.css';
@@ -79,7 +79,7 @@ function Profiler(_: {||}) {
79
}
80
81
return (
82
- <CommitFilterModalContextController>
82
+ <SettingsModalContextController>
83
<div className={styles.Profiler}>
84
<div className={styles.LeftColumn}>
85
<div className={styles.Toolbar}>
@@ -97,7 +97,7 @@ function Profiler(_: {||}) {
97
/>
98
<RootSelector />
99
<div className={styles.Spacer} />
100
- <ToggleCommitFilterModalButton />
100
+ <SettingsModalContextToggle />
101
{didRecordCommits && (
102
<Fragment>
103
<div className={styles.VRule} />
@@ -107,13 +107,13 @@ function Profiler(_: {||}) {
107
</div>
108
<div className={styles.Content}>
109
{view}
110
- <CommitFilterModal />
110
<ModalDialog />
111
</div>
112
</div>
113
<div className={styles.RightColumn}>{sidebar}</div>
114
+ <SettingsModal defaultTabID="profiler" />
115
</div>
116
- </CommitFilterModalContextController>
116
+ </SettingsModalContextController>
117
);
118
}
119
src/devtools/views/Profiler/ToggleCommitFilterModalButton.js
deleted
-20
@@ -1,20 +0,0 @@
1
-// @flow
2
-
3
-import React, { useCallback, useContext } from 'react';
4
-import { CommitFilterModalContext } from './CommitFilterModalContext';
5
-import Button from '../Button';
6
-import ButtonIcon from '../ButtonIcon';
7
-
8
-export default function ToggleCommitFilterModalButton() {
9
- const { setIsModalShowing } = useContext(CommitFilterModalContext);
10
-
11
- const showFilterModal = useCallback(() => setIsModalShowing(true), [
12
- setIsModalShowing,
13
- ]);
14
-
15
- return (
16
- <Button onClick={showFilterModal} title="Filter commits by duration">
17
- <ButtonIcon type="filter" />
18
- </Button>
19
- );
20
-}
src/devtools/views/Settings/ComponentsSettings.js
new
+417
@@ -0,0 +1,417 @@
1
+// @flow
2
+
3
+import React, {
4
+ useCallback,
5
+ useContext,
6
+ useEffect,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+} from 'react';
11
+import { useSubscription } from '../hooks';
12
+import { StoreContext } from '../context';
13
+import Store from 'src/devtools/store';
14
+import Button from '../Button';
15
+import ButtonIcon from '../ButtonIcon';
16
+import Toggle from '../Toggle';
17
+import { shallowDiffers } from 'src/utils';
18
+import {
19
+ ComponentFilterDisplayName,
20
+ ComponentFilterElementType,
21
+ ComponentFilterHOC,
22
+ ComponentFilterLocation,
23
+ ElementTypeClass,
24
+ ElementTypeContext,
25
+ ElementTypeEventTarget,
26
+ ElementTypeFunction,
27
+ ElementTypeForwardRef,
28
+ ElementTypeHostComponent,
29
+ ElementTypeMemo,
30
+ ElementTypeOtherOrUnknown,
31
+ ElementTypeProfiler,
32
+ ElementTypeSuspense,
33
+} from 'src/types';
34
+
35
+import styles from './SettingsShared.css';
36
+
37
+import type {
38
+ BooleanComponentFilter,
39
+ ComponentFilter,
40
+ ComponentFilterType,
41
+ ElementType,
42
+ ElementTypeComponentFilter,
43
+ RegExpComponentFilter,
44
+} from 'src/types';
45
+
46
+export default function ComponentsSettings(_: {||}) {
47
+ const store = useContext(StoreContext);
48
+
49
+ const collapseNodesByDefaultSubscription = useMemo(
50
+ () => ({
51
+ getCurrentValue: () => store.collapseNodesByDefault,
52
+ subscribe: (callback: Function) => {
53
+ store.addListener('collapseNodesByDefault', callback);
54
+ return () => store.removeListener('collapseNodesByDefault', callback);
55
+ },
56
+ }),
57
+ [store]
58
+ );
59
+ const collapseNodesByDefault = useSubscription<boolean, Store>(
60
+ collapseNodesByDefaultSubscription
61
+ );
62
+
63
+ const updateCollapseNodesByDefault = useCallback(
64
+ ({ currentTarget }) => {
65
+ store.collapseNodesByDefault = currentTarget.checked;
66
+ },
67
+ [store]
68
+ );
69
+
70
+ const [componentFilters, setComponentFilters] = useState<
71
+ Array<ComponentFilter>
72
+ >(() => [...store.componentFilters]);
73
+
74
+ const addFilter = useCallback(() => {
75
+ setComponentFilters(componentFilters => {
76
+ return [
77
+ ...componentFilters,
78
+ {
79
+ type: ComponentFilterElementType,
80
+ value: ElementTypeHostComponent,
81
+ isEnabled: true,
82
+ },
83
+ ];
84
+ });
85
+ }, []);
86
+
87
+ const changeFilterType = useCallback(
88
+ (componentFilter: ComponentFilter, type: ComponentFilterType) => {
89
+ setComponentFilters(componentFilters => {
90
+ const cloned: Array<ComponentFilter> = [...componentFilters];
91
+ const index = componentFilters.indexOf(componentFilter);
92
+ if (index >= 0) {
93
+ if (type === ComponentFilterElementType) {
94
+ cloned[index] = {
95
+ type: ComponentFilterElementType,
96
+ isEnabled: componentFilter.isEnabled,
97
+ value: ElementTypeHostComponent,
98
+ };
99
+ } else if (type === ComponentFilterDisplayName) {
100
+ cloned[index] = {
101
+ type: ComponentFilterDisplayName,
102
+ isEnabled: componentFilter.isEnabled,
103
+ isValid: true,
104
+ value: '',
105
+ };
106
+ } else if (type === ComponentFilterLocation) {
107
+ cloned[index] = {
108
+ type: ComponentFilterLocation,
109
+ isEnabled: componentFilter.isEnabled,
110
+ isValid: true,
111
+ value: '',
112
+ };
113
+ } else if (type === ComponentFilterHOC) {
114
+ cloned[index] = {
115
+ type: ComponentFilterHOC,
116
+ isEnabled: componentFilter.isEnabled,
117
+ isValid: true,
118
+ };
119
+ }
120
+ }
121
+ return cloned;
122
+ });
123
+ },
124
+ []
125
+ );
126
+
127
+ const updateFilterValueElementType = useCallback(
128
+ (componentFilter: ComponentFilter, value: ElementType) => {
129
+ if (componentFilter.type !== ComponentFilterElementType) {
130
+ throw Error('Invalid value for element type filter');
131
+ }
132
+
133
+ setComponentFilters(componentFilters => {
134
+ const cloned: Array<ComponentFilter> = [...componentFilters];
135
+ if (componentFilter.type === ComponentFilterElementType) {
136
+ const index = componentFilters.indexOf(componentFilter);
137
+ if (index >= 0) {
138
+ cloned[index] = {
139
+ ...componentFilter,
140
+ value,
141
+ };
142
+ }
143
+ }
144
+ return cloned;
145
+ });
146
+ },
147
+ []
148
+ );
149
+
150
+ const updateFilterValueRegExp = useCallback(
151
+ (componentFilter: ComponentFilter, value: string) => {
152
+ if (componentFilter.type === ComponentFilterElementType) {
153
+ throw Error('Invalid value for element type filter');
154
+ }
155
+
156
+ setComponentFilters(componentFilters => {
157
+ const cloned: Array<ComponentFilter> = [...componentFilters];
158
+ if (
159
+ componentFilter.type === ComponentFilterDisplayName ||
160
+ componentFilter.type === ComponentFilterLocation
161
+ ) {
162
+ const index = componentFilters.indexOf(componentFilter);
163
+ if (index >= 0) {
164
+ let isValid = true;
165
+ try {
166
+ new RegExp(value);
167
+ } catch (error) {
168
+ isValid = false;
169
+ }
170
+ cloned[index] = {
171
+ ...componentFilter,
172
+ isValid,
173
+ value,
174
+ };
175
+ }
176
+ }
177
+ return cloned;
178
+ });
179
+ },
180
+ []
181
+ );
182
+
183
+ const removeFilter = useCallback((index: number) => {
184
+ setComponentFilters(componentFilters => {
185
+ const cloned: Array<ComponentFilter> = [...componentFilters];
186
+ cloned.splice(index, 1);
187
+ return cloned;
188
+ });
189
+ }, []);
190
+
191
+ const toggleFilterIsEnabled = useCallback(
192
+ (componentFilter: ComponentFilter, isEnabled: boolean) => {
193
+ setComponentFilters(componentFilters => {
194
+ const cloned: Array<ComponentFilter> = [...componentFilters];
195
+ const index = componentFilters.indexOf(componentFilter);
196
+ if (index >= 0) {
197
+ if (componentFilter.type === ComponentFilterElementType) {
198
+ cloned[index] = {
199
+ ...((cloned[index]: any): ElementTypeComponentFilter),
200
+ isEnabled,
201
+ };
202
+ } else if (
203
+ componentFilter.type === ComponentFilterDisplayName ||
204
+ componentFilter.type === ComponentFilterLocation
205
+ ) {
206
+ cloned[index] = {
207
+ ...((cloned[index]: any): RegExpComponentFilter),
208
+ isEnabled,
209
+ };
210
+ } else if (componentFilter.type === ComponentFilterHOC) {
211
+ cloned[index] = {
212
+ ...((cloned[index]: any): BooleanComponentFilter),
213
+ isEnabled,
214
+ };
215
+ }
216
+ }
217
+ return cloned;
218
+ });
219
+ },
220
+ []
221
+ );
222
+
223
+ // Filter updates are expensive to apply (since they impact the entire tree).
224
+ // Only apply them on unmount, and only if they've actually changed.
225
+ const componentFiltersRef = useRef<Array<ComponentFilter>>(componentFilters);
226
+ useEffect(() => {
227
+ console.log('update componentFilters ref:', componentFilters);
228
+ componentFiltersRef.current = componentFilters;
229
+ return () => {};
230
+ }, [componentFilters]);
231
+ useEffect(
232
+ () => () => {
233
+ const prevComponentFilters = store.componentFilters;
234
+ const nextComponentFilters = componentFiltersRef.current;
235
+
236
+ let haveFiltersChanged =
237
+ prevComponentFilters.length !== nextComponentFilters.length;
238
+ if (!haveFiltersChanged) {
239
+ for (let i = 0; i < nextComponentFilters.length; i++) {
240
+ if (
241
+ shallowDiffers(prevComponentFilters[i], nextComponentFilters[i])
242
+ ) {
243
+ haveFiltersChanged = true;
244
+ break;
245
+ }
246
+ }
247
+ }
248
+
249
+ console.log('haveFiltersChanged?', haveFiltersChanged);
250
+ if (haveFiltersChanged) {
251
+ store.componentFilters = [...nextComponentFilters];
252
+ }
253
+ },
254
+ [store]
255
+ );
256
+
257
+ return (
258
+ <div className={styles.Settings}>
259
+ <label className={styles.Setting}>
260
+ <input
261
+ type="checkbox"
262
+ checked={collapseNodesByDefault}
263
+ onChange={updateCollapseNodesByDefault}
264
+ />{' '}
265
+ Collapse newly added components by default
266
+ </label>
267
+
268
+ <div className={styles.Header}>Hide components where...</div>
269
+
270
+ <table className={styles.Table}>
271
+ <tbody>
272
+ {componentFilters.length === 0 && (
273
+ <tr className={styles.TableRow}>
274
+ <td className={styles.NoFiltersCell}>
275
+ No filters have been added.
276
+ </td>
277
+ </tr>
278
+ )}
279
+ {componentFilters.map((componentFilter, index) => (
280
+ <tr className={styles.TableRow} key={index}>
281
+ <td className={styles.TableCell}>
282
+ <Toggle
283
+ className={
284
+ componentFilter.isValid !== false
285
+ ? ''
286
+ : styles.InvalidRegExp
287
+ }
288
+ isChecked={componentFilter.isEnabled}
289
+ onChange={isEnabled =>
290
+ toggleFilterIsEnabled(componentFilter, isEnabled)
291
+ }
292
+ title={
293
+ componentFilter.isValid === false
294
+ ? 'Filter invalid'
295
+ : componentFilter.isEnabled
296
+ ? 'Filter enabled'
297
+ : 'Filter disabled'
298
+ }
299
+ >
300
+ <ToggleIcon
301
+ isEnabled={componentFilter.isEnabled}
302
+ isValid={
303
+ componentFilter.isValid == null ||
304
+ componentFilter.isValid === true
305
+ }
306
+ />
307
+ </Toggle>
308
+ </td>
309
+ <td className={styles.TableCell}>
310
+ <select
311
+ className={styles.Select}
312
+ value={componentFilter.type}
313
+ onChange={({ currentTarget }) =>
314
+ changeFilterType(
315
+ componentFilter,
316
+ ((parseInt(
317
+ currentTarget.value,
318
+ 10
319
+ ): any): ComponentFilterType)
320
+ )
321
+ }
322
+ >
323
+ <option value={ComponentFilterLocation}>location</option>
324
+ <option value={ComponentFilterDisplayName}>name</option>
325
+ <option value={ComponentFilterElementType}>type</option>
326
+ <option value={ComponentFilterHOC}>hoc</option>
327
+ </select>
328
+ </td>
329
+ <td className={styles.TableCell}>
330
+ {componentFilter.type === ComponentFilterElementType &&
331
+ 'equals'}
332
+ {(componentFilter.type === ComponentFilterLocation ||
333
+ componentFilter.type === ComponentFilterDisplayName) &&
334
+ 'matches'}
335
+ </td>
336
+ <td className={styles.TableCell}>
337
+ {componentFilter.type === ComponentFilterElementType && (
338
+ <select
339
+ className={styles.Select}
340
+ value={componentFilter.value}
341
+ onChange={({ currentTarget }) =>
342
+ updateFilterValueElementType(
343
+ componentFilter,
344
+ ((parseInt(currentTarget.value, 10): any): ElementType)
345
+ )
346
+ }
347
+ >
348
+ <option value={ElementTypeClass}>class</option>
349
+ <option value={ElementTypeContext}>context</option>
350
+ <option value={ElementTypeEventTarget}>event</option>
351
+ <option value={ElementTypeFunction}>function</option>
352
+ <option value={ElementTypeForwardRef}>forward ref</option>
353
+ <option value={ElementTypeHostComponent}>
354
+ host (e.g. <div>)
355
+ </option>
356
+ <option value={ElementTypeMemo}>memo</option>
357
+ <option value={ElementTypeOtherOrUnknown}>other</option>
358
+ <option value={ElementTypeProfiler}>profiler</option>
359
+ <option value={ElementTypeSuspense}>suspense</option>
360
+ </select>
361
+ )}
362
+ {(componentFilter.type === ComponentFilterLocation ||
363
+ componentFilter.type === ComponentFilterDisplayName) && (
364
+ <input
365
+ className={styles.Input}
366
+ type="text"
367
+ placeholder="Regular expression"
368
+ onChange={({ currentTarget }) =>
369
+ updateFilterValueRegExp(
370
+ componentFilter,
371
+ currentTarget.value
372
+ )
373
+ }
374
+ value={componentFilter.value}
375
+ />
376
+ )}
377
+ </td>
378
+ <td className={styles.TableCell}>
379
+ <Button
380
+ onClick={() => removeFilter(index)}
381
+ title="Delete filter"
382
+ >
383
+ <ButtonIcon type="delete" />
384
+ </Button>
385
+ </td>
386
+ </tr>
387
+ ))}
388
+ </tbody>
389
+ </table>
390
+
391
+ <Button onClick={addFilter}>
392
+ <ButtonIcon className={styles.ButtonIcon} type="add" />
393
+ Add filter
394
+ </Button>
395
+ </div>
396
+ );
397
+}
398
+
399
+type ToggleIconProps = {|
400
+ isEnabled: boolean,
401
+ isValid: boolean,
402
+|};
403
+function ToggleIcon({ isEnabled, isValid }: ToggleIconProps) {
404
+ let className;
405
+ if (isValid) {
406
+ className = isEnabled ? styles.ToggleOn : styles.ToggleOff;
407
+ } else {
408
+ className = isEnabled ? styles.ToggleOnInvalid : styles.ToggleOffInvalid;
409
+ }
410
+ return (
411
+ <div className={className}>
412
+ <div
413
+ className={isEnabled ? styles.ToggleInsideOn : styles.ToggleInsideOff}
414
+ />
415
+ </div>
416
+ );
417
+}
src/devtools/views/Settings/GeneralSettings.js
new
+50
@@ -0,0 +1,50 @@
1
+// @flow
2
+
3
+import React, { useCallback, useContext } from 'react';
4
+import { SettingsContext } from './SettingsContext';
5
+
6
+import styles from './SettingsShared.css';
7
+
8
+export default function GeneralSettings(_: {||}) {
9
+ const { displayDensity, setDisplayDensity, theme, setTheme } = useContext(
10
+ SettingsContext
11
+ );
12
+
13
+ const updateDisplayDensity = useCallback(
14
+ ({ currentTarget }) => {
15
+ setDisplayDensity(currentTarget.value);
16
+ },
17
+ [setDisplayDensity]
18
+ );
19
+ const updateTheme = useCallback(
20
+ ({ currentTarget }) => {
21
+ setTheme(currentTarget.value);
22
+ },
23
+ [setTheme]
24
+ );
25
+
26
+ return (
27
+ <div className={styles.Settings}>
28
+ <div className={styles.Setting}>
29
+ <div className={styles.RadioLabel}>Theme</div>
30
+ <select className={styles.Select} value={theme} onChange={updateTheme}>
31
+ <option value="auto">Auto</option>
32
+ <option value="light">Light</option>
33
+ <option value="dark">Dark</option>
34
+ </select>
35
+ </div>
36
+
37
+ <div className={styles.Setting}>
38
+ <div className={styles.RadioLabel}>Display density</div>
39
+ <select
40
+ className={styles.Select}
41
+ value={displayDensity}
42
+ onChange={updateDisplayDensity}
43
+ >
44
+ <option value="compact">Compact</option>
45
+ <option value="comfortable">Comfortable</option>
46
+ </select>
47
+ </div>
48
+ </div>
49
+ );
50
+}
src/devtools/views/Settings/ProfilerSettings.js
new
+144
@@ -0,0 +1,144 @@
1
+// @flow
2
+
3
+import React, {
4
+ Fragment,
5
+ useCallback,
6
+ useContext,
7
+ useMemo,
8
+ useRef,
9
+} from 'react';
10
+import { useSubscription } from '../hooks';
11
+import { StoreContext } from '../context';
12
+import { ProfilerContext } from 'src/devtools/views/Profiler/ProfilerContext';
13
+import Store from 'src/devtools/store';
14
+
15
+import styles from './SettingsShared.css';
16
+
17
+export default function ProfilerSettings(_: {||}) {
18
+ const {
19
+ isCommitFilterEnabled,
20
+ minCommitDuration,
21
+ setIsCommitFilterEnabled,
22
+ setMinCommitDuration,
23
+ } = useContext(ProfilerContext);
24
+ const store = useContext(StoreContext);
25
+
26
+ const captureScreenshotsSubscription = useMemo(
27
+ () => ({
28
+ getCurrentValue: () => store.captureScreenshots,
29
+ subscribe: (callback: Function) => {
30
+ store.addListener('captureScreenshots', callback);
31
+ return () => store.removeListener('captureScreenshots', callback);
32
+ },
33
+ }),
34
+ [store]
35
+ );
36
+ const captureScreenshots = useSubscription<boolean, Store>(
37
+ captureScreenshotsSubscription
38
+ );
39
+
40
+ const recordChangeDescriptionsSubscription = useMemo(
41
+ () => ({
42
+ getCurrentValue: () => store.recordChangeDescriptions,
43
+ subscribe: (callback: Function) => {
44
+ store.addListener('recordChangeDescriptions', callback);
45
+ return () => store.removeListener('recordChangeDescriptions', callback);
46
+ },
47
+ }),
48
+ [store]
49
+ );
50
+ const recordChangeDescriptions = useSubscription<boolean, Store>(
51
+ recordChangeDescriptionsSubscription
52
+ );
53
+
54
+ const updateCaptureScreenshotsWhileProfiling = useCallback(
55
+ ({ currentTarget }) => {
56
+ store.captureScreenshots = currentTarget.checked;
57
+ },
58
+ [store]
59
+ );
60
+ const updateRecordChangeDescriptions = useCallback(
61
+ ({ currentTarget }) => {
62
+ store.recordChangeDescriptions = currentTarget.checked;
63
+ },
64
+ [store]
65
+ );
66
+ const updateMinCommitDuration = useCallback(
67
+ (event: SyntheticEvent<HTMLInputElement>) => {
68
+ const newValue = parseFloat(event.currentTarget.value);
69
+ setMinCommitDuration(
70
+ Number.isNaN(newValue) || newValue <= 0 ? 0 : newValue
71
+ );
72
+ },
73
+ [setMinCommitDuration]
74
+ );
75
+ const updateIsCommitFilterEnabled = useCallback(
76
+ (event: SyntheticEvent<HTMLInputElement>) => {
77
+ const checked = event.currentTarget.checked;
78
+ setIsCommitFilterEnabled(checked);
79
+ if (checked) {
80
+ if (minCommitDurationInputRef.current !== null) {
81
+ minCommitDurationInputRef.current.focus();
82
+ }
83
+ }
84
+ },
85
+ [setIsCommitFilterEnabled]
86
+ );
87
+
88
+ const minCommitDurationInputRef = useRef<HTMLInputElement | null>(null);
89
+
90
+ return (
91
+ <div className={styles.Settings}>
92
+ <div className={styles.Setting}>
93
+ <label>
94
+ <input
95
+ type="checkbox"
96
+ checked={recordChangeDescriptions}
97
+ onChange={updateRecordChangeDescriptions}
98
+ />{' '}
99
+ Record why each component rendered while profiling.
100
+ </label>
101
+ </div>
102
+
103
+ <div className={styles.Setting}>
104
+ <label>
105
+ <input
106
+ checked={isCommitFilterEnabled}
107
+ onChange={updateIsCommitFilterEnabled}
108
+ type="checkbox"
109
+ />{' '}
110
+ Hide commits below
111
+ </label>{' '}
112
+ <input
113
+ className={styles.Input}
114
+ onChange={updateMinCommitDuration}
115
+ ref={minCommitDurationInputRef}
116
+ type="number"
117
+ value={minCommitDuration}
118
+ />{' '}
119
+ (ms)
120
+ </div>
121
+
122
+ {store.supportsCaptureScreenshots && (
123
+ <Fragment>
124
+ <div className={styles.Setting}>
125
+ <label>
126
+ <input
127
+ type="checkbox"
128
+ checked={captureScreenshots}
129
+ onChange={updateCaptureScreenshotsWhileProfiling}
130
+ />{' '}
131
+ Capture screenshots while profiling
132
+ </label>
133
+ </div>
134
+ {captureScreenshots && (
135
+ <div className={styles.ScreenshotThrottling}>
136
+ Screenshots will be throttled in order to reduce the negative
137
+ impact on performance.
138
+ </div>
139
+ )}
140
+ </Fragment>
141
+ )}
142
+ </div>
143
+ );
144
+}
src/devtools/views/Settings/Settings.css
deleted
-86
@@ -1,86 +0,0 @@
1
-.Settings {
2
- width: 100%;
3
- padding: 0.5rem;
4
- display: flex;
5
- flex-direction: column;
6
- align-items: flex-start;
7
- justify-content: flex-start;
8
- border-top: 1px solid var(--color-border);
9
- font-family: var(--font-family-sans);
10
- font-size: var(--font-size-sans-normal);
11
- background-color: var(--color-background);
12
- color: var(--color-text);
13
-}
14
-
15
-.Section {
16
- width: 100%;
17
- padding: 0.5rem 0;
18
- border-top: 1px solid var(--color-border);
19
-}
20
-.Section:first-of-type {
21
- padding-top: 0;
22
- border-top: none;
23
-}
24
-
25
-.Header {
26
- margin-bottom: 0.5rem;
27
- font-size: var(--font-size-sans-large);
28
-}
29
-
30
-.OptionGroup {
31
- display: inline-flex;
32
- flex-direction: row;
33
- align-items: center;
34
- user-select: none;
35
- margin: 0 1rem 0.5rem 0;
36
-}
37
-.OptionGroup:last-of-type {
38
- margin-right: 0;
39
-}
40
-
41
-.OptionLabel {
42
- margin-right: 0.5rem;
43
- font-size: var(--font-size-sans-normal);
44
-}
45
-
46
-.RadioOption {
47
- cursor: pointer;
48
- padding: 0.5rem;
49
- border: 1px solid var(--color-border);
50
- border-right: none;
51
-}
52
-
53
-.RadioOption:hover {
54
- background-color: var(--color-background-hover);
55
-}
56
-
57
-.RadioOption:first-of-type {
58
- border-top-left-radius: 0.25rem;
59
- border-bottom-left-radius: 0.25rem;
60
-}
61
-
62
-.RadioOption:last-of-type {
63
- border-top-right-radius: 0.25rem;
64
- border-bottom-right-radius: 0.25rem;
65
- border-right: 1px solid var(--color-border);
66
-}
67
-
68
-.CheckboxOption {
69
- display: block;
70
- padding: 0 0 0.5rem;
71
-}
72
-
73
-.ScreenshotThrottling {
74
- display: inline-block;
75
- background-color: var(--color-background-hover);
76
- padding: 0.25rem 0.5rem;
77
- border-radius: 0.25rem;
78
-}
79
-
80
-.HRule {
81
- height: 1px;
82
- background-color: var(--color-border);
83
- width: 100%;
84
- border: none;
85
- margin: 0.5rem 0;
86
-}
src/devtools/views/Settings/Settings.js
deleted
-203
@@ -1,203 +0,0 @@
1
-// @flow
2
-
3
-import React, { Fragment, useCallback, useContext, useMemo } from 'react';
4
-import { useSubscription } from '../hooks';
5
-import { StoreContext } from '../context';
6
-import { SettingsContext } from './SettingsContext';
7
-import Store from 'src/devtools/store';
8
-import portaledContent from '../portaledContent';
9
-
10
-import styles from './Settings.css';
11
-
12
-function Settings(_: {||}) {
13
- const store = useContext(StoreContext);
14
- const { displayDensity, setDisplayDensity, theme, setTheme } = useContext(
15
- SettingsContext
16
- );
17
-
18
- const captureScreenshotsSubscription = useMemo(
19
- () => ({
20
- getCurrentValue: () => store.captureScreenshots,
21
- subscribe: (callback: Function) => {
22
- store.addListener('captureScreenshots', callback);
23
- return () => store.removeListener('captureScreenshots', callback);
24
- },
25
- }),
26
- [store]
27
- );
28
- const captureScreenshots = useSubscription<boolean, Store>(
29
- captureScreenshotsSubscription
30
- );
31
-
32
- const collapseNodesByDefaultSubscription = useMemo(
33
- () => ({
34
- getCurrentValue: () => store.collapseNodesByDefault,
35
- subscribe: (callback: Function) => {
36
- store.addListener('collapseNodesByDefault', callback);
37
- return () => store.removeListener('collapseNodesByDefault', callback);
38
- },
39
- }),
40
- [store]
41
- );
42
- const collapseNodesByDefault = useSubscription<boolean, Store>(
43
- collapseNodesByDefaultSubscription
44
- );
45
-
46
- const recordChangeDescriptionsSubscription = useMemo(
47
- () => ({
48
- getCurrentValue: () => store.recordChangeDescriptions,
49
- subscribe: (callback: Function) => {
50
- store.addListener('recordChangeDescriptions', callback);
51
- return () => store.removeListener('recordChangeDescriptions', callback);
52
- },
53
- }),
54
- [store]
55
- );
56
- const recordChangeDescriptions = useSubscription<boolean, Store>(
57
- recordChangeDescriptionsSubscription
58
- );
59
-
60
- const updateDisplayDensity = useCallback(
61
- ({ currentTarget }) => {
62
- setDisplayDensity(currentTarget.value);
63
- },
64
- [setDisplayDensity]
65
- );
66
-
67
- const updateTheme = useCallback(
68
- ({ currentTarget }) => {
69
- setTheme(currentTarget.value);
70
- },
71
- [setTheme]
72
- );
73
-
74
- const updateCaptureScreenshotsWhileProfiling = useCallback(
75
- ({ currentTarget }) => {
76
- store.captureScreenshots = currentTarget.checked;
77
- },
78
- [store]
79
- );
80
- const updateCollapseNodesByDefault = useCallback(
81
- ({ currentTarget }) => {
82
- store.collapseNodesByDefault = currentTarget.checked;
83
- },
84
- [store]
85
- );
86
- const updateRecordChangeDescriptions = useCallback(
87
- ({ currentTarget }) => {
88
- store.recordChangeDescriptions = currentTarget.checked;
89
- },
90
- [store]
91
- );
92
-
93
- return (
94
- <div className={styles.Settings}>
95
- <div className={styles.Section}>
96
- <div className={styles.Header}>Display preferences</div>
97
- <div className={styles.OptionGroup}>
98
- <div className={styles.OptionLabel}>Theme</div>
99
- <label className={styles.RadioOption}>
100
- <input
101
- type="radio"
102
- name="Settings-theme"
103
- checked={theme === 'auto'}
104
- value="auto"
105
- onChange={updateTheme}
106
- />{' '}
107
- Auto
108
- </label>
109
- <label className={styles.RadioOption}>
110
- <input
111
- type="radio"
112
- name="Settings-theme"
113
- checked={theme === 'light'}
114
- value="light"
115
- onChange={updateTheme}
116
- />{' '}
117
- Light
118
- </label>
119
- <label className={styles.RadioOption}>
120
- <input
121
- type="radio"
122
- name="Settings-theme"
123
- checked={theme === 'dark'}
124
- value="dark"
125
- onChange={updateTheme}
126
- />{' '}
127
- Dark
128
- </label>
129
- </div>
130
- <div className={styles.OptionGroup}>
131
- <div className={styles.OptionLabel}>Display density</div>
132
- <label className={styles.RadioOption}>
133
- <input
134
- type="radio"
135
- name="Settings-displayDensity"
136
- checked={displayDensity === 'compact'}
137
- value="compact"
138
- onChange={updateDisplayDensity}
139
- />{' '}
140
- Compact
141
- </label>
142
- <label className={styles.RadioOption}>
143
- <input
144
- type="radio"
145
- name="Settings-displayDensity"
146
- checked={displayDensity === 'comfortable'}
147
- value="comfortable"
148
- onChange={updateDisplayDensity}
149
- />{' '}
150
- Comfortable
151
- </label>
152
- </div>
153
- </div>
154
-
155
- <div className={styles.Section}>
156
- <div className={styles.Header}>Components tree</div>
157
-
158
- <label className={styles.CheckboxOption}>
159
- <input
160
- type="checkbox"
161
- checked={collapseNodesByDefault}
162
- onChange={updateCollapseNodesByDefault}
163
- />{' '}
164
- Collapse newly added components by default
165
- </label>
166
- </div>
167
-
168
- <div className={styles.Section}>
169
- <div className={styles.Header}>Profiler</div>
170
-
171
- <label className={styles.CheckboxOption}>
172
- <input
173
- type="checkbox"
174
- checked={recordChangeDescriptions}
175
- onChange={updateRecordChangeDescriptions}
176
- />{' '}
177
- Record why each component rendered while profiling.
178
- </label>
179
-
180
- {store.supportsCaptureScreenshots && (
181
- <Fragment>
182
- <label className={styles.CheckboxOption}>
183
- <input
184
- type="checkbox"
185
- checked={captureScreenshots}
186
- onChange={updateCaptureScreenshotsWhileProfiling}
187
- />{' '}
188
- Capture screenshots while profiling
189
- </label>
190
- {captureScreenshots && (
191
- <div className={styles.ScreenshotThrottling}>
192
- Screenshots will be throttled in order to reduce the negative
193
- impact on performance.
194
- </div>
195
- )}
196
- </Fragment>
197
- )}
198
- </div>
199
- </div>
200
- );
201
-}
202
-
203
-export default portaledContent(Settings);
src/devtools/views/Settings/SettingsModal.css
new
+43
@@ -0,0 +1,43 @@
1
+.Background {
2
+ position: absolute;
3
+ width: 100%;
4
+ top: 0;
5
+ bottom: 0;
6
+ background-color: var(--color-modal-background);
7
+ display: flex;
8
+ align-items: flex-start;
9
+ justify-content: center;
10
+ font-size: var(--font-size-sans-normal);
11
+ padding: 1rem;
12
+}
13
+
14
+.Modal {
15
+ display: flex;
16
+ flex-direction: column;
17
+ flex: 0 1 auto;
18
+ max-height: 100%;
19
+ background-color: var(--color-background);
20
+ border: 1px solid var(--color-border);
21
+ border-radius: 0.25rem;
22
+ overflow: auto;
23
+ width: 400px;
24
+ max-width: 100%;
25
+}
26
+
27
+.Spacer {
28
+ flex: 1;
29
+}
30
+
31
+.Tabs {
32
+ display: flex;
33
+ flex-direction: row;
34
+ border-bottom: 1px solid var(--color-border);
35
+ padding-right: 0.25rem;
36
+ flex: 0 0 auto;
37
+}
38
+
39
+.Content {
40
+ padding: 0.5rem;
41
+ flex: 0 1 auto;
42
+ overflow: auto;
43
+}
src/devtools/views/Settings/SettingsModal.js
new
+134
@@ -0,0 +1,134 @@
1
+// @flow
2
+
3
+import React, {
4
+ useCallback,
5
+ useContext,
6
+ useEffect,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+} from 'react';
11
+import { SettingsModalContext } from './SettingsModalContext';
12
+import Store from 'src/devtools/store';
13
+import Button from '../Button';
14
+import ButtonIcon from '../ButtonIcon';
15
+import TabBar from '../TabBar';
16
+import { StoreContext } from '../context';
17
+import { useModalDismissSignal, useSubscription } from '../hooks';
18
+import ComponentsSettings from './ComponentsSettings';
19
+import GeneralSettings from './GeneralSettings';
20
+import ProfilerSettings from './ProfilerSettings';
21
+
22
+import styles from './SettingsModal.css';
23
+
24
+type TabID = 'general' | 'components' | 'profiler';
25
+
26
+type Props = {|
27
+ defaultTabID: TabID,
28
+|};
29
+
30
+export default function SettingsModal({ defaultTabID = 'general' }: Props) {
31
+ const { isModalShowing, setIsModalShowing } = useContext(
32
+ SettingsModalContext
33
+ );
34
+ const store = useContext(StoreContext);
35
+ const { profilerStore } = store;
36
+
37
+ // Updating preferences while profiling is in progress could break things (e.g. filtering)
38
+ // Explicitly disallow it for now.
39
+ const isProfilingSubscription = useMemo(
40
+ () => ({
41
+ getCurrentValue: () => profilerStore.isProfiling,
42
+ subscribe: (callback: Function) => {
43
+ profilerStore.addListener('isProfiling', callback);
44
+ return () => profilerStore.removeListener('isProfiling', callback);
45
+ },
46
+ }),
47
+ [profilerStore]
48
+ );
49
+ const isProfiling = useSubscription<boolean, Store>(isProfilingSubscription);
50
+ if (isProfiling && isModalShowing) {
51
+ setIsModalShowing(false);
52
+ }
53
+
54
+ if (!isModalShowing) {
55
+ return null;
56
+ }
57
+
58
+ return <SettingsModalImpl defaultTabID={defaultTabID} />;
59
+}
60
+
61
+function SettingsModalImpl({ defaultTabID }: Props) {
62
+ const { setIsModalShowing } = useContext(SettingsModalContext);
63
+ const dismissModal = useCallback(() => setIsModalShowing(false), [
64
+ setIsModalShowing,
65
+ ]);
66
+
67
+ const [selectedTabID, selectTab] = useState<TabID>(defaultTabID);
68
+
69
+ const modalRef = useRef<HTMLDivElement | null>(null);
70
+ useModalDismissSignal(modalRef, dismissModal);
71
+
72
+ useEffect(() => {
73
+ if (modalRef.current !== null) {
74
+ modalRef.current.focus();
75
+ }
76
+ }, [modalRef]);
77
+
78
+ let view = null;
79
+ switch (selectedTabID) {
80
+ case 'general':
81
+ view = <GeneralSettings />;
82
+ break;
83
+ case 'profiler':
84
+ view = <ProfilerSettings />;
85
+ break;
86
+ case 'components':
87
+ view = <ComponentsSettings />;
88
+ break;
89
+ default:
90
+ break;
91
+ }
92
+
93
+ return (
94
+ <div className={styles.Background}>
95
+ <div className={styles.Modal} ref={modalRef}>
96
+ <div className={styles.Tabs}>
97
+ <TabBar
98
+ currentTab={selectedTabID}
99
+ id="Settings"
100
+ selectTab={selectTab}
101
+ size="small"
102
+ tabs={tabs}
103
+ />
104
+ <div className={styles.Spacer} />
105
+ <Button onClick={dismissModal} title="Close settings dialog">
106
+ <ButtonIcon type="close" />
107
+ </Button>
108
+ </div>
109
+ <div className={styles.Content}>{view}</div>
110
+ </div>
111
+ </div>
112
+ );
113
+}
114
+
115
+const tabs = [
116
+ {
117
+ id: 'general',
118
+ icon: 'settings',
119
+ label: 'General',
120
+ title: 'General preferences',
121
+ },
122
+ {
123
+ id: 'components',
124
+ icon: 'components',
125
+ label: 'Components',
126
+ title: 'Components preferences',
127
+ },
128
+ {
129
+ id: 'profiler',
130
+ icon: 'profiler',
131
+ label: 'Profiler',
132
+ title: 'Profiler preferences',
133
+ },
134
+];
src/devtools/views/Settings/SettingsModalContext.js
new
+35
@@ -0,0 +1,35 @@
1
+// @flow
2
+
3
+import React, { createContext, useMemo, useState } from 'react';
4
+
5
+export type DisplayDensity = 'comfortable' | 'compact';
6
+export type Theme = 'auto' | 'light' | 'dark';
7
+
8
+type Context = {
9
+ isModalShowing: boolean,
10
+ setIsModalShowing: (value: boolean) => void,
11
+};
12
+
13
+const SettingsModalContext = createContext<Context>(((null: any): Context));
14
+SettingsModalContext.displayName = 'SettingsModalContext';
15
+
16
+function SettingsModalContextController({
17
+ children,
18
+}: {|
19
+ children: React$Node,
20
+|}) {
21
+ const [isModalShowing, setIsModalShowing] = useState<boolean>(false);
22
+
23
+ const value = useMemo(() => ({ isModalShowing, setIsModalShowing }), [
24
+ isModalShowing,
25
+ setIsModalShowing,
26
+ ]);
27
+
28
+ return (
29
+ <SettingsModalContext.Provider value={value}>
30
+ {children}
31
+ </SettingsModalContext.Provider>
32
+ );
33
+}
34
+
35
+export { SettingsModalContext, SettingsModalContextController };
src/devtools/views/Settings/SettingsModalContextToggle.js
new
+43
@@ -0,0 +1,43 @@
1
+// @flow
2
+
3
+import React, { useCallback, useContext, useMemo } from 'react';
4
+import { SettingsModalContext } from './SettingsModalContext';
5
+import Button from '../Button';
6
+import ButtonIcon from '../ButtonIcon';
7
+import { StoreContext } from '../context';
8
+import { useSubscription } from '../hooks';
9
+import Store from 'src/devtools/store';
10
+
11
+export default function SettingsModalContextToggle() {
12
+ const { setIsModalShowing } = useContext(SettingsModalContext);
13
+ const store = useContext(StoreContext);
14
+ const { profilerStore } = store;
15
+
16
+ const showFilterModal = useCallback(() => setIsModalShowing(true), [
17
+ setIsModalShowing,
18
+ ]);
19
+
20
+ // Updating preferences while profiling is in progress could break things (e.g. filtering)
21
+ // Explicitly disallow it for now.
22
+ const isProfilingSubscription = useMemo(
23
+ () => ({
24
+ getCurrentValue: () => profilerStore.isProfiling,
25
+ subscribe: (callback: Function) => {
26
+ profilerStore.addListener('isProfiling', callback);
27
+ return () => profilerStore.removeListener('isProfiling', callback);
28
+ },
29
+ }),
30
+ [profilerStore]
31
+ );
32
+ const isProfiling = useSubscription<boolean, Store>(isProfilingSubscription);
33
+
34
+ return (
35
+ <Button
36
+ disabled={isProfiling}
37
+ onClick={showFilterModal}
38
+ title="View settings"
39
+ >
40
+ <ButtonIcon type="settings" />
41
+ </Button>
42
+ );
43
+}
src/devtools/views/Settings/SettingsShared.css
renamed
+49
-32
@@ -1,37 +1,61 @@
1
-.Background {
2
- position: absolute;
3
- width: 100%;
4
- height: 100%;
1
+.Settings {
2
display: flex;
3
+ flex-direction: column;
4
align-items: flex-start;
7
- justify-content: center;
8
- padding: 1rem;
9
- background-color: var(--color-modal-background);
10
- overflow: auto;
5
+ justify-content: flex-start;
6
+ font-family: var(--font-family-sans);
7
+ font-size: var(--font-size-sans-normal);
8
}
9
13
-.Modal {
14
- position: relative;
15
- z-index: 3;
16
- min-width: 20rem;
17
- max-width: 100%;
10
+.Setting {
11
+ margin-bottom: 0.5rem;
12
+}
13
+.Setting:last-of-type {
14
+ margin-bottom: 0;
15
+}
16
+
17
+.OptionGroup {
18
+ display: inline-flex;
19
+ flex-direction: row;
20
+ align-items: center;
21
+ user-select: none;
22
+ margin: 0 1rem 0.5rem 0;
23
+}
24
+.OptionGroup:last-of-type {
25
+ margin-right: 0;
26
+}
27
+
28
+.RadioLabel {
29
+ display: inline;
30
+ margin-right: 0.5rem;
31
+}
32
+
33
+.Select {
34
+}
35
+
36
+.CheckboxOption {
37
+ display: block;
38
+ padding: 0 0 0.5rem;
39
+}
40
+
41
+.ScreenshotThrottling {
42
display: inline-block;
19
- background-color: var(--color-background);
20
- padding: 0.5rem;
21
- border: 1px solid var(--color-border);
43
+ background-color: var(--color-background-hover);
44
+ padding: 0.25rem 0.5rem;
45
border-radius: 0.25rem;
46
}
47
25
-.LeftRight {
26
- display: flex;
27
-}
28
-.Left {
48
+.HRule {
49
+ height: 1px;
50
+ background-color: var(--color-border);
51
+ width: 100%;
52
+ border: none;
53
+ margin: 0.5rem 0;
54
}
30
-.Right {
31
- flex: 1;
32
- display: flex;
33
- align-items: center;
34
- justify-content: flex-end;
55
+
56
+.Header {
57
+ font-size: var(--font-size-sans-large);
58
+ margin-top: 0.5rem;
59
}
60
61
.ButtonIcon {
@@ -62,19 +86,12 @@
86
padding-right: 0;
87
}
88
65
-.Select {
66
-}
67
-
89
.Input {
90
border: 1px solid var(--color-border);
91
border-radius: 0.125rem;
92
padding: 0.125rem;
93
}
94
74
-.CancelButton {
75
- margin-right: 0.25rem;
76
-}
77
-
95
.InvalidRegExp,
96
.InvalidRegExp:active,
97
.InvalidRegExp:focus,
src/devtools/views/TabBar.css
+3
-2
@@ -7,7 +7,6 @@
7
cursor: pointer;
8
border-top: 3px solid transparent;
9
border-bottom: 3px solid transparent;
10
- cursor: pointer;
10
user-select: none;
11
color: var(--color-text);
12
}
@@ -30,9 +29,11 @@
29
}
30
31
.TabSizeLarge {
32
+ font-size: var(--font-size-sans-large);
33
padding: 0.5rem 1rem;
34
}
35
.TabSizeSmall {
36
+ font-size: var(--font-size-sans-normal);
37
padding: 0.25rem 0.5rem;
38
}
39
@@ -76,7 +77,7 @@
77
}
78
}
79
79
-@media screen and (max-width: 600px) {
80
+@media screen and (max-width: 525px) {
81
.IconSizeLarge {
82
margin-right: 0;
83
}
src/utils.js
+16
@@ -229,3 +229,19 @@ export function separateDisplayNameAndHOCs(
229
230
return [displayName, hocDisplayNames];
231
}
232
+
233
+// Pulled from react-compat
234
+// https://github.com/developit/preact-compat/blob/7c5de00e7c85e2ffd011bf3af02899b63f699d3a/src/index.js#L349
235
+export function shallowDiffers(prev: Object, next: Object): boolean {
236
+ for (let attribute in prev) {
237
+ if (!(attribute in next)) {
238
+ return true;
239
+ }
240
+ }
241
+ for (let attribute in next) {
242
+ if (prev[attribute] !== next[attribute]) {
243
+ return true;
244
+ }
245
+ }
246
+ return false;
247
+}