Bug 1471795 - Part 7: Implement tab list. r?jdescottes, r?ladybenko draft
authorDaisuke Akatsuka <dakatsuka@mozilla.com>
Thu, 19 Jul 2018 16:23:40 +0900
changeset 820256 dd7a6ff9b6a99509f4a9393e52c23f021eacced8
parent 820255 3c24193c4d06cf211fb4c134133acefda34318c2
child 820257 5c5041fec319cc4b0b330b9a7f8ca04ca3cca9b1
push id116772
push userbmo:dakatsuka@mozilla.com
push dateThu, 19 Jul 2018 09:51:56 +0000
reviewersjdescottes, ladybenko
bugs1471795
milestone63.0a1
Bug 1471795 - Part 7: Implement tab list. r?jdescottes, r?ladybenko MozReview-Commit-ID: LeLI9sKvrOI
devtools/client/aboutdebugging-new/aboutdebugging.css
devtools/client/aboutdebugging-new/aboutdebugging.js
devtools/client/aboutdebugging-new/src/actions/runtimes.js
devtools/client/aboutdebugging-new/src/components/DebugTargetsPane.js
devtools/client/aboutdebugging-new/src/components/RuntimeInfo.css
devtools/client/aboutdebugging-new/src/components/debugtarget/DebugTargetItem.css
devtools/client/aboutdebugging-new/src/components/debugtarget/DebugTargetItem.js
devtools/client/aboutdebugging-new/src/components/debugtarget/DebugTargetList.css
devtools/client/aboutdebugging-new/src/components/debugtarget/DebugTargetList.js
devtools/client/aboutdebugging-new/src/components/debugtarget/TabItem.js
devtools/client/aboutdebugging-new/src/components/debugtarget/moz.build
devtools/client/aboutdebugging-new/src/components/moz.build
devtools/client/aboutdebugging-new/src/create-store.js
devtools/client/aboutdebugging-new/src/runtimes/runtime.js
devtools/client/aboutdebugging-new/src/runtimes/this-firefox.js
--- a/devtools/client/aboutdebugging-new/aboutdebugging.css
+++ b/devtools/client/aboutdebugging-new/aboutdebugging.css
@@ -2,16 +2,18 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 @import "resource://devtools/client/themes/variables.css";
 @import "resource://devtools/client/aboutdebugging-new/src/components/App.css";
 @import "resource://devtools/client/aboutdebugging-new/src/components/DebugTargetsPane.css";
 @import "resource://devtools/client/aboutdebugging-new/src/components/RuntimeInfo.css";
 @import "resource://devtools/client/aboutdebugging-new/src/components/RuntimesPane.css";
+@import "resource://devtools/client/aboutdebugging-new/src/components/debugtarget/DebugTargetItem.css";
+@import "resource://devtools/client/aboutdebugging-new/src/components/debugtarget/DebugTargetList.css";
 @import "resource://devtools/client/aboutdebugging-new/src/components/runtime/ThisFirefoxItem.css";
 
 html, body, #mount {
   font-size: 15px;
   height: 100%;
   margin: 0;
   padding: 0;
   width: 100%;
--- a/devtools/client/aboutdebugging-new/aboutdebugging.js
+++ b/devtools/client/aboutdebugging-new/aboutdebugging.js
@@ -22,44 +22,44 @@ const Provider =
 
 const actions = require("./src/actions/index");
 const { configureStore } = require("./src/create-store");
 const ThisFirefox = require("./src/runtimes/this-firefox");
 
 const App = createFactory(require("./src/components/App"));
 
 const AboutDebugging = {
-  init() {
+  async init() {
     if (!Services.prefs.getBoolPref("devtools.enabled", true)) {
       // If DevTools are disabled, navigate to about:devtools.
       window.location = "about:devtools?reason=AboutDebugging";
       return;
     }
 
     this.updateSelectedRuntime = this.updateSelectedRuntime.bind(this);
 
     this.store = configureStore();
     this.actions = bindActionCreators(actions, this.store.dispatch);
 
     const thisFirefox = new ThisFirefox();
-    this.updateSelectedRuntime(thisFirefox);
+    await this.updateSelectedRuntime(thisFirefox);
 
     render(Provider({ store: this.store }, App({ thisFirefox })), this.mount);
   },
 
   destroy() {
     unmountComponentAtNode(this.mount);
   },
 
   get mount() {
     return document.getElementById("mount");
   },
 
-  updateSelectedRuntime(runtime) {
-    this.actions.updateSelectedRuntime(runtime);
+  async updateSelectedRuntime(runtime) {
+    await this.actions.updateSelectedRuntime(runtime);
   },
 };
 
 window.addEventListener("DOMContentLoaded", () => {
   AboutDebugging.init();
 }, { once: true });
 
 window.addEventListener("unload", () => {
--- a/devtools/client/aboutdebugging-new/src/actions/runtimes.js
+++ b/devtools/client/aboutdebugging-new/src/actions/runtimes.js
@@ -4,17 +4,20 @@
 
 "use strict";
 
 const {
   UPDATE_SELECTED_RUNTIME,
 } = require("../constants");
 
 function updateSelectedRuntime(runtime) {
-  return {
-    type: UPDATE_SELECTED_RUNTIME,
-    runtime,
+  return async dispatch => {
+    await runtime.connect();
+    dispatch({
+      type: UPDATE_SELECTED_RUNTIME,
+      runtime,
+    });
   };
 }
 
 module.exports = {
   updateSelectedRuntime,
 };
--- a/devtools/client/aboutdebugging-new/src/components/DebugTargetsPane.js
+++ b/devtools/client/aboutdebugging-new/src/components/DebugTargetsPane.js
@@ -5,53 +5,72 @@
 "use strict";
 
 const { createFactory, PureComponent } = require("devtools/client/shared/vendor/react");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 
 const Runtime = require("../runtimes/runtime");
 
+const DebugTargetList = createFactory(require("./debugtarget/DebugTargetList"));
 const RuntimeInfo = createFactory(require("./RuntimeInfo"));
+const TabItem = createFactory(require("./debugtarget/TabItem"));
 
 class DebugTargetsPane extends PureComponent {
   static get propTypes() {
     return {
       runtime: PropTypes.instanceOf(Runtime).isRequired,
     };
   }
 
   constructor(props) {
     super(props);
     this.state = {
       info: {},
+      tabs: [],
     };
     this.update(props.runtime);
   }
 
   componentDidUpdate(prevProps) {
     if (prevProps.runtime !== this.props.runtime) {
       this.update(this.props.runtime);
     }
   }
 
   update(runtime) {
     this.updateRuntimeInfo(runtime);
+    this.updateTabs(runtime);
   }
 
   async updateRuntimeInfo(runtime) {
     const info = await runtime.getRuntimeInfo(runtime);
     this.setState({ info });
   }
 
+  async updateTabs(runtime) {
+    // Filter out closed tabs (represented as `null`).
+    const tabs = (await runtime.getTabs()).filter(t => !!t);
+    this.setState({ tabs });
+  }
+
   render() {
-    const { info } = this.state;
+    const {
+      info,
+      tabs,
+    } = this.state;
 
     return dom.div(
       {
         className: "debug-targets-pane",
       },
-      RuntimeInfo({ info })
+      RuntimeInfo({ info }),
+      DebugTargetList({
+        className: "debug-target-list--tabs",
+        debugTargetItemComponent: TabItem,
+        debugTargets: tabs,
+        title: "Tabs",
+      })
     );
   }
 }
 
 module.exports = DebugTargetsPane;
--- a/devtools/client/aboutdebugging-new/src/components/RuntimeInfo.css
+++ b/devtools/client/aboutdebugging-new/src/components/RuntimeInfo.css
@@ -2,15 +2,16 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 .runtime-info {
   align-items: center;
   display: flex;
   font-size: 30px;
   margin-block-start: 60px;
+  margin-block-end: 50px;
 }
 
 .runtime-info__icon {
   height: 64px;
   margin-inline-end: 12px;
   width: 64px;
 }
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging-new/src/components/debugtarget/DebugTargetItem.css
@@ -0,0 +1,19 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.debug-target-item {
+  display: flex;
+  margin-block-end: 20px;
+}
+
+.debug-target-item__icon {
+  height: 36px;
+  margin-inline-end: 18px;
+  width: 36px;
+}
+
+.debug-target-item__info__name {
+  font-size: 20px;
+  margin-block-end: 2px;
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging-new/src/components/debugtarget/DebugTargetItem.js
@@ -0,0 +1,92 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { PureComponent } = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+/**
+ * This component shows the debug target as item in DebugTargetsPane.
+ */
+class DebugTargetItem extends PureComponent {
+  static get propTypes() {
+    return {
+      debugTarget: PropTypes.object.isRequired,
+    };
+  }
+
+  /**
+   * Return icon src.
+   * Subclass should override this method.
+   * @return {String} URL
+   */
+  getIcon() {
+    throw new Error("Subclass of DebugTargetItem should override getIcon()");
+  }
+
+  /**
+   * Return name of this debug target.
+   * Subclass should override this method.
+   * @return {String} name
+   */
+  getName() {
+    throw new Error("Subclass of DebugTargetItem should override getName()");
+  }
+
+  /**
+   * Render components which show more information.
+   * Subclass should override this method.
+   * @return {Array} or {ReactComponent} or null
+   */
+  renderDetailComponents() {
+    throw new Error("Subclass of DebugTargetItem should override " +
+                    "renderDetailComponents()");
+  }
+
+  renderIcon() {
+    return dom.img(
+      {
+        className: "debug-target-item__icon",
+        src: this.getIcon(),
+      }
+    );
+  }
+
+  renderInfo() {
+    const name = this.getName();
+
+    return dom.div(
+      {
+        className: "debug-target-item__info",
+      },
+      dom.div(
+        {
+          className: "debug-target-item__info__name",
+          title: name,
+        },
+        name
+      ),
+      dom.div(
+        {
+          className: "debug-target-item__info__detail",
+        },
+        this.renderDetailComponents()
+      ),
+    );
+  }
+
+  render() {
+    return dom.li(
+      {
+        className: "debug-target-item",
+      },
+      this.renderIcon(),
+      this.renderInfo(),
+    );
+  }
+}
+
+module.exports = DebugTargetItem;
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging-new/src/components/debugtarget/DebugTargetList.css
@@ -0,0 +1,16 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.debug-target-list {
+  margin-block-end: 30px;
+}
+
+.debug-target-list__title {
+  margin-block-start: 10px;
+  margin-block-end: 10px;
+}
+
+.debug-target-list__ul {
+  margin-inline-start: 60px;
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging-new/src/components/debugtarget/DebugTargetList.js
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { PureComponent } = require("devtools/client/shared/vendor/react");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+class DebugTargetList extends PureComponent {
+  static get propTypes() {
+    return {
+      className: PropTypes.string.isRequired,
+      debugTargetItemComponent: PropTypes.any.isRequired,
+      debugTargets: PropTypes.arrayOf(PropTypes.object).isRequired,
+      title: PropTypes.string.isRequired,
+    };
+  }
+
+  render() {
+    const { className, debugTargetItemComponent, debugTargets, title } = this.props;
+
+    return dom.div(
+      {
+        className: `debug-target-list ${ className }`,
+      },
+      dom.h2(
+        {
+          className: `debug-target-list__title ${ className }__title`,
+        },
+        title
+      ),
+      dom.ul(
+        {
+          className: `debug-target-list__ul ${ className }__ul`,
+        },
+        debugTargets.map(debugTarget => debugTargetItemComponent({ debugTarget }))
+      )
+    );
+  }
+}
+
+module.exports = DebugTargetList;
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging-new/src/components/debugtarget/TabItem.js
@@ -0,0 +1,41 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+
+const DebugTargetItem = require("./DebugTargetItem");
+
+class TabItem extends DebugTargetItem {
+  getIcon() {
+    const { debugTarget } = this.props;
+
+    if (debugTarget.favicon) {
+      const base64Favicon = btoa(String.fromCharCode.apply(String, debugTarget.favicon));
+      return `data:image/png;base64,${ base64Favicon }`;
+    }
+
+    return "chrome://devtools/skin/images/globe.svg";
+  }
+
+  getName() {
+    const { debugTarget } = this.props;
+    // If the title is empty, display the url instead.
+    return debugTarget.title || debugTarget.url;
+  }
+
+  renderDetailComponents() {
+    const { debugTarget } = this.props;
+
+    return dom.div(
+      {
+        className: "debug-target-item__info__detail__tab-url",
+      },
+      debugTarget.url
+    );
+  }
+}
+
+module.exports = TabItem;
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging-new/src/components/debugtarget/moz.build
@@ -0,0 +1,11 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DevToolsModules(
+    'DebugTargetItem.css',
+    'DebugTargetItem.js',
+    'DebugTargetList.css',
+    'DebugTargetList.js',
+    'TabItem.js'
+)
--- a/devtools/client/aboutdebugging-new/src/components/moz.build
+++ b/devtools/client/aboutdebugging-new/src/components/moz.build
@@ -1,13 +1,14 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DIRS += [
+    'debugtarget',
     'runtime',
 ]
 
 DevToolsModules(
     'App.css',
     'App.js',
     'DebugTargetsPane.css',
     'DebugTargetsPane.js',
--- a/devtools/client/aboutdebugging-new/src/create-store.js
+++ b/devtools/client/aboutdebugging-new/src/create-store.js
@@ -1,18 +1,19 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-const { createStore } = require("devtools/client/shared/vendor/redux");
+const { applyMiddleware, createStore } = require("devtools/client/shared/vendor/redux");
+const { thunk } = require("devtools/client/shared/redux/middleware/thunk");
 
 const rootReducer = require("./reducers/index");
 const { RuntimesState } = require("./reducers/runtimes-state");
 
 exports.configureStore = function() {
   const initialState = {
     runtimes: new RuntimesState(),
   };
 
-  return createStore(rootReducer, initialState);
+  return createStore(rootReducer, initialState, applyMiddleware(thunk));
 };
--- a/devtools/client/aboutdebugging-new/src/runtimes/runtime.js
+++ b/devtools/client/aboutdebugging-new/src/runtimes/runtime.js
@@ -4,26 +4,43 @@
 
 "use strict";
 
 /**
  * This class represents a runtime, such as a remote Firefox.
  */
 class Runtime {
   /**
+   * Connect to this runtime.
+   * Subclass should override this method.
+   */
+  async connect() {
+    throw new Error("Subclass of Runtime should override connect()");
+  }
+
+  /**
    * Return this runtime's information.
    * Subclass should override this method.
    * @return {Object}
    *         {
    *           icon: URL of the icon which represents this runtime
    *                 e.g. chrome://branding/content/icon64.png
    *           name: Name of this runtime
    *                 e.g. Firefox
    *           version: Version of this runtime
    *                 e.g. 62.0a1
    *         }
    */
   async getRuntimeInfo() {
     throw new Error("Subclass of Runtime should override getRuntimeInfo()");
   }
+
+  /**
+   * Return tabs on this runtime.
+   * Subclass should override this method.
+   * @return {Array}
+   */
+  async getTabs() {
+    throw new Error("Subclass of Runtime should override getTabs()");
+  }
 }
 
 module.exports = Runtime;
--- a/devtools/client/aboutdebugging-new/src/runtimes/this-firefox.js
+++ b/devtools/client/aboutdebugging-new/src/runtimes/this-firefox.js
@@ -1,25 +1,41 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
+const { DebuggerClient } = require("devtools/shared/client/debugger-client");
+const { DebuggerServer } = require("devtools/server/main");
 const Services = require("Services");
 
 const Runtime = require("./runtime");
 
 /**
  * This class represents the Firefox instance which runs in the same environment that
  * opened about:debugging.
  */
 class ThisFirefox extends Runtime {
+  async connect() {
+    // Setup a server if we don't have one already running
+    DebuggerServer.init();
+    DebuggerServer.registerAllActors();
+    const transport = DebuggerServer.connectPipe();
+    this.client = new DebuggerClient(transport);
+    return this.client.connect();
+  }
+
   async getRuntimeInfo() {
     return {
       icon: "chrome://branding/content/icon64.png",
       name: Services.appinfo.name,
       version: Services.appinfo.version,
     };
   }
+
+  async getTabs() {
+    const { tabs } = await this.client.listTabs({ favicons: true });
+    return tabs;
+  }
 }
 
 module.exports = ThisFirefox;