diff --git a/api/object_specifications.py b/api/object_specifications.py index d8fe8600d..e60e2083e 100644 --- a/api/object_specifications.py +++ b/api/object_specifications.py @@ -11,6 +11,14 @@ class JobChange(BaseModel): model_config = ConfigDict(extra='forbid') +### Watchlist + +class WatchlistChange(BaseModel): + watchlist_id: int + action: Literal['delete'] + + model_config = ConfigDict(extra='forbid') + ### Software Add class Software(BaseModel): diff --git a/api/scenario_runner.py b/api/scenario_runner.py index d2abf3c3b..2b14c1ec0 100644 --- a/api/scenario_runner.py +++ b/api/scenario_runner.py @@ -5,13 +5,13 @@ from datetime import date, datetime, timedelta import pprint -from fastapi import APIRouter, Response, Depends +from fastapi import APIRouter, Response, Depends, HTTPException from fastapi.responses import ORJSONResponse from fastapi.exceptions import RequestValidationError import anybadge -from api.object_specifications import Software, JobChange +from api.object_specifications import Software, JobChange, WatchlistChange from api.api_helpers import (ORJSONResponseObjKeep, add_phase_stats_statistics, determine_comparison_case,get_comparison_details, get_phase_stats, get_phase_stats_object, check_run_failed, @@ -147,6 +147,32 @@ async def update_job( error_helpers.log_error('Job update did return unexpected result', params=params, status_message=status_message) raise RuntimeError('Could not update job due to database error') +# A route for deleting watchlist entries +@router.put('/v1/watchlist') +async def update_watchlist( + change: WatchlistChange, + user: User = Depends(authenticate), # consistent with jobs +): + if change.action != 'delete': + raise RequestValidationError(f"Unsupported action: {change.action}") + + query = """ + DELETE FROM watchlist + WHERE id = %s + AND (TRUE = %s OR user_id = %s) + RETURNING id + """ + params = (change.watchlist_id, user.is_super_user(), user._id) + deleted = DB().fetch_one(query, params=params, fetch_mode="dict") + + if not deleted: + raise HTTPException( + status_code=404, + detail="Watchlist entry not found or not owned by user", + ) + + return ORJSONResponse({'success': True, 'deleted_id': deleted['id']}) + # A route to return all of the available entries in our catalog. @router.get('/v1/notes/{run_id}') async def get_notes(run_id, user: User = Depends(authenticate)): diff --git a/frontend/js/watchlist.js b/frontend/js/watchlist.js index 51f6d85dc..6f44cb84c 100644 --- a/frontend/js/watchlist.js +++ b/frontend/js/watchlist.js @@ -19,6 +19,10 @@ $(document).ready(function () { chart_node.classList.add('ui') chart_node.classList.add("card"); + //Giving each card a data-id + chart_node.setAttribute('data-id', id); + + const url_link = `${replaceRepoIcon(repo_url)} ${createExternalIconLink(repo_url)}`; let chart_node_html = `
@@ -60,14 +64,39 @@ $(document).ready(function () {
Show All Measurements - ` + +
+ `; + + chart_node.innerHTML = chart_node_html; document.querySelector('#scenario-runner-watchlist').appendChild(chart_node) }); document.querySelectorAll(".copy-badge").forEach(el => { el.addEventListener('click', copyToClipboard) - }) + }); + + //Event listener for delete buttons + $(document).on("click", ".delete-watchlist", async function () { + const id = $(this).data("id"); + if (!confirm("Are you sure you want to delete this watchlist entry?")) { + return; + } + + try { + await makeAPICall('/v1/watchlist', 'PUT', { + action: "delete", + watchlist_id: id + }); + showNotification("Watchlist entry deleted successfully!"); + $(this).closest(".card").remove(); + } catch (err) { + showNotification("Error deleting watchlist entry", err); + } + }); })(); });