Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions src/org/labkey/targetedms/TargetedMSController.java
Original file line number Diff line number Diff line change
Expand Up @@ -3185,8 +3185,31 @@ public void addNavTrail(NavTree root)
}
}

/**
* Reading library spectra from a large library file can take many seconds over network storage, especially for large
* EncyclopeDIA libraries. To protect public folders from aggressive bots, do not show library spectra to guests when
* the library is large. Show the login prompt instead.
* Returns true (and adds the login view) when the library spectrum should be withheld.
*/
private boolean addGuestSpectrumGate(TargetedMSRun run, VBox vbox)
{
if (!LibrarySpectrumMatchGetter.blockSpectraForGuest(getUser(), run.getId()))
{
return false;
}
HtmlView loginView = getLoginView(getViewContext(), getContainer());
loginView.setTitle("Library Spectrum");
loginView.setFrame(WebPartView.FrameType.PORTAL);
vbox.addView(loginView);
return true;
}

private void addSpectrumViews(TargetedMSRun run, VBox vbox, Precursor precursor, BindException errors)
{
if (addGuestSpectrumGate(run, vbox))
{
return;
}
PipeRoot root = PipelineService.get().getPipelineRootSetting(getContainer());
if (null != root)
{
Expand All @@ -3205,6 +3228,10 @@ private void addSpectrumViews(TargetedMSRun run, VBox vbox, Precursor precursor,

private void addSpectrumViews(TargetedMSRun run, VBox vbox, Peptide peptide, BindException errors)
{
if (addGuestSpectrumGate(run, vbox))
{
return;
}
PipeRoot root = PipelineService.get().getPipelineRootSetting(getContainer());
if (null != root)
{
Expand Down Expand Up @@ -3289,6 +3316,13 @@ public Object execute(SpectrumDataForm form, BindException errors)
}
TargetedMSRun run = TargetedMSManager.getRunForGeneralMolecule(peptide.getId());

// Apply the same guest gate as the spectrum views (see LibrarySpectrumMatchGetter.blockSpectraForGuest).
if (LibrarySpectrumMatchGetter.blockSpectraForGuest(getUser(), run.getId()))
{
response.put("error", "Login to view this data");
return response;
}

List<PeptideSettings.SpectrumLibrary> libraries = LibraryManager.getLibraries(run.getId());
PeptideSettings.SpectrumLibrary library = null;
for (PeptideSettings.SpectrumLibrary lib : libraries)
Expand Down
7 changes: 7 additions & 0 deletions src/org/labkey/targetedms/chart/ChromatogramDataset.java
Original file line number Diff line number Diff line change
Expand Up @@ -900,6 +900,13 @@ public void build()

protected List<LibrarySpectrumMatchGetter.PeptideIdRtInfo> getPeptideIdRetentionTimes()
{
// Skip peptide-ID retention-time markers for guests when the library is large. Reading large libraries can be slow over network storage.
// See LibrarySpectrumMatchGetter.blockSpectraForGuest.
if (LibrarySpectrumMatchGetter.blockSpectraForGuest(_user, _run.getId()))
{
return Collections.emptyList();
}

SampleFile sampleFile = ReplicateManager.getSampleFile(_pChromInfo.getSampleFileId());

// TODO: May want to move LocalDirectory up to controller, where others are created. Sharing probably desired.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,12 @@ $(document).ready(function () {
{
row.child.hide();
tr.removeClass('shown');
$("." + cls).children('img').attr('src', "<%=getWebappURL("_images/plus.gif")%>");
$("." + cls).children('img').attr('src', "<%=getWebappURL("_images/plus.gif")%>").attr('alt', 'Expand row details');
}
else {
row.child.show();
tr.addClass('shown');
$("." + cls).children('img').attr('src', "<%=getWebappURL("_images/minus.gif")%>");
$("." + cls).children('img').attr('src', "<%=getWebappURL("_images/minus.gif")%>").attr('alt', 'Collapse row details');
}

if(!srcTd.hasClass('content_loaded'))
Expand Down Expand Up @@ -271,7 +271,7 @@ function toggleCheckboxSelection(element)
<!--<td class="representative newPrecursor <%=precursor.getNewPrecursorId()%>"><%=precursor.getNewPrecursorId()%></td>-->
<td class="representative details-control newPrecursor <%=precursor.getNewPrecursorId()%>">
<span class="<%=precursor.getNewPrecursorId()%>_<%=precursor.getOldPrecursorId()%>">
<img src="<%=getWebappURL("_images/plus.gif")%>"/>
<img src="<%=getWebappURL("_images/plus.gif")%>" alt="Expand row details"/>
</span>
</td>
<td class="representative newPrecursor <%=precursor.getNewPrecursorId()%>">
Expand All @@ -291,7 +291,7 @@ function toggleCheckboxSelection(element)
<!--<td class="oldPrecursor <%=precursor.getNewPrecursorId()%>"><%=precursor.getOldPrecursorId()%></td>-->
<td class="details-control oldPrecursor <%=precursor.getNewPrecursorId()%>">
<span class="<%=precursor.getNewPrecursorId()%>_<%=precursor.getOldPrecursorId()%>">
<img src="<%=getWebappURL("_images/plus.gif")%>"/>
<img src="<%=getWebappURL("_images/plus.gif")%>" alt="Expand row details"/>
</span>
</td>
<td class="oldPrecursor <%=precursor.getNewPrecursorId()%>">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package org.labkey.targetedms.view.spectrum;

import org.apache.commons.io.FilenameUtils;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.labkey.api.cache.BlockingCache;
Expand All @@ -24,6 +25,7 @@
import org.labkey.api.data.Container;
import org.labkey.api.security.User;
import org.labkey.api.util.FileUtil;
import org.labkey.api.util.logging.LogHelper;
import org.labkey.targetedms.TargetedMSManager;
import org.labkey.targetedms.TargetedMSRun;
import org.labkey.targetedms.TargetedMSSchema;
Expand All @@ -42,6 +44,8 @@
import org.labkey.targetedms.query.PeptideManager;
import org.labkey.targetedms.query.PrecursorManager;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.sql.SQLException;
import java.util.ArrayList;
Expand All @@ -58,8 +62,64 @@
*/
public class LibrarySpectrumMatchGetter
{
private static final Logger LOG = LogHelper.getLogger(LibrarySpectrumMatchGetter.class, "Matches library spectra and retention times for the library spectrum viewer");

private static final int CACHE_SIZE = 10;

// Reading library spectra and retention times from large spectrum libraries can be slow over network storage.
// For EncyclopeDIA .elib we read one row per source file for the peptide. This can be hundreds of rows and the needed
// columns are not in the index, so each table row lookup is a separate network round-trip on GPFS.
// For BiblioSpec .blib we scan the unindexed RetentionTimes table for the RT of the peptide in all the scans and source
// files.
// PanoramaWeb has large files of both types, so the size gate covers both library types. To protect public folders from
// aggressive bots, library spectra are not shown to guests when the library file is at or above this size. Guests are
// asked to log in instead.
private static final long GUEST_SPECTRUM_LIBRARY_SIZE_LIMIT = 500L * 1024 * 1024; // 500 MB

/**
* Returns true if library spectra should NOT be shown to the given user for the given run,
* i.e. the user is a guest and the run references a supported spectrum library file that is at
* or above {@link #GUEST_SPECTRUM_LIBRARY_SIZE_LIMIT}. Logged-in users are never blocked, and
* small libraries are read in place as before.
*/
public static boolean blockSpectraForGuest(User user, long runId)
{
if (!user.isGuest())
{
return false;
}
for (Path libPath : LibraryManager.getLibraryFilePaths(runId).values())
{
if (isLargeSpectrumLibrary(libPath))
{
return true;
}
}
return false;
}

private static boolean isLargeSpectrumLibrary(Path libPath)
{
// Only .elib/.blib libraries are read for spectra; ignore anything we cannot read.
if (libPath == null || getReaderForLibrary(FileUtil.getFileName(libPath)) == null)
{
return false;
}
try
{
// Files.size throws NoSuchFileException if the file is missing, so a separate Files.exists
// check is unnecessary and would add a second filesystem round-trip on network storage.
return Files.size(libPath) >= GUEST_SPECTRUM_LIBRARY_SIZE_LIMIT;
}
catch (IOException e)
{
// If we cannot stat the file it is missing or unreadable, in which case the
// downstream library read will fail too.
LOG.warn("Could not determine size of spectrum library file " + libPath, e);
return false;
}
}

private static final BlockingCache<PrecursorKey, List<PeptideIdRtInfo>> _peptideIdRtsCache =
CacheManager.getBlockingCache(CACHE_SIZE, CacheManager.DAY, "TargetedMS peptide ID retention times",
(precursor, argument) -> {
Expand Down
33 changes: 33 additions & 0 deletions test/src/org/labkey/test/components/targetedms/QCPlotsWebPart.java
Original file line number Diff line number Diff line change
Expand Up @@ -901,6 +901,36 @@ public String toString()
}
}

public void performYAxisZoom(QCPlot qcPlot)
{
WebElement plotEl = qcPlot.getPlot();
WebElement overlay = elementCache().yZoomOverlay.findElement(plotEl);
getWrapper().scrollIntoView(overlay);

int clickOffset = 40;
new Actions(getWrapper().getDriver())
.moveToElement(overlay, 0, -clickOffset)
.click()
.moveToElement(overlay, 0, clickOffset)
.click()
.perform();

WebDriverWrapper.waitFor(() -> !elementCache().yZoomConfirmBtn.findElements(plotEl).isEmpty(),
"Zoom buttons did not appear after y-axis clicks", WAIT_FOR_JAVASCRIPT);

elementCache().yZoomConfirmBtn.findElement(plotEl).click();
}

public boolean isZoomActive(QCPlot qcPlot)
{
return !elementCache().yZoomBorder.findElements(qcPlot.getPlot()).isEmpty();
}

public void clickResetZoom(QCPlot qcPlot)
{
elementCache().yZoomOverlay.findElement(qcPlot.getPlot()).click();
}

public class Elements extends BodyWebPart<?>.ElementCache
{
WebElement startDate = Locator.css("#start-date-field input").findWhenNeeded(this);
Expand Down Expand Up @@ -936,6 +966,9 @@ public class Elements extends BodyWebPart<?>.ElementCache
WebElement plotPanel = Locator.css("div.tiledPlotPanel").findWhenNeeded(this);
WebElement paginationPanel = Locator.css("div.plotPaginationHeaderPanel").findWhenNeeded(this);
Locator extFormDisplay = Locator.css("div.x4-form-display-field");
Locator.CssLocator yZoomOverlay = Locator.css("svg rect.y-zoom-overlay");
Locator.CssLocator yZoomConfirmBtn = Locator.css("svg g.y-zoom-btn-zoom rect");
Locator.CssLocator yZoomBorder = Locator.css("svg rect.y-zoom-border");
Locator.CssLocator guideSetTrainingRect = Locator.css("svg rect.training");
Locator.CssLocator experimentRangeRect = Locator.css("svg rect.expRange");
Locator.CssLocator guideSetSvgButton = Locator.css("svg g.guideset-svg-button text");
Expand Down
50 changes: 50 additions & 0 deletions test/src/org/labkey/test/tests/targetedms/TargetedMSQCTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -1147,6 +1147,56 @@ private void verifyRow(DataRegionTable drt, int row, String sampleName, String s
assertEquals(skylineDocName, drt.getDataAsText(row, "File"));
}

@Test
public void testQCPlotYAxisZoom()
{
PanoramaDashboard qcDashboard = new PanoramaDashboard(this);
QCPlotsWebPart qcPlotsWebPart = qcDashboard.getQcPlotsWebPart();
qcPlotsWebPart.filterQCPlotsToInitialData(PRECURSORS.length, true);

List<QCPlot> plots = qcPlotsWebPart.getPlots();
assertTrue("Expected at least 2 plots for y-axis zoom test", plots.size() >= 2);

// 1. Verify zooming is possible: drag on y-axis, confirm zoom, border appears
log("Verifying y-axis zoom can be applied");
qcPlotsWebPart.performYAxisZoom(plots.get(0));
waitForElement(Locator.css("svg rect.y-zoom-border"), WAIT_FOR_JAVASCRIPT);

plots = qcPlotsWebPart.getPlots();
QCPlot firstPlot = plots.get(0);
QCPlot secondPlot = plots.get(1);

assertTrue("Zoom border should appear on first plot after zoom", qcPlotsWebPart.isZoomActive(firstPlot));

// 2. Verify zoom is per-plot: second plot is unaffected
log("Verifying zoom is independent per plot");
assertFalse("Second plot should not be zoomed", qcPlotsWebPart.isZoomActive(secondPlot));

// 3. Verify reset works: clicking the zoomed y-axis (zoom-out cursor) resets zoom
log("Verifying clicking the y-axis resets zoom on the target plot");
qcPlotsWebPart.clickResetZoom(firstPlot);
waitForElementToDisappear(Locator.css("svg rect.y-zoom-border"), WAIT_FOR_JAVASCRIPT);

plots = qcPlotsWebPart.getPlots();
firstPlot = plots.get(0);

assertFalse("Zoom border should be gone after reset", qcPlotsWebPart.isZoomActive(firstPlot));

// 4. Verify zoom is not persisted after page reload
log("Verifying zoom state is cleared on page reload");
qcPlotsWebPart.performYAxisZoom(firstPlot);
waitForElement(Locator.css("svg rect.y-zoom-border"), WAIT_FOR_JAVASCRIPT);

refresh();
qcDashboard = new PanoramaDashboard(this);
qcPlotsWebPart = qcDashboard.getQcPlotsWebPart();

plots = qcPlotsWebPart.getPlots();
firstPlot = plots.get(0);

assertFalse("Zoom should not persist after page reload", qcPlotsWebPart.isZoomActive(firstPlot));
}

private void createAndInsertAnnotations()
{
clickTab("Annotations");
Expand Down
23 changes: 22 additions & 1 deletion webapp/TargetedMS/css/qcTrendPlotReport.css
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
font-size: 18px;
padding: 0 8px;
border: solid #c0c0c0 1px;
background: none;
}
.qc-paging-prev {
border-right-width: 0;
Expand Down Expand Up @@ -115,4 +116,24 @@

.qc-combined-tree-legend .qc-tree-precursor:hover {
background-color: #f0f0f0;
}
}

.y-zoom-overlay {
cursor: zoom-in;
}

.y-zoom-pending-line {
stroke: rgba(20, 204, 201, 1);
stroke-width: 2px;
stroke-dasharray: 6, 3;
}

.y-zoom-selection {
fill: rgba(20, 204, 201, 0.3);
stroke: rgba(20, 204, 201, 1);
stroke-width: 1px;
}

.y-zoom-buttons g {
cursor: pointer;
}
Loading
Loading