diff --git a/src/designer/elsa-workflows-studio/package.json b/src/designer/elsa-workflows-studio/package.json index 482a07f9c..2349bddb3 100644 --- a/src/designer/elsa-workflows-studio/package.json +++ b/src/designer/elsa-workflows-studio/package.json @@ -1,6 +1,6 @@ { "name": "@elsa-workflows/elsa-workflows-studio", - "version": "2.1.3", + "version": "2.1.4", "description": "Elsa Workflow Studio is a collection of web components to manage Elsa workflows stored on an Elsa Workflows server.", "repository": { "type": "git", diff --git a/src/designer/elsa-workflows-studio/src/components.d.ts b/src/designer/elsa-workflows-studio/src/components.d.ts index 697717dba..1c72f27d4 100644 --- a/src/designer/elsa-workflows-studio/src/components.d.ts +++ b/src/designer/elsa-workflows-studio/src/components.d.ts @@ -5,7 +5,7 @@ * It contains typing information for all components that exist in this project. */ import { HTMLStencilElement, JSXBase } from "@stencil/core/internal"; -import { ActivityDefinitionProperty, ActivityDescriptor, ActivityModel, ActivityPropertyDescriptor, SelectListItem, VersionOptions, WorkflowBlueprint, WorkflowDefinition, WorkflowExecutionLogRecord, WorkflowModel } from "./models"; +import { ActivityDefinitionProperty, ActivityDescriptor, ActivityModel, ActivityPropertyDescriptor, OrderBy, SelectListItem, VersionOptions, WorkflowBlueprint, WorkflowDefinition, WorkflowExecutionLogRecord, WorkflowModel, WorkflowStatus } from "./models"; import { LocationSegments, MatchResults, RouterHistory } from "@stencil/router"; import { MenuItem } from "./components/controls/elsa-context-menu/models"; import { ActivityContextMenuState, LayoutDirection, WorkflowDesignerMode } from "./components/designers/tree/elsa-designer-tree/models"; @@ -14,6 +14,7 @@ import { AxiosInstance, AxiosRequestConfig } from "axios"; import { Service } from "axios-middleware"; import { MonacoValueChangedArgs } from "./components/controls/elsa-monaco/elsa-monaco"; import { Map } from "./utils/utils"; +import { PagerData } from "./components/controls/elsa-pager/elsa-pager"; import { ToastNotificationOptions } from "./components/shared/elsa-toast-notification/elsa-toast-notification"; import { WebhookDefinition } from "./models/webhook"; export namespace Components { @@ -252,7 +253,10 @@ export namespace Components { } interface ElsaWorkflowInstanceListScreen { "history"?: RouterHistory; + "orderBy"?: OrderBy; "serverUrl": string; + "workflowId"?: string; + "workflowStatus"?: WorkflowStatus; } interface ElsaWorkflowInstanceViewerScreen { "getServerUrl": () => Promise; @@ -734,6 +738,7 @@ declare namespace LocalJSX { interface ElsaPager { "history"?: RouterHistory; "location"?: LocationSegments; + "onPaged"?: (event: CustomEvent) => void; "page"?: number; "pageSize"?: number; "totalCount"?: number; @@ -850,7 +855,10 @@ declare namespace LocalJSX { } interface ElsaWorkflowInstanceListScreen { "history"?: RouterHistory; + "orderBy"?: OrderBy; "serverUrl"?: string; + "workflowId"?: string; + "workflowStatus"?: WorkflowStatus; } interface ElsaWorkflowInstanceViewerScreen { "serverUrl"?: string; diff --git a/src/designer/elsa-workflows-studio/src/components/controls/elsa-pager/elsa-pager.tsx b/src/designer/elsa-workflows-studio/src/components/controls/elsa-pager/elsa-pager.tsx index ff65b1789..c754efd85 100644 --- a/src/designer/elsa-workflows-studio/src/components/controls/elsa-pager/elsa-pager.tsx +++ b/src/designer/elsa-workflows-studio/src/components/controls/elsa-pager/elsa-pager.tsx @@ -1,172 +1,183 @@ import {Component, Event, EventEmitter, h, Prop} from '@stencil/core'; -import {LocationSegments, RouterHistory, injectHistory } from "@stencil/router"; +import {LocationSegments, RouterHistory, injectHistory} from "@stencil/router"; import {parseQuery, queryToString} from "../../../utils/utils"; +export interface PagerData { + page: number; + pageSize: number; + totalCount: number; +} + @Component({ - tag: 'elsa-pager', - shadow: false, + tag: 'elsa-pager', + shadow: false, }) export class ElsaPager { - @Prop() page: number; - @Prop() pageSize: number; - @Prop() totalCount: number; - @Prop() location: LocationSegments; - @Prop() history?: RouterHistory; + @Prop() page: number; + @Prop() pageSize: number; + @Prop() totalCount: number; + @Prop() location: LocationSegments; + @Prop() history?: RouterHistory; - basePath: string; + @Event() paged: EventEmitter - componentWillLoad() { - this.basePath = !!this.location ? this.location.pathname : document.location.pathname; + basePath: string; + + componentWillLoad() { + this.basePath = !!this.location ? this.location.pathname : document.location.pathname; + } + + navigate(path: string, page: number) { + + if (this.history) { + this.history.push(path); + return; + } + else { + this.paged.emit({page, pageSize: this.pageSize, totalCount: this.totalCount}); + } + } + + onNavigateClick(e: Event, page: number) { + const anchor = e.currentTarget as HTMLAnchorElement; + + e.preventDefault(); + this.navigate(`${anchor.pathname}${anchor.search}`, page); + } + + render() { + const page = this.page; + const pageSize = this.pageSize; + const totalCount = this.totalCount; + const basePath = this.basePath; + + const from = page * pageSize + 1; + const to = Math.min(from + pageSize, totalCount); + const pageCount = Math.round(((totalCount - 1) / pageSize) + 0.5); + + const maxPageButtons = 10; + const fromPage = Math.max(0, page - maxPageButtons / 2); + const toPage = Math.min(pageCount, fromPage + maxPageButtons); + const self = this; + const currentQuery = !!this.history ? parseQuery(this.history.location.search) : {page, pageSize}; + + currentQuery['pageSize'] = pageSize; + + const getNavUrl = (page: number) => { + const query = {...currentQuery, 'page': page}; + return `${basePath}?${queryToString(query)}`; + }; + + const renderPreviousButton = function () { + if (page <= 0) + return; + + return self.onNavigateClick(e, page - 1)} + class="elsa-relative elsa-inline-flex elsa-items-center elsa-px-4 elsa-py-2 elsa-border elsa-border-gray-300 elsa-text-sm elsa-leading-5 elsa-font-medium elsa-rounded-md elsa-text-gray-700 elsa-bg-white hover:elsa-text-gray-500 focus:elsa-outline-none focus:elsa-shadow-outline-blue focus:elsa-border-blue-300 active:elsa-bg-gray-100 active:elsa-text-gray-700 elsa-transition elsa-ease-in-out elsa-duration-150"> + Previous + } - navigate(path: string) { - if (this.history) { - this.history.push(path); - return; - } + const renderNextButton = function () { + if (page >= pageCount) + return; - document.location.pathname = path; + return self.onNavigateClick(e, page + 1)} + class="elsa-ml-3 elsa-relative elsa-inline-flex elsa-items-center elsa-px-4 elsa-py-2 elsa-border elsa-border-gray-300 elsa-text-sm elsa-leading-5 elsa-font-medium elsa-rounded-md elsa-text-gray-700 elsa-bg-white hover:elsa-text-gray-500 focus:elsa-outline-none focus:elsa-shadow-outline-blue focus:elsa-border-blue-300 active:elsa-bg-gray-100 active:elsa-text-gray-700 elsa-transition elsa-ease-in-out elsa-duration-150"> + Next + } - onNavigateClick(e: Event) { - const anchor = e.currentTarget as HTMLAnchorElement; + const renderChevronLeft = function () { + if (page <= 0) + return; - e.preventDefault(); - this.navigate(`${anchor.pathname}${anchor.search}`); + return ( + self.onNavigateClick(e, page - 1)} + class="elsa-relative elsa-inline-flex elsa-items-center elsa-px-2 elsa-py-2 elsa-rounded-l-md elsa-border elsa-border-gray-300 elsa-bg-white elsa-text-sm elsa-leading-5 elsa-font-medium elsa-text-gray-500 hover:elsa-text-gray-400 focus:elsa-z-10 focus:elsa-outline-none focus:elsa-border-blue-300 focus:elsa-shadow-outline-blue active:elsa-bg-gray-100 active:elsa-text-gray-500 elsa-transition elsa-ease-in-out elsa-duration-150" + aria-label="Previous"> + + + + ); } - render() { - const page = this.page; - const pageSize = this.pageSize; - const totalCount = this.totalCount; - const basePath = this.basePath; + const renderChevronRight = function () { + if (page >= pageCount - 1) + return; - const from = page * pageSize + 1; - const to = Math.min(from + pageSize, totalCount); - const pageCount = Math.round(((totalCount - 1) / pageSize) + 0.5); + return ( + self.onNavigateClick(e, page + 1)} + class="-elsa-ml-px elsa-relative elsa-inline-flex elsa-items-center elsa-px-2 elsa-py-2 elsa-rounded-r-md elsa-border elsa-border-gray-300 elsa-bg-white elsa-text-sm elsa-leading-5 elsa-font-medium elsa-text-gray-500 hover:elsa-text-gray-400 focus:elsa-z-10 focus:elsa-outline-none focus:elsa-border-blue-300 focus:elsa-shadow-outline-blue active:elsa-bg-gray-100 active:elsa-text-gray-500 elsa-transition elsa-ease-in-out elsa-duration-150" + aria-label="Next"> + + + + + ); + } - const maxPageButtons = 10; - const fromPage = Math.max(0, page - maxPageButtons / 2); - const toPage = Math.min(pageCount, fromPage + maxPageButtons); - const self = this; - const currentQuery = parseQuery(this.history.location.search); + const renderPagerButtons = function () { + const buttons = []; - currentQuery['pageSize'] = pageSize; - - const getNavUrl = (page: number) => { - const query = {...currentQuery, 'page': page}; - return `${basePath}?${queryToString(query)}`; - }; + for (let i = fromPage; i < toPage; i++) { + const isCurrent = page == i; + const isFirst = i == fromPage; + const isLast = i == toPage - 1; + const leftRoundedClass = isFirst && isCurrent ? 'elsa-rounded-l-md' : ''; + const rightRoundedClass = isLast && isCurrent ? 'elsa-rounded-r-md' : ''; - const renderPreviousButton = function () { - if (page <= 0) - return; - - return self.onNavigateClick(e)} - class="elsa-relative elsa-inline-flex elsa-items-center elsa-px-4 elsa-py-2 elsa-border elsa-border-gray-300 elsa-text-sm elsa-leading-5 elsa-font-medium elsa-rounded-md elsa-text-gray-700 elsa-bg-white hover:elsa-text-gray-500 focus:elsa-outline-none focus:elsa-shadow-outline-blue focus:elsa-border-blue-300 active:elsa-bg-gray-100 active:elsa-text-gray-700 elsa-transition elsa-ease-in-out elsa-duration-150"> - Previous - - } - - const renderNextButton = function () { - if (page >= pageCount) - return; - - return self.onNavigateClick(e)} - class="elsa-ml-3 elsa-relative elsa-inline-flex elsa-items-center elsa-px-4 elsa-py-2 elsa-border elsa-border-gray-300 elsa-text-sm elsa-leading-5 elsa-font-medium elsa-rounded-md elsa-text-gray-700 elsa-bg-white hover:elsa-text-gray-500 focus:elsa-outline-none focus:elsa-shadow-outline-blue focus:elsa-border-blue-300 active:elsa-bg-gray-100 active:elsa-text-gray-700 elsa-transition elsa-ease-in-out elsa-duration-150"> - Next - - } - - const renderChevronLeft = function () { - if (page <= 0) - return; - - return ( - self.onNavigateClick(e)} - class="elsa-relative elsa-inline-flex elsa-items-center elsa-px-2 elsa-py-2 elsa-rounded-l-md elsa-border elsa-border-gray-300 elsa-bg-white elsa-text-sm elsa-leading-5 elsa-font-medium elsa-text-gray-500 hover:elsa-text-gray-400 focus:elsa-z-10 focus:elsa-outline-none focus:elsa-border-blue-300 focus:elsa-shadow-outline-blue active:elsa-bg-gray-100 active:elsa-text-gray-500 elsa-transition elsa-ease-in-out elsa-duration-150" - aria-label="Previous"> - - - - ); - } - - const renderChevronRight = function () { - if (page >= pageCount - 1) - return; - - return ( - self.onNavigateClick(e)} - class="-elsa-ml-px elsa-relative elsa-inline-flex elsa-items-center elsa-px-2 elsa-py-2 elsa-rounded-r-md elsa-border elsa-border-gray-300 elsa-bg-white elsa-text-sm elsa-leading-5 elsa-font-medium elsa-text-gray-500 hover:elsa-text-gray-400 focus:elsa-z-10 focus:elsa-outline-none focus:elsa-border-blue-300 focus:elsa-shadow-outline-blue active:elsa-bg-gray-100 active:elsa-text-gray-500 elsa-transition elsa-ease-in-out elsa-duration-150" - aria-label="Next"> - - - - - ); - } - - const renderPagerButtons = function () { - const buttons = []; - - for (let i = fromPage; i < toPage; i++) { - const isCurrent = page == i; - const isFirst = i == fromPage; - const isLast = i == toPage - 1; - const leftRoundedClass = isFirst && isCurrent ? 'elsa-rounded-l-md' : ''; - const rightRoundedClass = isLast && isCurrent ? 'elsa-rounded-r-md' : ''; - - if (isCurrent) { - buttons.push( + if (isCurrent) { + buttons.push( {i + 1} ); - } else { - buttons.push( self.onNavigateClick(e)} - class={`-elsa-ml-px elsa-relative elsa-inline-flex elsa-items-center elsa-px-4 elsa-py-2 elsa-border elsa-border-gray-300 elsa-bg-white elsa-text-sm elsa-leading-5 elsa-font-medium elsa-text-gray-700 hover:elsa-text-gray-500 focus:elsa-z-10 focus:elsa-outline-none active:elsa-bg-gray-100 active:elsa-text-gray-700 elsa-transition elsa-ease-in-out elsa-duration-150 ${leftRoundedClass}`}> - {i + 1} - ) - } - } - - return buttons; + } else { + buttons.push( self.onNavigateClick(e, i)} + class={`-elsa-ml-px elsa-relative elsa-inline-flex elsa-items-center elsa-px-4 elsa-py-2 elsa-border elsa-border-gray-300 elsa-bg-white elsa-text-sm elsa-leading-5 elsa-font-medium elsa-text-gray-700 hover:elsa-text-gray-500 focus:elsa-z-10 focus:elsa-outline-none active:elsa-bg-gray-100 active:elsa-text-gray-700 elsa-transition elsa-ease-in-out elsa-duration-150 ${leftRoundedClass}`}> + {i + 1} + ) } + } - return ( -
-
- {renderPreviousButton()} - {renderNextButton()} -
- -
- ); + return buttons; } + + return ( +
+
+ {renderPreviousButton()} + {renderNextButton()} +
+ +
+ ); + } } injectHistory(ElsaPager); \ No newline at end of file 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 f3d7d6424..23bc22af5 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 @@ -1,446 +1,494 @@ -import {Component, h, Prop, State} from '@stencil/core'; +import {Component, h, Prop, State, Watch} from '@stencil/core'; 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, WorkflowInstanceSummary, WorkflowStatus} from "../../../../models"; +import {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"; +import {PagerData} from "../../../controls/elsa-pager/elsa-pager"; @Component({ - tag: 'elsa-workflow-instance-list-screen', - shadow: false, + tag: 'elsa-workflow-instance-list-screen', + shadow: false, }) export class ElsaWorkflowInstanceListScreen { - @Prop() history?: RouterHistory; - @Prop() serverUrl: string; - @State() workflowBlueprints: Array = []; - @State() workflowInstances: PagedList = {items: [], page: 1, pageSize: 50, totalCount: 0}; - @State() selectedWorkflowId?: string; - @State() selectedWorkflowStatus?: WorkflowStatus; - @State() selectedOrderBy?: OrderBy = OrderBy.Started; - @State() selectedWorkflowInstanceIds: Array = []; - @State() selectAllChecked: boolean; - @State() page: number = 0; - @State() pageSize: number = 15; - @State() searchTerm?: string; + @Prop() history?: RouterHistory; + @Prop({attribute: "server-url"}) serverUrl: string; + @Prop() workflowId?: string; + @Prop() workflowStatus?: WorkflowStatus; + @Prop() orderBy?: OrderBy = OrderBy.Started; + @State() workflowBlueprints: Array = []; + @State() workflowInstances: PagedList = {items: [], page: 1, pageSize: 50, totalCount: 0}; + @State() selectedWorkflowId?: string; + @State() selectedWorkflowStatus?: WorkflowStatus; + @State() selectedOrderByState?: OrderBy = OrderBy.Started; + @State() selectedWorkflowInstanceIds: Array = []; + @State() selectAllChecked: boolean; + @State() currentPage: number = 0; + @State() currentPageSize: number = 15; + @State() currentSearchTerm?: string; - confirmDialog: HTMLElsaConfirmDialogElement; + confirmDialog: HTMLElsaConfirmDialogElement; - async componentWillLoad() { - this.history.listen(e => this.routeChanged(e)); - this.applyQueryString(this.history.location.search); + async componentWillLoad() { - await this.loadWorkflowBlueprints(); - await this.loadWorkflowInstances(); + if (!!this.history) { + this.history.listen(e => this.routeChanged(e)); + this.applyQueryString(this.history.location.search); } - applyQueryString(queryString?: string) { - const query = parseQuery(queryString); + await this.loadWorkflowBlueprints(); + await this.loadWorkflowInstances(); + } - this.selectedWorkflowId = query.workflow; - this.selectedWorkflowStatus = query.status; - this.selectedOrderBy = query.orderBy ?? OrderBy.Started; - this.page = !!query.page ? parseInt(query.page) : 0; - this.pageSize = !!query.pageSize ? parseInt(query.pageSize) : 15; + @Watch("workflowId") + async handleWorkflowIdChanged(value: string) { + this.selectedWorkflowId = value; + await this.loadWorkflowInstances(); + } + + @Watch("workflowStatus") + async handleWorkflowStatusChanged(value: WorkflowStatus) { + this.selectedWorkflowStatus = value; + await this.loadWorkflowInstances(); + } + + @Watch("orderBy") + async handleOrderByChanged(value: OrderBy) { + this.selectedOrderByState = value; + await this.loadWorkflowInstances(); + } + + applyQueryString(queryString?: string) { + const query = parseQuery(queryString); + + this.selectedWorkflowId = query.workflow; + this.selectedWorkflowStatus = query.status; + this.selectedOrderByState = query.orderBy ?? OrderBy.Started; + this.currentPage = !!query.page ? parseInt(query.page) : 0; + this.currentPageSize = !!query.pageSize ? parseInt(query.pageSize) : 15; + } + + async loadWorkflowBlueprints() { + const elsaClient = this.createClient(); + const versionOptions: VersionOptions = {allVersions: true}; + const workflowBlueprintPagedList = await elsaClient.workflowRegistryApi.list(null, null, versionOptions); + this.workflowBlueprints = workflowBlueprintPagedList.items; + } + + async loadWorkflowInstances() { + const elsaClient = this.createClient(); + this.workflowInstances = await elsaClient.workflowInstancesApi.list(this.currentPage, this.currentPageSize, this.selectedWorkflowId, this.selectedWorkflowStatus, this.selectedOrderByState, this.currentSearchTerm); + } + + createClient() { + return createElsaClient(this.serverUrl); + } + + getLatestWorkflowBlueprintVersions(): Array { + const groups = collection.groupBy(this.workflowBlueprints, 'id'); + return collection.map(groups, x => array.first(collection.sortBy(x, 'version', 'desc'))); + } + + buildFilterUrl(workflowId?: string, workflowStatus?: WorkflowStatus, orderBy?: OrderBy) { + const filters: Map = {}; + + if (!!workflowId) + filters['workflow'] = workflowId; + + if (!!workflowStatus) + filters['status'] = workflowStatus; + + if (!!orderBy) + filters['orderBy'] = orderBy; + + if (!!this.currentPage) + filters['page'] = this.currentPage.toString(); + + if (!!this.currentPageSize) + filters['pageSize'] = this.currentPageSize.toString(); + + const queryString = collection.map(filters, (v, k) => `${k}=${v}`).join('&'); + return `/workflow-instances?${queryString}`; + } + + getStatusColor(status: WorkflowStatus) { + switch (status) { + default: + case WorkflowStatus.Idle: + return "gray"; + case WorkflowStatus.Running: + return "rose"; + case WorkflowStatus.Suspended: + return "blue"; + case WorkflowStatus.Finished: + return "green"; + case WorkflowStatus.Faulted: + return "red"; + case WorkflowStatus.Cancelled: + return "yellow"; } + } - async loadWorkflowBlueprints() { - const elsaClient = this.createClient(); - const versionOptions: VersionOptions = {allVersions: true}; - const workflowBlueprintPagedList = await elsaClient.workflowRegistryApi.list(null, null, versionOptions); - this.workflowBlueprints = workflowBlueprintPagedList.items; + updateSelectAllChecked() { + this.selectAllChecked = this.workflowInstances.items.findIndex(x => this.selectedWorkflowInstanceIds.findIndex(id => id == x.id) < 0) < 0; + } + + async routeChanged(e: LocationSegments) { + + if (e.pathname.toLowerCase().indexOf('workflow-instances') < 0) + return; + + this.applyQueryString(e.search); + await this.loadWorkflowInstances(); + } + + onSelectAllCheckChange(e: Event) { + const checkBox = e.target as HTMLInputElement; + const isChecked = checkBox.checked; + + this.selectAllChecked = isChecked; + this.selectedWorkflowInstanceIds = []; + + if (isChecked) + this.selectedWorkflowInstanceIds = this.workflowInstances.items.map(x => x.id); + } + + onWorkflowInstanceCheckChange(e: Event, workflowInstance: WorkflowInstanceSummary) { + const checkBox = e.target as HTMLInputElement; + const isChecked = checkBox.checked; + + if (isChecked) + this.selectedWorkflowInstanceIds = [...this.selectedWorkflowInstanceIds, workflowInstance.id]; + else + this.selectedWorkflowInstanceIds = this.selectedWorkflowInstanceIds.filter(x => x != workflowInstance.id); + + this.updateSelectAllChecked(); + } + + async onDeleteClick(e: Event, workflowInstance: WorkflowInstanceSummary) { + const result = await this.confirmDialog.show('Delete Workflow Instance', 'Are you sure you wish to permanently delete this workflow instance?'); + + if (!result) + return; + + const elsaClient = this.createClient(); + await elsaClient.workflowInstancesApi.delete(workflowInstance.id); + await this.loadWorkflowInstances(); + } + + async onBulkDelete() { + const result = await this.confirmDialog.show('Delete Selected Workflow Instances', 'Are you sure you wish to permanently delete all selected workflow instances?'); + + if (!result) + return; + + const elsaClient = this.createClient(); + await elsaClient.workflowInstancesApi.bulkDelete({workflowInstanceIds: this.selectedWorkflowInstanceIds}); + this.selectedWorkflowInstanceIds = []; + this.updateSelectAllChecked(); + await this.loadWorkflowInstances(); + this.currentPage = 0; + } + + async onBulkActionSelected(e: CustomEvent) { + const action = e.detail; + + switch (action.name) { + case 'Delete': + await this.onBulkDelete(); } + } - async loadWorkflowInstances() { - const elsaClient = this.createClient(); - this.workflowInstances = await elsaClient.workflowInstancesApi.list(this.page, this.pageSize, this.selectedWorkflowId, this.selectedWorkflowStatus, this.selectedOrderBy, this.searchTerm); - } + async onSearch(e: Event) { + e.preventDefault(); + const form = e.currentTarget as HTMLFormElement; + const formData = new FormData(form); + const searchTerm: FormDataEntryValue = formData.get('searchTerm'); - createClient() { - return createElsaClient(this.serverUrl); - } + this.currentSearchTerm = searchTerm.toString(); + await this.loadWorkflowInstances(); + } - getLatestWorkflowBlueprintVersions(): Array { - const groups = collection.groupBy(this.workflowBlueprints, 'id'); - return collection.map(groups, x => array.first(collection.sortBy(x, 'version', 'desc'))); - } + onPaged = async (e: CustomEvent) => { + this.currentPage = e.detail.page; + await this.loadWorkflowInstances(); + }; - buildFilterUrl(workflowId?: string, workflowStatus?: WorkflowStatus, orderBy?: OrderBy) { - const filters: Map = {}; + render() { + const workflowInstances = this.workflowInstances.items; + const workflowBlueprints = this.workflowBlueprints; - if (!!workflowId) - filters['workflow'] = workflowId; + const renderViewIcon = function () { + return ( + + + + + ); + }; - if (!!workflowStatus) - filters['status'] = workflowStatus; + const renderDeleteIcon = function () { + return ( + + + + + + + + + ); + }; - if (!!orderBy) - filters['orderBy'] = orderBy; - - if (!!this.page) - filters['page'] = this.page.toString(); - - if (!!this.pageSize) - filters['pageSize'] = this.pageSize.toString(); - - const queryString = collection.map(filters, (v, k) => `${k}=${v}`).join('&'); - return `/workflow-instances?${queryString}`; - } - - getStatusColor(status: WorkflowStatus) { - switch (status) { - default: - case WorkflowStatus.Idle: - return "gray"; - case WorkflowStatus.Running: - return "rose"; - case WorkflowStatus.Suspended: - return "blue"; - case WorkflowStatus.Finished: - return "green"; - case WorkflowStatus.Faulted: - return "red"; - case WorkflowStatus.Cancelled: - return "yellow"; - } - } - - updateSelectAllChecked(){ - this.selectAllChecked = this.workflowInstances.items.findIndex(x => this.selectedWorkflowInstanceIds.findIndex(id => id == x.id) < 0) < 0; - } - - async routeChanged(e: LocationSegments) { - - if (e.pathname.toLowerCase().indexOf('workflow-instances') < 0) - return; - - this.applyQueryString(e.search); - await this.loadWorkflowInstances(); - } - - onSelectAllCheckChange(e: Event) { - const checkBox = e.target as HTMLInputElement; - const isChecked = checkBox.checked; - - this.selectAllChecked = isChecked; - this.selectedWorkflowInstanceIds = []; - - if (isChecked) - this.selectedWorkflowInstanceIds = this.workflowInstances.items.map(x => x.id); - } - - onWorkflowInstanceCheckChange(e: Event, workflowInstance: WorkflowInstanceSummary) { - const checkBox = e.target as HTMLInputElement; - const isChecked = checkBox.checked; - - if (isChecked) - this.selectedWorkflowInstanceIds = [...this.selectedWorkflowInstanceIds, workflowInstance.id]; - else - this.selectedWorkflowInstanceIds = this.selectedWorkflowInstanceIds.filter(x => x != workflowInstance.id); - - this.updateSelectAllChecked(); - } - - async onDeleteClick(e: Event, workflowInstance: WorkflowInstanceSummary) { - const result = await this.confirmDialog.show('Delete Workflow Instance', 'Are you sure you wish to permanently delete this workflow instance?'); - - if (!result) - return; - - const elsaClient = this.createClient(); - await elsaClient.workflowInstancesApi.delete(workflowInstance.id); - await this.loadWorkflowInstances(); - } - - async onBulkDelete() { - const result = await this.confirmDialog.show('Delete Selected Workflow Instances', 'Are you sure you wish to permanently delete all selected workflow instances?'); - - if (!result) - return; - - const elsaClient = this.createClient(); - await elsaClient.workflowInstancesApi.bulkDelete({workflowInstanceIds: this.selectedWorkflowInstanceIds}); - this.selectedWorkflowInstanceIds = []; - this.updateSelectAllChecked(); - await this.loadWorkflowInstances(); - this.page = 0; - } - - async onBulkActionSelected(e: CustomEvent) { - const action = e.detail; - - switch (action.name) { - case 'Delete': - await this.onBulkDelete(); - } - } - - async onSearch(e: Event) { - e.preventDefault(); - const form = e.currentTarget as HTMLFormElement; - const formData = new FormData(form); - const searchTerm: FormDataEntryValue = formData.get('searchTerm'); - - this.searchTerm = searchTerm.toString(); - await this.loadWorkflowInstances(); - } - - render() { - const workflowInstances = this.workflowInstances.items; - const workflowBlueprints = this.workflowBlueprints; - - const renderViewIcon = function () { - return ( - - - - - ); - }; - - const renderDeleteIcon = function () { - return ( - - - - - - - - - ); - }; - - return ( -
-
-
-
-
this.onSearch(e)}> - -
-
- - - -
- -
-
-
-
-
- -
-
- {this.renderBulkActions()} -
-
-   -
- {this.renderWorkflowFilter()} - {this.renderStatusFilter()} - {this.renderOrderByFilter()} -
- -
-
- - - - - - - - - - - - - - - - - - {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 displayName = workflowBlueprint.displayName || workflowBlueprint.name || 'Untitled'; - const statusColor = this.getStatusColor(workflowInstance.workflowStatus); - const viewUrl = `/workflow-instances/${workflowInstance.id}`; - const instanceName = !workflowInstance.name ? '' : workflowInstance.name; - const isSelected = this.selectedWorkflowInstanceIds.findIndex(x => x === workflowInstance.id) >= 0; - const createdAt = moment(workflowInstance.createdAt); - const finishedAt = !!workflowInstance.finishedAt ? moment(workflowInstance.finishedAt) : null; - const lastExecutedAt = !!workflowInstance.lastExecutedAt ? moment(workflowInstance.lastExecutedAt) : null; - const faultedAt = !!workflowInstance.faultedAt ? moment(workflowInstance.faultedAt) : null; - - return - - - - - - - - - - - - - - })} - -
- 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"/> - - ID - - Workflow - - Instance Name - - Status - - Created - - Finished - - Last Executed - - Faulted - -
- 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} - - - {displayName} - - - {instanceName} - - this.onDeleteClick(e, workflowInstance), icon: renderDeleteIcon()} - ]}/> -
- -
- this.confirmDialog = el}/> + return ( +
+
+
+
+
this.onSearch(e)}> + +
+
+ + + +
+
+
- ); - } +
+
- renderBulkActions() { - const bulkActionIcon = - - ; +
+
+ {this.renderBulkActions()} +
+
+   +
+ {this.renderWorkflowFilter()} + {this.renderStatusFilter()} + {this.renderOrderByFilter()} +
- const actions: Array = [{ - text: 'Delete', - name: 'Delete', - }]; +
+
+ + + + + + + + + + + + + + + + + + {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 displayName = workflowBlueprint.displayName || workflowBlueprint.name || 'Untitled'; + const statusColor = this.getStatusColor(workflowInstance.workflowStatus); + const viewUrl = `/workflow-instances/${workflowInstance.id}`; + const instanceName = !workflowInstance.name ? '' : workflowInstance.name; + const isSelected = this.selectedWorkflowInstanceIds.findIndex(x => x === workflowInstance.id) >= 0; + const createdAt = moment(workflowInstance.createdAt); + const finishedAt = !!workflowInstance.finishedAt ? moment(workflowInstance.finishedAt) : null; + const lastExecutedAt = !!workflowInstance.lastExecutedAt ? moment(workflowInstance.lastExecutedAt) : null; + const faultedAt = !!workflowInstance.faultedAt ? moment(workflowInstance.faultedAt) : null; - return this.onBulkActionSelected(e)}/> - } + return + + + + + + + + + + + + + + })} + +
+ 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"/> + + ID + + Workflow + + Instance Name + + Status + + Created + + Finished + + Last Executed + + Faulted + +
+ 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} + + + {displayName} + + + {instanceName} + + this.onDeleteClick(e, workflowInstance), icon: renderDeleteIcon()} + ]}/> +
+ +
+ this.confirmDialog = el}/> +
+
+ ); + } - renderWorkflowFilter() { - const latestWorkflowBlueprints = this.getLatestWorkflowBlueprintVersions(); - const selectedWorkflowId = this.selectedWorkflowId; - const selectedWorkflow = latestWorkflowBlueprints.find(x => x.id == selectedWorkflowId); - const selectedWorkflowText = !selectedWorkflowId ? "Workflow" : !!selectedWorkflow ? (selectedWorkflow.displayName || selectedWorkflow.name) : "Workflow"; - const selectedWorkflowStatus = this.selectedWorkflowStatus; - const SelectedOrderBy = this.selectedOrderBy; - - let items: Array = latestWorkflowBlueprints.map(x => { - const displayName = !!x.displayName && x.displayName.length > 0 ? x.displayName : x.name || 'Untitled'; - - return ({ - text: displayName, - value: x.id, - url: this.buildFilterUrl(x.id, selectedWorkflowStatus, SelectedOrderBy), isSelected: x.id == selectedWorkflowId - }); - }); + renderBulkActions() { + const bulkActionIcon = + + ; - items = [{text: 'All', value: null, url: this.buildFilterUrl(null, selectedWorkflowStatus, SelectedOrderBy), isSelected: !selectedWorkflowId}, ...items]; + const actions: Array = [{ + text: 'Delete', + name: 'Delete', + }]; - const renderIcon = function () { - return - - - - - - ; - }; + return this.onBulkActionSelected(e)}/> + } - return this.selectedWorkflowId = e.detail.value as string}/> - } + renderWorkflowFilter() { + const latestWorkflowBlueprints = this.getLatestWorkflowBlueprintVersions(); + const selectedWorkflowId = this.selectedWorkflowId; + const selectedWorkflow = latestWorkflowBlueprints.find(x => x.id == selectedWorkflowId); + const selectedWorkflowText = !selectedWorkflowId ? "Workflow" : !!selectedWorkflow && (selectedWorkflow.name || selectedWorkflow.displayName) ? (selectedWorkflow.displayName || selectedWorkflow.name) : "Untitled"; + const selectedWorkflowStatus = this.selectedWorkflowStatus; + const selectedOrderBy = this.selectedOrderByState; + const history = this.history; - renderStatusFilter() { - const selectedWorkflowStatus = this.selectedWorkflowStatus; - const selectedWorkflowStatusText = !!selectedWorkflowStatus ? selectedWorkflowStatus : 'Status'; - const statuses: Array = [null, WorkflowStatus.Running, WorkflowStatus.Suspended, WorkflowStatus.Finished, WorkflowStatus.Faulted, WorkflowStatus.Cancelled, WorkflowStatus.Idle]; + let items: Array = latestWorkflowBlueprints.map(x => { + const displayName = !!x.displayName && x.displayName.length > 0 ? x.displayName : x.name || 'Untitled'; + const item: DropdownButtonItem = {text: displayName, value: x.id, isSelected: x.id == selectedWorkflowId}; - const items: Array = statuses.map(x => { - const text = x ?? 'All'; - return ({text: text, url: this.buildFilterUrl(this.selectedWorkflowId, x, this.selectedOrderBy), isSelected: x == selectedWorkflowStatus}); - }); + if (!!history) + item.url = this.buildFilterUrl(x.id, selectedWorkflowStatus, selectedOrderBy); - const renderIcon = function () { - return - - - - }; + return item; + }); - return - } + const allItem: DropdownButtonItem = {text: 'All', value: null, isSelected: !selectedWorkflowId}; - renderOrderByFilter() { - const selectedOrderBy = this.selectedOrderBy; - const selectedOrderByText = !!selectedOrderBy ? `Sort by: ${selectedOrderBy}` : 'Sort'; - const orderByValues: Array = [OrderBy.Finished, OrderBy.LastExecuted, OrderBy.Started]; + if (!!history) + allItem.url = this.buildFilterUrl(null, selectedWorkflowStatus, selectedOrderBy); - const items: Array = orderByValues.map(x => { - return ({text: x, url: this.buildFilterUrl(this.selectedWorkflowId, this.selectedWorkflowStatus, x), isSelected: x == selectedOrderBy}); - }); + items = [allItem, ...items]; - const renderIcon = function () { - return - - - }; + const renderIcon = function () { + return + + + + + + ; + }; - return - } + return this.handleWorkflowIdChanged(e.detail.value)}/> + } + + renderStatusFilter() { + const selectedWorkflowStatus = this.selectedWorkflowStatus; + const selectedWorkflowStatusText = !!selectedWorkflowStatus ? selectedWorkflowStatus : 'Status'; + const statuses: Array = [null, WorkflowStatus.Running, WorkflowStatus.Suspended, WorkflowStatus.Finished, WorkflowStatus.Faulted, WorkflowStatus.Cancelled, WorkflowStatus.Idle]; + const history = this.history; + + const items: Array = statuses.map(x => { + const text = x ?? 'All'; + const item: DropdownButtonItem = {text: text, isSelected: x == selectedWorkflowStatus, value: x}; + + if (!!history) + item.url = this.buildFilterUrl(this.selectedWorkflowId, x, this.selectedOrderByState); + + return item; + }); + + const renderIcon = function () { + return + + + + }; + + return this.handleWorkflowStatusChanged(e.detail.value)}/> + } + + renderOrderByFilter() { + const selectedOrderBy = this.selectedOrderByState; + const selectedOrderByText = !!selectedOrderBy ? `Sort by: ${selectedOrderBy}` : 'Sort'; + const orderByValues: Array = [OrderBy.Finished, OrderBy.LastExecuted, OrderBy.Started]; + const history = this.history; + + const items: Array = orderByValues.map(x => { + const item: DropdownButtonItem = {text: x, value: x, isSelected: x == selectedOrderBy}; + + if (!!history) + item.url = this.buildFilterUrl(this.selectedWorkflowId, this.selectedWorkflowStatus, x); + + return item; + }); + + const renderIcon = function () { + return + + + }; + + return this.handleOrderByChanged(e.detail.value)}/> + } } diff --git a/src/designer/elsa-workflows-studio/src/index.html b/src/designer/elsa-workflows-studio/src/index.html index 3c7220ea4..a56b287ed 100644 --- a/src/designer/elsa-workflows-studio/src/index.html +++ b/src/designer/elsa-workflows-studio/src/index.html @@ -16,32 +16,33 @@ +