Repository: deephaven/deephaven-plugins (language: python)

Issue:
deephaven.ui widgets in iframes open multiple panels
**Description**

When opening a deephaven.ui widget from the iframe/widget link, it opens up two panels unnecessarily.

**Steps to reproduce**

1. Create a widget:
```python
from deephaven.ui
b = ui.button("Hello!")
```
2. Open the iframe/widget link: http://localhost:10000/iframe/widget/?name=b

**Expected results**
2. Widget should appear, no panel tabs because it's just one component.

**Actual results**
2. Widget appears but there are two panel tabs. The one panel tab is empty.

**Additional details and attachments**

![image](https://github.com/user-attachments/assets/271e027d-add3-4b12-abbd-3e8fc8ae5c09)

I believe this is caused by @mattrunyon 's changes for https://github.com/deephaven/deephaven-plugins/pull/1023.
It is not present in the commit prior.

**Versions**

<!-- Version information. These versions can be copied from the Settings panel in the Deephaven UI. -->
Engine Version: 0.37.0
Web UI Version: 0.99.0
Python Version: 3.12.3
Java Version: 11.0.25
Groovy Version: 3.0.22
Barrage Version: 0.7.2
Browser Name: Chrome 130
User Agent OS: Linux
@deephaven/js-plugin-plotly-express: 0.12.0
@deephaven/js-plugin-ui: 0.23.1



Discussion / hints:
Investigated this a bit. It seems to be due to the panels in embed-widget being not closeable (which we did on purpose so  you can't get into unrecoverable states).

We could make panels closeable (would be needed if you had conditional rendering of panels anyway). Should also figure out why embed-widget is not re-using the panel like code-studio appears to be.

Fix patch:
diff --git a/plugins/ui/src/js/src/widget/DocumentUtils.tsx b/plugins/ui/src/js/src/widget/DocumentUtils.tsx
index 180295278..c145b5874 100644
--- a/plugins/ui/src/js/src/widget/DocumentUtils.tsx
+++ b/plugins/ui/src/js/src/widget/DocumentUtils.tsx
@@ -48,7 +48,7 @@ export function getRootChildren(
   if (nonLayoutCount === childrenArray.length) {
     // Just wrap it in a panel
     return (
-      <ReactPanel key="root" title={widget.name ?? widget.id ?? widget.type}>
+      <ReactPanel title={widget.name ?? widget.id ?? widget.type}>
         {children}
       </ReactPanel>
     );


Test patch:
diff --git a/tests/app.d/ui.py b/tests/app.d/ui.py
index 61990a1a5..0b57dc779 100644
--- a/tests/app.d/ui.py
+++ b/tests/app.d/ui.py
@@ -17,6 +17,14 @@ def ui_basic_component():
     )
 
 
+@ui.component
+def ui_multi_panel_component():
+    return [
+        ui.panel(ui.button("Hello"), title="foo"),
+        ui.panel(ui.text("World"), title="bar"),
+    ]
+
+
 @ui.component
 def ui_boom_component():
     raise Exception("BOOM!")
@@ -40,7 +48,7 @@ def ui_cell(label: str = "Cell"):
 
 
 @ui.component
-def ui_cells():
+def ui_cells_component():
     id_iter, _ = ui.use_state(lambda: count())
     cells, set_cells = ui.use_state(lambda: [next(id_iter)])
 
@@ -55,21 +63,37 @@ def delete_cell(delete_id: int):
             lambda i: ui.flex(
                 ui_cell(label=f"Cell {i}"),
                 ui.action_button(
-                    ui.icon("vsTrash"),
+                    ui.icon("trash"),
                     aria_label="Delete cell",
-                    on_press=lambda: delete_cell(i),
+                    on_press=lambda _: delete_cell(i),
                 ),
                 align_items="end",
                 key=str(i),
             ),
             cells,
         ),
-        ui.action_button(ui.icon("vsAdd"), "Add cell", on_press=add_cell),
+        ui.action_button(ui.icon("add"), "Add cell", on_press=add_cell),
         overflow="auto",
     )
 
 
 ui_component = ui_basic_component()
+ui_multi_panel = ui_multi_panel_component()
 ui_boom = ui_boom_component()
 ui_boom_counter = ui_boom_counter_component()
-ui_cells = ui_cells()
+ui_cells = ui_cells_component()
+
+ui_dashboard = ui.dashboard(
+    ui.column(
+        ui.stack(
+            ui.panel(ui_basic_component(), title="Component"),
+            ui.panel(ui_boom_counter_component(), title="Boom Counter"),
+            active_item_index=0,
+            height=75,
+        ),
+        ui.row(
+            ui_multi_panel_component(),
+            height=25,
+        ),
+    )
+)
diff --git a/tests/ui_embed_widget.spec.ts b/tests/ui_embed_widget.spec.ts
new file mode 100644
index 000000000..41f53178c
--- /dev/null
+++ b/tests/ui_embed_widget.spec.ts
@@ -0,0 +1,26 @@
+import { expect, test } from '@playwright/test';
+import { gotoPage, SELECTORS, waitForLoad } from './utils';
+
+test('UI single panel loads in embed widget', async ({ page }) => {
+  await gotoPage(page, '/iframe/widget/?name=ui_component');
+  await expect(page.locator(SELECTORS.REACT_PANEL_VISIBLE)).toBeVisible();
+  await waitForLoad(page);
+  await expect(page).toHaveScreenshot();
+});
+
+test('UI multi panel loads in embed widget', async ({ page }) => {
+  await gotoPage(page, '/iframe/widget/?name=ui_multi_panel');
+  await expect(page.locator(SELECTORS.REACT_PANEL)).toHaveCount(2);
+  // Wait for the titles because embed-widget has a slight delay in showing the headers
+  await expect(page.getByText('foo')).toBeVisible();
+  await expect(page.getByText('bar')).toBeVisible();
+  await waitForLoad(page);
+  await expect(page).toHaveScreenshot();
+});
+
+test('UI dashboard loads in embed widget', async ({ page }) => {
+  await gotoPage(page, '/iframe/widget/?name=ui_dashboard');
+  await expect(page.locator(SELECTORS.REACT_PANEL)).toHaveCount(4);
+  await waitForLoad(page);
+  await expect(page).toHaveScreenshot();
+});
diff --git a/tests/ui_embed_widget.spec.ts-snapshots/UI-dashboard-loads-in-embed-widget-1-chromium-linux.png b/tests/ui_embed_widget.spec.ts-snapshots/UI-dashboard-loads-in-embed-widget-1-chromium-linux.png
new file mode 100644
index 000000000..09460d49b
Binary files /dev/null and b/tests/ui_embed_widget.spec.ts-snapshots/UI-dashboard-loads-in-embed-widget-1-chromium-linux.png differ
diff --git a/tests/ui_embed_widget.spec.ts-snapshots/UI-dashboard-loads-in-embed-widget-1-firefox-linux.png b/tests/ui_embed_widget.spec.ts-snapshots/UI-dashboard-loads-in-embed-widget-1-firefox-linux.png
new file mode 100644
index 000000000..bfc8ae589
Binary files /dev/null and b/tests/ui_embed_widget.spec.ts-snapshots/UI-dashboard-loads-in-embed-widget-1-firefox-linux.png differ
diff --git a/tests/ui_embed_widget.spec.ts-snapshots/UI-dashboard-loads-in-embed-widget-1-webkit-linux.png b/tests/ui_embed_widget.spec.ts-snapshots/UI-dashboard-loads-in-embed-widget-1-webkit-linux.png
new file mode 100644
index 000000000..03f422ca6
Binary files /dev/null and b/tests/ui_embed_widget.spec.ts-snapshots/UI-dashboard-loads-in-embed-widget-1-webkit-linux.png differ
diff --git a/tests/ui_embed_widget.spec.ts-snapshots/UI-multi-panel-loads-in-embed-widget-1-chromium-linux.png b/tests/ui_embed_widget.spec.ts-snapshots/UI-multi-panel-loads-in-embed-widget-1-chromium-linux.png
new file mode 100644
index 000000000..1a97bd92b
Binary files /dev/null and b/tests/ui_embed_widget.spec.ts-snapshots/UI-multi-panel-loads-in-embed-widget-1-chromium-linux.png differ
diff --git a/tests/ui_embed_widget.spec.ts-snapshots/UI-multi-panel-loads-in-embed-widget-1-firefox-linux.png b/tests/ui_embed_widget.spec.ts-snapshots/UI-multi-panel-loads-in-embed-widget-1-firefox-linux.png
new file mode 100644
index 000000000..14e0326d9
Binary files /dev/null and b/tests/ui_embed_widget.spec.ts-snapshots/UI-multi-panel-loads-in-embed-widget-1-firefox-linux.png differ
diff --git a/tests/ui_embed_widget.spec.ts-snapshots/UI-multi-panel-loads-in-embed-widget-1-webkit-linux.png b/tests/ui_embed_widget.spec.ts-snapshots/UI-multi-panel-loads-in-embed-widget-1-webkit-linux.png
new file mode 100644
index 000000000..6a98fab1e
Binary files /dev/null and b/tests/ui_embed_widget.spec.ts-snapshots/UI-multi-panel-loads-in-embed-widget-1-webkit-linux.png differ
diff --git a/tests/ui_embed_widget.spec.ts-snapshots/UI-single-panel-loads-in-embed-widget-1-chromium-linux.png b/tests/ui_embed_widget.spec.ts-snapshots/UI-single-panel-loads-in-embed-widget-1-chromium-linux.png
new file mode 100644
index 000000000..edf6f1435
Binary files /dev/null and b/tests/ui_embed_widget.spec.ts-snapshots/UI-single-panel-loads-in-embed-widget-1-chromium-linux.png differ
diff --git a/tests/ui_embed_widget.spec.ts-snapshots/UI-single-panel-loads-in-embed-widget-1-firefox-linux.png b/tests/ui_embed_widget.spec.ts-snapshots/UI-single-panel-loads-in-embed-widget-1-firefox-linux.png
new file mode 100644
index 000000000..0f756e1fb
Binary files /dev/null and b/tests/ui_embed_widget.spec.ts-snapshots/UI-single-panel-loads-in-embed-widget-1-firefox-linux.png differ
diff --git a/tests/ui_embed_widget.spec.ts-snapshots/UI-single-panel-loads-in-embed-widget-1-webkit-linux.png b/tests/ui_embed_widget.spec.ts-snapshots/UI-single-panel-loads-in-embed-widget-1-webkit-linux.png
new file mode 100644
index 000000000..0d9f1aa53
Binary files /dev/null and b/tests/ui_embed_widget.spec.ts-snapshots/UI-single-panel-loads-in-embed-widget-1-webkit-linux.png differ
diff --git a/tests/utils.ts b/tests/utils.ts
index f5c4eb058..6adc2ee3a 100644
--- a/tests/utils.ts
+++ b/tests/utils.ts
@@ -25,10 +25,18 @@ export async function gotoPage(
     await page.goto(url, options);
     await expect(
       page.getByRole('progressbar', { name: 'Loading...', exact: true })
-    ).not.toBeVisible();
+    ).toHaveCount(0);
   });
 }
 
+/**
+ * Waits for all loading spinners to disappear
+ * @param page The page
+ */
+export async function waitForLoad(page: Page): Promise<void> {
+  await expect(page.locator('.loading-spinner')).toHaveCount(0);
+}
+
 /**
  * Opens a panel by clicking on the Panels button and then the panel button
  * @param page The page
@@ -71,7 +79,7 @@ export async function openPanel(
     // check for panel to be loaded
     await expect(page.locator(panelLocator)).toHaveCount(panelCount + 1);
     if (awaitLoad) {
-      await expect(page.locator('.loading-spinner')).toHaveCount(0);
+      await waitForLoad(page);
     }
   });
 }