diff --git a/CHANGELOG.unreleased.md b/CHANGELOG.unreleased.md index 88d7d3aa4a7..8fb1b1cd286 100644 --- a/CHANGELOG.unreleased.md +++ b/CHANGELOG.unreleased.md @@ -19,6 +19,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released - Added a button to the search popover in the skeleton and segment tab to select all matching non-group results. [#8123](https://github.com/scalableminds/webknossos/pull/8123) - Unified wording in UI and code: “Magnification”/“mag” is now used in place of “Resolution“ most of the time, compare [https://docs.webknossos.org/webknossos/terminology.html](terminology document). [#8111](https://github.com/scalableminds/webknossos/pull/8111) - Added support for adding remote OME-Zarr NGFF version 0.5 datasets. [#8122](https://github.com/scalableminds/webknossos/pull/8122) +- Workflow reports may be deleted by superusers. [#8156](https://github.com/scalableminds/webknossos/pull/8156) ### Changed - Some mesh-related actions were disabled in proofreading-mode when using meshfiles that were created for a mapping rather than an oversegmentation. [#8091](https://github.com/scalableminds/webknossos/pull/8091) diff --git a/app/controllers/VoxelyticsController.scala b/app/controllers/VoxelyticsController.scala index 9cd27ab9230..0400c317771 100644 --- a/app/controllers/VoxelyticsController.scala +++ b/app/controllers/VoxelyticsController.scala @@ -3,6 +3,7 @@ package controllers import com.scalableminds.util.time.Instant import com.scalableminds.util.tools.{Fox, FoxImplicits} import models.organization.OrganizationDAO +import models.user.UserService import models.voxelytics._ import play.api.libs.json._ import play.api.mvc._ @@ -19,6 +20,7 @@ class VoxelyticsController @Inject()( organizationDAO: OrganizationDAO, voxelyticsDAO: VoxelyticsDAO, voxelyticsService: VoxelyticsService, + userService: UserService, lokiClient: LokiClient, wkConf: WkConf, sil: Silhouette[WkEnv])(implicit ec: ExecutionContext, bodyParsers: PlayBodyParsers) @@ -158,6 +160,17 @@ class VoxelyticsController @Inject()( } yield JsonOk(result) } + def deleteWorkflow(workflowHash: String): Action[AnyContent] = + sil.SecuredAction.async { implicit request => + for { + _ <- bool2Fox(wkConf.Features.voxelyticsEnabled) ?~> "voxelytics.disabled" + _ <- userService.assertIsSuperUser(request.identity) + _ <- voxelyticsDAO.findWorkflowByHash(workflowHash) ?~> "voxelytics.workflowNotFound" ~> NOT_FOUND + _ = logger.info(s"Deleting workflow with hash $workflowHash in organization ${request.identity._organization}") + _ <- voxelyticsDAO.deleteWorkflow(workflowHash, request.identity._organization) + } yield Ok + } + def storeWorkflowEvents(workflowHash: String, runName: String): Action[List[WorkflowEvent]] = sil.SecuredAction.async(validateJson[List[WorkflowEvent]]) { implicit request => def createWorkflowEvent(runId: ObjectId, events: List[WorkflowEvent]): Fox[Unit] = diff --git a/app/models/voxelytics/VoxelyticsDAO.scala b/app/models/voxelytics/VoxelyticsDAO.scala index 799d72391fd..beffc0b3010 100644 --- a/app/models/voxelytics/VoxelyticsDAO.scala +++ b/app/models/voxelytics/VoxelyticsDAO.scala @@ -1135,4 +1135,19 @@ class VoxelyticsDAO @Inject()(sqlClient: SqlClient)(implicit ec: ExecutionContex """.asUpdate) } yield () + def deleteWorkflow(hash: String, organizationId: String): Fox[Unit] = + for { + _ <- run(q""" + DELETE FROM webknossos.voxelytics_workflows + WHERE hash = $hash + AND _organization = $organizationId; + """.asUpdate) + _ <- run(q""" + UPDATE webknossos.jobs + SET _voxelytics_workflowHash = NULL + WHERE _voxelytics_workflowHash = $hash + AND (SELECT _organization FROM webknossos.users AS u WHERE u._id = _owner) = $organizationId; + """.asUpdate) + } yield () + } diff --git a/conf/webknossos.latest.routes b/conf/webknossos.latest.routes index 470c28e1271..8c51aadafba 100644 --- a/conf/webknossos.latest.routes +++ b/conf/webknossos.latest.routes @@ -309,6 +309,7 @@ POST /verifyEmail POST /voxelytics/workflows controllers.VoxelyticsController.storeWorkflow() GET /voxelytics/workflows controllers.VoxelyticsController.listWorkflows() GET /voxelytics/workflows/:workflowHash controllers.VoxelyticsController.getWorkflow(workflowHash: String, runId: Option[String]) +DELETE /voxelytics/workflows/:workflowHash controllers.VoxelyticsController.deleteWorkflow(workflowHash: String) POST /voxelytics/workflows/:workflowHash/events controllers.VoxelyticsController.storeWorkflowEvents(workflowHash: String, runName: String) GET /voxelytics/workflows/:workflowHash/chunkStatistics controllers.VoxelyticsController.getChunkStatistics(workflowHash: String, runId: Option[String], taskName: String) GET /voxelytics/workflows/:workflowHash/artifactChecksums controllers.VoxelyticsController.getArtifactChecksums(workflowHash: String, runId: Option[String], taskName: String, artifactName: Option[String]) diff --git a/frontend/javascripts/admin/admin_rest_api.ts b/frontend/javascripts/admin/admin_rest_api.ts index 6c3b4ef8e78..83adbbc3dae 100644 --- a/frontend/javascripts/admin/admin_rest_api.ts +++ b/frontend/javascripts/admin/admin_rest_api.ts @@ -2501,6 +2501,12 @@ export function getVoxelyticsArtifactChecksums( ); } +export function deleteWorkflow(workflowHash: string): Promise { + return Request.triggerRequest(`/api/voxelytics/workflows/${workflowHash}`, { + method: "DELETE", + }); +} + // ### Help / Feedback userEmail export function sendHelpEmail(message: string) { return Request.receiveJSON( diff --git a/frontend/javascripts/admin/voxelytics/task_list_view.tsx b/frontend/javascripts/admin/voxelytics/task_list_view.tsx index a22d909d125..feecafa1149 100644 --- a/frontend/javascripts/admin/voxelytics/task_list_view.tsx +++ b/frontend/javascripts/admin/voxelytics/task_list_view.tsx @@ -50,10 +50,12 @@ import TaskView from "./task_view"; import { formatLog } from "./log_tab"; import { addAfterPadding, addBeforePadding } from "./utils"; import { LOG_LEVELS } from "oxalis/constants"; -import { getVoxelyticsLogs } from "admin/admin_rest_api"; +import { getVoxelyticsLogs, deleteWorkflow } from "admin/admin_rest_api"; import ArtifactsDiskUsageList from "./artifacts_disk_usage_list"; import { notEmpty } from "libs/utils"; import type { ArrayElement } from "types/globals"; +import { useSelector } from "react-redux"; +import type { OxalisState } from "oxalis/store"; const { Search } = Input; @@ -272,6 +274,8 @@ export default function TaskListView({ const highlightedTask = params.highlightedTask || ""; const location = useLocation(); + const isCurrentUserSuperUser = useSelector((state: OxalisState) => state.activeUser?.isSuperUser); + const singleRunId = report.runs.length === 1 ? report.runs[0].id : runId; useEffect(() => { @@ -421,6 +425,26 @@ export default function TaskListView({ } } + async function deleteWorkflowReport() { + await modal.confirm({ + title: "Delete Workflow Report", + content: + "Are you sure you want to delete this workflow report? This can not be undone. Note that if the workflow is still running, this may cause it to fail.", + okText: "Delete", + okButtonProps: { danger: true }, + onOk: async () => { + try { + await deleteWorkflow(report.workflow.hash); + history.push("/workflows"); + message.success("Workflow report deleted."); + } catch (error) { + console.error(error); + message.error("Could not delete workflow report."); + } + }, + }); + } + const overflowMenu: MenuProps = { items: [ { key: "1", onClick: copyAllArtifactPaths, label: "Copy All Artifact Paths" }, @@ -442,6 +466,12 @@ export default function TaskListView({ { key: "5", onClick: showArtifactsDiskUsageList, label: "Show Disk Usage of Artifacts" }, ], }; + if (isCurrentUserSuperUser) + overflowMenu.items?.push({ + key: "6", + onClick: deleteWorkflowReport, + label: "Delete Workflow Report", + }); type ItemType = ArrayElement;