Skip to content
Open
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
26 changes: 26 additions & 0 deletions resources/queries/targetedms/InstrumentUtilizationByDay.query.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<query xmlns="http://labkey.org/data/xml/query">
<metadata>
<tables xmlns="http://labkey.org/data/xml">
<table tableName="InstrumentUtilizationByDay" tableDbType="TABLE">
<tableTitle>Runs by Day</tableTitle>
<columns>
<column columnName="AcquisitionDate">
<columnTitle>Date</columnTitle>
<formatString>Date</formatString>
</column>
<column columnName="RunCount">
<columnTitle>Skyline Document Count</columnTitle>
<url>/targetedms-showInstrument.view?name=${InstrumentNickname}&amp;utilizationTab=samples&amp;SampleFile.AcquiredTime~dateeq=${AcquisitionDate:date('yyyy-MM-dd')}</url>
</column>
<column columnName="FileCount">
<columnTitle>Replicate Count</columnTitle>
<url>/targetedms-showInstrument.view?name=${InstrumentNickname}&amp;utilizationTab=samples&amp;SampleFile.AcquiredTime~dateeq=${AcquisitionDate:date('yyyy-MM-dd')}</url>
</column>
<column columnName="InstrumentNickname">
<columnTitle>Instrument</columnTitle>
</column>
</columns>
</table>
</tables>
</metadata>
</query>
23 changes: 23 additions & 0 deletions resources/queries/targetedms/InstrumentUtilizationByDay.sql
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<query xmlns="http://labkey.org/data/xml/query">
<metadata>
<tables xmlns="http://labkey.org/data/xml">
<table tableName="InstrumentUtilizationByMonth" tableDbType="TABLE">
<tableTitle>Runs by Month</tableTitle>
<columns>
<column columnName="MonthStart">
<columnTitle>Month</columnTitle>
<formatString>yyyy-MM</formatString>
</column>
<column columnName="RunCount">
<columnTitle>Skyline Document Count</columnTitle>
<url>/targetedms-showInstrument.view?name=${InstrumentNickname}&amp;utilizationTab=samples&amp;SampleFile.AcquiredTime~dategte=${MonthStart:date('yyyy-MM-dd')}&amp;SampleFile.AcquiredTime~datelt=${MonthEnd:date('yyyy-MM-dd')}</url>
</column>
<column columnName="FileCount">
<columnTitle>Replicate Count</columnTitle>
<url>/targetedms-showInstrument.view?name=${InstrumentNickname}&amp;utilizationTab=samples&amp;SampleFile.AcquiredTime~dategte=${MonthStart:date('yyyy-MM-dd')}&amp;SampleFile.AcquiredTime~datelt=${MonthEnd:date('yyyy-MM-dd')}</url>
</column>
<column columnName="InstrumentNickname">
<columnTitle>Instrument</columnTitle>
</column>
</columns>
</table>
</tables>
</metadata>
</query>
23 changes: 23 additions & 0 deletions resources/queries/targetedms/InstrumentUtilizationByMonth.sql
Original file line number Diff line number Diff line change
@@ -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 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,
TIMESTAMPADD('SQL_TSI_MONTH', 1, CAST(MonthStart || '-01' AS TIMESTAMP)) AS MonthEnd,
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
110 changes: 102 additions & 8 deletions src/org/labkey/targetedms/TargetedMSController.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -4846,11 +4848,31 @@ 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());
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);
}
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());
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);
}
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)
{
Expand All @@ -4868,29 +4890,101 @@ public ModelAndView getView(InstrumentForm form, BindException errors)

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);
}

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);
// 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);
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, sampleFileView, "Samples from " + form.getName()));
utilizationView.setTitle("Instrument Utilization Across Folders");
utilizationView.setFrame(WebPartView.FrameType.PORTAL);
result.addView(utilizationView);

return result;
}
}

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, QueryView sampleFileView, String sampleFileTitle)
{
_byDayView = byDayView;
_byMonthView = byMonthView;
_sampleFileView = sampleFileView;
_sampleFileTitle = sampleFileTitle;
}

public QueryView getByDayView()
{
return _byDayView;
}

public QueryView getByMonthView()
{
return _byMonthView;
}

public QueryView getSampleFileView()
{
return _sampleFileView;
}

public String getSampleFileTitle()
{
return _sampleFileTitle;
}
}

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<RunDetailsForm>
{
Expand Down
5 changes: 4 additions & 1 deletion src/org/labkey/targetedms/query/SampleFileTable.java
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
74 changes: 74 additions & 0 deletions src/org/labkey/targetedms/view/instrumentInfoRow.jsp
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<%
/*
* 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<InstrumentInfoRowBean> me = HttpView.currentView();
InstrumentInfoRowBean bean = me.getModelBean();
%>
<div class="row" id="lk-instrument-info-row">
<div class="col-md-5">
<% me.include(bean.getInfoView(), out); %>
</div>
<div class="col-md-7" style="max-width: 100%; overflow-x: auto;">
<% me.include(bean.getSummaryView(), out); %>
</div>
</div>

<script type="text/javascript" nonce="<%=getScriptNonce()%>">
(function() {
// Match the height of the two side-by-side web part panels
function equalizeHeights() {
var cols = document.querySelectorAll('#lk-instrument-info-row > [class*="col-"]');
var panels = [];
for (var i = 0; i < cols.length; i++) {
var panel = cols[i].querySelector('.panel');
if (panel) {
panels.push(panel);
}
}
if (panels.length < 2) {
return;
}

// Reset before measuring so re-runs (e.g. on resize) don't accumulate.
panels.forEach(function(p) { p.style.minHeight = ''; });

// If the columns have wrapped onto separate rows, leave their natural heights alone.
var firstTop = panels[0].getBoundingClientRect().top;
var sameRow = panels.every(function(p) { return Math.abs(p.getBoundingClientRect().top - firstTop) < 1; });
if (!sameRow) {
return;
}

var tallest = 0;
panels.forEach(function(p) { tallest = Math.max(tallest, p.offsetHeight); });
panels.forEach(function(p) { p.style.minHeight = tallest + 'px'; });
}

if (document.readyState !== 'loading') {
equalizeHeights();
}
else {
document.addEventListener('DOMContentLoaded', equalizeHeights);
}
window.addEventListener('resize', equalizeHeights);
})();
</script>
Loading
Loading