Add pencil buttons to initiate header override editing

Add a small button with a pencil icon next to each response header on
hover. Clicking this button does all of the following if necessary:
 - Prompt the user for a folder to store overrides in
 - Turn on the overrides setting
 - Create a '.headers' file to store header overrides in

The user ends up in the same panel which shows the response headers,
only that they are now editable to create header overrides.

Video: https://i.imgur.com/nSqcwLj.mp4

Bug: 1297533
Change-Id: I974b3314b504ef69d041495f291dcdee39c4db4d
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/4003964
Auto-Submit: Wolfgang Beyer <[email protected]>
Reviewed-by: Danil Somsikov <[email protected]>
Commit-Queue: Wolfgang Beyer <[email protected]>
diff --git a/front_end/core/i18n/locales/en-US.json b/front_end/core/i18n/locales/en-US.json
index 8a66737..ca77a25 100644
--- a/front_end/core/i18n/locales/en-US.json
+++ b/front_end/core/i18n/locales/en-US.json
@@ -6872,6 +6872,9 @@
   "panels/network/components/HeaderSectionRow.ts | decoded": {
     "message": "Decoded:"
   },
+  "panels/network/components/HeaderSectionRow.ts | editHeader": {
+    "message": "Override header"
+  },
   "panels/network/components/HeaderSectionRow.ts | learnMore": {
     "message": "Learn more"
   },
diff --git a/front_end/core/i18n/locales/en-XL.json b/front_end/core/i18n/locales/en-XL.json
index 42292f5..3246626 100644
--- a/front_end/core/i18n/locales/en-XL.json
+++ b/front_end/core/i18n/locales/en-XL.json
@@ -6872,6 +6872,9 @@
   "panels/network/components/HeaderSectionRow.ts | decoded": {
     "message": "D̂éĉód̂éd̂:"
   },
+  "panels/network/components/HeaderSectionRow.ts | editHeader": {
+    "message": "Ôv́êŕr̂íd̂é ĥéâd́êŕ"
+  },
   "panels/network/components/HeaderSectionRow.ts | learnMore": {
     "message": "L̂éâŕn̂ ḿôŕê"
   },
diff --git a/front_end/panels/network/NetworkLogView.ts b/front_end/panels/network/NetworkLogView.ts
index 8d3bf75..29ca825 100644
--- a/front_end/panels/network/NetworkLogView.ts
+++ b/front_end/panels/network/NetworkLogView.ts
@@ -1719,6 +1719,7 @@
         NetworkForward.UIRequestLocation.UIRequestLocation.responseHeaderMatch(request, {name: '', value: ''});
     const networkPersistanceManager = Persistence.NetworkPersistenceManager.NetworkPersistenceManager.instance();
     if (networkPersistanceManager.project()) {
+      Common.Settings.Settings.instance().moduleSetting('persistenceNetworkOverridesEnabled').set(true);
       await networkPersistanceManager.getOrCreateHeadersUISourceCodeFromUrl(request.url());
       await Common.Revealer.reveal(requestLocation);
     } else {  // If folder for local overrides has not been provided yet
diff --git a/front_end/panels/network/components/HeaderSectionRow.css b/front_end/panels/network/components/HeaderSectionRow.css
index b0feaeb..29143ee 100644
--- a/front_end/panels/network/components/HeaderSectionRow.css
+++ b/front_end/panels/network/components/HeaderSectionRow.css
@@ -169,3 +169,8 @@
   opacity: 100%;
   visibility: visible;
 }
+
+.row:hover .inline-button.enable-editing {
+  opacity: 100%;
+  visibility: visible;
+}
diff --git a/front_end/panels/network/components/HeaderSectionRow.ts b/front_end/panels/network/components/HeaderSectionRow.ts
index 1f7aed2..fe8f0aa 100644
--- a/front_end/panels/network/components/HeaderSectionRow.ts
+++ b/front_end/panels/network/components/HeaderSectionRow.ts
@@ -32,6 +32,10 @@
   */
   decoded: 'Decoded:',
   /**
+  *@description The title of a button to enable overriding a HTTP header.
+  */
+  editHeader: 'Override header',
+  /**
   *@description Text that is usually a hyperlink to more documentation
   */
   learnMore: 'Learn more',
@@ -49,6 +53,7 @@
 const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
 
 const trashIconUrl = new URL('../../../Images/trash_bin_material_icon.svg', import.meta.url).toString();
+const editIconUrl = new URL('../../../Images/edit-icon.svg', import.meta.url).toString();
 
 export class HeaderEditedEvent extends Event {
   static readonly eventName = 'headeredited';
@@ -74,6 +79,14 @@
   }
 }
 
+export class EnableHeaderEditingEvent extends Event {
+  static readonly eventName = 'enableheaderediting';
+
+  constructor() {
+    super(EnableHeaderEditingEvent.eventName, {});
+  }
+}
+
 export interface HeaderSectionRowData {
   header: HeaderDescriptor;
 }
@@ -146,9 +159,24 @@
       return LitHtml.nothing;
     }
     if (!this.#header.valueEditable) {
+      // clang-format off
       return html`
       ${this.#header.value || ''}
       ${this.#maybeRenderHeaderValueSuffix(this.#header)}
+      ${this.#header.isResponseHeader ? html`
+        <${Buttons.Button.Button.litTagName}
+          title=${i18nString(UIStrings.editHeader)}
+          .size=${Buttons.Button.Size.TINY}
+          .iconUrl=${editIconUrl}
+          .variant=${Buttons.Button.Variant.ROUND}
+          .iconWidth=${'13px'}
+          .iconHeight=${'13px'}
+          @click=${(): void => {
+            this.dispatchEvent(new EnableHeaderEditingEvent());
+          }}
+          class="enable-editing inline-button"
+        ></${Buttons.Button.Button.litTagName}>
+      ` : LitHtml.nothing}
     `;
     }
     return html`
@@ -165,6 +193,7 @@
         @click=${this.#onRemoveOverrideClick}
       ></${Buttons.Button.Button.litTagName}>
     `;
+    // clang-format on
   }
 
   focus(): void {
@@ -433,6 +462,7 @@
   headerNotSet?: boolean;
   setCookieBlockedReasons?: Protocol.Network.SetCookieBlockedReason[];
   highlight?: boolean;
+  isResponseHeader?: boolean;
 }
 
 export interface HeaderEditorDescriptor {
diff --git a/front_end/panels/network/components/RequestHeadersView.ts b/front_end/panels/network/components/RequestHeadersView.ts
index 862683c..098c61f 100644
--- a/front_end/panels/network/components/RequestHeadersView.ts
+++ b/front_end/panels/network/components/RequestHeadersView.ts
@@ -185,6 +185,9 @@
         Workspace.Workspace.Events.UISourceCodeAdded, this.#uiSourceCodeAddedOrRemoved, this);
     this.#workspace.addEventListener(
         Workspace.Workspace.Events.UISourceCodeRemoved, this.#uiSourceCodeAddedOrRemoved, this);
+    Common.Settings.Settings.instance()
+        .moduleSetting('persistenceNetworkOverridesEnabled')
+        .addChangeListener(this.#render, this);
   }
 
   disconnectedCallback(): void {
@@ -192,6 +195,9 @@
         Workspace.Workspace.Events.UISourceCodeAdded, this.#uiSourceCodeAddedOrRemoved, this);
     this.#workspace.removeEventListener(
         Workspace.Workspace.Events.UISourceCodeRemoved, this.#uiSourceCodeAddedOrRemoved, this);
+    Common.Settings.Settings.instance()
+        .moduleSetting('persistenceNetworkOverridesEnabled')
+        .removeChangeListener(this.#render, this);
   }
 
   #uiSourceCodeAddedOrRemoved(event: Common.EventTarget.EventTargetEvent<Workspace.UISourceCode.UISourceCode>): void {
diff --git a/front_end/panels/network/components/ResponseHeaderSection.ts b/front_end/panels/network/components/ResponseHeaderSection.ts
index 9a9b87d..10067b5 100644
--- a/front_end/panels/network/components/ResponseHeaderSection.ts
+++ b/front_end/panels/network/components/ResponseHeaderSection.ts
@@ -10,6 +10,8 @@
 import * as NetworkForward from '../../../panels/network/forward/forward.js';
 import * as ComponentHelpers from '../../../ui/components/helpers/helpers.js';
 import * as LitHtml from '../../../ui/lit-html/lit-html.js';
+import * as Sources from '../../../panels/sources/sources.js';
+import * as UI from '../../../ui/legacy/legacy.js';
 
 import {
   type HeaderDescriptor,
@@ -491,8 +493,8 @@
       return;
     }
 
-    const headerDescriptors: HeaderDescriptor[] =
-        this.#headerEditors.map((headerEditor, index) => ({...this.#headerDetails[index], ...headerEditor}));
+    const headerDescriptors: HeaderDescriptor[] = this.#headerEditors.map(
+        (headerEditor, index) => ({...this.#headerDetails[index], ...headerEditor, isResponseHeader: true}));
 
     // Disabled until https://crbug.com/1079231 is fixed.
     // clang-format off
@@ -500,7 +502,7 @@
       ${headerDescriptors.map((header, index) => html`
         <${HeaderSectionRow.litTagName} .data=${{
           header: header,
-        } as HeaderSectionRowData} @headeredited=${this.#onHeaderEdited} @headerremoved=${this.#onHeaderRemoved} data-index=${index}></${HeaderSectionRow.litTagName}>
+        } as HeaderSectionRowData} @headeredited=${this.#onHeaderEdited} @headerremoved=${this.#onHeaderRemoved} @enableheaderediting=${this.#onEnableHeaderEditingClick} data-index=${index}></${HeaderSectionRow.litTagName}>
       `)}
       ${this.#headersAreOverrideable ? html`
         <${Buttons.Button.Button.litTagName}
@@ -516,6 +518,23 @@
     `, this.#shadow, {host: this});
     // clang-format on
   }
+
+  async #onEnableHeaderEditingClick(): Promise<void> {
+    if (!this.#request) {
+      return;
+    }
+    const requestUrl = this.#request.url();
+    const networkPersistanceManager = Persistence.NetworkPersistenceManager.NetworkPersistenceManager.instance();
+    if (networkPersistanceManager.project()) {
+      Common.Settings.Settings.instance().moduleSetting('persistenceNetworkOverridesEnabled').set(true);
+      await networkPersistanceManager.getOrCreateHeadersUISourceCodeFromUrl(requestUrl);
+    } else {  // If folder for local overrides has not been provided yet
+      UI.InspectorView.InspectorView.instance().displaySelectOverrideFolderInfobar(async(): Promise<void> => {
+        await Sources.SourcesNavigator.OverridesNavigatorView.instance().setupNewWorkspace();
+        await networkPersistanceManager.getOrCreateHeadersUISourceCodeFromUrl(requestUrl);
+      });
+    }
+  }
 }
 
 ComponentHelpers.CustomElements.defineComponent('devtools-response-header-section', ResponseHeaderSection);