Skip to content

Commit 28c5340

Browse files
Merge 26.3 to 26.6
2 parents eba2345 + 5cf0aac commit 28c5340

9 files changed

Lines changed: 604 additions & 87 deletions

File tree

src/org/labkey/targetedms/TargetedMSController.java

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3185,8 +3185,31 @@ public void addNavTrail(NavTree root)
31853185
}
31863186
}
31873187

3188+
/**
3189+
* Reading library spectra from a large library file can take many seconds over network storage, especially for large
3190+
* EncyclopeDIA libraries. To protect public folders from aggressive bots, do not show library spectra to guests when
3191+
* the library is large. Show the login prompt instead.
3192+
* Returns true (and adds the login view) when the library spectrum should be withheld.
3193+
*/
3194+
private boolean addGuestSpectrumGate(TargetedMSRun run, VBox vbox)
3195+
{
3196+
if (!LibrarySpectrumMatchGetter.blockSpectraForGuest(getUser(), run.getId()))
3197+
{
3198+
return false;
3199+
}
3200+
HtmlView loginView = getLoginView(getViewContext(), getContainer());
3201+
loginView.setTitle("Library Spectrum");
3202+
loginView.setFrame(WebPartView.FrameType.PORTAL);
3203+
vbox.addView(loginView);
3204+
return true;
3205+
}
3206+
31883207
private void addSpectrumViews(TargetedMSRun run, VBox vbox, Precursor precursor, BindException errors)
31893208
{
3209+
if (addGuestSpectrumGate(run, vbox))
3210+
{
3211+
return;
3212+
}
31903213
PipeRoot root = PipelineService.get().getPipelineRootSetting(getContainer());
31913214
if (null != root)
31923215
{
@@ -3205,6 +3228,10 @@ private void addSpectrumViews(TargetedMSRun run, VBox vbox, Precursor precursor,
32053228

32063229
private void addSpectrumViews(TargetedMSRun run, VBox vbox, Peptide peptide, BindException errors)
32073230
{
3231+
if (addGuestSpectrumGate(run, vbox))
3232+
{
3233+
return;
3234+
}
32083235
PipeRoot root = PipelineService.get().getPipelineRootSetting(getContainer());
32093236
if (null != root)
32103237
{
@@ -3289,6 +3316,13 @@ public Object execute(SpectrumDataForm form, BindException errors)
32893316
}
32903317
TargetedMSRun run = TargetedMSManager.getRunForGeneralMolecule(peptide.getId());
32913318

3319+
// Apply the same guest gate as the spectrum views (see LibrarySpectrumMatchGetter.blockSpectraForGuest).
3320+
if (LibrarySpectrumMatchGetter.blockSpectraForGuest(getUser(), run.getId()))
3321+
{
3322+
response.put("error", "Login to view this data");
3323+
return response;
3324+
}
3325+
32923326
List<PeptideSettings.SpectrumLibrary> libraries = LibraryManager.getLibraries(run.getId());
32933327
PeptideSettings.SpectrumLibrary library = null;
32943328
for (PeptideSettings.SpectrumLibrary lib : libraries)

src/org/labkey/targetedms/chart/ChromatogramDataset.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -900,6 +900,13 @@ public void build()
900900

901901
protected List<LibrarySpectrumMatchGetter.PeptideIdRtInfo> getPeptideIdRetentionTimes()
902902
{
903+
// Skip peptide-ID retention-time markers for guests when the library is large. Reading large libraries can be slow over network storage.
904+
// See LibrarySpectrumMatchGetter.blockSpectraForGuest.
905+
if (LibrarySpectrumMatchGetter.blockSpectraForGuest(_user, _run.getId()))
906+
{
907+
return Collections.emptyList();
908+
}
909+
903910
SampleFile sampleFile = ReplicateManager.getSampleFile(_pChromInfo.getSampleFileId());
904911

905912
// TODO: May want to move LocalDirectory up to controller, where others are created. Sharing probably desired.

src/org/labkey/targetedms/view/precursorConflictResolutionView.jsp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,12 +119,12 @@ $(document).ready(function () {
119119
{
120120
row.child.hide();
121121
tr.removeClass('shown');
122-
$("." + cls).children('img').attr('src', "<%=getWebappURL("_images/plus.gif")%>");
122+
$("." + cls).children('img').attr('src', "<%=getWebappURL("_images/plus.gif")%>").attr('alt', 'Expand row details');
123123
}
124124
else {
125125
row.child.show();
126126
tr.addClass('shown');
127-
$("." + cls).children('img').attr('src', "<%=getWebappURL("_images/minus.gif")%>");
127+
$("." + cls).children('img').attr('src', "<%=getWebappURL("_images/minus.gif")%>").attr('alt', 'Collapse row details');
128128
}
129129
130130
if(!srcTd.hasClass('content_loaded'))
@@ -271,7 +271,7 @@ function toggleCheckboxSelection(element)
271271
<!--<td class="representative newPrecursor <%=precursor.getNewPrecursorId()%>"><%=precursor.getNewPrecursorId()%></td>-->
272272
<td class="representative details-control newPrecursor <%=precursor.getNewPrecursorId()%>">
273273
<span class="<%=precursor.getNewPrecursorId()%>_<%=precursor.getOldPrecursorId()%>">
274-
<img src="<%=getWebappURL("_images/plus.gif")%>"/>
274+
<img src="<%=getWebappURL("_images/plus.gif")%>" alt="Expand row details"/>
275275
</span>
276276
</td>
277277
<td class="representative newPrecursor <%=precursor.getNewPrecursorId()%>">
@@ -291,7 +291,7 @@ function toggleCheckboxSelection(element)
291291
<!--<td class="oldPrecursor <%=precursor.getNewPrecursorId()%>"><%=precursor.getOldPrecursorId()%></td>-->
292292
<td class="details-control oldPrecursor <%=precursor.getNewPrecursorId()%>">
293293
<span class="<%=precursor.getNewPrecursorId()%>_<%=precursor.getOldPrecursorId()%>">
294-
<img src="<%=getWebappURL("_images/plus.gif")%>"/>
294+
<img src="<%=getWebappURL("_images/plus.gif")%>" alt="Expand row details"/>
295295
</span>
296296
</td>
297297
<td class="oldPrecursor <%=precursor.getNewPrecursorId()%>">

src/org/labkey/targetedms/view/spectrum/LibrarySpectrumMatchGetter.java

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package org.labkey.targetedms.view.spectrum;
1717

1818
import org.apache.commons.io.FilenameUtils;
19+
import org.apache.logging.log4j.Logger;
1920
import org.jetbrains.annotations.NotNull;
2021
import org.jetbrains.annotations.Nullable;
2122
import org.labkey.api.cache.BlockingCache;
@@ -24,6 +25,7 @@
2425
import org.labkey.api.data.Container;
2526
import org.labkey.api.security.User;
2627
import org.labkey.api.util.FileUtil;
28+
import org.labkey.api.util.logging.LogHelper;
2729
import org.labkey.targetedms.TargetedMSManager;
2830
import org.labkey.targetedms.TargetedMSRun;
2931
import org.labkey.targetedms.TargetedMSSchema;
@@ -42,6 +44,8 @@
4244
import org.labkey.targetedms.query.PeptideManager;
4345
import org.labkey.targetedms.query.PrecursorManager;
4446

47+
import java.io.IOException;
48+
import java.nio.file.Files;
4549
import java.nio.file.Path;
4650
import java.sql.SQLException;
4751
import java.util.ArrayList;
@@ -58,8 +62,64 @@
5862
*/
5963
public class LibrarySpectrumMatchGetter
6064
{
65+
private static final Logger LOG = LogHelper.getLogger(LibrarySpectrumMatchGetter.class, "Matches library spectra and retention times for the library spectrum viewer");
66+
6167
private static final int CACHE_SIZE = 10;
6268

69+
// Reading library spectra and retention times from large spectrum libraries can be slow over network storage.
70+
// For EncyclopeDIA .elib we read one row per source file for the peptide. This can be hundreds of rows and the needed
71+
// columns are not in the index, so each table row lookup is a separate network round-trip on GPFS.
72+
// For BiblioSpec .blib we scan the unindexed RetentionTimes table for the RT of the peptide in all the scans and source
73+
// files.
74+
// PanoramaWeb has large files of both types, so the size gate covers both library types. To protect public folders from
75+
// aggressive bots, library spectra are not shown to guests when the library file is at or above this size. Guests are
76+
// asked to log in instead.
77+
private static final long GUEST_SPECTRUM_LIBRARY_SIZE_LIMIT = 500L * 1024 * 1024; // 500 MB
78+
79+
/**
80+
* Returns true if library spectra should NOT be shown to the given user for the given run,
81+
* i.e. the user is a guest and the run references a supported spectrum library file that is at
82+
* or above {@link #GUEST_SPECTRUM_LIBRARY_SIZE_LIMIT}. Logged-in users are never blocked, and
83+
* small libraries are read in place as before.
84+
*/
85+
public static boolean blockSpectraForGuest(User user, long runId)
86+
{
87+
if (!user.isGuest())
88+
{
89+
return false;
90+
}
91+
for (Path libPath : LibraryManager.getLibraryFilePaths(runId).values())
92+
{
93+
if (isLargeSpectrumLibrary(libPath))
94+
{
95+
return true;
96+
}
97+
}
98+
return false;
99+
}
100+
101+
private static boolean isLargeSpectrumLibrary(Path libPath)
102+
{
103+
// Only .elib/.blib libraries are read for spectra; ignore anything we cannot read.
104+
if (libPath == null || getReaderForLibrary(FileUtil.getFileName(libPath)) == null)
105+
{
106+
return false;
107+
}
108+
try
109+
{
110+
// Files.size throws NoSuchFileException if the file is missing, so a separate Files.exists
111+
// check is unnecessary and would add a second filesystem round-trip on network storage.
112+
return Files.size(libPath) >= GUEST_SPECTRUM_LIBRARY_SIZE_LIMIT;
113+
}
114+
catch (IOException e)
115+
{
116+
// If we cannot stat the file it is missing or unreadable, in which case the
117+
// downstream library read will fail too.
118+
LOG.warn("Could not determine size of spectrum library file " + libPath, e);
119+
return false;
120+
}
121+
}
122+
63123
private static final BlockingCache<PrecursorKey, List<PeptideIdRtInfo>> _peptideIdRtsCache =
64124
CacheManager.getBlockingCache(CACHE_SIZE, CacheManager.DAY, "TargetedMS peptide ID retention times",
65125
(precursor, argument) -> {

test/src/org/labkey/test/components/targetedms/QCPlotsWebPart.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -901,6 +901,36 @@ public String toString()
901901
}
902902
}
903903

904+
public void performYAxisZoom(QCPlot qcPlot)
905+
{
906+
WebElement plotEl = qcPlot.getPlot();
907+
WebElement overlay = elementCache().yZoomOverlay.findElement(plotEl);
908+
getWrapper().scrollIntoView(overlay);
909+
910+
int clickOffset = 40;
911+
new Actions(getWrapper().getDriver())
912+
.moveToElement(overlay, 0, -clickOffset)
913+
.click()
914+
.moveToElement(overlay, 0, clickOffset)
915+
.click()
916+
.perform();
917+
918+
WebDriverWrapper.waitFor(() -> !elementCache().yZoomConfirmBtn.findElements(plotEl).isEmpty(),
919+
"Zoom buttons did not appear after y-axis clicks", WAIT_FOR_JAVASCRIPT);
920+
921+
elementCache().yZoomConfirmBtn.findElement(plotEl).click();
922+
}
923+
924+
public boolean isZoomActive(QCPlot qcPlot)
925+
{
926+
return !elementCache().yZoomBorder.findElements(qcPlot.getPlot()).isEmpty();
927+
}
928+
929+
public void clickResetZoom(QCPlot qcPlot)
930+
{
931+
elementCache().yZoomOverlay.findElement(qcPlot.getPlot()).click();
932+
}
933+
904934
public class Elements extends BodyWebPart<?>.ElementCache
905935
{
906936
WebElement startDate = Locator.css("#start-date-field input").findWhenNeeded(this);
@@ -936,6 +966,9 @@ public class Elements extends BodyWebPart<?>.ElementCache
936966
WebElement plotPanel = Locator.css("div.tiledPlotPanel").findWhenNeeded(this);
937967
WebElement paginationPanel = Locator.css("div.plotPaginationHeaderPanel").findWhenNeeded(this);
938968
Locator extFormDisplay = Locator.css("div.x4-form-display-field");
969+
Locator.CssLocator yZoomOverlay = Locator.css("svg rect.y-zoom-overlay");
970+
Locator.CssLocator yZoomConfirmBtn = Locator.css("svg g.y-zoom-btn-zoom rect");
971+
Locator.CssLocator yZoomBorder = Locator.css("svg rect.y-zoom-border");
939972
Locator.CssLocator guideSetTrainingRect = Locator.css("svg rect.training");
940973
Locator.CssLocator experimentRangeRect = Locator.css("svg rect.expRange");
941974
Locator.CssLocator guideSetSvgButton = Locator.css("svg g.guideset-svg-button text");

test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1147,6 +1147,56 @@ private void verifyRow(DataRegionTable drt, int row, String sampleName, String s
11471147
assertEquals(skylineDocName, drt.getDataAsText(row, "File"));
11481148
}
11491149

1150+
@Test
1151+
public void testQCPlotYAxisZoom()
1152+
{
1153+
PanoramaDashboard qcDashboard = new PanoramaDashboard(this);
1154+
QCPlotsWebPart qcPlotsWebPart = qcDashboard.getQcPlotsWebPart();
1155+
qcPlotsWebPart.filterQCPlotsToInitialData(PRECURSORS.length, true);
1156+
1157+
List<QCPlot> plots = qcPlotsWebPart.getPlots();
1158+
assertTrue("Expected at least 2 plots for y-axis zoom test", plots.size() >= 2);
1159+
1160+
// 1. Verify zooming is possible: drag on y-axis, confirm zoom, border appears
1161+
log("Verifying y-axis zoom can be applied");
1162+
qcPlotsWebPart.performYAxisZoom(plots.get(0));
1163+
waitForElement(Locator.css("svg rect.y-zoom-border"), WAIT_FOR_JAVASCRIPT);
1164+
1165+
plots = qcPlotsWebPart.getPlots();
1166+
QCPlot firstPlot = plots.get(0);
1167+
QCPlot secondPlot = plots.get(1);
1168+
1169+
assertTrue("Zoom border should appear on first plot after zoom", qcPlotsWebPart.isZoomActive(firstPlot));
1170+
1171+
// 2. Verify zoom is per-plot: second plot is unaffected
1172+
log("Verifying zoom is independent per plot");
1173+
assertFalse("Second plot should not be zoomed", qcPlotsWebPart.isZoomActive(secondPlot));
1174+
1175+
// 3. Verify reset works: clicking the zoomed y-axis (zoom-out cursor) resets zoom
1176+
log("Verifying clicking the y-axis resets zoom on the target plot");
1177+
qcPlotsWebPart.clickResetZoom(firstPlot);
1178+
waitForElementToDisappear(Locator.css("svg rect.y-zoom-border"), WAIT_FOR_JAVASCRIPT);
1179+
1180+
plots = qcPlotsWebPart.getPlots();
1181+
firstPlot = plots.get(0);
1182+
1183+
assertFalse("Zoom border should be gone after reset", qcPlotsWebPart.isZoomActive(firstPlot));
1184+
1185+
// 4. Verify zoom is not persisted after page reload
1186+
log("Verifying zoom state is cleared on page reload");
1187+
qcPlotsWebPart.performYAxisZoom(firstPlot);
1188+
waitForElement(Locator.css("svg rect.y-zoom-border"), WAIT_FOR_JAVASCRIPT);
1189+
1190+
refresh();
1191+
qcDashboard = new PanoramaDashboard(this);
1192+
qcPlotsWebPart = qcDashboard.getQcPlotsWebPart();
1193+
1194+
plots = qcPlotsWebPart.getPlots();
1195+
firstPlot = plots.get(0);
1196+
1197+
assertFalse("Zoom should not persist after page reload", qcPlotsWebPart.isZoomActive(firstPlot));
1198+
}
1199+
11501200
private void createAndInsertAnnotations()
11511201
{
11521202
clickTab("Annotations");

webapp/TargetedMS/css/qcTrendPlotReport.css

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
font-size: 18px;
7171
padding: 0 8px;
7272
border: solid #c0c0c0 1px;
73+
background: none;
7374
}
7475
.qc-paging-prev {
7576
border-right-width: 0;
@@ -115,4 +116,24 @@
115116

116117
.qc-combined-tree-legend .qc-tree-precursor:hover {
117118
background-color: #f0f0f0;
118-
}
119+
}
120+
121+
.y-zoom-overlay {
122+
cursor: zoom-in;
123+
}
124+
125+
.y-zoom-pending-line {
126+
stroke: rgba(20, 204, 201, 1);
127+
stroke-width: 2px;
128+
stroke-dasharray: 6, 3;
129+
}
130+
131+
.y-zoom-selection {
132+
fill: rgba(20, 204, 201, 0.3);
133+
stroke: rgba(20, 204, 201, 1);
134+
stroke-width: 1px;
135+
}
136+
137+
.y-zoom-buttons g {
138+
cursor: pointer;
139+
}

0 commit comments

Comments
 (0)