Allow to stop running workflow (#15270)

https://github.com/user-attachments/assets/599154e4-8743-471b-b05a-721b635bcf4e

On stoppage:
- if no running steps, mark pending as failed and end the workflow
- if running steps, set as stopping and exit. Going to the next step, as
the workflow is not running anymore, it will naturally stop
This commit is contained in:
Thomas Trompette
2025-10-23 15:01:15 +02:00
committed by GitHub
parent 1cf442966d
commit 4effa5351c
23 changed files with 532 additions and 70 deletions
@@ -1825,7 +1825,7 @@ export type Mutation = {
restorePageLayoutWidget: PageLayoutWidget;
retryJobs: RetryJobsResponse;
revokeApiKey?: Maybe<ApiKey>;
runWorkflowVersion: WorkflowRun;
runWorkflowVersion: RunWorkflowVersionOutput;
saveImapSmtpCaldavAccount: ImapSmtpCaldavConnectionSuccess;
sendInvitations: SendInvitationsOutput;
setMeteredSubscriptionPrice: BillingUpdateOutput;
@@ -1836,6 +1836,7 @@ export type Mutation = {
skipBookOnboardingStep: OnboardingStepSuccess;
skipSyncEmailOnboardingStep: OnboardingStepSuccess;
startChannelSync: ChannelSyncSuccess;
stopWorkflowRun: WorkflowRun;
submitFormStep: Scalars['Boolean'];
switchBillingPlan: BillingUpdateOutput;
switchSubscriptionInterval: BillingUpdateOutput;
@@ -2516,6 +2517,11 @@ export type MutationStartChannelSyncArgs = {
};
export type MutationStopWorkflowRunArgs = {
workflowRunId: Scalars['UUID'];
};
export type MutationSubmitFormStepArgs = {
input: SubmitFormStepInput;
};
@@ -3702,6 +3708,11 @@ export type RunWorkflowVersionInput = {
workflowVersionId: Scalars['UUID'];
};
export type RunWorkflowVersionOutput = {
__typename?: 'RunWorkflowVersionOutput';
workflowRunId: Scalars['UUID'];
};
export type SsoConnection = {
__typename?: 'SSOConnection';
id: Scalars['UUID'];
@@ -4585,9 +4596,21 @@ export enum WorkflowActionType {
export type WorkflowRun = {
__typename?: 'WorkflowRun';
workflowRunId: Scalars['UUID'];
id: Scalars['UUID'];
status: WorkflowRunStatusEnum;
};
/** Status of the workflow run */
export enum WorkflowRunStatusEnum {
COMPLETED = 'COMPLETED',
ENQUEUED = 'ENQUEUED',
FAILED = 'FAILED',
NOT_STARTED = 'NOT_STARTED',
RUNNING = 'RUNNING',
STOPPED = 'STOPPED',
STOPPING = 'STOPPING'
}
export type WorkflowStepPosition = {
__typename?: 'WorkflowStepPosition';
x: Scalars['Float'];
@@ -6152,7 +6175,14 @@ export type RunWorkflowVersionMutationVariables = Exact<{
}>;
export type RunWorkflowVersionMutation = { __typename?: 'Mutation', runWorkflowVersion: { __typename?: 'WorkflowRun', workflowRunId: string } };
export type RunWorkflowVersionMutation = { __typename?: 'Mutation', runWorkflowVersion: { __typename?: 'RunWorkflowVersionOutput', workflowRunId: string } };
export type StopWorkflowRunMutationVariables = Exact<{
workflowRunId: Scalars['UUID'];
}>;
export type StopWorkflowRunMutation = { __typename?: 'Mutation', stopWorkflowRun: { __typename: 'WorkflowRun', id: string, status: WorkflowRunStatusEnum } };
export type UpdateWorkflowRunStepMutationVariables = Exact<{
input: UpdateWorkflowRunStepInput;
@@ -13865,6 +13895,41 @@ export function useRunWorkflowVersionMutation(baseOptions?: Apollo.MutationHookO
export type RunWorkflowVersionMutationHookResult = ReturnType<typeof useRunWorkflowVersionMutation>;
export type RunWorkflowVersionMutationResult = Apollo.MutationResult<RunWorkflowVersionMutation>;
export type RunWorkflowVersionMutationOptions = Apollo.BaseMutationOptions<RunWorkflowVersionMutation, RunWorkflowVersionMutationVariables>;
export const StopWorkflowRunDocument = gql`
mutation StopWorkflowRun($workflowRunId: UUID!) {
stopWorkflowRun(workflowRunId: $workflowRunId) {
id
status
__typename
}
}
`;
export type StopWorkflowRunMutationFn = Apollo.MutationFunction<StopWorkflowRunMutation, StopWorkflowRunMutationVariables>;
/**
* __useStopWorkflowRunMutation__
*
* To run a mutation, you first call `useStopWorkflowRunMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useStopWorkflowRunMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [stopWorkflowRunMutation, { data, loading, error }] = useStopWorkflowRunMutation({
* variables: {
* workflowRunId: // value for 'workflowRunId'
* },
* });
*/
export function useStopWorkflowRunMutation(baseOptions?: Apollo.MutationHookOptions<StopWorkflowRunMutation, StopWorkflowRunMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<StopWorkflowRunMutation, StopWorkflowRunMutationVariables>(StopWorkflowRunDocument, options);
}
export type StopWorkflowRunMutationHookResult = ReturnType<typeof useStopWorkflowRunMutation>;
export type StopWorkflowRunMutationResult = Apollo.MutationResult<StopWorkflowRunMutation>;
export type StopWorkflowRunMutationOptions = Apollo.BaseMutationOptions<StopWorkflowRunMutation, StopWorkflowRunMutationVariables>;
export const UpdateWorkflowRunStepDocument = gql`
mutation UpdateWorkflowRunStep($input: UpdateWorkflowRunStepInput!) {
updateWorkflowRunStep(input: $input) {
+25 -2
View File
@@ -1780,7 +1780,7 @@ export type Mutation = {
restorePageLayoutWidget: PageLayoutWidget;
retryJobs: RetryJobsResponse;
revokeApiKey?: Maybe<ApiKey>;
runWorkflowVersion: WorkflowRun;
runWorkflowVersion: RunWorkflowVersionOutput;
saveImapSmtpCaldavAccount: ImapSmtpCaldavConnectionSuccess;
sendInvitations: SendInvitationsOutput;
setMeteredSubscriptionPrice: BillingUpdateOutput;
@@ -1791,6 +1791,7 @@ export type Mutation = {
skipBookOnboardingStep: OnboardingStepSuccess;
skipSyncEmailOnboardingStep: OnboardingStepSuccess;
startChannelSync: ChannelSyncSuccess;
stopWorkflowRun: WorkflowRun;
submitFormStep: Scalars['Boolean'];
switchBillingPlan: BillingUpdateOutput;
switchSubscriptionInterval: BillingUpdateOutput;
@@ -2447,6 +2448,11 @@ export type MutationStartChannelSyncArgs = {
};
export type MutationStopWorkflowRunArgs = {
workflowRunId: Scalars['UUID'];
};
export type MutationSubmitFormStepArgs = {
input: SubmitFormStepInput;
};
@@ -3548,6 +3554,11 @@ export type RunWorkflowVersionInput = {
workflowVersionId: Scalars['UUID'];
};
export type RunWorkflowVersionOutput = {
__typename?: 'RunWorkflowVersionOutput';
workflowRunId: Scalars['UUID'];
};
export type SsoConnection = {
__typename?: 'SSOConnection';
id: Scalars['UUID'];
@@ -4413,9 +4424,21 @@ export enum WorkflowActionType {
export type WorkflowRun = {
__typename?: 'WorkflowRun';
workflowRunId: Scalars['UUID'];
id: Scalars['UUID'];
status: WorkflowRunStatusEnum;
};
/** Status of the workflow run */
export enum WorkflowRunStatusEnum {
COMPLETED = 'COMPLETED',
ENQUEUED = 'ENQUEUED',
FAILED = 'FAILED',
NOT_STARTED = 'NOT_STARTED',
RUNNING = 'RUNNING',
STOPPED = 'STOPPED',
STOPPING = 'STOPPING'
}
export type WorkflowStepPosition = {
__typename?: 'WorkflowStepPosition';
x: Scalars['Float'];
@@ -3,21 +3,45 @@ import { NoSelectionRecordActionKeys } from '@/action-menu/actions/record-action
import { SingleRecordActionKeys } from '@/action-menu/actions/record-actions/single-record/types/SingleRecordActionsKey';
import { SeeVersionWorkflowRunSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-run-actions/components/SeeVersionWorkflowRunSingleRecordAction';
import { SeeWorkflowWorkflowRunSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-run-actions/components/SeeWorkflowWorkflowRunSingleRecordAction';
import { StopWorkflowRunSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-run-actions/components/StopWorkflowRunSingleRecordAction';
import { WorkflowRunSingleRecordActionKeys } from '@/action-menu/actions/record-actions/single-record/workflow-run-actions/types/WorkflowRunSingleRecordActionsKeys';
import { inheritActionsFromDefaultConfig } from '@/action-menu/actions/record-actions/utils/inheritActionsFromDefaultConfig';
import { ActionScope } from '@/action-menu/actions/types/ActionScope';
import { ActionType } from '@/action-menu/actions/types/ActionType';
import { ActionViewType } from '@/action-menu/actions/types/ActionViewType';
import { msg } from '@lingui/core/macro';
import { IconSettingsAutomation, IconVersions } from 'twenty-ui/display';
import {
IconPlayerStop,
IconSettingsAutomation,
IconVersions,
} from 'twenty-ui/display';
export const WORKFLOW_RUNS_ACTIONS_CONFIG = inheritActionsFromDefaultConfig({
config: {
[WorkflowRunSingleRecordActionKeys.STOP]: {
key: WorkflowRunSingleRecordActionKeys.STOP,
label: msg`Stop`,
shortLabel: msg`Stop`,
position: 0,
isPinned: true,
type: ActionType.Standard,
scope: ActionScope.RecordSelection,
Icon: IconPlayerStop,
shouldBeRegistered: ({ selectedRecord }) => {
return selectedRecord?.status === 'RUNNING';
},
availableOn: [
ActionViewType.SHOW_PAGE,
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,
ActionViewType.INDEX_PAGE_BULK_SELECTION,
],
component: <StopWorkflowRunSingleRecordAction />,
},
[WorkflowRunSingleRecordActionKeys.SEE_WORKFLOW]: {
key: WorkflowRunSingleRecordActionKeys.SEE_WORKFLOW,
label: msg`See workflow`,
shortLabel: msg`See workflow`,
position: 0,
position: 1,
isPinned: true,
type: ActionType.Standard,
scope: ActionScope.RecordSelection,
@@ -33,7 +57,7 @@ export const WORKFLOW_RUNS_ACTIONS_CONFIG = inheritActionsFromDefaultConfig({
key: WorkflowRunSingleRecordActionKeys.SEE_VERSION,
label: msg`See version`,
shortLabel: msg`See version`,
position: 1,
position: 2,
isPinned: true,
type: ActionType.Standard,
scope: ActionScope.RecordSelection,
@@ -68,63 +92,63 @@ export const WORKFLOW_RUNS_ACTIONS_CONFIG = inheritActionsFromDefaultConfig({
propertiesToOverwrite: {
[SingleRecordActionKeys.ADD_TO_FAVORITES]: {
isPinned: false,
position: 2,
position: 3,
},
[SingleRecordActionKeys.REMOVE_FROM_FAVORITES]: {
isPinned: false,
position: 3,
position: 4,
},
[SingleRecordActionKeys.EXPORT_FROM_RECORD_INDEX]: {
position: 4,
position: 5,
label: msg`Export run`,
},
[SingleRecordActionKeys.EXPORT_FROM_RECORD_SHOW]: {
position: 4,
position: 5,
label: msg`Export run`,
},
[MultipleRecordsActionKeys.EXPORT]: {
position: 5,
position: 6,
label: msg`Export runs`,
},
[NoSelectionRecordActionKeys.EXPORT_VIEW]: {
position: 6,
position: 7,
label: msg`Export view`,
},
[NoSelectionRecordActionKeys.SEE_DELETED_RECORDS]: {
position: 7,
position: 8,
label: msg`See deleted runs`,
},
[NoSelectionRecordActionKeys.HIDE_DELETED_RECORDS]: {
position: 8,
position: 9,
label: msg`Hide deleted runs`,
},
[SingleRecordActionKeys.NAVIGATE_TO_PREVIOUS_RECORD]: {
position: 9,
},
[SingleRecordActionKeys.NAVIGATE_TO_NEXT_RECORD]: {
position: 10,
},
[NoSelectionRecordActionKeys.GO_TO_WORKFLOWS]: {
[SingleRecordActionKeys.NAVIGATE_TO_NEXT_RECORD]: {
position: 11,
},
[NoSelectionRecordActionKeys.GO_TO_WORKFLOWS]: {
position: 12,
isPinned: true,
},
[NoSelectionRecordActionKeys.GO_TO_PEOPLE]: {
position: 12,
},
[NoSelectionRecordActionKeys.GO_TO_COMPANIES]: {
position: 13,
},
[NoSelectionRecordActionKeys.GO_TO_OPPORTUNITIES]: {
[NoSelectionRecordActionKeys.GO_TO_COMPANIES]: {
position: 14,
},
[NoSelectionRecordActionKeys.GO_TO_SETTINGS]: {
[NoSelectionRecordActionKeys.GO_TO_OPPORTUNITIES]: {
position: 15,
},
[NoSelectionRecordActionKeys.GO_TO_TASKS]: {
[NoSelectionRecordActionKeys.GO_TO_SETTINGS]: {
position: 16,
},
[NoSelectionRecordActionKeys.GO_TO_NOTES]: {
[NoSelectionRecordActionKeys.GO_TO_TASKS]: {
position: 17,
},
[NoSelectionRecordActionKeys.GO_TO_NOTES]: {
position: 18,
},
},
});
@@ -0,0 +1,29 @@
import { Action } from '@/action-menu/actions/components/Action';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useStopWorkflowRun } from '@/workflow/hooks/useStopWorkflowRun';
export const StopWorkflowRunSingleRecordAction = () => {
const contextStoreTargetedRecordsRule = useRecoilComponentValue(
contextStoreTargetedRecordsRuleComponentState,
);
if (
contextStoreTargetedRecordsRule.mode === 'exclusion' ||
(contextStoreTargetedRecordsRule.mode === 'selection' &&
contextStoreTargetedRecordsRule.selectedRecordIds.length === 0)
) {
throw new Error('Selected record ID is required');
}
const selectedRecordIds = contextStoreTargetedRecordsRule.selectedRecordIds;
const { stopWorkflowRun } = useStopWorkflowRun();
const handleClick = async () => {
for (const selectedRecordId of selectedRecordIds) {
await stopWorkflowRun(selectedRecordId);
}
};
return <Action onClick={handleClick} />;
};
@@ -1,4 +1,5 @@
export enum WorkflowRunSingleRecordActionKeys {
STOP = 'stop-workflow-run-single-record',
SEE_WORKFLOW = 'see-workflow-workflow-run-single-record',
SEE_VERSION = 'see-version-workflow-run-single-record',
}
@@ -0,0 +1,11 @@
import { gql } from '@apollo/client';
export const STOP_WORKFLOW_RUN = gql`
mutation StopWorkflowRun($workflowRunId: UUID!) {
stopWorkflowRun(workflowRunId: $workflowRunId) {
id
status
__typename
}
}
`;
@@ -0,0 +1,29 @@
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { useRegisterObjectOperation } from '@/object-record/hooks/useRegisterObjectOperation';
import { useStopWorkflowRunMutation } from '~/generated-metadata/graphql';
export const useStopWorkflowRun = () => {
const apolloCoreClient = useApolloCoreClient();
const [mutate] = useStopWorkflowRunMutation({
client: apolloCoreClient,
});
const { registerObjectOperation } = useRegisterObjectOperation();
const stopWorkflowRun = async (workflowRunId: string) => {
const { data } = await mutate({
variables: {
workflowRunId,
},
});
registerObjectOperation('workflowRun', {
type: 'update-one',
result: {
updatedRecord: data?.stopWorkflowRun,
updateInput: { id: workflowRunId },
},
});
};
return { stopWorkflowRun };
};
@@ -34,6 +34,20 @@ export const getWorkflowRunStatusTagProps = ({
};
}
if (workflowRunStatus === 'STOPPING') {
return {
color: 'orange',
text: 'Stopping',
};
}
if (workflowRunStatus === 'STOPPED') {
return {
color: 'gray',
text: 'Stopped',
};
}
return {
color: 'red',
text: 'Failed',