Skip to content

Commit e4fca3c

Browse files
authored
Merge pull request #342 from AllenNeuralDynamics/release-v1.22.0
Release v1.22.0
2 parents a121051 + 2502523 commit e4fca3c

File tree

4 files changed

+277
-26
lines changed

4 files changed

+277
-26
lines changed

src/aind_data_transfer_service/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import os
44

5-
__version__ = "1.21.0"
5+
__version__ = "1.22.0"
66

77
# Global constants
88
OPEN_DATA_BUCKET_NAME = os.getenv("OPEN_DATA_BUCKET_NAME", "open")

src/aind_data_transfer_service/models/core.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,33 @@ def validate_with_context(cls, v: str, info: ValidationInfo) -> str:
240240
else:
241241
return v
242242

243+
@field_validator("tasks", mode="after")
244+
def validate_tasks(
245+
cls, v: Dict[str, Union[Task, Dict[str, Task]]]
246+
) -> Dict[str, Union[Task, Dict[str, Task]]]:
247+
"""Validate that modality-specific tasks are keyed by valid
248+
modality abbreviations."""
249+
modality_tasks = [
250+
"modality_transformation_settings",
251+
"codeocean_pipeline_settings",
252+
]
253+
for task_id, task_value in v.items():
254+
if task_id in modality_tasks:
255+
if isinstance(task_value, Task) and task_value.skip_task:
256+
continue
257+
if not isinstance(task_value, dict):
258+
raise ValueError(
259+
f"{task_id} must be a dictionary of modality "
260+
f"abbreviations to Task objects."
261+
)
262+
for modality_key in task_value.keys():
263+
if modality_key not in Modality.abbreviation_map:
264+
raise ValueError(
265+
f'Key {modality_key} in tasks["{task_id}"] is not '
266+
f"a valid modality abbreviation."
267+
)
268+
return v
269+
243270

244271
class SubmitJobRequestV2(BaseSettings):
245272
"""Main request that will be sent to the backend. Bundles jobs into a list

src/aind_data_transfer_service/templates/job_status.html

Lines changed: 188 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ <h4 class="mb-2">Jobs Submitted: <span id="total-entries"></span></h4>
4040
<!-- filter by job status -->
4141
<div class="input-group input-group-sm mb-1">
4242
<span class="input-group-text" style="width:35%">Status</span>
43-
<select class="form-select" onchange="filterJobsByColumn(4, this.value);this.blur();">
43+
<select class="form-select" onchange="filterJobsByColumn(5, this.value);this.blur();">
4444
{% for s in [
4545
{"label": "all", "value": "", "class": "text-dark"},
4646
{"label": "queued", "value": "queued", "class": "text-secondary"},
@@ -61,31 +61,31 @@ <h4 class="mb-2">Jobs Submitted: <span id="total-entries"></span></h4>
6161
<hr class="flex-grow-1 border-secondary">
6262
<div class="input-group input-group-sm mb-1">
6363
<span class="input-group-text" style="width:35%">Asset Name</span>
64-
<input id="filter-name-input" type="text" class="form-control" placeholder="asset name" oninput="filterJobsByColumn(0, this.value)">
65-
<button id="filter-name-clear" class="btn btn-outline-secondary" type="button" title="Clear" onclick="clearFilterJobsByColumn(0, '#filter-name-input')">
64+
<input id="filter-name-input" type="text" class="form-control" placeholder="asset name" oninput="filterJobsByColumn(1, this.value)">
65+
<button id="filter-name-clear" class="btn btn-outline-secondary" type="button" title="Clear" onclick="clearFilterJobsByColumn(1, '#filter-name-input')">
6666
<i class="bi bi-x-lg"></i>
6767
</button>
6868
</div>
6969
<!-- filter by job id-->
7070
<div class="input-group input-group-sm mb-1">
7171
<span class="input-group-text" style="width:35%">Job ID</span>
72-
<input id="filter-id-input" type="text" class="form-control" placeholder="job id" oninput="filterJobsByColumn(1, this.value)">
73-
<button id="filter-id-clear" class="btn btn-outline-secondary" type="button" title="Clear" onclick="clearFilterJobsByColumn(1, '#filter-id-input')">
72+
<input id="filter-id-input" type="text" class="form-control" placeholder="job id" oninput="filterJobsByColumn(2, this.value)">
73+
<button id="filter-id-clear" class="btn btn-outline-secondary" type="button" title="Clear" onclick="clearFilterJobsByColumn(2, '#filter-id-input')">
7474
<i class="bi bi-x-lg"></i>
7575
</button>
7676
</div>
7777
<!-- filter by job type-->
7878
<div class="input-group input-group-sm mb-1">
7979
<span class="input-group-text" style="width:35%">Job Type</span>
80-
<input id="filter-job-type-input" type="text" class="form-control" placeholder="job type" oninput="filterJobsByColumn(2, this.value)">
81-
<button id="filter-job-type-clear" class="btn btn-outline-secondary" type="button" title="Clear" onclick="clearFilterJobsByColumn(2, '#filter-job-type-input')">
80+
<input id="filter-job-type-input" type="text" class="form-control" placeholder="job type" oninput="filterJobsByColumn(3, this.value)">
81+
<button id="filter-job-type-clear" class="btn btn-outline-secondary" type="button" title="Clear" onclick="clearFilterJobsByColumn(3, '#filter-job-type-input')">
8282
<i class="bi bi-x-lg"></i>
8383
</button>
8484
</div>
8585
<!-- filter by dag id -->
8686
<div class="input-group input-group-sm mb-1">
8787
<span class="input-group-text" style="width:35%">Dag ID</span>
88-
<select class="form-select" onchange="filterJobsByColumn(3, this.value);this.blur();">
88+
<select class="form-select" onchange="filterJobsByColumn(4, this.value);this.blur();">
8989
<option class="text-dark" value="">all</option>
9090
{% for dag_id in dag_ids %}
9191
<option class="text-secondary" value="^{{ dag_id }}$">{{ dag_id }}</option>
@@ -99,6 +99,7 @@ <h4 class="mb-2">Jobs Submitted: <span id="total-entries"></span></h4>
9999
<table id="searchJobsTable" class="display compact table table-bordered table-sm" style="font-size: small">
100100
<thead>
101101
<tr>
102+
<th></th>
102103
<th>Asset Name</th>
103104
<th>Job ID</th>
104105
<th>Job Type</th>
@@ -130,6 +131,45 @@ <h4 class="mb-2">Jobs Submitted: <span id="total-entries"></span></h4>
130131
</div>
131132
</div>
132133
</div>
134+
<!-- modal for cancelling a running job -->
135+
<div class="modal fade" id="cancel-modal-full" tabindex="-1" aria-labelledby="cancel-modal-full-label" aria-hidden="true">
136+
<div class="modal-dialog modal-l">
137+
<div class="modal-content">
138+
<div class="modal-header p-2">
139+
<div class="modal-title fw-bold" id="cancel-modal-full-label" style="font-size: small">Cancel Upload Job</div>
140+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
141+
</div>
142+
<div class="cancel-modal-body p-3" style="font-size: small">
143+
<div class="alert alert-danger mb-3" role="alert">
144+
<div class="mb-2"><strong>WARNING!</strong> You are about to cancel the following upload job:</div>
145+
<div><strong>Asset Name:</strong> <span id="cancel-modal-job-name"></span></div>
146+
<div><strong>Job ID:</strong> <span id="cancel-modal-job-id"></span></div>
147+
</div>
148+
<div class="mb-3">
149+
If data has already begun uploading to the cloud, please contact a data administrator to
150+
remove the partial upload.
151+
</div>
152+
<div class="mb-3">
153+
Please type <strong>YES</strong> to confirm:
154+
<input id="cancel-confirm-input" type="text" class="form-control"placeholder="YES"
155+
oninput="onCancelConfirmationInput(this.value)"
156+
>
157+
</div>
158+
<button id="cancel-confirm-button" type="button" class="btn btn-danger" disabled
159+
onclick="cancelUploadJob()"
160+
>Confirm</button>
161+
<div id="cancel-feedback" class="mt-2">
162+
<div id="cancel-spinner" class="spinner-border text-primary ms-2" role="status" hidden><span class="visually-hidden">Loading...</span></div>
163+
</div>
164+
<div id="success-message" class="text-success mt-2" hidden>Cancellation request sent
165+
</div>
166+
<div id="error-message" class="alert alert-danger mt-2 p-2" hidden>Error sending cancellation request
167+
</div>
168+
</div>
169+
</div>
170+
</div>
171+
</div>
172+
</div>
133173
</div>
134174
</div>
135175
<script>
@@ -140,7 +180,7 @@ <h4 class="mb-2">Jobs Submitted: <span id="total-entries"></span></h4>
140180
const today = moment();
141181
const twoWeeksAgo = moment().subtract(13, 'days');
142182

143-
// initialize daterangepicker for submit date filter
183+
// Initialize daterangepicker for submit date filter
144184
$('#submit-date-range').daterangepicker({
145185
startDate: twoWeeksAgo,
146186
endDate: today,
@@ -154,15 +194,15 @@ <h4 class="mb-2">Jobs Submitted: <span id="total-entries"></span></h4>
154194
}
155195
}, filterJobsBySubmitTimeRange);
156196

157-
// initialize job status table with default values
197+
// Initialize job status table with default values
158198
const initialParams = {
159199
// set default submit date range client-side for browser's local time
160200
execution_date_gte: twoWeeksAgo.startOf('day').toISOString(),
161201
execution_date_lte: today.endOf('day').toISOString(),
162202
};
163203
updateJobStatusTable(initialParams);
164204

165-
// tasks modal for full jobs table
205+
// Tasks modal for full jobs table
166206
var tasksModal = document.getElementById('tasks-modal-full');
167207
tasksModal.addEventListener('show.bs.modal', function (event) {
168208
var sourceData = event.relatedTarget?.dataset;
@@ -171,6 +211,20 @@ <h4 class="mb-2">Jobs Submitted: <span id="total-entries"></span></h4>
171211
tasksModal.addEventListener('hidden.bs.modal', function (event) {
172212
updateJobTasksModal(null, null, null, null);
173213
})
214+
// Cancel modal
215+
var cancelModal = document.getElementById('cancel-modal-full');
216+
cancelModal.addEventListener('show.bs.modal', function (event) {
217+
// Store source data in the modal's dataset
218+
var sourceData = event.relatedTarget?.dataset;
219+
cancelModal.dataset.dagId = sourceData?.dagId;
220+
cancelModal.dataset.jobId = sourceData?.jobId;
221+
cancelModal.dataset.jobName = sourceData?.jobName;
222+
// update modal content
223+
updateCancelJobModal();
224+
})
225+
cancelModal.addEventListener('hidden.bs.modal', function (event) {
226+
resetCancelJobModal();
227+
})
174228
});
175229
// FULL JOBS TABLE -----------------------------------------------
176230
// Helper functions for custom column rendering
@@ -195,6 +249,29 @@ <h4 class="mb-2">Jobs Submitted: <span id="total-entries"></span></h4>
195249
}
196250
return data;
197251
}
252+
// Cancel Job Button Renderer
253+
function renderCancelJobButton(data, type, row) {
254+
// Only generate HTML for the UI display
255+
if (type !== 'display') {
256+
return data; // fallback
257+
}
258+
const disabled = row.job_state !== "running" && row.job_state !== "queued";
259+
return `
260+
<button
261+
type="button"
262+
class="btn btn-outline-danger btn-sm rounded-circle"
263+
${disabled ? "disabled" : ""}
264+
title="Cancel running job"
265+
data-bs-toggle="modal"
266+
data-bs-target="#cancel-modal-full"
267+
data-dag-id="${row.dag_id}"
268+
data-job-id="${row.job_id}"
269+
data-job-name="${row.name}"
270+
>
271+
<i class="bi bi-x-lg"></i>
272+
</button>
273+
`;
274+
}
198275
// Create DataTable for full jobs table
199276
function createFullJobsTable() {
200277
$('#searchJobsTable').DataTable({
@@ -204,6 +281,13 @@ <h4 class="mb-2">Jobs Submitted: <span id="total-entries"></span></h4>
204281
},
205282
processing: true,
206283
columns: [
284+
{
285+
data: null,
286+
orderable: false,
287+
searchable: false,
288+
width: '40px',
289+
render: renderCancelJobButton
290+
},
207291
{ data: 'name', searchable: true },
208292
{ data: 'job_id', searchable: true },
209293
{ data: 'job_type', searchable: true },
@@ -218,7 +302,7 @@ <h4 class="mb-2">Jobs Submitted: <span id="total-entries"></span></h4>
218302
initComplete: (settings, json) => updateJobsCount(json),
219303
// layout options
220304
pageLength: 25,
221-
order: [5, 'desc'], // submit time descending
305+
order: [6, 'desc'], // submit time descending
222306
layout: {
223307
topStart: null,
224308
topEnd: null,
@@ -271,13 +355,96 @@ <h4 class="mb-2">Jobs Submitted: <span id="total-entries"></span></h4>
271355
tasksIframe.src = "";
272356
}
273357
}
358+
// Modal for cancelling a job from full jobs table
359+
function updateCancelJobModal() {
360+
const cancelModal = document.getElementById('cancel-modal-full');
361+
document.getElementById('cancel-modal-job-name').textContent = cancelModal.dataset.jobName;
362+
document.getElementById('cancel-modal-job-id').textContent = cancelModal.dataset.jobId;
363+
}
364+
function resetCancelJobModal() {
365+
// Clear modal content
366+
document.getElementById('cancel-modal-job-name').textContent = '';
367+
document.getElementById('cancel-modal-job-id').textContent = '';
368+
document.getElementById('cancel-confirm-input').value = '';
369+
document.getElementById('cancel-confirm-button').disabled = true;
370+
371+
// Hide messages and spinner
372+
document.getElementById('success-message').hidden = true;
373+
document.getElementById('error-message').hidden = true;
374+
document.getElementById('cancel-spinner').hidden = true;
375+
376+
// Clear stored data in modal dataset
377+
const cancelModal = document.getElementById('cancel-modal-full');
378+
cancelModal.dataset.dagId = '';
379+
cancelModal.dataset.jobId = '';
380+
cancelModal.dataset.jobName = '';
381+
}
382+
function cancelUploadJob() {
383+
// Get job data from modal dataset
384+
const cancelModal = document.getElementById('cancel-modal-full');
385+
const dagId = cancelModal.dataset.dagId;
386+
const jobId = cancelModal.dataset.jobId;
387+
const jobName = cancelModal.dataset.jobName;
388+
if (!dagId || !jobId || !jobName) {
389+
alert($`Error: Missing data to cancel job!\n\nDag ID: ${dagId}\nJob ID: ${jobId}\nAsset Name: ${jobName}`);
390+
return;
391+
}
392+
393+
// Disable confirm button to prevent double submission
394+
const confirmButton = $('#cancel-confirm-button');
395+
confirmButton.prop('disabled', true);
396+
397+
// Show spinner while waiting for server response to cancel job
398+
const spinner = document.getElementById('cancel-spinner');
399+
spinner.hidden = false;
400+
401+
// Log the payload for debugging
402+
const payload = {
403+
dag_run_id: jobId,
404+
dag_id: dagId,
405+
s3_prefix: jobName
406+
};
407+
console.log('Sending cancellation payload:', payload);
408+
409+
$.ajax({
410+
type: 'POST',
411+
url: '{{ url_for("cancel_job") }}',
412+
contentType: 'application/json',
413+
data: JSON.stringify({
414+
dag_run_id: jobId,
415+
dag_id: dagId,
416+
s3_prefix: jobName
417+
}),
418+
success: function(response) {
419+
spinner.hidden = true;
420+
document.getElementById('success-message').hidden = false;
421+
// Refresh table to reflect job cancellation
422+
$('#searchJobsTable').DataTable().ajax.reload(null, false);
423+
},
424+
error: function(xhr, status, error) {
425+
spinner.hidden = true;
426+
const errMsg = xhr.responseJSON?.data?.error || error;
427+
const errorDiv = document.getElementById('error-message');
428+
errorDiv.textContent = `Error sending cancellation request: ${errMsg}`;
429+
errorDiv.hidden = false;
430+
confirmButton.prop('disabled', false);
431+
}
432+
});
433+
}
434+
// Attach Enter key listener ONCE; triggers only if the user types "YES"
435+
$('#cancel-confirm-input').on('keypress', function(e) {
436+
if (e.key === 'Enter' && this.value === 'YES') {
437+
cancelUploadJob();
438+
}
439+
});
440+
274441
// EVENT HANDLERS ------------------------------------------------
275442
function updateJobStatusTable(newParams) {
276443
Object.entries(newParams).forEach(([key, value]) => {
277444
tableUrl.searchParams.set(key, value);
278445
});
279446
if (DataTable.isDataTable('#searchJobsTable')) {
280-
// load the table with new params and update total entries
447+
// Load the table with new params and update total entries
281448
$('#searchJobsTable').DataTable().ajax.url(tableUrl.toString()).load(
282449
(data) => updateJobsCount(data)
283450
);
@@ -286,12 +453,12 @@ <h4 class="mb-2">Jobs Submitted: <span id="total-entries"></span></h4>
286453
}
287454
}
288455
function updateJobsCount(data) {
289-
// data is the response from the server after ajax call
456+
// Data is the response from the server after ajax call
290457
$('#total-entries').text(data?.data?.total_entries);
291458
}
292459
// Filters
293460
function filterJobsByColumn(columnIndex, value) {
294-
if (columnIndex == 3 || columnIndex == 4) {
461+
if (columnIndex == 4 || columnIndex == 5) {
295462
//exact match for dag_id and job_state dropdowns
296463
$('#searchJobsTable').DataTable().columns(columnIndex).search(value.trim(), true, false).draw();
297464
} else {
@@ -311,6 +478,12 @@ <h4 class="mb-2">Jobs Submitted: <span id="total-entries"></span></h4>
311478
execution_date_lte: end.toISOString(),
312479
});
313480
}
481+
function onCancelConfirmationInput(inputValue) {
482+
// Enable cancel jobs confirm button only if input is "YES"
483+
const confirmButton = $('#cancel-confirm-button');
484+
const disabled = inputValue !== 'YES';
485+
confirmButton.prop('disabled', disabled);
486+
}
314487
</script>
315488
</body>
316489
</html>

0 commit comments

Comments
 (0)