diff --git a/packages/runtime-tools-swf-enveloped-components/src/workflowDetails/envelope/components/WorkflowDetails/WorkflowDetails.tsx b/packages/runtime-tools-swf-enveloped-components/src/workflowDetails/envelope/components/WorkflowDetails/WorkflowDetails.tsx index 5f2ea170b45..a0fdb9a1fc0 100644 --- a/packages/runtime-tools-swf-enveloped-components/src/workflowDetails/envelope/components/WorkflowDetails/WorkflowDetails.tsx +++ b/packages/runtime-tools-swf-enveloped-components/src/workflowDetails/envelope/components/WorkflowDetails/WorkflowDetails.tsx @@ -17,7 +17,7 @@ * under the License. */ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useState, useCallback } from "react"; import { Flex, FlexItem } from "@patternfly/react-core/dist/js/layouts/Flex"; import { Grid, GridItem } from "@patternfly/react-core/dist/js/layouts/Grid"; import { Split, SplitItem } from "@patternfly/react-core/dist/js/layouts/Split"; @@ -48,9 +48,15 @@ import WorkflowVariables from "../WorkflowVariables/WorkflowVariables"; import WorkflowDetailsMilestonesPanel from "../WorkflowDetailsMilestonesPanel/WorkflowDetailsMilestonesPanel"; import WorkflowDetailsTimelinePanel from "../WorkflowDetailsTimelinePanel/WorkflowDetailsTimelinePanel"; import SwfCombinedEditor from "../SwfCombinedEditor/SwfCombinedEditor"; -import { Job, WorkflowInstance, WorkflowInstanceState } from "@kie-tools/runtime-tools-swf-gateway-api/dist/types"; +import { + Job, + JobStatus, + WorkflowInstance, + WorkflowInstanceState, +} from "@kie-tools/runtime-tools-swf-gateway-api/dist/types"; const SWFCOMBINEDEDITOR_WIDTH = 1000; +const CHECK_EXPIRED_JOBS_TIMEOUT = 5000; interface WorkflowDetailsProps { isEnvelopeConnectedToChannel: boolean; @@ -78,7 +84,7 @@ const WorkflowDetails: React.FC = ({ isEnvelopeConnectedTo try { const workflowResponse: WorkflowInstance = await driver.workflowDetailsQuery(workflowDetails.id); workflowResponse && setData(workflowResponse); - getAllJobs(); + loadJobs(); setIsLoading(false); } catch (errorString) { setError(errorString); @@ -86,10 +92,36 @@ const WorkflowDetails: React.FC = ({ isEnvelopeConnectedTo } }; - const getAllJobs = async (): Promise => { + const loadJobs = useCallback(async () => { const jobsResponse: Job[] = await driver.jobsQuery(workflowDetails.id); jobsResponse && setJobs(jobsResponse); - }; + }, [workflowDetails.id, driver]); + + /** + * check every N seconds for jobs which are SCHEDULED and epired + * @return + */ + const checkExpiredJobs = useCallback(async () => { + await new Promise((resolve) => setTimeout(resolve, CHECK_EXPIRED_JOBS_TIMEOUT)); + const scheduledJobs = jobs.filter((job) => job.status === JobStatus.Scheduled); + + if (!scheduledJobs.length) { + return; + } + + const expiredJob = scheduledJobs.find((job) => new Date(job.expirationTime) < new Date()); + + if (expiredJob) { + loadJobs(); + return; + } + + checkExpiredJobs(); + }, [loadJobs, jobs]); + + useEffect(() => { + jobs.length && checkExpiredJobs(); + }, [jobs, checkExpiredJobs]); useEffect(() => { const getVariableJSON = (): void => { @@ -101,7 +133,7 @@ const WorkflowDetails: React.FC = ({ isEnvelopeConnectedTo if (isEnvelopeConnectedToChannel) { getVariableJSON(); } - }, [data]); + }, [data, isEnvelopeConnectedToChannel, workflowDetails.id]); useEffect(() => { if (variableError && variableError.length > 0) { @@ -109,12 +141,16 @@ const WorkflowDetails: React.FC = ({ isEnvelopeConnectedTo } }, [variableError]); - useEffect(() => { - if (isEnvelopeConnectedToChannel) { - setData(workflowDetails); - getAllJobs(); - } - }, [isEnvelopeConnectedToChannel]); + useEffect( + () => { + if (isEnvelopeConnectedToChannel) { + setData(workflowDetails); + loadJobs(); + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [isEnvelopeConnectedToChannel] + ); const handleSave = (): void => { driver diff --git a/packages/runtime-tools-swf-enveloped-components/tests/components/WorkflowDetails.test.tsx b/packages/runtime-tools-swf-enveloped-components/tests/components/WorkflowDetails.test.tsx index f54261b947a..1b126ca33ca 100644 --- a/packages/runtime-tools-swf-enveloped-components/tests/components/WorkflowDetails.test.tsx +++ b/packages/runtime-tools-swf-enveloped-components/tests/components/WorkflowDetails.test.tsx @@ -18,13 +18,22 @@ */ import * as React from "react"; -import { render } from "@testing-library/react"; +import { render, waitFor } from "@testing-library/react"; import "@testing-library/jest-dom"; import WorkflowDetails from "@kie-tools/runtime-tools-swf-enveloped-components/dist/workflowDetails/envelope/components/WorkflowDetails/WorkflowDetails"; -import { WorkflowInstance, WorkflowInstanceState } from "@kie-tools/runtime-tools-swf-gateway-api/dist/types"; +import { + Job, + JobStatus, + WorkflowInstance, + WorkflowInstanceState, +} from "@kie-tools/runtime-tools-swf-gateway-api/dist/types"; + +jest.useFakeTimers(); const mockDriver = { - jobsQuery: jest.fn(), + jobsQuery: jest.fn((_id: string) => { + return [{ ...sampleJob, expirationTime: new Date(Date.now() + 10000).toISOString() }]; + }), }; const sampleWorkflowDetails: WorkflowInstance = { @@ -43,6 +52,25 @@ const sampleWorkflowDetails: WorkflowInstance = { nodes: [], }; +const sampleJob: Job = { + id: "a62d9d0a-87ea-4c13-87fb-67965d133020", + priority: 0, + lastUpdate: new Date("2024-10-30T15:31:46.709Z"), + workflowId: sampleWorkflowDetails.processId, + workflowInstanceId: sampleWorkflowDetails.id, + status: JobStatus.Scheduled, + expirationTime: new Date("2024-10-30T15:31:46.709Z"), + callbackEndpoint: + "http://localhost:4000/management/jobs/callback_state_timeouts/instances/9750c042-3fb2-40b7-96ba-ff10b6178c58/timers/-1", + repeatInterval: 0, + repeatLimit: 0, + scheduledId: "143", + retries: 0, + endpoint: "http://localhost:4000/jobs", + nodeInstanceId: "ee6c3f6e-8bc3-43dc-a249-dad1b19b52bb", + executionCounter: 0, +}; + describe("WorkflowDetails component", () => { beforeEach(() => { jest.clearAllMocks(); @@ -86,4 +114,51 @@ describe("WorkflowDetails component", () => { } } ); + + test("should render the job correctly", async () => { + const component = render( + + ); + + await waitFor(() => expect(mockDriver.jobsQuery).toHaveBeenCalledWith(sampleWorkflowDetails.id)); + + expect(component.queryByText("Jobs")).toBeInTheDocument(); + expect(component.queryByText("Scheduled")).toBeInTheDocument(); + expect(component.queryByText(sampleJob.id.slice(0, 7))).toBeInTheDocument(); + }); + + test("should update sampleJob status to EXECUTED after 30 seconds", async () => { + const component = render( + + ); + + await waitFor(() => expect(mockDriver.jobsQuery).toHaveBeenCalledWith(sampleWorkflowDetails.id)); + + expect(component.queryByText("Jobs")).toBeInTheDocument(); + expect(component.queryByText("Scheduled")).toBeInTheDocument(); + expect(component.queryByText(sampleJob.id.slice(0, 7))).toBeInTheDocument(); + + jest.advanceTimersByTime(10000); + + await waitFor(() => { + expect(component.queryByText("Scheduled")).toBeInTheDocument(); + }); + + mockDriver.jobsQuery.mockReturnValue([ + { ...sampleJob, expirationTime: new Date("2023-10-30T15:31:46.709Z").toISOString(), status: JobStatus.Executed }, + ]); + jest.advanceTimersByTime(20000); + + await waitFor(() => { + expect(component.queryByText("Executed")).toBeInTheDocument(); + }); + }); }); diff --git a/packages/sonataflow-dev-app/README.md b/packages/sonataflow-dev-app/README.md index 6c734a6da97..1a60602207f 100644 --- a/packages/sonataflow-dev-app/README.md +++ b/packages/sonataflow-dev-app/README.md @@ -29,6 +29,26 @@ To run the development app, use the following command: `pnpm start` +### GraphQL Modifications + +This section covers modifications to the GraphQL database. + +## Changing Job Status to Executed + +To update a job's status to `"EXECUTED"`, use the following `curl` command. Replace `{JOB_ID}` with the actual ID of the job you want to update. + +```bash +curl -X POST http://localhost:4000/graphql \ + -H "Content-Type: application/json" \ + -d '{ + "query": "mutation JobExecute($id: String!) { JobExecute(id: $id) }", + "variables": { + "id": "{JOB_ID}" + } + }' + +``` + --- Apache KIE (incubating) is an effort undergoing incubation at The Apache Software diff --git a/packages/sonataflow-dev-app/src/MockData/graphql.js b/packages/sonataflow-dev-app/src/MockData/graphql.js index 33f4f6fd014..42d2b323399 100644 --- a/packages/sonataflow-dev-app/src/MockData/graphql.js +++ b/packages/sonataflow-dev-app/src/MockData/graphql.js @@ -18,6 +18,171 @@ */ module.exports = { ProcessInstanceData: [ + { + id: "9750c042-3fb2-40b7-96ba-ff10b6178c58", + processId: "callback_state_timeouts", + processName: "Callback State Timeouts Example", + businessKey: null, + parentProcessInstanceId: null, + parentProcessInstance: null, + roles: [], + variables: null, + state: "ACTIVE", + start: "2024-10-30T15:31:46.571Z", + lastUpdate: "2024-10-30T15:31:46.571Z", + end: null, + addons: [ + "kubernetes", + "microprofile-config-service-catalog", + "process-management", + "source-files", + "cloudevents", + "knative-eventing", + "knative-serving", + "jobs-knative-eventing", + "jobs-management", + ], + endpoint: "http://localhost:4000/callback_state_timeouts", + serviceUrl: "http://localhost:4000", + source: + '{\n "id": "callback_state_timeouts",\n "version": "1.0",\n "name": "Callback State Timeouts Example",\n "description": "Simple process to show the callback state timeout working",\n "start": "PrintStartMessage",\n "events": [\n {\n "name": "callbackEvent",\n "source": "",\n "type": "callback_event_type"\n }\n ],\n "functions": [\n {\n "name": "systemOut",\n "type": "custom",\n "operation": "sysout"\n }\n ],\n "states": [\n {\n "name": "PrintStartMessage",\n "type": "operation",\n "actions": [\n {\n "name": "printSystemOut",\n "functionRef": {\n "refName": "systemOut",\n "arguments": {\n "message": "${\\"callback-state-timeouts: \\" + $WORKFLOW.instanceId + \\" has started.\\"}"\n }\n }\n }\n ],\n "transition": "CallbackState"\n },\n {\n "name": "CallbackState",\n "type": "callback",\n "action": {\n "name": "callbackAction",\n "functionRef": {\n "refName": "systemOut",\n "arguments": {\n "message": "${\\"callback-state-timeouts: \\" + $WORKFLOW.instanceId + \\" has executed the callbackFunction.\\"}"\n }\n }\n },\n "eventRef": "callbackEvent",\n "transition": "CheckEventArrival",\n "timeouts": {\n "eventTimeout": "PT30S"\n }\n },\n {\n "name": "CheckEventArrival",\n "type": "switch",\n "dataConditions": [\n {\n "condition": "${ .eventData != null }",\n "transition": "EventArrived"\n }\n ],\n "defaultCondition": {\n "transition": "EventNotArrived"\n }\n },\n {\n "name": "EventArrived",\n "type": "inject",\n "data": {\n "exitMessage": "The callback event has arrived."\n },\n "transition": "PrintExitMessage"\n },\n {\n "name": "EventNotArrived",\n "type": "inject",\n "data": {\n "exitMessage": "The callback event has not arrived, and the timeout has overdue."\n },\n "transition": "PrintExitMessage"\n },\n {\n "name": "PrintExitMessage",\n "type": "operation",\n "actions": [\n {\n "name": "printSystemOut",\n "functionRef": {\n "refName": "systemOut",\n "arguments": {\n "message": "${\\"callback-state-timeouts: \\" + $WORKFLOW.instanceId + \\" has finalized. \\" + .exitMessage + \\" eventData: \\" + .eventData}"\n }\n }\n }\n ],\n "end": true\n }\n ]\n}', + error: null, + childProcessInstances: [], + nodes: [ + { + id: "fe78615e-9aa0-4d08-9a3f-b67dbfcf5d9d", + nodeId: "9", + name: "CallbackState", + enter: "2024-10-30T15:31:46.566Z", + exit: null, + type: "CompositeContextNode", + definitionId: "9", + __typename: "NodeInstance", + }, + { + id: "7aaa5b45-ec4d-4267-8b8a-503cfc3e286b", + nodeId: "19", + name: "TimerNode_19", + enter: "2024-10-30T15:31:46.568Z", + exit: null, + type: "TimerNode", + definitionId: "19", + __typename: "NodeInstance", + }, + { + id: "e73715b4-d9de-49e8-b367-835e6fff3f53", + nodeId: "15", + name: "callbackEvent", + enter: "2024-10-30T15:31:46.569Z", + exit: null, + type: "EventNode", + definitionId: "15", + __typename: "NodeInstance", + }, + { + id: "ee6c3f6e-8bc3-43dc-a249-dad1b19b52bb", + nodeId: "17", + name: "EventSplit_17", + enter: "2024-10-30T15:31:46.567Z", + exit: "2024-10-30T15:31:46.57Z", + type: "Split", + definitionId: "17", + __typename: "NodeInstance", + }, + { + id: "e7f5fc8b-fb50-4983-b5dd-a0795bab324d", + nodeId: "13", + name: "Script", + enter: "2024-10-30T15:31:46.567Z", + exit: "2024-10-30T15:31:46.57Z", + type: "ActionNode", + definitionId: "13", + __typename: "NodeInstance", + }, + { + id: "7396e050-33c9-4096-bebd-7b579463c79a", + nodeId: "12", + name: "systemOut", + enter: "2024-10-30T15:31:46.566Z", + exit: "2024-10-30T15:31:46.57Z", + type: "ActionNode", + definitionId: "12", + __typename: "NodeInstance", + }, + { + id: "62dbdf7f-3922-411a-9cbe-201e1b2d0070", + nodeId: "10", + name: "EmbeddedStart", + enter: "2024-10-30T15:31:46.566Z", + exit: "2024-10-30T15:31:46.57Z", + type: "StartNode", + definitionId: "10", + __typename: "NodeInstance", + }, + { + id: "059f883b-cf4e-4cc2-8fc4-ea9a87ce55cf", + nodeId: "3", + name: "PrintStartMessage", + enter: "2024-10-30T15:31:46.559Z", + exit: "2024-10-30T15:31:46.57Z", + type: "CompositeContextNode", + definitionId: "3", + __typename: "NodeInstance", + }, + { + id: "9555d6b3-c00e-452a-964f-0a09799a7c74", + nodeId: "8", + name: "EmbeddedEnd", + enter: "2024-10-30T15:31:46.565Z", + exit: "2024-10-30T15:31:46.57Z", + type: "EndNode", + definitionId: "8", + __typename: "NodeInstance", + }, + { + id: "d337fe91-eeaa-4366-a87c-ceb4a1f80aa0", + nodeId: "7", + name: "Script", + enter: "2024-10-30T15:31:46.565Z", + exit: "2024-10-30T15:31:46.57Z", + type: "ActionNode", + definitionId: "7", + __typename: "NodeInstance", + }, + { + id: "5ba61e92-0683-4ecb-b686-58d45a169c3e", + nodeId: "6", + name: "systemOut", + enter: "2024-10-30T15:31:46.559Z", + exit: "2024-10-30T15:31:46.57Z", + type: "ActionNode", + definitionId: "6", + __typename: "NodeInstance", + }, + { + id: "42c5c1cd-960e-4380-af71-dced7c01cdbb", + nodeId: "4", + name: "EmbeddedStart", + enter: "2024-10-30T15:31:46.559Z", + exit: "2024-10-30T15:31:46.571Z", + type: "StartNode", + definitionId: "4", + __typename: "NodeInstance", + }, + { + id: "40a009ca-35f3-483a-b87f-e479a51eba39", + nodeId: "1", + name: "Start", + enter: "2024-10-30T15:31:46.558Z", + exit: "2024-10-30T15:31:46.571Z", + type: "StartNode", + definitionId: "1", + __typename: "NodeInstance", + }, + ], + milestones: [], + __typename: "ProcessInstance", + }, { id: "e995b0d2-078a-488f-8346-0176ed8d5033", processId: "service", @@ -494,5 +659,46 @@ module.exports = { __typename: "ProcessDefinition", }, ], - JobsData: [], + JobsData: [ + { + id: "a62d9d0a-87ea-4c13-87fb-67965d133020", + processId: "callback_state_timeouts", + processInstanceId: "9750c042-3fb2-40b7-96ba-ff10b6178c58", + rootProcessId: null, + status: "SCHEDULED", + expirationTime: () => new Date(Date.now() + 1 * 10 * 1000).toISOString(), + priority: 0, + callbackEndpoint: + "http://localhost:4000/management/jobs/callback_state_timeouts/instances/9750c042-3fb2-40b7-96ba-ff10b6178c58/timers/-1", + repeatInterval: 0, + repeatLimit: 0, + scheduledId: "143", + retries: 0, + lastUpdate: "2024-10-30T15:31:46.709Z", + endpoint: "http://localhost:4000/jobs", + nodeInstanceId: "ee6c3f6e-8bc3-43dc-a249-dad1b19b52bb", + executionCounter: 0, + __typename: "Job", + }, + { + id: "e47fa096-8bc8-42c0-a66d-ad9b3b4b0d7f", + processId: "callback_state_timeouts", + processInstanceId: "9750c042-3fb2-40b7-96ba-ff10b6178c58", + rootProcessId: null, + status: "EXECUTED", + expirationTime: null, + priority: 0, + callbackEndpoint: + "http://localhost:4000/management/jobs/callback_state_timeouts/instances/d818e6dc-e949-4b11-b87b-678b614c0739/timers/-1", + repeatInterval: 0, + repeatLimit: 0, + scheduledId: "283", + retries: 0, + lastUpdate: "2024-10-30T16:27:22.201Z", + endpoint: "http://localhost:4000/jobs", + nodeInstanceId: "fb066c9c-4b25-42b6-a202-cbcdcac68d1b", + executionCounter: 1, + __typename: "Job", + }, + ], }; diff --git a/packages/sonataflow-dev-app/src/MockData/types.js b/packages/sonataflow-dev-app/src/MockData/types.js index cdf6c23ef90..eaa22f959dd 100644 --- a/packages/sonataflow-dev-app/src/MockData/types.js +++ b/packages/sonataflow-dev-app/src/MockData/types.js @@ -26,6 +26,10 @@ module.exports = typeDefs = gql` query: Query } + type Mutation { + JobExecute(id: String): String + } + type Query { ProcessInstances( where: ProcessInstanceArgument diff --git a/packages/sonataflow-dev-app/src/server.js b/packages/sonataflow-dev-app/src/server.js index 4be09f446dd..f885872b496 100644 --- a/packages/sonataflow-dev-app/src/server.js +++ b/packages/sonataflow-dev-app/src/server.js @@ -112,6 +112,16 @@ function paginatedResult(arr, offset, limit) { } // Provide resolver functions for your schema fields const resolvers = { + Mutation: { + JobExecute: async (_parent, args) => { + const job = data.JobsData.find((data) => { + return data.id === args["id"]; + }); + if (!job) return; + job.expirationTime = null; + job.status = "EXECUTED"; + }, + }, Query: { ProcessInstances: async (parent, args) => { let result = data.ProcessInstanceData.filter((datum) => { @@ -169,7 +179,16 @@ const resolvers = { await timeout(); return data.ProcessDefinitionData; }, - Jobs: () => [], + Jobs: async (parent, args) => + data.JobsData.filter((job) => { + if (!args["where"]) { + return true; + } else if (args["where"].processInstanceId && args["where"].processInstanceId.equal) { + return job.processInstanceId == args["where"].processInstanceId.equal; + } else { + return false; + } + }), }, DateTime: new GraphQLScalarType({