Incremental work on workflow instances list screen

This commit is contained in:
Sipke Schoorstra 2021-04-08 21:19:56 +02:00
parent f329db3b25
commit 22ce7e2e81
6 changed files with 504 additions and 332 deletions

View file

@ -67,7 +67,13 @@ export class ElsaContextMenu {
const originClass = this.getOriginClass();
return <div ref={el => this.contextMenu = el} class={`hidden ${originClass} z-10 absolute mt-2 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5`}>
return <div ref={el => this.contextMenu = el}
data-transition-enter="transition ease-out duration-100"
data-transition-enter-start="transform opacity-0 scale-95"
data-transition-leave="transition ease-in duration-75"
data-transition-leave-start="transform opacity-100 scale-100"
data-transition-leave-end="transform opacity-0 scale-95"
class={`hidden ${originClass} z-10 absolute mt-2 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5`}>
<div class="py-1" role="menu" aria-orientation="vertical">
{this.renderItems()}
</div>
@ -79,7 +85,7 @@ export class ElsaContextMenu {
const selectedCssClass = item.isSelected ? "bg-blue-600 hover:bg-blue-700 text-white" : "hover:bg-gray-100 text-gray-700 hover:text-gray-900";
return !!item.url
? <stencil-route-link onClick={e => this.closeContextMenu()} url={item.url} class={`block px-4 py-2 text-sm ${selectedCssClass}`} role="menuitem">{item.text}</stencil-route-link>
? <stencil-route-link onClick={e => this.closeContextMenu()} url={item.url} anchorClass={`block px-4 py-2 text-sm ${selectedCssClass} cursor-pointer`} role="menuitem">{item.text}</stencil-route-link>
: <a href="#" onClick={e => this.onItemClick(e, item)} class={`block px-4 py-2 text-sm ${selectedCssClass}`} role="menuitem">{item.text}</a>;
})
}

View file

@ -5,7 +5,8 @@
}
export interface DropdownButtonItem {
name: string;
name?: string;
value?: any;
text: string;
url?: string;
isSelected?: boolean;

View file

@ -1,11 +1,12 @@
import {Component, h, Prop, State} from '@stencil/core';
import {RouterHistory} from "@stencil/router";
import {LocationSegments, RouterHistory} from "@stencil/router";
import * as collection from 'lodash/collection';
import * as array from 'lodash/array';
import {createElsaClient} from "../../../../services/elsa-client";
import {OrderBy, PagedList, VersionOptions, WorkflowBlueprintSummary, WorkflowDefinitionSummary, WorkflowStatus} from "../../../../models";
import {OrderBy, PagedList, VersionOptions, WorkflowBlueprintSummary, WorkflowInstanceSummary, WorkflowStatus} from "../../../../models";
import {DropdownButtonItem, DropdownButtonOrigin} from "../../../controls/elsa-dropdown-button/models";
import {Map} from '../../../../utils/utils';
import {Map, parseQuery} from '../../../../utils/utils';
import {parseQueryString} from "@stencil/router/dist/types/utils/path-utils";
@Component({
tag: 'elsa-workflow-instances-list-screen',
@ -15,27 +16,53 @@ import {Map} from '../../../../utils/utils';
export class ElsaWorkflowInstancesListScreen {
@Prop() history?: RouterHistory;
@Prop() serverUrl: string;
@State() workflowDefinitions: PagedList<WorkflowDefinitionSummary> = {items: [], page: 1, pageSize: 50, totalCount: 0};
@State() workflowBlueprints: Array<WorkflowBlueprintSummary> = [];
@State() workflowInstances: PagedList<WorkflowInstanceSummary> = {items: [], page: 1, pageSize: 50, totalCount: 0};
@State() selectedWorkflowId?: string;
@State() selectedWorkflowStatus?: WorkflowStatus;
@State() selectedOrderBy?: OrderBy;
@State() selectedWorkflowInstanceIds: Array<string> = [];
confirmDialog: HTMLElsaConfirmDialogElement;
async componentWillLoad() {
this.history.listen(e => this.routeChanged(e));
this.applyQueryString(this.history.location.search);
await this.loadWorkflowBlueprints();
await this.loadWorkflowInstances();
}
async routeChanged(e: LocationSegments) {
if(!e.pathname.toLowerCase().indexOf('workflow-instances'))
return;
this.applyQueryString(e.search);
await this.loadWorkflowInstances();
}
applyQueryString(queryString?: string){
const query = parseQuery(queryString);
this.selectedWorkflowId = !!query.workflow ? query.workflow : null;
this.selectedWorkflowStatus = !!query.status ? this.parseStatusText(query.status) : null;
}
async loadWorkflowBlueprints() {
const elsaClient = this.createClient();
const page = 0;
const pageSize = 50;
const versionOptions: VersionOptions = {isLatest: true};
const workflowBlueprintPagedList = await elsaClient.workflowRegistryApi.list(page, pageSize, versionOptions);
const workflowBlueprintPagedList = await elsaClient.workflowRegistryApi.list(null, null, versionOptions);
this.workflowBlueprints = workflowBlueprintPagedList.items;
}
async loadWorkflowInstances() {
const elsaClient = this.createClient();
const page = 1;
const pageSize = 25;
this.workflowInstances = await elsaClient.workflowInstancesApi.list(page, pageSize, this.selectedWorkflowId, this.selectedWorkflowStatus, this.selectedOrderBy);
}
createClient() {
return createElsaClient(this.serverUrl);
}
@ -46,7 +73,7 @@ export class ElsaWorkflowInstancesListScreen {
}
buildFilterUrl(workflowId?: string, workflowStatus?: WorkflowStatus, orderBy?: OrderBy) {
const workflowStatusText = !!workflowStatus ? workflowStatus.toString() : null;
const workflowStatusText = workflowStatus != null ? this.getStatusText(workflowStatus) : null;
const orderByText = !!orderBy ? orderBy.toString() : null;
const filters: Map<string> = {};
@ -64,14 +91,69 @@ export class ElsaWorkflowInstancesListScreen {
return `/workflow-instances?${queryString}`;
}
render() {
const workflowDefinitions = this.workflowDefinitions.items;
const groupings = collection.groupBy(workflowDefinitions, 'definitionId');
getStatusColor(status: WorkflowStatus) {
switch (status) {
default:
case WorkflowStatus.Idle:
return "gray";
case WorkflowStatus.Running:
return "pink";
case WorkflowStatus.Suspended:
return "blue";
case WorkflowStatus.Finished:
return "green";
case WorkflowStatus.Faulted:
return "red";
case WorkflowStatus.Cancelled:
return "yellow";
}
}
getStatusText(status: WorkflowStatus){
switch (status) {
default:
case WorkflowStatus.Idle:
return "Idle";
case WorkflowStatus.Running:
return "Running";
case WorkflowStatus.Suspended:
return "Suspended";
case WorkflowStatus.Finished:
return "Finished";
case WorkflowStatus.Faulted:
return "Faulted";
case WorkflowStatus.Cancelled:
return "Cancelled";
}
}
parseStatusText(status?: string): WorkflowStatus {
if(!status)
return null;
switch (status.toLowerCase()) {
default:
case 'idle':
return WorkflowStatus.Idle;
case 'running':
return WorkflowStatus.Running;
case 'suspended':
return WorkflowStatus.Suspended;
case 'finished':
return WorkflowStatus.Finished;
case 'faulted':
return WorkflowStatus.Faulted;
case 'cancelled':
return WorkflowStatus.Cancelled;
}
}
render() {
const workflowInstances = this.workflowInstances.items;
const workflowBlueprints = this.workflowBlueprints;
return (
<div>
<div class="p-8 flex content-end justify-right bg-white space-x-4">
<div class="flex-shrink-0">
{this.renderBulkActions()}
@ -80,95 +162,105 @@ export class ElsaWorkflowInstancesListScreen {
&nbsp;
</div>
{this.renderWorkflowFilter()}
<elsa-dropdown-button text="@SelectedWorkflowStatusText" items={[]} icon={null} origin={DropdownButtonOrigin.TopRight}/>
{this.renderStatusFilter()}
<elsa-dropdown-button text="@SelectedOrderByText" items={[]} icon={null} origin={DropdownButtonOrigin.TopRight}/>
</div>
<div class="mt-8 sm:block">
<div class="align-middle inline-block min-w-full border-b border-gray-200">
<table class="min-w-full">
<thead>
<tr class="border-t border-gray-200">
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
<input type="checkbox" value="true" checked={false} class="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded"/>
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
<span class="lg:pl-2">ID</span>
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
Workflow
</th>
<th class="hidden md:table-cell px-6 py-3 border-b border-gray-200 bg-gray-50 text-right text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
Version
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
Instance Name
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
Status
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
Created
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
Finished
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
Last Executed
</th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
Faulted
</th>
<th class="pr-6 py-3 border-b border-gray-200 bg-gray-50 text-right text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider"/>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-100">
{workflowInstances.map(workflowInstance => {
const workflowBlueprint = workflowBlueprints.find(x => x.id == workflowInstance.definitionId && x.version == workflowInstance.version) ?? {displayName: '(Workflow definition not found)'};
const displayName = workflowBlueprint.displayName;
const statusColor = this.getStatusColor(workflowInstance.workflowStatus);
const viewUrl = `/workflow-instances/${workflowInstance.id}/viewer`;
const instanceName = !workflowInstance.name ? '' : workflowInstance.name;
const isSelected = this.selectedWorkflowInstanceIds.findIndex(x => x === workflowInstance.id) >= 0;
//var displayContext = WorkflowInstanceDisplayContexts[workflowInstance];
<div class="align-middle inline-block min-w-full border-b border-gray-200">
<table class="min-w-full">
<thead>
<tr class="border-t border-gray-200">
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider"><span class="lg:pl-2">Name</span></th>
<th class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
Instances
</th>
<th class="hidden md:table-cell px-6 py-3 border-b border-gray-200 bg-gray-50 text-right text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
Latest Version
</th>
<th class="hidden md:table-cell px-6 py-3 border-b border-gray-200 bg-gray-50 text-right text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider">
Published Version
</th>
<th class="pr-6 py-3 border-b border-gray-200 bg-gray-50 text-right text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider"/>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-100">
{collection.map(groupings, group => {
const versions = collection.orderBy(group, 'version', 'desc');
const workflowDefinition = versions[0]
const latestVersionNumber = workflowDefinition.version;
const publishedVersion: WorkflowDefinitionSummary = versions.find(x => x.isPublished);
const publishedVersionNumber = !!publishedVersion ? publishedVersion.version : '-';
let workflowDisplayName = workflowDefinition.displayName;
if (!workflowDisplayName || workflowDisplayName.trim().length == 0)
workflowDisplayName = workflowDefinition.name;
if (!workflowDisplayName || workflowDisplayName.trim().length == 0)
workflowDisplayName = 'Untitled';
const editUrl = `/workflow-definitions/${workflowDefinition.definitionId}`;
const editIcon = (
<svg class="h-5 w-5 text-gray-500" width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
</svg>
);
const deleteIcon = (
<svg class="h-5 w-5 text-gray-500" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<line x1="4" y1="7" x2="20" y2="7"/>
<line x1="10" y1="11" x2="10" y2="17"/>
<line x1="14" y1="11" x2="14" y2="17"/>
<path d="M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2 -2l1 -12"/>
<path d="M9 7v-3a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v3"/>
</svg>
);
return (
<tr>
return <tr>
<td class="px-6 py-3 whitespace-no-wrap text-sm leading-5 font-medium text-gray-900">
<input type="checkbox" value={workflowInstance.id} checked={isSelected} class="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded"/>
</td>
<td class="px-6 py-3 whitespace-no-wrap text-sm leading-5 font-medium text-gray-900">
<a href={viewUrl} class="truncate hover:text-gray-600">{workflowInstance.id}</a>
</td>
<td class="px-6 py-3 whitespace-no-wrap text-sm leading-5 font-medium text-gray-900 text-left">
<a href={`/workflow-registry/${workflowInstance.definitionId}/viewer`} class="truncate hover:text-gray-600">
{displayName}
</a>
</td>
<td class="hidden md:table-cell px-6 py-3 whitespace-no-wrap text-sm leading-5 text-gray-500 text-right">
{workflowInstance.version}
</td>
<td class="px-6 py-3 whitespace-no-wrap text-sm leading-5 font-medium text-gray-900 text-left">
<a href={`"@($" workflow-registry/{workflowInstance.DefinitionId}/viewer")"`} class="truncate hover:text-gray-600">{instanceName}</a>
</td>
<td class="hidden md:table-cell px-6 py-3 whitespace-no-wrap text-sm leading-5 text-gray-500 text-right">
<div class="flex items-center space-x-3 lg:pl-2">
<stencil-route-link url={editUrl} anchorClass="truncate hover:text-gray-600"><span>{workflowDisplayName}</span></stencil-route-link>
<div class="flex-shrink-0 w-2.5 h-2.5 rounded-full bg-@statusColor-600"/>
<span>{workflowInstance.workflowStatus}</span>
</div>
</td>
<td class="px-6 py-3 text-sm leading-5 text-gray-500 font-medium">
<div class="flex items-center space-x-2">
<div class="flex items-center space-x-2">
<div class="flex flex-shrink-0 -space-x-1"><a class="max-w-none h-9 w-9 rounded-full text-white shadow-solid p-2 text-xs bg-blue-500 hover:bg-blue-400" href="#">4</a>
<a class="max-w-none h-9 w-9 rounded-full text-white shadow-solid p-2 text-xs bg-green-500 hover:bg-green-400" href="#">999</a>
<a class="max-w-none h-9 w-9 rounded-full text-white shadow-solid p-2 text-xs bg-red-500 hover:bg-red-400" href="#">75</a></div>
</div>
</div>
<td class="hidden md:table-cell px-6 py-3 whitespace-no-wrap text-sm leading-5 text-gray-500 text-left">
{workflowInstance.createdAt}
</td>
<td class="hidden md:table-cell px-6 py-3 whitespace-no-wrap text-sm leading-5 text-gray-500 text-left">
{!!workflowInstance.finishedAt ? workflowInstance.finishedAt.toString() : '-'}
</td>
<td class="hidden md:table-cell px-6 py-3 whitespace-no-wrap text-sm leading-5 text-gray-500 text-left">
{!!workflowInstance.lastExecutedAt ? workflowInstance.lastExecutedAt.toString() : '-'}
</td>
<td class="hidden md:table-cell px-6 py-3 whitespace-no-wrap text-sm leading-5 text-gray-500 text-left">
{!!workflowInstance.faultedAt ? workflowInstance.faultedAt.toString() : '-'}
</td>
<td class="hidden md:table-cell px-6 py-3 whitespace-no-wrap text-sm leading-5 text-gray-500 text-right">{latestVersionNumber}</td>
<td class="hidden md:table-cell px-6 py-3 whitespace-no-wrap text-sm leading-5 text-gray-500 text-right">{publishedVersionNumber}</td>
<td class="pr-6">
<elsa-context-menu history={this.history} menuItems={[
{text: 'Edit', anchorUrl: editUrl, icon: editIcon}
]}/>
<elsa-context-menu/>
</td>
</tr>
);
})}
</tbody>
</table>
})}
</tbody>
</table>
<elsa-pager page="@WorkflowInstances.Page" pageSize="@WorkflowInstances.PageSize" totalCount="@WorkflowInstances.TotalCount"/>
</div>
<elsa-confirm-dialog ref={el => this.confirmDialog = el}/>
</div>
<elsa-confirm-dialog ref={el => this.confirmDialog = el}/>
</div>
);
}
@ -193,10 +285,23 @@ export class ElsaWorkflowInstancesListScreen {
const selectedWorkflowText = !selectedWorkflowId ? "Workflow" : !!selectedWorkflow ? (selectedWorkflow.displayName || selectedWorkflow.name) : "Workflow";
const selectedWorkflowStatus = this.selectedWorkflowStatus;
const SelectedOrderBy = this.selectedOrderBy;
let items: Array<DropdownButtonItem> = latestWorkflowBlueprints.map(x => ({text: x.displayName!, name: x.id, url: this.buildFilterUrl(x.id, selectedWorkflowStatus, SelectedOrderBy), isSelected: x.id == selectedWorkflowId}));
let items: Array<DropdownButtonItem> = latestWorkflowBlueprints.map(x => ({text: x.displayName!, value: x.id, url: this.buildFilterUrl(x.id, selectedWorkflowStatus, SelectedOrderBy), isSelected: x.id == selectedWorkflowId}));
items = [{text: 'All', name: 'All', url: this.buildFilterUrl(null, selectedWorkflowStatus, SelectedOrderBy), isSelected: !selectedWorkflowId}, ...items];
return <elsa-dropdown-button text={selectedWorkflowText} items={items} icon={null} origin={DropdownButtonOrigin.TopRight}/>
items = [{text: 'All', value: null, url: this.buildFilterUrl(null, selectedWorkflowStatus, SelectedOrderBy), isSelected: !selectedWorkflowId}, ...items];
return <elsa-dropdown-button text={selectedWorkflowText} items={items} icon={null} origin={DropdownButtonOrigin.TopRight} onItemSelected={e => this.selectedWorkflowId = e.detail.value as string}/>
}
renderStatusFilter(){
const selectedWorkflowStatus = this.selectedWorkflowStatus;
const selectedWorkflowStatusText = selectedWorkflowStatus != null ? this.getStatusText(selectedWorkflowStatus) : 'Status';
const statuses: Array<WorkflowStatus> = [null, WorkflowStatus.Running, WorkflowStatus.Suspended, WorkflowStatus.Finished, WorkflowStatus.Faulted, WorkflowStatus.Cancelled, WorkflowStatus.Idle];
const items: Array<DropdownButtonItem> = statuses.map(x => {
const enumText = x != null ? this.getStatusText(x) : null;
const text = !!enumText ? enumText : 'All';
return ({text: text, url: this.buildFilterUrl(this.selectedWorkflowId, x, this.selectedOrderBy), isSelected: x == selectedWorkflowStatus});
});
return <elsa-dropdown-button text={selectedWorkflowStatusText} items={items} icon={null} origin={DropdownButtonOrigin.TopRight}/>
}
}

View file

@ -46,6 +46,24 @@ export interface WorkflowBlueprintSummary {
isLatest: boolean;
}
export interface WorkflowInstanceSummary
{
id: string;
definitionId: string;
tenantId?: string;
version: number;
workflowStatus: WorkflowStatus;
correlationId?: string;
contextType?: string;
contextId?: string;
name?: string;
createdAt?: Date
lastExecutedAt?: Date;
finishedAt?: Date;
cancelledAt?: Date;
faultedAt?: Date;
}
export interface ActivityDefinition {
activityId: string;
type: string;

View file

@ -1,148 +1,176 @@
import axios, {AxiosRequestConfig} from "axios";
import * as collection from 'lodash/collection';
import {
ActivityDefinition,
ActivityDescriptor,
ConnectionDefinition,
getVersionOptionsString,
PagedList,
Variables,
VersionOptions, WorkflowBlueprintSummary,
WorkflowContextOptions,
WorkflowDefinition,
WorkflowDefinitionSummary,
WorkflowPersistenceBehavior
ActivityDefinition,
ActivityDescriptor,
ConnectionDefinition,
getVersionOptionsString, OrderBy,
PagedList,
Variables,
VersionOptions, WorkflowBlueprintSummary,
WorkflowContextOptions,
WorkflowDefinition,
WorkflowDefinitionSummary, WorkflowInstanceSummary,
WorkflowPersistenceBehavior, WorkflowStatus
} from "../models";
export const createElsaClient = function (serverUrl: string): ElsaClient {
const config: AxiosRequestConfig = {
baseURL: serverUrl
};
const config: AxiosRequestConfig = {
baseURL: serverUrl
};
const httpClient = axios.create(config);
const httpClient = axios.create(config);
return {
activitiesApi: {
list: async () => {
const response = await httpClient.get<Array<ActivityDescriptor>>('v1/activities');
return response.data;
}
},
workflowDefinitionsApi: {
list: async (page?: number, pageSize?: number, versionOptions?: VersionOptions) => {
const versionOptionsString = getVersionOptionsString(versionOptions);
const response = await httpClient.get<PagedList<WorkflowDefinitionSummary>>(`v1/workflow-definitions?version=${versionOptionsString}`);
return response.data;
},
getByDefinitionAndVersion: async (definitionId: string, versionOptions: VersionOptions) => {
const versionOptionsString = getVersionOptionsString(versionOptions);
const response = await httpClient.get<WorkflowDefinition>(`v1/workflow-definitions/${definitionId}/${versionOptionsString}`);
return response.data;
},
save: async request => {
const response = await httpClient.post<WorkflowDefinition>('v1/workflow-definitions', request);
return response.data;
},
delete: async definitionId => {
await httpClient.delete(`v1/workflow-definitions/${definitionId}`);
},
retract: async workflowDefinitionId => {
const response = await httpClient.post<WorkflowDefinition>(`v1/workflow-definitions/${workflowDefinitionId}/retract`);
return response.data;
},
export: async (workflowDefinitionId, versionOptions): Promise<ExportWorkflowResponse> => {
const versionOptionsString = getVersionOptionsString(versionOptions);
const response = await httpClient.post(`v1/workflow-definitions/${workflowDefinitionId}/${versionOptionsString}/export`, null, {
responseType: 'blob'
});
return {
activitiesApi: {
list: async () => {
const response = await httpClient.get<Array<ActivityDescriptor>>('v1/activities');
return response.data;
}
},
workflowDefinitionsApi: {
list: async (page?: number, pageSize?: number, versionOptions?: VersionOptions) => {
const versionOptionsString = getVersionOptionsString(versionOptions);
const response = await httpClient.get<PagedList<WorkflowDefinitionSummary>>(`v1/workflow-definitions?version=${versionOptionsString}`);
return response.data;
},
getByDefinitionAndVersion: async (definitionId: string, versionOptions: VersionOptions) => {
const versionOptionsString = getVersionOptionsString(versionOptions);
const response = await httpClient.get<WorkflowDefinition>(`v1/workflow-definitions/${definitionId}/${versionOptionsString}`);
return response.data;
},
save: async request => {
const response = await httpClient.post<WorkflowDefinition>('v1/workflow-definitions', request);
return response.data;
},
delete: async definitionId => {
await httpClient.delete(`v1/workflow-definitions/${definitionId}`);
},
retract: async workflowDefinitionId => {
const response = await httpClient.post<WorkflowDefinition>(`v1/workflow-definitions/${workflowDefinitionId}/retract`);
return response.data;
},
export: async (workflowDefinitionId, versionOptions): Promise<ExportWorkflowResponse> => {
const versionOptionsString = getVersionOptionsString(versionOptions);
const response = await httpClient.post(`v1/workflow-definitions/${workflowDefinitionId}/${versionOptionsString}/export`, null, {
responseType: 'blob'
});
const contentDispositionHeader = response.headers["content-disposition"]; // Only available if the Elsa Server exposes the "Content-Disposition" header.
const fileName = contentDispositionHeader ? contentDispositionHeader.split(";")[1].split("=")[1] : `workflow-definition-${workflowDefinitionId}.json`;
const data = response.data;
const contentDispositionHeader = response.headers["content-disposition"]; // Only available if the Elsa Server exposes the "Content-Disposition" header.
const fileName = contentDispositionHeader ? contentDispositionHeader.split(";")[1].split("=")[1] : `workflow-definition-${workflowDefinitionId}.json`;
const data = response.data;
return {
fileName: fileName,
data: data
};
},
import: async (workflowDefinitionId, file: File): Promise<WorkflowDefinition> => {
const formData = new FormData();
formData.append("file", file);
const response = await httpClient.post<WorkflowDefinition>(`v1/workflow-definitions/${workflowDefinitionId}/import`, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
return response.data;
}
},
workflowRegistryApi: {
list: async (page?: number, pageSize?: number, versionOptions?: VersionOptions): Promise<PagedList<WorkflowBlueprintSummary>> => {
const versionOptionsString = getVersionOptionsString(versionOptions);
const response = await httpClient.get<PagedList<WorkflowBlueprintSummary>>(`v1/workflow-registry?version=${versionOptionsString}`);
return response.data;
}
},
scriptingApi: {
getJavaScriptTypeDefinitions: async (workflowDefinitionId: string, context?: string): Promise<string> => {
context = context || '';
const response = await httpClient.get<string>(`v1/scripting/javascript/type-definitions/${workflowDefinitionId}?t=${new Date().getTime()}&context=${context}`);
return response.data;
}
return {
fileName: fileName,
data: data
};
},
import: async (workflowDefinitionId, file: File): Promise<WorkflowDefinition> => {
const formData = new FormData();
formData.append("file", file);
const response = await httpClient.post<WorkflowDefinition>(`v1/workflow-definitions/${workflowDefinitionId}/import`, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
return response.data;
}
},
workflowRegistryApi: {
list: async (page?: number, pageSize?: number, versionOptions?: VersionOptions): Promise<PagedList<WorkflowBlueprintSummary>> => {
const versionOptionsString = getVersionOptionsString(versionOptions);
const response = await httpClient.get<PagedList<WorkflowBlueprintSummary>>(`v1/workflow-registry?version=${versionOptionsString}`);
return response.data;
}
},
workflowInstancesApi: {
list: async (page?: number, pageSize?: number, workflowDefinitionId?: string, workflowStatus?: WorkflowStatus, orderBy?: OrderBy, searchTerm?: string): Promise<PagedList<WorkflowInstanceSummary>> => {
const queryString = {};
if(!!workflowDefinitionId)
queryString['workflow'] = workflowDefinitionId;
if(workflowStatus != null)
queryString['status'] = workflowStatus;
if(!!orderBy)
queryString['orderBy'] = orderBy;
if(!!searchTerm)
queryString['searchTerm'] = searchTerm;
const queryStringItems = collection.map(queryString, (v, k) => `${k}=${v}`);
const queryStringText = queryStringItems.length > 0 ? `?${queryStringItems.join('&')}` : '';
const response = await httpClient.get<PagedList<WorkflowInstanceSummary>>(`v1/workflow-instances${queryStringText}`);
return response.data;
}
},
scriptingApi: {
getJavaScriptTypeDefinitions: async (workflowDefinitionId: string, context?: string): Promise<string> => {
context = context || '';
const response = await httpClient.get<string>(`v1/scripting/javascript/type-definitions/${workflowDefinitionId}?t=${new Date().getTime()}&context=${context}`);
return response.data;
}
}
}
}
}
export interface ElsaClient {
activitiesApi: ActivitiesApi;
workflowDefinitionsApi: WorkflowDefinitionsApi;
workflowRegistryApi: WorkflowRegistryApi;
scriptingApi: ScriptingApi;
activitiesApi: ActivitiesApi;
workflowDefinitionsApi: WorkflowDefinitionsApi;
workflowRegistryApi: WorkflowRegistryApi;
workflowInstancesApi: WorkflowInstancesApi;
scriptingApi: ScriptingApi;
}
export interface ActivitiesApi {
list(): Promise<Array<ActivityDescriptor>>;
list(): Promise<Array<ActivityDescriptor>>;
}
export interface WorkflowDefinitionsApi {
getByDefinitionAndVersion(definitionId: string, versionOptions: VersionOptions): Promise<WorkflowDefinition>
getByDefinitionAndVersion(definitionId: string, versionOptions: VersionOptions): Promise<WorkflowDefinition>
list(page?: number, pageSize?: number, versionOptions?: VersionOptions): Promise<PagedList<WorkflowDefinitionSummary>>
list(page?: number, pageSize?: number, versionOptions?: VersionOptions): Promise<PagedList<WorkflowDefinitionSummary>>
save(request: SaveWorkflowDefinitionRequest): Promise<WorkflowDefinition>
save(request: SaveWorkflowDefinitionRequest): Promise<WorkflowDefinition>
delete(definitionId: string): Promise<void>
delete(definitionId: string): Promise<void>
retract(workflowDefinitionId: string): Promise<WorkflowDefinition>
retract(workflowDefinitionId: string): Promise<WorkflowDefinition>
export(workflowDefinitionId: string, versionOptions: VersionOptions): Promise<ExportWorkflowResponse>
export(workflowDefinitionId: string, versionOptions: VersionOptions): Promise<ExportWorkflowResponse>
import(workflowDefinitionId: string, file: File): Promise<WorkflowDefinition>
import(workflowDefinitionId: string, file: File): Promise<WorkflowDefinition>
}
export interface WorkflowRegistryApi {
list(page?: number, pageSize?: number, versionOptions?: VersionOptions): Promise<PagedList<WorkflowBlueprintSummary>>
list(page?: number, pageSize?: number, versionOptions?: VersionOptions): Promise<PagedList<WorkflowBlueprintSummary>>
}
export interface WorkflowInstancesApi {
list(page?: number, pageSize?: number, workflowDefinitionId?: string, workflowStatus?: WorkflowStatus, orderBy?: OrderBy, searchTerm?: string): Promise<PagedList<WorkflowInstanceSummary>>
}
export interface ScriptingApi {
getJavaScriptTypeDefinitions(workflowDefinitionId: string, context?: string): Promise<string>
getJavaScriptTypeDefinitions(workflowDefinitionId: string, context?: string): Promise<string>
}
export interface SaveWorkflowDefinitionRequest {
workflowDefinitionId?: string
name?: string
displayName?: string
description?: string
variables?: Variables
contextOptions?: WorkflowContextOptions
isSingleton?: boolean
persistenceBehavior?: WorkflowPersistenceBehavior
deleteCompletedInstances?: boolean
publish?: boolean
activities: Array<ActivityDefinition>
connections: Array<ConnectionDefinition>
workflowDefinitionId?: string
name?: string
displayName?: string
description?: string
variables?: Variables
contextOptions?: WorkflowContextOptions
isSingleton?: boolean
persistenceBehavior?: WorkflowPersistenceBehavior
deleteCompletedInstances?: boolean
publish?: boolean
activities: Array<ActivityDefinition>
connections: Array<ConnectionDefinition>
}
export interface ExportWorkflowResponse {
fileName: string;
data: Blob;
fileName: string;
data: Blob;
}

View file

@ -1,201 +1,215 @@
import {ActivityDefinition, ActivityDefinitionProperty, ActivityModel, ConnectionModel, WorkflowModel} from "../models";
declare global {
interface Array<T> {
distinct(): Array<T>;
interface Array<T> {
distinct(): Array<T>;
last(): T;
}
last(): T;
}
}
export type Map<T> = {
[key: string]: T
[key: string]: T
};
export function format(first: string, middle: string, last: string): string {
return (first || '') + (middle ? ` ${middle}` : '') + (last ? ` ${last}` : '');
return (first || '') + (middle ? ` ${middle}` : '') + (last ? ` ${last}` : '');
}
export interface Array<T> {
distinct(): Array<T>;
distinct(): Array<T>;
last(): T;
last(): T;
find<S extends T>(predicate: (this: void, value: T, index: number, obj: T[]) => value is S, thisArg?: any): S | undefined;
find<S extends T>(predicate: (this: void, value: T, index: number, obj: T[]) => value is S, thisArg?: any): S | undefined;
find(predicate: (value: T, index: number, obj: T[]) => unknown, thisArg?: any): T | undefined;
find(predicate: (value: T, index: number, obj: T[]) => unknown, thisArg?: any): T | undefined;
push(...items: T[]): number;
push(...items: T[]): number;
}
Array.prototype.distinct = function () {
return [...new Set(this)];
return [...new Set(this)];
}
if (!Array.prototype.last) {
Array.prototype.last = function () {
return this[this.length - 1];
};
Array.prototype.last = function () {
return this[this.length - 1];
};
}
export function isNumeric(str: string): boolean {
return !isNaN(str as any) && // use type coercion to parse the _entirety_ of the string (`parseFloat` alone does not do this)...
!isNaN(parseFloat(str)) // ...and ensure strings of whitespace fail
return !isNaN(str as any) && // use type coercion to parse the _entirety_ of the string (`parseFloat` alone does not do this)...
!isNaN(parseFloat(str)) // ...and ensure strings of whitespace fail
}
export function getChildActivities(workflowModel: WorkflowModel, parentId?: string) {
if (parentId == null) {
const targetIds = new Set(workflowModel.connections.map(x => x.targetId));
return workflowModel.activities.filter(x => !targetIds.has(x.activityId));
} else {
const targetIds = new Set(workflowModel.connections.filter(x => x.sourceId === parentId).map(x => x.targetId));
return workflowModel.activities.filter(x => targetIds.has(x.activityId));
}
if (parentId == null) {
const targetIds = new Set(workflowModel.connections.map(x => x.targetId));
return workflowModel.activities.filter(x => !targetIds.has(x.activityId));
} else {
const targetIds = new Set(workflowModel.connections.filter(x => x.sourceId === parentId).map(x => x.targetId));
return workflowModel.activities.filter(x => targetIds.has(x.activityId));
}
}
export function getInboundConnections(workflowModel: WorkflowModel, activityId: string) {
return workflowModel.connections.filter(x => x.targetId === activityId);
return workflowModel.connections.filter(x => x.targetId === activityId);
}
export function getOutboundConnections(workflowModel: WorkflowModel, activityId: string) {
return workflowModel.connections.filter(x => x.sourceId === activityId);
return workflowModel.connections.filter(x => x.sourceId === activityId);
}
export function removeActivity(workflowModel: WorkflowModel, activityId: string): WorkflowModel {
const inboundConnections = getInboundConnections(workflowModel, activityId);
const outboundConnections = getOutboundConnections(workflowModel, activityId);
const connectionsToRemove = [...inboundConnections, ...outboundConnections];
const inboundConnections = getInboundConnections(workflowModel, activityId);
const outboundConnections = getOutboundConnections(workflowModel, activityId);
const connectionsToRemove = [...inboundConnections, ...outboundConnections];
return {
...workflowModel,
activities: workflowModel.activities.filter(x => x.activityId != activityId),
connections: workflowModel.connections.filter(x => connectionsToRemove.indexOf(x) < 0)
};
return {
...workflowModel,
activities: workflowModel.activities.filter(x => x.activityId != activityId),
connections: workflowModel.connections.filter(x => connectionsToRemove.indexOf(x) < 0)
};
}
export function findActivity(workflowModel: WorkflowModel, activityId: string) {
return workflowModel.activities.find(x => x.activityId === activityId);
return workflowModel.activities.find(x => x.activityId === activityId);
}
export function addConnection(workflowModel: WorkflowModel, connection: ConnectionModel);
export function addConnection(workflowModel: WorkflowModel, sourceId: string, targetId: string, outcome: string);
export function addConnection(workflowModel: WorkflowModel, ...args: any) {
const connection = typeof (args) == 'object' ? args as ConnectionModel : {sourceId: args[0], targetId: args[1], outcome: args[3]};
const connection = typeof (args) == 'object' ? args as ConnectionModel : {sourceId: args[0], targetId: args[1], outcome: args[3]};
return {
...workflowModel,
connections: [...workflowModel.connections, connection]
};
return {
...workflowModel,
connections: [...workflowModel.connections, connection]
};
}
export function setActivityDefinitionProperty(activityDefinition: ActivityDefinition, name: string, expression: string, syntax: string) {
setProperty(activityDefinition.properties, name, expression, syntax);
setProperty(activityDefinition.properties, name, expression, syntax);
}
export function setActivityModelProperty(activityModel: ActivityModel, name: string, expression: string, syntax: string) {
setProperty(activityModel.properties, name, expression, syntax);
setProperty(activityModel.properties, name, expression, syntax);
}
export function setProperty(properties: Array<ActivityDefinitionProperty>, name: string, expression: string, syntax?: string) {
let property: ActivityDefinitionProperty = properties.find(x => x.name == name);
let property: ActivityDefinitionProperty = properties.find(x => x.name == name);
if(!syntax)
syntax = 'Literal';
if (!syntax)
syntax = 'Literal';
if (!property) {
const expressions = {};
expressions[syntax] = expression;
property = {name: name, expressions: expressions, syntax: syntax};
properties.push(property);
} else {
property.expressions[syntax] = expression;
property.syntax = syntax;
}
if (!property) {
const expressions = {};
expressions[syntax] = expression;
property = {name: name, expressions: expressions, syntax: syntax};
properties.push(property);
} else {
property.expressions[syntax] = expression;
property.syntax = syntax;
}
}
export function getOrCreateProperty(activity: ActivityModel , name: string, defaultExpression?: () => string, defaultSyntax?: () => string): ActivityDefinitionProperty {
let property: ActivityDefinitionProperty = activity.properties.find(x => x.name == name);
export function getOrCreateProperty(activity: ActivityModel, name: string, defaultExpression?: () => string, defaultSyntax?: () => string): ActivityDefinitionProperty {
let property: ActivityDefinitionProperty = activity.properties.find(x => x.name == name);
if (!property) {
const expressions = {};
let syntax = defaultSyntax ? defaultSyntax() : undefined;
if (!property) {
const expressions = {};
let syntax = defaultSyntax ? defaultSyntax() : undefined;
if(!syntax)
syntax = 'Literal';
if (!syntax)
syntax = 'Literal';
expressions[syntax] = defaultExpression ? defaultExpression() : undefined;
property = {name: name, expressions: expressions, syntax: null};
activity.properties.push(property);
}
expressions[syntax] = defaultExpression ? defaultExpression() : undefined;
property = {name: name, expressions: expressions, syntax: null};
activity.properties.push(property);
}
return property;
return property;
}
export function parseJson(json: string): any {
try {
return JSON.parse(json);
} catch (e) {
console.warn(`Error parsing JSON: ${e}`);
}
return undefined;
try {
return JSON.parse(json);
} catch (e) {
console.warn(`Error parsing JSON: ${e}`);
}
return undefined;
}
export function parseQuery(queryString?: string): any {
if (!queryString)
return {};
const query = {};
const pairs = (queryString[0] === '?' ? queryString.substr(1) : queryString).split('&');
for (let i = 0; i < pairs.length; i++) {
const pair = pairs[i].split('=');
query[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || '');
}
return query;
}
export function timeSince(time) {
switch (typeof time) {
case 'number':
break;
case 'string':
time = +new Date(time);
break;
case 'object':
if (time.constructor === Date) time = time.getTime();
break;
default:
time = +new Date();
}
const time_formats = [
[60, 'seconds', 1], // 60
[120, '1 minute ago', '1 minute from now'], // 60*2
[3600, 'minutes', 60], // 60*60, 60
[7200, '1 hour ago', '1 hour from now'], // 60*60*2
[86400, 'hours', 3600], // 60*60*24, 60*60
[172800, 'Yesterday', 'Tomorrow'], // 60*60*24*2
[604800, 'days', 86400], // 60*60*24*7, 60*60*24
[1209600, 'Last week', 'Next week'], // 60*60*24*7*4*2
[2419200, 'weeks', 604800], // 60*60*24*7*4, 60*60*24*7
[4838400, 'Last month', 'Next month'], // 60*60*24*7*4*2
[29030400, 'months', 2419200], // 60*60*24*7*4*12, 60*60*24*7*4
[58060800, 'Last year', 'Next year'], // 60*60*24*7*4*12*2
[2903040000, 'years', 29030400], // 60*60*24*7*4*12*100, 60*60*24*7*4*12
[5806080000, 'Last century', 'Next century'], // 60*60*24*7*4*12*100*2
[58060800000, 'centuries', 2903040000] // 60*60*24*7*4*12*100*20, 60*60*24*7*4*12*100
];
let seconds = (+new Date() - time) / 1000,
token = 'ago',
list_choice = 1;
if (seconds === 0) {
return 'Just now';
}
if (seconds < 0) {
seconds = Math.abs(seconds);
token = 'from now';
list_choice = 2;
}
let i = 0,
format;
// tslint:disable-next-line: no-conditional-assignment
while (format = time_formats[i++]) {
if (seconds < format[0]) {
if (typeof format[2] === 'string') {
return format[list_choice];
} else {
return Math.floor(seconds / format[2]) + ' ' + format[1] + ' ' + token;
}
switch (typeof time) {
case 'number':
break;
case 'string':
time = +new Date(time);
break;
case 'object':
if (time.constructor === Date) time = time.getTime();
break;
default:
time = +new Date();
}
}
return time;
const time_formats = [
[60, 'seconds', 1], // 60
[120, '1 minute ago', '1 minute from now'], // 60*2
[3600, 'minutes', 60], // 60*60, 60
[7200, '1 hour ago', '1 hour from now'], // 60*60*2
[86400, 'hours', 3600], // 60*60*24, 60*60
[172800, 'Yesterday', 'Tomorrow'], // 60*60*24*2
[604800, 'days', 86400], // 60*60*24*7, 60*60*24
[1209600, 'Last week', 'Next week'], // 60*60*24*7*4*2
[2419200, 'weeks', 604800], // 60*60*24*7*4, 60*60*24*7
[4838400, 'Last month', 'Next month'], // 60*60*24*7*4*2
[29030400, 'months', 2419200], // 60*60*24*7*4*12, 60*60*24*7*4
[58060800, 'Last year', 'Next year'], // 60*60*24*7*4*12*2
[2903040000, 'years', 29030400], // 60*60*24*7*4*12*100, 60*60*24*7*4*12
[5806080000, 'Last century', 'Next century'], // 60*60*24*7*4*12*100*2
[58060800000, 'centuries', 2903040000] // 60*60*24*7*4*12*100*20, 60*60*24*7*4*12*100
];
let seconds = (+new Date() - time) / 1000,
token = 'ago',
list_choice = 1;
if (seconds === 0) {
return 'Just now';
}
if (seconds < 0) {
seconds = Math.abs(seconds);
token = 'from now';
list_choice = 2;
}
let i = 0,
format;
// tslint:disable-next-line: no-conditional-assignment
while (format = time_formats[i++]) {
if (seconds < format[0]) {
if (typeof format[2] === 'string') {
return format[list_choice];
} else {
return Math.floor(seconds / format[2]) + ' ' + format[1] + ' ' + token;
}
}
}
return time;
}