From 96ea6c345827d9157ee6feda8daa95dd4c2c53a9 Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Thu, 9 Sep 2021 23:00:51 +0200 Subject: [PATCH] Implement Cancel workflow on dashboard --- .../elsa-workflow-instance-list-screen.tsx | 207 ++++++++++++++---- .../localizations.ts | 22 +- .../src/services/elsa-client.ts | 33 ++- .../Endpoints/WorkflowInstances/BulkCancel.cs | 44 ++++ .../Endpoints/WorkflowInstances/BulkDelete.cs | 5 +- .../Endpoints/WorkflowInstances/Models.cs | 4 + 6 files changed, 251 insertions(+), 64 deletions(-) create mode 100644 src/server/Elsa.Server.Api/Endpoints/WorkflowInstances/BulkCancel.cs diff --git a/src/designer/elsa-workflows-studio/src/components/screens/workflow-instance-list/elsa-workflow-instance-list-screen/elsa-workflow-instance-list-screen.tsx b/src/designer/elsa-workflows-studio/src/components/screens/workflow-instance-list/elsa-workflow-instance-list-screen/elsa-workflow-instance-list-screen.tsx index 1db462fd4..24fb2ad2a 100644 --- a/src/designer/elsa-workflows-studio/src/components/screens/workflow-instance-list/elsa-workflow-instance-list-screen/elsa-workflow-instance-list-screen.tsx +++ b/src/designer/elsa-workflows-studio/src/components/screens/workflow-instance-list/elsa-workflow-instance-list-screen/elsa-workflow-instance-list-screen.tsx @@ -3,7 +3,15 @@ import {injectHistory, LocationSegments, RouterHistory} from "@stencil/router"; import * as collection from 'lodash/collection'; import * as array from 'lodash/array'; import {confirmDialogService, eventBus, createElsaClient} from "../../../../services"; -import {EventTypes, OrderBy, PagedList, VersionOptions, WorkflowBlueprintSummary, WorkflowInstanceSummary, WorkflowStatus} from "../../../../models"; +import { + EventTypes, + OrderBy, + PagedList, + VersionOptions, + WorkflowBlueprintSummary, + WorkflowInstanceSummary, + WorkflowStatus +} from "../../../../models"; import {DropdownButtonItem, DropdownButtonOrigin} from "../../../controls/elsa-dropdown-button/models"; import {Map, parseQuery} from '../../../../utils/utils'; import moment from "moment"; @@ -20,7 +28,7 @@ import Tunnel from "../../../../data/dashboard"; export class ElsaWorkflowInstanceListScreen { @Prop() history?: RouterHistory; @Prop() serverUrl: string; - @Prop()basePath: string; + @Prop() basePath: string; @Prop() workflowId?: string; @Prop() workflowStatus?: WorkflowStatus; @Prop() orderBy?: OrderBy = OrderBy.Started; @@ -53,6 +61,9 @@ export class ElsaWorkflowInstanceListScreen { const t = this.t; let bulkActions = [{ + text: t('BulkActions.Actions.Cancel'), + name: 'Cancel', + }, { text: t('BulkActions.Actions.Delete'), name: 'Delete', }]; @@ -205,6 +216,18 @@ export class ElsaWorkflowInstanceListScreen { this.updateSelectAllChecked(); } + async onCancelClick(e: Event, workflowInstance: WorkflowInstanceSummary) { + const t = this.t; + const result = await confirmDialogService.show(t('CancelDialog.Title'), t('CancelDialog.Message')); + + if (!result) + return; + + const elsaClient = this.createClient(); + await elsaClient.workflowInstancesApi.cancel(workflowInstance.id); + await this.loadWorkflowInstances(); + } + async onDeleteClick(e: Event, workflowInstance: WorkflowInstanceSummary) { const t = this.t; const result = await confirmDialogService.show(t('DeleteDialog.Title'), t('DeleteDialog.Message')); @@ -217,6 +240,20 @@ export class ElsaWorkflowInstanceListScreen { await this.loadWorkflowInstances(); } + async onBulkCancel() { + const t = this.t; + const result = await confirmDialogService.show(t('BulkCancelDialog.Title'), t('BulkCancelDialog.Message')); + + if (!result) + return; + + const elsaClient = this.createClient(); + await elsaClient.workflowInstancesApi.bulkCancel({workflowInstanceIds: this.selectedWorkflowInstanceIds}); + this.selectedWorkflowInstanceIds = []; + await this.loadWorkflowInstances(); + this.currentPage = 0; + } + async onBulkDelete() { const t = this.t; const result = await confirmDialogService.show(t('BulkDeleteDialog.Title'), t('BulkDeleteDialog.Message')); @@ -235,6 +272,9 @@ export class ElsaWorkflowInstanceListScreen { const action = e.detail; switch (action.name) { + case 'Cancel': + await this.onBulkCancel(); + break; case 'Delete': await this.onBulkDelete(); break; @@ -269,16 +309,29 @@ export class ElsaWorkflowInstanceListScreen { const renderViewIcon = function () { return ( - + ); }; + const renderCancelIcon = function () { + return ( + + + + + ); + }; + const renderDeleteIcon = function () { return ( - + @@ -291,15 +344,18 @@ export class ElsaWorkflowInstanceListScreen { return (
-
+
this.onSearch(e)}>
-
+
- +
- - this.onSelectAllCheckChange(e)} class="focus:elsa-ring-blue-500 elsa-h-4 elsa-w-4 elsa-text-blue-600 elsa-border-gray-300 elsa-rounded"/> + + this.onSelectAllCheckChange(e)} + class="focus:elsa-ring-blue-500 elsa-h-4 elsa-w-4 elsa-text-blue-600 elsa-border-gray-300 elsa-rounded"/> - + {t('Table.Id')} - + {t('Table.CorrelationId')} - + {t('Table.Workflow')} - + {t('Table.Version')} - + {t('Table.InstanceName')} - + {t('Table.Status')} - + {t('Table.Created')} - + {t('Table.Finished')} - + {t('Table.LastExecuted')} - + {t('Table.Faulted')} - + {workflowInstances.map(workflowInstance => { - const workflowBlueprint = workflowBlueprints.find(x => x.id == workflowInstance.definitionId && x.version == workflowInstance.version) ?? {name: 'Not Found', displayName: '(Workflow definition not found)'}; + const workflowBlueprint = workflowBlueprints.find(x => x.id == workflowInstance.definitionId && x.version == workflowInstance.version) ?? { + name: 'Not Found', + displayName: '(Workflow definition not found)' + }; const displayName = workflowBlueprint.displayName || workflowBlueprint.name || 'Untitled'; const statusColor = this.getStatusColor(workflowInstance.workflowStatus); const viewUrl = `${basePath}/workflow-instances/${workflowInstance.id}`; @@ -379,56 +452,81 @@ export class ElsaWorkflowInstanceListScreen { const faultedAt = !!workflowInstance.faultedAt ? moment(workflowInstance.faultedAt) : null; return - - this.onWorkflowInstanceCheckChange(e, workflowInstance)} + + this.onWorkflowInstanceCheckChange(e, workflowInstance)} class="focus:elsa-ring-blue-500 elsa-h-4 elsa-w-4 elsa-text-blue-600 elsa-border-gray-300 elsa-rounded"/> - - {workflowInstance.id} + + {workflowInstance.id} - + {!!workflowInstance.correlationId ? workflowInstance.correlationId : ''} - - + + {displayName} - + {workflowInstance.version} - - {instanceName} + + {instanceName} - +
{workflowInstance.workflowStatus}
- + {createdAt.format('DD-MM-YYYY HH:mm:ss')} - + {!!finishedAt ? finishedAt.format('DD-MM-YYYY HH:mm:ss') : '-'} - + {!!lastExecutedAt ? lastExecutedAt.format('DD-MM-YYYY HH:mm:ss') : '-'} - + {!!faultedAt ? faultedAt.format('DD-MM-YYYY HH:mm:ss') : '-'} this.onDeleteClick(e, workflowInstance), icon: renderDeleteIcon()} + { + text: t('Table.ContextMenu.Cancel'), + clickHandler: e => this.onCancelClick(e, workflowInstance), + icon: renderCancelIcon() + }, + { + text: t('Table.ContextMenu.Delete'), + clickHandler: e => this.onDeleteClick(e, workflowInstance), + icon: renderDeleteIcon() + } ]}/> })} - +
@@ -436,14 +534,17 @@ export class ElsaWorkflowInstanceListScreen { } renderBulkActions() { - const bulkActionIcon = + const bulkActionIcon = ; const t = this.t; const actions = this.bulkActions; - return this.onBulkActionSelected(e)}/> + return this.onBulkActionSelected(e)}/> } renderWorkflowFilter() { @@ -474,7 +575,8 @@ export class ElsaWorkflowInstanceListScreen { items = [allItem, ...items]; const renderIcon = function () { - return + return @@ -483,7 +585,9 @@ export class ElsaWorkflowInstanceListScreen { ; }; - return this.handleWorkflowIdChanged(e.detail.value)}/> + return this.handleWorkflowIdChanged(e.detail.value)}/> } renderStatusFilter() { @@ -504,13 +608,16 @@ export class ElsaWorkflowInstanceListScreen { }); const renderIcon = function () { - return + return }; - return this.handleWorkflowStatusChanged(e.detail.value)}/> + return this.handleWorkflowStatusChanged(e.detail.value)}/> } renderOrderByFilter() { @@ -530,14 +637,18 @@ export class ElsaWorkflowInstanceListScreen { }); const renderIcon = function () { - return - + return + }; - return this.handleOrderByChanged(e.detail.value)}/> + return this.handleOrderByChanged(e.detail.value)}/> } } Tunnel.injectProps(ElsaWorkflowInstanceListScreen, ['serverUrl', 'culture', 'basePath']); -injectHistory(ElsaWorkflowInstanceListScreen); \ No newline at end of file +injectHistory(ElsaWorkflowInstanceListScreen); diff --git a/src/designer/elsa-workflows-studio/src/components/screens/workflow-instance-list/elsa-workflow-instance-list-screen/localizations.ts b/src/designer/elsa-workflows-studio/src/components/screens/workflow-instance-list/elsa-workflow-instance-list-screen/localizations.ts index 5f97fa97d..d88d0ca3a 100644 --- a/src/designer/elsa-workflows-studio/src/components/screens/workflow-instance-list/elsa-workflow-instance-list-screen/localizations.ts +++ b/src/designer/elsa-workflows-studio/src/components/screens/workflow-instance-list/elsa-workflow-instance-list-screen/localizations.ts @@ -15,16 +15,25 @@ 'Finished': 'Finished', 'LastExecuted': 'Last Executed', 'Faulted': 'Faulted', - 'ContextMenu':{ + 'ContextMenu': { 'View': 'View', + 'Cancel': 'Cancel', 'Delete': 'Delete' } }, - 'DeleteDialog':{ + 'CancelDialog': { + 'Title': 'Cancel Workflow Instance', + 'Message': 'Are you sure you wish to cancel this workflow instance?' + }, + 'DeleteDialog': { 'Title': 'Delete Workflow Instance', 'Message': 'Are you sure you wish to permanently delete this workflow instance?' }, - 'BulkDeleteDialog':{ + 'BulkCancelDialog': { + 'Title': 'Cancel Selected Workflow Instances', + 'Message': 'Are you sure you wish to cancel all selected workflow instances?' + }, + 'BulkDeleteDialog': { 'Title': 'Delete Selected Workflow Instances', 'Message': 'Are you sure you wish to permanently delete all selected workflow instances?' }, @@ -34,6 +43,7 @@ 'BulkActions': { 'Title': 'Bulk Actions', 'Actions': { + 'Cancel': 'Cancel', 'Delete': 'Delete' } }, @@ -63,8 +73,6 @@ } }, 'nl-NL': { - 'default': { - - } + 'default': {} } -}; \ No newline at end of file +}; diff --git a/src/designer/elsa-workflows-studio/src/services/elsa-client.ts b/src/designer/elsa-workflows-studio/src/services/elsa-client.ts index 97c91e66f..8f24910a8 100644 --- a/src/designer/elsa-workflows-studio/src/services/elsa-client.ts +++ b/src/designer/elsa-workflows-studio/src/services/elsa-client.ts @@ -29,9 +29,8 @@ import { let _httpClient: AxiosInstance = null; let _elsaClient: ElsaClient = null; -export const createHttpClient = function(baseAddress: string) : AxiosInstance -{ - if(!!_httpClient) +export const createHttpClient = function (baseAddress: string): AxiosInstance { + if (!!_httpClient) return _httpClient; const config: AxiosRequestConfig = { @@ -44,7 +43,7 @@ export const createHttpClient = function(baseAddress: string) : AxiosInstance const service = new Service(httpClient); eventBus.emit(EventTypes.HttpClientCreated, this, {service, httpClient}); - + return _httpClient = httpClient; } @@ -159,9 +158,16 @@ export const createElsaClient = function (serverUrl: string): ElsaClient { const response = await httpClient.get(`v1/workflow-instances/${id}`); return response.data; }, + cancel: async id => { + await httpClient.post(`v1/workflow-instances/${id}/cancel`); + }, delete: async id => { await httpClient.delete(`v1/workflow-instances/${id}`); }, + bulkCancel: async request => { + const response = await httpClient.post(`v1/workflow-instances/bulk/cancel`, request); + return response.data; + }, bulkDelete: async request => { const response = await httpClient.delete(`v1/workflow-instances/bulk`, { data: request @@ -195,7 +201,10 @@ export const createElsaClient = function (serverUrl: string): ElsaClient { designerApi: { runtimeSelectItemsApi: { get: async (providerTypeName: string, context?: any): Promise> => { - const response = await httpClient.post('v1/designer/runtime-select-list-items', {providerTypeName: providerTypeName, context: context}); + const response = await httpClient.post('v1/designer/runtime-select-list-items', { + providerTypeName: providerTypeName, + context: context + }); return response.data; } } @@ -270,8 +279,12 @@ export interface WorkflowInstancesApi { get(id: string): Promise; + cancel(id: string): Promise; + delete(id: string): Promise; + bulkCancel(request: BulkCancelWorkflowsRequest): Promise; + bulkDelete(request: BulkDeleteWorkflowsRequest): Promise; } @@ -281,6 +294,14 @@ export interface WorkflowExecutionLogApi { } +export interface BulkCancelWorkflowsRequest { + workflowInstanceIds: Array; +} + +export interface BulkCancelWorkflowsResponse { + cancelledWorkflowCount: number; +} + export interface BulkDeleteWorkflowsRequest { workflowInstanceIds: Array; } @@ -351,4 +372,4 @@ interface ActivityEventCount { interface ActivityFault { message: string; -} \ No newline at end of file +} diff --git a/src/server/Elsa.Server.Api/Endpoints/WorkflowInstances/BulkCancel.cs b/src/server/Elsa.Server.Api/Endpoints/WorkflowInstances/BulkCancel.cs new file mode 100644 index 000000000..d4e257ed9 --- /dev/null +++ b/src/server/Elsa.Server.Api/Endpoints/WorkflowInstances/BulkCancel.cs @@ -0,0 +1,44 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Elsa.Services; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; + +namespace Elsa.Server.Api.Endpoints.WorkflowInstances +{ + [ApiController] + [ApiVersion("1")] + [Route("v{apiVersion:apiVersion}/workflow-instances/bulk/cancel")] + [Produces("application/json")] + public class BulkCancel : Controller + { + private readonly IWorkflowInstanceCanceller _workflowInstanceCanceller; + + public BulkCancel(IWorkflowInstanceCanceller workflowInstanceCanceller) + { + _workflowInstanceCanceller = workflowInstanceCanceller; + } + + [HttpPost] + [ProducesResponseType(StatusCodes.Status200OK)] + [SwaggerOperation( + Summary = "Cancels a set of workflow instances.", + Description = "Cancels a set of workflow instances.", + OperationId = "WorkflowInstances.BulkCancel", + Tags = new[] { "WorkflowInstances" }) + ] + public async Task Handle(BulkCancelWorkflowsRequest request, CancellationToken cancellationToken = default) + { + var tasks = request.WorkflowInstanceIds.Select(x => _workflowInstanceCanceller.CancelAsync(x, cancellationToken)); + var results = await Task.WhenAll(tasks); + var count = results.Where(x => x.Status == CancelWorkflowInstanceResultStatus.Ok); + + return Ok(new + { + CancelledWorkflowCount = count + }); + } + } +} \ No newline at end of file diff --git a/src/server/Elsa.Server.Api/Endpoints/WorkflowInstances/BulkDelete.cs b/src/server/Elsa.Server.Api/Endpoints/WorkflowInstances/BulkDelete.cs index edd20c0b8..7dd2923ed 100644 --- a/src/server/Elsa.Server.Api/Endpoints/WorkflowInstances/BulkDelete.cs +++ b/src/server/Elsa.Server.Api/Endpoints/WorkflowInstances/BulkDelete.cs @@ -23,10 +23,9 @@ namespace Elsa.Server.Api.Endpoints.WorkflowInstances [HttpDelete] [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] [SwaggerOperation( - Summary = "Deletes a workflow instance.", - Description = "Deletes a workflow instance.", + Summary = "Deletes a set of workflow instances.", + Description = "Deletes a set of workflow instances.", OperationId = "WorkflowInstances.BulkDelete", Tags = new[] { "WorkflowInstances" }) ] diff --git a/src/server/Elsa.Server.Api/Endpoints/WorkflowInstances/Models.cs b/src/server/Elsa.Server.Api/Endpoints/WorkflowInstances/Models.cs index f2a45a3ed..7b35b4764 100644 --- a/src/server/Elsa.Server.Api/Endpoints/WorkflowInstances/Models.cs +++ b/src/server/Elsa.Server.Api/Endpoints/WorkflowInstances/Models.cs @@ -34,6 +34,10 @@ namespace Elsa.Server.Api.Endpoints.WorkflowInstances { } + public record BulkCancelWorkflowsRequest(ICollection WorkflowInstanceIds) + { + } + public record BulkDeleteWorkflowsRequest(ICollection WorkflowInstanceIds) { }