From 571691d698c2dcbdf3e7b49ac6404f24fc53326c Mon Sep 17 00:00:00 2001 From: Lorna Kelly Date: Wed, 30 Sep 2026 11:08:11 +0100 Subject: [PATCH] Diagram editor full screen Signed-off-by: Lorna Kelly --- .../components/qwc-flow-workflow-header.js | 75 ++++++ .../dev-ui/qwc-flow-workflow-execution.js | 37 +-- .../resources/dev-ui/qwc-flow-workflows.js | 243 ++++++++++-------- .../WorkflowDiagramEditorRoundTripTest.java | 84 +++--- .../test/devui/WorkflowDiagramEditorTest.java | 2 +- 5 files changed, 263 insertions(+), 178 deletions(-) create mode 100644 core/deployment/src/main/resources/dev-ui/components/qwc-flow-workflow-header.js diff --git a/core/deployment/src/main/resources/dev-ui/components/qwc-flow-workflow-header.js b/core/deployment/src/main/resources/dev-ui/components/qwc-flow-workflow-header.js new file mode 100644 index 000000000..d625d7aa8 --- /dev/null +++ b/core/deployment/src/main/resources/dev-ui/components/qwc-flow-workflow-header.js @@ -0,0 +1,75 @@ +import { LitElement, html, css } from 'lit'; +import '@vaadin/button'; +import '@vaadin/icon'; + +/** +* Shared header for workflow detail. +* Renders a Back button plus the workflow name, namespace/version and description. +*/ +export class QwcFlowWorkflowHeader extends LitElement { + static properties = { + // WorkflowDefinitionId from backend: { namespace, name, version } + workflowId: { type: Object }, + description: { type: String }, + }; + + static styles = css` + :host { + display: block; + } + + .workflow-name { + margin: 12px 0px 4px 0px; + } + + .workflow-meta { + font-size: var(--lumo-font-size-s); + color: var(--lumo-secondary-text-color); + margin-bottom: 12px; + } + `; + + constructor() { + super(); + this.workflowId = null; + this.description = ''; + } + + render() { + const id = this.workflowId || {}; + const name = id.name || '(unknown)'; + const ns = id.namespace || ''; + const version = id.version || ''; + + return html` + + + Back + +

${name}

+
+ ${ns ? html`Namespace: ${ns}` : ''} + ${version ? html`${ns ? ' · ' : ''}Version: ${version}` : ''} + ${this.description + ? html` +
+ ${this.description} + ` + : ''} +
+ `; + } + + _backAction() { + this.dispatchEvent( + new CustomEvent('flow-header-back', { + detail: {}, + bubbles: true, + cancelable: true, + composed: false, + }) + ); + } +} + +customElements.define('qwc-flow-workflow-header', QwcFlowWorkflowHeader); diff --git a/core/deployment/src/main/resources/dev-ui/qwc-flow-workflow-execution.js b/core/deployment/src/main/resources/dev-ui/qwc-flow-workflow-execution.js index 4d5e63804..32429673c 100644 --- a/core/deployment/src/main/resources/dev-ui/qwc-flow-workflow-execution.js +++ b/core/deployment/src/main/resources/dev-ui/qwc-flow-workflow-execution.js @@ -16,6 +16,8 @@ import '@vaadin/number-field'; import '@vaadin/checkbox'; import '@vaadin/text-area'; +import './components/qwc-flow-workflow-header.js' + import 'qui-themed-code-block'; import 'qui-badge'; @@ -58,16 +60,6 @@ export class QwcFlowExecution extends observeState(QwcHotReloadElement) { justify-content: flex-start; } - .workflow-name { - margin: 12px 0px 4px 0px; - } - - .workflow-meta { - font-size: var(--lumo-font-size-s); - color: var(--lumo-secondary-text-color); - margin-bottom: 12px; - } - .form-field { margin-bottom: 8px; max-width: 500px; @@ -138,27 +130,12 @@ export class QwcFlowExecution extends observeState(QwcHotReloadElement) { } _renderTopBar() { - const id = this.workflowId || {}; - const name = id.name || '(unknown)'; - const ns = id.namespace || ''; - const version = id.version || ''; - return html` -
- - - Back - -

${name}

-
- ${ns ? html`Namespace: ${ns}` : ''} - ${version ? html`${ns ? ' · ' : ''}Version: ${version}` : ''} - ${this.description ? html` -
- ${this.description} - ` : ''} -
-
+ + `; } 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 678188a79..666c990f5 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,100 +1,73 @@ import { QwcHotReloadElement, html, css } from 'qwc-hot-reload-element'; -import { observeState } from 'lit-element-state'; import { JsonRpc } from 'jsonrpc'; import '@vaadin/grid'; import '@vaadin/button'; import '@vaadin/icon'; -import '@vaadin/dialog'; -import { dialogRenderer } from '@vaadin/dialog/lit.js'; import { columnBodyRenderer } from '@vaadin/grid/lit.js'; import './components/openworkflowspec-diagram-editor.js'; +import './components/qwc-flow-workflow-header.js'; import './qwc-flow-workflow-execution.js'; -import { themeState } from 'theme-state'; -export class QwcFlow extends observeState(QwcHotReloadElement) { +export class QwcFlow extends QwcHotReloadElement { jsonRpc = new JsonRpc(this); static styles = css` - .workflows { - padding: 8px; - } - `; + :host { + display: block; + height: 100%; + min-height: 0; + } + + .workflows { + padding: 8px; + } + + .diagram-view { + display: flex; + flex-direction: column; + height: 100%; + min-height: 0; + padding: 0px 8px; + } + + .diagram-view-header { + flex-shrink: 0; + } + + /* min-height matches openworkflowspec-diagram-editor.js so the + diagram never collapses if an ancestor has no definite height. */ + .diagram-view-body { + flex: 1 1 auto; + min-height: 520px; + position: relative; + overflow: hidden; + } + + .diagram-editor { + height: 100%; + } + `; static properties = { _workflows: { state: true }, - _currentDiagramEditorWorkflow: { state: true }, - _diagramEditorDialogOpened: { state: true }, - _selectedWorkflow: { state: true } + _view: { state: true } }; constructor() { super(); this._workflows = []; - this._currentDiagramEditorWorkflow = null; - this._diagramEditorDialogOpened = false; - this._selectedWorkflow = null; + this._view = { mode: 'list', workflow: null }; } render() { - if (this._selectedWorkflow != null) { - // Pass WorkflowDefinitionId as a *property*, not as a string attribute - return html` - - - `; - } else { - return html` -
- - - - - - - - - - - - - - html` - this._visualizeDiagramEditor(workflow)} - id="see-${this._generateDiagramEditorId(workflow.id)}"> - - - this._executeWorkflow(workflow)}> - - - `, [])}> - - - ${this._diagramEditorDialog()} -
- `; - } + switch(this._view.mode){ + case 'diagram': + return this._renderDiagramView(this._view.workflow); + case 'execute': + return this._renderExecuteView(this._view.workflow) + default: + return this._renderWorkflowList(); + } } hotReload() { @@ -108,51 +81,105 @@ export class QwcFlow extends observeState(QwcHotReloadElement) { }); } + _renderWorkflowList() { + return html` +
+ + + + + + + + + + + + + + html` + this._viewDiagram(workflow)} + id="see-${this._generateDiagramEditorId(workflow.id)}" + title="View diagram"> + + + this._executeWorkflow(workflow)} + title="Execute workflow"> + + + `, [])}> + + +
+ `; + } + + _renderDiagramView(workflow) { + return html` +
+ + +
+ + +
+
+ `; + } + + _renderExecuteView(workflow) { + return html` + + + `; + } + _generateDiagramEditorId(workflowId) { const { namespace, name, version } = workflowId; return 'diagramEditor-' + `${namespace.replaceAll('.', '-')}-${name.replaceAll('.', '-')}-${version.replaceAll('.', '-')}`; } - _visualizeDiagramEditor(workflow) { - this._diagramEditorDialogOpened = true; - // Force new object to trigger re-render and not use stale workflow reference - this._currentDiagramEditorWorkflow = {...workflow}; - } - - _diagramEditorDialog() { - return html` - (this._diagramEditorDialogOpened = e.detail.value)} - ${dialogRenderer(() => this._diagramEditorContent(), [ - this._diagramEditorDialogOpened - ])} - .width=${'80%'} - .height=${'80%'} - resizable - draggable - header-title="Flow Diagram" - theme="${themeState.theme.name}"> - - `; - } - - _diagramEditorContent() { - return html` - - - `; + _viewDiagram(workflow) { + this._view = { mode: 'diagram', workflow } } _executeWorkflow(workflow) { - this._selectedWorkflow = workflow; + this._view = { mode: 'execute', workflow } } _showWorkflows() { - this._selectedWorkflow = null; + this._view = { mode: 'list', workflow: null } } } diff --git a/core/deployment/src/test/java/io/quarkiverse/flow/deployment/test/devui/WorkflowDiagramEditorRoundTripTest.java b/core/deployment/src/test/java/io/quarkiverse/flow/deployment/test/devui/WorkflowDiagramEditorRoundTripTest.java index a1c27c8cb..f29047a10 100644 --- a/core/deployment/src/test/java/io/quarkiverse/flow/deployment/test/devui/WorkflowDiagramEditorRoundTripTest.java +++ b/core/deployment/src/test/java/io/quarkiverse/flow/deployment/test/devui/WorkflowDiagramEditorRoundTripTest.java @@ -276,13 +276,52 @@ void editorRendersWorkflow( page.navigate(DEV_UI_URL); - // Step 1 — wait for the grid data to arrive (any eye button in the DOM). - // The vaadin-grid virtual-scroll only renders rows currently in the viewport; - // rows outside the visible area are not attached to the DOM at all. - // We use locator.evaluate() — Playwright resolves the vaadin-grid element - // through shadow DOM and then calls scrollToIndex on it directly, which is - // the only reliable way to reach into a shadow root from page.evaluate(). + // Step 1 — scroll the target row into teh DOM and click the eye button. + openDiagramFor(diagramEditorId); + + // Step 2 — the eye button navigates to a full page diagram screen. + // The shared header attaching is the signal the navigation happened + page.locator("qwc-flow-workflow-header") + .waitFor(new Locator.WaitForOptions() + .setState(WaitForSelectorState.ATTACHED)); + + // Step 3 — diagram container appears (loading completes) + page.locator("[data-testid='diagram-container']").waitFor(); + + // Step 4 — expected task node is rendered with correct text + Locator taskNode = page.locator("[data-testid='" + taskTestId + "']"); + taskNode.waitFor(); + assertThat(taskNode.textContent()) + .as("task node '%s' must contain text '%s'", taskTestId, taskName) + .contains(taskName); + + // Step 5 - navigate back and re-enter: re-renders correctly (no stale state) + page.locator(".backButton").click(); + page.locator("[data-testid='diagram-container']") + .waitFor(new Locator.WaitForOptions() + .setState(WaitForSelectorState.DETACHED)); + + // Back on the list the grid re-virtualises, so scroll to row again + openDiagramFor(diagramEditorId); + page.locator("[data-testid='diagram-container']").waitFor(); + + Locator taskNodeAfterReopen = page.locator("[data-testid='" + taskTestId + "']"); + taskNodeAfterReopen.waitFor(); + assertThat(taskNodeAfterReopen.textContent()) + .as("task node '%s' must still render correctly after reopen", taskTestId) + .contains(taskName); + } + + // Scrolls the workflow grid until the row for {@code diagramEditorId} is rendered, + // then clicks its eye button. + // The vaadin-grid virtual-scroll only renders rows currently in the viewport; + // rows outside the visible area are not attached to the DOM at all. We use + // locator.evaluate() — Playwright resolves the vaadin-grid element through shadow + // DOM and then calls scrollToIndex on it directly, which is the only reliable way + // to reach into a shadow root from page.evaluate(). + private void openDiagramFor(String diagramEditorId) { String buttonSelector = "#see-" + diagramEditorId; + // Wait for the grid data to arrive (any eye button in the DOM). page.waitForSelector("[id^='see-diagramEditor-']"); // Find the row index whose generated button id matches our target, then // scroll the grid to that index so Vaadin renders the row into the DOM. @@ -304,39 +343,6 @@ void editorRendersWorkflow( new Page.WaitForSelectorOptions() .setState(WaitForSelectorState.ATTACHED) .setTimeout(10_000)); - - // Step 2 — click eye button; wait for dialog to attach (it is inside a Vaadin - // overlay and never becomes "visible" in Playwright's sense while animating) page.locator(buttonSelector).click(); - page.locator("vaadin-dialog[opened]") - .waitFor(new Locator.WaitForOptions() - .setState(WaitForSelectorState.ATTACHED)); - - // Step 3 — diagram container appears (loading completes) - page.locator("[data-testid='diagram-container']").waitFor(); - - // Step 4 — expected task node is rendered with correct text - Locator taskNode = page.locator("[data-testid='" + taskTestId + "']"); - taskNode.waitFor(); - assertThat(taskNode.textContent()) - .as("task node '%s' must contain text '%s'", taskTestId, taskName) - .contains(taskName); - - // Step 5 — close and reopen: re-renders correctly (no stale state) - page.keyboard().press("Escape"); - page.locator("vaadin-dialog[opened]") - .waitFor(new Locator.WaitForOptions() - .setState(WaitForSelectorState.HIDDEN)); - page.locator(buttonSelector).click(); - page.locator("vaadin-dialog[opened]") - .waitFor(new Locator.WaitForOptions() - .setState(WaitForSelectorState.ATTACHED)); - page.locator("[data-testid='diagram-container']").waitFor(); - - Locator taskNodeAfterReopen = page.locator("[data-testid='" + taskTestId + "']"); - taskNodeAfterReopen.waitFor(); - assertThat(taskNodeAfterReopen.textContent()) - .as("task node '%s' must still render correctly after reopen", taskTestId) - .contains(taskName); } } diff --git a/core/deployment/src/test/java/io/quarkiverse/flow/deployment/test/devui/WorkflowDiagramEditorTest.java b/core/deployment/src/test/java/io/quarkiverse/flow/deployment/test/devui/WorkflowDiagramEditorTest.java index 8410e443a..87cab3031 100644 --- a/core/deployment/src/test/java/io/quarkiverse/flow/deployment/test/devui/WorkflowDiagramEditorTest.java +++ b/core/deployment/src/test/java/io/quarkiverse/flow/deployment/test/devui/WorkflowDiagramEditorTest.java @@ -82,7 +82,7 @@ void shouldRenderDiagramEditorWithSetEchoNode() { page.waitForSelector(BUTTON_SELECTOR); page.locator(BUTTON_SELECTOR).click(); - page.waitForSelector("vaadin-dialog[opened]", + page.waitForSelector("qwc-flow-workflow-header", new Page.WaitForSelectorOptions().setState(WaitForSelectorState.ATTACHED)); page.waitForSelector("[data-testid='diagram-container']");