Skip to content

Commit 9a4a095

Browse files
committed
Combine instrument utilization views into tabbed web part; pair info panel
1 parent 6b371cc commit 9a4a095

6 files changed

Lines changed: 257 additions & 131 deletions

File tree

src/org/labkey/targetedms/TargetedMSController.java

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4888,38 +4888,40 @@ public ModelAndView getView(InstrumentForm form, BindException errors) throws Ex
48884888

48894889
VBox result = new VBox();
48904890

4891+
VBox instrumentInfoView = new VBox();
48914892
for (InstrumentNickname name : names)
48924893
{
48934894
var nameView = new JspView<>("/org/labkey/targetedms/view/nickname.jsp", name);
48944895
nameView.setTitle("Instrument Info");
48954896
nameView.setFrame(WebPartView.FrameType.PORTAL);
4896-
result.addView(nameView);
4897+
instrumentInfoView.addView(nameView);
48974898
}
48984899

4899-
var calendarView = new JspView<>("/org/labkey/targetedms/view/instrumentUtilizationCalendar.jsp", form.getName());
4900-
calendarView.setTitle("Utilization Calendar");
4901-
calendarView.setFrame(WebPartView.FrameType.PORTAL);
4902-
result.addView(calendarView);
4900+
QueryView folderSummaryView = createQueryView(form, errors, false, FOLDER_SUMMARY);
4901+
folderSummaryView.setTitle("Summary by Folder");
4902+
folderSummaryView.setFrame(WebPartView.FrameType.PORTAL);
4903+
4904+
// Instrument info is narrow; pair it with the folder summary grid in a two-column row so the
4905+
// otherwise-empty space to the right of the info panel is put to use.
4906+
var infoRowView = new JspView<>("/org/labkey/targetedms/view/instrumentInfoRow.jsp",
4907+
new InstrumentInfoRowBean(instrumentInfoView, folderSummaryView));
4908+
infoRowView.setFrame(WebPartView.FrameType.NONE);
4909+
result.addView(infoRowView);
49034910

49044911
QueryView byDayView = createInitializedQueryView(form, errors, false, UTILIZATION_BY_DAY);
49054912
byDayView.setFrame(WebPartView.FrameType.NONE);
49064913
QueryView byMonthView = createInitializedQueryView(form, errors, false, UTILIZATION_BY_MONTH);
49074914
byMonthView.setFrame(WebPartView.FrameType.NONE);
49084915

4909-
var utilizationView = new JspView<>("/org/labkey/targetedms/view/instrumentUtilizationGrids.jsp", new InstrumentUtilizationBean(byDayView, byMonthView));
4910-
utilizationView.setTitle("Runs Acquired");
4916+
var utilizationView = new JspView<>("/org/labkey/targetedms/view/instrumentUtilization.jsp", new InstrumentUtilizationBean(byDayView, byMonthView));
4917+
utilizationView.setTitle("Instrument Utilization Across Folders");
49114918
utilizationView.setFrame(WebPartView.FrameType.PORTAL);
49124919
result.addView(utilizationView);
49134920

4914-
QueryView folderSummaryView = createQueryView(form, errors, false, FOLDER_SUMMARY);
4915-
folderSummaryView.setTitle("Summary by Folder");
4916-
folderSummaryView.setFrame(WebPartView.FrameType.PORTAL);
4917-
49184921
QueryView sampleFileView = createQueryView(form, errors, false, TargetedMSSchema.TABLE_SAMPLE_FILE);
49194922
sampleFileView.setTitle("Samples from " + form.getName());
49204923
sampleFileView.setFrame(WebPartView.FrameType.PORTAL);
49214924

4922-
result.addView(folderSummaryView);
49234925
result.addView(sampleFileView);
49244926

49254927
return result;
@@ -4948,6 +4950,28 @@ public QueryView getByMonthView()
49484950
}
49494951
}
49504952

4953+
public static class InstrumentInfoRowBean
4954+
{
4955+
private final HttpView _infoView;
4956+
private final HttpView _summaryView;
4957+
4958+
public InstrumentInfoRowBean(HttpView infoView, HttpView summaryView)
4959+
{
4960+
_infoView = infoView;
4961+
_summaryView = summaryView;
4962+
}
4963+
4964+
public HttpView getInfoView()
4965+
{
4966+
return _infoView;
4967+
}
4968+
4969+
public HttpView getSummaryView()
4970+
{
4971+
return _summaryView;
4972+
}
4973+
}
4974+
49514975
@RequiresPermission(ReadPermission.class)
49524976
public class ShowReplicatesAction extends ShowRunSingleDetailsAction<RunDetailsForm>
49534977
{
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<%
2+
/*
3+
* Copyright (c) 2026 LabKey Corporation
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
%>
18+
<%@ page import="org.labkey.api.view.HttpView" %>
19+
<%@ page import="org.labkey.api.view.JspView" %>
20+
<%@ page import="org.labkey.targetedms.TargetedMSController.InstrumentInfoRowBean" %>
21+
<%@ page extends="org.labkey.api.jsp.JspBase" %>
22+
<%
23+
JspView<InstrumentInfoRowBean> me = HttpView.currentView();
24+
InstrumentInfoRowBean bean = me.getModelBean();
25+
%>
26+
<div class="row" id="lk-instrument-info-row">
27+
<div class="col-md-5">
28+
<% me.include(bean.getInfoView(), out); %>
29+
</div>
30+
<div class="col-md-7" style="max-width: 100%; overflow-x: auto;">
31+
<% me.include(bean.getSummaryView(), out); %>
32+
</div>
33+
</div>
34+
35+
<script type="text/javascript" nonce="<%=getScriptNonce()%>">
36+
(function() {
37+
// Match the height of the two side-by-side web part panels (Instrument Info is shorter than the
38+
// Summary by Folder grid). Done in JS rather than flex CSS so it is robust to the web part frame
39+
// markup, and it self-disables when the columns stack on narrow viewports.
40+
function equalizeHeights() {
41+
var cols = document.querySelectorAll('#lk-instrument-info-row > [class*="col-"]');
42+
var panels = [];
43+
for (var i = 0; i < cols.length; i++) {
44+
var panel = cols[i].querySelector('.panel');
45+
if (panel) {
46+
panels.push(panel);
47+
}
48+
}
49+
if (panels.length < 2) {
50+
return;
51+
}
52+
53+
// Reset before measuring so re-runs (e.g. on resize) don't accumulate.
54+
panels.forEach(function(p) { p.style.minHeight = ''; });
55+
56+
// If the columns have wrapped onto separate rows, leave their natural heights alone.
57+
var firstTop = panels[0].getBoundingClientRect().top;
58+
var sameRow = panels.every(function(p) { return Math.abs(p.getBoundingClientRect().top - firstTop) < 1; });
59+
if (!sameRow) {
60+
return;
61+
}
62+
63+
var tallest = 0;
64+
panels.forEach(function(p) { tallest = Math.max(tallest, p.offsetHeight); });
65+
panels.forEach(function(p) { p.style.minHeight = tallest + 'px'; });
66+
}
67+
68+
if (document.readyState !== 'loading') {
69+
equalizeHeights();
70+
}
71+
else {
72+
document.addEventListener('DOMContentLoaded', equalizeHeights);
73+
}
74+
window.addEventListener('resize', equalizeHeights);
75+
})();
76+
</script>

src/org/labkey/targetedms/view/instrumentUtilizationCalendar.jsp renamed to src/org/labkey/targetedms/view/instrumentUtilization.jsp

Lines changed: 92 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515
* limitations under the License.
1616
*/
1717
%>
18+
<%@ page import="org.labkey.api.view.HttpView" %>
19+
<%@ page import="org.labkey.api.view.JspView" %>
1820
<%@ page import="org.labkey.api.view.template.ClientDependencies" %>
21+
<%@ page import="org.labkey.targetedms.TargetedMSController.InstrumentUtilizationBean" %>
1922
<%@ page extends="org.labkey.api.jsp.JspBase" %>
2023
<%!
2124
@Override
@@ -25,6 +28,10 @@
2528
dependencies.add("targetedms/yearCalendar");
2629
}
2730
%>
31+
<%
32+
JspView<InstrumentUtilizationBean> me = HttpView.currentView();
33+
InstrumentUtilizationBean bean = me.getModelBean();
34+
%>
2835

2936
<style nonce="<%=getScriptNonce()%>">
3037
/*
@@ -43,6 +50,91 @@
4350
}
4451
</style>
4552

53+
<ul class="nav nav-tabs" id="utilizationTabs" role="tablist">
54+
<li class="active"><a href="#utilizationTabCalendar" data-utilization-tab="calendar">Utilization Calendar</a></li>
55+
<li><a href="#utilizationTabMonth" data-utilization-tab="month">Runs by Month</a></li>
56+
<li><a href="#utilizationTabDay" data-utilization-tab="day">Runs by Day</a></li>
57+
</ul>
58+
59+
<div class="tab-content" style="padding-top: 15px;">
60+
<div class="tab-pane active" id="utilizationTabCalendar">
61+
<div style="text-align: center; width: 100%">
62+
<label for="utilizationMonthNumberSelect">Display:</label>
63+
<select id="utilizationMonthNumberSelect">
64+
<option value="1">1 month</option>
65+
<option value="4">4 months</option>
66+
<option value="12">12 months</option>
67+
</select>
68+
</div>
69+
70+
<div id="instrumentUtilizationCalendarWrapper" style="min-height: 300px; max-width: 100%; overflow-x: auto;">
71+
<div id="instrumentUtilizationCalendar">
72+
Loading...
73+
</div>
74+
</div>
75+
76+
<div class="heatmap-footer-container">
77+
<div class="heatmap-legend-container">
78+
<div class="heatmap-legend-label" style="text-align: right">No data</div>
79+
<div class="heatmap-legend">
80+
<div class="heatmap-legend-element"></div>
81+
<div class="heatmap-legend-element heatmap-shade0"></div>
82+
<div class="heatmap-legend-element heatmap-shade3"></div>
83+
<div class="heatmap-legend-element heatmap-shade6"></div>
84+
<div class="heatmap-legend-element heatmap-shade9"></div>
85+
<div class="heatmap-legend-element heatmap-shade13"></div>
86+
</div>
87+
<div class="heatmap-legend-label" id="heatmapFileLegendMax">Files acquired</div>
88+
</div>
89+
</div>
90+
</div>
91+
92+
<div class="tab-pane" id="utilizationTabMonth">
93+
<div id="utilizationByMonthGrid" style="max-width: 100%; overflow-x: auto;">
94+
<% me.include(bean.getByMonthView(), out); %>
95+
</div>
96+
</div>
97+
98+
<div class="tab-pane" id="utilizationTabDay">
99+
<div id="utilizationByDayGrid" style="max-width: 100%; overflow-x: auto;">
100+
<% me.include(bean.getByDayView(), out); %>
101+
</div>
102+
</div>
103+
</div>
104+
105+
<script type="text/javascript" nonce="<%=getScriptNonce()%>">
106+
(function() {
107+
// Simple client-side tab switching. Panes are pre-rendered; we toggle Bootstrap's `active` class
108+
// (which drives .tab-pane visibility) so the calendar (default/active tab) initializes while
109+
// visible and the grids simply reveal on demand.
110+
const tabLinks = document.querySelectorAll('#utilizationTabs a[data-utilization-tab]');
111+
const panes = {
112+
calendar: document.getElementById('utilizationTabCalendar'),
113+
month: document.getElementById('utilizationTabMonth'),
114+
day: document.getElementById('utilizationTabDay')
115+
};
116+
117+
function activate(which) {
118+
for (const link of tabLinks) {
119+
const li = link.parentNode;
120+
li.classList.toggle('active', link.getAttribute('data-utilization-tab') === which);
121+
}
122+
for (const key in panes) {
123+
if (panes[key]) {
124+
panes[key].classList.toggle('active', key === which);
125+
}
126+
}
127+
}
128+
129+
for (const link of tabLinks) {
130+
link.addEventListener('click', function(e) {
131+
e.preventDefault();
132+
activate(link.getAttribute('data-utilization-tab'));
133+
});
134+
}
135+
})();
136+
</script>
137+
46138
<script type="text/javascript" nonce="<%=getScriptNonce()%>">
47139
(function() {
48140
let calendar = null;
@@ -214,33 +306,3 @@
214306
});
215307
})();
216308
</script>
217-
218-
<div style="text-align: center; width: 100%">
219-
<label for="utilizationMonthNumberSelect">Display:</label>
220-
<select id="utilizationMonthNumberSelect">
221-
<option value="1">1 month</option>
222-
<option value="4">4 months</option>
223-
<option value="12">12 months</option>
224-
</select>
225-
</div>
226-
227-
<div id="instrumentUtilizationCalendarWrapper" style="min-height: 300px; max-width: 100%; overflow-x: auto;">
228-
<div id="instrumentUtilizationCalendar">
229-
Loading...
230-
</div>
231-
</div>
232-
233-
<div class="heatmap-footer-container">
234-
<div class="heatmap-legend-container">
235-
<div class="heatmap-legend-label" style="text-align: right">No data</div>
236-
<div class="heatmap-legend">
237-
<div class="heatmap-legend-element"></div>
238-
<div class="heatmap-legend-element heatmap-shade0"></div>
239-
<div class="heatmap-legend-element heatmap-shade3"></div>
240-
<div class="heatmap-legend-element heatmap-shade6"></div>
241-
<div class="heatmap-legend-element heatmap-shade9"></div>
242-
<div class="heatmap-legend-element heatmap-shade13"></div>
243-
</div>
244-
<div class="heatmap-legend-label" id="heatmapFileLegendMax">Files acquired</div>
245-
</div>
246-
</div>

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

Lines changed: 0 additions & 55 deletions
This file was deleted.

0 commit comments

Comments
 (0)