From 48ad32688a44293fc7fa6feb69e0ed0eb2cfa730 Mon Sep 17 00:00:00 2001 From: Ahmed Awan Date: Wed, 25 Oct 2023 19:39:00 -0500 Subject: [PATCH 1/2] Change `api/tool_panel` to `api/tool_panels/...` As discussed in https://matrix.to/#/!NhNWKNcgINoFZdbUbY:gitter.im/$uw-EVkllUcG91TXjmX5OaicOLHy6zhs5wDHMoPO2cnY?via=gitter.im&via=matrix.org, The new (alternate) tool panel api structure being used in the client now would make more sense if: - Our source of tools for the toolStore is: `api/tools?in_panel=False` - Each panel view (also stored in the toolStore) comes from: `api/tool_panels/{view}` - Dict of available tool panel views comes from: `api/tool_panels` This means, we get rid of the `in_panel` and `view` (and other) params for the `api/tool_panels` api (they still exist for the older `api/tools` api. --- client/src/components/Panels/ToolBox.test.js | 11 +- .../src/components/Panels/utilities.test.js | 58 ++--- .../ToolsSchemaJson/ToolsJson.test.js | 11 +- .../ToolsView/ToolsSchemaJson/ToolsJson.vue | 17 +- .../ToolsView/testData/toolsList.json | 204 +++++++++--------- .../ToolsView/testData/toolsListInPanel.json | 48 ++--- .../Toolshed/RepositoryDetails/Index.test.js | 2 +- client/src/stores/toolStore.ts | 23 +- lib/galaxy/tool_util/toolbox/base.py | 44 ++-- lib/galaxy/webapps/galaxy/api/tools.py | 55 ++--- lib/galaxy/webapps/galaxy/buildapp.py | 4 +- lib/galaxy_test/api/test_tools.py | 14 +- lib/galaxy_test/base/populators.py | 16 +- .../test/functional/test_galaxy_install.py | 8 +- test/integration/test_edam_toolbox.py | 20 +- test/integration/test_panel_views.py | 62 +++--- 16 files changed, 278 insertions(+), 319 deletions(-) diff --git a/client/src/components/Panels/ToolBox.test.js b/client/src/components/Panels/ToolBox.test.js index e8e14f029cf8..6a7e9b3f9ee5 100644 --- a/client/src/components/Panels/ToolBox.test.js +++ b/client/src/components/Panels/ToolBox.test.js @@ -15,8 +15,11 @@ useConfig.mockReturnValue({ }); describe("ToolBox", () => { - const toolsMock = toolsList.tools; - const toolPanelMock = toolsListInPanel.default; + const toolsMock = toolsList.reduce((acc, item) => { + acc[item.id] = item; + return acc; + }, {}); + const toolPanelMock = toolsListInPanel; const resultsMock = ["liftOver1", "__FILTER_EMPTY_DATASETS__", "__UNZIP_COLLECTION__"]; let axiosMock; @@ -26,9 +29,9 @@ describe("ToolBox", () => { it("test filter functions correctly matching: (1) Tools store array-of-objects with (2) Results array", async () => { axiosMock - .onGet(`/api/tool_panel`) + .onGet(`/api/tool_panels/default`) .replyOnce(200, toolsListInPanel) - .onGet(`/api/tool_panel?in_panel=False`) + .onGet(`/api/tools?in_panel=False`) .replyOnce(200, toolsMock) .onGet(/api\/tools?.*/) .replyOnce(200, resultsMock); diff --git a/client/src/components/Panels/utilities.test.js b/client/src/components/Panels/utilities.test.js index dcf5e39c53a0..d743e64c4d98 100644 --- a/client/src/components/Panels/utilities.test.js +++ b/client/src/components/Panels/utilities.test.js @@ -80,8 +80,8 @@ describe("test helpers in tool searching utilities", () => { "__ZIP_COLLECTION__", ], keys: { description: 1, name: 0 }, - tools: Object.values(toolsList.tools), - panel: toolsListInPanel.default, + tools: toolsList, + panel: toolsListInPanel, }, { // name prioritized @@ -93,16 +93,16 @@ describe("test helpers in tool searching utilities", () => { "__FILTER_EMPTY_DATASETS__", ], keys: { description: 0, name: 1 }, - tools: Object.values(toolsList.tools), - panel: toolsListInPanel.default, + tools: toolsList, + panel: toolsListInPanel, }, { // whitespace precedes to ensure query.trim() works q: " filter empty datasets", expectedResults: ["__FILTER_EMPTY_DATASETS__"], keys: { description: 1, name: 2, combined: 0 }, - tools: Object.values(toolsList.tools), - panel: toolsListInPanel.default, + tools: toolsList, + panel: toolsListInPanel, }, { // hyphenated tool-name is searchable @@ -125,16 +125,16 @@ describe("test helpers in tool searching utilities", () => { q: "__ZIP_COLLECTION__", expectedResults: [], keys: { description: 1, name: 2 }, - tools: Object.values(toolsList.tools), - panel: toolsListInPanel.default, + tools: toolsList, + panel: toolsListInPanel, }, { // id is searchable if provided "id:" q: "id:__ZIP_COLLECTION__", expectedResults: ["__ZIP_COLLECTION__"], keys: { description: 1, name: 2 }, - tools: Object.values(toolsList.tools), - panel: toolsListInPanel.default, + tools: toolsList, + panel: toolsListInPanel, }, { // id is searchable if provided "tool_id:" @@ -152,8 +152,8 @@ describe("test helpers in tool searching utilities", () => { q: "filter datasets", expectedResults: ["__FILTER_FAILED_DATASETS__", "__FILTER_EMPTY_DATASETS__"], keys: { combined: 1, wordMatch: 0 }, - tools: Object.values(toolsList.tools), - panel: toolsListInPanel.default, + tools: toolsList, + panel: toolsListInPanel, }, ]; searches.forEach((search) => { @@ -168,38 +168,20 @@ describe("test helpers in tool searching utilities", () => { // Testing if just names work with DL search const filterQueries = ["Fillter", "FILYER", " Fitler", " filtr"]; filterQueries.forEach((q) => { - const { results, closestTerm } = searchToolsByKeys( - Object.values(toolsList.tools), - keys, - q, - "default", - toolsListInPanel.default - ); + const { results, closestTerm } = searchToolsByKeys(toolsList, keys, q, "default", toolsListInPanel); expect(results).toEqual(expectedResults); expect(closestTerm).toEqual("filter"); }); // Testing if names and description function with DL search let queries = ["datases from a collection", "from a colleection", "from a colleection"]; queries.forEach((q) => { - const { results } = searchToolsByKeys( - Object.values(toolsList.tools), - keys, - q, - "default", - toolsListInPanel.default - ); + const { results } = searchToolsByKeys(toolsList, keys, q, "default", toolsListInPanel); expect(results).toEqual(expectedResults); }); // Testing if different length queries correctly trigger changes in max DL distance queries = ["datae", "ppasetsfrom", "datass from a cppollection"]; queries.forEach((q) => { - const { results } = searchToolsByKeys( - Object.values(toolsList.tools), - keys, - q, - "default", - toolsListInPanel.default - ); + const { results } = searchToolsByKeys(toolsList, keys, q, "default", toolsListInPanel); expect(results).toEqual(expectedResults); }); }); @@ -207,16 +189,20 @@ describe("test helpers in tool searching utilities", () => { it("test tool filtering helpers on toolsList given list of ids", async () => { const ids = ["__FILTER_FAILED_DATASETS__", "liftOver1"]; // check length of first section from imported const toolsList - expect(toolsListInPanel.default["collection_operations"].tools).toHaveLength(4); + expect(toolsListInPanel["collection_operations"].tools).toHaveLength(4); // check length of same section from filtered toolsList const matchedTools = ids.map((id) => { return { id: id, sections: [], order: 0 }; }); - const toolResultsPanel = createSortedResultObject(matchedTools, toolsListInPanel.default); + const toolResultsPanel = createSortedResultObject(matchedTools, toolsListInPanel); const toolResultsSection = toolResultsPanel.resultPanel["collection_operations"]; expect(toolResultsSection.tools).toHaveLength(1); // check length of filtered tools (regardless of sections) - const filteredToolIds = Object.keys(filterTools(toolsList.tools, ids)); + const toolsById = toolsList.reduce((acc, item) => { + acc[item.id] = item; + return acc; + }, {}); + const filteredToolIds = Object.keys(filterTools(toolsById, ids)); expect(filteredToolIds).toHaveLength(2); }); }); diff --git a/client/src/components/ToolsView/ToolsSchemaJson/ToolsJson.test.js b/client/src/components/ToolsView/ToolsSchemaJson/ToolsJson.test.js index f798127e6711..f009126de465 100644 --- a/client/src/components/ToolsView/ToolsSchemaJson/ToolsJson.test.js +++ b/client/src/components/ToolsView/ToolsSchemaJson/ToolsJson.test.js @@ -18,15 +18,18 @@ describe("ToolSchemaJson/ToolsView.vue", () => { beforeEach(async () => { axiosMock = new MockAdapter(axios); - axiosMock.onGet("/api/tool_panel?in_panel=False&tool_help=True").reply(200, testToolsListResponse); - axiosMock.onGet("/api/tool_panel").reply(200, testToolsListInPanelResponse); + axiosMock.onGet("/api/tools?in_panel=False&tool_help=True").reply(200, testToolsListResponse); + axiosMock.onGet("/api/tool_panels/default").reply(200, testToolsListInPanelResponse); wrapper = shallowMount(ToolsJson, { localVue }); await flushPromises(); }); it("schema.org script element is created", async () => { - const toolsList = testToolsListResponse.tools; - const toolsListInPanel = testToolsListInPanelResponse.default; + const toolsList = testToolsListResponse.reduce((acc, item) => { + acc[item.id] = item; + return acc; + }, {}); + const toolsListInPanel = testToolsListInPanelResponse; const tools = wrapper.vm.createToolsJson(toolsList, toolsListInPanel); const schemaElement = document.getElementById("schema-json"); const schemaText = JSON.parse(schemaElement.text); diff --git a/client/src/components/ToolsView/ToolsSchemaJson/ToolsJson.vue b/client/src/components/ToolsView/ToolsSchemaJson/ToolsJson.vue index e5e2db7d1fbe..02faf529479f 100644 --- a/client/src/components/ToolsView/ToolsSchemaJson/ToolsJson.vue +++ b/client/src/components/ToolsView/ToolsSchemaJson/ToolsJson.vue @@ -11,20 +11,23 @@ export default { return { schemaTagObj: {} }; }, async created() { - let tools = {}; + let tools = []; await axios - .get(`${getAppRoot()}api/tool_panel?in_panel=False&tool_help=True`) + .get(`${getAppRoot()}api/tools?in_panel=False&tool_help=True`) .then(({ data }) => { - tools = data.tools; + tools = data.reduce((acc, item) => { + acc[item.id] = item; + return acc; + }, {}); }) .catch((error) => { - console.error("All tools by id not loaded", error); + console.error("List of all tools not loaded", error); }); if (Object.keys(tools).length > 0) { await axios - .get(`${getAppRoot()}api/tool_panel`) + .get(`${getAppRoot()}api/tool_panels/default`) .then(({ data }) => { - this.schemaTagObj = this.createToolsJson(tools, data.default); + this.schemaTagObj = this.createToolsJson(tools, data); const el = document.createElement("script"); el.id = "schema-json"; el.type = "application/ld+json"; @@ -32,7 +35,7 @@ export default { document.head.appendChild(el); }) .catch((error) => { - console.error("Tool sections not loaded", error); + console.error("Tool sections by id not loaded", error); }); } }, diff --git a/client/src/components/ToolsView/testData/toolsList.json b/client/src/components/ToolsView/testData/toolsList.json index 61cd0038ad64..cceb264bee95 100644 --- a/client/src/components/ToolsView/testData/toolsList.json +++ b/client/src/components/ToolsView/testData/toolsList.json @@ -1,104 +1,102 @@ -{ - "tools": { - "__UNZIP_COLLECTION__": { - "panel_section_name": "Collection Operations", - "xrefs": [], - "description": "", - "is_workflow_compatible": true, - "labels": [], - "help": "

This tool takes a paired dataset collection and builds two datasets from it. If mapped over a list of paired datasets, this tool will produce two lists of datasets.<\/p>\n


\n

Example<\/strong><\/p>\n

If a collection consists of two forward and two reverse datasets (e.g., forward and reverse reads from a sequencing experiment) this tool will output two collections: one consisting of forward reads and one of reverse reads.<\/p>\n

This tool will create new history datasets from your collection but your quota usage will not increase.<\/p>\n", - "edam_operations": [], - "form_style": "regular", - "edam_topics": [], - "panel_section_id": "collection_operations", - "version": "1.0.0", - "link": "/tool_runner?tool_id=__UNZIP_COLLECTION__", - "target": "galaxy_main", - "min_width": -1, - "model_class": "UnzipCollectionTool", - "hidden": "", - "id": "__UNZIP_COLLECTION__", - "name": "Unzip Collection" - }, - "__ZIP_COLLECTION__": { - "panel_section_name": "Collection Operations", - "xrefs": [], - "description": "", - "is_workflow_compatible": true, - "labels": [], - "help": "

This tool takes two datasets and creates a dataset pair from them. Mapping over two lists, this tool can be used to build a list of dataset pairs from two individual lists of datasets.<\/p>\n


\n

Example<\/strong><\/p>\n

If you have one collection containing only forward reads and one containing only reverse, this tools will "zip" them together into a simple paired collection.<\/p>\n

This tool will create new history datasets for your collection but your quota usage will not increase.<\/p>\n", - "edam_operations": [], - "form_style": "regular", - "edam_topics": [], - "panel_section_id": "collection_operations", - "version": "1.0.0", - "link": "/tool_runner?tool_id=__ZIP_COLLECTION__", - "target": "galaxy_main", - "min_width": -1, - "model_class": "ZipCollectionTool", - "hidden": "", - "id": "__ZIP_COLLECTION__", - "name": "Zip Collection" - }, - "__FILTER_FAILED_DATASETS__": { - "panel_section_name": "Collection Operations", - "xrefs": [], - "description": "datasets from a collection", - "is_workflow_compatible": true, - "labels": [], - "help": "

This tool takes a dataset collection and filters out datasets in the failed state. This is useful for continuing a multi-sample analysis when one of more of the samples fails at some point.<\/p>\n

This tool will create new history datasets from your collection but your quota usage will not increase.<\/p>\n", - "edam_operations": [], - "form_style": "regular", - "edam_topics": [], - "panel_section_id": "collection_operations", - "version": "1.0.0", - "link": "/tool_runner?tool_id=__FILTER_FAILED_DATASETS__", - "target": "galaxy_main", - "min_width": -1, - "model_class": "FilterFailedDatasetsTool", - "hidden": "", - "id": "__FILTER_FAILED_DATASETS__", - "name": "Filter failed" - }, - "__FILTER_EMPTY_DATASETS__": { - "panel_section_name": "Collection Operations", - "xrefs": [], - "description": "datasets from a collection", - "is_workflow_compatible": true, - "labels": [], - "help": "

This tool takes a dataset collection and filters out empty datasets. This is useful for continuing a multi-sample analysis when downstream tools require datasets to have content.<\/p>\n

This tool will create new history datasets from your collection but your quota usage will not increase.<\/p>\n", - "edam_operations": [], - "form_style": "regular", - "edam_topics": [], - "panel_section_id": "collection_operations", - "version": "1.0.0", - "link": "/tool_runner?tool_id=__FILTER_EMPTY_DATASETS__", - "target": "galaxy_main", - "min_width": -1, - "model_class": "FilterEmptyDatasetsTool", - "hidden": "", - "id": "__FILTER_EMPTY_DATASETS__", - "name": "Filter empty" - }, - "liftOver1": { - "panel_section_name": "Lift-Over", - "xrefs": [], - "description": "between assemblies and genomes", - "is_workflow_compatible": true, - "labels": [], - "help": "

Make sure that the genome build of the input dataset is specified (click the pencil icon in the history item to set it if necessary).<\/p>\n

This tool can work with interval, GFF, and GTF datasets. It requires the interval datasets to have chromosome in column 1,\nstart co-ordinate in column 2 and end co-ordinate in column 3. BED comments\nand track and browser lines will be ignored, but if other non-interval lines\nare present the tool will return empty output datasets.<\/p>\n


\n

What it does<\/strong><\/p>\n

This tool is based on the LiftOver utility and Chain track from the UC Santa Cruz Genome Browser<\/a>.<\/p>\n

It converts coordinates and annotations between assemblies and genomes. It produces 2 files, one containing all the mapped coordinates and the other containing the unmapped coordinates, if any.<\/p>\n

\n<\/blockquote>\n
\n

Example<\/strong><\/p>\n

Converting the following hg16 intervals to hg18 intervals:<\/p>\n

\nchrX  85170   112199  AK002185  0  +\nchrX  110458  112199  AK097346  0  +\nchrX  112203  121212  AK074528  0  -\n<\/pre>\n

will produce the following hg18 intervals:<\/p>\n

\nchrX  132991  160020  AK002185  0  +\nchrX  158279  160020  AK097346  0  +\nchrX  160024  169033  AK074528  0  -\n<\/pre>\n",
-      "edam_operations": [],
-      "form_style": "regular",
-      "edam_topics": [],
-      "panel_section_id": "liftOver",
-      "version": "1.0.6",
-      "link": "/tool_runner?tool_id=liftOver1",
-      "target": "galaxy_main",
-      "min_width": -1,
-      "model_class": "Tool",
-      "hidden": "",
-      "id": "liftOver1",
-      "name": "Convert genome coordinates"
-    }
+[
+  {
+    "panel_section_name": "Collection Operations",
+    "xrefs": [],
+    "description": "",
+    "is_workflow_compatible": true,
+    "labels": [],
+    "help": "

This tool takes a paired dataset collection and builds two datasets from it. If mapped over a list of paired datasets, this tool will produce two lists of datasets.<\/p>\n


\n

Example<\/strong><\/p>\n

If a collection consists of two forward and two reverse datasets (e.g., forward and reverse reads from a sequencing experiment) this tool will output two collections: one consisting of forward reads and one of reverse reads.<\/p>\n

This tool will create new history datasets from your collection but your quota usage will not increase.<\/p>\n", + "edam_operations": [], + "form_style": "regular", + "edam_topics": [], + "panel_section_id": "collection_operations", + "version": "1.0.0", + "link": "/tool_runner?tool_id=__UNZIP_COLLECTION__", + "target": "galaxy_main", + "min_width": -1, + "model_class": "UnzipCollectionTool", + "hidden": "", + "id": "__UNZIP_COLLECTION__", + "name": "Unzip Collection" + }, + { + "panel_section_name": "Collection Operations", + "xrefs": [], + "description": "", + "is_workflow_compatible": true, + "labels": [], + "help": "

This tool takes two datasets and creates a dataset pair from them. Mapping over two lists, this tool can be used to build a list of dataset pairs from two individual lists of datasets.<\/p>\n


\n

Example<\/strong><\/p>\n

If you have one collection containing only forward reads and one containing only reverse, this tools will "zip" them together into a simple paired collection.<\/p>\n

This tool will create new history datasets for your collection but your quota usage will not increase.<\/p>\n", + "edam_operations": [], + "form_style": "regular", + "edam_topics": [], + "panel_section_id": "collection_operations", + "version": "1.0.0", + "link": "/tool_runner?tool_id=__ZIP_COLLECTION__", + "target": "galaxy_main", + "min_width": -1, + "model_class": "ZipCollectionTool", + "hidden": "", + "id": "__ZIP_COLLECTION__", + "name": "Zip Collection" + }, + { + "panel_section_name": "Collection Operations", + "xrefs": [], + "description": "datasets from a collection", + "is_workflow_compatible": true, + "labels": [], + "help": "

This tool takes a dataset collection and filters out datasets in the failed state. This is useful for continuing a multi-sample analysis when one of more of the samples fails at some point.<\/p>\n

This tool will create new history datasets from your collection but your quota usage will not increase.<\/p>\n", + "edam_operations": [], + "form_style": "regular", + "edam_topics": [], + "panel_section_id": "collection_operations", + "version": "1.0.0", + "link": "/tool_runner?tool_id=__FILTER_FAILED_DATASETS__", + "target": "galaxy_main", + "min_width": -1, + "model_class": "FilterFailedDatasetsTool", + "hidden": "", + "id": "__FILTER_FAILED_DATASETS__", + "name": "Filter failed" + }, + { + "panel_section_name": "Collection Operations", + "xrefs": [], + "description": "datasets from a collection", + "is_workflow_compatible": true, + "labels": [], + "help": "

This tool takes a dataset collection and filters out empty datasets. This is useful for continuing a multi-sample analysis when downstream tools require datasets to have content.<\/p>\n

This tool will create new history datasets from your collection but your quota usage will not increase.<\/p>\n", + "edam_operations": [], + "form_style": "regular", + "edam_topics": [], + "panel_section_id": "collection_operations", + "version": "1.0.0", + "link": "/tool_runner?tool_id=__FILTER_EMPTY_DATASETS__", + "target": "galaxy_main", + "min_width": -1, + "model_class": "FilterEmptyDatasetsTool", + "hidden": "", + "id": "__FILTER_EMPTY_DATASETS__", + "name": "Filter empty" + }, + { + "panel_section_name": "Lift-Over", + "xrefs": [], + "description": "between assemblies and genomes", + "is_workflow_compatible": true, + "labels": [], + "help": "

Make sure that the genome build of the input dataset is specified (click the pencil icon in the history item to set it if necessary).<\/p>\n

This tool can work with interval, GFF, and GTF datasets. It requires the interval datasets to have chromosome in column 1,\nstart co-ordinate in column 2 and end co-ordinate in column 3. BED comments\nand track and browser lines will be ignored, but if other non-interval lines\nare present the tool will return empty output datasets.<\/p>\n


\n

What it does<\/strong><\/p>\n

This tool is based on the LiftOver utility and Chain track from the UC Santa Cruz Genome Browser<\/a>.<\/p>\n

It converts coordinates and annotations between assemblies and genomes. It produces 2 files, one containing all the mapped coordinates and the other containing the unmapped coordinates, if any.<\/p>\n

\n<\/blockquote>\n
\n

Example<\/strong><\/p>\n

Converting the following hg16 intervals to hg18 intervals:<\/p>\n

\nchrX  85170   112199  AK002185  0  +\nchrX  110458  112199  AK097346  0  +\nchrX  112203  121212  AK074528  0  -\n<\/pre>\n

will produce the following hg18 intervals:<\/p>\n

\nchrX  132991  160020  AK002185  0  +\nchrX  158279  160020  AK097346  0  +\nchrX  160024  169033  AK074528  0  -\n<\/pre>\n",
+    "edam_operations": [],
+    "form_style": "regular",
+    "edam_topics": [],
+    "panel_section_id": "liftOver",
+    "version": "1.0.6",
+    "link": "/tool_runner?tool_id=liftOver1",
+    "target": "galaxy_main",
+    "min_width": -1,
+    "model_class": "Tool",
+    "hidden": "",
+    "id": "liftOver1",
+    "name": "Convert genome coordinates"
   }
-}
+]
diff --git a/client/src/components/ToolsView/testData/toolsListInPanel.json b/client/src/components/ToolsView/testData/toolsListInPanel.json
index ab5d9aa7090e..b2ca87e36281 100644
--- a/client/src/components/ToolsView/testData/toolsListInPanel.json
+++ b/client/src/components/ToolsView/testData/toolsListInPanel.json
@@ -1,28 +1,26 @@
 {
-  "default": {
-    "collection_operations": {
-      "tools": [
-        "__UNZIP_COLLECTION__", "__ZIP_COLLECTION__", "__FILTER_FAILED_DATASETS__", "__FILTER_EMPTY_DATASETS__"
-      ],
-      "model_class": "ToolSection",
-      "version": "",
-      "id": "collection_operations",
-      "name": "Collection Operations"
-    },
-    "liftOver": {
-      "tools": ["liftOver1"],
-      "model_class": "ToolSection",
-      "version": "",
-      "id": "liftOver",
-      "name": "Lift-Over"
-    },
-    "testlabel1": {
-      "model_class": "ToolSectionLabel",
-      "id": "testlabel1",
-      "text": null,
-      "version": "",
-      "description": null,
-      "links": null
-    }
+  "collection_operations": {
+    "tools": [
+      "__UNZIP_COLLECTION__", "__ZIP_COLLECTION__", "__FILTER_FAILED_DATASETS__", "__FILTER_EMPTY_DATASETS__"
+    ],
+    "model_class": "ToolSection",
+    "version": "",
+    "id": "collection_operations",
+    "name": "Collection Operations"
+  },
+  "liftOver": {
+    "tools": ["liftOver1"],
+    "model_class": "ToolSection",
+    "version": "",
+    "id": "liftOver",
+    "name": "Lift-Over"
+  },
+  "testlabel1": {
+    "model_class": "ToolSectionLabel",
+    "id": "testlabel1",
+    "text": null,
+    "version": "",
+    "description": null,
+    "links": null
   }
 }
diff --git a/client/src/components/Toolshed/RepositoryDetails/Index.test.js b/client/src/components/Toolshed/RepositoryDetails/Index.test.js
index e2049ec08aa7..ba48a8ae6901 100644
--- a/client/src/components/Toolshed/RepositoryDetails/Index.test.js
+++ b/client/src/components/Toolshed/RepositoryDetails/Index.test.js
@@ -34,7 +34,7 @@ Services.mockImplementation(() => {
 describe("RepositoryDetails", () => {
     it("test repository details index", async () => {
         const axiosMock = new MockAdapter(axios);
-        axiosMock.onGet("api/tool_panel?in_panel=true&view=default").reply(200, {});
+        axiosMock.onGet("api/tool_panels/default").reply(200, {});
         mockFetcher.path("/api/configuration").method("get").mock({ data: {} });
         const localVue = getLocalVue();
         const pinia = createPinia();
diff --git a/client/src/stores/toolStore.ts b/client/src/stores/toolStore.ts
index f6c969cc46c7..122b07db1a25 100644
--- a/client/src/stores/toolStore.ts
+++ b/client/src/stores/toolStore.ts
@@ -156,9 +156,9 @@ export const useToolStore = defineStore("toolStore", () => {
         if (!loading.value && !allToolsByIdFetched.value) {
             loading.value = true;
             await axios
-                .get(`${getAppRoot()}api/tool_panel?in_panel=False`)
+                .get(`${getAppRoot()}api/tools?in_panel=False`)
                 .then(({ data }) => {
-                    saveAllTools(data.tools);
+                    saveAllTools(data as Tool[]);
                     loading.value = false;
                 })
                 .catch((error) => {
@@ -177,10 +177,10 @@ export const useToolStore = defineStore("toolStore", () => {
                 currentPanelView.value = panelView;
             }
             await axios
-                .get(`${getAppRoot()}api/tool_panel?in_panel=true&view=${panelView}`)
+                .get(`${getAppRoot()}api/tool_panels/${panelView}`)
                 .then(({ data }) => {
                     loading.value = false;
-                    savePanelView(panelView, data[panelView]);
+                    savePanelView(panelView, data);
                 })
                 .catch(async (error) => {
                     loading.value = false;
@@ -199,15 +199,15 @@ export const useToolStore = defineStore("toolStore", () => {
                 return;
             }
             loading.value = true;
-            const { data } = await axios.get(`${getAppRoot()}api/tool_panel?in_panel=true&view=${panelView}`);
-            savePanelView(panelView, data[panelView]);
+            const { data } = await axios.get(`${getAppRoot()}api/tool_panels/${panelView}`);
+            savePanelView(panelView, data);
             loading.value = false;
         }
     }
 
     async function fetchPanel(panelView: string) {
-        const { data } = await axios.get(`${getAppRoot()}api/tool_panel?in_panel=true&view=${panelView}`);
-        savePanelView(panelView, data[panelView]);
+        const { data } = await axios.get(`${getAppRoot()}api/tool_panels/${panelView}`);
+        savePanelView(panelView, data);
     }
 
     function saveToolForId(toolId: string, toolData: Tool) {
@@ -218,8 +218,11 @@ export const useToolStore = defineStore("toolStore", () => {
         Vue.set(toolResults.value, whooshQuery, toolsData);
     }
 
-    function saveAllTools(toolsData: Record) {
-        toolsById.value = toolsData;
+    function saveAllTools(toolsData: Tool[]) {
+        toolsById.value = toolsData.reduce((acc, item) => {
+            acc[item.id] = item;
+            return acc;
+        }, {} as Record);
     }
 
     function savePanelView(panelView: string, newPanel: { [id: string]: ToolSection | Tool }) {
diff --git a/lib/galaxy/tool_util/toolbox/base.py b/lib/galaxy/tool_util/toolbox/base.py
index 793f200dda55..264b96d26d60 100644
--- a/lib/galaxy/tool_util/toolbox/base.py
+++ b/lib/galaxy/tool_util/toolbox/base.py
@@ -1331,38 +1331,24 @@ def to_dict(self, trans, in_panel=True, tool_help=False, view=None, **kwds):
                 rval.append(self.get_tool_to_dict(trans, tool, tool_help=tool_help))
         return rval
 
-    def to_panel_view(self, trans, in_panel=True, tool_help=False, view=None, **kwds):
+    def to_panel_view(self, trans, view="default_panel_view", **kwds):
         """
         Create a panel view representation of the toolbox.
-        For in_panel=True, uses the structure:
-            {view: {section_id: { section but with .tools=List[all tool ids] }, ...}}}
-        For in_panel=False, uses the structure:
-            {tools: {tool_id: tool.to_dict(), ...}}}
+        Uses the structure:
+            {section_id: { section but with .tools=List[all tool ids] }, ...}}
         """
-        rval = {}
-        if in_panel:
-            if view is None:
-                view = self._default_panel_view
-            view_contents: Dict[str, Dict] = {}
-            rval[view] = view_contents
-            panel_elts = self.tool_panel_contents(trans, view=view, **kwds)
-            for elt in panel_elts:
-                # Only use cache for objects that are Tools.
-                if hasattr(elt, "tool_type"):
-                    view_contents[elt.id] = self.get_tool_to_dict(trans, elt, tool_help=False)
-                else:
-                    kwargs = dict(trans=trans, link_details=True, tool_help=tool_help, toolbox=self, only_ids=True)
-                    view_contents[elt.id] = elt.to_dict(**kwargs)
-        else:
-            returned_tools: Dict[str, Dict] = {}
-            rval["tools"] = returned_tools
-            filter_method = self._build_filter_method(trans)
-            for tool in self._tools_by_id.values():
-                tool = filter_method(tool, panel_item_types.TOOL)
-                if not tool:
-                    continue
-                returned_tools[tool.id] = self.get_tool_to_dict(trans, tool, tool_help=tool_help)
-        return rval
+        if view == "default_panel_view":
+            view = self._default_panel_view
+        view_contents: Dict[str, Dict] = {}
+        panel_elts = self.tool_panel_contents(trans, view=view, **kwds)
+        for elt in panel_elts:
+            # Only use cache for objects that are Tools.
+            if hasattr(elt, "tool_type"):
+                view_contents[elt.id] = self.get_tool_to_dict(trans, elt, tool_help=False)
+            else:
+                kwargs = dict(trans=trans, link_details=True, tool_help=False, toolbox=self, only_ids=True)
+                view_contents[elt.id] = elt.to_dict(**kwargs)
+        return view_contents
 
     def _lineage_in_panel(self, panel_dict, tool=None, tool_lineage=None):
         """If tool with same lineage already in panel (or section) - find
diff --git a/lib/galaxy/webapps/galaxy/api/tools.py b/lib/galaxy/webapps/galaxy/api/tools.py
index cbce36d549c2..06763d1fa31f 100644
--- a/lib/galaxy/webapps/galaxy/api/tools.py
+++ b/lib/galaxy/webapps/galaxy/api/tools.py
@@ -178,56 +178,31 @@ def index(self, trans: GalaxyWebTransaction, **kwds):
             raise exceptions.InternalServerError("Error: Could not convert toolbox to dictionary")
 
     @expose_api_anonymous_and_sessionless
-    def panel_view(self, trans: GalaxyWebTransaction, **kwds):
+    def panel_views(self, trans: GalaxyWebTransaction, **kwds):
+        """
+        GET /api/tool_panels
+        returns a dictionary of available tool panel views
         """
-        GET /api/tool_panel
 
-        returns a dictionary of all tools or panels defined by parameters
+        return self.app.toolbox.panel_view_dicts()
+
+    @expose_api_anonymous_and_sessionless
+    def panel_view(self, trans: GalaxyWebTransaction, view, **kwds):
+        """
+        GET /api/tool_panels/{view}
+
+        returns a dictionary of tools and tool sections for the given view
 
-        :param in_panel: if true, tool sections are returned in panel
-                         structure, with tool ids and labels
-        :param view: ToolBox view to apply (default is 'default')
         :param trackster: if true, only tools that are compatible with
                           Trackster are returned
-        :param q: if present search on the given query will be performed
-        :param tool_id: if present the given tool_id will be searched for
-                        all installed versions
         """
 
-        # Read params.
-        in_panel = util.string_as_bool(kwds.get("in_panel", "True"))
+        # Read param.
         trackster = util.string_as_bool(kwds.get("trackster", "False"))
-        q = kwds.get("q", "")
-        tool_id = kwds.get("tool_id", "")
-        tool_help = util.string_as_bool(kwds.get("tool_help", "False"))
-        view = kwds.get("view", None)
-
-        # Find whether to search.
-        if q:
-            hits = self.service._search(q, view)
-            results = []
-            if hits:
-                for hit in hits:
-                    try:
-                        tool = self.service._get_tool(trans, hit, user=trans.user)
-                        if tool:
-                            results.append(tool.id)
-                    except exceptions.AuthenticationFailed:
-                        pass
-                    except exceptions.ObjectNotFound:
-                        pass
-            return results
 
-        # Find whether to detect.
-        if tool_id:
-            detected_versions = self.service._detect(trans, tool_id)
-            return detected_versions
-
-        # Return everything.
+        # Return panel view.
         try:
-            return self.app.toolbox.to_panel_view(
-                trans, in_panel=in_panel, trackster=trackster, tool_help=tool_help, view=view
-            )
+            return self.app.toolbox.to_panel_view(trans, trackster=trackster, view=view)
         except exceptions.MessageException:
             raise
         except Exception:
diff --git a/lib/galaxy/webapps/galaxy/buildapp.py b/lib/galaxy/webapps/galaxy/buildapp.py
index 33f1a85e9928..6dcd4c291613 100644
--- a/lib/galaxy/webapps/galaxy/buildapp.py
+++ b/lib/galaxy/webapps/galaxy/buildapp.py
@@ -367,7 +367,9 @@ def populate_api_routes(webapp, app):
     # ====== TOOLS API ======
     # =======================
 
-    webapp.mapper.connect("/api/tool_panel", action="panel_view", controller="tools")
+    webapp.mapper.connect("/api/tool_panels", action="panel_views", controller="tools")
+    webapp.mapper.connect("/api/tool_panels/{view}", action="panel_view", controller="tools")
+
     webapp.mapper.connect("/api/tools/all_requirements", action="all_requirements", controller="tools")
     webapp.mapper.connect("/api/tools/error_stack", action="error_stack", controller="tools")
     webapp.mapper.connect("/api/tools/{id:.+?}/build", action="build", controller="tools")
diff --git a/lib/galaxy_test/api/test_tools.py b/lib/galaxy_test/api/test_tools.py
index a217c572920c..a50f1c36cc21 100644
--- a/lib/galaxy_test/api/test_tools.py
+++ b/lib/galaxy_test/api/test_tools.py
@@ -164,11 +164,11 @@ def test_search_grep(self):
         assert "Grep1" in get_response
 
     def test_no_panel_index(self):
-        index = self._get("tool_panel", data=dict(in_panel=False))
-        tools_index = index.json().get("tools", {})
-        # No need to flatten out sections, with in_panel=False, only tools by
-        # ids are returned.
-        tool_ids = list(tools_index.keys())
+        index = self._get("tools", data=dict(in_panel=False))
+        tools_index = index.json()
+        # No need to flatten out sections, with in_panel=False, only tools are
+        # returned.
+        tool_ids = [_["id"] for _ in tools_index]
         assert "upload1" in tool_ids
 
     @skip_without_tool("test_sam_to_bam_conversions")
@@ -2640,8 +2640,8 @@ def _run_cat1(self, history_id, inputs, assert_ok=False, **kwargs):
         return self._run("cat1", history_id, inputs, assert_ok=assert_ok, **kwargs)
 
     def __tool_ids(self):
-        index = self._get("tool_panel")
-        tools_index = index.json().get("default", {})
+        index = self._get("tool_panels/default")
+        tools_index = index.json()
         # In panels by default, so flatten out sections...
         tool_ids = []
         for id, tool_or_section in tools_index.items():
diff --git a/lib/galaxy_test/base/populators.py b/lib/galaxy_test/base/populators.py
index 1c948bc88508..cd6d72f9388b 100644
--- a/lib/galaxy_test/base/populators.py
+++ b/lib/galaxy_test/base/populators.py
@@ -52,6 +52,7 @@
 )
 from functools import wraps
 from io import StringIO
+from operator import itemgetter
 from typing import (
     Any,
     Callable,
@@ -154,11 +155,11 @@ def skip_without_tool(tool_id: str):
     def method_wrapper(method):
         def get_tool_ids(api_test_case: HasAnonymousGalaxyInteractor):
             interactor = api_test_case.anonymous_galaxy_interactor
-            index = interactor.get("tool_panel", data=dict(in_panel=False))
+            index = interactor.get("tools", data=dict(in_panel=False))
             api_asserts.assert_status_code_is_ok(index, "Failed to fetch toolbox for target Galaxy.")
-            tools = index.json().get("tools", {})
-            # `in_panel=False`, so we have a toolsById dict...
-            tool_ids = list(tools.keys())
+            tools = index.json()
+            # In panels by default, so flatten out sections...
+            tool_ids = [itemgetter("id")(_) for _ in tools]
             return tool_ids
 
         @wraps(method)
@@ -2287,10 +2288,11 @@ def _run_cwl_tool_job(
 
         if os.path.exists(tool_id):
             raw_tool_id = os.path.basename(tool_id)
-            index = self.dataset_populator._get("tool_panel", data=dict(in_panel=False))
+            index = self.dataset_populator._get("tools", data=dict(in_panel=False))
             index.raise_for_status()
-            tools = index.json().get("tools", {})
-            tool_ids = list(tools.keys())
+            tools = index.json()
+            # In panels by default, so flatten out sections...
+            tool_ids = [itemgetter("id")(_) for _ in tools]
             if raw_tool_id in tool_ids:
                 galaxy_tool_id = raw_tool_id
             else:
diff --git a/lib/tool_shed/test/functional/test_galaxy_install.py b/lib/tool_shed/test/functional/test_galaxy_install.py
index 21cc8b1dfc93..227e41aa49c1 100644
--- a/lib/tool_shed/test/functional/test_galaxy_install.py
+++ b/lib/tool_shed/test/functional/test_galaxy_install.py
@@ -10,10 +10,10 @@ def test_install_simple_tool(self):
         installable_revisions = populator.get_ordered_installable_revisions(owner, name)
         latest_install_revision = installable_revisions.__root__[-1]
         self.install_repository(owner, name, latest_install_revision, tool_shed_url=self.url)
-        response = self.galaxy_interactor._get("tool_panel?in_panel=False")
+        response = self.galaxy_interactor._get("tools?in_panel=False")
         response.raise_for_status()
         expected_tool = populator.tool_guid(self, repository, "Add_a_column1", "1.1.0")
-        tool_ids = list((response.json().get("tools", {})).keys())
+        tool_ids = [t["id"] for t in response.json()]
         assert expected_tool in tool_ids, f"Didn't find {expected_tool} in {tool_ids}"
 
     def test_install_simple_after_repository_metadata_reset(self):
@@ -26,8 +26,8 @@ def test_install_simple_after_repository_metadata_reset(self):
         metadata_response = populator.reset_metadata(repository)
         assert metadata_response.status == "ok"
         self.install_repository(owner, name, latest_install_revision, tool_shed_url=self.url)
-        response = self.galaxy_interactor._get("tool_panel?in_panel=False")
+        response = self.galaxy_interactor._get("tools?in_panel=False")
         response.raise_for_status()
         expected_tool = f"{self.host}:{self.port}/repos/{owner}/{name}/Add_a_column1/1.1.0"
-        tool_ids = list((response.json().get("tools", {})).keys())
+        tool_ids = [t["id"] for t in response.json()]
         assert expected_tool in tool_ids, f"Didn't find {expected_tool} in {tool_ids}"
diff --git a/test/integration/test_edam_toolbox.py b/test/integration/test_edam_toolbox.py
index 05b923c544ae..33da7f23f176 100644
--- a/test/integration/test_edam_toolbox.py
+++ b/test/integration/test_edam_toolbox.py
@@ -10,9 +10,9 @@ def handle_galaxy_config_kwds(cls, config):
         config["edam_panel_views"] = "merged"
 
     def test_edam_toolbox(self):
-        index = self.galaxy_interactor.get("tool_panel", data=dict(in_panel=True, view="ontology:edam_merged"))
+        index = self.galaxy_interactor.get("tool_panels/ontology:edam_merged")
         index.raise_for_status()
-        index_panel = index.json().get("ontology:edam_merged", {})
+        index_panel = index.json()
         sections = [x for _, x in index_panel.items() if x["model_class"] == "ToolSection"]
         section_names = [s["name"] for s in sections]
         assert "Mapping" in section_names
@@ -21,9 +21,9 @@ def test_edam_toolbox(self):
         # make sure our mapper tool was mapped using Edam correctly...
         assert [x for x in mapping_section_tools if x == "mapper"]
 
-        config_index = self.galaxy_interactor.get("configuration")
-        config_index.raise_for_status()
-        panel_views = config_index.json()["panel_views"]
+        tool_panels = self.galaxy_interactor.get("tool_panels")
+        tool_panels.raise_for_status()
+        panel_views = tool_panels.json()
         assert len(panel_views) > 1
         assert isinstance(panel_views, dict)
         edam_panel_view = panel_views["ontology:edam_merged"]
@@ -40,9 +40,9 @@ def handle_galaxy_config_kwds(cls, config):
         config["default_panel_view"] = "ontology:edam_topics"
 
     def test_edam_toolbox(self):
-        index = self.galaxy_interactor.get("tool_panel", data=dict(in_panel=True))
+        index = self.galaxy_interactor.get("tool_panels/default_panel_view")
         index.raise_for_status()
-        index_panel = index.json().get("ontology:edam_topics", {})
+        index_panel = index.json()
         sections = [x for _, x in index_panel.items() if x["model_class"] == "ToolSection"]
         section_names = [s["name"] for s in sections]
         assert "Mapping" in section_names
@@ -51,9 +51,9 @@ def test_edam_toolbox(self):
         # make sure our mapper tool was mapped using Edam correctly...
         assert [x for x in mapping_section_tools if x == "mapper"]
 
-        config_index = self.galaxy_interactor.get("configuration")
-        config_index.raise_for_status()
-        panel_views = config_index.json()["panel_views"]
+        tool_panels = self.galaxy_interactor.get("tool_panels")
+        tool_panels.raise_for_status()
+        panel_views = tool_panels.json()
         assert len(panel_views) > 1
         assert isinstance(panel_views, dict)
         edam_panel_view = panel_views["ontology:edam_topics"]
diff --git a/test/integration/test_panel_views.py b/test/integration/test_panel_views.py
index e1c60e0cb938..a1b42f3e0d9d 100644
--- a/test/integration/test_panel_views.py
+++ b/test/integration/test_panel_views.py
@@ -16,21 +16,21 @@ def handle_galaxy_config_kwds(cls, config):
         config["panel_views_dir"] = PANEL_VIEWS_DIR_1
 
     def test_section_copy(self):
-        index = self.galaxy_interactor.get("tool_panel", data=dict(in_panel=True, view="filter"))
-        index_panel = index.json().get("filter", {})
+        index = self.galaxy_interactor.get("tool_panels/filter")
+        index_panel = index.json()
         sections = get_sections(index_panel)
         section_names = [s["name"] for s in sections]
         assert len(section_names) == 1
         assert "For Tours" in section_names
 
     def test_custom_label_order(self):
-        index = self.galaxy_interactor.get("tool_panel", data=dict(in_panel=True, view="my-custom"))
+        index = self.galaxy_interactor.get("tool_panels/my-custom")
         verify_my_custom(index)
 
     def test_filtering_sections_by_tool_id(self):
-        index = self.galaxy_interactor.get("tool_panel", data=dict(in_panel=True, view="custom_2"))
+        index = self.galaxy_interactor.get("tool_panels/custom_2")
         index.raise_for_status()
-        index_panel = index.json().get("custom_2", {})
+        index_panel = index.json()
         sections = get_sections(index_panel)
         assert len(sections) == 1
         section = sections[0]
@@ -38,22 +38,22 @@ def test_filtering_sections_by_tool_id(self):
         assert len(tools) == 3, len(tools)
 
     def test_filtering_sections_by_tool_id_regex(self):
-        index = self.galaxy_interactor.get("tool_panel", data=dict(in_panel=True, view="custom_3"))
-        verify_custom_regex_filtered(index, "custom_3")
+        index = self.galaxy_interactor.get("tool_panels/custom_3")
+        verify_custom_regex_filtered(index)
 
     def test_filtering_root_by_type(self):
-        index = self.galaxy_interactor.get("tool_panel", data=dict(in_panel=True, view="custom_4"))
+        index = self.galaxy_interactor.get("tool_panels/custom_4")
         index.raise_for_status()
-        index_panel = index.json().get("custom_4", {})
+        index_panel = index.json()
         assert len(index_panel) == 2
         # Labels are filtered out...
         assert model_classes(index_panel) == ["Tool", "Tool"]
         assert list(index_panel.keys()) == ["empty_list", "count_list"]
 
     def test_custom_section_def(self):
-        index = self.galaxy_interactor.get("tool_panel", data=dict(in_panel=True, view="custom_6"))
+        index = self.galaxy_interactor.get("tool_panels/custom_6")
         index.raise_for_status()
-        index_panel = index.json().get("custom_6", {})
+        index_panel = index.json()
         assert len(index_panel) == 1
         assert model_classes(index_panel) == ["ToolSection"]
         section = list(index_panel.values())[0]
@@ -67,13 +67,13 @@ def test_custom_section_def(self):
         ]
 
     def test_section_embed(self):
-        index = self.galaxy_interactor.get("tool_panel", data=dict(in_panel=True, view="custom_5"))
-        verify_custom_embed(index, "custom_5")
+        index = self.galaxy_interactor.get("tool_panels/custom_5")
+        verify_custom_embed(index)
 
     def test_section_embed_filtering(self):
-        index = self.galaxy_interactor.get("tool_panel", data=dict(in_panel=True, view="custom_7"))
+        index = self.galaxy_interactor.get("tool_panels/custom_7")
         index.raise_for_status()
-        index_panel = index.json().get("custom_7", {})
+        index_panel = index.json()
         assert len(index_panel) == 1
         assert model_classes(index_panel) == ["ToolSection"]
         section = list(index_panel.values())[0]
@@ -84,28 +84,28 @@ def test_section_embed_filtering(self):
         assert section_elems[3]["model_class"] == "ToolSectionLabel"
 
     def test_section_reference_by_name(self):
-        index = self.galaxy_interactor.get("tool_panel", data=dict(in_panel=True, view="custom_8"))
-        verify_custom_embed(index, "custom_8")
+        index = self.galaxy_interactor.get("tool_panels/custom_8")
+        verify_custom_embed(index)
 
     def test_section_alias(self):
-        index = self.galaxy_interactor.get("tool_panel", data=dict(in_panel=True, view="custom_9"))
-        verify_custom_regex_filtered(index, "custom_9")
+        index = self.galaxy_interactor.get("tool_panels/custom_9")
+        verify_custom_regex_filtered(index)
 
     def test_expand_section_aliases(self):
-        index = self.galaxy_interactor.get("tool_panel", data=dict(in_panel=True, view="custom_10"))
+        index = self.galaxy_interactor.get("tool_panels/custom_10")
         index.raise_for_status()
-        index_panel = index.json().get("custom_10", {})
+        index_panel = index.json()
         assert len(index_panel) == 2
         assert model_classes(index_panel) == ["ToolSection", "ToolSection"]
 
     def test_global_filters(self):
-        index = self.galaxy_interactor.get("tool_panel", data=dict(in_panel=True, view="custom_11"))
-        verify_custom_regex_filtered(index, "custom_11")
+        index = self.galaxy_interactor.get("tool_panels/custom_11")
+        verify_custom_regex_filtered(index)
 
     def test_global_filters_on_integrated_panel(self):
-        index = self.galaxy_interactor.get("tool_panel", data=dict(in_panel=True, view="custom_12"))
+        index = self.galaxy_interactor.get("tool_panels/custom_12")
         index.raise_for_status()
-        index_panel = index.json().get("custom_12", {})
+        index_panel = index.json()
         sections = get_sections(index_panel)
         assert len(sections) == 2
         assert "test" in index_panel
@@ -153,13 +153,13 @@ def handle_galaxy_config_kwds(cls, config):
         config["default_panel_view"] = "my-custom"
 
     def test_custom_label_order(self):
-        index = self.galaxy_interactor.get("tool_panel", data=dict(in_panel=True))
+        index = self.galaxy_interactor.get("tool_panels/default_panel_view")
         verify_my_custom(index)
 
 
 def verify_my_custom(index):
     index.raise_for_status()
-    index_panel = index.json().get("my-custom", {})
+    index_panel = index.json()
     sections = get_sections(index_panel)
     assert len(sections) == 0
 
@@ -167,10 +167,10 @@ def verify_my_custom(index):
     assert model_classes(index_panel) == ["ToolSectionLabel", "Tool", "ToolSectionLabel", "Tool", "ToolSectionLabel"]
 
 
-def verify_custom_embed(index, view):
+def verify_custom_embed(index):
     # custom_5 / custom_8
     index.raise_for_status()
-    index_panel = index.json().get(view, {})
+    index_panel = index.json()
     assert len(index_panel) == 1
     assert model_classes(index_panel) == ["ToolSection"]
     assert "my-new-section" in index_panel
@@ -188,10 +188,10 @@ def verify_custom_embed(index, view):
     ]
 
 
-def verify_custom_regex_filtered(index, view):
+def verify_custom_regex_filtered(index):
     # custom_3 / custom_9
     index.raise_for_status()
-    index_panel = index.json().get(view, {})
+    index_panel = index.json()
     sections = get_sections(index_panel)
     assert len(sections) == 1
     section = sections[0]

From 34b02964aa3970ca3e1ffb6d86c4a2fa67834437 Mon Sep 17 00:00:00 2001
From: Ahmed Awan 
Date: Thu, 26 Oct 2023 12:50:31 -0500
Subject: [PATCH 2/2] change `api/tool_panels` from dict of views to dict
 containing `default_panel_view` and `views` dict

use this api instead of waiting for `isConfigLoaded` in `ToolPanel`
---
 client/src/components/Panels/ToolPanel.vue | 49 ++++++++++++----------
 lib/galaxy/webapps/galaxy/api/tools.py     |  7 +++-
 test/integration/test_edam_toolbox.py      | 11 +++--
 3 files changed, 40 insertions(+), 27 deletions(-)

diff --git a/client/src/components/Panels/ToolPanel.vue b/client/src/components/Panels/ToolPanel.vue
index ed166d8fc29b..a4e1a3e9bd18 100644
--- a/client/src/components/Panels/ToolPanel.vue
+++ b/client/src/components/Panels/ToolPanel.vue
@@ -1,8 +1,9 @@