Bug 1477598 - Part 2: Implement UI for extensions. r=jdescottes
authorDaisuke Akatsuka <dakatsuka@mozilla.com>
Fri, 10 Aug 2018 16:04:47 +0900
changeset 486281 72e1e5b24aaa8de32c254d19b68b70412414819d
parent 486280 a6ccfdee5d73e4acb85fdbfee7d78007ef659b82
child 486282 7e20f49730d2f249fa382479ec2a58f33bf91373
push id9719
push userffxbld-merge
push dateFri, 24 Aug 2018 17:49:46 +0000
treeherdermozilla-beta@719ec98fba77 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjdescottes
bugs1477598
milestone63.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1477598 - Part 2: Implement UI for extensions. r=jdescottes Differential Revision: https://phabricator.services.mozilla.com/D3084
devtools/client/aboutdebugging-new/aboutdebugging.css
devtools/client/aboutdebugging-new/src/components/DebugTargetItem.js
devtools/client/aboutdebugging-new/src/components/DebugTargetList.js
devtools/client/aboutdebugging-new/src/components/RuntimePage.js
devtools/client/aboutdebugging-new/src/components/debugtarget/ExtensionDetail.css
devtools/client/aboutdebugging-new/src/components/debugtarget/ExtensionDetail.js
devtools/client/aboutdebugging-new/src/components/debugtarget/TabDetail.js
devtools/client/aboutdebugging-new/src/components/debugtarget/moz.build
devtools/client/aboutdebugging-new/src/components/moz.build
devtools/client/aboutdebugging-new/src/constants.js
devtools/client/aboutdebugging-new/src/reducers/runtime-state.js
--- a/devtools/client/aboutdebugging-new/aboutdebugging.css
+++ b/devtools/client/aboutdebugging-new/aboutdebugging.css
@@ -1,29 +1,36 @@
 /* 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/. */
 
 @import "chrome://global/skin/in-content/common.css";
 @import "resource://devtools/client/aboutdebugging-new/src/components/App.css";
+@import "resource://devtools/client/aboutdebugging-new/src/components/ConnectPage.css";
 @import "resource://devtools/client/aboutdebugging-new/src/components/DebugTargetItem.css";
 @import "resource://devtools/client/aboutdebugging-new/src/components/DebugTargetList.css";
-@import "resource://devtools/client/aboutdebugging-new/src/components/ConnectPage.css";
 @import "resource://devtools/client/aboutdebugging-new/src/components/RuntimeInfo.css";
 @import "resource://devtools/client/aboutdebugging-new/src/components/Sidebar.css";
 @import "resource://devtools/client/aboutdebugging-new/src/components/SidebarItem.css";
+@import "resource://devtools/client/aboutdebugging-new/src/components/debugtarget/ExtensionDetail.css";
+@import "resource://devtools/client/aboutdebugging-new/src/components/debugtarget/TabDetail.css";
 
 :root {
   /* Import css variables from common.css */
   --text-color: var(--in-content-page-color);
 }
 
 html, body {
   margin: 0;
   padding: 0;
   color: var(--text-color);
 }
 
+dd {
+  margin: 0;
+  padding: 0;
+}
+
 ul {
   list-style: none;
   margin: 0;
   padding: 0;
 }
--- a/devtools/client/aboutdebugging-new/src/components/DebugTargetItem.js
+++ b/devtools/client/aboutdebugging-new/src/components/DebugTargetItem.js
@@ -1,65 +1,76 @@
 /* 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 { 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 ExtensionDetail = createFactory(require("./debugtarget/ExtensionDetail"));
+const TabDetail = createFactory(require("./debugtarget/TabDetail"));
+
 const Actions = require("../actions/index");
+const { DEBUG_TARGETS } = require("../constants");
 
 /**
  * This component displays debug target.
  */
 class DebugTargetItem extends PureComponent {
   static get propTypes() {
     return {
       dispatch: PropTypes.func.isRequired,
-      icon: PropTypes.string.isRequired,
-      id: PropTypes.string.isRequired,
-      name: PropTypes.string.isRequired,
-      type: PropTypes.string.isRequired,
-      url: PropTypes.string.isRequired,
+      target: PropTypes.object.isRequired,
     };
   }
 
   inspect() {
-    const { dispatch, type, id } = this.props;
-    dispatch(Actions.inspectDebugTarget(type, id));
+    const { dispatch, target } = this.props;
+    dispatch(Actions.inspectDebugTarget(target.type, target.id));
+  }
+
+  renderDetail() {
+    const { target } = this.props;
+
+    switch (target.type) {
+      case DEBUG_TARGETS.EXTENSION:
+        return ExtensionDetail({ target });
+      case DEBUG_TARGETS.TAB:
+        return TabDetail({ target });
+
+      default:
+        return null;
+    }
   }
 
   renderIcon() {
     return dom.img({
       className: "debug-target-item__icon",
-      src: this.props.icon,
+      src: this.props.target.icon,
     });
   }
 
   renderInfo() {
+    const { target } = this.props;
+
     return dom.div(
       {
         className: "debug-target-item__info",
       },
       dom.div(
         {
           className: "debug-target-item__info__name ellipsis-text",
-          title: this.props.name,
+          title: target.name,
         },
-        this.props.name
+        target.name
       ),
-      dom.div(
-        {
-          className: "debug-target-item__info__url ellipsis-text",
-        },
-        this.props.url
-      ),
+      this.renderDetail(),
     );
   }
 
   renderInspectButton() {
     return dom.button(
       {
         onClick: e => this.inspect(),
         className: "debug-target-item__inspect-button",
--- a/devtools/client/aboutdebugging-new/src/components/DebugTargetList.js
+++ b/devtools/client/aboutdebugging-new/src/components/DebugTargetList.js
@@ -12,34 +12,25 @@ const DebugTargetItem = createFactory(re
 
 /**
  * This component displays list of debug target.
  */
 class DebugTargetList extends PureComponent {
   static get propTypes() {
     return {
       dispatch: PropTypes.func.isRequired,
-      targets: PropTypes.arrayOf(PropTypes.Object).isRequired,
+      targets: PropTypes.arrayOf(PropTypes.object).isRequired,
     };
   }
 
   render() {
     const { dispatch, targets } = this.props;
 
     return dom.ul(
       {
         className: "debug-target-list",
       },
-      targets.map(target =>
-        DebugTargetItem({
-          dispatch,
-          icon: target.icon,
-          id: target.id,
-          name: target.name,
-          type: target.type,
-          url: target.url,
-        })
-      ),
+      targets.map(target => DebugTargetItem({ dispatch, target })),
     );
   }
 }
 
 module.exports = DebugTargetList;
--- a/devtools/client/aboutdebugging-new/src/components/RuntimePage.js
+++ b/devtools/client/aboutdebugging-new/src/components/RuntimePage.js
@@ -33,28 +33,29 @@ class RuntimePage extends PureComponent 
       },
       RuntimeInfo({
         icon: "chrome://branding/content/icon64.png",
         name: Services.appinfo.name,
         version: Services.appinfo.version,
       }),
       DebugTargetPane({
         dispatch,
+        name: "Temporary Extensions",
+        targets: temporaryExtensions,
+      }),
+      DebugTargetPane({
+        dispatch,
+        name: "Extensions",
+        targets: installedExtensions,
+      }),
+      DebugTargetPane({
+        dispatch,
         name: "Tabs",
         targets: tabs
       }),
-      // Temporary implementation
-      dom.ul(
-        {},
-        temporaryExtensions.map(e => dom.li({}, e.name))
-      ),
-      dom.ul(
-        {},
-        installedExtensions.map(e => dom.li({}, e.name))
-      ),
     );
   }
 }
 
 const mapStateToProps = state => {
   return {
     installedExtensions: state.runtime.installedExtensions,
     tabs: state.runtime.tabs,
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging-new/src/components/debugtarget/ExtensionDetail.css
@@ -0,0 +1,25 @@
+/* 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/. */
+
+/*
+ * The current layout of extension detail is
+ *
+ *  +----------------+--------------------+
+ *  | detail name dt | detail value dd    |
+ *  | (120px)        | (auto)             |
+ *  +----------------+--------------------+
+ *  | detail name dt | detail value dd    |
+ *  +----------------+--------------------+
+ *  | detail name dt | detail value dd    |
+ *  +----------------+--------------------+
+ */
+.extension-detail {
+  display: grid;
+  grid-template-columns: 120px auto;
+  margin-block-start: 4px;
+}
+
+.extension-detail__manifest {
+  margin-inline-start: 1ch;
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging-new/src/components/debugtarget/ExtensionDetail.js
@@ -0,0 +1,68 @@
+/* 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 displays detail information for extension.
+ */
+class ExtensionDetail extends PureComponent {
+  static get propTypes() {
+    return {
+      target: PropTypes.object.isRequired,
+    };
+  }
+
+  renderField(name, value, title) {
+    return [
+      dom.dt({}, name),
+      dom.dd(
+        {
+          className: "ellipsis-text",
+          title: title || value,
+        },
+        value,
+      ),
+    ];
+  }
+
+  renderUUID() {
+    const { target } = this.props;
+    const { manifestURL, uuid } = target;
+
+    const value = [
+      uuid,
+      dom.a(
+        {
+          className: "extension-detail__manifest",
+          href: manifestURL,
+          target: "_blank",
+        },
+        "Manifest URL",
+      )
+    ];
+
+    return this.renderField("Internal UUID", value, uuid);
+  }
+
+  render() {
+    const { target } = this.props;
+    const { id, location, uuid } = target;
+
+    return dom.dl(
+      {
+        className: "extension-detail",
+      },
+      location ? this.renderField("Location", location) : null,
+      this.renderField("Extension ID", id),
+      uuid ? this.renderUUID() : null,
+    );
+  }
+}
+
+module.exports = ExtensionDetail;
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging-new/src/components/debugtarget/TabDetail.js
@@ -0,0 +1,26 @@
+/* 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 displays detail information for tab.
+ */
+class TabDetail extends PureComponent {
+  static get propTypes() {
+    return {
+      target: PropTypes.object.isRequired,
+    };
+  }
+
+  render() {
+    return dom.div({ className: "ellipsis-text" }, this.props.target.url);
+  }
+}
+
+module.exports = TabDetail;
new file mode 100644
--- /dev/null
+++ b/devtools/client/aboutdebugging-new/src/components/debugtarget/moz.build
@@ -0,0 +1,9 @@
+# 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(
+    'ExtensionDetail.css',
+    'ExtensionDetail.js',
+    'TabDetail.js',
+)
--- a/devtools/client/aboutdebugging-new/src/components/moz.build
+++ b/devtools/client/aboutdebugging-new/src/components/moz.build
@@ -1,12 +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/.
 
+DIRS += [
+    'debugtarget',
+]
+
 DevToolsModules(
     'App.css',
     'App.js',
     'ConnectPage.css',
     'ConnectPage.js',
     'DebugTargetItem.css',
     'DebugTargetItem.js',
     'DebugTargetList.css',
--- a/devtools/client/aboutdebugging-new/src/constants.js
+++ b/devtools/client/aboutdebugging-new/src/constants.js
@@ -16,16 +16,17 @@ const actionTypes = {
   REQUEST_EXTENSIONS_SUCCESS: "REQUEST_EXTENSIONS_SUCCESS",
   REQUEST_TABS_FAILURE: "REQUEST_TABS_FAILURE",
   REQUEST_TABS_START: "REQUEST_TABS_START",
   REQUEST_TABS_SUCCESS: "REQUEST_TABS_SUCCESS",
   PAGE_SELECTED: "PAGE_SELECTED",
 };
 
 const DEBUG_TARGETS = {
+  EXTENSION: "EXTENSION",
   TAB: "TAB",
 };
 
 const PAGES = {
   THIS_FIREFOX: "this-firefox",
   CONNECT: "connect",
 };
 
--- a/devtools/client/aboutdebugging-new/src/reducers/runtime-state.js
+++ b/devtools/client/aboutdebugging-new/src/reducers/runtime-state.js
@@ -27,28 +27,62 @@ function runtimeReducer(state = RuntimeS
       const { client } = action;
       return Object.assign({}, state, { client });
     }
     case DISCONNECT_RUNTIME_SUCCESS: {
       return RuntimeState();
     }
     case REQUEST_EXTENSIONS_SUCCESS: {
       const { installedExtensions, temporaryExtensions } = action;
-      return Object.assign({}, state, { installedExtensions, temporaryExtensions });
+      return Object.assign({}, state, {
+        installedExtensions: toExtensionComponentData(installedExtensions),
+        temporaryExtensions: toExtensionComponentData(temporaryExtensions),
+      });
     }
     case REQUEST_TABS_SUCCESS: {
       const { tabs } = action;
       return Object.assign({}, state, { tabs: toTabComponentData(tabs) });
     }
 
     default:
       return state;
   }
 }
 
+function getExtensionFilePath(extension) {
+  // Only show file system paths, and only for temporarily installed add-ons.
+  if (!extension.temporarilyInstalled ||
+      !extension.url ||
+      !extension.url.startsWith("file://")) {
+    return null;
+  }
+
+  // Strip a leading slash from Windows drive letter URIs.
+  // file:///home/foo ~> /home/foo
+  // file:///C:/foo ~> C:/foo
+  const windowsRegex = /^file:\/\/\/([a-zA-Z]:\/.*)/;
+
+  if (windowsRegex.test(extension.url)) {
+    return windowsRegex.exec(extension.url)[1];
+  }
+
+  return extension.url.slice("file://".length);
+}
+
+function toExtensionComponentData(extensions) {
+  return extensions.map(extension => {
+    const type = DEBUG_TARGETS.EXTENSION;
+    const { iconURL, id, manifestURL, name } = extension;
+    const icon = iconURL || "chrome://mozapps/skin/extensions/extensionGeneric.svg";
+    const location = getExtensionFilePath(extension);
+    const uuid = manifestURL ? /moz-extension:\/\/([^/]*)/.exec(manifestURL)[1] : null;
+    return { type, id, icon, location, manifestURL, name, uuid };
+  });
+}
+
 function toTabComponentData(tabs) {
   return tabs.map(tab => {
     const type = DEBUG_TARGETS.TAB;
     const id = tab.outerWindowID;
     const icon = tab.favicon
       ? `data:image/png;base64,${ btoa(String.fromCharCode.apply(String, tab.favicon)) }`
       : "chrome://devtools/skin/images/globe.svg";
     const name = tab.title || tab.url;