-
Notifications
You must be signed in to change notification settings - Fork 4
Easy metrics based on replicate and precursor annotation values #1210
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: release26.3-SNAPSHOT
Are you sure you want to change the base?
Changes from all commits
0f8edc7
68ac525
ebfb488
1ccb5fd
bfccdfc
e5aea27
664597c
6effe18
37aff17
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| ALTER TABLE targetedms.QCMetricConfiguration ADD COLUMN AnnotationName VARCHAR(200); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -59,11 +59,14 @@ | |
| }); | ||
|
|
||
| qcMetricsTable += '</table><br>' + | ||
| '<button type="button" class="labkey-button primary" id="saveButton" style="margin-right: 20px;">Save</button>' + | ||
| '<button type="button" class="labkey-button" id="cancelButton" style="margin-right: 20px;">Cancel</button>' + | ||
| '<button type="button" class="labkey-button" id="createNewCustomMetricButton" style="margin-right: 20px;">Add New Custom Metric</button>' + | ||
| '<button type="button" class="labkey-button" id="createNewTraceMetricButton" style="margin-right: 20px;">Add New Trace Metric</button>' + | ||
| '<div style="display:flex;flex-wrap:wrap;gap:8px;">' + | ||
| '<button type="button" class="labkey-button primary" id="saveButton">Save</button>' + | ||
| '<button type="button" class="labkey-button" id="cancelButton">Cancel</button>' + | ||
| '<button type="button" class="labkey-button" id="createNewCustomMetricButton">Add New Custom Metric</button>' + | ||
| '<button type="button" class="labkey-button" id="createNewTraceMetricButton">Add New Trace Metric</button>' + | ||
| '<button type="button" class="labkey-button" id="createNewAnnotationMetricButton">Add Annotation-Backed Metric</button>' + | ||
| '<button type="button" class="labkey-button" id="clearCacheButton">Clear Cached Metric Values</button>' + | ||
| '</div>' + | ||
| '</form><br>Edits to queries backing existing custom metrics require a manual cache clearing to display the updated results.</p>'; | ||
|
|
||
| jQuery('#qcMetricsTable').html(qcMetricsTable); | ||
|
|
@@ -80,7 +83,10 @@ | |
| LABKEY.internal.ConfigureQCMetrics.addNewMetric('custom'); | ||
| }); | ||
| jQuery('#createNewTraceMetricButton').click(function() { | ||
| LABKEY.internal.ConfigureQCMetrics.addNewMetric('trace') | ||
| LABKEY.internal.ConfigureQCMetrics.addNewMetric('trace'); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use a constant here and other places, including for |
||
| }); | ||
| jQuery('#createNewAnnotationMetricButton').click(function() { | ||
| LABKEY.internal.ConfigureQCMetrics.addNewMetric('annotation'); | ||
| }); | ||
| jQuery('#clearCacheButton').click(function() { | ||
| jQuery('#qcMetricsError').text('Clearing cached metrics...'); | ||
|
|
@@ -169,7 +175,10 @@ | |
|
|
||
| const op = 'update'; | ||
| if (clickedQcMetricConfig.TraceName) { | ||
| LABKEY.internal.ConfigureQCMetrics.showTraceMetricWindow(op, clickedQcMetricConfig) | ||
| LABKEY.internal.ConfigureQCMetrics.showTraceMetricWindow(op, clickedQcMetricConfig); | ||
| } | ||
| else if (clickedQcMetricConfig.AnnotationName) { | ||
| LABKEY.internal.ConfigureQCMetrics.showAnnotationMetricWindow(op, clickedQcMetricConfig); | ||
| } | ||
| else { | ||
| LABKEY.internal.ConfigureQCMetrics.showCustomMetricWindow(op, clickedQcMetricConfig); | ||
|
|
@@ -242,13 +251,27 @@ | |
| }); | ||
| }, | ||
|
|
||
| showAnnotationMetricWindow: function (op, clickedMetric) { | ||
| const windowConfig = { | ||
| parent: this, | ||
| operation: op | ||
| }; | ||
| if (clickedMetric) { | ||
| windowConfig.metric = clickedMetric; | ||
| } | ||
| Panorama.Window.AddAnnotationMetricWindow.show(windowConfig); | ||
| }, | ||
|
|
||
| addNewMetric: function (metricType) { | ||
| const op = 'insert'; | ||
| if (metricType === 'custom') { | ||
| this.showCustomMetricWindow(op); | ||
| } | ||
| else if (metricType === 'trace') { | ||
| this.showTraceMetricWindow(op) | ||
| this.showTraceMetricWindow(op); | ||
| } | ||
| else if (metricType === 'annotation') { | ||
| this.showAnnotationMetricWindow(op); | ||
| } | ||
| }, | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,226 @@ | ||||||
| /* | ||||||
| * Copyright (c) 2025 LabKey Corporation. All rights reserved. No portion of this work may be reproduced in | ||||||
| * any form or by any electronic or mechanical means without written permission from LabKey Corporation. | ||||||
| */ | ||||||
|
|
||||||
| (function($) { | ||||||
| window.Panorama = window.Panorama || {}; | ||||||
| window.Panorama.Window = window.Panorama.Window || {}; | ||||||
|
|
||||||
| const DIALOG_ID = 'lk-annotation-metric-dialog'; | ||||||
| let _config = null; | ||||||
| let _allAnnotations = []; | ||||||
|
|
||||||
| function closeDialog() { | ||||||
| $('#' + DIALOG_ID).remove(); | ||||||
| } | ||||||
|
|
||||||
| function showError(msg) { | ||||||
| $('#lk-annotation-metric-error').text(msg).show(); | ||||||
| } | ||||||
|
|
||||||
| function clearErrors() { | ||||||
| $('#lk-annotation-metric-error').hide().text(''); | ||||||
| $('#lk-annotation-metric-name, #lk-annotation-metric-ylabel, #lk-annotation-name-select') | ||||||
| .css('border-color', ''); | ||||||
| } | ||||||
|
|
||||||
| function markInvalid($field) { | ||||||
| $field.css('border-color', 'red'); | ||||||
| } | ||||||
|
|
||||||
| function validate() { | ||||||
| clearErrors(); | ||||||
| let isValid = true; | ||||||
|
|
||||||
| if (!$('#lk-annotation-metric-name').val().trim()) { | ||||||
| markInvalid($('#lk-annotation-metric-name')); | ||||||
| isValid = false; | ||||||
| } | ||||||
| if (!$('#lk-annotation-metric-ylabel').val().trim()) { | ||||||
| markInvalid($('#lk-annotation-metric-ylabel')); | ||||||
| isValid = false; | ||||||
| } | ||||||
| if (!$('#lk-annotation-name-select').val()) { | ||||||
| markInvalid($('#lk-annotation-name-select')); | ||||||
| isValid = false; | ||||||
| } | ||||||
|
|
||||||
| if (!isValid) { | ||||||
| showError('Please fill in all required fields.'); | ||||||
| } | ||||||
| return isValid; | ||||||
| } | ||||||
|
|
||||||
| function getAnnotationTarget() { | ||||||
| return $('input[name="annotationType"]:checked').val() === 'precursor' | ||||||
| ? 'precursor_result' | ||||||
| : 'replicate'; | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the custom query metric dialog and in the table of metrics we use "run" instead of "replicate". I think "replicate" is better. Can you update the other dialog and table to be consistent with the Precursor and Replicate? |
||||||
| } | ||||||
|
|
||||||
| function getFilteredAnnotations() { | ||||||
| const target = getAnnotationTarget(); | ||||||
| const seen = {}; | ||||||
| const result = []; | ||||||
| _allAnnotations.forEach(function(row) { | ||||||
| const targets = (row['Targets'] || '').split(',').map(function(s) { return s.trim(); }); | ||||||
| if (targets.indexOf(target) >= 0 && !seen[row['Name']]) { | ||||||
| seen[row['Name']] = true; | ||||||
| result.push(row['Name']); | ||||||
| } | ||||||
| }); | ||||||
| result.sort(); | ||||||
| return result; | ||||||
| } | ||||||
|
|
||||||
| function refreshAnnotationsSelect() { | ||||||
| const $select = $('#lk-annotation-name-select'); | ||||||
| const currentVal = $select.val(); | ||||||
| $select.empty().append($('<option>').val('').text('-- Select annotation --')); | ||||||
| getFilteredAnnotations().forEach(function(name) { | ||||||
| $select.append($('<option>').val(name).text(name)); | ||||||
| }); | ||||||
|
|
||||||
| const metric = _config && _config.metric; | ||||||
| if (_config.operation === 'update' && metric && metric.AnnotationName) { | ||||||
| $select.val(metric.AnnotationName); | ||||||
| } else if (currentVal) { | ||||||
| $select.val(currentVal); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| function checkMetricNameExists(metricName, callback) { | ||||||
| const filterArray = [LABKEY.Filter.create('Name', metricName, LABKEY.Filter.Types.EQUAL)]; | ||||||
| if (_config.operation === 'update' && _config.metric) { | ||||||
| filterArray.push(LABKEY.Filter.create('id', _config.metric.id, LABKEY.Filter.Types.NOT_EQUAL)); | ||||||
| } | ||||||
| LABKEY.Query.selectRows({ | ||||||
| containerPath: LABKEY.container.id, | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While this should work in practice, better to use the path.
Suggested change
|
||||||
| schemaName: 'targetedms', | ||||||
| queryName: 'qcmetricconfiguration', | ||||||
| filterArray: filterArray, | ||||||
| success: function(data) { callback(data.rows.length > 0); }, | ||||||
| failure: function() { callback(false); } | ||||||
| }); | ||||||
| } | ||||||
|
|
||||||
| function save() { | ||||||
| if (!validate()) return; | ||||||
|
|
||||||
| const metricName = $('#lk-annotation-metric-name').val().trim(); | ||||||
| checkMetricNameExists(metricName, function(exists) { | ||||||
| if (exists) { | ||||||
| showError('A metric with the name "' + LABKEY.Utils.encodeHtml(metricName) + '" already exists. Please choose a different name.'); | ||||||
| markInvalid($('#lk-annotation-metric-name')); | ||||||
| return; | ||||||
| } | ||||||
|
|
||||||
| const newMetric = { | ||||||
| Name: metricName, | ||||||
| QueryName: 'QCAnnotationMetric', | ||||||
| YAxisLabel: $('#lk-annotation-metric-ylabel').val().trim(), | ||||||
| PrecursorScoped: $('input[name="annotationType"]:checked').val() === 'precursor', | ||||||
| AnnotationName: $('#lk-annotation-name-select').val() | ||||||
| }; | ||||||
| if (_config.operation === 'update') { | ||||||
| newMetric.id = _config.metric.id; | ||||||
| } | ||||||
|
|
||||||
| LABKEY.Query.saveRows({ | ||||||
| containerPath: LABKEY.container.id, | ||||||
| commands: [{ schemaName: 'targetedms', queryName: 'qcmetricconfiguration', command: _config.operation, rows: [newMetric] }], | ||||||
| method: 'POST', | ||||||
| success: function() { window.location.reload(); }, | ||||||
| failure: function(response) { | ||||||
| showError((response && (response.exception || response.message)) || 'Error saving metric'); | ||||||
| } | ||||||
| }); | ||||||
| }); | ||||||
| } | ||||||
|
|
||||||
| function deleteMetric() { | ||||||
| if (!confirm('This will delete the "' + _config.metric.name + '" metric. Are you sure?')) return; | ||||||
|
|
||||||
| LABKEY.Query.saveRows({ | ||||||
| containerPath: LABKEY.container.id, | ||||||
| commands: [ | ||||||
| { schemaName: 'targetedms', queryName: 'qcenabledmetrics', command: 'delete', rows: [{ metric: _config.metric.id }] }, | ||||||
| { schemaName: 'targetedms', queryName: 'qcmetricconfiguration', command: 'delete', rows: [{ id: _config.metric.id }] } | ||||||
| ], | ||||||
| method: 'POST', | ||||||
| success: function() { window.location.reload(); }, | ||||||
| failure: function(response) { | ||||||
| showError((response && (response.exception || response.message)) || 'Error deleting metric'); | ||||||
| } | ||||||
| }); | ||||||
| } | ||||||
|
|
||||||
| function buildDialogHtml() { | ||||||
| const op = _config.operation; | ||||||
| const metric = _config.metric || {}; | ||||||
| const isPrecursor = op === 'update' && metric.PrecursorScoped; | ||||||
| const title = op === 'insert' ? 'Add Annotation-Backed Metric' : 'Edit Annotation-Backed Metric'; | ||||||
|
|
||||||
| return '<div id="' + DIALOG_ID + '" style="position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);z-index:9999;display:flex;align-items:center;justify-content:center;">' | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is somewhat frustratingly inconsistent with the query based custom metric dialog, which has almost identical controls. The fields are in different orders, we use a radio vs a drop-down for replicate vs precursor, and the button layout is different. While I'd like to refactor all of this into React soon, can you adapt the query metric dialog to use this plain HTML/JS approach too? |
||||||
| + '<div class="x4-window x4-window-default" style="min-width:480px;max-width:580px;">' | ||||||
| + '<div class="x4-window-header x4-window-header-default x4-window-header-default-top" style="padding:4px 8px;border:none;">' | ||||||
| + '<p class="x4-window-header-text-container-default" style="font-size:14px;margin:0;">' + LABKEY.Utils.encodeHtml(title) + '</p>' | ||||||
| + '</div>' | ||||||
| + '<div class="x4-window-body" style="background:white;padding:10px 12px;">' | ||||||
| + '<table style="border-collapse:collapse;width:100%;">' | ||||||
| + '<tr><td style="padding:5px 10px 5px 0;white-space:nowrap;"><label for="lk-annotation-metric-name">Metric Name *</label></td>' | ||||||
| + '<td style="padding:5px 0;"><input type="text" id="lk-annotation-metric-name" style="width:100%;box-sizing:border-box;" value="' + LABKEY.Utils.encodeHtml(metric.name || '') + '"/></td></tr>' | ||||||
| + '<tr><td style="padding:5px 10px 5px 0;white-space:nowrap;"><label for="lk-annotation-metric-ylabel">Y-Axis Label *</label></td>' | ||||||
| + '<td style="padding:5px 0;"><input type="text" id="lk-annotation-metric-ylabel" style="width:100%;box-sizing:border-box;" value="' + LABKEY.Utils.encodeHtml(metric.YAxisLabel || '') + '"/></td></tr>' | ||||||
| + '<tr><td style="padding:5px 10px 5px 0;white-space:nowrap;">Annotation Type</td>' | ||||||
| + '<td style="padding:5px 0;">' | ||||||
| + '<label style="margin-right:16px;"><input type="radio" name="annotationType" value="replicate"' + (!isPrecursor ? ' checked' : '') + '> Replicate</label>' | ||||||
| + '<label><input type="radio" name="annotationType" value="precursor"' + (isPrecursor ? ' checked' : '') + '> Precursor</label>' | ||||||
| + '</td></tr>' | ||||||
| + '<tr><td style="padding:5px 10px 5px 0;white-space:nowrap;"><label for="lk-annotation-name-select">Annotation *</label></td>' | ||||||
| + '<td style="padding:5px 0;"><select id="lk-annotation-name-select" style="width:100%;box-sizing:border-box;"><option value="">Loading...</option></select></td></tr>' | ||||||
| + '</table>' | ||||||
| + '<div id="lk-annotation-metric-error" class="labkey-error" style="display:none;margin-top:8px;"></div>' | ||||||
| + '<div style="margin-top:12px;text-align:right;">' | ||||||
| + '<button type="button" class="labkey-button" id="lk-annotation-metric-cancel">Cancel</button>' | ||||||
| + (op === 'update' ? ' <button type="button" class="labkey-button" id="lk-annotation-metric-delete">Delete</button>' : '') | ||||||
| + ' <button type="button" class="labkey-button primary" id="lk-annotation-metric-save">Save</button>' | ||||||
| + '</div>' | ||||||
| + '</div>' | ||||||
| + '</div>' | ||||||
| + '</div>'; | ||||||
| } | ||||||
|
|
||||||
| window.Panorama.Window.AddAnnotationMetricWindow = { | ||||||
| show: function(config) { | ||||||
| _config = config; | ||||||
| _allAnnotations = []; | ||||||
|
|
||||||
| $('#' + DIALOG_ID).remove(); | ||||||
| $('body').append(buildDialogHtml()); | ||||||
|
|
||||||
| $('#lk-annotation-metric-cancel').on('click', closeDialog); | ||||||
| $('#lk-annotation-metric-save').on('click', save); | ||||||
| if (config.operation === 'update') { | ||||||
| $('#lk-annotation-metric-delete').on('click', deleteMetric); | ||||||
| } | ||||||
| $('input[name="annotationType"]').on('change', refreshAnnotationsSelect); | ||||||
|
|
||||||
| // close on overlay click | ||||||
| $('#' + DIALOG_ID).on('click', function(e) { | ||||||
| if (e.target === this) closeDialog(); | ||||||
| }); | ||||||
|
|
||||||
| LABKEY.Query.selectRows({ | ||||||
| schemaName: 'targetedms', | ||||||
| queryName: 'AnnotationSettings', | ||||||
| columns: ['Name', 'Targets', 'Type'], | ||||||
| filterArray: [LABKEY.Filter.create('Type', 'number', LABKEY.Filter.Types.EQUAL)], | ||||||
| success: function(data) { | ||||||
| _allAnnotations = data.rows || []; | ||||||
| refreshAnnotationsSelect(); | ||||||
| } | ||||||
| }); | ||||||
| } | ||||||
| }; | ||||||
| })(jQuery); | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should match other tables.