From 6a3a605b1191ec900d1a9f69f040ac35a01f26af Mon Sep 17 00:00:00 2001 From: Oli Jones Date: Tue, 6 Oct 2026 13:53:46 +0000 Subject: [PATCH] feat(workflows user interface): add button to stop workflows Create stop workflows button Only show stop button if workflow is stoppable Add error messages when unsucessful Disable button after its been clicked fix(worfklows user interface): clear error on successful cancellation fix(workflows user interface): set loading on request --- .../lib/components/BaseWorkflowRelay.tsx | 10 +++ .../lib/mutation-components/StopWorkflow.tsx | 71 +++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 frontend/relay-workflows-lib/lib/mutation-components/StopWorkflow.tsx diff --git a/frontend/relay-workflows-lib/lib/components/BaseWorkflowRelay.tsx b/frontend/relay-workflows-lib/lib/components/BaseWorkflowRelay.tsx index 3ba378360..eb8fb9702 100644 --- a/frontend/relay-workflows-lib/lib/components/BaseWorkflowRelay.tsx +++ b/frontend/relay-workflows-lib/lib/components/BaseWorkflowRelay.tsx @@ -10,6 +10,7 @@ import { ResizableBox } from "react-resizable"; import { Box } from "@mui/material"; import { WorkflowAccordion, type WorkflowStatus } from "workflows-lib"; import RetriggerWorkflow from "../query-components/RetriggerWorkflow"; +import StopWorkflow from "../mutation-components/StopWorkflow"; import { useSelectedTaskIds } from "../utils/workflowRelayUtils"; import { graphql } from "relay-runtime"; import { useFragment } from "react-relay"; @@ -80,6 +81,14 @@ export default function BaseWorkflowRelay({ ? (data.status.startTime as string | undefined) : undefined; + const stoppable = + data.status?.__typename == undefined || + ![ + "WorkflowSucceededStatus", + "WorkflowErroredStatus", + "WorkflowFailedStatus", + ].includes(data.status.__typename); + const [selectedTaskIds, setSelectedTaskIds] = useSelectedTaskIds() as [ string[], Dispatch>, @@ -189,6 +198,7 @@ export default function BaseWorkflowRelay({ }} /> + {stoppable && } ); diff --git a/frontend/relay-workflows-lib/lib/mutation-components/StopWorkflow.tsx b/frontend/relay-workflows-lib/lib/mutation-components/StopWorkflow.tsx new file mode 100644 index 000000000..3b39d5c42 --- /dev/null +++ b/frontend/relay-workflows-lib/lib/mutation-components/StopWorkflow.tsx @@ -0,0 +1,71 @@ +import React, { useState, useEffect } from "react"; +import Tooltip from "@mui/material/Tooltip"; +import Cancel from "@mui/icons-material/Cancel"; +import { graphql } from "relay-runtime"; +import { useMutation } from "react-relay"; +import { IconButton, Box } from "@mui/material"; +import { StopWorkflowMutation as StopWorkflowMutationType } from "./__generated__/StopWorkflowMutation.graphql"; + +const stopWorkflowMutation = graphql` + mutation StopWorkflowMutation($id: ID!) { + stopWorkflow(id: $id) { + id + } + } +`; + +interface StopWorkflowProps { + workflowId: string; +} + +const StopWorkflow: React.FC = ({ workflowId }) => { + const [loading, setLoading] = useState(false); + const buttonCooldown = 5000; + useEffect(() => { + if (loading) { + const timeout = setTimeout(() => { + setLoading(false); + }, buttonCooldown); + return () => { + clearTimeout(timeout); + }; + } + }, [loading]); + const [errorMessage, setErrorMessage] = useState(""); + + const [commitMutation] = + useMutation(stopWorkflowMutation); + + const stopWorkflow = () => { + setLoading(true); + commitMutation({ + variables: { id: workflowId }, + onCompleted: () => { + // Puts button on a cooldown so its not spammed + setErrorMessage(""); + }, + onError: (err) => { + setLoading(false); + setErrorMessage(err.message); + }, + }); + }; + + return ( + + + { + stopWorkflow(); + }} + color={errorMessage === "" ? "default" : "error"} + loading={loading} + > + + + + + ); +}; + +export default StopWorkflow;