Skip to content
Open
25 changes: 25 additions & 0 deletions resources/queries/targetedms/InstrumentUtilizationByDay.query.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<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>
</column>
<column columnName="ReplicateCount">
<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 ReplicateCount,
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,25 @@
<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>
</column>
<column columnName="ReplicateCount">
<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 ReplicateCount,
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
103 changes: 95 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("ReplicateCount")));
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("ReplicateCount")));
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,94 @@ 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));
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;

public InstrumentUtilizationBean(QueryView byDayView, QueryView byMonthView, QueryView sampleFileView)
{
_byDayView = byDayView;
_byMonthView = byMonthView;
_sampleFileView = sampleFileView;
}

public QueryView getByDayView()
{
return _byDayView;
}

public QueryView getByMonthView()
{
return _byMonthView;
}

public QueryView getSampleFileView()
{
return _sampleFileView;
}
}

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