import './instance-route-history.scss' ; import React , { useEffect , useState } from 'react' ; import ReactDOM from 'react-dom' ; import { Expression , GridDataAutoCompleteHandler } from 'react-filter-box' ; import SplitPane from 'react-split-pane' ; import { Tab , TabList , TabPanel , Tabs } from 'react-tabs' ; import AuditLogTable from './Components/AuditLogTable' ; import BPMN from './Components/BPMN' ; import BreadcrumbsPanel from './Components/BreadcrumbsPanel' ; import { Clippy } from './Components/Clippy' ; import Container from './Components/Container' ; import FilterBox from './Components/FilterBox' ; import HistoryTable from './Components/HistoryTable' ; import Page from './Components/Page' ; import Portal from './Components/Portal' ; import { ToggleHistoryViewButton } from './Components/ToggleHistoryViewButton' ; import VariablesTable from './Components/VariablesTable' ; import { DefinitionPluginParams , RoutePluginParams } from './types' ; import { get , post } from './utils/api' ; class InstanceQueryAutoCompleteHandler extends GridDataAutoCompleteHandler { query = '' ; setQuery ( query ) { this . query = query ; } hasCategory ( category ) { return true ; } needCategories ( ) { return super . needCategories ( ) . filter ( ( value ) => ! ( [ 'key' , 'started' , 'finished' ] . includes ( value ) && this . query . includes ( value ) ) ) ; } needOperators ( parsedCategory ) { if ( parsedCategory === 'started' ) { return [ 'after' ] ; } if ( parsedCategory === 'finished' ) { return [ 'before' ] ; } if ( parsedCategory === 'maxResults' ) { return [ 'is' ] ; } if ( parsedCategory === 'key' ) { return [ '==' , 'like' ] ; } return [ '==' , 'like' , 'ilike' ] ; } needValues ( parsedCategory , parsedOperator ) { if ( parsedOperator === 'after' || parsedOperator === 'before' ) { return [ { customType : 'date' } ] ; } return super . needValues ( parsedCategory , parsedOperator ) ; } } const InstanceQueryOptions = [ { columnField : 'started' , type : 'date' , } , { columnField : 'finished' , type : 'date' , } , { columnField : 'maxResults' , type : 'text' , } , { columnField : 'key' , type : 'string' , } , ] ; const initialState = { historyTabNode : null , } ; const hooks = { setHistoryTabNode : ( node ) => ( initialState . historyTabNode = node ) , } ; const Plugin = ( { root , api , processDefinitionId } ) => { const [ autoCompleteHandler ] = useState ( new InstanceQueryAutoCompleteHandler ( [ ] , InstanceQueryOptions ) ) ; const [ expressions , setExpressions ] = useState ( [ ] as Expression [ ] ) ; const [ query , setQuery ] = useState ( { } as Record < string , string | null > ) ; const [ historyTabNode , setHistoryTabNode ] = useState ( initialState . historyTabNode ) ; hooks . setHistoryTabNode = setHistoryTabNode ; const [ instances , setInstances ] = useState ( [ ] as any [ ] ) ; useEffect ( ( ) => { ( async ( ) => { const maxResults = ! isNaN ( parseInt ( `${ query . maxResults }` , 10 ) ) ? parseInt ( `${ query . maxResults }` , 10 ) : 1000 ; setInstances ( await post ( api , '/history/process-instance' , { maxResults : `${ maxResults }` } , JSON . stringify ( { sortBy : 'endTime' , sortOrder : 'desc' , processDefinitionId , ... query , } ) ) ) ; } ) ( ) ; } , [ query ] ) ; useEffect ( ( ) => { const query = { } ; const variables = [ ] ; for ( const { category , operator , value } of expressions ) { if ( category === 'started' && operator === 'after' && ! isNaN ( new Date ( `${ value }` ) . getTime ( ) ) ) { query [ 'startedAfter' ] = `${ value }T00:00:00.000+0000` ; } else if ( category === 'finished' && operator === 'before' && ! isNaN ( new Date ( `${ value }` ) . getTime ( ) ) ) { query [ 'finishedBefore' ] = `${ value }T00:00:00.000+0000` ; } else if ( category === 'maxResults' && operator == 'is' && ! isNaN ( parseInt ( `${ value }` , 10 ) ) ) { query [ 'maxResults' ] = parseInt ( `${ value }` , 10 ) ; } else if ( category === 'key' && operator === '==' ) { query . processInstanceBusinessKey = value ; } else if ( category === 'key' && operator === 'like' ) { query . processInstanceBusinessKeyLike = value ; } else if ( operator === '==' ) { variables . push ( { name : category , operator : 'eq' , value : value , } ) ; } else if ( operator === 'like' || operator === 'ilike' ) { variables . push ( { name : category , operator : 'like' , value : value , } ) ; } if ( operator === 'ilike' ) { query . variableNamesIgnoreCase = true ; query . variableValuesIgnoreCase = true ; } } if ( variables . length ) { query [ 'variables' ] = variables ; } setQuery ( query ) ; } , [ expressions ] ) ; if ( historyTabNode && ! Array . from ( historyTabNode . children ) . includes ( root ) ) { historyTabNode . appendChild ( root ) ; } return historyTabNode ? ( < Portal node = { root } > < FilterBox options = { InstanceQueryOptions } autoCompleteHandler = { autoCompleteHandler } onParseOk = { setExpressions } defaultQuery = { ( ) : string => '' } /> { instances . length ? < HistoryTable instances = { instances } /> : null} < /Portal>) : null; } ; export default [ { id : 'definitionTabHistoricInstances' , pluginPoint : 'cockpit.processDefinition.runtime.tab' , properties : { label : 'History' , } , render : ( node ) => hooks . setHistoryTabNode ( node ) , } , { id : 'definitionHistoricInstancesPlugin' , pluginPoint : 'cockpit.processDefinition.runtime.action' , render : ( node , { api , processDefinitionId } ) => { ReactDOM . render ( < React . StrictMode > < Plugin root = { node } api = { api } processDefinitionId = { processDefinitionId } / > < /React.StrictMode>, node); } , } , { id : 'instanceDiagramHistoricToggle' , pluginPoint : 'cockpit.processInstance.diagram.plugin' , render : ( viewer ) => { ( async ( ) => { const buttons = document . createElement ( 'div' ) ; buttons . style . cssText = `
          position: absolute;
          right: 15px;
          top: 60px;
        ` ; viewer . _container . appendChild ( buttons ) ; ReactDOM . render ( < React . StrictMode > < ToggleHistoryViewButton onToggleHistoryView = { ( value ) => { if ( value ) { window . location . href = window . location . href . split ( '#' ) [ 0 ] + window . location . hash . split ( '?' ) [ 0 ] . replace ( /^#\/process-instance/ , '#/history/process-instance' ) . replace ( /\/runtime/ , '/' ) ; } } } initial = { false } / > < /React.StrictMode>, buttons); } ) ( ) ; } , } , { id : 'instanceRouteHistory' , pluginPoint : 'cockpit.route' , properties : { path : '/history/process-instance/:id' , label : '/history' , } , render : ( node , { api } ) => { const hash = window ?. location ?. hash ?? '' ; const match = hash . match ( /\/history\/process-instance\/([^\/]*)/ ) ; const processInstanceId = match ? match [ 1 ] . split ( '?' ) [ 0 ] : null ; if ( processInstanceId ) { ( async ( ) => { const instance = await get ( api , `/history/process-instance/${ processInstanceId }` ) ; const [ { version } , diagram , activities , variables , decisions ] = await Promise . all ( [ get ( api , `/version` ) , get ( api , `/process-definition/${ instance . processDefinitionId }/xml` ) , get ( api , '/history/activity-instance' , { processInstanceId } ) , get ( api , '/history/variable-instance' , { processInstanceId } ) , get ( api , '/history/decision-instance' , { processInstanceId } ) , ] ) ; const decisionByActivity = new Map ( decisions . map ( ( decision ) => [ decision . activityInstanceId , decision . id ] ) ) ; const activityById = new Map ( activities . map ( ( activity ) => [ activity . id , activity ] ) ) ; activities . sort ( ( a , b ) => { a = a . endTime ? new Date ( a . endTime ) : new Date ( ) ; b = b . endTime ? new Date ( b . endTime ) : new Date ( ) ; if ( a > b ) { return - 1 ; } if ( a < b ) { return 1 ; } return 0 ; } ) ; variables . sort ( ( a , b ) => { a = a . name ; b = b . name ; if ( a > b ) { return 1 ; } if ( a < b ) { return - 1 ; } return 0 ; } ) ; ReactDOM . render ( < React . StrictMode > < Page version = { version ? ( version as string ) : '7.15.0' } api = { api } > < BreadcrumbsPanel processDefinitionId = { instance . processDefinitionId } processDefinitionName = { instance . processDefinitionName } processInstanceId = { processInstanceId } / > < Container > < SplitPane split = "vertical" size = { 200 } > < div className = "ctn-column" > < dl className = "process-information" > < dt > < Clippy value = { instance . id } > Instance ID : < /Clippy> < /dt> < dd > { instance . id } < /dd> < dt > < Clippy value = { instance . businessKey || 'null' } > Business Key : < /Clippy> < /dt> < dd > { instance . businessKey || 'null' } < /dd> < dt > < Clippy value = { instance . processDefinitionVersion } > Definition Version : < /Clippy> < /dt> < dd > { instance . processDefinitionVersion } < /dd> < dt > < Clippy value = { instance . processdefinitionid } > Definition ID : < /Clippy> < /dt> < dd > { instance . processDefinitionId } < /dd> < dt > < Clippy value = { instance . processDefinitionKey } > Definition Key : < /Clippy> < /dt> < dd > { instance . processDefinitionKey } < /dd> < dt > < Clippy value = { instance . processDefinitionName } > Definition Name : < /Clippy> < /dt> < dd > { instance . processDefinitionName } < /dd> < dt > < Clippy value = { instance . tenantId || 'null' } > Tenant ID : < /Clippy> < /dt> < dd > { instance . tenantId || 'null' } < /dd> < dt > < Clippy value = { instance . superProcessInstanceId } > Super Process instance ID : < /Clippy> < /dt> < dd > { ( instance . superProcessInstanceId && ( < a href = { `#/history/process-instance/${ instance . superProcessInstanceId }` } > { instance . superProcessInstanceId } < /a>)) || 'null' } < /dd> < dt > < Clippy value = { instance . state } > State < /Clippy> < /dt> < dd > { instance . state } < /dd> < /dl> < /div> < SplitPane split = "horizontal" size = { 300 } > < BPMN activities = { activities } diagramXML = { diagram . bpmn20Xml } className = "ctn-content" style = { { width : '100%' } } showRuntimeToggle = { instance . state === 'ACTIVE' } / > < Tabs className = "ctn-row ctn-content-bottom ctn-tabbed" selectedTabClassName = "active" > < TabList className = "nav nav-tabs" > < Tab > < a > Audit Log < / a > < /Tab> < Tab > < a > Variables < /a> < /Tab> < /TabList> < TabPanel className = "ctn-tabbed-content ctn-scroll" > < AuditLogTable activities = { activities } decisions = { decisionByActivity } / > < /TabPanel> < TabPanel className = "ctn-tabbed-content ctn-scroll" > < VariablesTable instance = { instance } activities = { activityById } variables = { variables } / > < /TabPanel> < /Tabs> < /SplitPane> < /SplitPane> < /Container> < /Page> < /React.StrictMode>, node); } ) ( ) ; } } , } , ] ;