Add network request context menu entry for creating header override

This CL adds the ability to create header overrides from the list of
network requests in the network panel via the context menu.

If a local folder for storing local overrides has already been
configured beforehand, the context menu takes the user straight to the
'.headers' file where header overrides are to be specified. If
overrides have not been configured before, the user has to name a
local folder for storing overrides first.

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

Bug: 1297533
Change-Id: I8b31fe41d922e1662da68320fcc188c596c0a058
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/3542245
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 6641ee2..4a5f40f 100644
--- a/front_end/core/i18n/locales/en-US.json
+++ b/front_end/core/i18n/locales/en-US.json
@@ -7025,6 +7025,9 @@
   "panels/network/NetworkLogView.ts | copyStacktrace": {
     "message": "Copy stack trace"
   },
+  "panels/network/NetworkLogView.ts | createResponseHeaderOverride": {
+    "message": "Create response header override"
+  },
   "panels/network/NetworkLogView.ts | domcontentloadedS": {
     "message": "DOMContentLoaded: {PH1}"
   },
@@ -12497,6 +12500,12 @@
   "ui/legacy/InspectorView.ts | reloadDevtools": {
     "message": "Reload DevTools"
   },
+  "ui/legacy/InspectorView.ts | selectFolder": {
+    "message": "Select folder"
+  },
+  "ui/legacy/InspectorView.ts | selectOverrideFolder": {
+    "message": "Select a folder to store override files in."
+  },
   "ui/legacy/InspectorView.ts | setToBrowserLanguage": {
     "message": "Always match Chrome's language"
   },
diff --git a/front_end/core/i18n/locales/en-XL.json b/front_end/core/i18n/locales/en-XL.json
index da2b4d2..082fffa 100644
--- a/front_end/core/i18n/locales/en-XL.json
+++ b/front_end/core/i18n/locales/en-XL.json
@@ -7025,6 +7025,9 @@
   "panels/network/NetworkLogView.ts | copyStacktrace": {
     "message": "Ĉóp̂ý ŝt́âćk̂ t́r̂áĉé"
   },
+  "panels/network/NetworkLogView.ts | createResponseHeaderOverride": {
+    "message": "Ĉŕêát̂é r̂éŝṕôńŝé ĥéâd́êŕ ôv́êŕr̂íd̂é"
+  },
   "panels/network/NetworkLogView.ts | domcontentloadedS": {
     "message": "D̂ÓM̂Ćôńt̂én̂t́L̂óâd́êd́: {PH1}"
   },
@@ -12497,6 +12500,12 @@
   "ui/legacy/InspectorView.ts | reloadDevtools": {
     "message": "R̂él̂óâd́ D̂év̂T́ôól̂ś"
   },
+  "ui/legacy/InspectorView.ts | selectFolder": {
+    "message": "Ŝél̂éĉt́ f̂ól̂d́êŕ"
+  },
+  "ui/legacy/InspectorView.ts | selectOverrideFolder": {
+    "message": "Ŝél̂éĉt́ â f́ôĺd̂ér̂ t́ô śt̂ór̂é ôv́êŕr̂íd̂é f̂íl̂éŝ ín̂."
+  },
   "ui/legacy/InspectorView.ts | setToBrowserLanguage": {
     "message": "Âĺŵáŷś m̂át̂ćĥ Ćĥŕôḿê'ś l̂án̂ǵûáĝé"
   },
diff --git a/front_end/models/persistence/NetworkPersistenceManager.ts b/front_end/models/persistence/NetworkPersistenceManager.ts
index 16c54d6..b095316 100644
--- a/front_end/models/persistence/NetworkPersistenceManager.ts
+++ b/front_end/models/persistence/NetworkPersistenceManager.ts
@@ -272,6 +272,26 @@
         (this.projectInternal as FileSystem).fileSystemPath(), '/', this.encodedPathFromUrl(url, ignoreInactive));
   }
 
+  private getHeadersUISourceCodeFromUrl(url: Platform.DevToolsPath.UrlString): Workspace.UISourceCode.UISourceCode
+      |null {
+    const fileUrlFromRequest = this.fileUrlFromNetworkUrl(url, /* ignoreNoActive */ true);
+    const folderUrlFromRequest =
+        Common.ParsedURL.ParsedURL.substring(fileUrlFromRequest, 0, fileUrlFromRequest.lastIndexOf('/'));
+    const headersFileUrl = Common.ParsedURL.ParsedURL.concatenate(folderUrlFromRequest, '/', HEADERS_FILENAME);
+    return Workspace.Workspace.WorkspaceImpl.instance().uiSourceCodeForURL(headersFileUrl);
+  }
+
+  async getOrCreateHeadersUISourceCodeFromUrl(url: Platform.DevToolsPath.UrlString):
+      Promise<Workspace.UISourceCode.UISourceCode|null> {
+    let uiSourceCode = this.getHeadersUISourceCodeFromUrl(url);
+    if (!uiSourceCode && this.projectInternal) {
+      const encodedFilePath = this.encodedPathFromUrl(url, /* ignoreNoActive */ true);
+      const encodedPath = Common.ParsedURL.ParsedURL.substring(encodedFilePath, 0, encodedFilePath.lastIndexOf('/'));
+      uiSourceCode = await this.projectInternal.createFile(encodedPath, HEADERS_FILENAME, '');
+    }
+    return uiSourceCode;
+  }
+
   private decodeLocalPathToUrlPath(path: string): string {
     try {
       return unescape(path);
diff --git a/front_end/panels/network/NetworkLogView.ts b/front_end/panels/network/NetworkLogView.ts
index cdedfdd..9d5fca9 100644
--- a/front_end/panels/network/NetworkLogView.ts
+++ b/front_end/panels/network/NetworkLogView.ts
@@ -42,8 +42,10 @@
 import * as HAR from '../../models/har/har.js';
 import * as IssuesManager from '../../models/issues_manager/issues_manager.js';
 import * as Logs from '../../models/logs/logs.js';
+import * as Persistence from '../../models/persistence/persistence.js';
 import * as TextUtils from '../../models/text_utils/text_utils.js';
 import * as NetworkForward from '../../panels/network/forward/forward.js';
+import * as Sources from '../../panels/sources/sources.js';
 import * as DataGrid from '../../ui/legacy/components/data_grid/data_grid.js';
 import * as PerfUI from '../../ui/legacy/components/perf_ui/perf_ui.js';
 import * as Components from '../../ui/legacy/components/utils/utils.js';
@@ -346,6 +348,11 @@
   *@description Text in Network Log View of the Network panel
   */
   areYouSureYouWantToClearBrowserCookies: 'Are you sure you want to clear browser cookies?',
+  /**
+  *@description A context menu item in the Network Log View of the Network panel
+  * for creating a header override
+  */
+  createResponseHeaderOverride: 'Create response header override',
 };
 const str_ = i18n.i18n.registerUIStrings('panels/network/NetworkLogView.ts', UIStrings);
 const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
@@ -1576,6 +1583,10 @@
 
     contextMenu.saveSection().appendItem(i18nString(UIStrings.saveAllAsHarWithContent), this.exportAll.bind(this));
 
+    contextMenu.editSection().appendItem(
+        i18nString(UIStrings.createResponseHeaderOverride),
+        this.#handleCreateResponseHeaderOverrideClick.bind(this, request));
+    contextMenu.editSection().appendSeparator();
     contextMenu.editSection().appendItem(i18nString(UIStrings.clearBrowserCache), this.clearBrowserCache.bind(this));
     contextMenu.editSection().appendItem(
         i18nString(UIStrings.clearBrowserCookies), this.clearBrowserCookies.bind(this));
@@ -1691,6 +1702,27 @@
     void stream.close();
   }
 
+  async #handleCreateResponseHeaderOverrideClick(request: SDK.NetworkRequest.NetworkRequest): Promise<void> {
+    if (Persistence.NetworkPersistenceManager.NetworkPersistenceManager.instance().project()) {
+      await this.#revealHeaderOverrideEditor(request);
+    } 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 this.#revealHeaderOverrideEditor(request);
+      });
+    }
+  }
+
+  async #revealHeaderOverrideEditor(request: SDK.NetworkRequest.NetworkRequest): Promise<void> {
+    const networkPersistanceManager = Persistence.NetworkPersistenceManager.NetworkPersistenceManager.instance();
+    const uiSourceCode = await networkPersistanceManager.getOrCreateHeadersUISourceCodeFromUrl(request.url());
+    if (uiSourceCode) {
+      const sourcesPanel = Sources.SourcesPanel.SourcesPanel.instance();
+      sourcesPanel.showUISourceCode(uiSourceCode);
+      sourcesPanel.revealInNavigator(uiSourceCode);
+    }
+  }
+
   private clearBrowserCache(): void {
     if (confirm(i18nString(UIStrings.areYouSureYouWantToClearBrowser))) {
       SDK.NetworkManager.MultitargetNetworkManager.instance().clearBrowserCache();
diff --git a/front_end/panels/sources/SourcesNavigator.ts b/front_end/panels/sources/SourcesNavigator.ts
index 90f4eda..26f2f2e 100644
--- a/front_end/panels/sources/SourcesNavigator.ts
+++ b/front_end/panels/sources/SourcesNavigator.ts
@@ -278,7 +278,7 @@
     this.toolbar.appendToolbarItem(setupButton);
   }
 
-  private async setupNewWorkspace(): Promise<void> {
+  async setupNewWorkspace(): Promise<void> {
     const fileSystem =
         await Persistence.IsolatedFileSystemManager.IsolatedFileSystemManager.instance().addFileSystem('overrides');
     if (!fileSystem) {
diff --git a/front_end/panels/sources/SourcesPanel.ts b/front_end/panels/sources/SourcesPanel.ts
index 04bd6a88..43cffde 100644
--- a/front_end/panels/sources/SourcesPanel.ts
+++ b/front_end/panels/sources/SourcesPanel.ts
@@ -528,7 +528,7 @@
     this.showUISourceCode(uiLocation.uiSourceCode, uiLocation.lineNumber, uiLocation.columnNumber, omitFocus);
   }
 
-  private revealInNavigator(uiSourceCode: Workspace.UISourceCode.UISourceCode, skipReveal?: boolean): void {
+  revealInNavigator(uiSourceCode: Workspace.UISourceCode.UISourceCode, skipReveal?: boolean): void {
     for (const navigator of registeredNavigatorViews) {
       const navigatorView = navigator.navigatorView();
       const viewId = navigator.viewId;
diff --git a/front_end/ui/legacy/InspectorView.ts b/front_end/ui/legacy/InspectorView.ts
index af9de1a..e12eca1 100644
--- a/front_end/ui/legacy/InspectorView.ts
+++ b/front_end/ui/legacy/InspectorView.ts
@@ -115,6 +115,15 @@
   *@description The aria label for the drawer hidden.
   */
   drawerHidden: 'Drawer hidden',
+  /**
+  * @description Request for the user to select a local file system folder for DevTools
+  * to store local overrides in.
+  */
+  selectOverrideFolder: 'Select a folder to store override files in.',
+  /**
+  *@description Label for a button which opens a file picker.
+  */
+  selectFolder: 'Select folder',
 };
 const str_ = i18n.i18n.registerUIStrings('ui/legacy/InspectorView.ts', UIStrings);
 const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
@@ -133,6 +142,7 @@
   private focusRestorer?: WidgetFocusRestorer|null;
   private ownerSplitWidget?: SplitWidget;
   private reloadRequiredInfobar?: Infobar;
+  #selectOverrideFolderInfobar?: Infobar;
 
   constructor() {
     super();
@@ -439,6 +449,25 @@
     }
   }
 
+  displaySelectOverrideFolderInfobar(callback: () => void): void {
+    if (!this.#selectOverrideFolderInfobar) {
+      const infobar = new Infobar(InfobarType.Info, i18nString(UIStrings.selectOverrideFolder), [
+        {
+          text: i18nString(UIStrings.selectFolder),
+          highlight: true,
+          delegate: (): void => callback(),
+          dismiss: true,
+        },
+      ]);
+      infobar.setParentView(this);
+      this.attachInfobar(infobar);
+      this.#selectOverrideFolderInfobar = infobar;
+      infobar.setCloseCallback(() => {
+        this.#selectOverrideFolderInfobar = undefined;
+      });
+    }
+  }
+
   private createInfoBarDiv(): void {
     if (!this.infoBarDiv) {
       this.infoBarDiv = document.createElement('div');