DevTools: Revamp Audits2 UI

- Restructures Audits2 file organization
- Revamps design to match latest mocks

Change-Id: I302ff348ae939b4e48e7b79883fe77ffb97825cf
Reviewed-on: https://chromium-review.googlesource.com/1058708
Commit-Queue: Patrick Hulce <phulce@chromium.org>
Reviewed-by: Paul Irish <paulirish@chromium.org>
Reviewed-by: Dmitry Gozman <dgozman@chromium.org>
Cr-Original-Commit-Position: refs/heads/master@{#559754}
Cr-Mirrored-From: https://chromium.googlesource.com/chromium/src
Cr-Mirrored-Commit: 3f2dc028ad0011e5d68b17bf41870a9296181f28
diff --git a/front_end/audits2/Audits2Controller.js b/front_end/audits2/Audits2Controller.js
new file mode 100644
index 0000000..6a58490
--- /dev/null
+++ b/front_end/audits2/Audits2Controller.js
@@ -0,0 +1,285 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @implements {SDK.SDKModelObserver<!SDK.ServiceWorkerManager>}
+ * @unrestricted
+ */
+Audits2.AuditController = class extends Common.Object {
+  constructor(protocolService) {
+    super();
+
+    protocolService.registerStatusCallback(
+        message => this.dispatchEventToListeners(Audits2.Events.AuditProgressChanged, {message}));
+
+    for (const preset of Audits2.Presets)
+      preset.setting.addChangeListener(this.recomputePageAuditability.bind(this));
+
+    SDK.targetManager.observeModels(SDK.ServiceWorkerManager, this);
+    SDK.targetManager.addEventListener(
+        SDK.TargetManager.Events.InspectedURLChanged, this.recomputePageAuditability, this);
+  }
+
+  /**
+   * @override
+   * @param {!SDK.ServiceWorkerManager} serviceWorkerManager
+   */
+  modelAdded(serviceWorkerManager) {
+    if (this._manager)
+      return;
+
+    this._manager = serviceWorkerManager;
+    this._serviceWorkerListeners = [
+      this._manager.addEventListener(
+          SDK.ServiceWorkerManager.Events.RegistrationUpdated, this.recomputePageAuditability, this),
+      this._manager.addEventListener(
+          SDK.ServiceWorkerManager.Events.RegistrationDeleted, this.recomputePageAuditability, this),
+    ];
+
+    this.recomputePageAuditability();
+  }
+
+  /**
+   * @override
+   * @param {!SDK.ServiceWorkerManager} serviceWorkerManager
+   */
+  modelRemoved(serviceWorkerManager) {
+    if (this._manager !== serviceWorkerManager)
+      return;
+
+    Common.EventTarget.removeEventListeners(this._serviceWorkerListeners);
+    this._manager = null;
+    this.recomputePageAuditability();
+  }
+
+  /**
+   * @return {boolean}
+   */
+  _hasActiveServiceWorker() {
+    if (!this._manager)
+      return false;
+
+    const mainTarget = this._manager.target();
+    if (!mainTarget)
+      return false;
+
+    const inspectedURL = mainTarget.inspectedURL().asParsedURL();
+    const inspectedOrigin = inspectedURL && inspectedURL.securityOrigin();
+    for (const registration of this._manager.registrations().values()) {
+      if (registration.securityOrigin !== inspectedOrigin)
+        continue;
+
+      for (const version of registration.versions.values()) {
+        if (version.controlledClients.length > 1)
+          return true;
+      }
+    }
+
+    return false;
+  }
+
+  /**
+   * @return {boolean}
+   */
+  _hasAtLeastOneCategory() {
+    return Audits2.Presets.some(preset => preset.setting.get());
+  }
+
+  /**
+   * @return {?string}
+   */
+  _unauditablePageMessage() {
+    if (!this._manager)
+      return null;
+
+    const mainTarget = this._manager.target();
+    const inspectedURL = mainTarget && mainTarget.inspectedURL();
+    if (inspectedURL && !/^(http|chrome-extension)/.test(inspectedURL)) {
+      return Common.UIString(
+          'Can only audit HTTP/HTTPS pages and Chrome extensions. ' +
+          'Navigate to a different page to start an audit.');
+    }
+
+    // Audits don't work on most undockable targets (extension popup pages, remote debugging, etc).
+    // However, the tests run in a content shell which is not dockable yet audits just fine,
+    // so disable this check when under test.
+    if (!Host.isUnderTest() && !Runtime.queryParam('can_dock'))
+      return Common.UIString('Can only audit tabs. Navigate to this page in a separate tab to start an audit.');
+
+    return null;
+  }
+
+  /**
+   * @return {!Promise<string>}
+   */
+  async _evaluateInspectedURL() {
+    const mainTarget = this._manager.target();
+    const runtimeModel = mainTarget.model(SDK.RuntimeModel);
+    const executionContext = runtimeModel && runtimeModel.defaultExecutionContext();
+    let inspectedURL = mainTarget.inspectedURL();
+    if (!executionContext)
+      return inspectedURL;
+
+    // Evaluate location.href for a more specific URL than inspectedURL provides so that SPA hash navigation routes
+    // will be respected and audited.
+    try {
+      const result = await executionContext.evaluate(
+          {
+            expression: 'window.location.href',
+            objectGroup: 'audits',
+            includeCommandLineAPI: false,
+            silent: false,
+            returnByValue: true,
+            generatePreview: false
+          },
+          /* userGesture */ false, /* awaitPromise */ false);
+      if (!result.exceptionDetails && result.object) {
+        inspectedURL = result.object.value;
+        result.object.release();
+      }
+    } catch (err) {
+      console.error(err);
+    }
+
+    return inspectedURL;
+  }
+
+  /**
+   * @return {!Object}
+   */
+  getFlags() {
+    const flags = {};
+    for (const runtimeSetting of Audits2.RuntimeSettings)
+      runtimeSetting.setFlags(flags, runtimeSetting.setting.get());
+    return flags;
+  }
+
+  /**
+   * @return {!Array<string>}
+   */
+  getCategoryIDs() {
+    const categoryIDs = [];
+    for (const preset of Audits2.Presets) {
+      if (preset.setting.get())
+        categoryIDs.push(preset.configID);
+    }
+    return categoryIDs;
+  }
+
+  /**
+   * @param {{force: boolean}=} options
+   * @return {!Promise<string>}
+   */
+  async getInspectedURL(options) {
+    if (options && options.force || !this._inspectedURL)
+      this._inspectedURL = await this._evaluateInspectedURL();
+    return this._inspectedURL;
+  }
+
+  recomputePageAuditability() {
+    const hasActiveServiceWorker = this._hasActiveServiceWorker();
+    const hasAtLeastOneCategory = this._hasAtLeastOneCategory();
+    const unauditablePageMessage = this._unauditablePageMessage();
+
+    let helpText = '';
+    if (hasActiveServiceWorker) {
+      helpText = Common.UIString(
+          'Multiple tabs are being controlled by the same service worker. ' +
+          'Close your other tabs on the same origin to audit this page.');
+    } else if (!hasAtLeastOneCategory) {
+      helpText = Common.UIString('At least one category must be selected.');
+    } else if (unauditablePageMessage) {
+      helpText = unauditablePageMessage;
+    }
+
+    this.dispatchEventToListeners(Audits2.Events.PageAuditabilityChanged, {helpText});
+  }
+};
+
+
+/** @typedef {{type: string, setting: !Common.Setting, configID: string, title: string, description: string}} */
+Audits2.Preset;
+
+/** @type {!Array.<!Audits2.Preset>} */
+Audits2.Presets = [
+  // configID maps to Lighthouse's Object.keys(config.categories)[0] value
+  {
+    setting: Common.settings.createSetting('audits2.cat_perf', true),
+    configID: 'performance',
+    title: 'Performance',
+    description: 'How long does this app take to show content and become usable'
+  },
+  {
+    setting: Common.settings.createSetting('audits2.cat_pwa', true),
+    configID: 'pwa',
+    title: 'Progressive Web App',
+    description: 'Does this page meet the standard of a Progressive Web App'
+  },
+  {
+    setting: Common.settings.createSetting('audits2.cat_best_practices', true),
+    configID: 'best-practices',
+    title: 'Best practices',
+    description: 'Does this page follow best practices for modern web development'
+  },
+  {
+    setting: Common.settings.createSetting('audits2.cat_a11y', true),
+    configID: 'accessibility',
+    title: 'Accessibility',
+    description: 'Is this page usable by people with disabilities or impairments'
+  },
+  {
+    setting: Common.settings.createSetting('audits2.cat_seo', true),
+    configID: 'seo',
+    title: 'SEO',
+    description: 'Is this page optimized for search engine results ranking'
+  },
+];
+
+/** @typedef {{setting: !Common.Setting, description: string, setFlags: function(!Object, string), options: !Array}} */
+Audits2.RuntimeSetting;
+
+/** @type {!Array.<!Audits2.RuntimeSetting>} */
+Audits2.RuntimeSettings = [
+  {
+    setting: Common.settings.createSetting('audits2.device_type', 'mobile'),
+    description: Common.UIString('Apply mobile emulation during auditing'),
+    setFlags: (flags, value) => {
+      flags.disableDeviceEmulation = value === 'desktop';
+    },
+    options: [
+      {label: Common.UIString('Mobile'), value: 'mobile'},
+      {label: Common.UIString('Desktop'), value: 'desktop'},
+    ],
+  },
+  {
+    setting: Common.settings.createSetting('audits2.throttling', 'default'),
+    description: Common.UIString('Apply network and CPU throttling during performance auditing'),
+    setFlags: (flags, value) => {
+      flags.disableNetworkThrottling = value === 'off';
+      flags.disableCpuThrottling = value === 'off';
+    },
+    options: [
+      {label: Common.UIString('3G w/ CPU slowdown'), value: 'default'},
+      {label: Common.UIString('No throttling'), value: 'off'},
+    ],
+  },
+  {
+    setting: Common.settings.createSetting('audits2.storage_reset', 'on'),
+    description: Common.UIString('Reset storage (localStorage, IndexedDB, etc) to a clean baseline before auditing'),
+    setFlags: (flags, value) => {
+      flags.disableStorageReset = value === 'off';
+    },
+    options: [
+      {label: Common.UIString('Clear storage'), value: 'on'},
+      {label: Common.UIString('Preserve storage'), value: 'off'},
+    ],
+  },
+];
+
+Audits2.Events = {
+  PageAuditabilityChanged: Symbol('PageAuditabilityChanged'),
+  AuditProgressChanged: Symbol('AuditProgressChanged'),
+  RequestAuditStart: Symbol('RequestAuditStart'),
+  RequestAuditCancel: Symbol('RequestAuditCancel'),
+};
\ No newline at end of file
diff --git a/front_end/audits2/Audits2Dialog.js b/front_end/audits2/Audits2Dialog.js
deleted file mode 100644
index ecee4da..0000000
--- a/front_end/audits2/Audits2Dialog.js
+++ /dev/null
@@ -1,262 +0,0 @@
-// Copyright 2018 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-/**
- * @unrestricted
- */
-Audits2.Audits2Dialog = class {
-  constructor(onAuditComplete, protocolService) {
-    this._onAuditComplete = onAuditComplete;
-    this._protocolService = protocolService;
-    this._protocolService.registerStatusCallback(msg => this._updateStatus(Common.UIString(msg)));
-  }
-
-  /**
-   * @param {!Element} dialogRenderElement
-   */
-  render(dialogRenderElement) {
-    this._dialog = new UI.Dialog();
-    this._dialog.setOutsideClickCallback(event => event.consume(true));
-    const root = UI.createShadowRootWithCoreStyles(this._dialog.contentElement, 'audits2/audits2Dialog.css');
-    const auditsViewElement = root.createChild('div', 'audits2-view vbox');
-
-    const closeButton = auditsViewElement.createChild('div', 'dialog-close-button', 'dt-close-button');
-    closeButton.addEventListener('click', () => this._cancelAndClose());
-
-    const uiElement = auditsViewElement.createChild('div', 'vbox launcher-container');
-    const headerElement = uiElement.createChild('header');
-    this._headerTitleElement = headerElement.createChild('p');
-    this._headerTitleElement.textContent = Common.UIString('Audits to perform');
-    uiElement.appendChild(headerElement);
-
-    this._auditSelectorForm = uiElement.createChild('form', 'audits2-form');
-
-    for (const preset of Audits2.Audits2Panel.Presets) {
-      preset.setting.setTitle(preset.title);
-      const checkbox = new UI.ToolbarSettingCheckbox(preset.setting);
-      const row = this._auditSelectorForm.createChild('div', 'vbox audits2-launcher-row');
-      row.appendChild(checkbox.element);
-      row.createChild('span', 'audits2-launcher-description dimmed').textContent = preset.description;
-    }
-
-    this._statusView = new Audits2.Audits2StatusView();
-    this._statusView.render(uiElement);
-    this._dialogHelpText = uiElement.createChild('div', 'audits2-dialog-help-text');
-
-    const buttonsRow = uiElement.createChild('div', 'audits2-dialog-buttons hbox');
-    this._startButton =
-        UI.createTextButton(Common.UIString('Run audit'), this._start.bind(this), '', true /* primary */);
-    buttonsRow.appendChild(this._startButton);
-    this._cancelButton = UI.createTextButton(Common.UIString('Cancel'), this._cancel.bind(this));
-    buttonsRow.appendChild(this._cancelButton);
-
-    auditsViewElement.tabIndex = 0;
-    this._dialog.setDefaultFocusedElement(this._startButton);
-    this._dialog.setSizeBehavior(UI.GlassPane.SizeBehavior.SetExactWidthMaxHeight);
-    this._dialog.setMaxContentSize(new UI.Size(500, 400));
-    this._dialog.show(dialogRenderElement);
-  }
-
-  hide() {
-    if (!this._dialog)
-      return;
-
-    this._dialog.hide();
-
-    delete this._dialog;
-    delete this._statusView;
-    delete this._startButton;
-    delete this._cancelButton;
-    delete this._auditSelectorForm;
-    delete this._headerTitleElement;
-    delete this._emulationEnabledBefore;
-    delete this._emulationOutlineEnabledBefore;
-  }
-
-  /**
-   * @param {boolean} isEnabled
-   */
-  setStartEnabled(isEnabled) {
-    if (this._dialogHelpText)
-      this._dialogHelpText.classList.toggle('hidden', isEnabled);
-
-    if (this._startButton)
-      this._startButton.disabled = !isEnabled;
-  }
-
-  /**
-   * @param {string} text
-   */
-  setHelpText(text) {
-    if (this._dialogHelpText)
-      this._dialogHelpText.textContent = text;
-  }
-
-  /**
-   * @param {string} message
-   */
-  _updateStatus(message) {
-    if (!this._statusView)
-      return;
-    this._statusView.updateStatus(message);
-  }
-
-  _cancelAndClose() {
-    this._cancel();
-    this.hide();
-  }
-
-  async _cancel() {
-    if (this._auditRunning) {
-      this._updateStatus(Common.UIString('Cancelling\u2026'));
-      await this._stopAndReattach();
-
-      if (this._statusView)
-        this._statusView.reset();
-    } else {
-      this.hide();
-    }
-  }
-
-  /**
-   * @return {!Promise<undefined>}
-   */
-  _getInspectedURL() {
-    const mainTarget = SDK.targetManager.mainTarget();
-    const runtimeModel = mainTarget.model(SDK.RuntimeModel);
-    const executionContext = runtimeModel && runtimeModel.defaultExecutionContext();
-    let inspectedURL = mainTarget.inspectedURL();
-    if (!executionContext)
-      return Promise.resolve(inspectedURL);
-
-    // Evaluate location.href for a more specific URL than inspectedURL provides so that SPA hash navigation routes
-    // will be respected and audited.
-    return executionContext
-        .evaluate(
-            {
-              expression: 'window.location.href',
-              objectGroup: 'audits',
-              includeCommandLineAPI: false,
-              silent: false,
-              returnByValue: true,
-              generatePreview: false
-            },
-            /* userGesture */ false, /* awaitPromise */ false)
-        .then(result => {
-          if (!result.exceptionDetails && result.object) {
-            inspectedURL = result.object.value;
-            result.object.release();
-          }
-
-          return inspectedURL;
-        });
-  }
-
-  /**
-   * @return {!Object}
-   */
-  _getFlags() {
-    const flags = {};
-    for (const runtimeSetting of Audits2.Audits2Panel.RuntimeSettings)
-      runtimeSetting.setFlags(flags, runtimeSetting.setting.get());
-    return flags;
-  }
-
-  async _start() {
-    const flags = this._getFlags();
-
-    const emulationModel = self.singleton(Emulation.DeviceModeModel);
-    this._emulationEnabledBefore = emulationModel.enabledSetting().get();
-    this._emulationOutlineEnabledBefore = emulationModel.deviceOutlineSetting().get();
-    emulationModel.toolbarControlsEnabledSetting().set(false);
-
-    if (flags.disableDeviceEmulation) {
-      emulationModel.enabledSetting().set(false);
-      emulationModel.deviceOutlineSetting().set(false);
-      emulationModel.emulate(Emulation.DeviceModeModel.Type.None, null, null);
-    } else {
-      emulationModel.enabledSetting().set(true);
-      emulationModel.deviceOutlineSetting().set(true);
-
-      for (const device of Emulation.EmulatedDevicesList.instance().standard()) {
-        if (device.title === 'Nexus 5X')
-          emulationModel.emulate(Emulation.DeviceModeModel.Type.Device, device, device.modes[0], 1);
-      }
-    }
-
-    this._dialog.setCloseOnEscape(false);
-
-    const categoryIDs = [];
-    for (const preset of Audits2.Audits2Panel.Presets) {
-      if (preset.setting.get())
-        categoryIDs.push(preset.configID);
-    }
-    Host.userMetrics.actionTaken(Host.UserMetrics.Action.Audits2Started);
-
-    try {
-      this._auditURL = await this._getInspectedURL();
-      await this._protocolService.attach();
-
-      this._auditRunning = true;
-      this._updateButton(this._auditURL);
-      this._updateStatus(Common.UIString('Loading\u2026'));
-
-      const lighthouseResult = await this._protocolService.startLighthouse(this._auditURL, categoryIDs, flags);
-      if (lighthouseResult && lighthouseResult.fatal) {
-        const error = new Error(lighthouseResult.message);
-        error.stack = lighthouseResult.stack;
-        throw error;
-      }
-
-      if (!lighthouseResult)
-        throw new Error('Auditing failed to produce a result');
-
-      await this._stopAndReattach();
-      await this._onAuditComplete(lighthouseResult);
-    } catch (err) {
-      if (err instanceof Error)
-        this._statusView.renderBugReport(err, this._auditURL);
-    }
-  }
-
-  /**
-   * @return {!Promise<undefined>}
-   */
-  async _stopAndReattach() {
-    await this._protocolService.detach();
-
-    const emulationModel = self.singleton(Emulation.DeviceModeModel);
-    emulationModel.enabledSetting().set(this._emulationEnabledBefore);
-    emulationModel.deviceOutlineSetting().set(this._emulationOutlineEnabledBefore);
-    emulationModel.toolbarControlsEnabledSetting().set(true);
-    Emulation.InspectedPagePlaceholder.instance().update(true);
-
-    Host.userMetrics.actionTaken(Host.UserMetrics.Action.Audits2Finished);
-    const resourceTreeModel = SDK.targetManager.mainTarget().model(SDK.ResourceTreeModel);
-    // reload to reset the page state
-    await resourceTreeModel.navigate(this._auditURL);
-    this._auditRunning = false;
-    this._updateButton(this._auditURL);
-  }
-
-  /**
-   * @param {string} auditURL
-   */
-  _updateButton(auditURL) {
-    if (!this._dialog)
-      return;
-    this._startButton.classList.toggle('hidden', this._auditRunning);
-    this._startButton.disabled = this._auditRunning;
-    this._statusView.setVisible(this._auditRunning);
-    this._auditSelectorForm.classList.toggle('hidden', this._auditRunning);
-    if (this._auditRunning) {
-      const parsedURL = (auditURL || '').asParsedURL();
-      const pageHost = parsedURL && parsedURL.host;
-      this._headerTitleElement.textContent =
-          pageHost ? ls`Auditing ${pageHost}\u2026` : ls`Auditing your web page\u2026`;
-    } else {
-      this._headerTitleElement.textContent = Common.UIString('Audits to perform');
-    }
-  }
-};
diff --git a/front_end/audits2/Audits2Panel.js b/front_end/audits2/Audits2Panel.js
index cb97841..012d222 100644
--- a/front_end/audits2/Audits2Panel.js
+++ b/front_end/audits2/Audits2Panel.js
@@ -3,7 +3,6 @@
 // found in the LICENSE file.
 
 /**
- * @implements {SDK.SDKModelObserver<!SDK.ServiceWorkerManager>}
  * @unrestricted
  */
 Audits2.Audits2Panel = class extends UI.Panel {
@@ -13,132 +12,43 @@
     this.registerRequiredCSS('audits2/audits2Panel.css');
 
     this._protocolService = new Audits2.ProtocolService();
+    this._controller = new Audits2.AuditController(this._protocolService);
+    this._startView = new Audits2.StartView(this._controller);
+    this._statusView = new Audits2.StatusView(this._controller);
 
-    this._renderToolbar();
+    this._unauditableExplanation = null;
+    this._cachedRenderedReports = new Map();
 
-    this._auditResultsElement = this.contentElement.createChild('div', 'audits2-results-container');
     this._dropTarget = new UI.DropTarget(
         this.contentElement, [UI.DropTarget.Type.File], Common.UIString('Drop audit file here'),
         this._handleDrop.bind(this));
 
-    for (const preset of Audits2.Audits2Panel.Presets)
-      preset.setting.addChangeListener(this._refreshDialogUI.bind(this));
+    this._controller.addEventListener(Audits2.Events.PageAuditabilityChanged, this._refreshStartAuditUI.bind(this));
+    this._controller.addEventListener(Audits2.Events.AuditProgressChanged, this._refreshStatusUI.bind(this));
+    this._controller.addEventListener(Audits2.Events.RequestAuditStart, this._startAudit.bind(this));
+    this._controller.addEventListener(Audits2.Events.RequestAuditCancel, this._cancelAudit.bind(this));
 
-    this._showLandingPage();
-    SDK.targetManager.observeModels(SDK.ServiceWorkerManager, this);
-    SDK.targetManager.addEventListener(SDK.TargetManager.Events.InspectedURLChanged, this._refreshDialogUI, this);
+    this._renderToolbar();
+    this._auditResultsElement = this.contentElement.createChild('div', 'audits2-results-container');
+    this._renderStartView();
+
+    this._controller.recomputePageAuditability();
   }
 
   /**
-   * @override
-   * @param {!SDK.ServiceWorkerManager} serviceWorkerManager
+   * @param {!Common.Event} evt
    */
-  modelAdded(serviceWorkerManager) {
-    if (this._manager)
-      return;
-
-    this._manager = serviceWorkerManager;
-    this._serviceWorkerListeners = [
-      this._manager.addEventListener(SDK.ServiceWorkerManager.Events.RegistrationUpdated, this._refreshDialogUI, this),
-      this._manager.addEventListener(SDK.ServiceWorkerManager.Events.RegistrationDeleted, this._refreshDialogUI, this),
-    ];
-
-    this._refreshDialogUI();
+  _refreshStartAuditUI(evt) {
+    this._unauditableExplanation = evt.data.helpText;
+    this._startView.setUnauditableExplanation(evt.data.helpText);
+    this._startView.setStartButtonEnabled(!evt.data.helpText);
   }
 
   /**
-   * @override
-   * @param {!SDK.ServiceWorkerManager} serviceWorkerManager
+   * @param {!Common.Event} evt
    */
-  modelRemoved(serviceWorkerManager) {
-    if (!this._manager || this._manager !== serviceWorkerManager)
-      return;
-
-    Common.EventTarget.removeEventListeners(this._serviceWorkerListeners);
-    this._manager = null;
-    this._serviceWorkerListeners = null;
-    this._refreshDialogUI();
-  }
-
-  /**
-   * @return {boolean}
-   */
-  _hasActiveServiceWorker() {
-    if (!this._manager)
-      return false;
-
-    const mainTarget = SDK.targetManager.mainTarget();
-    if (!mainTarget)
-      return false;
-
-    const inspectedURL = mainTarget.inspectedURL().asParsedURL();
-    const inspectedOrigin = inspectedURL && inspectedURL.securityOrigin();
-    for (const registration of this._manager.registrations().values()) {
-      if (registration.securityOrigin !== inspectedOrigin)
-        continue;
-
-      for (const version of registration.versions.values()) {
-        if (version.controlledClients.length > 1)
-          return true;
-      }
-    }
-
-    return false;
-  }
-
-  /**
-   * @return {boolean}
-   */
-  _hasAtLeastOneCategory() {
-    return Audits2.Audits2Panel.Presets.some(preset => preset.setting.get());
-  }
-
-  /**
-   * @return {?string}
-   */
-  _unauditablePageMessage() {
-    if (!this._manager)
-      return null;
-
-    const mainTarget = SDK.targetManager.mainTarget();
-    const inspectedURL = mainTarget && mainTarget.inspectedURL();
-    if (inspectedURL && !/^(http|chrome-extension)/.test(inspectedURL)) {
-      return Common.UIString(
-          'Can only audit HTTP/HTTPS pages and Chrome extensions. ' +
-          'Navigate to a different page to start an audit.');
-    }
-
-    // Audits don't work on most undockable targets (extension popup pages, remote debugging, etc).
-    // However, the tests run in a content shell which is not dockable yet audits just fine,
-    // so disable this check when under test.
-    if (!Host.isUnderTest() && !Runtime.queryParam('can_dock'))
-      return Common.UIString('Can only audit tabs. Navigate to this page in a separate tab to start an audit.');
-
-    return null;
-  }
-
-  _refreshDialogUI() {
-    if (!this._dialog)
-      return;
-
-    const hasActiveServiceWorker = this._hasActiveServiceWorker();
-    const hasAtLeastOneCategory = this._hasAtLeastOneCategory();
-    const unauditablePageMessage = this._unauditablePageMessage();
-    const isDisabled = hasActiveServiceWorker || !hasAtLeastOneCategory || !!unauditablePageMessage;
-
-    let helpText = '';
-    if (hasActiveServiceWorker) {
-      helpText = Common.UIString(
-          'Multiple tabs are being controlled by the same service worker. ' +
-          'Close your other tabs on the same origin to audit this page.');
-    } else if (!hasAtLeastOneCategory) {
-      helpText = Common.UIString('At least one category must be selected.');
-    } else if (unauditablePageMessage) {
-      helpText = unauditablePageMessage;
-    }
-
-    this._dialog.setHelpText(helpText);
-    this._dialog.setStartEnabled(!isDisabled);
+  _refreshStatusUI(evt) {
+    this._statusView.updateStatus(evt.data.message);
   }
 
   _refreshToolbarUI() {
@@ -148,7 +58,7 @@
 
   _clearAll() {
     this._reportSelector.clearAll();
-    this._showLandingPage();
+    this._renderStartView();
     this._refreshToolbarUI();
   }
 
@@ -161,7 +71,7 @@
 
     this._newButton = new UI.ToolbarButton(Common.UIString('Perform an audit\u2026'), 'largeicon-add');
     toolbar.appendToolbarItem(this._newButton);
-    this._newButton.addEventListener(UI.ToolbarButton.Events.Click, this._showDialog.bind(this));
+    this._newButton.addEventListener(UI.ToolbarButton.Events.Click, this._renderStartView.bind(this));
 
     this._downloadButton = new UI.ToolbarButton(Common.UIString('Download report'), 'largeicon-download');
     toolbar.appendToolbarItem(this._downloadButton);
@@ -169,62 +79,71 @@
 
     toolbar.appendSeparator();
 
-    this._reportSelector = new Audits2.ReportSelector();
+    this._reportSelector = new Audits2.ReportSelector(() => this._renderStartView());
     toolbar.appendToolbarItem(this._reportSelector.comboBox());
 
     this._clearButton = new UI.ToolbarButton(Common.UIString('Clear all'), 'largeicon-clear');
     toolbar.appendToolbarItem(this._clearButton);
     this._clearButton.addEventListener(UI.ToolbarButton.Events.Click, this._clearAll.bind(this));
 
-    toolbar.appendSeparator();
+    this._refreshToolbarUI();
+  }
 
-    toolbar.appendText(ls`Emulation: `);
-    for (const runtimeSetting of Audits2.Audits2Panel.RuntimeSettings) {
-      const control = new UI.ToolbarSettingComboBox(runtimeSetting.options, runtimeSetting.setting);
-      control.element.title = runtimeSetting.description;
-      toolbar.appendToolbarItem(control);
+  _renderStartView() {
+    this._auditResultsElement.removeChildren();
+    this._statusView.hide();
+
+    this._reportSelector.selectNewAudit();
+    this.contentElement.classList.toggle('in-progress', false);
+
+    this._startView.show(this.contentElement);
+    this._startView.setUnauditableExplanation(this._unauditableExplanation);
+    this._startView.setStartButtonEnabled(!this._unauditableExplanation);
+    this._refreshToolbarUI();
+  }
+
+  /**
+   * @param {string} inspectedURL
+   */
+  _renderStatusView(inspectedURL) {
+    this.contentElement.classList.toggle('in-progress', true);
+    this._statusView.setInspectedURL(inspectedURL);
+    this._statusView.show(this.contentElement);
+  }
+
+  /**
+   * @param {!ReportRenderer.ReportJSON} lighthouseResult
+   */
+  _renderReport(lighthouseResult) {
+    this.contentElement.classList.toggle('in-progress', false);
+    this._startView.hideWidget();
+    this._statusView.hide();
+    this._auditResultsElement.removeChildren();
+    this._refreshToolbarUI();
+
+    const cachedRenderedReport = this._cachedRenderedReports.get(lighthouseResult);
+    if (cachedRenderedReport) {
+      this._auditResultsElement.appendChild(cachedRenderedReport);
+      return;
     }
 
-    this._refreshToolbarUI();
-  }
+    const reportContainer = this._auditResultsElement.createChild('div', 'lh-vars lh-root lh-devtools');
 
-  _showLandingPage() {
-    if (this._reportSelector.hasCurrentSelection())
+    const dom = new DOM(/** @type {!Document} */ (this._auditResultsElement.ownerDocument));
+    const detailsRenderer = new Audits2.DetailsRenderer(dom);
+    const categoryRenderer = new Audits2.CategoryRenderer(dom, detailsRenderer);
+    categoryRenderer.setTraceArtifact(lighthouseResult);
+    const renderer = new Audits2.ReportRenderer(dom, categoryRenderer);
+
+    const templatesHTML = Runtime.cachedResources['audits2/lighthouse/templates.html'];
+    const templatesDOM = new DOMParser().parseFromString(templatesHTML, 'text/html');
+    if (!templatesDOM)
       return;
 
-    this._auditResultsElement.removeChildren();
-    const landingPage = this._auditResultsElement.createChild('div', 'vbox audits2-landing-page');
-    const landingCenter = landingPage.createChild('div', 'vbox audits2-landing-center');
-    landingCenter.createChild('div', 'audits2-logo');
-    const text = landingCenter.createChild('div', 'audits2-landing-text');
-    text.createChild('span', 'audits2-landing-bold-text').textContent = Common.UIString('Audits');
-    text.createChild('span').textContent = Common.UIString(
-        ' help you identify and fix common problems that affect' +
-        ' your site\'s performance, accessibility, and user experience. ');
-    const link = text.createChild('span', 'link');
-    link.textContent = Common.UIString('Learn more');
-    link.addEventListener(
-        'click', () => InspectorFrontendHost.openInNewTab('https://developers.google.com/web/tools/lighthouse/'));
+    renderer.setTemplateContext(templatesDOM);
+    renderer.renderReport(lighthouseResult, reportContainer);
 
-    const newButton = UI.createTextButton(
-        Common.UIString('Perform an audit\u2026'), this._showDialog.bind(this), '', true /* primary */);
-    landingCenter.appendChild(newButton);
-    this.setDefaultFocusedElement(newButton);
-    this._refreshToolbarUI();
-  }
-
-  _showDialog() {
-    this._dialog = new Audits2.Audits2Dialog(result => this._buildReportUI(result), this._protocolService);
-    this._dialog.render(this._auditResultsElement);
-    this._refreshDialogUI();
-  }
-
-  _hideDialog() {
-    if (!this._dialog)
-      return;
-
-    this._dialog.hide();
-    delete this._dialog;
+    this._cachedRenderedReports.set(lighthouseResult, reportContainer);
   }
 
   /**
@@ -234,11 +153,11 @@
     if (lighthouseResult === null)
       return;
 
-    const optionElement =
-        new Audits2.ReportSelector.Item(lighthouseResult, this._auditResultsElement, this._showLandingPage.bind(this));
+    const optionElement = new Audits2.ReportSelector.Item(
+        lighthouseResult, () => this._renderReport(lighthouseResult), this._renderStartView.bind(this));
     this._reportSelector.prepend(optionElement);
-    this._hideDialog();
     this._refreshToolbarUI();
+    this._renderReport(lighthouseResult);
   }
 
   /**
@@ -262,377 +181,96 @@
   }
 
   /**
-   * @param {string} profile
+   * @param {string} report
    */
-  _loadedFromFile(profile) {
-    const data = JSON.parse(profile);
+  _loadedFromFile(report) {
+    const data = JSON.parse(report);
     if (!data['lighthouseVersion'])
       return;
     this._buildReportUI(/** @type {!ReportRenderer.ReportJSON} */ (data));
   }
-};
 
-/**
- * @override
- */
-Audits2.Audits2Panel.ReportRenderer = class extends ReportRenderer {
-  /**
-   * Provides empty element for left nav
-   * @override
-   * @returns {!DocumentFragment}
-   */
-  _renderReportNav() {
-    return createDocumentFragment();
-  }
+  async _startAudit() {
+    Host.userMetrics.actionTaken(Host.UserMetrics.Action.Audits2Started);
 
-  /**
-   * @param {!ReportRenderer.ReportJSON} report
-   * @override
-   * @return {!DocumentFragment}
-   */
-  _renderReportHeader(report) {
-    return createDocumentFragment();
-  }
-};
+    try {
+      const inspectedURL = await this._controller.getInspectedURL({force: true});
+      const categoryIDs = this._controller.getCategoryIDs();
+      const flags = this._controller.getFlags();
 
-class ReportUIFeatures {
-  /**
-   * @param {!ReportRenderer.ReportJSON} report
-   */
-  initFeatures(report) {
-  }
-}
+      await this._setupEmulationAndProtocolConnection();
 
-/** @typedef {{type: string, setting: !Common.Setting, configID: string, title: string, description: string}} */
-Audits2.Audits2Panel.Preset;
+      this._renderStatusView(inspectedURL);
 
-/** @type {!Array.<!Audits2.Audits2Panel.Preset>} */
-Audits2.Audits2Panel.Presets = [
-  // configID maps to Lighthouse's Object.keys(config.categories)[0] value
-  {
-    setting: Common.settings.createSetting('audits2.cat_perf', true),
-    configID: 'performance',
-    title: 'Performance',
-    description: 'How long does this app take to show content and become usable'
-  },
-  {
-    setting: Common.settings.createSetting('audits2.cat_pwa', true),
-    configID: 'pwa',
-    title: 'Progressive Web App',
-    description: 'Does this page meet the standard of a Progressive Web App'
-  },
-  {
-    setting: Common.settings.createSetting('audits2.cat_best_practices', true),
-    configID: 'best-practices',
-    title: 'Best practices',
-    description: 'Does this page follow best practices for modern web development'
-  },
-  {
-    setting: Common.settings.createSetting('audits2.cat_a11y', true),
-    configID: 'accessibility',
-    title: 'Accessibility',
-    description: 'Is this page usable by people with disabilities or impairments'
-  },
-  {
-    setting: Common.settings.createSetting('audits2.cat_seo', true),
-    configID: 'seo',
-    title: 'SEO',
-    description: 'Is this page optimized for search engine results ranking'
-  },
-];
+      const lighthouseResult = await this._protocolService.startLighthouse(inspectedURL, categoryIDs, flags);
 
-/** @typedef {{setting: !Common.Setting, description: string, setFlags: function(!Object, string), options: !Array}} */
-Audits2.Audits2Panel.RuntimeSetting;
+      if (lighthouseResult && lighthouseResult.fatal) {
+        const error = new Error(lighthouseResult.message);
+        error.stack = lighthouseResult.stack;
+        throw error;
+      }
 
-/** @type {!Array.<!Audits2.Audits2Panel.RuntimeSetting>} */
-Audits2.Audits2Panel.RuntimeSettings = [
-  {
-    setting: Common.settings.createSetting('audits2.device_type', 'mobile'),
-    description: Common.UIString('Apply mobile emulation during auditing'),
-    setFlags: (flags, value) => {
-      flags.disableDeviceEmulation = value === 'desktop';
-    },
-    options: [
-      {label: Common.UIString('Mobile'), value: 'mobile'},
-      {label: Common.UIString('Desktop'), value: 'desktop'},
-    ],
-  },
-  {
-    setting: Common.settings.createSetting('audits2.throttling', 'default'),
-    description: Common.UIString('Apply network and CPU throttling during performance auditing'),
-    setFlags: (flags, value) => {
-      flags.disableNetworkThrottling = value === 'off';
-      flags.disableCpuThrottling = value === 'off';
-    },
-    options: [
-      {label: Common.UIString('3G w/ CPU slowdown'), value: 'default'},
-      {label: Common.UIString('No throttling'), value: 'off'},
-    ],
-  },
-  {
-    setting: Common.settings.createSetting('audits2.storage_reset', 'on'),
-    description: Common.UIString('Reset storage (localStorage, IndexedDB, etc) to a clean baseline before auditing'),
-    setFlags: (flags, value) => {
-      flags.disableStorageReset = value === 'off';
-    },
-    options: [
-      {label: Common.UIString('Clear storage'), value: 'on'},
-      {label: Common.UIString('Preserve storage'), value: 'off'},
-    ],
-  },
-];
+      if (!lighthouseResult)
+        throw new Error('Auditing failed to produce a result');
 
-Audits2.ReportSelector = class {
-  constructor() {
-    this._emptyItem = null;
-    this._comboBox = new UI.ToolbarComboBox(this._handleChange.bind(this), 'audits2-report');
-    this._comboBox.setMaxWidth(180);
-    this._comboBox.setMinWidth(140);
-    this._itemByOptionElement = new Map();
-    this._setPlaceholderState();
-  }
+      Host.userMetrics.actionTaken(Host.UserMetrics.Action.Audits2Finished);
 
-  _setPlaceholderState() {
-    this._comboBox.setEnabled(false);
-    this._emptyItem = createElement('option');
-    this._emptyItem.label = Common.UIString('(no reports)');
-    this._comboBox.selectElement().appendChild(this._emptyItem);
-    this._comboBox.select(this._emptyItem);
-  }
-
-  /**
-   * @param {!Event} event
-   */
-  _handleChange(event) {
-    const item = this._selectedItem();
-    if (item)
-      item.select();
-  }
-
-  /**
-   * @return {!Audits2.ReportSelector.Item}
-   */
-  _selectedItem() {
-    const option = this._comboBox.selectedOption();
-    return this._itemByOptionElement.get(option);
-  }
-
-  /**
-   * @return {boolean}
-   */
-  hasCurrentSelection() {
-    return !!this._selectedItem();
-  }
-
-  /**
-   * @return {boolean}
-   */
-  hasItems() {
-    return this._itemByOptionElement.size > 0;
-  }
-
-  /**
-   * @return {!UI.ToolbarComboBox}
-   */
-  comboBox() {
-    return this._comboBox;
-  }
-
-  /**
-   * @param {!Audits2.ReportSelector.Item} item
-   */
-  prepend(item) {
-    if (this._emptyItem) {
-      this._emptyItem.remove();
-      delete this._emptyItem;
-    }
-
-    const optionEl = item.optionElement();
-    const selectEl = this._comboBox.selectElement();
-
-    this._itemByOptionElement.set(optionEl, item);
-    selectEl.insertBefore(optionEl, selectEl.firstElementChild);
-    this._comboBox.setEnabled(true);
-    this._comboBox.select(optionEl);
-    item.select();
-  }
-
-  clearAll() {
-    for (const elem of this._comboBox.options()) {
-      this._itemByOptionElement.get(elem).delete();
-      this._itemByOptionElement.delete(elem);
-    }
-
-    this._setPlaceholderState();
-  }
-
-  downloadSelected() {
-    const item = this._selectedItem();
-    item.download();
-  }
-};
-
-Audits2.ReportSelector.Item = class {
-  /**
-   * @param {!ReportRenderer.ReportJSON} lighthouseResult
-   * @param {!Element} resultsView
-   * @param {function()} showLandingCallback
-   */
-  constructor(lighthouseResult, resultsView, showLandingCallback) {
-    this._lighthouseResult = lighthouseResult;
-    this._resultsView = resultsView;
-    this._showLandingCallback = showLandingCallback;
-    /** @type {?Element} */
-    this._reportContainer = null;
-
-
-    const url = new Common.ParsedURL(lighthouseResult.url);
-    const timestamp = lighthouseResult.generatedTime;
-    this._element = createElement('option');
-    this._element.label = `${new Date(timestamp).toLocaleTimeString()} - ${url.domain()}`;
-  }
-
-  select() {
-    this._renderReport();
-  }
-
-  /**
-   * @return {!Element}
-   */
-  optionElement() {
-    return this._element;
-  }
-
-  delete() {
-    if (this._element)
-      this._element.remove();
-    this._showLandingCallback();
-  }
-
-  download() {
-    const url = new Common.ParsedURL(this._lighthouseResult.url).domain();
-    const timestamp = this._lighthouseResult.generatedTime;
-    const fileName = `${url}-${new Date(timestamp).toISO8601Compact()}.json`;
-    Workspace.fileManager.save(fileName, JSON.stringify(this._lighthouseResult), true);
-  }
-
-  _renderReport() {
-    this._resultsView.removeChildren();
-    if (this._reportContainer) {
-      this._resultsView.appendChild(this._reportContainer);
-      return;
-    }
-
-    this._reportContainer = this._resultsView.createChild('div', 'lh-vars lh-root lh-devtools');
-
-    const dom = new DOM(/** @type {!Document} */ (this._resultsView.ownerDocument));
-    const detailsRenderer = new Audits2.DetailsRenderer(dom);
-    const categoryRenderer = new Audits2.CategoryRenderer(dom, detailsRenderer);
-    categoryRenderer.setTraceArtifact(this._lighthouseResult);
-    const renderer = new Audits2.Audits2Panel.ReportRenderer(dom, categoryRenderer);
-
-    const templatesHTML = Runtime.cachedResources['audits2/lighthouse/templates.html'];
-    const templatesDOM = new DOMParser().parseFromString(templatesHTML, 'text/html');
-    if (!templatesDOM)
-      return;
-
-    renderer.setTemplateContext(templatesDOM);
-    renderer.renderReport(this._lighthouseResult, this._reportContainer);
-  }
-};
-
-Audits2.CategoryRenderer = class extends CategoryRenderer {
-  /**
-   * @override
-   * @param {!DOM} dom
-   * @param {!DetailsRenderer} detailsRenderer
-   */
-  constructor(dom, detailsRenderer) {
-    super(dom, detailsRenderer);
-    this._defaultPassTrace = null;
-  }
-
-  /**
-   * @param {!ReportRenderer.ReportJSON} lhr
-   */
-  setTraceArtifact(lhr) {
-    if (!lhr.artifacts || !lhr.artifacts.traces || !lhr.artifacts.traces.defaultPass)
-      return;
-    this._defaultPassTrace = lhr.artifacts.traces.defaultPass;
-  }
-
-  /**
-   * @override
-   * @param {!ReportRenderer.CategoryJSON} category
-   * @param {!Object<string, !ReportRenderer.GroupJSON>} groups
-   * @return {!Element}
-   */
-  renderPerformanceCategory(category, groups) {
-    const defaultPassTrace = this._defaultPassTrace;
-    const element = super.renderPerformanceCategory(category, groups);
-    if (!defaultPassTrace)
-      return element;
-
-    const timelineButton = UI.createTextButton(Common.UIString('View Trace'), onViewTraceClick, 'view-trace');
-    element.querySelector('.lh-audit-group').prepend(timelineButton);
-    return element;
-
-    async function onViewTraceClick() {
-      await UI.inspectorView.showPanel('timeline');
-      Timeline.TimelinePanel.instance().loadFromEvents(defaultPassTrace.traceEvents);
+      await this._resetEmulationAndProtocolConnection();
+      this._buildReportUI(lighthouseResult);
+    } catch (err) {
+      if (err instanceof Error)
+        this._statusView.renderBugReport(err);
     }
   }
-};
 
-Audits2.DetailsRenderer = class extends DetailsRenderer {
-  /**
-   * @param {!DOM} dom
-   */
-  constructor(dom) {
-    super(dom);
-    this._onLoadPromise = null;
+  async _cancelAudit() {
+    this._statusView.updateStatus(ls`Cancelling`);
+    await this._resetEmulationAndProtocolConnection();
+    this._renderStartView();
   }
 
-  /**
-   * @override
-   * @param {!DetailsRenderer.NodeDetailsJSON} item
-   * @return {!Element}
-   */
-  renderNode(item) {
-    const element = super.renderNode(item);
-    this._replaceWithDeferredNodeBlock(element, item);
-    return element;
-  }
+  async _setupEmulationAndProtocolConnection() {
+    const flags = this._controller.getFlags();
 
-  /**
-   * @param {!Element} origElement
-   * @param {!DetailsRenderer.NodeDetailsJSON} detailsItem
-   */
-  async _replaceWithDeferredNodeBlock(origElement, detailsItem) {
-    const mainTarget = SDK.targetManager.mainTarget();
-    if (!this._onLoadPromise) {
-      const resourceTreeModel = mainTarget.model(SDK.ResourceTreeModel);
-      this._onLoadPromise = resourceTreeModel.once(SDK.ResourceTreeModel.Events.Load);
+    const emulationModel = self.singleton(Emulation.DeviceModeModel);
+    this._emulationEnabledBefore = emulationModel.enabledSetting().get();
+    this._emulationOutlineEnabledBefore = emulationModel.deviceOutlineSetting().get();
+    emulationModel.toolbarControlsEnabledSetting().set(false);
+
+    if (flags.disableDeviceEmulation) {
+      emulationModel.enabledSetting().set(false);
+      emulationModel.deviceOutlineSetting().set(false);
+      emulationModel.emulate(Emulation.DeviceModeModel.Type.None, null, null);
+    } else {
+      emulationModel.enabledSetting().set(true);
+      emulationModel.deviceOutlineSetting().set(true);
+
+      for (const device of Emulation.EmulatedDevicesList.instance().standard()) {
+        if (device.title === 'Nexus 5X')
+          emulationModel.emulate(Emulation.DeviceModeModel.Type.Device, device, device.modes[0], 1);
+      }
     }
 
-    await this._onLoadPromise;
+    await this._protocolService.attach();
+    this._isLHAttached = true;
+  }
 
-    const domModel = mainTarget.model(SDK.DOMModel);
-    if (!detailsItem.path)
+  async _resetEmulationAndProtocolConnection() {
+    if (!this._isLHAttached)
       return;
 
-    const nodeId = await domModel.pushNodeByPathToFrontend(detailsItem.path);
+    this._isLHAttached = false;
+    await this._protocolService.detach();
 
-    if (!nodeId)
-      return;
-    const node = domModel.nodeForId(nodeId);
-    if (!node)
-      return;
+    const emulationModel = self.singleton(Emulation.DeviceModeModel);
+    emulationModel.enabledSetting().set(this._emulationEnabledBefore);
+    emulationModel.deviceOutlineSetting().set(this._emulationOutlineEnabledBefore);
+    emulationModel.toolbarControlsEnabledSetting().set(true);
+    Emulation.InspectedPagePlaceholder.instance().update(true);
 
-    const element =
-        await Common.Linkifier.linkify(node, /** @type {!Common.Linkifier.Options} */ ({title: detailsItem.snippet}));
-    origElement.title = '';
-    origElement.textContent = '';
-    origElement.appendChild(element);
+    const resourceTreeModel = SDK.targetManager.mainTarget().model(SDK.ResourceTreeModel);
+    // reload to reset the page state
+    const inspectedURL = await this._controller.getInspectedURL();
+    await resourceTreeModel.navigate(inspectedURL);
   }
 };
diff --git a/front_end/audits2/Audits2ReportRenderer.js b/front_end/audits2/Audits2ReportRenderer.js
new file mode 100644
index 0000000..0460011
--- /dev/null
+++ b/front_end/audits2/Audits2ReportRenderer.js
@@ -0,0 +1,130 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @override
+ */
+Audits2.ReportRenderer = class extends ReportRenderer {
+  /**
+   * Provides empty element for left nav
+   * @override
+   * @returns {!DocumentFragment}
+   */
+  _renderReportNav() {
+    return createDocumentFragment();
+  }
+
+  /**
+   * @param {!ReportRenderer.ReportJSON} report
+   * @override
+   * @return {!DocumentFragment}
+   */
+  _renderReportHeader(report) {
+    return createDocumentFragment();
+  }
+};
+
+class ReportUIFeatures {
+  /**
+   * @param {!ReportRenderer.ReportJSON} report
+   */
+  initFeatures(report) {
+  }
+}
+
+Audits2.CategoryRenderer = class extends CategoryRenderer {
+  /**
+   * @override
+   * @param {!DOM} dom
+   * @param {!DetailsRenderer} detailsRenderer
+   */
+  constructor(dom, detailsRenderer) {
+    super(dom, detailsRenderer);
+    this._defaultPassTrace = null;
+  }
+
+  /**
+   * @param {!ReportRenderer.ReportJSON} lhr
+   */
+  setTraceArtifact(lhr) {
+    if (!lhr.artifacts || !lhr.artifacts.traces || !lhr.artifacts.traces.defaultPass)
+      return;
+    this._defaultPassTrace = lhr.artifacts.traces.defaultPass;
+  }
+
+  /**
+   * @override
+   * @param {!ReportRenderer.CategoryJSON} category
+   * @param {!Object<string, !ReportRenderer.GroupJSON>} groups
+   * @return {!Element}
+   */
+  renderPerformanceCategory(category, groups) {
+    const defaultPassTrace = this._defaultPassTrace;
+    const element = super.renderPerformanceCategory(category, groups);
+    if (!defaultPassTrace)
+      return element;
+
+    const timelineButton = UI.createTextButton(Common.UIString('View Trace'), onViewTraceClick, 'view-trace');
+    element.querySelector('.lh-audit-group').prepend(timelineButton);
+    return element;
+
+    async function onViewTraceClick() {
+      await UI.inspectorView.showPanel('timeline');
+      Timeline.TimelinePanel.instance().loadFromEvents(defaultPassTrace.traceEvents);
+    }
+  }
+};
+
+Audits2.DetailsRenderer = class extends DetailsRenderer {
+  /**
+   * @param {!DOM} dom
+   */
+  constructor(dom) {
+    super(dom);
+    this._onLoadPromise = null;
+  }
+
+  /**
+   * @override
+   * @param {!DetailsRenderer.NodeDetailsJSON} item
+   * @return {!Element}
+   */
+  renderNode(item) {
+    const element = super.renderNode(item);
+    this._replaceWithDeferredNodeBlock(element, item);
+    return element;
+  }
+
+  /**
+   * @param {!Element} origElement
+   * @param {!DetailsRenderer.NodeDetailsJSON} detailsItem
+   */
+  async _replaceWithDeferredNodeBlock(origElement, detailsItem) {
+    const mainTarget = SDK.targetManager.mainTarget();
+    if (!this._onLoadPromise) {
+      const resourceTreeModel = mainTarget.model(SDK.ResourceTreeModel);
+      this._onLoadPromise = resourceTreeModel.once(SDK.ResourceTreeModel.Events.Load);
+    }
+
+    await this._onLoadPromise;
+
+    const domModel = mainTarget.model(SDK.DOMModel);
+    if (!detailsItem.path)
+      return;
+
+    const nodeId = await domModel.pushNodeByPathToFrontend(detailsItem.path);
+
+    if (!nodeId)
+      return;
+    const node = domModel.nodeForId(nodeId);
+    if (!node)
+      return;
+
+    const element =
+        await Common.Linkifier.linkify(node, /** @type {!Common.Linkifier.Options} */ ({title: detailsItem.snippet}));
+    origElement.title = '';
+    origElement.textContent = '';
+    origElement.appendChild(element);
+  }
+};
diff --git a/front_end/audits2/Audits2ReportSelector.js b/front_end/audits2/Audits2ReportSelector.js
new file mode 100644
index 0000000..fda21c7
--- /dev/null
+++ b/front_end/audits2/Audits2ReportSelector.js
@@ -0,0 +1,143 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+Audits2.ReportSelector = class {
+  constructor(renderNewAuditView) {
+    this._renderNewAuditView = renderNewAuditView;
+    this._newAuditItem = createElement('option');
+    this._comboBox = new UI.ToolbarComboBox(this._handleChange.bind(this), 'audits2-report');
+    this._comboBox.setMaxWidth(180);
+    this._comboBox.setMinWidth(140);
+    this._itemByOptionElement = new Map();
+    this._setEmptyState();
+  }
+
+  _setEmptyState() {
+    this._comboBox.selectElement().removeChildren();
+
+    this._comboBox.setEnabled(false);
+    this._newAuditItem = createElement('option');
+    this._newAuditItem.label = Common.UIString('(new audit)');
+    this._comboBox.selectElement().appendChild(this._newAuditItem);
+    this._comboBox.select(this._newAuditItem);
+  }
+
+  /**
+   * @param {!Event} event
+   */
+  _handleChange(event) {
+    const item = this._selectedItem();
+    if (item)
+      item.select();
+    else
+      this._renderNewAuditView();
+  }
+
+  /**
+   * @return {!Audits2.ReportSelector.Item}
+   */
+  _selectedItem() {
+    const option = this._comboBox.selectedOption();
+    return this._itemByOptionElement.get(option);
+  }
+
+  /**
+   * @return {boolean}
+   */
+  hasCurrentSelection() {
+    return !!this._selectedItem();
+  }
+
+  /**
+   * @return {boolean}
+   */
+  hasItems() {
+    return this._itemByOptionElement.size > 0;
+  }
+
+  /**
+   * @return {!UI.ToolbarComboBox}
+   */
+  comboBox() {
+    return this._comboBox;
+  }
+
+  /**
+   * @param {!Audits2.ReportSelector.Item} item
+   */
+  prepend(item) {
+    const optionEl = item.optionElement();
+    const selectEl = this._comboBox.selectElement();
+
+    this._itemByOptionElement.set(optionEl, item);
+    selectEl.insertBefore(optionEl, selectEl.firstElementChild);
+    this._comboBox.setEnabled(true);
+    this._comboBox.select(optionEl);
+    item.select();
+  }
+
+  clearAll() {
+    for (const elem of this._comboBox.options()) {
+      if (elem === this._newAuditItem)
+        continue;
+
+      this._itemByOptionElement.get(elem).delete();
+      this._itemByOptionElement.delete(elem);
+    }
+
+    this._setEmptyState();
+  }
+
+  downloadSelected() {
+    const item = this._selectedItem();
+    if (item)
+      item.download();
+  }
+
+  selectNewAudit() {
+    this._comboBox.select(this._newAuditItem);
+  }
+};
+
+Audits2.ReportSelector.Item = class {
+  /**
+   * @param {!ReportRenderer.ReportJSON} lighthouseResult
+   * @param {function()} renderReport
+   * @param {function()} showLandingCallback
+   */
+  constructor(lighthouseResult, renderReport, showLandingCallback) {
+    this._lighthouseResult = lighthouseResult;
+    this._renderReport = renderReport;
+    this._showLandingCallback = showLandingCallback;
+
+    const url = new Common.ParsedURL(lighthouseResult.url);
+    const timestamp = lighthouseResult.generatedTime;
+    this._element = createElement('option');
+    this._element.label = `${new Date(timestamp).toLocaleTimeString()} - ${url.domain()}`;
+  }
+
+  select() {
+    this._renderReport();
+  }
+
+  /**
+   * @return {!Element}
+   */
+  optionElement() {
+    return this._element;
+  }
+
+  delete() {
+    if (this._element)
+      this._element.remove();
+    this._showLandingCallback();
+  }
+
+  download() {
+    const url = new Common.ParsedURL(this._lighthouseResult.url).domain();
+    const timestamp = this._lighthouseResult.generatedTime;
+    const fileName = `${url}-${new Date(timestamp).toISO8601Compact()}.json`;
+    Workspace.fileManager.save(fileName, JSON.stringify(this._lighthouseResult), true);
+  }
+};
\ No newline at end of file
diff --git a/front_end/audits2/Audits2StartView.js b/front_end/audits2/Audits2StartView.js
new file mode 100644
index 0000000..f62804f
--- /dev/null
+++ b/front_end/audits2/Audits2StartView.js
@@ -0,0 +1,145 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @unrestricted
+ */
+Audits2.StartView = class extends UI.Widget {
+  /**
+   * @param {!Audits2.AuditController} controller
+   */
+  constructor(controller) {
+    super();
+    this.registerRequiredCSS('audits2/audits2StartView.css');
+    this._controller = controller;
+    this._render();
+  }
+
+  /**
+   * @param {string} settingName
+   * @param {!Element} parentElement
+   */
+  _populateRuntimeSettingAsComboBox(settingName, parentElement) {
+    const runtimeSetting = Audits2.RuntimeSettings.find(item => item.setting.name === settingName);
+    const control = new UI.ToolbarSettingComboBox(runtimeSetting.options, runtimeSetting.setting);
+    control.element.title = runtimeSetting.description;
+    parentElement.appendChild(control.element);
+  }
+
+  /**
+   * @param {!UI.Fragment} fragment
+   */
+  _populateFormControls(fragment) {
+    // Populate the device type
+    const deviceTypeFormElements = fragment.$('device-type-form-elements');
+    this._populateRuntimeSettingAsComboBox('audits2.device_type', deviceTypeFormElements);
+
+    // Populate the audit categories
+    const categoryFormElements = fragment.$('categories-form-elements');
+    for (const preset of Audits2.Presets) {
+      preset.setting.setTitle(preset.title);
+      const checkbox = new UI.ToolbarSettingCheckbox(preset.setting);
+      const row = categoryFormElements.createChild('div', 'vbox audits2-launcher-row');
+      row.appendChild(checkbox.element);
+      row.createChild('span', 'audits2-launcher-description dimmed').textContent = preset.description;
+    }
+
+    // Populate the throttling
+    const throttlingFormElements = fragment.$('throttling-form-elements');
+    this._populateRuntimeSettingAsComboBox('audits2.throttling', throttlingFormElements);
+
+
+    // Populate other settings
+    const otherFormElements = fragment.$('other-form-elements');
+    this._populateRuntimeSettingAsComboBox('audits2.storage_reset', otherFormElements);
+  }
+
+  _render() {
+    this._startButton = UI.createTextButton(
+        ls`Run audit`, () => this._controller.dispatchEventToListeners(Audits2.Events.RequestAuditStart),
+        'audits2-start-button', true /* primary */);
+    const deviceIcon = UI.Icon.create('largeicon-phone');
+    const categoriesIcon = UI.Icon.create('largeicon-checkmark');
+    const throttlingIcon = UI.Icon.create('largeicon-settings-gear');
+
+    const fragment = UI.Fragment.build`
+      <div class="vbox audits2-start-view">
+        <div class="audits2-dialog-overlay"></div>
+        <header>
+          <div class="audits2-logo"></div>
+          <div class="audits2-start-view-text">
+            <h2>Audits</h2>
+            <p>
+              Identify and fix common problems that affect your site's performance, accessibility, and user experience.
+              <span class="link" $="learn-more">Learn more.</a>
+            </p>
+          </div>
+        </header>
+        <form>
+          <div class="audits2-form-section">
+            <div class="audits2-form-section-label">
+              <i>${deviceIcon}</i>
+              <div class="audits2-icon-label">Device</div>
+            </div>
+            <div class="audits2-form-elements" $="device-type-form-elements"></div>
+          </div>
+          <div class="audits2-form-section">
+            <div class="audits2-form-section-label">
+              <i>${categoriesIcon}</i>
+              <div class="audits2-icon-label">Audits</div>
+            </div>
+            <div class="audits2-form-elements" $="categories-form-elements"></div>
+          </div>
+          <div class="audits2-form-section">
+            <div class="audits2-form-section-label">
+              <i>${throttlingIcon}</i>
+              <div class="audits2-icon-label">Throttling</div>
+            </div>
+            <div class="audits2-form-elements" $="throttling-form-elements"></div>
+          </div>
+          <div class="audits2-form-section">
+            <div class="audits2-form-section-label"></div>
+            <div class="audits2-form-elements" $="other-form-elements"></div>
+          </div>
+          <div class="audits2-form-section">
+            <div class="audits2-form-section-label"></div>
+            <div class="audits2-form-elements audits2-start-button-container hbox">
+              ${this._startButton}
+              <div $="help-text" class="audits2-help-text hidden"></div>
+            </div>
+          </div>
+        </form>
+      </div>
+    `;
+
+    this._helpText = fragment.$('help-text');
+
+    const learnMoreLink = fragment.$('learn-more');
+    learnMoreLink.addEventListener(
+        'click', () => InspectorFrontendHost.openInNewTab('https://developers.google.com/web/tools/lighthouse/'));
+
+    this._populateFormControls(fragment);
+    this.contentElement.appendChild(fragment.element());
+    this.contentElement.style.overflow = 'auto';
+  }
+
+  /**
+   * @param {boolean} isEnabled
+   */
+  setStartButtonEnabled(isEnabled) {
+    if (this._helpText)
+      this._helpText.classList.toggle('hidden', isEnabled);
+
+    if (this._startButton)
+      this._startButton.disabled = !isEnabled;
+  }
+
+  /**
+   * @param {?string} text
+   */
+  setUnauditableExplanation(text) {
+    if (this._helpText)
+      this._helpText.textContent = text;
+  }
+};
diff --git a/front_end/audits2/Audits2StatusView.js b/front_end/audits2/Audits2StatusView.js
index 64e2e0d..98cfc67 100644
--- a/front_end/audits2/Audits2StatusView.js
+++ b/front_end/audits2/Audits2StatusView.js
@@ -2,53 +2,98 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-Audits2.Audits2StatusView = class {
-  constructor() {
+Audits2.StatusView = class {
+  /**
+   * @param {!Audits2.AuditController} controller
+   */
+  constructor(controller) {
+    this._controller = controller;
+
     this._statusView = null;
+    this._statusHeader = null;
     this._progressWrapper = null;
     this._progressBar = null;
     this._statusText = null;
 
+    this._inspectedURL = '';
     this._textChangedAt = 0;
-    this._fastFactsQueued = Audits2.Audits2StatusView.FastFacts.slice();
+    this._fastFactsQueued = Audits2.StatusView.FastFacts.slice();
     this._currentPhase = null;
     this._scheduledTextChangeTimeout = null;
     this._scheduledFastFactTimeout = null;
+
+    this._dialog = new UI.Dialog();
+    this._dialog.setOutsideClickCallback(event => event.consume(true));
+    this._render();
   }
 
-  /**
-   * @param {!Element} parentElement
-   */
-  render(parentElement) {
-    this.reset();
+  _render() {
+    const dialogRoot = UI.createShadowRootWithCoreStyles(this._dialog.contentElement, 'audits2/audits2Dialog.css');
+    const auditsViewElement = dialogRoot.createChild('div', 'audits2-view vbox');
 
-    this._statusView = parentElement.createChild('div', 'audits2-status vbox hidden');
-    this._progressWrapper = this._statusView.createChild('div', 'audits2-progress-wrapper');
-    this._progressBar = this._progressWrapper.createChild('div', 'audits2-progress-bar');
-    this._statusText = this._statusView.createChild('div', 'audits2-status-text');
+    const cancelButton = UI.createTextButton(ls`Cancel`, this._cancel.bind(this));
+    const fragment = UI.Fragment.build`
+      <div class="audits2-view vbox">
+        <h2 $="status-header">Auditing your web page\u2026</h2>
+        <div class="audits2-status vbox" $="status-view">
+          <div class="audits2-progress-wrapper" $="progress-wrapper">
+            <div class="audits2-progress-bar" $="progress-bar"></div>
+          </div>
+          <div class="audits2-status-text" $="status-text"></div>
+        </div>
+        ${cancelButton}
+      </div>
+    `;
 
-    this.updateStatus(Common.UIString('Loading...'));
+    auditsViewElement.appendChild(fragment.element());
+    auditsViewElement.tabIndex = 0;
+
+    this._statusView = fragment.$('status-view');
+    this._statusHeader = fragment.$('status-header');
+    this._progressWrapper = fragment.$('progress-wrapper');
+    this._progressBar = fragment.$('progress-bar');
+    this._statusText = fragment.$('status-text');
+
+    this._dialog.setDefaultFocusedElement(cancelButton);
+    this._dialog.setSizeBehavior(UI.GlassPane.SizeBehavior.SetExactWidthMaxHeight);
+    this._dialog.setMaxContentSize(new UI.Size(500, 400));
   }
 
-  reset() {
+  _reset() {
     this._resetProgressBarClasses();
     clearTimeout(this._scheduledFastFactTimeout);
 
     this._textChangedAt = 0;
-    this._fastFactsQueued = Audits2.Audits2StatusView.FastFacts.slice();
+    this._fastFactsQueued = Audits2.StatusView.FastFacts.slice();
     this._currentPhase = null;
     this._scheduledTextChangeTimeout = null;
     this._scheduledFastFactTimeout = null;
   }
 
   /**
-   * @param {boolean} isVisible
+   * @param {!Element} dialogRenderElement
    */
-  setVisible(isVisible) {
-    this._statusView.classList.toggle('hidden', !isVisible);
+  show(dialogRenderElement) {
+    this._reset();
+    this.updateStatus(ls`Loading\u2026`);
 
-    if (!isVisible)
-      clearTimeout(this._scheduledFastFactTimeout);
+    const parsedURL = this._inspectedURL.asParsedURL();
+    const pageHost = parsedURL && parsedURL.host;
+    const statusHeader = pageHost ? ls`Auditing ${pageHost}` : ls`Auditing your web page`;
+    this._statusHeader.textContent = `${statusHeader}\u2026`;
+    this._dialog.show(dialogRenderElement);
+  }
+
+  hide() {
+    if (this._dialog.isShowing())
+      this._dialog.hide();
+  }
+
+  /**
+   * @param {string=} url
+   */
+  setInspectedURL(url = '') {
+    this._inspectedURL = url;
   }
 
   /**
@@ -77,19 +122,21 @@
     }
   }
 
+  _cancel() {
+    this._controller.dispatchEventToListeners(Audits2.Events.RequestAuditCancel);
+  }
+
   /**
-   * @param {!Audits2.Audits2StatusView.StatusPhases} phase
+   * @param {!Audits2.StatusView.StatusPhases} phase
    * @return {string}
    */
   _getMessageForPhase(phase) {
     if (phase.message)
       return Common.UIString(phase.message);
 
-    const deviceType =
-        Audits2.Audits2Panel.RuntimeSettings.find(item => item.setting.name === 'audits2.device_type').setting.get();
-    const throttling =
-        Audits2.Audits2Panel.RuntimeSettings.find(item => item.setting.name === 'audits2.throttling').setting.get();
-    const match = Audits2.Audits2StatusView.LoadingMessages.find(item => {
+    const deviceType = Audits2.RuntimeSettings.find(item => item.setting.name === 'audits2.device_type').setting.get();
+    const throttling = Audits2.RuntimeSettings.find(item => item.setting.name === 'audits2.throttling').setting.get();
+    const match = Audits2.StatusView.LoadingMessages.find(item => {
       return item.deviceType === deviceType && item.throttling === throttling;
     });
 
@@ -98,10 +145,10 @@
 
   /**
    * @param {string} message
-   * @return {?Audits2.Audits2StatusView.StatusPhases}
+   * @return {?Audits2.StatusView.StatusPhases}
    */
   _getPhaseForMessage(message) {
-    return Audits2.Audits2StatusView.StatusPhases.find(phase => message.startsWith(phase.statusMessagePrefix));
+    return Audits2.StatusView.StatusPhases.find(phase => message.startsWith(phase.statusMessagePrefix));
   }
 
   _resetProgressBarClasses() {
@@ -125,7 +172,7 @@
 
   _updateFastFactIfNecessary() {
     const now = performance.now();
-    if (now - this._textChangedAt < Audits2.Audits2StatusView.fastFactRotationInterval)
+    if (now - this._textChangedAt < Audits2.StatusView.fastFactRotationInterval)
       return;
     if (!this._fastFactsQueued.length)
       return;
@@ -153,7 +200,7 @@
       clearTimeout(this._scheduledTextChangeTimeout);
 
     const msSinceLastChange = performance.now() - this._textChangedAt;
-    const msToTextChange = Audits2.Audits2StatusView.minimumTextVisibilityDuration - msSinceLastChange;
+    const msToTextChange = Audits2.StatusView.minimumTextVisibilityDuration - msSinceLastChange;
 
     this._scheduledTextChangeTimeout = setTimeout(() => {
       this._commitTextChange(text);
@@ -162,9 +209,8 @@
 
   /**
    * @param {!Error} err
-   * @param {string} auditURL
    */
-  renderBugReport(err, auditURL) {
+  renderBugReport(err) {
     console.error(err);
     clearTimeout(this._scheduledFastFactTimeout);
     clearTimeout(this._scheduledTextChangeTimeout);
@@ -174,13 +220,13 @@
     this._commitTextChange('');
     this._statusText.createTextChild(Common.UIString('Ah, sorry! We ran into an error: '));
     this._statusText.createChild('em').createTextChild(err.message);
-    if (Audits2.Audits2StatusView.KnownBugPatterns.some(pattern => pattern.test(err.message))) {
+    if (Audits2.StatusView.KnownBugPatterns.some(pattern => pattern.test(err.message))) {
       const message = Common.UIString(
           'Try to navigate to the URL in a fresh Chrome profile without any other tabs or ' +
           'extensions open and try again.');
       this._statusText.createChild('p').createTextChild(message);
     } else {
-      this._renderBugReportLink(err, auditURL);
+      this._renderBugReportLink(err, this._inspectedURL);
     }
   }
 
@@ -210,7 +256,7 @@
 
 
 /** @type {!Array.<!RegExp>} */
-Audits2.Audits2StatusView.KnownBugPatterns = [
+Audits2.StatusView.KnownBugPatterns = [
   /PARSING_PROBLEM/,
   /DOCUMENT_REQUEST/,
   /READ_FAILED/,
@@ -220,7 +266,7 @@
 ];
 
 /** @typedef {{message: string, progressBarClass: string, order: number}} */
-Audits2.Audits2StatusView.StatusPhases = [
+Audits2.StatusView.StatusPhases = [
   {
     id: 'loading',
     progressBarClass: 'loading',
@@ -244,7 +290,7 @@
 ];
 
 /** @typedef {{message: string, deviceType: string, throttling: string}} */
-Audits2.Audits2StatusView.LoadingMessages = [
+Audits2.StatusView.LoadingMessages = [
   {
     deviceType: 'mobile',
     throttling: 'on',
@@ -267,7 +313,7 @@
   },
 ];
 
-Audits2.Audits2StatusView.FastFacts = [
+Audits2.StatusView.FastFacts = [
   '1MB takes a minimum of 5 seconds to download on a typical 3G connection [Source: WebPageTest and DevTools 3G definition].',
   'Rebuilding Pinterest pages for performance increased conversion rates by 15% [Source: WPO Stats]',
   'BBC has seen a loss of 10% of their users for every extra second of page load [Source: WPO Stats]',
@@ -287,6 +333,6 @@
 ];
 
 /** @const */
-Audits2.Audits2StatusView.fastFactRotationInterval = 6000;
+Audits2.StatusView.fastFactRotationInterval = 6000;
 /** @const */
-Audits2.Audits2StatusView.minimumTextVisibilityDuration = 3000;
+Audits2.StatusView.minimumTextVisibilityDuration = 3000;
diff --git a/front_end/audits2/audits2Dialog.css b/front_end/audits2/audits2Dialog.css
index ae4b406..d5468b0 100644
--- a/front_end/audits2/audits2Dialog.css
+++ b/front_end/audits2/audits2Dialog.css
@@ -13,37 +13,17 @@
     max-width: 500px;
 }
 
-.launcher-container {
-  width: 100%;
-}
-
-.audits2-view .dialog-close-button {
-    position: absolute;
-    top: 0px;
-    right: -14px;
-}
-
-header {
+.audits2-view h2 {
     color: #666;
     font-weight: bold;
     font-size: 14px;
     flex: none;
-}
-
-.audits2-form {
-  flex: auto;
-  overflow-y: auto;
-}
-
-.audits2-form label {
-    display: flex;
-}
-
-.audits2-form label div {
-    display: inline;
+    width: 100%;
+    text-align: left;
 }
 
 .audits2-status {
+    width: 100%;
     flex: auto;
     align-items: center;
     color: #666;
@@ -119,25 +99,3 @@
     display: block;
     margin-top: 5px;
 }
-
-button {
-    margin: 15px 10px;
-}
-
-.audits2-launcher-row {
-    padding: 6px;
-}
-
-.audits2-launcher-description {
-    padding: 3px 0 0 22px;
-}
-
-.audits2-dialog-help-text {
-    color: red;
-    margin-top: 10px;
-}
-
-.audits2-dialog-buttons {
-    justify-content: center;
-    min-height: 40px;
-}
diff --git a/front_end/audits2/audits2Panel.css b/front_end/audits2/audits2Panel.css
index 22f9278..01305bb 100644
--- a/front_end/audits2/audits2Panel.css
+++ b/front_end/audits2/audits2Panel.css
@@ -9,44 +9,6 @@
     border-bottom: var(--divider-border);
 }
 
-.audits2-logo {
-    width: 210px;
-    height: 200px;
-    flex-shrink: 0;
-    background-repeat: no-repeat;
-    background-size: contain;
-    margin-top: 10px;
-    background-image: url(Images/audits_logo.svg);
-}
-
-.audits2-landing-page {
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    overflow: auto;
-}
-
-
-.audits2-landing-center {
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    max-width: 400px;
-    margin: 50px;
-}
-
-.audits2-landing-center > * {
-    flex-shrink: 0;
-}
-
-.audits2-landing-text {
-    color: #666;
-}
-
-.audits2-landing-bold-text {
-    font-weight: bold;
-}
-
 .lh-root {
   --report-menu-width: 0;
   user-select: initial;
@@ -82,10 +44,6 @@
   right: 0;
 }
 
-.audits2-landing-center button {
-    margin-top: 20px;
-}
-
 .audits2-results-container {
     overflow-y: scroll;
     position: relative;
diff --git a/front_end/audits2/audits2StartView.css b/front_end/audits2/audits2StartView.css
new file mode 100644
index 0000000..3d43ca9
--- /dev/null
+++ b/front_end/audits2/audits2StartView.css
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2018 The Chromium Authors. All rights reserved.
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+.audits2-start-view {
+  font-family: Roboto, sans-serif;
+}
+
+.audits2-dialog-overlay {
+  position: fixed;
+  width: 100%;
+  height: 100%;
+  display: none;
+  background: rgba(0,0,0,.3);
+  z-index: 100;
+}
+
+.in-progress .audits2-dialog-overlay {
+  display: block;
+}
+
+.audits2-start-view header {
+  padding: 10px;
+  display: flex;
+}
+
+.audits2-logo {
+    width: 150px;
+    height: 150px;
+    flex-shrink: 0;
+    background-repeat: no-repeat;
+    background-size: contain;
+    margin-right: 18px;
+    background-image: url(Images/audits_logo.svg);
+}
+
+.audits2-start-view-text {
+  color: #757575;
+  margin-top: 24px;
+}
+
+.audits2-start-view-text h2 {
+  color: black;
+  font-weight: normal;
+}
+
+.audits2-form-section {
+  border-top: 1px solid #e8e8e8;
+  display: flex;
+  padding: 20px;
+}
+
+.audits2-form-section-label {
+  flex: 1;
+  display: flex;
+}
+
+.audits2-form-section-label i {
+  width: 30px;
+  height: 30px;
+  border-radius: 50%;
+  background: #e6e6e6;
+  display: block;
+  text-align: center;
+  padding-top: 3px;
+}
+
+.audits2-icon-label {
+  margin: 7px;
+  font-size: 1.2em;
+}
+
+.audits2-form-section-label span.largeicon-checkmark {
+  transform: scale(1.3);
+  position: relative;
+  right: -2px;
+}
+
+.audits2-form-elements {
+  flex: 3;
+}
+
+.audits2-start-button-container {
+  align-items: center;
+}
+
+.audits2-start-button {
+  max-width: 100px;
+}
+
+.audits2-start-view .toolbar-dropdown-arrow {
+  display: none;
+}
+
+.audits2-launcher-row {
+  padding-bottom: 10px;
+}
+
+.audits2-launcher-row:last-of-type {
+  padding-bottom: 0;
+}
+
+.audits2-launcher-row .dimmed {
+  padding-left: 22px;
+}
+
+.audits2-help-text {
+  color: red;
+  font-weight: bold;
+  padding-left: 10px;
+}
\ No newline at end of file
diff --git a/front_end/audits2/module.json b/front_end/audits2/module.json
index 2ec939f..49fb323 100644
--- a/front_end/audits2/module.json
+++ b/front_end/audits2/module.json
@@ -26,12 +26,16 @@
         "lighthouse/renderer/crc-details-renderer.js",
         "lighthouse/renderer/report-renderer.js",
         "Audits2Panel.js",
-        "Audits2Dialog.js",
+        "Audits2Controller.js",
+        "Audits2ReportSelector.js",
+        "Audits2ReportRenderer.js",
+        "Audits2StartView.js",
         "Audits2StatusView.js",
         "Audits2ProtocolService.js"
     ],
     "resources": [
         "audits2Dialog.css",
+        "audits2StartView.css",
         "audits2Panel.css",
         "lighthouse/report-styles.css",
         "lighthouse/templates.html"
diff --git a/front_end/audits2_test_runner/Audits2TestRunner.js b/front_end/audits2_test_runner/Audits2TestRunner.js
index 4d0261b..4dbddf6 100644
--- a/front_end/audits2_test_runner/Audits2TestRunner.js
+++ b/front_end/audits2_test_runner/Audits2TestRunner.js
@@ -17,6 +17,13 @@
 /**
  * @return {?Element}
  */
+Audits2TestRunner.getContainerElement = function() {
+  return Audits2TestRunner._panel().contentElement;
+};
+
+/**
+ * @return {?Element}
+ */
 Audits2TestRunner.getResultsElement = function() {
   return Audits2TestRunner._panel()._auditResultsElement;
 };
@@ -25,14 +32,14 @@
  * @return {?Element}
  */
 Audits2TestRunner.getDialogElement = function() {
-  return Audits2TestRunner._panel()._dialog._dialog.contentElement.shadowRoot.querySelector('.audits2-view');
+  return Audits2TestRunner._panel()._statusView._dialog.contentElement.shadowRoot.querySelector('.audits2-view');
 };
 
 /**
  * @return {?Element}
  */
 Audits2TestRunner.getRunButton = function() {
-  const dialog = Audits2TestRunner.getDialogElement();
+  const dialog = Audits2TestRunner.getContainerElement();
   return dialog && dialog.querySelectorAll('button')[0];
 };
 
@@ -41,19 +48,18 @@
  */
 Audits2TestRunner.getCancelButton = function() {
   const dialog = Audits2TestRunner.getDialogElement();
-  return dialog && dialog.querySelectorAll('button')[1];
+  return dialog && dialog.querySelectorAll('button')[0];
 };
 
-Audits2TestRunner.openDialog = function() {
-  const resultsElement = Audits2TestRunner.getResultsElement();
-  resultsElement.querySelector('button').click();
+Audits2TestRunner.openStartAudit = function() {
+  Audits2TestRunner._panel()._renderStartView();
 };
 
 /**
  * @param {function(string)} onMessage
  */
 Audits2TestRunner.addStatusListener = function(onMessage) {
-  TestRunner.addSniffer(Audits2.Audits2Dialog.prototype, '_updateStatus', onMessage, true);
+  TestRunner.addSniffer(Audits2.StatusView.prototype, 'updateStatus', onMessage, true);
 };
 
 /**
@@ -65,6 +71,10 @@
   });
 };
 
+Audits2TestRunner.forcePageAuditabilityCheck = function() {
+  Audits2TestRunner._panel()._controller.recomputePageAuditability();
+};
+
 /**
  * @param {?Element} checkboxContainer
  * @return {string}
@@ -91,23 +101,18 @@
   return `${button.textContent}: ${enabledLabel} ${hiddenLabel}`;
 };
 
-Audits2TestRunner.dumpDialogState = function() {
-  TestRunner.addResult('\n========== Audits2 Dialog State ==========');
-  const dialog = Audits2TestRunner._panel()._dialog && Audits2TestRunner._panel()._dialog._dialog;
-  TestRunner.addResult(dialog ? 'Dialog is visible\n' : 'No dialog');
-  if (!dialog)
-    return;
+Audits2TestRunner.dumpStartAuditState = function() {
+  TestRunner.addResult('\n========== Audits2 Start Audit State ==========');
 
-  const dialogElement = Audits2TestRunner.getDialogElement();
-  const checkboxes = [...dialogElement.querySelectorAll('.checkbox')];
+  const containerElement = Audits2TestRunner.getContainerElement();
+  const checkboxes = [...containerElement.querySelectorAll('.checkbox')];
   checkboxes.forEach(element => {
     TestRunner.addResult(Audits2TestRunner._checkboxStateLabel(element));
   });
 
-  const helpText = dialogElement.querySelector('.audits2-dialog-help-text');
+  const helpText = containerElement.querySelector('.audits2-help-text');
   if (!helpText.classList.contains('hidden'))
     TestRunner.addResult(`Help text: ${helpText.textContent}`);
 
   TestRunner.addResult(Audits2TestRunner._buttonStateLabel(Audits2TestRunner.getRunButton()));
-  TestRunner.addResult(Audits2TestRunner._buttonStateLabel(Audits2TestRunner.getCancelButton()) + '\n');
-};
\ No newline at end of file
+};