@@ -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