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(