@samitouri / QOS-React-2 / commits / 4ea81337f3

Initial pass at adding Settings panel to browser extension

Brian Vaughn committed Feb 13, 2019 at 10:59 UTC 4ea81337f382a75226d619b1d8eca98f4010dd7b
13 files changed +197 -55
shells/browser/chrome/manifest.json
+6 -1
@@ -27,7 +27,12 @@
27 "devtools_page": "main.html",
28
29 "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
30 - "web_accessible_resources": ["main.html", "panel.html", "build/backend.js"],
30 + "web_accessible_resources": [
31 + "main.html",
32 + "panel.html",
33 + "settings.html",
34 + "build/backend.js"
35 + ],
36
37 "background": {
38 "scripts": ["build/background.js"],
shells/browser/firefox/manifest.json
+6 -1
@@ -33,7 +33,12 @@
33 "devtools_page": "main.html",
34
35 "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
36 - "web_accessible_resources": ["main.html", "panel.html", "build/backend.js"],
36 + "web_accessible_resources": [
37 + "main.html",
38 + "panel.html",
39 + "settings.html",
40 + "build/backend.js"
41 + ],
42
43 "background": {
44 "scripts": ["build/background.js"],
shells/browser/shared/build.js
+7 -1
@@ -7,7 +7,13 @@ const { join } = require('path');
7
8 // These files are copied along with Webpack-bundled files
9 // to produce the final web extension
10 -const STATIC_FILES = ['icons', 'popups', 'main.html', 'panel.html'];
10 +const STATIC_FILES = [
11 + 'icons',
12 + 'popups',
13 + 'main.html',
14 + 'panel.html',
15 + 'settings.html',
16 +];
17
18 const preProcess = async (destinationPath, tempPath) => {
19 await remove(destinationPath); // Clean up from previously completed builds
shells/browser/shared/settings.html new
+32
@@ -0,0 +1,32 @@
1 +<!doctype html>
2 +<html style="display: flex">
3 + <head>
4 + <meta charset="utf8">
5 + <style>
6 + html {
7 + display: flex;
8 + }
9 + body {
10 + margin: 0;
11 + padding: 0;
12 + flex: 1;
13 + display: flex;
14 + }
15 + #container {
16 + display: flex;
17 + flex: 1;
18 + width: 100%;
19 + position: fixed;
20 + top: 0;
21 + left: 0;
22 + right: 0;
23 + bottom: 0;
24 + }
25 + </style>
26 + </head>
27 + <body>
28 + <!-- main react mount point -->
29 + <div id="container">Unable to find React on the page.</div>
30 + <script src="./build/settings.js"></script>
31 + </body>
32 +</html>
shells/browser/shared/src/main.js
+9 -3
@@ -6,6 +6,7 @@ function createPanelIfReactLoaded() {
6 if (panelCreated) {
7 return;
8 }
9 +
10 chrome.devtools.inspectedWindow.eval(
11 'window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && window.__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.size > 0',
12 function(pageHasReact, err) {
@@ -15,9 +16,7 @@ function createPanelIfReactLoaded() {
16
17 clearInterval(loadCheckInterval);
18 panelCreated = true;
18 - chrome.devtools.panels.create('⚛ Elements', '', 'panel.html', function(
19 - panel
20 - ) {
19 + chrome.devtools.panels.create('⚛ Elements', '', 'panel.html', panel => {
20 panel.onShown.addListener(function(window) {
21 // TODO: When the user switches to the panel, check for an Elements tab selection.
22 });
@@ -25,6 +24,13 @@ function createPanelIfReactLoaded() {
24 // TODO: Stop highlighting and stuff.
25 });
26 });
27 +
28 + chrome.devtools.panels.create(
29 + '⚛ Settings',
30 + '',
31 + 'settings.html',
32 + panel => {}
33 + );
34 }
35 );
36 }
shells/browser/shared/src/panel.js
+4 -30
@@ -5,34 +5,7 @@ import { createRoot, flushSync } from 'react-dom';
5 import Bridge from 'src/bridge';
6 import DevTools from 'src/devtools/views/DevTools';
7 import inject from './inject';
8 -
9 -const IS_CHROME = navigator.userAgent.indexOf('Firefox') < 0;
10 -
11 -let browserName;
12 -let themeName;
13 -
14 -if (IS_CHROME) {
15 - browserName = 'Chrome';
16 -
17 - // chrome.devtools.panels added in Chrome 18.
18 - // chrome.devtools.panels.themeName added in Chrome 54.
19 - themeName = chrome.devtools.panels.themeName === 'dark' ? 'dark' : 'light';
20 -} else {
21 - browserName = 'Firefox';
22 -
23 - // chrome.devtools.panels.themeName added in Firefox 55.
24 - // https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/devtools.panels/themeName
25 - if (chrome.devtools && chrome.devtools.panels) {
26 - switch (chrome.devtools.panels.themeName) {
27 - case 'dark':
28 - themeName = 'dark';
29 - break;
30 - default:
31 - themeName = 'light';
32 - break;
33 - }
34 - }
35 -}
8 +import { getBrowserName, getBrowserTheme } from './utils';
9
10 const container = ((document.getElementById('container'): any): HTMLElement);
11
@@ -65,8 +38,9 @@ function injectAndInit() {
38 root.render(
39 createElement(DevTools, {
40 bridge,
68 - browserName,
69 - browserTheme: themeName,
41 + browserName: getBrowserName(),
42 + browserTheme: getBrowserTheme(),
43 + defaultTab: 'elements',
44 showTabBar: false,
45 })
46 );
shells/browser/shared/src/settings.js new
+48
@@ -0,0 +1,48 @@
1 +/* global chrome */
2 +
3 +import { createElement } from 'react';
4 +import { createRoot, flushSync } from 'react-dom';
5 +import Bridge from 'src/bridge';
6 +import DevTools from 'src/devtools/views/DevTools';
7 +import inject from './inject';
8 +import { getBrowserName, getBrowserTheme } from './utils';
9 +
10 +const container = ((document.getElementById('container'): any): HTMLElement);
11 +
12 +function injectAndInit() {
13 + // Noop Bridge for the Settings panel
14 + const bridge = new Bridge({
15 + listen(fn) {},
16 + send(event: string, payload: any, transferable?: Array<any>) {},
17 + });
18 +
19 + // Clear the "React not found" initial message before rendering.
20 + container.innerHTML = '';
21 +
22 + const root = createRoot(container);
23 + root.render(
24 + createElement(DevTools, {
25 + bridge,
26 + browserName: getBrowserName(),
27 + browserTheme: getBrowserTheme(),
28 + defaultTab: 'settings',
29 + showTabBar: false,
30 + })
31 + );
32 +
33 + // Initialize the backend only once the DevTools frontend Store has been initialized.
34 + // Otherwise the Store may miss important initial tree op codes.
35 + inject(chrome.runtime.getURL('build/backend.js'));
36 +
37 + // Reload the DevTools extension when the user navigates to a new page.
38 + function onNavigated() {
39 + chrome.devtools.network.onNavigated.removeListener(onNavigated);
40 +
41 + bridge.send('shutdown');
42 +
43 + flushSync(() => root.unmount(injectAndInit));
44 + }
45 + chrome.devtools.network.onNavigated.addListener(onNavigated);
46 +}
47 +
48 +injectAndInit();
shells/browser/shared/src/utils.js new
+26
@@ -0,0 +1,26 @@
1 +/* global chrome */
2 +
3 +const IS_CHROME = navigator.userAgent.indexOf('Firefox') < 0;
4 +
5 +export function getBrowserName() {
6 + return IS_CHROME ? 'Chrome' : 'Firefox';
7 +}
8 +
9 +export function getBrowserTheme() {
10 + if (IS_CHROME) {
11 + // chrome.devtools.panels added in Chrome 18.
12 + // chrome.devtools.panels.themeName added in Chrome 54.
13 + return chrome.devtools.panels.themeName === 'dark' ? 'dark' : 'light';
14 + } else {
15 + // chrome.devtools.panels.themeName added in Firefox 55.
16 + // https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/devtools.panels/themeName
17 + if (chrome.devtools && chrome.devtools.panels) {
18 + switch (chrome.devtools.panels.themeName) {
19 + case 'dark':
20 + return 'dark';
21 + default:
22 + return 'light';
23 + }
24 + }
25 + }
26 +}
shells/browser/shared/webpack.config.js
+1
@@ -17,6 +17,7 @@ module.exports = {
17 inject: './src/GlobalHook.js',
18 main: './src/main.js',
19 panel: './src/panel.js',
20 + settings: './src/settings.js',
21 },
22 output: {
23 path: __dirname + '/build',
src/devtools/views/DevTools.css new
+17
@@ -0,0 +1,17 @@
1 +.DevTools {
2 + width: 100%;
3 + height: 100%;
4 + display: flex;
5 + flex-direction: column;
6 + background-color: var(--color-background);
7 + color: var(--color-text-color);
8 +}
9 +
10 +.TabBar {
11 + flex: 0 0 auto;
12 +}
13 +
14 +.TabContent {
15 + flex: 1 1 100%;
16 + overflow: auto;
17 +}
src/devtools/views/DevTools.js
+13 -4
@@ -10,16 +10,19 @@ import TabBar from './TabBar';
10 import { SettingsContextController } from './SettingsContext';
11 import { TreeContextController } from './TreeContext';
12
13 +import styles from './DevTools.css';
14 +
15 import './root.css';
16
17 import type { Bridge } from '../../types';
18
17 -export type TabID = 'elements' | 'profiler' | 'settings';
19 +export type BrowserName = 'Chrome' | 'Firefox';
20 export type BrowserTheme = 'dark' | 'light';
21 +export type TabID = 'elements' | 'profiler' | 'settings';
22
23 export type Props = {|
24 bridge: Bridge,
22 - browserName: string,
25 + browserName: BrowserName,
26 defaultTab?: TabID,
27 browserTheme: BrowserTheme,
28 showTabBar?: boolean,
@@ -54,8 +57,14 @@ export default function DevTools({
57 <StoreContext.Provider value={store}>
58 <SettingsContextController browserTheme={browserTheme}>
59 <TreeContextController>
57 - {showTabBar && <TabBar currentTab={tab} selectTab={setTab} />}
58 - {tabElement}
60 + <div className={styles.DevTools}>
61 + {showTabBar && (
62 + <div className={styles.TabBar}>
63 + <TabBar currentTab={tab} selectTab={setTab} />
64 + </div>
65 + )}
66 + <div className={styles.TabContent}>{tabElement}</div>
67 + </div>
68 </TreeContextController>
69 </SettingsContextController>
70 </StoreContext.Provider>
src/devtools/views/Element.js
+6
@@ -24,6 +24,12 @@ export default function ElementView({ index, style }: Props) {
24 } = useContext(TreeContext);
25
26 const element = getElementAtIndex(index);
27 +
28 + if (element == null) {
29 + console.warn(`<ElementView> Could not find element at index ${index}`);
30 + return null;
31 + }
32 +
33 const { depth, displayName, id, key, type } = ((element: any): Element);
34
35 const handleDoubleClick = useCallback(() => selectOwner(id), [id]);
src/devtools/views/hooks.js
+22 -15
@@ -1,43 +1,50 @@
1 // @flow
2
3 -import { useState } from 'react';
3 +import { useLayoutEffect, useState } from 'react';
4
5 -// https://usehooks.com/useLocalStorage/
5 +// Forked from https://usehooks.com/useLocalStorage/
6 export function useLocalStorage<T>(
7 key: string,
8 initialValue: T
9 ): [T, (value: T | (() => T)) => void] {
10 - // State to store our value
11 - // Pass initial state function to useState so logic is only executed once
12 - const [storedValue, setStoredValue] = useState(() => {
10 + const getValueFromLocalStorage = () => {
11 try {
14 - // Get from local storage by key
12 const item = window.localStorage.getItem(key);
16 - // Parse stored json or if none return initialValue
13 return item ? JSON.parse(item) : initialValue;
14 } catch (error) {
19 - // If error also return initialValue
15 console.log(error);
16 return initialValue;
17 }
23 - });
18 + };
19 +
20 + const [storedValue, setStoredValue] = useState(getValueFromLocalStorage);
21
25 - // Return a wrapped version of useState's setter function that ...
26 - // ... persists the new value to localStorage.
22 const setValue = value => {
23 try {
29 - // Allow value to be a function so we have same API as useState
24 const valueToStore =
25 value instanceof Function ? value(storedValue) : value;
32 - // Save state
26 setStoredValue(valueToStore);
34 - // Save to local storage
27 window.localStorage.setItem(key, JSON.stringify(valueToStore));
28 } catch (error) {
37 - // A more advanced implementation would handle the error case
29 console.log(error);
30 }
31 };
32
33 + // Listen for changes in other extension panels.
34 + useLayoutEffect(() => {
35 + const onStorage = event => {
36 + const newValue = getValueFromLocalStorage();
37 + if (key === event.key && storedValue !== newValue) {
38 + setValue(newValue);
39 + }
40 + };
41 +
42 + window.addEventListener('storage', onStorage);
43 +
44 + return () => {
45 + window.removeEventListener('storage', onStorage);
46 + };
47 + }, [key, storedValue]);
48 +
49 return [storedValue, setValue];
50 }