From ecd42bbef27be77ccce2b8d92ddbff2b3e02db1f Mon Sep 17 00:00:00 2001 From: ankurjuneja Date: Mon, 29 Jun 2026 13:21:40 -0700 Subject: [PATCH 1/8] Cross-folder instrument utilization report --- .../InstrumentUtilizationByDay.query.xml | 24 ++ .../targetedms/InstrumentUtilizationByDay.sql | 23 ++ .../InstrumentUtilizationByMonth.query.xml | 24 ++ .../InstrumentUtilizationByMonth.sql | 22 ++ resources/views/gen/instrumentScheduler.html | 2 + .../views/gen/instrumentScheduler.view.xml | 22 ++ .../views/gen/instrumentSchedulerDev.html | 4 + .../views/gen/instrumentSchedulerDev.view.xml | 18 ++ .../targetedms/TargetedMSController.java | 59 ++++- .../view/instrumentUtilizationCalendar.jsp | 246 ++++++++++++++++++ .../view/instrumentUtilizationGrids.jsp | 55 ++++ 11 files changed, 498 insertions(+), 1 deletion(-) create mode 100644 resources/queries/targetedms/InstrumentUtilizationByDay.query.xml create mode 100644 resources/queries/targetedms/InstrumentUtilizationByDay.sql create mode 100644 resources/queries/targetedms/InstrumentUtilizationByMonth.query.xml create mode 100644 resources/queries/targetedms/InstrumentUtilizationByMonth.sql create mode 100644 resources/views/gen/instrumentScheduler.html create mode 100644 resources/views/gen/instrumentScheduler.view.xml create mode 100644 resources/views/gen/instrumentSchedulerDev.html create mode 100644 resources/views/gen/instrumentSchedulerDev.view.xml create mode 100644 src/org/labkey/targetedms/view/instrumentUtilizationCalendar.jsp create mode 100644 src/org/labkey/targetedms/view/instrumentUtilizationGrids.jsp diff --git a/resources/queries/targetedms/InstrumentUtilizationByDay.query.xml b/resources/queries/targetedms/InstrumentUtilizationByDay.query.xml new file mode 100644 index 000000000..eb4cdfaee --- /dev/null +++ b/resources/queries/targetedms/InstrumentUtilizationByDay.query.xml @@ -0,0 +1,24 @@ + + + + + Runs by Day + + + Date + Date + + + Runs + + + Files + + + Instrument + + +
+
+
+
diff --git a/resources/queries/targetedms/InstrumentUtilizationByDay.sql b/resources/queries/targetedms/InstrumentUtilizationByDay.sql new file mode 100644 index 000000000..c45310152 --- /dev/null +++ b/resources/queries/targetedms/InstrumentUtilizationByDay.sql @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2026 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ + +-- Number of sample files and runs acquired per day, by instrument, across all folders the user can read. +-- Consumed by the instrument utilization calendar and the "Runs by Day" grid on the Show Instrument page. +SELECT + CAST(AcquisitionDay AS TIMESTAMP) AS AcquisitionDate, + COUNT(*) AS FileCount, + COUNT(DISTINCT RunId) AS RunCount, + InstrumentNickname +FROM + (SELECT + CAST(YEAR(AcquiredTime) AS VARCHAR) || '-' || + (CASE WHEN MONTH(AcquiredTime) < 10 THEN '0' ELSE '' END) || CAST(MONTH(AcquiredTime) AS VARCHAR) || '-' || + (CASE WHEN DAYOFMONTH(AcquiredTime) < 10 THEN '0' ELSE '' END) || CAST(DAYOFMONTH(AcquiredTime) AS VARCHAR) AS AcquisitionDay, + ReplicateId.RunId AS RunId, + InstrumentNickname + FROM targetedms.SampleFile + WHERE AcquiredTime IS NOT NULL) X +GROUP BY AcquisitionDay, InstrumentNickname diff --git a/resources/queries/targetedms/InstrumentUtilizationByMonth.query.xml b/resources/queries/targetedms/InstrumentUtilizationByMonth.query.xml new file mode 100644 index 000000000..5199207f2 --- /dev/null +++ b/resources/queries/targetedms/InstrumentUtilizationByMonth.query.xml @@ -0,0 +1,24 @@ + + + + + Runs by Month + + + Month + yyyy-MM + + + Runs + + + Files + + + Instrument + + +
+
+
+
diff --git a/resources/queries/targetedms/InstrumentUtilizationByMonth.sql b/resources/queries/targetedms/InstrumentUtilizationByMonth.sql new file mode 100644 index 000000000..16f18fa62 --- /dev/null +++ b/resources/queries/targetedms/InstrumentUtilizationByMonth.sql @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2026 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 + */ + +-- Number of sample files and runs acquired per month, by instrument, across all folders the user can read. +-- Consumed by the "Runs by Month" grid on the Show Instrument page. +SELECT + CAST(MonthStart || '-01' AS TIMESTAMP) AS MonthStart, + COUNT(*) AS FileCount, + COUNT(DISTINCT RunId) AS RunCount, + InstrumentNickname +FROM + (SELECT + CAST(YEAR(AcquiredTime) AS VARCHAR) || '-' || + (CASE WHEN MONTH(AcquiredTime) < 10 THEN '0' ELSE '' END) || CAST(MONTH(AcquiredTime) AS VARCHAR) AS MonthStart, + ReplicateId.RunId AS RunId, + InstrumentNickname + FROM targetedms.SampleFile + WHERE AcquiredTime IS NOT NULL) X +GROUP BY MonthStart, InstrumentNickname diff --git a/resources/views/gen/instrumentScheduler.html b/resources/views/gen/instrumentScheduler.html new file mode 100644 index 000000000..c3694f398 --- /dev/null +++ b/resources/views/gen/instrumentScheduler.html @@ -0,0 +1,2 @@ +
+ diff --git a/resources/views/gen/instrumentScheduler.view.xml b/resources/views/gen/instrumentScheduler.view.xml new file mode 100644 index 000000000..9df3059b0 --- /dev/null +++ b/resources/views/gen/instrumentScheduler.view.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/views/gen/instrumentSchedulerDev.html b/resources/views/gen/instrumentSchedulerDev.html new file mode 100644 index 000000000..8a31be6f0 --- /dev/null +++ b/resources/views/gen/instrumentSchedulerDev.html @@ -0,0 +1,4 @@ +
+ + + diff --git a/resources/views/gen/instrumentSchedulerDev.view.xml b/resources/views/gen/instrumentSchedulerDev.view.xml new file mode 100644 index 000000000..e25c8b743 --- /dev/null +++ b/resources/views/gen/instrumentSchedulerDev.view.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/org/labkey/targetedms/TargetedMSController.java b/src/org/labkey/targetedms/TargetedMSController.java index e7f80dcd9..1b6dc5f85 100644 --- a/src/org/labkey/targetedms/TargetedMSController.java +++ b/src/org/labkey/targetedms/TargetedMSController.java @@ -4815,6 +4815,8 @@ public ShowInstrumentAction() } private static final String FOLDER_SUMMARY = "FolderSummary"; + private static final String UTILIZATION_BY_DAY = "UtilizationByDay"; + private static final String UTILIZATION_BY_MONTH = "UtilizationByMonth"; private InstrumentForm _form; @@ -4846,11 +4848,29 @@ protected QueryView createQueryView(InstrumentForm form, BindException errors, b TargetedMSSchema schema = new TargetedMSSchema(getUser(), getContainer()); return schema.createView(getViewContext(), settings, errors); } + if (UTILIZATION_BY_DAY.equalsIgnoreCase(dataRegion)) + { + QuerySettings settings = new QuerySettings(getViewContext(), UTILIZATION_BY_DAY, "InstrumentUtilizationByDay"); + settings.setBaseSort(new Sort("-AcquisitionDate")); + settings.setBaseFilter(new SimpleFilter(FieldKey.fromParts("InstrumentNickname"), form.getName())); + settings.setContainerFilterName(ContainerFilter.Type.AllFolders.name()); + TargetedMSSchema schema = new TargetedMSSchema(getUser(), getContainer()); + return schema.createView(getViewContext(), settings, errors); + } + if (UTILIZATION_BY_MONTH.equalsIgnoreCase(dataRegion)) + { + QuerySettings settings = new QuerySettings(getViewContext(), UTILIZATION_BY_MONTH, "InstrumentUtilizationByMonth"); + settings.setBaseSort(new Sort("-MonthStart")); + settings.setBaseFilter(new SimpleFilter(FieldKey.fromParts("InstrumentNickname"), form.getName())); + settings.setContainerFilterName(ContainerFilter.Type.AllFolders.name()); + TargetedMSSchema schema = new TargetedMSSchema(getUser(), getContainer()); + return schema.createView(getViewContext(), settings, errors); + } throw new NotFoundException("Unknown dataRegion: " + dataRegion); } @Override - public ModelAndView getView(InstrumentForm form, BindException errors) + public ModelAndView getView(InstrumentForm form, BindException errors) throws Exception { if (form.getName() == null) { @@ -4876,6 +4896,21 @@ public ModelAndView getView(InstrumentForm form, BindException errors) result.addView(nameView); } + var calendarView = new JspView<>("/org/labkey/targetedms/view/instrumentUtilizationCalendar.jsp", form.getName()); + calendarView.setTitle("Utilization Calendar"); + calendarView.setFrame(WebPartView.FrameType.PORTAL); + result.addView(calendarView); + + QueryView byDayView = createInitializedQueryView(form, errors, false, UTILIZATION_BY_DAY); + byDayView.setFrame(WebPartView.FrameType.NONE); + QueryView byMonthView = createInitializedQueryView(form, errors, false, UTILIZATION_BY_MONTH); + byMonthView.setFrame(WebPartView.FrameType.NONE); + + var utilizationView = new JspView<>("/org/labkey/targetedms/view/instrumentUtilizationGrids.jsp", new InstrumentUtilizationBean(byDayView, byMonthView)); + utilizationView.setTitle("Runs Acquired"); + utilizationView.setFrame(WebPartView.FrameType.PORTAL); + result.addView(utilizationView); + QueryView folderSummaryView = createQueryView(form, errors, false, FOLDER_SUMMARY); folderSummaryView.setTitle("Summary by Folder"); folderSummaryView.setFrame(WebPartView.FrameType.PORTAL); @@ -4891,6 +4926,28 @@ public ModelAndView getView(InstrumentForm form, BindException errors) } } + public static class InstrumentUtilizationBean + { + private final QueryView _byDayView; + private final QueryView _byMonthView; + + public InstrumentUtilizationBean(QueryView byDayView, QueryView byMonthView) + { + _byDayView = byDayView; + _byMonthView = byMonthView; + } + + public QueryView getByDayView() + { + return _byDayView; + } + + public QueryView getByMonthView() + { + return _byMonthView; + } + } + @RequiresPermission(ReadPermission.class) public class ShowReplicatesAction extends ShowRunSingleDetailsAction { diff --git a/src/org/labkey/targetedms/view/instrumentUtilizationCalendar.jsp b/src/org/labkey/targetedms/view/instrumentUtilizationCalendar.jsp new file mode 100644 index 000000000..0a2f5adae --- /dev/null +++ b/src/org/labkey/targetedms/view/instrumentUtilizationCalendar.jsp @@ -0,0 +1,246 @@ +<% + /* + * Copyright (c) 2026 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +%> +<%@ page import="org.labkey.api.view.template.ClientDependencies" %> +<%@ page extends="org.labkey.api.jsp.JspBase" %> +<%! + @Override + public void addClientDependencies(ClientDependencies dependencies) + { + dependencies.add("internal/jQuery"); + dependencies.add("targetedms/yearCalendar"); + } +%> + + + + + +
+ + +
+ +
+
+ Loading... +
+
+ + diff --git a/src/org/labkey/targetedms/view/instrumentUtilizationGrids.jsp b/src/org/labkey/targetedms/view/instrumentUtilizationGrids.jsp new file mode 100644 index 000000000..d047598e7 --- /dev/null +++ b/src/org/labkey/targetedms/view/instrumentUtilizationGrids.jsp @@ -0,0 +1,55 @@ +<% + /* + * Copyright (c) 2026 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +%> +<%@ page import="org.labkey.api.view.HttpView" %> +<%@ page import="org.labkey.api.view.JspView" %> +<%@ page import="org.labkey.targetedms.TargetedMSController.InstrumentUtilizationBean" %> +<%@ page extends="org.labkey.api.jsp.JspBase" %> +<% + JspView me = HttpView.currentView(); + InstrumentUtilizationBean bean = me.getModelBean(); +%> +
+ + +
+ +
+ <% me.include(bean.getByDayView(), out); %> +
+ + + From aea27c35e666bd9d014e04cc8fa55346469dbd66 Mon Sep 17 00:00:00 2001 From: ankurjuneja Date: Wed, 1 Jul 2026 03:17:27 -0700 Subject: [PATCH 2/8] add selenium testing --- .../InstrumentUtilizationWebPart.java | 104 +++++++++++ .../TargetedMSInstrumentUtilizationTest.java | 162 ++++++++++++++++++ 2 files changed, 266 insertions(+) create mode 100644 test/src/org/labkey/test/components/targetedms/InstrumentUtilizationWebPart.java create mode 100644 test/src/org/labkey/test/tests/targetedms/TargetedMSInstrumentUtilizationTest.java diff --git a/test/src/org/labkey/test/components/targetedms/InstrumentUtilizationWebPart.java b/test/src/org/labkey/test/components/targetedms/InstrumentUtilizationWebPart.java new file mode 100644 index 000000000..01864ead5 --- /dev/null +++ b/test/src/org/labkey/test/components/targetedms/InstrumentUtilizationWebPart.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2026 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.labkey.test.components.targetedms; + +import org.labkey.test.Locator; +import org.labkey.test.WebDriverWrapper; +import org.labkey.test.components.BodyWebPart; +import org.labkey.test.util.DataRegionTable; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; + +/** + * The "Runs Acquired" web part on the Show Instrument page. It shows an instrument's utilization, + * aggregated across all readable folders, as a grid of runs/files by day, with a client-side toggle + * to switch to a by-month summary. + */ +public class InstrumentUtilizationWebPart extends BodyWebPart +{ + public static final String DEFAULT_TITLE = "Runs Acquired"; + public static final String BY_DAY_REGION = "UtilizationByDay"; + public static final String BY_MONTH_REGION = "UtilizationByMonth"; + public static final String FILES_COLUMN = "Files"; + + public InstrumentUtilizationWebPart(WebDriver driver) + { + super(driver, DEFAULT_TITLE); + WebDriverWrapper.waitFor(() -> elementCache().byDayToggle.isDisplayed(), + "Runs Acquired web part did not load", getWrapper().defaultWaitForPage); + } + + public boolean isByDayVisible() + { + return elementCache().byDayGrid.isDisplayed(); + } + + public boolean isByMonthVisible() + { + return elementCache().byMonthGrid.isDisplayed(); + } + + public InstrumentUtilizationWebPart showByDay() + { + if (!isByDayVisible()) + elementCache().byDayToggle.click(); + WebDriverWrapper.waitFor(this::isByDayVisible, "By Day grid did not become visible", 5000); + return this; + } + + public InstrumentUtilizationWebPart showByMonth() + { + if (!isByMonthVisible()) + elementCache().byMonthToggle.click(); + WebDriverWrapper.waitFor(this::isByMonthVisible, "By Month grid did not become visible", 5000); + return this; + } + + public DataRegionTable getByDayTable() + { + return new DataRegionTable(BY_DAY_REGION, getDriver()); + } + + public DataRegionTable getByMonthTable() + { + return new DataRegionTable(BY_MONTH_REGION, getDriver()); + } + + /** Sum of the (integer) "Files" column, i.e. the total number of sample files represented by the grid. */ + public int getTotalFiles(DataRegionTable table) + { + int total = 0; + for (String value : table.getColumnDataAsText(FILES_COLUMN)) + { + total += Integer.parseInt(value.trim()); + } + return total; + } + + @Override + protected Elements newElementCache() + { + return new Elements(); + } + + public class Elements extends BodyWebPart.ElementCache + { + final WebElement byDayToggle = Locator.id("utilizationToggleDay").findWhenNeeded(this); + final WebElement byMonthToggle = Locator.id("utilizationToggleMonth").findWhenNeeded(this); + final WebElement byDayGrid = Locator.id("utilizationByDayGrid").findWhenNeeded(this); + final WebElement byMonthGrid = Locator.id("utilizationByMonthGrid").findWhenNeeded(this); + } +} diff --git a/test/src/org/labkey/test/tests/targetedms/TargetedMSInstrumentUtilizationTest.java b/test/src/org/labkey/test/tests/targetedms/TargetedMSInstrumentUtilizationTest.java new file mode 100644 index 000000000..e2b869b77 --- /dev/null +++ b/test/src/org/labkey/test/tests/targetedms/TargetedMSInstrumentUtilizationTest.java @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2026 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.labkey.test.tests.targetedms; + +import org.jetbrains.annotations.Nullable; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.labkey.remoteapi.CommandException; +import org.labkey.remoteapi.query.Filter; +import org.labkey.remoteapi.query.SelectRowsCommand; +import org.labkey.remoteapi.query.SelectRowsResponse; +import org.labkey.test.BaseWebDriverTest; +import org.labkey.test.Locator; +import org.labkey.test.WebTestHelper; +import org.labkey.test.components.targetedms.InstrumentUtilizationWebPart; +import org.labkey.test.util.DataRegionTable; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * Verifies the instrument-scoped, cross-folder utilization views on the Show Instrument page: the + * "Utilization Calendar" web part and the "Runs Acquired" by-day/by-month grids. The same Skyline + * document is imported into two folders so we can assert that the views aggregate across every folder + * the user can read, not just the current one. + */ +@Category({}) +@BaseWebDriverTest.ClassTimeout(minutes = 5) +public class TargetedMSInstrumentUtilizationTest extends TargetedMSTest +{ + private static final String QC_SUBFOLDER = "QC Subfolder"; + + private String _instrumentName; + + @Override + protected @Nullable String getProjectName() + { + return getClass().getSimpleName() + " Project"; + } + + @BeforeClass + public static void initProject() + { + TargetedMSInstrumentUtilizationTest init = getCurrentTest(); + init.doInit(); + } + + private void doInit() + { + setupFolder(FolderType.QC); + setupSubfolder(getProjectName(), QC_SUBFOLDER, FolderType.QC); + + // Import the same document into two folders so the same instrument has data in both + goToProjectHome(); + importData(SProCoP_FILE); + + clickFolder(QC_SUBFOLDER); + importData(SProCoP_FILE); + } + + @Test + public void testCrossFolderInstrumentUtilization() throws IOException, CommandException + { + _instrumentName = getInstrumentNickname(); + + // The two folders hold identical imports, so the cross-folder total should be exactly double + // the count in a single folder. + int singleFolderFileCount = getFileCount(getProjectName()); + int expectedCrossFolderFileCount = singleFolderFileCount * 2; + assertTrue("Expected the single folder to contain sample files with acquisition times", singleFolderFileCount > 0); + + beginAt(WebTestHelper.buildURL("targetedms", getProjectName(), "showInstrument", Map.of("name", _instrumentName))); + + verifyCalendar(); + verifyRunsAcquiredGrids(expectedCrossFolderFileCount); + } + + private void verifyCalendar() + { + log("Verifying the cross-folder Utilization Calendar renders"); + assertElementPresent(Locator.tagWithClass("span", "labkey-wp-title-text").withText("Utilization Calendar")); + + // Wait for the async selectRows call to populate the calendar with day cells + waitForElement(Locator.tagWithClassContaining("div", "day-content")); + assertEquals("Calendar should default to a single month", + "1 month", getSelectedOptionText(Locator.id("utilizationMonthNumberSelect"))); + } + + private void verifyRunsAcquiredGrids(int expectedCrossFolderFileCount) + { + InstrumentUtilizationWebPart utilization = new InstrumentUtilizationWebPart(getDriver()); + + log("Verifying the By Day grid is shown by default and aggregates across folders"); + assertTrue("By Day grid should be visible by default", utilization.isByDayVisible()); + assertFalse("By Month grid should be hidden by default", utilization.isByMonthVisible()); + + DataRegionTable byDay = utilization.getByDayTable(); + assertTrue("By Day grid is missing expected columns", + byDay.getColumnLabels().containsAll(List.of("Date", "Runs", "Files"))); + assertEquals("By Day grid should sum files across both folders", + expectedCrossFolderFileCount, utilization.getTotalFiles(byDay)); + + log("Toggling to the By Month summary"); + utilization.showByMonth(); + assertTrue("By Month grid should be visible after toggling", utilization.isByMonthVisible()); + assertFalse("By Day grid should be hidden after toggling", utilization.isByDayVisible()); + + DataRegionTable byMonth = utilization.getByMonthTable(); + assertTrue("By Month grid is missing expected columns", + byMonth.getColumnLabels().containsAll(List.of("Month", "Runs", "Files"))); + // Grouping by month rather than day changes the row count but not the overall file total + assertEquals("By Month grid should sum to the same cross-folder file total", + expectedCrossFolderFileCount, utilization.getTotalFiles(byMonth)); + + log("Toggling back to the By Day grid"); + utilization.showByDay(); + assertTrue("By Day grid should be visible after toggling back", utilization.isByDayVisible()); + assertFalse("By Month grid should be hidden after toggling back", utilization.isByMonthVisible()); + } + + /** @return the default nickname (model - serial number) for the instrument that acquired the imported data */ + private String getInstrumentNickname() throws IOException, CommandException + { + SelectRowsCommand command = new SelectRowsCommand("targetedms", "SampleFile"); + command.setColumns(List.of("InstrumentNickname")); + command.setMaxRows(1); + SelectRowsResponse response = command.execute(createDefaultConnection(), getProjectName()); + assertFalse("No sample files were imported", response.getRows().isEmpty()); + return (String) response.getRows().get(0).get("InstrumentNickname"); + } + + /** @return the number of sample files (with an acquisition time) for the instrument in a single container */ + private int getFileCount(String containerPath) throws IOException, CommandException + { + SelectRowsCommand command = new SelectRowsCommand("targetedms", "SampleFile"); + command.setColumns(List.of("Id")); + command.setFilters(List.of( + new Filter("InstrumentNickname", _instrumentName), + new Filter("AcquiredTime", null, Filter.Operator.NONBLANK))); + SelectRowsResponse response = command.execute(createDefaultConnection(), containerPath); + return response.getRowCount().intValue(); + } +} From 425c0b298dbd9bf3a8dc8523df8366d63260da4a Mon Sep 17 00:00:00 2001 From: ankurjuneja Date: Wed, 1 Jul 2026 03:24:16 -0700 Subject: [PATCH 3/8] Remove stray instrumentScheduler generated view stubs These generated view files were accidentally included in the cross-folder instrument utilization commit and are unrelated to that feature. Co-Authored-By: Claude Opus 4.8 --- resources/views/gen/instrumentScheduler.html | 2 -- .../views/gen/instrumentScheduler.view.xml | 22 ------------------- .../views/gen/instrumentSchedulerDev.html | 4 ---- .../views/gen/instrumentSchedulerDev.view.xml | 18 --------------- 4 files changed, 46 deletions(-) delete mode 100644 resources/views/gen/instrumentScheduler.html delete mode 100644 resources/views/gen/instrumentScheduler.view.xml delete mode 100644 resources/views/gen/instrumentSchedulerDev.html delete mode 100644 resources/views/gen/instrumentSchedulerDev.view.xml diff --git a/resources/views/gen/instrumentScheduler.html b/resources/views/gen/instrumentScheduler.html deleted file mode 100644 index c3694f398..000000000 --- a/resources/views/gen/instrumentScheduler.html +++ /dev/null @@ -1,2 +0,0 @@ -
- diff --git a/resources/views/gen/instrumentScheduler.view.xml b/resources/views/gen/instrumentScheduler.view.xml deleted file mode 100644 index 9df3059b0..000000000 --- a/resources/views/gen/instrumentScheduler.view.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/resources/views/gen/instrumentSchedulerDev.html b/resources/views/gen/instrumentSchedulerDev.html deleted file mode 100644 index 8a31be6f0..000000000 --- a/resources/views/gen/instrumentSchedulerDev.html +++ /dev/null @@ -1,4 +0,0 @@ -
- - - diff --git a/resources/views/gen/instrumentSchedulerDev.view.xml b/resources/views/gen/instrumentSchedulerDev.view.xml deleted file mode 100644 index e25c8b743..000000000 --- a/resources/views/gen/instrumentSchedulerDev.view.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - From 6b371ccec7380a4667f622c5ee53b9f457390e60 Mon Sep 17 00:00:00 2001 From: labkey-jeckels Date: Thu, 9 Jul 2026 18:58:22 -0700 Subject: [PATCH 4/8] Optimize query a bit --- src/org/labkey/targetedms/query/SampleFileTable.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/org/labkey/targetedms/query/SampleFileTable.java b/src/org/labkey/targetedms/query/SampleFileTable.java index 56cd1e8d1..62ef9a9b8 100644 --- a/src/org/labkey/targetedms/query/SampleFileTable.java +++ b/src/org/labkey/targetedms/query/SampleFileTable.java @@ -184,7 +184,10 @@ public SampleFileTable(TargetedMSSchema schema, ContainerFilter cf, @Nullable Ta // Do a query to look across the entire server for samples where the name matches with names from the // targetedms.SampleFiles table, as currently filtered by the SampleFileTable (container and/or run) SQLFragment sql = new SQLFragment("SELECT DISTINCT m.Container, m.CpasType FROM (\n"); - sql.append("SELECT COALESCE(ra.value, sf.SampleId, sf.SampleName) AS SampleIdentifier FROM \n"); + // DISTINCT collapses the many sample files that share an identifier down to the distinct set, so the + // planner builds the hash on this small side and scans exp.material once as the probe (instead of + // hashing and spilling the entire, server-wide material table). + sql.append("SELECT DISTINCT COALESCE(ra.value, sf.SampleId, sf.SampleName) AS SampleIdentifier FROM \n"); sql.append(TargetedMSManager.getTableInfoSampleFile(), "sf"); sql.append(" INNER JOIN \n"); sql.append(TargetedMSManager.getTableInfoReplicate(), "rep"); From 9a4a095fd427b2d859bb763d43fdc5b7570df82c Mon Sep 17 00:00:00 2001 From: ankurjuneja Date: Tue, 14 Jul 2026 17:39:54 -0700 Subject: [PATCH 5/8] Combine instrument utilization views into tabbed web part; pair info panel --- .../targetedms/TargetedMSController.java | 48 +++++-- .../targetedms/view/instrumentInfoRow.jsp | 76 +++++++++++ ...Calendar.jsp => instrumentUtilization.jsp} | 122 +++++++++++++----- .../view/instrumentUtilizationGrids.jsp | 55 -------- .../InstrumentUtilizationWebPart.java | 51 +++++--- .../TargetedMSInstrumentUtilizationTest.java | 36 +++--- 6 files changed, 257 insertions(+), 131 deletions(-) create mode 100644 src/org/labkey/targetedms/view/instrumentInfoRow.jsp rename src/org/labkey/targetedms/view/{instrumentUtilizationCalendar.jsp => instrumentUtilization.jsp} (68%) delete mode 100644 src/org/labkey/targetedms/view/instrumentUtilizationGrids.jsp diff --git a/src/org/labkey/targetedms/TargetedMSController.java b/src/org/labkey/targetedms/TargetedMSController.java index 1b6dc5f85..8414ed3c9 100644 --- a/src/org/labkey/targetedms/TargetedMSController.java +++ b/src/org/labkey/targetedms/TargetedMSController.java @@ -4888,38 +4888,40 @@ public ModelAndView getView(InstrumentForm form, BindException errors) throws Ex VBox result = new VBox(); + VBox instrumentInfoView = new VBox(); for (InstrumentNickname name : names) { var nameView = new JspView<>("/org/labkey/targetedms/view/nickname.jsp", name); nameView.setTitle("Instrument Info"); nameView.setFrame(WebPartView.FrameType.PORTAL); - result.addView(nameView); + instrumentInfoView.addView(nameView); } - var calendarView = new JspView<>("/org/labkey/targetedms/view/instrumentUtilizationCalendar.jsp", form.getName()); - calendarView.setTitle("Utilization Calendar"); - calendarView.setFrame(WebPartView.FrameType.PORTAL); - result.addView(calendarView); + QueryView folderSummaryView = createQueryView(form, errors, false, FOLDER_SUMMARY); + folderSummaryView.setTitle("Summary by Folder"); + folderSummaryView.setFrame(WebPartView.FrameType.PORTAL); + + // Instrument info is narrow; pair it with the folder summary grid in a two-column row so the + // otherwise-empty space to the right of the info panel is put to use. + var infoRowView = new JspView<>("/org/labkey/targetedms/view/instrumentInfoRow.jsp", + new InstrumentInfoRowBean(instrumentInfoView, folderSummaryView)); + infoRowView.setFrame(WebPartView.FrameType.NONE); + result.addView(infoRowView); QueryView byDayView = createInitializedQueryView(form, errors, false, UTILIZATION_BY_DAY); byDayView.setFrame(WebPartView.FrameType.NONE); QueryView byMonthView = createInitializedQueryView(form, errors, false, UTILIZATION_BY_MONTH); byMonthView.setFrame(WebPartView.FrameType.NONE); - var utilizationView = new JspView<>("/org/labkey/targetedms/view/instrumentUtilizationGrids.jsp", new InstrumentUtilizationBean(byDayView, byMonthView)); - utilizationView.setTitle("Runs Acquired"); + var utilizationView = new JspView<>("/org/labkey/targetedms/view/instrumentUtilization.jsp", new InstrumentUtilizationBean(byDayView, byMonthView)); + utilizationView.setTitle("Instrument Utilization Across Folders"); utilizationView.setFrame(WebPartView.FrameType.PORTAL); result.addView(utilizationView); - QueryView folderSummaryView = createQueryView(form, errors, false, FOLDER_SUMMARY); - folderSummaryView.setTitle("Summary by Folder"); - folderSummaryView.setFrame(WebPartView.FrameType.PORTAL); - QueryView sampleFileView = createQueryView(form, errors, false, TargetedMSSchema.TABLE_SAMPLE_FILE); sampleFileView.setTitle("Samples from " + form.getName()); sampleFileView.setFrame(WebPartView.FrameType.PORTAL); - result.addView(folderSummaryView); result.addView(sampleFileView); return result; @@ -4948,6 +4950,28 @@ public QueryView getByMonthView() } } + public static class InstrumentInfoRowBean + { + private final HttpView _infoView; + private final HttpView _summaryView; + + public InstrumentInfoRowBean(HttpView infoView, HttpView summaryView) + { + _infoView = infoView; + _summaryView = summaryView; + } + + public HttpView getInfoView() + { + return _infoView; + } + + public HttpView getSummaryView() + { + return _summaryView; + } + } + @RequiresPermission(ReadPermission.class) public class ShowReplicatesAction extends ShowRunSingleDetailsAction { diff --git a/src/org/labkey/targetedms/view/instrumentInfoRow.jsp b/src/org/labkey/targetedms/view/instrumentInfoRow.jsp new file mode 100644 index 000000000..ffe50ad64 --- /dev/null +++ b/src/org/labkey/targetedms/view/instrumentInfoRow.jsp @@ -0,0 +1,76 @@ +<% + /* + * Copyright (c) 2026 LabKey Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +%> +<%@ page import="org.labkey.api.view.HttpView" %> +<%@ page import="org.labkey.api.view.JspView" %> +<%@ page import="org.labkey.targetedms.TargetedMSController.InstrumentInfoRowBean" %> +<%@ page extends="org.labkey.api.jsp.JspBase" %> +<% + JspView me = HttpView.currentView(); + InstrumentInfoRowBean bean = me.getModelBean(); +%> +
+
+ <% me.include(bean.getInfoView(), out); %> +
+
+ <% me.include(bean.getSummaryView(), out); %> +
+
+ + diff --git a/src/org/labkey/targetedms/view/instrumentUtilizationCalendar.jsp b/src/org/labkey/targetedms/view/instrumentUtilization.jsp similarity index 68% rename from src/org/labkey/targetedms/view/instrumentUtilizationCalendar.jsp rename to src/org/labkey/targetedms/view/instrumentUtilization.jsp index 0a2f5adae..75b5a7b0a 100644 --- a/src/org/labkey/targetedms/view/instrumentUtilizationCalendar.jsp +++ b/src/org/labkey/targetedms/view/instrumentUtilization.jsp @@ -15,7 +15,10 @@ * limitations under the License. */ %> +<%@ page import="org.labkey.api.view.HttpView" %> +<%@ page import="org.labkey.api.view.JspView" %> <%@ page import="org.labkey.api.view.template.ClientDependencies" %> +<%@ page import="org.labkey.targetedms.TargetedMSController.InstrumentUtilizationBean" %> <%@ page extends="org.labkey.api.jsp.JspBase" %> <%! @Override @@ -25,6 +28,10 @@ dependencies.add("targetedms/yearCalendar"); } %> +<% + JspView me = HttpView.currentView(); + InstrumentUtilizationBean bean = me.getModelBean(); +%> + + +
+
+
+ + +
+ +
+
+ Loading... +
+
+ + +
+ +
+
+ <% me.include(bean.getByMonthView(), out); %> +
+
+ +
+
+ <% me.include(bean.getByDayView(), out); %> +
+
+
+ + + - -
- - -
- -
-
- Loading... -
-
- - diff --git a/src/org/labkey/targetedms/view/instrumentUtilizationGrids.jsp b/src/org/labkey/targetedms/view/instrumentUtilizationGrids.jsp deleted file mode 100644 index d047598e7..000000000 --- a/src/org/labkey/targetedms/view/instrumentUtilizationGrids.jsp +++ /dev/null @@ -1,55 +0,0 @@ -<% - /* - * Copyright (c) 2026 LabKey Corporation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -%> -<%@ page import="org.labkey.api.view.HttpView" %> -<%@ page import="org.labkey.api.view.JspView" %> -<%@ page import="org.labkey.targetedms.TargetedMSController.InstrumentUtilizationBean" %> -<%@ page extends="org.labkey.api.jsp.JspBase" %> -<% - JspView me = HttpView.currentView(); - InstrumentUtilizationBean bean = me.getModelBean(); -%> -
- - -
- -
- <% me.include(bean.getByDayView(), out); %> -
- - - diff --git a/test/src/org/labkey/test/components/targetedms/InstrumentUtilizationWebPart.java b/test/src/org/labkey/test/components/targetedms/InstrumentUtilizationWebPart.java index 01864ead5..726db5295 100644 --- a/test/src/org/labkey/test/components/targetedms/InstrumentUtilizationWebPart.java +++ b/test/src/org/labkey/test/components/targetedms/InstrumentUtilizationWebPart.java @@ -23,13 +23,13 @@ import org.openqa.selenium.WebElement; /** - * The "Runs Acquired" web part on the Show Instrument page. It shows an instrument's utilization, - * aggregated across all readable folders, as a grid of runs/files by day, with a client-side toggle - * to switch to a by-month summary. + * The "Instrument Utilization Across Folders" web part on the Show Instrument page. It shows an + * instrument's utilization, aggregated across all readable folders, in three tabs: a heatmap calendar, + * a runs-by-month grid, and a runs-by-day grid. Only the active tab's pane is visible at a time. */ public class InstrumentUtilizationWebPart extends BodyWebPart { - public static final String DEFAULT_TITLE = "Runs Acquired"; + public static final String DEFAULT_TITLE = "Instrument Utilization Across Folders"; public static final String BY_DAY_REGION = "UtilizationByDay"; public static final String BY_MONTH_REGION = "UtilizationByMonth"; public static final String FILES_COLUMN = "Files"; @@ -37,43 +37,60 @@ public class InstrumentUtilizationWebPart extends BodyWebPart elementCache().byDayToggle.isDisplayed(), - "Runs Acquired web part did not load", getWrapper().defaultWaitForPage); + WebDriverWrapper.waitFor(() -> elementCache().calendarTab.isDisplayed(), + "Instrument utilization web part did not load", getWrapper().defaultWaitForPage); + } + + public boolean isCalendarVisible() + { + return elementCache().calendarPane.isDisplayed(); } public boolean isByDayVisible() { - return elementCache().byDayGrid.isDisplayed(); + return elementCache().byDayPane.isDisplayed(); } public boolean isByMonthVisible() { - return elementCache().byMonthGrid.isDisplayed(); + return elementCache().byMonthPane.isDisplayed(); + } + + public InstrumentUtilizationWebPart showCalendar() + { + if (!isCalendarVisible()) + elementCache().calendarTab.click(); + WebDriverWrapper.waitFor(this::isCalendarVisible, "Calendar tab did not become visible", 5000); + return this; } public InstrumentUtilizationWebPart showByDay() { if (!isByDayVisible()) - elementCache().byDayToggle.click(); - WebDriverWrapper.waitFor(this::isByDayVisible, "By Day grid did not become visible", 5000); + elementCache().byDayTab.click(); + WebDriverWrapper.waitFor(this::isByDayVisible, "By Day tab did not become visible", 5000); return this; } public InstrumentUtilizationWebPart showByMonth() { if (!isByMonthVisible()) - elementCache().byMonthToggle.click(); - WebDriverWrapper.waitFor(this::isByMonthVisible, "By Month grid did not become visible", 5000); + elementCache().byMonthTab.click(); + WebDriverWrapper.waitFor(this::isByMonthVisible, "By Month tab did not become visible", 5000); return this; } + /** Selects the By Day tab (a hidden data region reports no cell text) and returns its grid. */ public DataRegionTable getByDayTable() { + showByDay(); return new DataRegionTable(BY_DAY_REGION, getDriver()); } + /** Selects the By Month tab (a hidden data region reports no cell text) and returns its grid. */ public DataRegionTable getByMonthTable() { + showByMonth(); return new DataRegionTable(BY_MONTH_REGION, getDriver()); } @@ -96,9 +113,11 @@ protected Elements newElementCache() public class Elements extends BodyWebPart.ElementCache { - final WebElement byDayToggle = Locator.id("utilizationToggleDay").findWhenNeeded(this); - final WebElement byMonthToggle = Locator.id("utilizationToggleMonth").findWhenNeeded(this); - final WebElement byDayGrid = Locator.id("utilizationByDayGrid").findWhenNeeded(this); - final WebElement byMonthGrid = Locator.id("utilizationByMonthGrid").findWhenNeeded(this); + final WebElement calendarTab = Locator.css("#utilizationTabs a[data-utilization-tab='calendar']").findWhenNeeded(this); + final WebElement byMonthTab = Locator.css("#utilizationTabs a[data-utilization-tab='month']").findWhenNeeded(this); + final WebElement byDayTab = Locator.css("#utilizationTabs a[data-utilization-tab='day']").findWhenNeeded(this); + final WebElement calendarPane = Locator.id("utilizationTabCalendar").findWhenNeeded(this); + final WebElement byMonthPane = Locator.id("utilizationTabMonth").findWhenNeeded(this); + final WebElement byDayPane = Locator.id("utilizationTabDay").findWhenNeeded(this); } } diff --git a/test/src/org/labkey/test/tests/targetedms/TargetedMSInstrumentUtilizationTest.java b/test/src/org/labkey/test/tests/targetedms/TargetedMSInstrumentUtilizationTest.java index e2b869b77..2a5ed4036 100644 --- a/test/src/org/labkey/test/tests/targetedms/TargetedMSInstrumentUtilizationTest.java +++ b/test/src/org/labkey/test/tests/targetedms/TargetedMSInstrumentUtilizationTest.java @@ -90,14 +90,15 @@ public void testCrossFolderInstrumentUtilization() throws IOException, CommandEx beginAt(WebTestHelper.buildURL("targetedms", getProjectName(), "showInstrument", Map.of("name", _instrumentName))); - verifyCalendar(); - verifyRunsAcquiredGrids(expectedCrossFolderFileCount); + InstrumentUtilizationWebPart utilization = new InstrumentUtilizationWebPart(getDriver()); + verifyCalendarTab(utilization); + verifyRunsGrids(utilization, expectedCrossFolderFileCount); } - private void verifyCalendar() + private void verifyCalendarTab(InstrumentUtilizationWebPart utilization) { - log("Verifying the cross-folder Utilization Calendar renders"); - assertElementPresent(Locator.tagWithClass("span", "labkey-wp-title-text").withText("Utilization Calendar")); + log("Verifying the utilization web part opens on the Utilization Calendar tab"); + assertTrue("The Utilization Calendar tab should be active by default", utilization.isCalendarVisible()); // Wait for the async selectRows call to populate the calendar with day cells waitForElement(Locator.tagWithClassContaining("div", "day-content")); @@ -105,13 +106,12 @@ private void verifyCalendar() "1 month", getSelectedOptionText(Locator.id("utilizationMonthNumberSelect"))); } - private void verifyRunsAcquiredGrids(int expectedCrossFolderFileCount) + private void verifyRunsGrids(InstrumentUtilizationWebPart utilization, int expectedCrossFolderFileCount) { - InstrumentUtilizationWebPart utilization = new InstrumentUtilizationWebPart(getDriver()); - - log("Verifying the By Day grid is shown by default and aggregates across folders"); - assertTrue("By Day grid should be visible by default", utilization.isByDayVisible()); - assertFalse("By Month grid should be hidden by default", utilization.isByMonthVisible()); + log("Selecting the Runs by Day tab and verifying it aggregates across folders"); + utilization.showByDay(); + assertTrue("By Day grid should be visible after selecting its tab", utilization.isByDayVisible()); + assertFalse("Calendar should be hidden when the By Day tab is active", utilization.isCalendarVisible()); DataRegionTable byDay = utilization.getByDayTable(); assertTrue("By Day grid is missing expected columns", @@ -119,10 +119,10 @@ private void verifyRunsAcquiredGrids(int expectedCrossFolderFileCount) assertEquals("By Day grid should sum files across both folders", expectedCrossFolderFileCount, utilization.getTotalFiles(byDay)); - log("Toggling to the By Month summary"); + log("Selecting the Runs by Month tab"); utilization.showByMonth(); - assertTrue("By Month grid should be visible after toggling", utilization.isByMonthVisible()); - assertFalse("By Day grid should be hidden after toggling", utilization.isByDayVisible()); + assertTrue("By Month grid should be visible after selecting its tab", utilization.isByMonthVisible()); + assertFalse("By Day grid should be hidden when the By Month tab is active", utilization.isByDayVisible()); DataRegionTable byMonth = utilization.getByMonthTable(); assertTrue("By Month grid is missing expected columns", @@ -131,10 +131,10 @@ private void verifyRunsAcquiredGrids(int expectedCrossFolderFileCount) assertEquals("By Month grid should sum to the same cross-folder file total", expectedCrossFolderFileCount, utilization.getTotalFiles(byMonth)); - log("Toggling back to the By Day grid"); - utilization.showByDay(); - assertTrue("By Day grid should be visible after toggling back", utilization.isByDayVisible()); - assertFalse("By Month grid should be hidden after toggling back", utilization.isByMonthVisible()); + log("Selecting the Utilization Calendar tab again"); + utilization.showCalendar(); + assertTrue("Calendar should be visible after selecting its tab", utilization.isCalendarVisible()); + assertFalse("By Month grid should be hidden when the calendar tab is active", utilization.isByMonthVisible()); } /** @return the default nickname (model - serial number) for the instrument that acquired the imported data */ From ab46042cba11eca1ba212281b822328283236d17 Mon Sep 17 00:00:00 2001 From: ankurjuneja Date: Wed, 15 Jul 2026 13:42:36 -0700 Subject: [PATCH 6/8] feedback changes --- .../InstrumentUtilizationByDay.query.xml | 4 +-- .../InstrumentUtilizationByMonth.query.xml | 4 +-- .../targetedms/TargetedMSController.java | 29 +++++++++++++----- .../targetedms/view/instrumentUtilization.jsp | 16 +++++++--- .../InstrumentUtilizationWebPart.java | 30 +++++++++++++++++-- .../TargetedMSInstrumentUtilizationTest.java | 17 ++++++++--- 6 files changed, 77 insertions(+), 23 deletions(-) diff --git a/resources/queries/targetedms/InstrumentUtilizationByDay.query.xml b/resources/queries/targetedms/InstrumentUtilizationByDay.query.xml index eb4cdfaee..29d60eddd 100644 --- a/resources/queries/targetedms/InstrumentUtilizationByDay.query.xml +++ b/resources/queries/targetedms/InstrumentUtilizationByDay.query.xml @@ -9,10 +9,10 @@ Date - Runs + Skyline Document Count - Files + Replicate Count Instrument diff --git a/resources/queries/targetedms/InstrumentUtilizationByMonth.query.xml b/resources/queries/targetedms/InstrumentUtilizationByMonth.query.xml index 5199207f2..f3a008f7f 100644 --- a/resources/queries/targetedms/InstrumentUtilizationByMonth.query.xml +++ b/resources/queries/targetedms/InstrumentUtilizationByMonth.query.xml @@ -9,10 +9,10 @@ yyyy-MM - Runs + Skyline Document Count - Files + Replicate Count Instrument diff --git a/src/org/labkey/targetedms/TargetedMSController.java b/src/org/labkey/targetedms/TargetedMSController.java index 8414ed3c9..c3a726bd2 100644 --- a/src/org/labkey/targetedms/TargetedMSController.java +++ b/src/org/labkey/targetedms/TargetedMSController.java @@ -4854,6 +4854,7 @@ protected QueryView createQueryView(InstrumentForm form, BindException errors, b settings.setBaseSort(new Sort("-AcquisitionDate")); settings.setBaseFilter(new SimpleFilter(FieldKey.fromParts("InstrumentNickname"), form.getName())); settings.setContainerFilterName(ContainerFilter.Type.AllFolders.name()); + settings.setFieldKeys(List.of(FieldKey.fromParts("AcquisitionDate"), FieldKey.fromParts("RunCount"), FieldKey.fromParts("FileCount"))); TargetedMSSchema schema = new TargetedMSSchema(getUser(), getContainer()); return schema.createView(getViewContext(), settings, errors); } @@ -4863,6 +4864,7 @@ protected QueryView createQueryView(InstrumentForm form, BindException errors, b settings.setBaseSort(new Sort("-MonthStart")); settings.setBaseFilter(new SimpleFilter(FieldKey.fromParts("InstrumentNickname"), form.getName())); settings.setContainerFilterName(ContainerFilter.Type.AllFolders.name()); + settings.setFieldKeys(List.of(FieldKey.fromParts("MonthStart"), FieldKey.fromParts("RunCount"), FieldKey.fromParts("FileCount"))); TargetedMSSchema schema = new TargetedMSSchema(getUser(), getContainer()); return schema.createView(getViewContext(), settings, errors); } @@ -4912,18 +4914,15 @@ public ModelAndView getView(InstrumentForm form, BindException errors) throws Ex byDayView.setFrame(WebPartView.FrameType.NONE); QueryView byMonthView = createInitializedQueryView(form, errors, false, UTILIZATION_BY_MONTH); byMonthView.setFrame(WebPartView.FrameType.NONE); + QueryView sampleFileView = createInitializedQueryView(form, errors, false, TargetedMSSchema.TABLE_SAMPLE_FILE); + sampleFileView.setFrame(WebPartView.FrameType.NONE); - var utilizationView = new JspView<>("/org/labkey/targetedms/view/instrumentUtilization.jsp", new InstrumentUtilizationBean(byDayView, byMonthView)); + var utilizationView = new JspView<>("/org/labkey/targetedms/view/instrumentUtilization.jsp", + new InstrumentUtilizationBean(byDayView, byMonthView, sampleFileView, "Samples from " + form.getName())); utilizationView.setTitle("Instrument Utilization Across Folders"); utilizationView.setFrame(WebPartView.FrameType.PORTAL); result.addView(utilizationView); - QueryView sampleFileView = createQueryView(form, errors, false, TargetedMSSchema.TABLE_SAMPLE_FILE); - sampleFileView.setTitle("Samples from " + form.getName()); - sampleFileView.setFrame(WebPartView.FrameType.PORTAL); - - result.addView(sampleFileView); - return result; } } @@ -4932,11 +4931,15 @@ public static class InstrumentUtilizationBean { private final QueryView _byDayView; private final QueryView _byMonthView; + private final QueryView _sampleFileView; + private final String _sampleFileTitle; - public InstrumentUtilizationBean(QueryView byDayView, QueryView byMonthView) + public InstrumentUtilizationBean(QueryView byDayView, QueryView byMonthView, QueryView sampleFileView, String sampleFileTitle) { _byDayView = byDayView; _byMonthView = byMonthView; + _sampleFileView = sampleFileView; + _sampleFileTitle = sampleFileTitle; } public QueryView getByDayView() @@ -4948,6 +4951,16 @@ public QueryView getByMonthView() { return _byMonthView; } + + public QueryView getSampleFileView() + { + return _sampleFileView; + } + + public String getSampleFileTitle() + { + return _sampleFileTitle; + } } public static class InstrumentInfoRowBean diff --git a/src/org/labkey/targetedms/view/instrumentUtilization.jsp b/src/org/labkey/targetedms/view/instrumentUtilization.jsp index 75b5a7b0a..316a852a6 100644 --- a/src/org/labkey/targetedms/view/instrumentUtilization.jsp +++ b/src/org/labkey/targetedms/view/instrumentUtilization.jsp @@ -51,9 +51,10 @@
@@ -100,6 +101,12 @@ <% me.include(bean.getByDayView(), out); %>
+ +
+
+ <% me.include(bean.getSampleFileView(), out); %> +
+
@@ -151,11 +162,25 @@ const instrumentName = LABKEY.ActionURL.getParameter('name'); + // Following a drill-in link (a summary-grid count cell or a calendar day) reloads this page with + // utilizationTab=samples and an AcquiredTime filter. Let the calendar render first (it needs to be + // visible to size itself), then reveal the pre-filtered Samples tab the link was targeting. + function honorRequestedTab() { + if (LABKEY.ActionURL.getParameter('utilizationTab') === 'samples' && window.showInstrumentSamplesTab) { + window.showInstrumentSamplesTab(); + } + } + let dateOnly = function (d) { let dateTime = new Date(d); return new Date(dateTime.getFullYear(), dateTime.getMonth(), dateTime.getDate()); }; + let formatDate = function (d) { + let pad = function (n) { return (n < 10 ? '0' : '') + n; }; + return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()); + }; + function addEvent(data, date) { data.push({ startDate: new Date(date.getTime()), @@ -253,6 +278,7 @@ loadData(function (data) { if (!data.length) { $('#instrumentUtilizationCalendar').text('No samples acquired by this instrument.'); + honorRequestedTab(); return; } @@ -297,6 +323,19 @@ mouseOutDay: function (e) { $(e.element).popover('hide'); }, + clickDay: function (e) { + // Drill into the samples acquired on the clicked day, matching the summary-grid links: + // navigate to this page on the Samples tab with a single-day AcquiredTime filter applied. + let event = e.events && e.events.length > 0 ? e.events[0] : null; + if (!event || !event.fileCount) { + return; + } + window.location = LABKEY.ActionURL.buildURL('targetedms', 'showInstrument', null, { + name: instrumentName, + utilizationTab: 'samples', + 'SampleFile.AcquiredTime~dateeq': formatDate(dateOnly(e.date)) + }); + }, dataSource: data }); @@ -311,6 +350,8 @@ } } }); + + honorRequestedTab(); }); })(); diff --git a/test/src/org/labkey/test/components/targetedms/InstrumentUtilizationWebPart.java b/test/src/org/labkey/test/components/targetedms/InstrumentUtilizationWebPart.java index d174239ca..1c78f540f 100644 --- a/test/src/org/labkey/test/components/targetedms/InstrumentUtilizationWebPart.java +++ b/test/src/org/labkey/test/components/targetedms/InstrumentUtilizationWebPart.java @@ -116,6 +116,20 @@ public DataRegionTable getSamplesTable() return new DataRegionTable(SAMPLE_FILE_REGION, getDriver()); } + /** + * Clicks the "Replicate Count" drill-in link in the given summary-grid row. The link reloads the page + * on the Samples tab with the row's date-range filter applied, so this waits for the reload and returns + * a fresh web part handle positioned on the (filtered) Samples tab. + */ + public InstrumentUtilizationWebPart drillIntoSamples(DataRegionTable summaryTable, int row) + { + getWrapper().clickAndWait(summaryTable.link(row, "Replicate Count")); + InstrumentUtilizationWebPart reloaded = new InstrumentUtilizationWebPart(getDriver()); + WebDriverWrapper.waitFor(reloaded::isSamplesVisible, + "Samples tab did not open after the drill-in navigation", reloaded.getWrapper().defaultWaitForPage); + return reloaded; + } + /** Sum of the (integer) "Files" column, i.e. the total number of sample files represented by the grid. */ public int getTotalFiles(DataRegionTable table) { diff --git a/test/src/org/labkey/test/tests/targetedms/TargetedMSInstrumentUtilizationTest.java b/test/src/org/labkey/test/tests/targetedms/TargetedMSInstrumentUtilizationTest.java index 9c1085a57..b3791c870 100644 --- a/test/src/org/labkey/test/tests/targetedms/TargetedMSInstrumentUtilizationTest.java +++ b/test/src/org/labkey/test/tests/targetedms/TargetedMSInstrumentUtilizationTest.java @@ -93,6 +93,36 @@ public void testCrossFolderInstrumentUtilization() throws IOException, CommandEx InstrumentUtilizationWebPart utilization = new InstrumentUtilizationWebPart(getDriver()); verifyCalendarTab(utilization); verifyRunsGrids(utilization, expectedCrossFolderFileCount); + verifyDrillIntoSamples(utilization); + } + + /** + * The Skyline Document Count / Replicate Count cells in the summary grids drill into the Samples tab + * with a matching date filter applied. Verify the by-day and by-month links land on the Samples tab + * and narrow the sample-file grid to exactly the replicates that row represents. + */ + private void verifyDrillIntoSamples(InstrumentUtilizationWebPart utilization) + { + log("Drilling into the Samples tab from a Summary by Day count link"); + DataRegionTable byDay = utilization.getByDayTable(); + int dayReplicates = Integer.parseInt(byDay.getDataAsText(0, "Replicate Count").trim()); + utilization = utilization.drillIntoSamples(byDay, 0); + assertTrue("Samples tab should open when a day's count is clicked", utilization.isSamplesVisible()); + waitForSamplesRowCount(dayReplicates); + + log("Drilling into the Samples tab from a Summary by Month count link"); + DataRegionTable byMonth = utilization.getByMonthTable(); + int monthReplicates = Integer.parseInt(byMonth.getDataAsText(0, "Replicate Count").trim()); + utilization = utilization.drillIntoSamples(byMonth, 0); + assertTrue("Samples tab should open when a month's count is clicked", utilization.isSamplesVisible()); + waitForSamplesRowCount(monthReplicates); + } + + /** Waits for the sample-file grid to refresh to the expected filtered row count (rebuilt each poll to dodge staleness). */ + private void waitForSamplesRowCount(int expected) + { + waitFor(() -> new DataRegionTable(InstrumentUtilizationWebPart.SAMPLE_FILE_REGION, getDriver()).getDataRowCount() == expected, + "Samples grid did not filter to the expected " + expected + " row(s)", 10_000); } private void verifyCalendarTab(InstrumentUtilizationWebPart utilization) From 141a7813251a403029186648bac81eca8dd305e4 Mon Sep 17 00:00:00 2001 From: ankurjuneja Date: Wed, 15 Jul 2026 16:17:12 -0700 Subject: [PATCH 8/8] update comment --- src/org/labkey/targetedms/view/instrumentInfoRow.jsp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/org/labkey/targetedms/view/instrumentInfoRow.jsp b/src/org/labkey/targetedms/view/instrumentInfoRow.jsp index ffe50ad64..0a9eebfbc 100644 --- a/src/org/labkey/targetedms/view/instrumentInfoRow.jsp +++ b/src/org/labkey/targetedms/view/instrumentInfoRow.jsp @@ -34,9 +34,7 @@