Repository: grafana/grafana
Pull request #103488: AppChrome: Improved responsive use of vertical space 
Author: login: torkelo
Labels: name: type/docs
description: Flags the technical writing team for documentation support; auto adds to org-wide docs project
name: area/dashboard/templating
name: area/backend
name: area/explore
name: area/frontend
name: no-changelog
description: Skip including change in changelog/release notes
Created: 2025-04-05T13:34:03Z | Merged: 2025-04-09T12:51:23Z

Problem this PR is trying to solve

* Poor use of vertical space with 2 navbar levels and the second level is only used for 1-3 actions 
* Poor responsive behaviors on smaller screens given the new additions to navbar (Invite button, unified history, sidebar apps) 
* Spacing is a bit tight between search input and navbar border (4px), 
* [Users complaining about not being able to collapse the top (or bottom level anymore)](https://github.com/grafana/grafana/issues/99946). 
* Fixes https://github.com/grafana/grafana/issues/99946

Solution
* Switch to 1 level when 
  * viewport is > xl breakpoint (1200) 
  * AND mega menu is closed 
  * AND scopes is disabled (Solve later) 
  * AND scenes is enabled (Old dashboards have a time picker in the nav toolbar and it does not work well in a single level) 
  
### I know this is a big change late in the game for G12, I am sorry about that, please focus on helping test and review it 

We need to make this change asap as it will force us to become much more careful about adding actions and trying to simplify what we put in the navbar/toolbar. The longer the wait the harder it will be as more actions are added (invite button, unified history, sidebar etc) thinking we have the space when we don't. 

Changes & fixes 
* New unifiedNavbars feature toggle
  * Controls the new responsive behaviors for page actions (single level on large screens when mega menu is undocked) 
  * <strike>Controls the change in level height from 40 to 48px </strike> (Reverted height change for now)
* This tries to improve AppChrome responsive behavior by unifying the two navbar levels into 1 on larger screens, only goes to 1 level when mega menu is undocked
* Improves responsive behavior on smaller screens by hiding invite button and sidebar button (For now, we can improve this and add them to the second level if we want to have them available on mobile)
* Fixes bug in explore with docked nav not undocking when screen size is reduced, related to buggy useMediaQueryChange hook
* Fixes buggy / poorly designed useMediaQueryChange hook  (onChange was never memorized so always triggered the useEffect, and having multiple useMediaQueryChange with the same breakpoint caused conflicts) 
* Adds new useMediaQueryMinWidth hook 
* Tries to refactor and unify logic of how many levels to show into a single place / hook.  This was spread out between headerHeightObservable in AppChromeService and useChromeHeaderHeight (and elsewhere)  
* Improve / refactor logic in AppChrome around docking / undocking mega menu when screen size change 
* Hide the invite button when there are page actions in the navbar  (temp fix to help simplify nav)  
* Hide unified history on mobile* 
* Fixes dashboard dynamic right side actions added to wrong side 
* Fixes bug https://github.com/grafana/grafana/issues/103495
* Make search / command palette trigger width responsive 
* Move kiosk toggle button to only exist on dashboards

Extracted part of the changes into a separate PR to help review this:
https://github.com/grafana/grafana/pull/103507/f

Bugs in main identified but not fixed
* We add buttons to dashboard that conditionally render themselves (ie render null if there are not alert rules for example for the alert rule button). This still creates entries in the ToolbarButtonRow and causes extra spacing between buttons and sometimes ghost overflow buttons (ie you get the ... dot overflow button dropdown but with empty dropdown). 

### Scopes 

When scopes is enabled 2 levels are always used. Fitting scopes into the single level is a bit tricky and more work is needed there and finding a place for the scope dashboards toggle. But breaking up the problem here and making it so we can tackle scopes separately. So for now if scopes are enabled you will always see the two navbar levels. 

Things to test

* Disable scopeFilters feature toggle otherwise you will never see the unified single level 
* Test with unifiedNavbars disabled 
* Test with dashboardsNewLayouts 
* Test with dashboardsNewLayouts disabled 
* Test with unified history enabled 
* Test invite button enabled (needs external user management as well) 
* Enable scopes and make sure you always see two levels 

Follow-ups / things that can help simplify and clean up the navbar 

* The invite button to takes up too precious space,  put it in mega menu? 
* The TV kiosk button I think belongs in profile drawer as toggle switch 
* The create actions are also available in the command palette, do we need both? 
* The export button on dashboards feels unnecessary , move into share button menu? 
* [Explore actions should use small buttons](https://github.com/grafana/grafana/pull/103496) 
* Alert rule edit page should use fewer actions (not two save primary actions), and long term should not use them at all as, should use actions on Page level instead. 
* Simplify / reduce panel edit actions 
* Reduce / combine dashboard actions 
* Rethink how we add separtor lines between buttons, and where ToolbarButtonRow is rendered (probably best to leave that to AppChrome) 

[grafana_responsive_improvements.webm](https://github.com/user-attachments/assets/e99236ca-8716-4f3d-a76b-131735dfb099)


![Screenshot 2025-04-05 at 16 16 25](https://github.com/user-attachments/assets/873128ec-da58-482a-910e-c35fdbdec73b)

With 48px height. (I reverted this as scopes feature toggle makes it so we always show two levels and always having two levels each 48px feels bad). If I can refactor it so that when two levels are shown we revert to 40 pixel levels then maybe we can keep single level at 48 but anyway revert it for now. 
![Screenshot 2025-04-05 at 15 46 46](https://github.com/user-attachments/assets/2f95778d-869c-409d-8b13-4c54b8aba401)





Diff:
diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
index 54453fa6e674c..a9318226a735a 100644
--- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
+++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
@@ -86,6 +86,7 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general-
 | `azureMonitorEnableUserAuth`           | Enables user auth for Azure Monitor datasource only                                                                                 | Yes                |
 | `alertingNotificationsStepMode`        | Enables simplified step mode in the notifications section                                                                           | Yes                |
 | `lokiLabelNamesQueryApi`               | Defaults to using the Loki `/labels` API instead of `/series`                                                                       | Yes                |
+| `unifiedNavbars`                       | Enables unified navbars                                                                                                             |                    |
 
 ## Public preview feature toggles
 
diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts
index 93d2201e0767b..de4c0da72921a 100644
--- a/packages/grafana-data/src/types/featureToggles.gen.ts
+++ b/packages/grafana-data/src/types/featureToggles.gen.ts
@@ -1032,6 +1032,11 @@ export interface FeatureToggles {
   */
   localizationForPlugins?: boolean;
   /**
+  * Enables unified navbars
+  * @default false
+  */
+  unifiedNavbars?: boolean;
+  /**
   * Enables a control component for the logs panel in Explore
   * @default false
   */
diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go
index 2471e77f5bd9c..f489bd7949b67 100644
--- a/pkg/services/featuremgmt/registry.go
+++ b/pkg/services/featuremgmt/registry.go
@@ -1775,6 +1775,14 @@ var (
 			Owner:        grafanaPluginsPlatformSquad,
 			FrontendOnly: false,
 		},
+		{
+			Name:         "unifiedNavbars",
+			Description:  "Enables unified navbars",
+			Stage:        FeatureStageGeneralAvailability,
+			Owner:        grafanaPluginsPlatformSquad,
+			FrontendOnly: true,
+			Expression:   "false", // enabled by default
+		},
 		{
 			Name:         "logsPanelControls",
 			Description:  "Enables a control component for the logs panel in Explore",
diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv
index e53cf9603c53f..365ab46eeeb7e 100644
--- a/pkg/services/featuremgmt/toggles_gen.csv
+++ b/pkg/services/featuremgmt/toggles_gen.csv
@@ -232,4 +232,5 @@ alertingRuleRecoverDeleted,GA,@grafana/alerting-squad,false,false,true
 xrayApplicationSignals,experimental,@grafana/aws-datasources,false,false,true
 multiTenantTempCredentials,experimental,@grafana/aws-datasources,false,false,false
 localizationForPlugins,experimental,@grafana/plugins-platform-backend,false,false,false
+unifiedNavbars,GA,@grafana/plugins-platform-backend,false,false,true
 logsPanelControls,privatePreview,@grafana/observability-logs,false,false,true
diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go
index 512306a266c0f..86b813a15ef98 100644
--- a/pkg/services/featuremgmt/toggles_gen.go
+++ b/pkg/services/featuremgmt/toggles_gen.go
@@ -939,6 +939,10 @@ const (
 	// Enables localization for plugins
 	FlagLocalizationForPlugins = "localizationForPlugins"
 
+	// FlagUnifiedNavbars
+	// Enables unified navbars
+	FlagUnifiedNavbars = "unifiedNavbars"
+
 	// FlagLogsPanelControls
 	// Enables a control component for the logs panel in Explore
 	FlagLogsPanelControls = "logsPanelControls"
diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json
index 4cb98fea89d3d..7e14fd191ebb0 100644
--- a/pkg/services/featuremgmt/toggles_gen.json
+++ b/pkg/services/featuremgmt/toggles_gen.json
@@ -3053,6 +3053,23 @@
         "frontend": true
       }
     },
+    {
+      "metadata": {
+        "name": "unifiedNavbars",
+        "resourceVersion": "1744174965165",
+        "creationTimestamp": "2025-04-05T06:53:21Z",
+        "annotations": {
+          "grafana.app/updatedTimestamp": "2025-04-09 05:02:45.165634 +0000 UTC"
+        }
+      },
+      "spec": {
+        "description": "Enables unified navbars",
+        "stage": "GA",
+        "codeowner": "@grafana/plugins-platform-backend",
+        "frontend": true,
+        "expression": "false"
+      }
+    },
     {
       "metadata": {
         "name": "unifiedRequestLog",
diff --git a/public/app/app.ts b/public/app/app.ts
index 9ba23db15e750..b7bab973b852a 100644
--- a/public/app/app.ts
+++ b/public/app/app.ts
@@ -57,10 +57,11 @@ import getDefaultMonacoLanguages from '../lib/monaco-languages';
 import { AppWrapper } from './AppWrapper';
 import appEvents from './core/app_events';
 import { AppChromeService } from './core/components/AppChrome/AppChromeService';
+import { useChromeHeaderHeight } from './core/components/AppChrome/TopBar/useChromeHeaderHeight';
 import { LazyFolderPicker } from './core/components/NestedFolderPicker/LazyFolderPicker';
 import { getAllOptionEditors, getAllStandardFieldConfigs } from './core/components/OptionsUI/registry';
 import { PluginPage } from './core/components/Page/PluginPage';
-import { GrafanaContextType, useChromeHeaderHeight, useReturnToPreviousInternal } from './core/context/GrafanaContext';
+import { GrafanaContextType, useReturnToPreviousInternal } from './core/context/GrafanaContext';
 import { initializeCrashDetection } from './core/crash';
 import { initializeI18n } from './core/internationalization';
 import { setMonacoEnv } from './core/monacoEnv';
diff --git a/public/app/core/components/AppChrome/AppChrome.tsx b/public/app/core/components/AppChrome/AppChrome.tsx
index d47465b7508c7..4cd95dd8d4e5f 100644
--- a/public/app/core/components/AppChrome/AppChrome.tsx
+++ b/public/app/core/components/AppChrome/AppChrome.tsx
@@ -20,8 +20,7 @@ import { MegaMenu, MENU_WIDTH } from './MegaMenu/MegaMenu';
 import { useMegaMenuFocusHelper } from './MegaMenu/utils';
 import { ReturnToPrevious } from './ReturnToPrevious/ReturnToPrevious';
 import { SingleTopBar } from './TopBar/SingleTopBar';
-import { SingleTopBarActions } from './TopBar/SingleTopBarActions';
-import { TOP_BAR_LEVEL_HEIGHT } from './types';
+import { getChromeHeaderLevelHeight, useChromeHeaderLevels } from './TopBar/useChromeHeaderHeight';
 
 export interface Props extends PropsWithChildren<{}> {}
 
@@ -35,7 +34,9 @@ export function AppChrome({ children }: Props) {
   const isScopesDashboardsOpen = Boolean(
     scopes?.state.enabled && scopes?.state.drawerOpened && !scopes?.state.readOnly
   );
-  const styles = useStyles2(getStyles, Boolean(state.actions) || !!scopes?.state.enabled);
+
+  const headerLevels = useChromeHeaderLevels();
+  const styles = useStyles2(getStyles, headerLevels * getChromeHeaderLevelHeight());
 
   useResponsiveDockedMegaMenu(chrome);
   useMegaMenuFocusHelper(state.megaMenuOpen, state.megaMenuDocked);
@@ -92,8 +93,11 @@ export function AppChrome({ children }: Props) {
               pageNav={state.pageNav}
               onToggleMegaMenu={handleMegaMenu}
               onToggleKioskMode={chrome.onToggleKioskMode}
+              actions={state.actions}
+              breadcrumbActions={state.breadcrumbActions}
+              scopes={scopes}
+              showToolbarLevel={headerLevels === 2}
             />
-            {(state.actions || scopes?.state.enabled) && <SingleTopBarActions>{state.actions}</SingleTopBarActions>}
           </header>
         </>
       )}
@@ -158,12 +162,13 @@ function useResponsiveDockedMegaMenu(chrome: AppChromeService) {
   }, [isLargeScreen, chrome, dockedMenuLocalStorageState]);
 }
 
-const getStyles = (theme: GrafanaTheme2, hasActions: boolean) => {
+const getStyles = (theme: GrafanaTheme2, headerHeight: number) => {
   return {
     content: css({
+      label: 'page-content',
       display: 'flex',
       flexDirection: 'column',
-      paddingTop: hasActions ? TOP_BAR_LEVEL_HEIGHT * 2 : TOP_BAR_LEVEL_HEIGHT,
+      paddingTop: headerHeight,
       flexGrow: 1,
       height: 'auto',
     }),
@@ -185,12 +190,13 @@ const getStyles = (theme: GrafanaTheme2, hasActions: boolean) => {
       zIndex: 2,
 
       [theme.breakpoints.up('xl')]: {
-        display: 'block',
+        display: 'flex',
+        flexDirection: 'column',
       },
     }),
     scopesDashboardsContainer: css({
       position: 'fixed',
-      height: `calc(100% - ${TOP_BAR_LEVEL_HEIGHT}px)`,
+      height: `calc(100% - ${headerHeight}px)`,
       zIndex: 1,
     }),
     scopesDashboardsContainerDocked: css({
@@ -249,7 +255,7 @@ const getStyles = (theme: GrafanaTheme2, hasActions: boolean) => {
     }),
     sidebarContainer: css({
       position: 'fixed',
-      height: `calc(100% - ${TOP_BAR_LEVEL_HEIGHT}px)`,
+      height: `calc(100% - ${headerHeight}px)`,
       zIndex: 2,
       right: 0,
     }),
diff --git a/public/app/core/components/AppChrome/AppChromeService.tsx b/public/app/core/components/AppChrome/AppChromeService.tsx
index f93f33c87b6b2..f3de83d38a532 100644
--- a/public/app/core/components/AppChrome/AppChromeService.tsx
+++ b/public/app/core/components/AppChrome/AppChromeService.tsx
@@ -1,5 +1,5 @@
 import { useObservable } from 'react-use';
-import { BehaviorSubject, distinctUntilChanged, map } from 'rxjs';
+import { BehaviorSubject } from 'rxjs';
 
 import { AppEvents, NavModel, NavModelItem, PageLayoutType, UrlQueryValue } from '@grafana/data';
 import { config, locationService, reportInteraction } from '@grafana/runtime';
@@ -14,13 +14,14 @@ import { buildBreadcrumbs } from '../Breadcrumbs/utils';
 
 import { logDuplicateUnifiedHistoryEntryEvent } from './History/eventsTracking';
 import { ReturnToPreviousProps } from './ReturnToPrevious/ReturnToPrevious';
-import { HistoryEntry, TOP_BAR_LEVEL_HEIGHT } from './types';
+import { HistoryEntry } from './types';
 
 export interface AppChromeState {
   chromeless?: boolean;
   sectionNav: NavModel;
   pageNav?: NavModelItem;
   actions?: React.ReactNode;
+  breadcrumbActions?: React.ReactNode;
   megaMenuOpen: boolean;
   megaMenuDocked: boolean;
   kioskMode: KioskMode | null;
@@ -58,21 +59,6 @@ export class AppChromeService {
     returnToPrevious: this.returnToPreviousData,
   });
 
-  public headerHeightObservable = this.state
-    .pipe(
-      map(({ actions, chromeless, kioskMode }) => {
-        if (kioskMode || chromeless) {
-          return 0;
-        } else if (actions) {
-          return TOP_BAR_LEVEL_HEIGHT * 2;
-        } else {
-          return TOP_BAR_LEVEL_HEIGHT;
-        }
-      })
-    )
-    // only emit if the state has actually changed
-    .pipe(distinctUntilChanged());
-
   public setMatchedRoute(route: RouteDescriptor) {
     if (this.currentRoute !== route) {
       this.currentRoute = route;
diff --git a/public/app/core/components/AppChrome/AppChromeUpdate.tsx b/public/app/core/components/AppChrome/AppChromeUpdate.tsx
index 8daf42dba2231..692b61e2fe0dc 100644
--- a/public/app/core/components/AppChrome/AppChromeUpdate.tsx
+++ b/public/app/core/components/AppChrome/AppChromeUpdate.tsx
@@ -5,19 +5,30 @@ import { useGrafana } from 'app/core/context/GrafanaContext';
 
 export interface AppChromeUpdateProps {
   actions?: React.ReactNode;
+  breadcrumbActions?: React.ReactNode;
 }
 /**
  * This is the way core pages add actions to the second chrome toolbar
  */
-export const AppChromeUpdate = React.memo<AppChromeUpdateProps>(({ actions }: AppChromeUpdateProps) => {
-  const { chrome } = useGrafana();
+export const AppChromeUpdate = React.memo<AppChromeUpdateProps>(
+  ({ actions, breadcrumbActions }: AppChromeUpdateProps) => {
+    const { chrome } = useGrafana();
 
-  // We use useLayoutEffect here to make sure that the chrome is updated before the page is rendered
-  // This prevents flickering actions when going from one dashboard to another for example
-  useLayoutEffect(() => {
-    chrome.update({ actions });
-  });
-  return null;
-});
+    // Unmount cleanup
+    useLayoutEffect(() => {
+      return () => {
+        chrome.update({ actions: undefined, breadcrumbActions: undefined });
+      };
+    }, [chrome]);
+
+    // We use useLayoutEffect here to make sure that the chrome is updated before the page is rendered
+    // This prevents flickering actions when going from one dashboard to another for example
+    useLayoutEffect(() => {
+      chrome.update({ actions, breadcrumbActions });
+    });
+
+    return null;
+  }
+);
 
 AppChromeUpdate.displayName = 'TopNavUpdate';
diff --git a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.tsx b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.tsx
index c56eadad7a764..d7af125d8947d 100644
--- a/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.tsx
+++ b/public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.tsx
@@ -2,6 +2,8 @@ import { useState } from 'react';
 
 import { Dropdown, Menu, ToolbarButton } from '@grafana/ui';
 
+import { NavToolbarSeparator } from '../NavToolbar/NavToolbarSeparator';
+
 import { getComponentIdFromComponentMeta, useExtensionSidebarContext } from './ExtensionSidebarProvider';
 
 export function ExtensionToolbarItem() {
@@ -24,19 +26,22 @@ export function ExtensionToolbarItem() {
 
   if (components.length === 1) {
     return (
-      <ToolbarButton
-        icon="web-section"
-        data-testid="extension-toolbar-button"
-        variant={isOpen ? 'active' : 'default'}
-        tooltip={components[0].description}
-        onClick={() => {
-          if (isOpen) {
-            setDockedComponentId(undefined);
-          } else {
-            setDockedComponentId(getComponentIdFromComponentMeta(components[0].pluginId, components[0]));
-          }
-        }}
-      />
+      <>
+        <ToolbarButton
+          icon="web-section"
+          data-testid="extension-toolbar-button"
+          variant={isOpen ? 'active' : 'default'}
+          tooltip={components[0].description}
+          onClick={() => {
+            if (isOpen) {
+              setDockedComponentId(undefined);
+            } else {
+              setDockedComponentId(getComponentIdFromComponentMeta(components[0].pluginId, components[0]));
+            }
+          }}
+        />
+        <NavToolbarSeparator />
+      </>
     );
   }
 
@@ -62,13 +67,16 @@ export function ExtensionToolbarItem() {
     </Menu>
   );
   return (
-    <Dropdown overlay={MenuItems} onVisibleChange={setIsMenuOpen} placement="bottom-end">
-      <ToolbarButton
-        data-testid="extension-toolbar-button"
-        icon="web-section"
-        isOpen={isMenuOpen}
-        variant={isOpen ? 'active' : 'default'}
-      />
-    </Dropdown>
+    <>
+      <Dropdown overlay={MenuItems} onVisibleChange={setIsMenuOpen} placement="bottom-end">
+        <ToolbarButton
+          data-testid="extension-toolbar-button"
+          icon="web-section"
+          isOpen={isMenuOpen}
+          variant={isOpen ? 'active' : 'default'}
+        />
+      </Dropdown>
+      <NavToolbarSeparator />
+    </>
   );
 }
diff --git a/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx b/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx
index d7045bb5960bc..3dedb1ea0d5fd 100644
--- a/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx
+++ b/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx
@@ -13,8 +13,6 @@ import { setBookmark } from 'app/core/reducers/navBarTree';
 import { usePatchUserPreferencesMutation } from 'app/features/preferences/api/index';
 import { useDispatch, useSelector } from 'app/types';
 
-import { TOP_BAR_LEVEL_HEIGHT } from '../types';
-
 import { MegaMenuHeader } from './MegaMenuHeader';
 import { MegaMenuItem } from './MegaMenuItem';
 import { usePinnedItems } from './hooks';
@@ -140,8 +138,8 @@ const getStyles = (theme: GrafanaTheme2) => {
     content: css({
       display: 'flex',
       flexDirection: 'column',
-      height: `calc(100% - ${TOP_BAR_LEVEL_HEIGHT}px)`,
       minHeight: 0,
+      flexGrow: 1,
       position: 'relative',
     }),
     mobileHeader: css({
diff --git a/public/app/core/components/AppChrome/MegaMenu/MegaMenuHeader.tsx b/public/app/core/components/AppChrome/MegaMenu/MegaMenuHeader.tsx
index 92062891de77a..91f476bdef15e 100644
--- a/public/app/core/components/AppChrome/MegaMenu/MegaMenuHeader.tsx
+++ b/public/app/core/components/AppChrome/MegaMenu/MegaMenuHeader.tsx
@@ -7,7 +7,7 @@ import { t } from 'app/core/internationalization';
 
 import { Branding } from '../../Branding/Branding';
 import { OrganizationSwitcher } from '../OrganizationSwitcher/OrganizationSwitcher';
-import { TOP_BAR_LEVEL_HEIGHT } from '../types';
+import { getChromeHeaderLevelHeight } from '../TopBar/useChromeHeaderHeight';
 
 export interface Props {
   handleMegaMenu: () => void;
@@ -78,8 +78,8 @@ const getStyles = (theme: GrafanaTheme2) => ({
     gap: theme.spacing(1),
     justifyContent: 'space-between',
     padding: theme.spacing(0, 1, 0, 0.75),
-    height: TOP_BAR_LEVEL_HEIGHT,
-    minHeight: TOP_BAR_LEVEL_HEIGHT,
+    height: getChromeHeaderLevelHeight(),
+    flexShrink: 0,
   }),
   img: css({
     alignSelf: 'center',
diff --git a/public/app/core/components/AppChrome/NavToolbar/NavToolbarSeparator.tsx b/public/app/core/components/AppChrome/NavToolbar/NavToolbarSeparator.tsx
index d2bb32e22cdbb..b9c65fc8c7012 100644
--- a/public/app/core/components/AppChrome/NavToolbar/NavToolbarSeparator.tsx
+++ b/public/app/core/components/AppChrome/NavToolbar/NavToolbarSeparator.tsx
@@ -30,6 +30,9 @@ const getStyles = (theme: GrafanaTheme2) => {
       height: 24,
       flexShrink: 0,
       flexGrow: 0,
+      [theme.breakpoints.down('sm')]: {
+        display: 'none',
+      },
     }),
   };
 };
diff --git a/public/app/core/components/AppChrome/QuickAdd/QuickAdd.tsx b/public/app/core/components/AppChrome/QuickAdd/QuickAdd.tsx
index f1fa0a359027f..a7e5e6ac5fe43 100644
--- a/public/app/core/components/AppChrome/QuickAdd/QuickAdd.tsx
+++ b/public/app/core/components/AppChrome/QuickAdd/QuickAdd.tsx
@@ -1,10 +1,7 @@
-import { css } from '@emotion/css';
 import { useMemo, useState } from 'react';
 
-import { GrafanaTheme2 } from '@grafana/data';
 import { reportInteraction } from '@grafana/runtime';
-import { Menu, Dropdown, useStyles2, ToolbarButton } from '@grafana/ui';
-import { useMediaQueryMinWidth } from 'app/core/hooks/useMediaQueryMinWidth';
+import { Menu, Dropdown, ToolbarButton } from '@grafana/ui';
 import { useSelector } from 'app/types';
 
 import { t } from '../../../internationalization';
@@ -15,13 +12,14 @@ import { findCreateActions } from './utils';
 export interface Props {}
 
 export const QuickAdd = ({}: Props) => {
-  const styles = useStyles2(getStyles);
   const navBarTree = useSelector((state) => state.navBarTree);
   const [isOpen, setIsOpen] = useState(false);
-
   const createActions = useMemo(() => findCreateActions(navBarTree), [navBarTree]);
-  const isSmallScreen = !useMediaQueryMinWidth('sm');
-  const showQuickAdd = createActions.length > 0 && !isSmallScreen;
+  const showQuickAdd = createActions.length > 0;
+
+  if (!showQuickAdd) {
+    return null;
+  }
 
   const MenuActions = () => {
     return (
@@ -43,29 +41,12 @@ export const QuickAdd = ({}: Props) => {
       <Dropdown overlay={MenuActions} placement="bottom-end" onVisibleChange={setIsOpen}>
         <ToolbarButton
           iconOnly
-          icon={isSmallScreen ? 'plus-circle' : 'plus'}
-          isOpen={isSmallScreen ? undefined : isOpen}
+          icon={'plus'}
+          isOpen={isOpen}
           aria-label={t('navigation.quick-add.aria-label', 'New')}
         />
       </Dropdown>
-      <NavToolbarSeparator className={styles.separator} />
+      <NavToolbarSeparator />
     </>
   ) : null;
 };
-
-const getStyles = (theme: GrafanaTheme2) => ({
-  buttonContent: css({
-    alignItems: 'center',
-    display: 'flex',
-  }),
-  buttonText: css({
-    [theme.breakpoints.down('md')]: {
-      display: 'none',
-    },
-  }),
-  separator: css({
-    [theme.breakpoints.down('sm')]: {
-      display: 'none',
-    },
-  }),
-});
diff --git a/public/app/core/components/AppChrome/TopBar/InviteUserButton.tsx b/public/app/core/components/AppChrome/TopBar/InviteUserButton.tsx
index a788f798fc68a..452e899ca47d1 100644
--- a/public/app/core/components/AppChrome/TopBar/InviteUserButton.tsx
+++ b/public/app/core/components/AppChrome/TopBar/InviteUserButton.tsx
@@ -1,5 +1,5 @@
 import { reportInteraction } from '@grafana/runtime';
-import { Button, Stack } from '@grafana/ui';
+import { Box, Button } from '@grafana/ui';
 import { config } from 'app/core/config';
 import { t } from 'app/core/internationalization';
 import { contextSrv } from 'app/core/services/context_srv';
@@ -10,8 +10,7 @@ import { NavToolbarSeparator } from '../NavToolbar/NavToolbarSeparator';
 
 export function InviteUserButton() {
   return config.externalUserMngLinkUrl && contextSrv.hasPermission(AccessControlAction.OrgUsersAdd) ? (
-    <Stack gap={2} alignItems="center">
-      <NavToolbarSeparator />
+    <Box paddingLeft={1} gap={2} alignItems="center" display="flex">
       <Button
         icon="add-user"
         size="sm"
@@ -30,6 +29,6 @@ export function InviteUserButton() {
         {t('navigation.invite-user.invite-button', 'Invite')}
       </Button>
       <NavToolbarSeparator />
-    </Stack>
+    </Box>
   ) : null;
 }
diff --git a/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx b/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx
index 7a3b05b946ef6..5db53ff730ed9 100644
--- a/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx
+++ b/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx
@@ -3,11 +3,14 @@ import { cloneDeep } from 'lodash';
 import { memo } from 'react';
 
 import { GrafanaTheme2, NavModelItem } from '@grafana/data';
+import { Components } from '@grafana/e2e-selectors';
+import { ScopesContextValue } from '@grafana/runtime';
 import { Dropdown, Icon, Stack, ToolbarButton, useStyles2 } from '@grafana/ui';
 import { config } from 'app/core/config';
 import { MEGA_MENU_TOGGLE_ID } from 'app/core/constants';
 import { useGrafana } from 'app/core/context/GrafanaContext';
 import { contextSrv } from 'app/core/core';
+import { useMediaQueryMinWidth } from 'app/core/hooks/useMediaQueryMinWidth';
 import { t } from 'app/core/internationalization';
 import { HOME_NAV_ID } from 'app/core/reducers/navModel';
 import { useSelector } from 'app/types';
@@ -18,20 +21,26 @@ import { buildBreadcrumbs } from '../../Breadcrumbs/utils';
 import { ExtensionToolbarItem } from '../ExtensionSidebar/ExtensionToolbarItem';
 import { HistoryContainer } from '../History/HistoryContainer';
 import { enrichHelpItem } from '../MegaMenu/utils';
+import { NavToolbarSeparator } from '../NavToolbar/NavToolbarSeparator';
 import { QuickAdd } from '../QuickAdd/QuickAdd';
-import { TOP_BAR_LEVEL_HEIGHT } from '../types';
 
 import { InviteUserButton } from './InviteUserButton';
 import { ProfileButton } from './ProfileButton';
 import { SignInLink } from './SignInLink';
+import { SingleTopBarActions } from './SingleTopBarActions';
 import { TopNavBarMenu } from './TopNavBarMenu';
 import { TopSearchBarCommandPaletteTrigger } from './TopSearchBarCommandPaletteTrigger';
+import { getChromeHeaderLevelHeight } from './useChromeHeaderHeight';
 
 interface Props {
   sectionNav: NavModelItem;
   pageNav?: NavModelItem;
   onToggleMegaMenu(): void;
   onToggleKioskMode(): void;
+  actions?: React.ReactNode;
+  breadcrumbActions?: React.ReactNode;
+  scopes?: ScopesContextValue | undefined;
+  showToolbarLevel: boolean;
 }
 
 export const SingleTopBar = memo(function SingleTopBar({
@@ -39,60 +48,79 @@ export const SingleTopBar = memo(function SingleTopBar({
   onToggleKioskMode,
   pageNav,
   sectionNav,
+  scopes,
+  actions,
+  breadcrumbActions,
+  showToolbarLevel,
 }: Props) {
   const { chrome } = useGrafana();
   const state = chrome.useState();
   const menuDockedAndOpen = !state.chromeless && state.megaMenuDocked && state.megaMenuOpen;
   const styles = useStyles2(getStyles, menuDockedAndOpen);
   const navIndex = useSelector((state) => state.navIndex);
-
   const helpNode = cloneDeep(navIndex['help']);
   const enrichedHelpNode = helpNode ? enrichHelpItem(helpNode) : undefined;
   const profileNode = navIndex['profile'];
   const homeNav = useSelector((state) => state.navIndex)[HOME_NAV_ID];
   const breadcrumbs = buildBreadcrumbs(sectionNav, pageNav, homeNav);
   const unifiedHistoryEnabled = config.featureToggles.unifiedHistory;
+  const isSmallScreen = !useMediaQueryMinWidth('sm');
 
   return (
-    <div className={styles.layout}>
-      <Stack minWidth={0} gap={0.5} alignItems="center">
-        {!menuDockedAndOpen && (
-          <ToolbarButton
-            narrow
-            id={MEGA_MENU_TOGGLE_ID}
-            onClick={onToggleMegaMenu}
-            tooltip={t('navigation.megamenu.open', 'Open menu')}
-          >
-            <Stack gap={0} alignItems="center">
-              <Branding.MenuLogo className={styles.img} />
-              <Icon size="sm" name="angle-down" />
-            </Stack>
-          </ToolbarButton>
-        )}
-        <Breadcrumbs breadcrumbs={breadcrumbs} className={styles.breadcrumbsWrapper} />
-      </Stack>
+    <>
+      <div className={styles.layout}>
+        <Stack minWidth={0} gap={0.5} alignItems="center" flex={{ xs: 2, lg: 1 }}>
+          {!menuDockedAndOpen && (
+            <ToolbarButton
+              narrow
+              id={MEGA_MENU_TOGGLE_ID}
+              onClick={onToggleMegaMenu}
+              tooltip={t('navigation.megamenu.open', 'Open menu')}
+            >
+              <Stack gap={0} alignItems="center">
+                <Branding.MenuLogo className={styles.img} />
+                <Icon size="sm" name="angle-down" />
+              </Stack>
+            </ToolbarButton>
+          )}
+          <Breadcrumbs breadcrumbs={breadcrumbs} className={styles.breadcrumbsWrapper} />
+          {!showToolbarLevel && breadcrumbActions}
+        </Stack>
 
-      <Stack gap={0.5} alignItems="center">
-        <TopSearchBarCommandPaletteTrigger />
-        {unifiedHistoryEnabled && <HistoryContainer />}
-        <QuickAdd />
-        {enrichedHelpNode && (
-          <Dropdown overlay={() => <TopNavBarMenu node={enrichedHelpNode} />} placement="bottom-end">
-            <ToolbarButton iconOnly icon="question-circle" aria-label={t('navigation.help.aria-label', 'Help')} />
-          </Dropdown>
-        )}
-        {!contextSrv.user.isSignedIn && <SignInLink />}
-        {config.featureToggles.inviteUserExperimental && <InviteUserButton />}
-        {config.featureToggles.extensionSidebar && <ExtensionToolbarItem />}
-        {profileNode && <ProfileButton profileNode={profileNode} onToggleKioskMode={onToggleKioskMode} />}
-      </Stack>
-    </div>
+        <Stack
+          gap={0.5}
+          alignItems="center"
+          justifyContent={'flex-end'}
+          flex={1}
+          data-testid={!showToolbarLevel ? Components.NavToolbar.container : undefined}
+          minWidth={{ xs: 'unset', lg: 0 }}
+        >
+          <TopSearchBarCommandPaletteTrigger />
+          {unifiedHistoryEnabled && !isSmallScreen && <HistoryContainer />}
+          {!isSmallScreen && <QuickAdd />}
+          {enrichedHelpNode && (
+            <Dropdown overlay={() => <TopNavBarMenu node={enrichedHelpNode} />} placement="bottom-end">
+              <ToolbarButton iconOnly icon="question-circle" aria-label={t('navigation.help.aria-label', 'Help')} />
+            </Dropdown>
+          )}
+          <NavToolbarSeparator />
+          {config.featureToggles.inviteUserExperimental && !isSmallScreen && <InviteUserButton />}
+          {config.featureToggles.extensionSidebar && !isSmallScreen && <ExtensionToolbarItem />}
+          {!showToolbarLevel && actions}
+          {!contextSrv.user.isSignedIn && <SignInLink />}
+          {profileNode && <ProfileButton profileNode={profileNode} onToggleKioskMode={onToggleKioskMode} />}
+        </Stack>
+      </div>
+      {showToolbarLevel && (
+        <SingleTopBarActions scopes={scopes} actions={actions} breadcrumbActions={breadcrumbActions} />
+      )}
+    </>
   );
 });
 
 const getStyles = (theme: GrafanaTheme2, menuDockedAndOpen: boolean) => ({
   layout: css({
-    height: TOP_BAR_LEVEL_HEIGHT,
+    height: getChromeHeaderLevelHeight(),
     display: 'flex',
     gap: theme.spacing(2),
     alignItems: 'center',
@@ -100,12 +128,6 @@ const getStyles = (theme: GrafanaTheme2, menuDockedAndOpen: boolean) => ({
     paddingLeft: menuDockedAndOpen ? theme.spacing(3.5) : theme.spacing(0.75),
     borderBottom: `1px solid ${theme.colors.border.weak}`,
     justifyContent: 'space-between',
-
-    [theme.breakpoints.up('lg')]: {
-      gridTemplateColumns: '2fr minmax(550px, 1fr)',
-      display: 'grid',
-      justifyContent: 'flex-start',
-    },
   }),
   breadcrumbsWrapper: css({
     display: 'flex',
diff --git a/public/app/core/components/AppChrome/TopBar/SingleTopBarActions.tsx b/public/app/core/components/AppChrome/TopBar/SingleTopBarActions.tsx
index 363327f23a198..9e7e0ce7c7c80 100644
--- a/public/app/core/components/AppChrome/TopBar/SingleTopBarActions.tsx
+++ b/public/app/core/components/AppChrome/TopBar/SingleTopBarActions.tsx
@@ -1,41 +1,34 @@
 import { css } from '@emotion/css';
-import { PropsWithChildren } from 'react';
 
 import { GrafanaTheme2 } from '@grafana/data';
 import { Components } from '@grafana/e2e-selectors';
-import { useScopes } from '@grafana/runtime';
+import { ScopesContextValue } from '@grafana/runtime';
 import { Stack, useStyles2 } from '@grafana/ui';
 import { ScopesSelector } from 'app/features/scopes/selector/ScopesSelector';
 
-import { TOP_BAR_LEVEL_HEIGHT } from '../types';
+import { NavToolbarSeparator } from '../NavToolbar/NavToolbarSeparator';
 
-export function SingleTopBarActions({ children }: PropsWithChildren) {
-  const styles = useStyles2(getStyles);
-  const scopes = useScopes();
+import { getChromeHeaderLevelHeight } from './useChromeHeaderHeight';
+
+export interface Props {
+  actions?: React.ReactNode;
+  breadcrumbActions?: React.ReactNode;
+  scopes?: ScopesContextValue | undefined;
+}
 
-  const scopesRender = scopes?.state.enabled ? <ScopesSelector /> : undefined;
-  const childrenRender = children ? (
-    <Stack
-      alignItems="center"
-      justifyContent={scopes?.state.enabled ? 'space-between' : 'flex-end'}
-      flex={1}
-      wrap="nowrap"
-      minWidth={0}
-    >
-      {children}
-    </Stack>
-  ) : undefined;
+export function SingleTopBarActions({ actions, breadcrumbActions, scopes }: Props) {
+  const styles = useStyles2(getStyles);
 
   return (
     <div data-testid={Components.NavToolbar.container} className={styles.actionsBar}>
-      {scopesRender ? (
-        <Stack alignItems="center" justifyContent="flex-start" flex={1} wrap="nowrap" minWidth={0}>
-          {scopesRender}
-          {children}
+      <Stack alignItems="center" justifyContent="flex-start" flex={1} wrap="nowrap" minWidth={0}>
+        {scopes?.state.enabled ? <ScopesSelector /> : undefined}
+        <Stack alignItems="center" justifyContent={'flex-end'} flex={1} wrap="nowrap" minWidth={0}>
+          {breadcrumbActions}
+          {breadcrumbActions && actions && <NavToolbarSeparator />}
+          {actions}
         </Stack>
-      ) : (
-        childrenRender
-      )}
+      </Stack>
     </div>
   );
 }
@@ -47,7 +40,7 @@ const getStyles = (theme: GrafanaTheme2) => {
       backgroundColor: theme.colors.background.primary,
       borderBottom: `1px solid ${theme.colors.border.weak}`,
       display: 'flex',
-      height: TOP_BAR_LEVEL_HEIGHT,
+      height: getChromeHeaderLevelHeight(),
       padding: theme.spacing(0, 1, 0, 2),
     }),
   };
diff --git a/public/app/core/components/AppChrome/TopBar/TopSearchBarCommandPaletteTrigger.tsx b/public/app/core/components/AppChrome/TopBar/TopSearchBarCommandPaletteTrigger.tsx
index d33464c250338..d878d835a0560 100644
--- a/public/app/core/components/AppChrome/TopBar/TopSearchBarCommandPaletteTrigger.tsx
+++ b/public/app/core/components/AppChrome/TopBar/TopSearchBarCommandPaletteTrigger.tsx
@@ -1,6 +1,6 @@
-import { css } from '@emotion/css';
+import { css, cx } from '@emotion/css';
 import { useKBar, VisualState } from 'kbar';
-import { useMemo } from 'react';
+import React, { useMemo } from 'react';
 
 import { GrafanaTheme2 } from '@grafana/data';
 import { selectors } from '@grafana/e2e-selectors';
@@ -10,31 +10,36 @@ import { useMediaQueryMinWidth } from 'app/core/hooks/useMediaQueryMinWidth';
 import { t } from 'app/core/internationalization';
 import { getModKey } from 'app/core/utils/browser';
 
-export function TopSearchBarCommandPaletteTrigger() {
+import { NavToolbarSeparator } from '../NavToolbar/NavToolbarSeparator';
+
+export const TopSearchBarCommandPaletteTrigger = React.memo(() => {
   const { query: kbar } = useKBar((kbarState) => ({
     kbarSearchQuery: kbarState.searchQuery,
     kbarIsOpen: kbarState.visualState === VisualState.showing,
   }));
 
-  const isSmallScreen = !useMediaQueryMinWidth('lg');
+  const isLargeScreen = useMediaQueryMinWidth('lg');
 
   const onOpenSearch = () => {
     kbar.toggle();
   };
 
-  if (isSmallScreen) {
+  if (!isLargeScreen) {
     return (
-      <ToolbarButton
-        iconOnly
-        icon="search"
-        aria-label={t('nav.search.placeholderCommandPalette', 'Search or jump to...')}
-        onClick={onOpenSearch}
-      />
+      <>
+        <ToolbarButton
+          iconOnly
+          icon="search"
+          aria-label={t('nav.search.placeholderCommandPalette', 'Search...')}
+          onClick={onOpenSearch}
+        />
+        <NavToolbarSeparator />
+      </>
     );
   }
 
   return <PretendTextInput onClick={onOpenSearch} />;
-}
+});
 
 interface PretendTextInputProps {
   onClick: () => void;
@@ -56,11 +61,10 @@ function PretendTextInput({ onClick }: PretendTextInputProps) {
         </div>
 
         <button className={styles.fakeInput} onClick={onClick}>
-          {t('nav.search.placeholderCommandPalette', 'Search or jump to...')}
+          {t('nav.search.placeholderCommandPalette', 'Search...')}
         </button>
 
         <div className={styles.suffix}>
-          <Icon name="keyboard" />
           <Text variant="bodySmall">{`${modKey}+k`}</Text>
         </div>
       </div>
@@ -72,7 +76,15 @@ const getStyles = (theme: GrafanaTheme2) => {
   const baseStyles = getInputStyles({ theme });
 
   return {
-    wrapper: baseStyles.wrapper,
+    wrapper: cx(
+      baseStyles.wrapper,
+      css({
+        width: 'auto',
+        minWidth: 140,
+        maxWidth: 350,
+        flexGrow: 1,
+      })
+    ),
     inputWrapper: baseStyles.inputWrapper,
     prefix: baseStyles.prefix,
     suffix: css([
diff --git a/public/app/core/components/AppChrome/TopBar/useChromeHeaderHeight.ts b/public/app/core/components/AppChrome/TopBar/useChromeHeaderHeight.ts
new file mode 100644
index 0000000000000..fa3555741b6e6
--- /dev/null
+++ b/public/app/core/components/AppChrome/TopBar/useChromeHeaderHeight.ts
@@ -0,0 +1,99 @@
+import { useEffect, useState } from 'react';
+
+import { config, useScopes } from '@grafana/runtime';
+import { useGrafana } from 'app/core/context/GrafanaContext';
+import { useMediaQueryMinWidth } from 'app/core/hooks/useMediaQueryMinWidth';
+
+import { AppChromeState } from '../AppChromeService';
+import { useExtensionSidebarContext } from '../ExtensionSidebar/ExtensionSidebarProvider';
+
+/**
+ * Returns the current header levels given current app chrome state, scopes and screen size.
+ */
+export function useChromeHeaderLevels() {
+  const { chrome } = useGrafana();
+  const state = chrome.state.getValue();
+  const scopes = useScopes();
+
+  const isLargeScreen = useMediaQueryMinWidth('xl');
+
+  const [headerLevels, setHeaderLevels] = useState(
+    getHeaderLevelsGivenState(state, scopes?.state.enabled, isLargeScreen)
+  );
+
+  // Subscribe to chrome state changes and update header height
+  useEffect(() => {
+    const unsub = chrome.state.subscribe((state) => {
+      const newLevels = getHeaderLevelsGivenState(state, scopes?.state.enabled, isLargeScreen);
+      if (newLevels !== headerLevels) {
+        setHeaderLevels(newLevels);
+      }
+    });
+
+    return () => unsub.unsubscribe();
+  }, [chrome, headerLevels, scopes, isLargeScreen]);
+
+  return headerLevels;
+}
+
+function getHeaderLevelsGivenState(
+  chromeState: AppChromeState,
+  scopesEnabled: boolean | undefined = false,
+  isLargeScreen: boolean
+) {
+  // No levels when chromeless or kiosk mode
+  if (chromeState.kioskMode || chromeState.chromeless) {
+    return 0;
+  }
+
+  // Always use two levels scopes is enabled
+  if (scopesEnabled) {
+    return 2;
+  }
+
+  // No actions we can always use 1 level
+  if (!chromeState.actions) {
+    return 1;
+  }
+
+  // We have actions
+  // If mega menu docked always use two levels
+  // If scenes disabled always use two levels (mainly because of the time range picker)
+  if (chromeState.megaMenuDocked || !config.featureToggles.dashboardScene) {
+    return 2;
+  }
+
+  // If screen is large and unifiedNavbars is not disabled then we can use 1 level
+  if (isLargeScreen && config.featureToggles.unifiedNavbars) {
+    return 1;
+  }
+
+  return 2;
+}
+
+/**
+ * Translates header levels to header height but also takes the
+ * sidebar into account as header height can be treated as zero when the sidebar is open
+ * this should be better named as useStickyTopPadding or something as that is what is's used for
+ */
+export function useChromeHeaderHeight() {
+  const levels = useChromeHeaderLevels();
+
+  // if the extension sidebar is open, the inner pane will be scrollable, thus we need to set the header height to 0
+  const { isOpen: isExtensionSidebarOpen } = useExtensionSidebarContext();
+
+  if (isExtensionSidebarOpen) {
+    return 0;
+  }
+
+  return levels * getChromeHeaderLevelHeight();
+}
+
+/**
+ * Can replace with constant once unifiedNavbars feature toggle is removed
+ **/
+export function getChromeHeaderLevelHeight() {
+  // Waiting with switch to 48 until we have a story for scopes
+  // return config.featureToggles.unifiedNavbars ? 48 : 40;
+  return 40;
+}
diff --git a/public/app/core/components/AppChrome/types.ts b/public/app/core/components/AppChrome/types.ts
index 1882e24d9a85c..6cf72c936f510 100644
--- a/public/app/core/components/AppChrome/types.ts
+++ b/public/app/core/components/AppChrome/types.ts
@@ -1,5 +1,4 @@
 import { NavModelItem } from '@grafana/data';
-export const TOP_BAR_LEVEL_HEIGHT = 40;
 
 export interface ToolbarUpdateProps {
   pageNav?: NavModelItem;
diff --git a/public/app/core/context/GrafanaContext.ts b/public/app/core/context/GrafanaContext.ts
index 8ce1dcacd0ed9..1f3bf86372b06 100644
--- a/public/app/core/context/GrafanaContext.ts
+++ b/public/app/core/context/GrafanaContext.ts
@@ -1,12 +1,9 @@
 import { createContext, useCallback, useContext } from 'react';
-import { useObservable } from 'react-use';
-import { of } from 'rxjs';
 
 import { GrafanaConfig } from '@grafana/data';
 import { LocationService, locationService, BackendSrv } from '@grafana/runtime';
 
 import { AppChromeService } from '../components/AppChrome/AppChromeService';
-import { useExtensionSidebarContext } from '../components/AppChrome/ExtensionSidebar/ExtensionSidebarProvider';
 import { NewFrontendAssetsChecker } from '../services/NewFrontendAssetsChecker';
 import { KeybindingSrv } from '../services/keybindingSrv';
 
@@ -44,11 +41,3 @@ export function useReturnToPreviousInternal() {
     [chrome]
   );
 }
-
-export function useChromeHeaderHeight() {
-  const { chrome } = useGrafana();
-  // if the extension sidebar is open, the inner pane will be scrollable, thus we need to set the header height to 0
-  const { isOpen: isExtensionSidebarOpen } = useExtensionSidebarContext();
-
-  return useObservable(isExtensionSidebarOpen ? of(0) : chrome.headerHeightObservable, 0);
-}
diff --git a/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx b/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx
index 431dfed0d1b71..a778dd364f9bb 100644
--- a/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx
+++ b/public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx
@@ -7,6 +7,7 @@ import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/Pan
 
 import { DashboardScene } from '../scene/DashboardScene';
 import { useLayoutCategory } from '../scene/layouts-shared/DashboardLayoutSelector';
+import { EditSchemaV2Button } from '../scene/new-toolbar/actions/EditSchemaV2Button';
 import { EditableDashboardElement, EditableDashboardElementInfo } from '../scene/types/EditableDashboardElement';
 
 export class DashboardEditableElement implements EditableDashboardElement {
@@ -53,19 +54,22 @@ export class DashboardEditableElement implements EditableDashboardElement {
 
   public renderActions(): ReactNode {
     return (
-      <Button
-        variant="secondary"
-        size="sm"
-        onClick={() => this.dashboard.onOpenSettings()}
-        tooltip={t('dashboard.toolbar.dashboard-settings.tooltip', 'Dashboard settings')}
-      >
-        <Stack direction="row" gap={1} justifyContent="space-between" alignItems={'center'}>
-          <span>
-            <Trans i18nKey="dashboard.actions.open-settings">Settings</Trans>
-          </span>
-          <Icon name="sliders-v-alt" />
-        </Stack>
-      </Button>
+      <>
+        <EditSchemaV2Button dashboard={this.dashboard} />
+        <Button
+          variant="secondary"
+          size="sm"
+          onClick={() => this.dashboard.onOpenSettings()}
+          tooltip={t('dashboard.toolbar.dashboard-settings.tooltip', 'Dashboard settings')}
+        >
+          <Stack direction="row" gap={1} justifyContent="space-between" alignItems={'center'}>
+            <span>
+              <Trans i18nKey="dashboard.actions.open-settings">Settings</Trans>
+            </span>
+            <Icon name="sliders-v-alt" />
+          </Stack>
+        </Button>
+      </>
     );
   }
 }
diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx
index 003ea1b438b9b..e33e6acd112d4 100644
--- a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx
+++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx
@@ -11,7 +11,6 @@ import {
   Dropdown,
   Icon,
   Menu,
-  Stack,
   ToolbarButton,
   ToolbarButtonRow,
   useStyles2,
@@ -38,7 +37,8 @@ import { isLibraryPanel } from '../utils/utils';
 import { DashboardScene } from './DashboardScene';
 import { GoToSnapshotOriginButton } from './GoToSnapshotOriginButton';
 import ManagedDashboardNavBarBadge from './ManagedDashboardNavBarBadge';
-import { ToolbarActionsNew } from './new-toolbar/ToolbarActionsNew';
+import { LeftActions } from './new-toolbar/LeftActions';
+import { RightActions } from './new-toolbar/RightActions';
 
 interface Props {
   dashboard: DashboardScene;
@@ -47,12 +47,14 @@ interface Props {
 export const NavToolbarActions = memo<Props>(({ dashboard }) => {
   const hasNewToolbar = config.featureToggles.dashboardNewLayouts && config.featureToggles.newDashboardSharingComponent;
 
-  const actions = hasNewToolbar ? (
-    <ToolbarActionsNew dashboard={dashboard} key={`${dashboard.state.key}-toolbar-actions-new`} />
+  return hasNewToolbar ? (
+    <AppChromeUpdate
+      breadcrumbActions={<LeftActions dashboard={dashboard} />}
+      actions={<RightActions dashboard={dashboard} />}
+    />
   ) : (
-    <ToolbarActions dashboard={dashboard} key={`${dashboard.state.key}-toolbar-actions`} />
+    <AppChromeUpdate actions={<ToolbarActions dashboard={dashboard} />} />
   );
-  return <AppChromeUpdate actions={actions} />;
 });
 
 NavToolbarActions.displayName = 'NavToolbarActions';
@@ -68,7 +70,6 @@ export function ToolbarActions({ dashboard }: Props) {
 
   const canSaveAs = contextSrv.hasEditPermissionInFolders;
   const toolbarActions: ToolbarAction[] = [];
-  const leftActions: ToolbarAction[] = [];
   const styles = useStyles2(getStyles);
   const isEditingPanel = Boolean(editPanel);
   const isViewingPanel = Boolean(viewPanelScene);
@@ -593,16 +594,7 @@ export function ToolbarActions({ dashboard }: Props) {
     },
   });
 
-  const rightActionsElements: ReactNode[] = renderActionElements(toolbarActions);
-  const leftActionsElements: ReactNode[] = renderActionElements(leftActions);
-  const hasActionsToLeftAndRight = leftActionsElements.length > 0;
-
-  return (
-    <Stack flex={1} minWidth={0} justifyContent={hasActionsToLeftAndRight ? 'space-between' : 'flex-end'}>
-      {leftActionsElements.length > 0 && <ToolbarButtonRow alignment="left">{leftActionsElements}</ToolbarButtonRow>}
-      <ToolbarButtonRow alignment="right">{rightActionsElements}</ToolbarButtonRow>
-    </Stack>
-  );
+  return <ToolbarButtonRow alignment="right">{renderActionElements(toolbarActions)}</ToolbarButtonRow>;
 }
 
 function renderActionElements(toolbarActions: ToolbarAction[]) {
diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/LeftActions.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/LeftActions.tsx
index 5c0836735214d..ec37d44614549 100644
--- a/public/app/features/dashboard-scene/scene/new-toolbar/LeftActions.tsx
+++ b/public/app/features/dashboard-scene/scene/new-toolbar/LeftActions.tsx
@@ -1,6 +1,4 @@
-import { css } from '@emotion/css';
-
-import { ToolbarButtonRow, useStyles2 } from '@grafana/ui';
+import { ToolbarButtonRow } from '@grafana/ui';
 
 import { dynamicDashNavActions } from '../../utils/registerDynamicDashNavAction';
 import { DashboardScene } from '../DashboardScene';
@@ -12,7 +10,6 @@ import { StarButton } from './actions/StarButton';
 import { getDynamicActions, renderActionElements } from './utils';
 
 export const LeftActions = ({ dashboard }: { dashboard: DashboardScene }) => {
-  const styles = useStyles2(getStyles);
   const { editview, editPanel, isEditing, uid, meta, viewPanelScene } = dashboard.useState();
 
   const hasEditView = Boolean(editview);
@@ -54,8 +51,6 @@ export const LeftActions = ({ dashboard }: { dashboard: DashboardScene }) => {
         group: 'actions',
         condition: isSnapshot && !isEditingDashboard,
       },
-      // This adds the presence indicators in enterprise
-      ...getDynamicActions(dynamicDashNavActions.right, 'right-dynamic', !isEditingPanel && !isEditingDashboard),
     ],
     dashboard
   );
@@ -64,15 +59,5 @@ export const LeftActions = ({ dashboard }: { dashboard: DashboardScene }) => {
     return null;
   }
 
-  return (
-    <ToolbarButtonRow alignment="left" className={styles.container}>
-      {elements}
-    </ToolbarButtonRow>
-  );
+  return <ToolbarButtonRow alignment="left">{elements}</ToolbarButtonRow>;
 };
-
-const getStyles = () => ({
-  container: css({
-    flex: 1,
-  }),
-});
diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/RightActions.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/RightActions.tsx
index b809a9fb90a5c..428906c1ab0d1 100644
--- a/public/app/features/dashboard-scene/scene/new-toolbar/RightActions.tsx
+++ b/public/app/features/dashboard-scene/scene/new-toolbar/RightActions.tsx
@@ -1,9 +1,11 @@
 import { css } from '@emotion/css';
 
+import { GrafanaTheme2 } from '@grafana/data';
 import { ToolbarButtonRow, useStyles2 } from '@grafana/ui';
 import { contextSrv } from 'app/core/services/context_srv';
 import { playlistSrv } from 'app/features/playlist/PlaylistSrv';
 
+import { dynamicDashNavActions } from '../../utils/registerDynamicDashNavAction';
 import { isLibraryPanel } from '../../utils/utils';
 import { DashboardScene } from '../DashboardScene';
 
@@ -12,7 +14,6 @@ import { DashboardSettingsButton } from './actions/DashboardSettingsButton';
 import { DiscardLibraryPanelButton } from './actions/DiscardLibraryPanelButton';
 import { DiscardPanelButton } from './actions/DiscardPanelButton';
 import { EditDashboardSwitch } from './actions/EditDashboardSwitch';
-import { EditSchemaV2Button } from './actions/EditSchemaV2Button';
 import { ExportDashboardButton } from './actions/ExportDashboardButton';
 import { MakeDashboardEditableButton } from './actions/MakeDashboardEditableButton';
 import { PlayListNextButton } from './actions/PlayListNextButton';
@@ -22,12 +23,12 @@ import { SaveDashboard } from './actions/SaveDashboard';
 import { SaveLibraryPanelButton } from './actions/SaveLibraryPanelButton';
 import { ShareDashboardButton } from './actions/ShareDashboardButton';
 import { UnlinkLibraryPanelButton } from './actions/UnlinkLibraryPanelButton';
-import { renderActionElements } from './utils';
+import { getDynamicActions, renderActionElements } from './utils';
 
 export const RightActions = ({ dashboard }: { dashboard: DashboardScene }) => {
-  const styles = useStyles2(getStyles);
   const { editPanel, editable, editview, isEditing, uid, meta, viewPanelScene } = dashboard.useState();
   const { isPlaying } = playlistSrv.useState();
+  const styles = useStyles2(getStyles);
 
   const isEditable = Boolean(editable);
   const canSave = Boolean(meta.canSave);
@@ -44,12 +45,15 @@ export const RightActions = ({ dashboard }: { dashboard: DashboardScene }) => {
 
   const showPanelButtons = isEditingPanel && !hasEditView && !isViewingPanel;
   const showPlayButtons = isPlaying && isShowingDashboard && !isEditingDashboard;
-  const showShareButton = hasUid && !isSnapshot && !isPlaying;
+  const showShareButton = hasUid && !isSnapshot && !isPlaying && !isEditingPanel;
 
   return (
     <ToolbarButtonRow alignment="right" className={styles.container}>
       {renderActionElements(
         [
+          // This adds the presence indicators in enterprise
+          // Leaving group empty here as these are sometimes not rendered leaving separators with blank space between them
+          ...getDynamicActions(dynamicDashNavActions.right, '', !isEditingPanel && !isEditingDashboard),
           {
             key: 'play-list-previous-button',
             component: PlayListPreviousButton,
@@ -98,12 +102,6 @@ export const RightActions = ({ dashboard }: { dashboard: DashboardScene }) => {
             group: 'panel',
             condition: showPanelButtons && isEditingLibraryPanel,
           },
-          {
-            key: 'edit-schema-v2-button',
-            component: EditSchemaV2Button,
-            group: 'dashboard',
-            condition: isEditingAndShowingDashboard && hasUid,
-          },
           {
             key: 'dashboard-settings',
             component: DashboardSettingsButton,
@@ -120,13 +118,20 @@ export const RightActions = ({ dashboard }: { dashboard: DashboardScene }) => {
             key: 'make-dashboard-editable-button',
             component: MakeDashboardEditableButton,
             group: 'save-edit',
-            condition: !isEditing && dashboard.canEditDashboard() && !isViewingPanel && !isEditable,
+            condition: !isEditing && dashboard.canEditDashboard() && !isViewingPanel && !isEditable && !isPlaying,
           },
           {
             key: 'edit-dashboard-switch',
             component: EditDashboardSwitch,
             group: 'save-edit',
-            condition: dashboard.canEditDashboard() && !isEditingLibraryPanel && !isViewingPanel && isEditable,
+            condition:
+              dashboard.canEditDashboard() &&
+              !isEditingPanel &&
+              !isEditingLibraryPanel &&
+              !isViewingPanel &&
+              isEditable &&
+              !isPlaying &&
+              !isEditingPanel,
           },
           {
             key: 'new-export-dashboard-button',
@@ -147,8 +152,6 @@ export const RightActions = ({ dashboard }: { dashboard: DashboardScene }) => {
   );
 };
 
-const getStyles = () => ({
-  container: css({
-    flex: 1,
-  }),
+const getStyles = (theme: GrafanaTheme2) => ({
+  container: css({ paddingLeft: theme.spacing(0.5) }),
 });
diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/actions/EditSchemaV2Button.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/actions/EditSchemaV2Button.tsx
index fc18a235bdc5d..3b0f729f7112c 100644
--- a/public/app/features/dashboard-scene/scene/new-toolbar/actions/EditSchemaV2Button.tsx
+++ b/public/app/features/dashboard-scene/scene/new-toolbar/actions/EditSchemaV2Button.tsx
@@ -1,10 +1,12 @@
-import { Icon, ToolbarButton } from '@grafana/ui';
+import { Button, Icon } from '@grafana/ui';
 import { t } from 'app/core/internationalization';
 
 import { ToolbarActionProps } from '../types';
 
 export const EditSchemaV2Button = ({ dashboard }: ToolbarActionProps) => (
-  <ToolbarButton
+  <Button
+    size="sm"
+    variant="secondary"
     tooltip={t('dashboard.toolbar.new.edit-dashboard-v2-schema.tooltip', 'Edit dashboard v2 schema')}
     icon={<Icon name="brackets-curly" size="lg" type="default" />}
     onClick={() => dashboard.openV2SchemaEditor()}
diff --git a/public/app/features/dashboard-scene/scene/new-toolbar/utils.tsx b/public/app/features/dashboard-scene/scene/new-toolbar/utils.tsx
index 936faf0e054ff..aaa5fcbf6a2b7 100644
--- a/public/app/features/dashboard-scene/scene/new-toolbar/utils.tsx
+++ b/public/app/features/dashboard-scene/scene/new-toolbar/utils.tsx
@@ -17,7 +17,7 @@ export function renderActionElements(toolbarActions: ToolbarAction[], dashboard:
       continue;
     }
 
-    if (lastGroup && lastGroup !== action.group) {
+    if (action.group && lastGroup && lastGroup !== action.group) {
       actionElements.push(<NavToolbarSeparator key={`${action.group}-separator`} />);
     }
 
diff --git a/public/app/features/explore/Logs/LogsNavigation.tsx b/public/app/features/explore/Logs/LogsNavigation.tsx
index aae833cffc67e..64a306c819295 100644
--- a/public/app/features/explore/Logs/LogsNavigation.tsx
+++ b/public/app/features/explore/Logs/LogsNavigation.tsx
@@ -6,7 +6,7 @@ import { AbsoluteTimeRange, GrafanaTheme2, LogsSortOrder } from '@grafana/data';
 import { config, reportInteraction } from '@grafana/runtime';
 import { DataQuery, TimeZone } from '@grafana/schema';
 import { Button, Icon, Spinner, useTheme2 } from '@grafana/ui';
-import { TOP_BAR_LEVEL_HEIGHT } from 'app/core/components/AppChrome/types';
+import { getChromeHeaderLevelHeight } from 'app/core/components/AppChrome/TopBar/useChromeHeaderHeight';
 import { t, Trans } from 'app/core/internationalization';
 
 import { LogsNavigationPages } from './LogsNavigationPages';
@@ -231,7 +231,8 @@ function LogsNavigation({
 export default memo(LogsNavigation);
 
 const getStyles = (theme: GrafanaTheme2, oldestLogsFirst: boolean) => {
-  const navContainerHeight = `calc(100vh - 2*${theme.spacing(2)} - 2*${TOP_BAR_LEVEL_HEIGHT}px)`;
+  const navContainerHeight = `calc(100vh - 2*${theme.spacing(2)} - 2*${getChromeHeaderLevelHeight()}px)`;
+
   return {
     navContainer: css({
       maxHeight: navContainerHeight,
diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json
index 088e63e3dc3f4..cca1c809e9be6 100644
--- a/public/locales/en-US/grafana.json
+++ b/public/locales/en-US/grafana.json
@@ -5757,7 +5757,7 @@
       "title": "Scenes"
     },
     "search": {
-      "placeholderCommandPalette": "Search or jump to..."
+      "placeholderCommandPalette": "Search..."
     },
     "search-dashboards": {
       "title": "Search dashboards"


Reviews:
user: login: github-advanced-security[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-05T13:34:35Z
user: login: torkelo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-06T10:23:18Z
user: login: torkelo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T05:47:03Z
user: login: torkelo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T05:52:05Z
user: login: torkelo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T05:54:13Z
user: login: svennergr
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T14:52:22Z
user: login: svennergr
body: Not exactly sure which code change, but I think this needs some improvement. The extensionsidebar button can very likely be hidden in edit mode:


https://github.com/user-attachments/assets/8f2eb3cf-6bba-4682-89fa-518b7d4bba5a




state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T15:03:16Z
user: login: joshhunt
body: Gave this a spin with the toggle disabled to focus on making sure there's no regressions. 

Just a few minor ones...
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-07T16:21:43Z
user: login: torkelo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T07:49:36Z
user: login: torkelo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T07:50:58Z
user: login: torkelo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T07:52:22Z
user: login: torkelo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T07:55:01Z
user: login: torkelo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T07:58:50Z
user: login: torkelo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T08:06:07Z
user: login: torkelo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T12:59:49Z
user: login: github-advanced-security[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T05:03:53Z
user: login: ashharrison90
body: given this is behind a feature toggle, lgtm 👍 

putting my general opposition to this being a long term solution down in words again:
- i think it's a step backwards for the overall architecture of the nav. imo keeping that top bar always global is a good thing
- however, we definitely have to do something about page actions. we should use this as a motivating factor to prioritise that work ASAP post-G12
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T09:40:34Z
user: login: torkelo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T10:33:19Z

Review comments:
diff_hunk: @@ -86,6 +86,7 @@
 | `azureMonitorEnableUserAuth`           | Enables user auth for Azure Monitor datasource only                                                                                 | Yes                |
 | `alertingNotificationsStepMode`        | Enables simplified step mode in the notifications section                                                                           | Yes                |
 | `lokiLabelNamesQueryApi`               | Defaults to using the Loki `/labels` API instead of `/series`                                                                       | Yes                |
+| `unifiedNavbars`                       | Enables unified navbars                                                                                                             | Yes                |
path: docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
user: login: github-advanced-security[bot]
body: Did you really mean 'navbars'? The Grafana dictionary might not know of this word yet

[Show more details](https://github.com/grafana/grafana/security/code-scanning/1730)
created_at: 2025-04-05T13:34:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -3,41 +3,58 @@ import { fromBaseUrl } from '../utils/support/url';
 
 describe('Docked Navigation', () => {
   beforeEach(() => {
+    // This is a breakpoint where the mega menu can be docked (and docked is the default state)
     cy.viewport(1280, 800);
+    cy.clearAllLocalStorage();
     e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
 
     cy.visit(fromBaseUrl('/'));
   });
 
-  it('should remain docked when reloading the page', () => {
-    // Expand, then dock the mega menu
-    cy.get('[aria-label="Open menu"]').click();
-    cy.get('[aria-label="Dock menu"]').click();
+  it('should remain un-docked when reloading the page', () => {
+    // Undock the menu
+    cy.get('[aria-label="Undock menu"]').click({ force: true });
 
-    e2e.components.NavMenu.Menu().should('be.visible');
+    e2e.components.NavMenu.Menu().should('not.exist');
 
     cy.reload();
+    e2e.components.NavMenu.Menu().should('not.exist');
+  });
+
+  it('Can re-dock after undock', () => {
+    // Undock the menu
+    cy.get('[aria-label="Undock menu"]').click({ force: true });
+    cy.get('[aria-label="Open menu"]').click({ force: true });
+    cy.get('[aria-label="Dock menu"]').click({ force: true });
+
path: e2e/various-suite/navigation.spec.ts
user: login: torkelo
body: If do not add force: true. when I run e2e tests locally (yarn e2e:dev), in main or here I get 

`
Timed out retrying after 30050ms: cy.click() failed because this element is not visible:

<button id="dock-menu-button" variant="secondary" aria-label="Dock menu" class="css-duxynr" type="button" tabindex="0"></button>

This element <button#dock-menu-button.css-duxynr> is not visible because it has an effective width and height of: 0 x 0 pixels.
```

For me the icons are not loading in e2e tests. Does anyone else see this? 
created_at: 2025-04-06T10:23:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,43 +0,0 @@
-import { e2e } from '../utils';
-import { fromBaseUrl } from '../utils/support/url';
-
-describe('Docked Navigation', () => {
path: e2e/old-arch/various-suite/navigation.spec.ts
user: login: torkelo
body: deleted this from the old-arch (that tests old dashboard arch), as there is nothing in in the old dashboard architecture that relates / changes how mega menu and mega menu docking works 
created_at: 2025-04-07T05:47:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,17 +0,0 @@
-import { useEffect } from 'react';
-
-export function useMediaQueryChange({
-  breakpoint,
-  onChange,
-}: {
-  breakpoint: number;
-  onChange: (e: MediaQueryListEvent) => void;
-}) {
-  useEffect(() => {
-    const mediaQuery = window.matchMedia(`(min-width: ${breakpoint}px)`);
-    const onMediaQueryChange = (e: MediaQueryListEvent) => onChange(e);
-    mediaQuery.addEventListener('change', onMediaQueryChange);
-
-    return () => mediaQuery.removeEventListener('change', onMediaQueryChange);
-  }, [breakpoint, onChange]);
path: public/app/core/hooks/useMediaQueryChange.ts
user: login: torkelo
body: because onChange was never memoized by any user of this hook this useEffect was always firing / cleanup on ever re-render. This caused (at least in explore which triggers a lot of AppChromeService state updates) bugs to appear when you resize.  

I think this hook was a bit poorly designed, better to return a "isMatch" state instead and have the change effect handled by the caller rather than passing in a on change handler. 
created_at: 2025-04-07T05:52:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1823,6 +1823,14 @@ var (
 			Owner:        grafanaPluginsPlatformSquad,
 			FrontendOnly: false,
 		},
+		{
+			Name:         "unifiedNavbars",
+			Description:  "Enables unified navbars",
+			Stage:        FeatureStageGeneralAvailability,
+			Owner:        grafanaPluginsPlatformSquad,
+			FrontendOnly: true,
+			Expression:   "true", // enabled by default
path: pkg/services/featuremgmt/registry.go
user: login: torkelo
body: will switch this to false before merge, just to help review & ephemeral instances for now 
created_at: 2025-04-07T05:54:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,6 +30,9 @@ const getStyles = (theme: GrafanaTheme2) => {
       height: 24,
       flexShrink: 0,
       flexGrow: 0,
+      [theme.breakpoints.down('sm')]: {
+        display: 'none',
+      },
path: public/app/core/components/AppChrome/NavToolbar/NavToolbarSeparator.tsx
user: login: svennergr
body: I'm wondering if this should be the case for all `NavToolbarSeparators`. Here is an example where I think separators still make sense:

https://github.com/user-attachments/assets/29ef4cca-f738-4d17-a8ff-933ddbb3462f


created_at: 2025-04-07T14:52:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -18,100 +21,113 @@ import { buildBreadcrumbs } from '../../Breadcrumbs/utils';
 import { ExtensionToolbarItem } from '../ExtensionSidebar/ExtensionToolbarItem';
 import { HistoryContainer } from '../History/HistoryContainer';
 import { enrichHelpItem } from '../MegaMenu/utils';
+import { NavToolbarSeparator } from '../NavToolbar/NavToolbarSeparator';
 import { QuickAdd } from '../QuickAdd/QuickAdd';
-import { TOP_BAR_LEVEL_HEIGHT } from '../types';
 
 import { InviteUserButton } from './InviteUserButton';
 import { ProfileButton } from './ProfileButton';
 import { SignInLink } from './SignInLink';
+import { SingleTopBarActions } from './SingleTopBarActions';
 import { TopNavBarMenu } from './TopNavBarMenu';
 import { TopSearchBarCommandPaletteTrigger } from './TopSearchBarCommandPaletteTrigger';
+import { getChromeHeaderLevelHeight } from './useChromeHeaderHeight';
 
 interface Props {
   sectionNav: NavModelItem;
   pageNav?: NavModelItem;
   onToggleMegaMenu(): void;
   onToggleKioskMode(): void;
+  actions?: React.ReactNode;
+  breadcrumbActions?: React.ReactNode;
+  scopes?: ScopesContextValue | undefined;
+  showToolbarLevel: boolean;
 }
 
 export const SingleTopBar = memo(function SingleTopBar({
   onToggleMegaMenu,
   onToggleKioskMode,
   pageNav,
   sectionNav,
+  scopes,
+  actions,
+  breadcrumbActions,
+  showToolbarLevel,
 }: Props) {
   const { chrome } = useGrafana();
   const state = chrome.useState();
   const menuDockedAndOpen = !state.chromeless && state.megaMenuDocked && state.megaMenuOpen;
   const styles = useStyles2(getStyles, menuDockedAndOpen);
   const navIndex = useSelector((state) => state.navIndex);
-
   const helpNode = cloneDeep(navIndex['help']);
   const enrichedHelpNode = helpNode ? enrichHelpItem(helpNode) : undefined;
   const profileNode = navIndex['profile'];
   const homeNav = useSelector((state) => state.navIndex)[HOME_NAV_ID];
   const breadcrumbs = buildBreadcrumbs(sectionNav, pageNav, homeNav);
   const unifiedHistoryEnabled = config.featureToggles.unifiedHistory;
+  const isSmallScreen = !useMediaQueryMinWidth('sm');
 
   return (
-    <div className={styles.layout}>
-      <Stack minWidth={0} gap={0.5} alignItems="center">
-        {!menuDockedAndOpen && (
-          <ToolbarButton
-            narrow
-            id={MEGA_MENU_TOGGLE_ID}
-            onClick={onToggleMegaMenu}
-            tooltip={t('navigation.megamenu.open', 'Open menu')}
-          >
-            <Stack gap={0} alignItems="center">
-              <Branding.MenuLogo className={styles.img} />
-              <Icon size="sm" name="angle-down" />
-            </Stack>
-          </ToolbarButton>
-        )}
-        <Breadcrumbs breadcrumbs={breadcrumbs} className={styles.breadcrumbsWrapper} />
-      </Stack>
+    <>
+      <div className={styles.layout}>
+        <Stack minWidth={0} gap={0.5} alignItems="center" flex={{ xs: 2, lg: 1 }}>
+          {!menuDockedAndOpen && (
+            <ToolbarButton
+              narrow
+              id={MEGA_MENU_TOGGLE_ID}
+              onClick={onToggleMegaMenu}
+              tooltip={t('navigation.megamenu.open', 'Open menu')}
+            >
+              <Stack gap={0} alignItems="center">
+                <Branding.MenuLogo className={styles.img} />
+                <Icon size="sm" name="angle-down" />
+              </Stack>
+            </ToolbarButton>
+          )}
+          <Breadcrumbs breadcrumbs={breadcrumbs} className={styles.breadcrumbsWrapper} />
+          {!showToolbarLevel && breadcrumbActions}
+        </Stack>
 
-      <Stack gap={0.5} alignItems="center">
-        <TopSearchBarCommandPaletteTrigger />
-        {unifiedHistoryEnabled && <HistoryContainer />}
-        <QuickAdd />
-        {enrichedHelpNode && (
-          <Dropdown overlay={() => <TopNavBarMenu node={enrichedHelpNode} />} placement="bottom-end">
-            <ToolbarButton iconOnly icon="question-circle" aria-label={t('navigation.help.aria-label', 'Help')} />
-          </Dropdown>
-        )}
-        <ToolbarButton
-          icon="monitor"
-          className={styles.kioskToggle}
-          onClick={onToggleKioskMode}
-          tooltip={t('navigation.kiosk.tooltip', 'Enable kiosk mode')}
-        />
-        {!contextSrv.user.isSignedIn && <SignInLink />}
-        {config.featureToggles.inviteUserExperimental && <InviteUserButton />}
-        {config.featureToggles.extensionSidebar && <ExtensionToolbarItem />}
-        {profileNode && <ProfileButton profileNode={profileNode} />}
-      </Stack>
-    </div>
+        <Stack
+          gap={0.5}
+          alignItems="center"
+          justifyContent={'flex-end'}
+          flex={1}
+          data-testid={!showToolbarLevel ? Components.NavToolbar.container : undefined}
+          minWidth={{ xs: 'unset', lg: 0 }}
+        >
+          <TopSearchBarCommandPaletteTrigger />
+          {unifiedHistoryEnabled && !isSmallScreen && <HistoryContainer />}
+          {!isSmallScreen && <QuickAdd />}
+          {enrichedHelpNode && (
+            <Dropdown overlay={() => <TopNavBarMenu node={enrichedHelpNode} />} placement="bottom-end">
+              <ToolbarButton iconOnly icon="question-circle" aria-label={t('navigation.help.aria-label', 'Help')} />
+            </Dropdown>
+          )}
+          <NavToolbarSeparator />
+          {config.featureToggles.inviteUserExperimental && !isSmallScreen && !actions && <InviteUserButton />}
path: public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx
user: login: joshhunt
body: Hmm. I have it configured on main, but I couldn't get the InviteUserButton to appear when the `unifiedNavbars` toggle is disabled.
created_at: 2025-04-07T16:12:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,12 +46,21 @@ export const RightActions = ({ dashboard }: { dashboard: DashboardScene }) => {
 
   const showPanelButtons = isEditingPanel && !hasEditView && !isViewingPanel;
   const showPlayButtons = isPlaying && isShowingDashboard && !isEditingDashboard;
-  const showShareButton = hasUid && !isSnapshot && !isPlaying;
+  const showShareButton = hasUid && !isSnapshot && !isPlaying && !isEditingPanel;
 
   return (
     <ToolbarButtonRow alignment="right" className={styles.container}>
       {renderActionElements(
         [
+          {
+            key: 'dashboard-settings',
+            component: KioskToggleButton,
+            group: '',
+            condition: !isEditingDashboard,
path: public/app/features/dashboard-scene/scene/new-toolbar/RightActions.tsx
user: login: joshhunt
body: I think this change should be gated behind the `unifiedNavbars` toggle, and put it back in the topnav when toggle is off.
created_at: 2025-04-07T16:14:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -18,100 +21,113 @@ import { buildBreadcrumbs } from '../../Breadcrumbs/utils';
 import { ExtensionToolbarItem } from '../ExtensionSidebar/ExtensionToolbarItem';
 import { HistoryContainer } from '../History/HistoryContainer';
 import { enrichHelpItem } from '../MegaMenu/utils';
+import { NavToolbarSeparator } from '../NavToolbar/NavToolbarSeparator';
 import { QuickAdd } from '../QuickAdd/QuickAdd';
-import { TOP_BAR_LEVEL_HEIGHT } from '../types';
 
 import { InviteUserButton } from './InviteUserButton';
 import { ProfileButton } from './ProfileButton';
 import { SignInLink } from './SignInLink';
+import { SingleTopBarActions } from './SingleTopBarActions';
 import { TopNavBarMenu } from './TopNavBarMenu';
 import { TopSearchBarCommandPaletteTrigger } from './TopSearchBarCommandPaletteTrigger';
+import { getChromeHeaderLevelHeight } from './useChromeHeaderHeight';
 
 interface Props {
   sectionNav: NavModelItem;
   pageNav?: NavModelItem;
   onToggleMegaMenu(): void;
   onToggleKioskMode(): void;
+  actions?: React.ReactNode;
+  breadcrumbActions?: React.ReactNode;
+  scopes?: ScopesContextValue | undefined;
+  showToolbarLevel: boolean;
 }
 
 export const SingleTopBar = memo(function SingleTopBar({
   onToggleMegaMenu,
   onToggleKioskMode,
   pageNav,
   sectionNav,
+  scopes,
+  actions,
+  breadcrumbActions,
+  showToolbarLevel,
 }: Props) {
   const { chrome } = useGrafana();
   const state = chrome.useState();
   const menuDockedAndOpen = !state.chromeless && state.megaMenuDocked && state.megaMenuOpen;
   const styles = useStyles2(getStyles, menuDockedAndOpen);
   const navIndex = useSelector((state) => state.navIndex);
-
   const helpNode = cloneDeep(navIndex['help']);
   const enrichedHelpNode = helpNode ? enrichHelpItem(helpNode) : undefined;
   const profileNode = navIndex['profile'];
   const homeNav = useSelector((state) => state.navIndex)[HOME_NAV_ID];
   const breadcrumbs = buildBreadcrumbs(sectionNav, pageNav, homeNav);
   const unifiedHistoryEnabled = config.featureToggles.unifiedHistory;
+  const isSmallScreen = !useMediaQueryMinWidth('sm');
 
   return (
-    <div className={styles.layout}>
-      <Stack minWidth={0} gap={0.5} alignItems="center">
-        {!menuDockedAndOpen && (
-          <ToolbarButton
-            narrow
-            id={MEGA_MENU_TOGGLE_ID}
-            onClick={onToggleMegaMenu}
-            tooltip={t('navigation.megamenu.open', 'Open menu')}
-          >
-            <Stack gap={0} alignItems="center">
-              <Branding.MenuLogo className={styles.img} />
-              <Icon size="sm" name="angle-down" />
-            </Stack>
-          </ToolbarButton>
-        )}
-        <Breadcrumbs breadcrumbs={breadcrumbs} className={styles.breadcrumbsWrapper} />
-      </Stack>
+    <>
+      <div className={styles.layout}>
+        <Stack minWidth={0} gap={0.5} alignItems="center" flex={{ xs: 2, lg: 1 }}>
+          {!menuDockedAndOpen && (
+            <ToolbarButton
+              narrow
+              id={MEGA_MENU_TOGGLE_ID}
+              onClick={onToggleMegaMenu}
+              tooltip={t('navigation.megamenu.open', 'Open menu')}
+            >
+              <Stack gap={0} alignItems="center">
+                <Branding.MenuLogo className={styles.img} />
+                <Icon size="sm" name="angle-down" />
+              </Stack>
+            </ToolbarButton>
+          )}
+          <Breadcrumbs breadcrumbs={breadcrumbs} className={styles.breadcrumbsWrapper} />
+          {!showToolbarLevel && breadcrumbActions}
+        </Stack>
 
-      <Stack gap={0.5} alignItems="center">
-        <TopSearchBarCommandPaletteTrigger />
-        {unifiedHistoryEnabled && <HistoryContainer />}
-        <QuickAdd />
-        {enrichedHelpNode && (
-          <Dropdown overlay={() => <TopNavBarMenu node={enrichedHelpNode} />} placement="bottom-end">
-            <ToolbarButton iconOnly icon="question-circle" aria-label={t('navigation.help.aria-label', 'Help')} />
-          </Dropdown>
-        )}
-        <ToolbarButton
-          icon="monitor"
-          className={styles.kioskToggle}
-          onClick={onToggleKioskMode}
-          tooltip={t('navigation.kiosk.tooltip', 'Enable kiosk mode')}
-        />
-        {!contextSrv.user.isSignedIn && <SignInLink />}
-        {config.featureToggles.inviteUserExperimental && <InviteUserButton />}
-        {config.featureToggles.extensionSidebar && <ExtensionToolbarItem />}
-        {profileNode && <ProfileButton profileNode={profileNode} />}
-      </Stack>
-    </div>
+        <Stack
+          gap={0.5}
+          alignItems="center"
+          justifyContent={'flex-end'}
+          flex={1}
+          data-testid={!showToolbarLevel ? Components.NavToolbar.container : undefined}
+          minWidth={{ xs: 'unset', lg: 0 }}
+        >
+          <TopSearchBarCommandPaletteTrigger />
+          {unifiedHistoryEnabled && !isSmallScreen && <HistoryContainer />}
+          {!isSmallScreen && <QuickAdd />}
+          {enrichedHelpNode && (
+            <Dropdown overlay={() => <TopNavBarMenu node={enrichedHelpNode} />} placement="bottom-end">
+              <ToolbarButton iconOnly icon="question-circle" aria-label={t('navigation.help.aria-label', 'Help')} />
+            </Dropdown>
+          )}
+          <NavToolbarSeparator />
path: public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx
user: login: joshhunt
body: I looked briefly and I can't see why, but I get an extra separator on non-dashboards pages (such as /admin)

![image](https://github.com/user-attachments/assets/83696e70-e3ef-4c2b-8de7-0d33a3e3e86a)

I have toggle disabled.

created_at: 2025-04-07T16:18:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -18,100 +21,113 @@ import { buildBreadcrumbs } from '../../Breadcrumbs/utils';
 import { ExtensionToolbarItem } from '../ExtensionSidebar/ExtensionToolbarItem';
 import { HistoryContainer } from '../History/HistoryContainer';
 import { enrichHelpItem } from '../MegaMenu/utils';
+import { NavToolbarSeparator } from '../NavToolbar/NavToolbarSeparator';
 import { QuickAdd } from '../QuickAdd/QuickAdd';
-import { TOP_BAR_LEVEL_HEIGHT } from '../types';
 
 import { InviteUserButton } from './InviteUserButton';
 import { ProfileButton } from './ProfileButton';
 import { SignInLink } from './SignInLink';
+import { SingleTopBarActions } from './SingleTopBarActions';
 import { TopNavBarMenu } from './TopNavBarMenu';
 import { TopSearchBarCommandPaletteTrigger } from './TopSearchBarCommandPaletteTrigger';
+import { getChromeHeaderLevelHeight } from './useChromeHeaderHeight';
 
 interface Props {
   sectionNav: NavModelItem;
   pageNav?: NavModelItem;
   onToggleMegaMenu(): void;
   onToggleKioskMode(): void;
+  actions?: React.ReactNode;
+  breadcrumbActions?: React.ReactNode;
+  scopes?: ScopesContextValue | undefined;
+  showToolbarLevel: boolean;
 }
 
 export const SingleTopBar = memo(function SingleTopBar({
   onToggleMegaMenu,
   onToggleKioskMode,
   pageNav,
   sectionNav,
+  scopes,
+  actions,
+  breadcrumbActions,
+  showToolbarLevel,
 }: Props) {
   const { chrome } = useGrafana();
   const state = chrome.useState();
   const menuDockedAndOpen = !state.chromeless && state.megaMenuDocked && state.megaMenuOpen;
   const styles = useStyles2(getStyles, menuDockedAndOpen);
   const navIndex = useSelector((state) => state.navIndex);
-
   const helpNode = cloneDeep(navIndex['help']);
   const enrichedHelpNode = helpNode ? enrichHelpItem(helpNode) : undefined;
   const profileNode = navIndex['profile'];
   const homeNav = useSelector((state) => state.navIndex)[HOME_NAV_ID];
   const breadcrumbs = buildBreadcrumbs(sectionNav, pageNav, homeNav);
   const unifiedHistoryEnabled = config.featureToggles.unifiedHistory;
+  const isSmallScreen = !useMediaQueryMinWidth('sm');
 
   return (
-    <div className={styles.layout}>
-      <Stack minWidth={0} gap={0.5} alignItems="center">
-        {!menuDockedAndOpen && (
-          <ToolbarButton
-            narrow
-            id={MEGA_MENU_TOGGLE_ID}
-            onClick={onToggleMegaMenu}
-            tooltip={t('navigation.megamenu.open', 'Open menu')}
-          >
-            <Stack gap={0} alignItems="center">
-              <Branding.MenuLogo className={styles.img} />
-              <Icon size="sm" name="angle-down" />
-            </Stack>
-          </ToolbarButton>
-        )}
-        <Breadcrumbs breadcrumbs={breadcrumbs} className={styles.breadcrumbsWrapper} />
-      </Stack>
+    <>
+      <div className={styles.layout}>
+        <Stack minWidth={0} gap={0.5} alignItems="center" flex={{ xs: 2, lg: 1 }}>
+          {!menuDockedAndOpen && (
+            <ToolbarButton
+              narrow
+              id={MEGA_MENU_TOGGLE_ID}
+              onClick={onToggleMegaMenu}
+              tooltip={t('navigation.megamenu.open', 'Open menu')}
+            >
+              <Stack gap={0} alignItems="center">
+                <Branding.MenuLogo className={styles.img} />
+                <Icon size="sm" name="angle-down" />
+              </Stack>
+            </ToolbarButton>
+          )}
+          <Breadcrumbs breadcrumbs={breadcrumbs} className={styles.breadcrumbsWrapper} />
+          {!showToolbarLevel && breadcrumbActions}
+        </Stack>
 
-      <Stack gap={0.5} alignItems="center">
-        <TopSearchBarCommandPaletteTrigger />
-        {unifiedHistoryEnabled && <HistoryContainer />}
-        <QuickAdd />
-        {enrichedHelpNode && (
-          <Dropdown overlay={() => <TopNavBarMenu node={enrichedHelpNode} />} placement="bottom-end">
-            <ToolbarButton iconOnly icon="question-circle" aria-label={t('navigation.help.aria-label', 'Help')} />
-          </Dropdown>
-        )}
-        <ToolbarButton
-          icon="monitor"
-          className={styles.kioskToggle}
-          onClick={onToggleKioskMode}
-          tooltip={t('navigation.kiosk.tooltip', 'Enable kiosk mode')}
-        />
-        {!contextSrv.user.isSignedIn && <SignInLink />}
-        {config.featureToggles.inviteUserExperimental && <InviteUserButton />}
-        {config.featureToggles.extensionSidebar && <ExtensionToolbarItem />}
-        {profileNode && <ProfileButton profileNode={profileNode} />}
-      </Stack>
-    </div>
+        <Stack
+          gap={0.5}
+          alignItems="center"
+          justifyContent={'flex-end'}
+          flex={1}
+          data-testid={!showToolbarLevel ? Components.NavToolbar.container : undefined}
+          minWidth={{ xs: 'unset', lg: 0 }}
+        >
+          <TopSearchBarCommandPaletteTrigger />
+          {unifiedHistoryEnabled && !isSmallScreen && <HistoryContainer />}
+          {!isSmallScreen && <QuickAdd />}
+          {enrichedHelpNode && (
+            <Dropdown overlay={() => <TopNavBarMenu node={enrichedHelpNode} />} placement="bottom-end">
+              <ToolbarButton iconOnly icon="question-circle" aria-label={t('navigation.help.aria-label', 'Help')} />
+            </Dropdown>
+          )}
+          <NavToolbarSeparator />
+          {config.featureToggles.inviteUserExperimental && !isSmallScreen && !actions && <InviteUserButton />}
path: public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx
user: login: torkelo
body: @joshhunt you need external user management as well 
created_at: 2025-04-08T07:49:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -18,100 +21,113 @@ import { buildBreadcrumbs } from '../../Breadcrumbs/utils';
 import { ExtensionToolbarItem } from '../ExtensionSidebar/ExtensionToolbarItem';
 import { HistoryContainer } from '../History/HistoryContainer';
 import { enrichHelpItem } from '../MegaMenu/utils';
+import { NavToolbarSeparator } from '../NavToolbar/NavToolbarSeparator';
 import { QuickAdd } from '../QuickAdd/QuickAdd';
-import { TOP_BAR_LEVEL_HEIGHT } from '../types';
 
 import { InviteUserButton } from './InviteUserButton';
 import { ProfileButton } from './ProfileButton';
 import { SignInLink } from './SignInLink';
+import { SingleTopBarActions } from './SingleTopBarActions';
 import { TopNavBarMenu } from './TopNavBarMenu';
 import { TopSearchBarCommandPaletteTrigger } from './TopSearchBarCommandPaletteTrigger';
+import { getChromeHeaderLevelHeight } from './useChromeHeaderHeight';
 
 interface Props {
   sectionNav: NavModelItem;
   pageNav?: NavModelItem;
   onToggleMegaMenu(): void;
   onToggleKioskMode(): void;
+  actions?: React.ReactNode;
+  breadcrumbActions?: React.ReactNode;
+  scopes?: ScopesContextValue | undefined;
+  showToolbarLevel: boolean;
 }
 
 export const SingleTopBar = memo(function SingleTopBar({
   onToggleMegaMenu,
   onToggleKioskMode,
   pageNav,
   sectionNav,
+  scopes,
+  actions,
+  breadcrumbActions,
+  showToolbarLevel,
 }: Props) {
   const { chrome } = useGrafana();
   const state = chrome.useState();
   const menuDockedAndOpen = !state.chromeless && state.megaMenuDocked && state.megaMenuOpen;
   const styles = useStyles2(getStyles, menuDockedAndOpen);
   const navIndex = useSelector((state) => state.navIndex);
-
   const helpNode = cloneDeep(navIndex['help']);
   const enrichedHelpNode = helpNode ? enrichHelpItem(helpNode) : undefined;
   const profileNode = navIndex['profile'];
   const homeNav = useSelector((state) => state.navIndex)[HOME_NAV_ID];
   const breadcrumbs = buildBreadcrumbs(sectionNav, pageNav, homeNav);
   const unifiedHistoryEnabled = config.featureToggles.unifiedHistory;
+  const isSmallScreen = !useMediaQueryMinWidth('sm');
 
   return (
-    <div className={styles.layout}>
-      <Stack minWidth={0} gap={0.5} alignItems="center">
-        {!menuDockedAndOpen && (
-          <ToolbarButton
-            narrow
-            id={MEGA_MENU_TOGGLE_ID}
-            onClick={onToggleMegaMenu}
-            tooltip={t('navigation.megamenu.open', 'Open menu')}
-          >
-            <Stack gap={0} alignItems="center">
-              <Branding.MenuLogo className={styles.img} />
-              <Icon size="sm" name="angle-down" />
-            </Stack>
-          </ToolbarButton>
-        )}
-        <Breadcrumbs breadcrumbs={breadcrumbs} className={styles.breadcrumbsWrapper} />
-      </Stack>
+    <>
+      <div className={styles.layout}>
+        <Stack minWidth={0} gap={0.5} alignItems="center" flex={{ xs: 2, lg: 1 }}>
+          {!menuDockedAndOpen && (
+            <ToolbarButton
+              narrow
+              id={MEGA_MENU_TOGGLE_ID}
+              onClick={onToggleMegaMenu}
+              tooltip={t('navigation.megamenu.open', 'Open menu')}
+            >
+              <Stack gap={0} alignItems="center">
+                <Branding.MenuLogo className={styles.img} />
+                <Icon size="sm" name="angle-down" />
+              </Stack>
+            </ToolbarButton>
+          )}
+          <Breadcrumbs breadcrumbs={breadcrumbs} className={styles.breadcrumbsWrapper} />
+          {!showToolbarLevel && breadcrumbActions}
+        </Stack>
 
-      <Stack gap={0.5} alignItems="center">
-        <TopSearchBarCommandPaletteTrigger />
-        {unifiedHistoryEnabled && <HistoryContainer />}
-        <QuickAdd />
-        {enrichedHelpNode && (
-          <Dropdown overlay={() => <TopNavBarMenu node={enrichedHelpNode} />} placement="bottom-end">
-            <ToolbarButton iconOnly icon="question-circle" aria-label={t('navigation.help.aria-label', 'Help')} />
-          </Dropdown>
-        )}
-        <ToolbarButton
-          icon="monitor"
-          className={styles.kioskToggle}
-          onClick={onToggleKioskMode}
-          tooltip={t('navigation.kiosk.tooltip', 'Enable kiosk mode')}
-        />
-        {!contextSrv.user.isSignedIn && <SignInLink />}
-        {config.featureToggles.inviteUserExperimental && <InviteUserButton />}
-        {config.featureToggles.extensionSidebar && <ExtensionToolbarItem />}
-        {profileNode && <ProfileButton profileNode={profileNode} />}
-      </Stack>
-    </div>
+        <Stack
+          gap={0.5}
+          alignItems="center"
+          justifyContent={'flex-end'}
+          flex={1}
+          data-testid={!showToolbarLevel ? Components.NavToolbar.container : undefined}
+          minWidth={{ xs: 'unset', lg: 0 }}
+        >
+          <TopSearchBarCommandPaletteTrigger />
+          {unifiedHistoryEnabled && !isSmallScreen && <HistoryContainer />}
+          {!isSmallScreen && <QuickAdd />}
+          {enrichedHelpNode && (
+            <Dropdown overlay={() => <TopNavBarMenu node={enrichedHelpNode} />} placement="bottom-end">
+              <ToolbarButton iconOnly icon="question-circle" aria-label={t('navigation.help.aria-label', 'Help')} />
+            </Dropdown>
+          )}
+          <NavToolbarSeparator />
+          {config.featureToggles.inviteUserExperimental && !isSmallScreen && !actions && <InviteUserButton />}
path: public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx
user: login: torkelo
body: @joshhunt this responsive fix is not related to unified navbars but just a responsive fix for small screens 

On ops right now 
![Screenshot 2025-04-08 at 09 50 34](https://github.com/user-attachments/assets/867e2446-146d-4877-a4fc-810d4401db29)
![Screenshot 2025-04-08 at 09 50 41](https://github.com/user-attachments/assets/f73e7fb2-b3e0-4a3c-b0ce-8225cd715aab)

created_at: 2025-04-08T07:50:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,12 +46,21 @@ export const RightActions = ({ dashboard }: { dashboard: DashboardScene }) => {
 
   const showPanelButtons = isEditingPanel && !hasEditView && !isViewingPanel;
   const showPlayButtons = isPlaying && isShowingDashboard && !isEditingDashboard;
-  const showShareButton = hasUid && !isSnapshot && !isPlaying;
+  const showShareButton = hasUid && !isSnapshot && !isPlaying && !isEditingPanel;
 
   return (
     <ToolbarButtonRow alignment="right" className={styles.container}>
       {renderActionElements(
         [
+          {
+            key: 'dashboard-settings',
+            component: KioskToggleButton,
+            group: '',
+            condition: !isEditingDashboard,
path: public/app/features/dashboard-scene/scene/new-toolbar/RightActions.tsx
user: login: torkelo
body: @joshhunt have you not been lobbying for it to only exist in dashboards for a while?  I would instead we move it to profile menu. Given the new app sidecar icon, new invite button, unified history we have in global nav something needs to be removed, we cannot only add things 
created_at: 2025-04-08T07:52:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,12 +46,21 @@ export const RightActions = ({ dashboard }: { dashboard: DashboardScene }) => {
 
   const showPanelButtons = isEditingPanel && !hasEditView && !isViewingPanel;
   const showPlayButtons = isPlaying && isShowingDashboard && !isEditingDashboard;
-  const showShareButton = hasUid && !isSnapshot && !isPlaying;
+  const showShareButton = hasUid && !isSnapshot && !isPlaying && !isEditingPanel;
 
   return (
     <ToolbarButtonRow alignment="right" className={styles.container}>
       {renderActionElements(
         [
+          {
+            key: 'dashboard-settings',
+            component: KioskToggleButton,
+            group: '',
+            condition: !isEditingDashboard,
path: public/app/features/dashboard-scene/scene/new-toolbar/RightActions.tsx
user: login: torkelo
body: @EdPoole given the "Change theme" we have in profile drawer now , how do you feel about a new entry there "Enter kiosk mode" ? just feels so much better to have this action in a menu vs taking up icon button space ALL the time 
created_at: 2025-04-08T07:55:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -18,100 +21,113 @@ import { buildBreadcrumbs } from '../../Breadcrumbs/utils';
 import { ExtensionToolbarItem } from '../ExtensionSidebar/ExtensionToolbarItem';
 import { HistoryContainer } from '../History/HistoryContainer';
 import { enrichHelpItem } from '../MegaMenu/utils';
+import { NavToolbarSeparator } from '../NavToolbar/NavToolbarSeparator';
 import { QuickAdd } from '../QuickAdd/QuickAdd';
-import { TOP_BAR_LEVEL_HEIGHT } from '../types';
 
 import { InviteUserButton } from './InviteUserButton';
 import { ProfileButton } from './ProfileButton';
 import { SignInLink } from './SignInLink';
+import { SingleTopBarActions } from './SingleTopBarActions';
 import { TopNavBarMenu } from './TopNavBarMenu';
 import { TopSearchBarCommandPaletteTrigger } from './TopSearchBarCommandPaletteTrigger';
+import { getChromeHeaderLevelHeight } from './useChromeHeaderHeight';
 
 interface Props {
   sectionNav: NavModelItem;
   pageNav?: NavModelItem;
   onToggleMegaMenu(): void;
   onToggleKioskMode(): void;
+  actions?: React.ReactNode;
+  breadcrumbActions?: React.ReactNode;
+  scopes?: ScopesContextValue | undefined;
+  showToolbarLevel: boolean;
 }
 
 export const SingleTopBar = memo(function SingleTopBar({
   onToggleMegaMenu,
   onToggleKioskMode,
   pageNav,
   sectionNav,
+  scopes,
+  actions,
+  breadcrumbActions,
+  showToolbarLevel,
 }: Props) {
   const { chrome } = useGrafana();
   const state = chrome.useState();
   const menuDockedAndOpen = !state.chromeless && state.megaMenuDocked && state.megaMenuOpen;
   const styles = useStyles2(getStyles, menuDockedAndOpen);
   const navIndex = useSelector((state) => state.navIndex);
-
   const helpNode = cloneDeep(navIndex['help']);
   const enrichedHelpNode = helpNode ? enrichHelpItem(helpNode) : undefined;
   const profileNode = navIndex['profile'];
   const homeNav = useSelector((state) => state.navIndex)[HOME_NAV_ID];
   const breadcrumbs = buildBreadcrumbs(sectionNav, pageNav, homeNav);
   const unifiedHistoryEnabled = config.featureToggles.unifiedHistory;
+  const isSmallScreen = !useMediaQueryMinWidth('sm');
 
   return (
-    <div className={styles.layout}>
-      <Stack minWidth={0} gap={0.5} alignItems="center">
-        {!menuDockedAndOpen && (
-          <ToolbarButton
-            narrow
-            id={MEGA_MENU_TOGGLE_ID}
-            onClick={onToggleMegaMenu}
-            tooltip={t('navigation.megamenu.open', 'Open menu')}
-          >
-            <Stack gap={0} alignItems="center">
-              <Branding.MenuLogo className={styles.img} />
-              <Icon size="sm" name="angle-down" />
-            </Stack>
-          </ToolbarButton>
-        )}
-        <Breadcrumbs breadcrumbs={breadcrumbs} className={styles.breadcrumbsWrapper} />
-      </Stack>
+    <>
+      <div className={styles.layout}>
+        <Stack minWidth={0} gap={0.5} alignItems="center" flex={{ xs: 2, lg: 1 }}>
+          {!menuDockedAndOpen && (
+            <ToolbarButton
+              narrow
+              id={MEGA_MENU_TOGGLE_ID}
+              onClick={onToggleMegaMenu}
+              tooltip={t('navigation.megamenu.open', 'Open menu')}
+            >
+              <Stack gap={0} alignItems="center">
+                <Branding.MenuLogo className={styles.img} />
+                <Icon size="sm" name="angle-down" />
+              </Stack>
+            </ToolbarButton>
+          )}
+          <Breadcrumbs breadcrumbs={breadcrumbs} className={styles.breadcrumbsWrapper} />
+          {!showToolbarLevel && breadcrumbActions}
+        </Stack>
 
-      <Stack gap={0.5} alignItems="center">
-        <TopSearchBarCommandPaletteTrigger />
-        {unifiedHistoryEnabled && <HistoryContainer />}
-        <QuickAdd />
-        {enrichedHelpNode && (
-          <Dropdown overlay={() => <TopNavBarMenu node={enrichedHelpNode} />} placement="bottom-end">
-            <ToolbarButton iconOnly icon="question-circle" aria-label={t('navigation.help.aria-label', 'Help')} />
-          </Dropdown>
-        )}
-        <ToolbarButton
-          icon="monitor"
-          className={styles.kioskToggle}
-          onClick={onToggleKioskMode}
-          tooltip={t('navigation.kiosk.tooltip', 'Enable kiosk mode')}
-        />
-        {!contextSrv.user.isSignedIn && <SignInLink />}
-        {config.featureToggles.inviteUserExperimental && <InviteUserButton />}
-        {config.featureToggles.extensionSidebar && <ExtensionToolbarItem />}
-        {profileNode && <ProfileButton profileNode={profileNode} />}
-      </Stack>
-    </div>
+        <Stack
+          gap={0.5}
+          alignItems="center"
+          justifyContent={'flex-end'}
+          flex={1}
+          data-testid={!showToolbarLevel ? Components.NavToolbar.container : undefined}
+          minWidth={{ xs: 'unset', lg: 0 }}
+        >
+          <TopSearchBarCommandPaletteTrigger />
+          {unifiedHistoryEnabled && !isSmallScreen && <HistoryContainer />}
+          {!isSmallScreen && <QuickAdd />}
+          {enrichedHelpNode && (
+            <Dropdown overlay={() => <TopNavBarMenu node={enrichedHelpNode} />} placement="bottom-end">
+              <ToolbarButton iconOnly icon="question-circle" aria-label={t('navigation.help.aria-label', 'Help')} />
+            </Dropdown>
+          )}
+          <NavToolbarSeparator />
path: public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx
user: login: torkelo
body: @joshhunt good catch! The invite button had double separator lines inside it. Fixed 
created_at: 2025-04-08T07:58:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,6 +30,9 @@ const getStyles = (theme: GrafanaTheme2) => {
       height: 24,
       flexShrink: 0,
       flexGrow: 0,
+      [theme.breakpoints.down('sm')]: {
+        display: 'none',
+      },
path: public/app/core/components/AppChrome/NavToolbar/NavToolbarSeparator.tsx
user: login: torkelo
body: @svennergr looks good to me, on mobile they don't add much and take up precious space 

![Screenshot 2025-04-08 at 10 05 44](https://github.com/user-attachments/assets/cb3c7c08-ccaf-471a-92f1-0805353d7248)

created_at: 2025-04-08T08:06:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,12 +46,21 @@ export const RightActions = ({ dashboard }: { dashboard: DashboardScene }) => {
 
   const showPanelButtons = isEditingPanel && !hasEditView && !isViewingPanel;
   const showPlayButtons = isPlaying && isShowingDashboard && !isEditingDashboard;
-  const showShareButton = hasUid && !isSnapshot && !isPlaying;
+  const showShareButton = hasUid && !isSnapshot && !isPlaying && !isEditingPanel;
 
   return (
     <ToolbarButtonRow alignment="right" className={styles.container}>
       {renderActionElements(
         [
+          {
+            key: 'dashboard-settings',
+            component: KioskToggleButton,
+            group: '',
+            condition: !isEditingDashboard,
path: public/app/features/dashboard-scene/scene/new-toolbar/RightActions.tsx
user: login: torkelo
body: @joshhunt merged a separate PR into main and merged main into this PR, now kiosk action lives in profile drawer 
created_at: 2025-04-08T12:59:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -86,6 +86,7 @@
 | `azureMonitorEnableUserAuth`           | Enables user auth for Azure Monitor datasource only                                                                                 | Yes                |
 | `alertingNotificationsStepMode`        | Enables simplified step mode in the notifications section                                                                           | Yes                |
 | `lokiLabelNamesQueryApi`               | Defaults to using the Loki `/labels` API instead of `/series`                                                                       | Yes                |
+| `unifiedNavbars`                       | Enables unified navbars                                                                                                             |                    |
path: docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
user: login: github-advanced-security[bot]
body: Did you really mean 'navbars'? The Grafana dictionary might not know of this word yet

[Show more details](https://github.com/grafana/grafana/security/code-scanning/1755)
created_at: 2025-04-09T05:03:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -72,7 +76,15 @@ const getStyles = (theme: GrafanaTheme2) => {
   const baseStyles = getInputStyles({ theme });
 
   return {
-    wrapper: baseStyles.wrapper,
+    wrapper: cx(
+      baseStyles.wrapper,
+      css({
+        width: 'auto',
+        minWidth: 140,
+        maxWidth: 350,
+        flexGrow: 1,
+      })
+    ),
path: public/app/core/components/AppChrome/TopBar/TopSearchBarCommandPaletteTrigger.tsx
user: login: ashharrison90
body: i don't think these values will work in other languages 🤔 don't have to do anything about it now, just stating it 
created_at: 2025-04-09T09:03:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -9,9 +9,10 @@ import { AccessControlAction } from 'app/types';
 import { NavToolbarSeparator } from '../NavToolbar/NavToolbarSeparator';
 
 export function InviteUserButton() {
+  config.externalUserMngLinkUrl = '(true)';
path: public/app/core/components/AppChrome/TopBar/InviteUserButton.tsx
user: login: ashharrison90
body: don't have context - what does this do? 🤔 
created_at: 2025-04-09T09:06:12Z
author_association: CONTRIBUTOR
path: public/app/core/components/AppChrome/TopBar/useChromeHeaderHeight.ts
user: login: ashharrison90
body: this is so much complexity 😭 

on the plus side: it's almost all contained here. but as a follow up we need a better long term solution.
created_at: 2025-04-09T09:18:11Z
author_association: CONTRIBUTOR
diff_hunk: @@ -9,9 +9,10 @@ import { AccessControlAction } from 'app/types';
 import { NavToolbarSeparator } from '../NavToolbar/NavToolbarSeparator';
 
 export function InviteUserButton() {
+  config.externalUserMngLinkUrl = '(true)';
path: public/app/core/components/AppChrome/TopBar/InviteUserButton.tsx
user: login: torkelo
body: @ashharrison90 sorry, this was just to test the invite button temporarily, will revert 
created_at: 2025-04-09T10:33:19Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-05T13:36:39Z
body: :computer: Deploy preview deleted.
author_association: CONTRIBUTOR
user: login: torkelo
created_at: 2025-04-05T14:20:06Z
body: Fixes this explore & app chrome bug 

[explore_bug.webm](https://github.com/user-attachments/assets/7eed1fa9-ee0c-4dd5-bb84-bfaf074e256e)

Before: 
![Screenshot 2025-04-05 at 15 38 42](https://github.com/user-attachments/assets/2b2e91c2-f1e5-4591-9624-1650e6e22aae)

After:  (mainly effect of hiding invite)
![Screenshot 2025-04-05 at 15 38 49](https://github.com/user-attachments/assets/c7b85ec0-6e92-4e1e-b5a3-eb42fa3d830c)

Before even smaller screen:
![Screenshot 2025-04-05 at 15 40 53](https://github.com/user-attachments/assets/f82f754a-d79a-48c4-8d82-21048be4b81e)

After even smaller screen: 
![Screenshot 2025-04-05 at 15 41 40](https://github.com/user-attachments/assets/56ec4a2c-a336-4a73-84c2-e88c91f1c58a)

The improvement would be better as there is no sidebar app / icon in that test 




author_association: CONTRIBUTOR
user: login: torkelo
created_at: 2025-04-07T06:01:21Z
body: /deploy-to-hg 


author_association: CONTRIBUTOR
user: login: ephemeral-instances-bot[bot]
created_at: 2025-04-07T06:01:55Z
body: - Preparing your instance. A comment containing your instance's url will be added to this PR when the instance is ready.
- Your instance will be ready in ~10 minutes. [Follow the workflow progress](https://github.com/grafana/grafana/actions/runs/14301950201)
- Slack channel: #proj-ephemeral-hg-instances
- Building instance with **unified-navbars** oss branch and **main** enterprise branch. [How to choose a branch](https://github.com/grafana/hosted-grafana/wiki/Ephemeral-instances#deploying-a-grafana-and-grafana-enterprise-branch-together)
author_association: NONE
user: login: ephemeral-instances-bot[bot]
created_at: 2025-04-07T06:21:01Z
body: - Your instance can be accessed at: https://ephemeral15111821103488torkel.grafana-dev.net
- The instance is not using the CDN assets.
- [How to access / How to update instance config / How to build a specific branch](https://github.com/grafana/hosted-grafana/wiki/Ephemeral-instances#choosing-which-oss-and-enterprise-branches-are-used-to-build-the-instance)
author_association: NONE
user: login: yaelleC
created_at: 2025-04-07T08:05:52Z
body: Playing around in the ephemeral, when you have the menu docked leaving not enough space for the unified nav bars it switches to 2 bars, but also leaves a blank bar if there is nothing to show there.
Not reproduce-able in smaller viewports, just wide + menu docked 🤷‍♀️ 

|before|after|
|-----------|-----|
|![Screenshot 2025-04-07 at 10 02 47](https://github.com/user-attachments/assets/9605630f-3db8-4dbe-acb7-1990e59cfdc8)|![Screenshot 2025-04-07 at 10 03 34](https://github.com/user-attachments/assets/ebaa68fd-2185-4334-a741-9a2c054ed4a7)|

author_association: CONTRIBUTOR
user: login: torkelo
created_at: 2025-04-07T08:17:49Z
body: @yaelleC ah, good catch I am missing a condition there when menu is docked and there are no toolbar actions! will fix 


author_association: CONTRIBUTOR
user: login: torkelo
created_at: 2025-04-08T08:08:36Z
body: > Not exactly sure which code change, but I think this needs some improvement. The extensionsidebar button can very likely be hidden in edit mode:

@svennergr  does not look more hidden than usual or do you mean the add button? The add button is going away new dashboard layout editing (will be add buttons directly on the dashboard canvas) 

author_association: CONTRIBUTOR
user: login: svennergr
created_at: 2025-04-08T08:10:55Z
body: > > Not exactly sure which code change, but I think this needs some improvement. The extensionsidebar button can very likely be hidden in edit mode:
> 
> @svennergr does not look more hidden than usual or do you mean the add button? The add button is going away new dashboard layout editing (will be add buttons directly on the dashboard canvas)

Ah, okay, that makes sense 👍 . Yea, I was afraid the `Add` button would be too hidden. Let me try again with `dashboardsNewLayouts`.
author_association: CONTRIBUTOR
user: login: torkelo
created_at: 2025-04-08T08:12:31Z
body: @svennergr and we will make further clean up of dashboard buttons in navtoolbar (move save button inside edit pane), combine export + share into one action ec. So will only be "Share" and Edit buttons in navbar. but that will come later. We can keep this feature toggle disabled by default until we are happy there 
author_association: CONTRIBUTOR
user: login: torkelo
created_at: 2025-04-09T05:04:37Z
body: pushed an update to disable feature toggle by default 
author_association: CONTRIBUTOR
user: login: torkelo
created_at: 2025-04-09T10:41:24Z
body: @ashharrison90 

> i think it's a step backwards for the overall architecture of the nav. imo keeping that top bar always global is a good thing

to me it's a step toward a better and more elegant/minimalistic design, one that forces simplicity vs the "Show all the actions in a random button corner" thing we have now. 

To me this is pretty ugly design with random buttons and icons stacked on top, with no alignment or much separation. I don't think this PR alone solves it but sets us on a path to have a single navbar with 1-2 page specific actions (Share + edit), and a much more separated and clear page-specific toolbar that can be used more sparingly (dashboards only in edit mode)   

![Screenshot 2025-04-09 at 12 35 19](https://github.com/user-attachments/assets/e1f775f2-bbe6-4169-bd3a-b6ff730120a1)

author_association: CONTRIBUTOR
user: login: torkelo
created_at: 2025-04-09T10:52:07Z
body: @ashharrison90 the other option is to revert so we can have an optional top level that can be collapsed with 

```
scopes +                                                search     global icons / buttons 
---------------------------------------------------------------------
breadcrumbs <star/bookmark>       <page actions> <collapse top level>
```

The problem with the current version was that we lost the ability to collapse the global level, which was very useful as it's seldom needed. The change to "single top nav" was I think a bit mistaken (my idea,  I know), in that it prioritized all pages that do not need page level AppChrome actions. Given dashboards, explore and many coming scene apps will need that maybe that was bad thing to prioritize those admin/simple pages that do no need it.

With the change to single top nav we made the page specific actions it's own full row, but we do not have enough actions to warrant the use of a full row is my realization. And because you cannot collapse the top row (as the breadcrumbs are essential) it kind of left dashboards and other pages a lot worse of.  

author_association: CONTRIBUTOR
user: login: ashharrison90
created_at: 2025-04-09T11:20:39Z
body: @torkelo unified history doesn't exist, the invite button doesn't exist (or shouldn't - imo we should push back heavily on that actually being included in the global nav).

i'm not disagreeing we need to do something about the page actions, i'm just disagreeing about how we do It long-term 😄 having a dedicated space for 1 or 2 page actions in that global nav (as you've said is the intention) might be ok. i just want someone to spend a bit of time post-g12 to map out our different page scenarios and propose some options 🙏 
author_association: CONTRIBUTOR