Repository: grafana/scenes
Pull request #1093: fix: (VizPanelRenderer) - move showMenuAlways prop to the top level
Author: login: L2D2Grafana
Labels: name: minor
description: Increment the minor version when merged
name: release
description: Create a release when this pr is merged
name: released
Created: 2025-04-09T17:49:55Z | Merged: 2025-04-15T15:46:59Z

# Description
Move showMenuAlways VizPanelRenderer.tsx prop out of collapse conditional. In Logs Drilldown we have a situation where we'd like to `showMenuAlways` but do not use collapsible. [upgrade to Scenes6](https://github.com/grafana/logs-drilldown/pull/1019/files#diff-0c9eb85b8eb906fbac0d474a7c68685514172ee72d0597664fa8618f27826087)
I'm not totally familiar with the change to making `showMenuAlways` conditional on the `collapsible` property [PR](https://github.com/grafana/scenes/pull/1040/files#diff-b4e762e26aeb297a8f36c1ada3213ecd5a9bcd972799a753018dccf96fc75a30R195
).🤔 @tskarhed let me know what you think. 

<!-- GITHUB_RELEASE PR BODY: canary-version -->
<details>
  <summary>📦 Published PR as canary version: <code>6.9.0--canary.1093.14409340532.0</code></summary>
  <br />

  :sparkles: Test out this PR locally via:
  
  ```bash
  npm install @grafana/scenes-react@6.9.0--canary.1093.14409340532.0
  npm install @grafana/scenes@6.9.0--canary.1093.14409340532.0
  # or 
  yarn add @grafana/scenes-react@6.9.0--canary.1093.14409340532.0
  yarn add @grafana/scenes@6.9.0--canary.1093.14409340532.0
  ```
</details>
<!-- GITHUB_RELEASE PR BODY: canary-version -->



Diff:
diff --git a/packages/scenes/src/components/VizPanel/VizPanel.test.tsx b/packages/scenes/src/components/VizPanel/VizPanel.test.tsx
index 7bf152891..a8a8ba966 100644
--- a/packages/scenes/src/components/VizPanel/VizPanel.test.tsx
+++ b/packages/scenes/src/components/VizPanel/VizPanel.test.tsx
@@ -24,6 +24,7 @@ import { SeriesVisibilityChangeMode } from '@grafana/ui';
 import { SceneTimeRange } from '../../core/SceneTimeRange';
 import { act, render, screen } from '@testing-library/react';
 import { RefreshEvent } from '@grafana/runtime';
+import { VizPanelMenu } from './VizPanelMenu';
 
 let pluginToLoad: PanelPlugin | undefined;
 
@@ -849,6 +850,109 @@ describe('VizPanel', () => {
       });
     });
   });
+
+  describe('Menu visibility functionality', () => {
+    let panel: VizPanel<OptionsPlugin1, FieldConfigPlugin1>;
+
+    describe('collapsible false and showMenuAlways true', () => {
+      beforeEach(async () => {
+        panel = new VizPanel<OptionsPlugin1, FieldConfigPlugin1>({
+          title: 'Menu Panel',
+          pluginId: 'custom-plugin-id',
+          collapsible: false,
+          showMenuAlways: true,
+          menu: new VizPanelMenu({}),
+          $data: getDataNodeWithTestData(),
+        });
+
+        pluginToLoad = getTestPlugin1();
+        panel.activate();
+      });
+
+      it('should showMenuAlways when panel is not collapsible', async () => {
+        expect(panel.state.showMenuAlways).toBe(true);
+        render(<panel.Component model={panel} />);
+
+        // The menu should be visible despite collapsible being false
+        const menuButton = screen.queryByRole('button', { name: /menu/i });
+        expect(menuButton).toBeInTheDocument();
+      });
+    });
+
+    describe('collapsible true and showMenuAlways true', () => {
+      beforeEach(async () => {
+        panel = new VizPanel<OptionsPlugin1, FieldConfigPlugin1>({
+          title: 'Menu Panel',
+          pluginId: 'custom-plugin-id',
+          collapsible: true,
+          showMenuAlways: true,
+          menu: new VizPanelMenu({}),
+          $data: getDataNodeWithTestData(),
+        });
+
+        pluginToLoad = getTestPlugin1();
+        panel.activate();
+      });
+
+      it('should show showMenuAlways when panel is collapsible', async () => {
+        expect(panel.state.showMenuAlways).toBe(true);
+        render(<panel.Component model={panel} />);
+        // The menu should be visible because both collapsible and showMenuAlways are true
+        // Query for all menu buttons and select the first one
+        const menuButtons = screen.queryAllByRole('button', { name: /menu/i });
+        const firstMenuButton = menuButtons[0]; // Get the first element
+        // Check if the first menu button is in the document
+        expect(firstMenuButton).toBeInTheDocument();
+      });
+    });
+
+    describe('showMenuAlways true', () => {
+      beforeEach(async () => {
+        panel = new VizPanel<OptionsPlugin1, FieldConfigPlugin1>({
+          title: 'Menu Panel',
+          pluginId: 'custom-plugin-id',
+          showMenuAlways: true,
+          menu: new VizPanelMenu({}),
+          $data: getDataNodeWithTestData(),
+        });
+
+        pluginToLoad = getTestPlugin1();
+        panel.activate();
+      });
+
+      it('should showMenuAlways when panel is not collapsible', async () => {
+        expect(panel.state.showMenuAlways).toBe(true);
+        render(<panel.Component model={panel} />);
+
+        // The menu should be visible despite collapsible being false
+        const menuButton = screen.queryByRole('button', { name: /menu/i });
+        expect(menuButton).toBeInTheDocument();
+      });
+    });
+
+    describe('showMenuAlways false', () => {
+      beforeEach(async () => {
+        panel = new VizPanel<OptionsPlugin1, FieldConfigPlugin1>({
+          title: 'Menu Panel',
+          pluginId: 'custom-plugin-id',
+          showMenuAlways: false,
+          menu: new VizPanelMenu({}),
+          $data: getDataNodeWithTestData(),
+        });
+
+        pluginToLoad = getTestPlugin1();
+        panel.activate();
+      });
+
+      it('should not showMenuAlways', async () => {
+        expect(panel.state.showMenuAlways).toBe(false);
+        render(<panel.Component model={panel} />);
+
+        const menuButton = screen.queryByRole('button', { name: /menu/i });
+        expect(menuButton).toHaveClass('show-on-hover');
+      });
+    });
+  });
 });
 
 function getDataNodeWithTestData() {
diff --git a/packages/scenes/src/components/VizPanel/VizPanelRenderer.tsx b/packages/scenes/src/components/VizPanel/VizPanelRenderer.tsx
index f5b30ee60..2853dddf5 100644
--- a/packages/scenes/src/components/VizPanel/VizPanelRenderer.tsx
+++ b/packages/scenes/src/components/VizPanel/VizPanelRenderer.tsx
@@ -169,6 +169,7 @@ export function VizPanelRenderer({ model }: SceneComponentProps<VizPanel>) {
     <div className={relativeWrapper}>
       <div ref={ref as RefCallback<HTMLDivElement>} className={absoluteWrapper} data-viz-panel-key={model.state.key}>
         {width > 0 && height > 0 && (
+          // @ts-expect-error showMenuAlways remove when updating to @grafana/ui@12 fixed in https://github.com/grafana/grafana/pull/103553
           <PanelChrome
             title={titleInterpolated}
             description={description?.trim() ? model.getDescription : undefined}
@@ -192,12 +193,12 @@ export function VizPanelRenderer({ model }: SceneComponentProps<VizPanel>) {
             onDragStart={(e: React.PointerEvent) => {
               dragHooks.onDragStart?.(e, model);
             }}
+            showMenuAlways={showMenuAlways}
             {...(collapsible
               ? {
                   collapsible: Boolean(collapsible),
                   collapsed,
                   onToggleCollapse: model.onToggleCollapse,
-                  showMenuAlways,
                 }
               : { hoverHeader, hoverHeaderOffset })}
           >


Reviews:
user: login: gtk-grafana
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T18:28:32Z
user: login: gtk-grafana
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T20:00:16Z
user: login: gtk-grafana
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T20:03:37Z
user: login: gtk-grafana
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T20:04:58Z
user: login: gtk-grafana
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T20:06:28Z
user: login: L2D2Grafana
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T21:47:29Z
user: login: gtk-grafana
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-10T12:10:08Z
user: login: torkelo
body: I don't understand all the tests, a don't see any corresponding logic ? 

looks like they just test the constructor passes state props to state? 
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-10T12:49:11Z
user: login: gtk-grafana
body: Looks good, thanks for taking a look at this @L2D2Grafana!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T12:10:53Z

Review comments:
diff_hunk: @@ -192,14 +192,14 @@ export function VizPanelRenderer({ model }: SceneComponentProps<VizPanel>) {
             onDragStart={(e: React.PointerEvent) => {
               dragHooks.onDragStart?.(e, model);
             }}
-            {...(collapsible
-              ? {
-                  collapsible: Boolean(collapsible),
-                  collapsed,
-                  onToggleCollapse: model.onToggleCollapse,
-                  showMenuAlways,
-                }
-              : { hoverHeader, hoverHeaderOffset })}
+            {...{
+              ...(collapsible !== undefined && { collapsible: Boolean(collapsible) }),
+              ...(collapsed !== undefined && { collapsed }),
+              ...(model.onToggleCollapse && { onToggleCollapse: model.onToggleCollapse }),
+              ...(showMenuAlways !== undefined && { showMenuAlways }),
path: packages/scenes/src/components/VizPanel/VizPanelRenderer.tsx
user: login: gtk-grafana
body: I think the `showMenuAlways` should be moved outside of the collapsible conditional, I had added the type incorrectly (but it was [fixed recently](https://github.com/grafana/grafana/pull/103553)) to the collapsible interface, but it should be a valid prop whenever the hoverHeader is not set. Might require a `@ts-expect-error` though until the version of `@grafana/grafana-ui` that contains [that PR](https://github.com/grafana/grafana/pull/103553) is added as a dep
created_at: 2025-04-09T18:28:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -192,12 +192,12 @@ export function VizPanelRenderer({ model }: SceneComponentProps<VizPanel>) {
             onDragStart={(e: React.PointerEvent) => {
               dragHooks.onDragStart?.(e, model);
             }}
+            showMenuAlways={showMenuAlways}
path: packages/scenes/src/components/VizPanel/VizPanelRenderer.tsx
user: login: gtk-grafana
body: this'll need a ts-expect-error until grafana-ui is updated to v12 and the type fix is available
created_at: 2025-04-09T20:00:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -849,6 +849,96 @@ describe('VizPanel', () => {
       });
     });
   });
+
+  describe('Collapsible panel functionality', () => {
+    let panel: VizPanel<OptionsPlugin1, FieldConfigPlugin1>;
+
+    beforeEach(async () => {
+      panel = new VizPanel<OptionsPlugin1, FieldConfigPlugin1>({
+        title: 'Collapsible Panel',
+        pluginId: 'custom-plugin-id',
+        collapsible: true,
+        collapsed: false,
+        $data: getDataNodeWithTestData(),
+      });
+
+      pluginToLoad = getTestPlugin1();
+      panel.activate();
+    });
+
+    it('should initialize with correct collapsible state', () => {
+      expect(panel.state.collapsible).toBe(true);
+      expect(panel.state.collapsed).toBe(false);
+    });
+
+    it('should toggle collapsed state', () => {
+      panel.onToggleCollapse(true);
+      expect(panel.state.collapsed).toBe(true);
+
+      panel.onToggleCollapse(false);
+      expect(panel.state.collapsed).toBe(false);
+    });
path: packages/scenes/src/components/VizPanel/VizPanel.test.tsx
user: login: gtk-grafana
body: let's assert in here that we can set the `showMenuAlways` when collapsible is true
created_at: 2025-04-09T20:03:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -192,12 +192,12 @@ export function VizPanelRenderer({ model }: SceneComponentProps<VizPanel>) {
             onDragStart={(e: React.PointerEvent) => {
               dragHooks.onDragStart?.(e, model);
             }}
+            showMenuAlways={showMenuAlways}
path: packages/scenes/src/components/VizPanel/VizPanelRenderer.tsx
user: login: gtk-grafana
body: let's make sure to add a comment explaining why, and link to the PR with the fix: https://github.com/grafana/grafana/pull/103553
created_at: 2025-04-09T20:04:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -849,6 +849,96 @@ describe('VizPanel', () => {
       });
     });
   });
+
+  describe('Collapsible panel functionality', () => {
+    let panel: VizPanel<OptionsPlugin1, FieldConfigPlugin1>;
+
+    beforeEach(async () => {
+      panel = new VizPanel<OptionsPlugin1, FieldConfigPlugin1>({
+        title: 'Collapsible Panel',
+        pluginId: 'custom-plugin-id',
+        collapsible: true,
+        collapsed: false,
+        $data: getDataNodeWithTestData(),
+      });
+
+      pluginToLoad = getTestPlugin1();
+      panel.activate();
+    });
+
+    it('should initialize with correct collapsible state', () => {
+      expect(panel.state.collapsible).toBe(true);
+      expect(panel.state.collapsed).toBe(false);
+    });
+
+    it('should toggle collapsed state', () => {
+      panel.onToggleCollapse(true);
+      expect(panel.state.collapsed).toBe(true);
+
+      panel.onToggleCollapse(false);
+      expect(panel.state.collapsed).toBe(false);
+    });
path: packages/scenes/src/components/VizPanel/VizPanel.test.tsx
user: login: gtk-grafana
body: and we should also assert that we can set the `showMenuAlways` when the width is set and also when undefined.
created_at: 2025-04-09T20:06:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -849,6 +849,96 @@ describe('VizPanel', () => {
       });
     });
   });
+
+  describe('Collapsible panel functionality', () => {
+    let panel: VizPanel<OptionsPlugin1, FieldConfigPlugin1>;
+
+    beforeEach(async () => {
+      panel = new VizPanel<OptionsPlugin1, FieldConfigPlugin1>({
+        title: 'Collapsible Panel',
+        pluginId: 'custom-plugin-id',
+        collapsible: true,
+        collapsed: false,
+        $data: getDataNodeWithTestData(),
+      });
+
+      pluginToLoad = getTestPlugin1();
+      panel.activate();
+    });
+
+    it('should initialize with correct collapsible state', () => {
+      expect(panel.state.collapsible).toBe(true);
+      expect(panel.state.collapsed).toBe(false);
+    });
+
+    it('should toggle collapsed state', () => {
+      panel.onToggleCollapse(true);
+      expect(panel.state.collapsed).toBe(true);
+
+      panel.onToggleCollapse(false);
+      expect(panel.state.collapsed).toBe(false);
+    });
path: packages/scenes/src/components/VizPanel/VizPanel.test.tsx
user: login: L2D2Grafana
body: Updated the collapsible test with `showMenuAlways`. Not sure I understand, how is that dependent on width?
created_at: 2025-04-09T21:47:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -849,6 +849,96 @@ describe('VizPanel', () => {
       });
     });
   });
+
+  describe('Collapsible panel functionality', () => {
+    let panel: VizPanel<OptionsPlugin1, FieldConfigPlugin1>;
+
+    beforeEach(async () => {
+      panel = new VizPanel<OptionsPlugin1, FieldConfigPlugin1>({
+        title: 'Collapsible Panel',
+        pluginId: 'custom-plugin-id',
+        collapsible: true,
+        collapsed: false,
+        $data: getDataNodeWithTestData(),
+      });
+
+      pluginToLoad = getTestPlugin1();
+      panel.activate();
+    });
+
+    it('should initialize with correct collapsible state', () => {
+      expect(panel.state.collapsible).toBe(true);
+      expect(panel.state.collapsed).toBe(false);
+    });
+
+    it('should toggle collapsed state', () => {
+      panel.onToggleCollapse(true);
+      expect(panel.state.collapsed).toBe(true);
+
+      panel.onToggleCollapse(false);
+      expect(panel.state.collapsed).toBe(false);
+    });
path: packages/scenes/src/components/VizPanel/VizPanel.test.tsx
user: login: gtk-grafana
body: Just another way to assert that the showMenuAlways prop is valid for all panel chrome subtypes except for `HoverHeader`. Look at the [PanelChromeProp types definition](https://github.com/grafana/grafana/blob/main/packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx#L28).

It is probably overkill but would be nice to demonstrate that the ability to set the panel menu as always visible should be an option if the `PanelChromeProps` type is `Collapsible`, `FixedDimensions`, or `AutoSize` but not `HoverHeader`.

This way if someone tries to remove that ts-expect-error in the future before the type fix is pulled in the unit tests will provide an extra layer of insulation against a breaking change and hint to people that the types are wrong.

created_at: 2025-04-10T12:10:08Z
author_association: CONTRIBUTOR

PR comments:
user: login: gtk-grafana
created_at: 2025-04-10T13:00:30Z
body: > I don't understand all the tests, a don't see any corresponding logic ?
> 
> looks like they just test the constructor passes state props to state?

These tests are just trying to ensure that we don't break the desired functionality to match the types that were incorrect until this [recent fix in g12](https://github.com/grafana/grafana/pull/103553), like what happened here: https://github.com/grafana/scenes/pull/1040/files#diff-b4e762e26aeb297a8f36c1ada3213ecd5a9bcd972799a753018dccf96fc75a30R195.
author_association: CONTRIBUTOR
user: login: L2D2Grafana
created_at: 2025-04-11T18:08:33Z
body: > I don't understand all the tests, a don't see any corresponding logic ?
> 
> looks like they just test the constructor passes state props to state?

Updated it to check for the dom element as well.
author_association: CONTRIBUTOR
user: login: scenes-repo-bot-access-token[bot]
created_at: 2025-04-17T13:53:31Z
body: <!-- GITHUB_RELEASE COMMENT: released -->
:rocket: PR was released in [`v6.9.0`](https://github.com/grafana/scenes/releases/tag/v6.9.0) :rocket:
author_association: NONE