-
Notifications
You must be signed in to change notification settings - Fork 24
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
Allow for the deletion of workflow reports #8156
Changes from 3 commits
31babbe
5ad996b
fd64a57
ba82913
c26f05d
856f9c6
9f65900
0eaf018
54037c7
443b71c
114ff78
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 |
---|---|---|
|
@@ -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") | ||
_ <- voxelyticsDAO.deleteWorkflow(workflowHash) | ||
} yield Ok | ||
} | ||
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. Add validation for running workflows The PR objectives mention concerns about deleting running workflows. Consider adding a check to prevent deletion of active workflows. 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
+ workflow <- voxelyticsDAO.findWorkflowByHash(workflowHash) ?~> "voxelytics.workflowNotFound" ~> NOT_FOUND
+ isRunning <- voxelyticsDAO.isWorkflowRunning(workflowHash)
+ _ <- bool2Fox(!isRunning) ?~> "voxelytics.cannotDeleteRunningWorkflow" ~> CONFLICT
_ = logger.info(s"Deleting workflow with hash $workflowHash")
_ <- voxelyticsDAO.deleteWorkflow(workflowHash)
} 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] = | ||
|
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -1135,4 +1135,17 @@ class VoxelyticsDAO @Inject()(sqlClient: SqlClient)(implicit ec: ExecutionContex | |||||||||
""".asUpdate) | ||||||||||
} yield () | ||||||||||
|
||||||||||
def deleteWorkflow(hash: String): Fox[Unit] = | ||||||||||
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.
Suggested change
|
||||||||||
for { | ||||||||||
_ <- run(q""" | ||||||||||
DELETE FROM webknossos.voxelytics_workflows | ||||||||||
WHERE hash = $hash; | ||||||||||
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.
Suggested change
|
||||||||||
""".asUpdate) | ||||||||||
_ <- run(q""" | ||||||||||
UPDATE webknossos.jobs | ||||||||||
SET _voxelytics_workflowhash = NULL | ||||||||||
WHERE _voxelytics_workflowhash = $hash; | ||||||||||
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.
Suggested change
|
||||||||||
""".asUpdate) | ||||||||||
} yield () | ||||||||||
|
||||||||||
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. Consider wrapping the delete operations in a transaction. The workflow deletion involves multiple database operations that should be atomic to maintain data consistency. If the first operation succeeds but the second fails, it could leave the database in an inconsistent state. Consider wrapping both operations in a transaction: def deleteWorkflow(hash: String): Fox[Unit] =
for {
- _ <- run(q"""
- DELETE FROM webknossos.voxelytics_workflows
- WHERE hash = $hash;
- """.asUpdate)
- _ <- run(q"""
- UPDATE webknossos.jobs
- SET _voxelytics_workflowhash = NULL
- WHERE _voxelytics_workflowhash = $hash;
- """.asUpdate)
+ _ <- sqlClient.withTransaction { implicit client =>
+ for {
+ _ <- run(q"""
+ DELETE FROM webknossos.voxelytics_workflows
+ WHERE hash = $hash;
+ """.asUpdate)
+ _ <- run(q"""
+ UPDATE webknossos.jobs
+ SET _voxelytics_workflowhash = NULL
+ WHERE _voxelytics_workflowhash = $hash;
+ """.asUpdate)
+ } yield ()
+ }
} yield ()
|
||||||||||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -50,7 +50,7 @@ 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"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -421,6 +421,25 @@ 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?", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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."); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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. Add validation for running workflows and user permissions. The deletion functionality should include additional checks:
Consider updating the implementation: async function deleteWorkflowReport() {
+ // Check if any task is running
+ const hasRunningTasks = report.tasks.some(
+ (task) => task.state === VoxelyticsRunState.RUNNING
+ );
+
+ if (hasRunningTasks) {
+ return message.error(
+ "Cannot delete workflow report while tasks are running."
+ );
+ }
+
await modal.confirm({
title: "Delete Workflow Report",
content: "Are you sure you want to delete this workflow report?",
okText: "Delete",
okButtonProps: { danger: true },
onOk: async () => {
try {
+ // Add permission check before deletion
+ if (!user.isSuperUser) {
+ throw new Error("Insufficient permissions");
+ }
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.");
+ message.error(
+ error.message === "Insufficient permissions"
+ ? "You don't have permission to delete workflow reports."
+ : "Could not delete workflow report."
+ );
}
},
});
}
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. 🛠️ Refactor suggestion Consider adding safeguards and improving error handling. The deletion implementation could be enhanced in several ways:
Consider this improved implementation: async function deleteWorkflowReport() {
+ const isRunning = report.tasks.some(task => task.state === VoxelyticsRunState.RUNNING);
+ if (isRunning) {
+ message.error("Cannot delete a running workflow.");
+ return;
+ }
await modal.confirm({
title: "Delete Workflow Report",
content: "Are you sure you want to delete this workflow report?",
okText: "Delete",
okButtonProps: { danger: true },
onOk: async () => {
+ const hide = message.loading("Deleting workflow report...", 0);
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.");
+ message.error(`Failed to delete workflow report: ${error instanceof Error ? error.message : 'Unknown error'}`);
+ } finally {
+ hide();
}
},
});
} 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const overflowMenu: MenuProps = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
items: [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
{ key: "1", onClick: copyAllArtifactPaths, label: "Copy All Artifact Paths" }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -440,6 +459,7 @@ export default function TaskListView({ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
{ key: "5", onClick: showArtifactsDiskUsageList, label: "Show Disk Usage of Artifacts" }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
{ key: "6", onClick: deleteWorkflowReport, label: "Delete Workflow Report" }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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. Conditionally render or disable the delete option. The delete menu item should be conditionally rendered or disabled based on user permissions and workflow state. Update the menu item to include these conditions: - { key: "6", onClick: deleteWorkflowReport, label: "Delete Workflow Report" },
+ {
+ key: "6",
+ onClick: deleteWorkflowReport,
+ label: "Delete Workflow Report",
+ disabled: !user.isSuperUser || report.tasks.some(
+ (task) => task.state === VoxelyticsRunState.RUNNING
+ ),
+ tooltip: !user.isSuperUser
+ ? "Only super users can delete workflow reports"
+ : report.tasks.some((task) => task.state === VoxelyticsRunState.RUNNING)
+ ? "Cannot delete workflow report while tasks are running"
+ : undefined,
+ },
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. Add access control for the delete option. According to the PR objectives, the delete functionality should be restricted to super users. The menu item should be conditionally rendered or disabled based on user permissions. Consider this implementation: - { key: "6", onClick: deleteWorkflowReport, label: "Delete Workflow Report" },
+ {
+ key: "6",
+ onClick: deleteWorkflowReport,
+ label: "Delete Workflow Report",
+ disabled: !isSuperUser,
+ title: !isSuperUser ? "Only super users can delete workflow reports" : undefined
+ }, You'll need to add the
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
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.