diff --git a/.gitignore b/.gitignore index ba54f5f90..b368128ee 100644 --- a/.gitignore +++ b/.gitignore @@ -64,6 +64,10 @@ pom.xml.versionsBackup release.properties .m2/ +# Esbuild Maven Plugin for dev-ui +node_modules/ +build.js + # Claude Code .claude/settings.local.json diff --git a/core/deployment/pom.xml b/core/deployment/pom.xml index b834e5faa..17222a137 100644 --- a/core/deployment/pom.xml +++ b/core/deployment/pom.xml @@ -10,6 +10,14 @@ quarkus-flow-deployment Quarkus Flow :: Core :: Deployment + + 2.0.0 + 19.2.7 + 0.27.0 + 0.2.0 + 1.6.0 + + io.quarkus @@ -117,6 +125,16 @@ + + + src/main/resources + + + dev-ui/bundle.js + + + + maven-compiler-plugin @@ -135,6 +153,75 @@ + + + io.mvnpm + esbuild-maven-plugin + ${esbuild-maven-plugin.version} + + + + esbuild + + esbuild + + + + + + src/main/resources/dev-ui + target/classes/dev-ui + bundle.js + + + + + + org.mvnpm.at.openworkflowspec + diagram-editor + ${openworkflowspec.diagram-editor.version} + + + + org.mvnpm.at.serverlessworkflow + sdk + + + + org.mvnpm + d3-interpolate + + + + + + + org.mvnpm + react + ${react.version} + + + + org.mvnpm + react-dom + ${react.version} + + + + + org.mvnpm + scheduler + ${scheduler.version} + + + + + org.mvnpm + use-sync-external-store + ${use-sync-external-store.version} + + + diff --git a/core/deployment/src/main/resources/dev-ui/bundle.js b/core/deployment/src/main/resources/dev-ui/bundle.js new file mode 100644 index 000000000..a11dc3d0b --- /dev/null +++ b/core/deployment/src/main/resources/dev-ui/bundle.js @@ -0,0 +1,28 @@ +import * as React from "react"; +import { createRoot } from "react-dom/client"; +import { DiagramEditor } from "@openworkflowspec/diagram-editor"; +import "@openworkflowspec/diagram-editor/styles.css"; + +const roots = new WeakMap(); + +export function renderOpenWorkflowSpecDiagramEditor(container, props) { + let root = roots.get(container); + + if (!root) { + root = createRoot(container); + roots.set(container, root); + } + + root.render(React.createElement(DiagramEditor, {key: props.workflowKey, ...props})); + + return root; +} + +export function unmountOpenWorkflowSpecDiagramEditor(container) { + const root = roots.get(container); + + if (root) { + root.unmount(); + roots.delete(container); + } +} diff --git a/core/deployment/src/main/resources/dev-ui/components/openworkflowspec-diagram-editor.js b/core/deployment/src/main/resources/dev-ui/components/openworkflowspec-diagram-editor.js new file mode 100644 index 000000000..63f0f84ce --- /dev/null +++ b/core/deployment/src/main/resources/dev-ui/components/openworkflowspec-diagram-editor.js @@ -0,0 +1,138 @@ +import { LitElement, html, css } from 'lit'; +import '@vaadin/progress-bar'; +import { + renderOpenWorkflowSpecDiagramEditor, + unmountOpenWorkflowSpecDiagramEditor, +} from '../bundle.js'; +import { JsonRpc } from 'jsonrpc'; + +const diagramEditorStylesUrl = new URL('../bundle.css', import.meta.url).href; + +export class OpenWorkflowSpecDiagramEditorElement extends LitElement { + _workflowDefinition = null; + + static properties = { + workflow: { type: Object }, + readonly: { type: Boolean }, + extensionName: { type: String }, + workflowKey: { state: true }, + _isLoading: { state: true }, + }; + + static styles = css` + :host { + display: block; + width: 100%; + height: 100%; + min-height: 520px; + } + + #container { + width: 100%; + height: 100%; + min-height: 520px; + } + + .loading-workflow-definition { + max-width: 80%; + margin: auto; + padding-top: 80px; + text-align: center; + } + `; + + constructor() { + super(); + this.workflow = null; + this.readonly = true; + this.extensionName = 'quarkus-flow'; + this.workflowKey = null; + this._isLoading = false; + } + + connectedCallback() { + super.connectedCallback(); + this.jsonRpc = new JsonRpc(this.extensionName); + } + + render() { + return html` + + + ${this._isLoading + ? html` +
+ +

Loading workflow definition…

+
+ ` + : html` +
+ `} + `; + } + + async updated(changedProperties) { + if (changedProperties.has('workflow')) { + this._unmountReactEditor(); + + this._isLoading = true; + await this.updateComplete; + + try { + await this._loadWorkflowDefinition(); + } finally { + this._isLoading = false; + await this.updateComplete; + this._renderReactEditor(); + } + } + } + + async _loadWorkflowDefinition() { + if (!this.workflow?.id) { + this._workflowDefinition = null; + return; + } + + try { + const { result } = await this.jsonRpc.getWorkflowDefinition({ id: this.workflow.id }); + this._workflowDefinition = result; + } catch (error) { + console.error('Failed to load workflow definition:', error); + this._workflowDefinition = null; + } + } + + disconnectedCallback() { + this._unmountReactEditor(); + super.disconnectedCallback(); + } + + _renderReactEditor() { + const container = this.renderRoot.querySelector('#container'); + + if (!container) { + return; + } + + renderOpenWorkflowSpecDiagramEditor(container, { + content: this._workflowDefinition, + isReadOnly: this.readonly, + workflowKey: this.workflowKey, + }); + } + + _unmountReactEditor() { + const container = this.renderRoot?.querySelector('#container'); + + if (container) { + unmountOpenWorkflowSpecDiagramEditor(container); + } + } +} + +customElements.define( + 'qwc-openworkflowspec-diagram-editor', + OpenWorkflowSpecDiagramEditorElement +); diff --git a/core/deployment/src/main/resources/dev-ui/qwc-flow-workflows.js b/core/deployment/src/main/resources/dev-ui/qwc-flow-workflows.js index e6c418724..4f391add3 100644 --- a/core/deployment/src/main/resources/dev-ui/qwc-flow-workflows.js +++ b/core/deployment/src/main/resources/dev-ui/qwc-flow-workflows.js @@ -1,19 +1,14 @@ import { QwcHotReloadElement, html, css } from 'qwc-hot-reload-element'; import { observeState } from 'lit-element-state'; import { JsonRpc } from 'jsonrpc'; -import { unsafeHTML } from "lit/directives/unsafe-html.js" import '@vaadin/grid'; import '@vaadin/button'; import '@vaadin/icon'; import '@vaadin/dialog'; -import { dialogRenderer, dialogFooterRenderer } from '@vaadin/dialog/lit.js'; +import { dialogRenderer } from '@vaadin/dialog/lit.js'; import { columnBodyRenderer } from '@vaadin/grid/lit.js'; -import { devuiState } from 'devui-state'; - -import { notifier } from 'notifier'; - +import './components/openworkflowspec-diagram-editor.js'; import './qwc-flow-workflow-execution.js'; - import { themeState } from 'theme-state'; export class QwcFlow extends observeState(QwcHotReloadElement) { @@ -23,37 +18,20 @@ export class QwcFlow extends observeState(QwcHotReloadElement) { .workflows { padding: 8px; } - .buttonBar { - display: flex; - justify-content: space-between; - gap: 10px; - align-items: center; - width: 90%; - color: var(--lumo-primary-text-color); - width: 100%; - } - .mermaidError { - color: var(--lumo-error-text-color); - padding: 20px; - word-wrap: break-word; - overflow-wrap: break-word; - white-space: pre-wrap; - } `; static properties = { _workflows: { state: true }, - _currentMermaid: { state: true }, - _currentMermaidWorkflow: { state: true }, - _mermaidDialogOpened: { state: true }, + _currentDiagramEditorWorkflow: { state: true }, + _diagramEditorDialogOpened: { state: true }, _selectedWorkflow: { state: true } }; constructor() { super(); this._workflows = []; - this._currentMermaid = ''; - this._mermaidDialogOpened = false; + this._currentDiagramEditorWorkflow = null; + this._diagramEditorDialogOpened = false; this._selectedWorkflow = null; } @@ -103,17 +81,17 @@ export class QwcFlow extends observeState(QwcHotReloadElement) { header="Actions" auto-width ${columnBodyRenderer(workflow => html` - this._visualizeMermaid(workflow)} - id="see-${this._generateMermaidId(workflow.id)}"> + this._visualizeDiagramEditor(workflow)} + id="see-${this._generateDiagramEditorId(workflow.id)}"> - this._executeWorkflow(workflow)}> + this._executeWorkflow(workflow)}> `, [])}> - ${this._mermaidDialog()} + ${this._diagramEditorDialog()} `; } @@ -125,148 +103,31 @@ export class QwcFlow extends observeState(QwcHotReloadElement) { connectedCallback() { super.connectedCallback(); - if (!window.mermaid) { - const script = document.createElement('script'); - script.src = 'https://cdn.jsdelivr.net/npm/mermaid@11.12.0/dist/mermaid.min.js'; - script.onload = () => { - this._initializeMermaid(); - }; - document.head.appendChild(script); - } else { - this._initializeMermaid(); - } - this.jsonRpc.getWorkflows().then(({ result }) => { this._workflows = result; }); } - disconnectedCallback() { - super.disconnectedCallback(); - } - - async updated(changedProps) { - super.updated?.(changedProps); - if (this._currentMermaid && window.mermaid) { - const mermaidElement = this.shadowRoot?.querySelector('.mermaid'); - if (mermaidElement && !mermaidElement.hasAttribute('data-processed')) { - try { - const id = this._generateMermaidId(this._currentMermaidWorkflow.id); - const { svg } = await window.mermaid.render(id, this._currentMermaid); - mermaidElement.innerHTML = svg; - mermaidElement.setAttribute('data-processed', 'true'); - } catch (error) { - mermaidElement.innerHTML = `
-

Error rendering diagram: ${error.message || 'Unknown error'}

-
`; - } - } - } - } - - _initializeMermaid() { - window.mermaid.initialize({ - startOnLoad: false, - theme: themeState.theme.name === 'dark' ? 'dark' : 'default', - fontFamily: 'var(--lumo-font-family)', - fontSize: 12, - flowchart: { - useMaxWidth: true, - htmlLabels: true, - }, - themeCSS: ".label foreignObject { font-size: 14px; overflow: visible; width: auto; }" - }); - }; - - _generateMermaidId(workflowId) { + _generateDiagramEditorId(workflowId) { const { namespace, name, version } = workflowId; - return 'mermaid-' + `${namespace.replaceAll('.', '-')}-${name.replaceAll('.', '-')}-${version.replaceAll('.', '-')}`; + return 'diagramEditor-' + `${namespace.replaceAll('.', '-')}-${name.replaceAll('.', '-')}-${version.replaceAll('.', '-')}`; } - _visualizeMermaid(workflow) { - // Backend expects: generateMermaidDiagram(WorkflowDefinitionId id) - this.jsonRpc.generateMermaidDiagram({ - id: workflow.id - }).then(({ result }) => { - this._currentMermaid = result.mermaid.replace(/^---[\s\S]*?---\s*/, ''); - this._mermaidDialogOpened = true; - }); - this._currentMermaidWorkflow = workflow; - } - - _mermaidContent() { - return unsafeHTML(` - ${!this._currentMermaid ? ` - - - - Diagram not available. Run the workflow once to generate it. - - ` : ''} -
-            ${this._currentMermaid}
-            
`); - } - - _downloadDiagramAsPng() { - let svgData = document.querySelector("#page > qwc-flow-workflows") - .shadowRoot.querySelector("svg"); - if (!svgData) return; - - const scale = 3; - const bbox = svgData.getBBox(); - const padding = 20; - const width = bbox.width + bbox.x + padding * 2; - const height = bbox.height + bbox.y + padding * 2; - - const svgClone = svgData.cloneNode(true); - svgClone.setAttribute("width", width); - svgClone.setAttribute("height", height); - svgClone.setAttribute("viewBox", `0 0 ${width} ${height}`); - - const svgString = new XMLSerializer().serializeToString(svgClone); - const utf8Bytes = new TextEncoder().encode(svgString); - const base64 = btoa(String.fromCharCode(...utf8Bytes)); - - let img = new Image(width, height); - img.src = `data:image/svg+xml;base64,${base64}`; - - img.onload = function () { - let cnv = document.createElement('canvas'); - cnv.width = width * scale; - cnv.height = height * scale; - - const ctx = cnv.getContext("2d"); - ctx.scale(scale, scale); - ctx.drawImage(img, 0, 0); - - cnv.toBlob((blob) => { - let lnk = document.createElement('a'); - lnk.href = URL.createObjectURL(blob); - lnk.download = "flow-" + devuiState.applicationInfo.applicationName - + "-" + new Date().toISOString().replace(/\D/g, '') + ".png"; - lnk.click(); - notifier.showSuccessMessage(lnk.download + " downloaded.", 'bottom-end'); - }, 'image/png'); - }; + _visualizeDiagramEditor(workflow) { + this._diagramEditorDialogOpened = true; + this._currentDiagramEditorWorkflow = workflow; } - _mermaidDialog() { + _diagramEditorDialog() { return html` (this._mermaidDialogOpened = e.detail.value)} - ${dialogRenderer(() => this._mermaidContent(), [])} - ${dialogFooterRenderer(() => html` -
- this._downloadDiagramAsPng()}> - - Export - -
- `, [])} - .width=${'800px'} - .height=${'600px'} + .opened=${this._diagramEditorDialogOpened} + @opened-changed=${e => (this._diagramEditorDialogOpened = e.detail.value)} + ${dialogRenderer(() => this._diagramEditorContent(), [ + this._diagramEditorDialogOpened + ])} + .width=${'80%'} + .height=${'80%'} resizable draggable header-title="Flow Diagram" @@ -275,6 +136,16 @@ export class QwcFlow extends observeState(QwcHotReloadElement) { `; } + _diagramEditorContent() { + return html` + + + `; + } + _executeWorkflow(workflow) { this._selectedWorkflow = workflow; } diff --git a/core/deployment/src/test/java/io/quarkiverse/flow/deployment/test/devui/EchoNameWorkflow.java b/core/deployment/src/test/java/io/quarkiverse/flow/deployment/test/devui/EchoNameWorkflow.java new file mode 100644 index 000000000..cb0e21aec --- /dev/null +++ b/core/deployment/src/test/java/io/quarkiverse/flow/deployment/test/devui/EchoNameWorkflow.java @@ -0,0 +1,17 @@ +package io.quarkiverse.flow.deployment.test.devui; + +import jakarta.enterprise.context.ApplicationScoped; + +import io.quarkiverse.flow.Flow; +import io.serverlessworkflow.api.types.Workflow; +import io.serverlessworkflow.fluent.spec.WorkflowBuilder; + +@ApplicationScoped +public class EchoNameWorkflow extends Flow { + public Workflow descriptor() { + return WorkflowBuilder + .workflow("echo-name", "flow", "0.1.0") + .tasks(t -> t.set("setEcho", "${ \"Echo from test: \" + .name }")) + .build(); + } +} diff --git a/core/deployment/src/test/java/io/quarkiverse/flow/deployment/test/devui/WorkflowDiagramEditorIT.java b/core/deployment/src/test/java/io/quarkiverse/flow/deployment/test/devui/WorkflowDiagramEditorIT.java new file mode 100644 index 000000000..2b0c7c3b0 --- /dev/null +++ b/core/deployment/src/test/java/io/quarkiverse/flow/deployment/test/devui/WorkflowDiagramEditorIT.java @@ -0,0 +1,122 @@ +package io.quarkiverse.flow.deployment.test.devui; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import com.microsoft.playwright.Browser; +import com.microsoft.playwright.BrowserType; +import com.microsoft.playwright.ElementHandle; +import com.microsoft.playwright.Locator; +import com.microsoft.playwright.Page; +import com.microsoft.playwright.Playwright; +import com.microsoft.playwright.options.WaitForSelectorState; + +import io.quarkus.test.QuarkusDevModeTest; + +public class WorkflowDiagramEditorIT { + + @RegisterExtension + static QuarkusDevModeTest devMode = new QuarkusDevModeTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClass(EchoNameWorkflow.class)); + + static BrowserType.LaunchOptions launchOptions; + + @BeforeAll + static void setUp() { + launchOptions = new BrowserType.LaunchOptions() + .setHeadless(true) + .setChromiumSandbox(false) + .setChannel("") + .setArgs(List.of("--disable-gpu")); + } + + @Test + @DisplayName("Should show loading state before DiagramEditor renders") + void shouldShowLoadingBeforeDiagramEditor() { + final Map env = new HashMap<>(System.getenv()); + env.put("DEBUG", "pw:api"); + + try (Playwright playwright = Playwright.create(new Playwright.CreateOptions().setEnv(env))) { + try (Browser browser = playwright.chromium().launch(launchOptions)) { + Page page = browser.newPage(); + page.navigate("http://localhost:8080/q/dev-ui/quarkus-flow/workflows"); + + String diagramEditorId = "diagramEditor-flow-echo-name-0-1-0"; + String buttonSelector = "#see-" + diagramEditorId; + + page.waitForSelector(buttonSelector); + Locator eyeButton = page.locator(buttonSelector); + + eyeButton.click(); + + ElementHandle dialog = page.waitForSelector("vaadin-dialog[opened]", new Page.WaitForSelectorOptions() + .setState(WaitForSelectorState.ATTACHED)); + + String initialHtml = dialog.innerHTML(); + boolean hasLoadingState = initialHtml.contains("Loading workflow definition") || + dialog.querySelector("vaadin-progress-bar[indeterminate]") != null; + + Assertions.assertTrue(hasLoadingState, + "Loading state (progress bar or text) must be visible immediately after dialog opens"); + + ElementHandle editorBeforeLoad = dialog.querySelector("[data-testid='diagram-container']"); + Assertions.assertNull(editorBeforeLoad, + "DiagramEditor should NOT be visible during loading state"); + + page.waitForSelector("[data-testid='diagram-container']"); + + ElementHandle progressBarAfter = dialog.querySelector("vaadin-progress-bar[indeterminate]"); + Assertions.assertNull(progressBarAfter, + "Progress bar should be removed after loading completes"); + } + } + } + + @Test + @DisplayName("Should render DiagramEditor with greet node") + void shouldRenderDiagramEditorWithGreetNode() { + final Map env = new HashMap<>(System.getenv()); + env.put("DEBUG", "pw:api"); + + try (Playwright playwright = Playwright.create(new Playwright.CreateOptions().setEnv(env))) { + try (Browser browser = playwright.chromium().launch(launchOptions)) { + Page page = browser.newPage(); + page.navigate("http://localhost:8080/q/dev-ui/quarkus-flow/workflows"); + + String diagramEditorId = "diagramEditor-flow-echo-name-0-1-0"; + String buttonSelector = "#see-" + diagramEditorId; + + page.waitForSelector(buttonSelector); + Locator eyeButton = page.locator(buttonSelector); + + eyeButton.click(); + + ElementHandle dialog = page.waitForSelector("vaadin-dialog[opened]", new Page.WaitForSelectorOptions() + .setState(WaitForSelectorState.ATTACHED)); + + page.waitForSelector("[data-testid='diagram-container']"); + + String nodeSelector = "[data-testid='set-node-/do/0/setEcho']"; + page.waitForSelector(nodeSelector); + ElementHandle setEchoNode = dialog.querySelector(nodeSelector); + + Assertions.assertNotNull(setEchoNode, + "DiagramEditor should render the setEcho node with data-testid='set-node-/do/0/setEcho'"); + + String nodeText = setEchoNode.textContent(); + Assertions.assertTrue(nodeText.contains("setEcho"), + "setEcho node should contain the text 'setEcho'"); + } + } + } +} diff --git a/core/deployment/src/test/java/io/quarkiverse/flow/deployment/test/devui/WorkflowMermaidIT.java b/core/deployment/src/test/java/io/quarkiverse/flow/deployment/test/devui/WorkflowMermaidIT.java deleted file mode 100644 index ac78ded2d..000000000 --- a/core/deployment/src/test/java/io/quarkiverse/flow/deployment/test/devui/WorkflowMermaidIT.java +++ /dev/null @@ -1,114 +0,0 @@ -package io.quarkiverse.flow.deployment.test.devui; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.jboss.shrinkwrap.api.ShrinkWrap; -import org.jboss.shrinkwrap.api.spec.JavaArchive; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; - -import com.microsoft.playwright.Browser; -import com.microsoft.playwright.BrowserType; -import com.microsoft.playwright.ElementHandle; -import com.microsoft.playwright.Locator; -import com.microsoft.playwright.Page; -import com.microsoft.playwright.Playwright; -import com.microsoft.playwright.options.BoundingBox; -import com.microsoft.playwright.options.WaitForSelectorState; - -import io.quarkiverse.flow.deployment.test.HelloWorldWorkflow; -import io.quarkus.test.QuarkusDevModeTest; - -public class WorkflowMermaidIT { - - @RegisterExtension - static QuarkusDevModeTest devMode = new QuarkusDevModeTest() - .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) - .addClass(HelloWorldWorkflow.class)); - - static BrowserType.LaunchOptions launchOptions; - - @BeforeAll - static void setUp() { - launchOptions = new BrowserType.LaunchOptions() - .setHeadless(true) - .setChromiumSandbox(false) - .setChannel("") - .setArgs(List.of("--disable-gpu")); - - } - - @Test - @DisplayName("Should generate the diagram as expected") - void shouldGenerateMermaidSvg() { - final Map env = new HashMap<>(System.getenv()); - env.put("DEBUG", "pw:api"); - - try (Playwright playwright = Playwright.create(new Playwright.CreateOptions().setEnv(env))) { - - try (Browser browser = playwright.chromium().launch(launchOptions)) { - - Page page = browser.newPage(); - page.navigate("http://localhost:8080/q/dev-ui/quarkus-flow/workflows"); - - String mermaidId = "mermaid-devui-hello-world-v1"; - String buttonSelector = "#see-" + mermaidId; - - page.waitForSelector(buttonSelector); - Locator eyeButton = page.locator(buttonSelector); - - eyeButton.click(); - - ElementHandle dialog = page.waitForSelector("vaadin-dialog[opened]", new Page.WaitForSelectorOptions() - .setState(WaitForSelectorState.ATTACHED)); - - // Wait for the dialog to open and mermaid rendering - page.waitForTimeout(8000); - - Assertions.assertTrue(dialog.innerText().contains("set: setMessage")); // the name of the set task - } - } - } - - @Test - @DisplayName("Should keep the execution output and action button separated") - void shouldRenderWorkflowExecutionWithoutOverlappingControls() { - final Map env = new HashMap<>(System.getenv()); - env.put("DEBUG", "pw:api"); - - try (Playwright playwright = Playwright.create(new Playwright.CreateOptions().setEnv(env))) { - try (Browser browser = playwright.chromium().launch(launchOptions)) { - Page page = browser.newPage(); - page.navigate("http://localhost:8080/q/dev-ui/quarkus-flow/workflows"); - - String mermaidId = "mermaid-devui-hello-world-v1"; - String playButtonSelector = "#play-" + mermaidId; - - page.waitForSelector(playButtonSelector); - page.locator(playButtonSelector).click(); - - Locator execution = page.locator("qwc-flow-workflow-execution"); - execution.waitFor(); - - Locator outputBadge = execution.locator("qui-badge").nth(1); - Locator startButton = execution.locator("#execute-workflow"); - - outputBadge.waitFor(); - startButton.waitFor(); - - BoundingBox outputBox = outputBadge.boundingBox(); - BoundingBox buttonBox = startButton.boundingBox(); - - Assertions.assertNotNull(outputBox, "Output badge should be visible"); - Assertions.assertNotNull(buttonBox, "Start workflow button should be visible"); - Assertions.assertTrue(outputBox.y + outputBox.height <= buttonBox.y, - "Start workflow button should appear below the output section"); - } - } - } -} diff --git a/core/runtime-dev/src/main/java/io/quarkiverse/flow/devui/WorkflowRPCService.java b/core/runtime-dev/src/main/java/io/quarkiverse/flow/devui/WorkflowRPCService.java index 4da204fbf..75e5ec5e7 100644 --- a/core/runtime-dev/src/main/java/io/quarkiverse/flow/devui/WorkflowRPCService.java +++ b/core/runtime-dev/src/main/java/io/quarkiverse/flow/devui/WorkflowRPCService.java @@ -26,6 +26,7 @@ import io.quarkus.arc.InstanceHandle; import io.quarkus.logging.Log; import io.quarkus.runtime.annotations.JsonRpcDescription; +import io.serverlessworkflow.api.WorkflowFormat; import io.serverlessworkflow.api.types.Input; import io.serverlessworkflow.api.types.SchemaInline; import io.serverlessworkflow.api.types.SchemaUnion; @@ -85,6 +86,18 @@ public MermaidDefinition generateMermaidDiagram( return mermaidDefinition; } + @JsonRpcDescription("Get the workflow definition") + public String getWorkflowDefinition( + @JsonRpcDescription("Workflow's id") WorkflowDefinitionId id) { + final Workflow workflow = Objects.requireNonNull(registryCache.get(id), "Workflow with id '" + id + "' not found"); + + try { + return WorkflowFormat.JSON.mapper().writeValueAsString(workflow); + } catch (Exception e) { + throw new RuntimeException("Failed to serialize workflow definition", e); + } + } + @Blocking @JsonRpcDescription("Execute a workflow given an input") public WorkflowOutput executeWorkflow(