diff --git a/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.tsx b/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.tsx
index b99e176a142..05a3c5c5bdb 100644
--- a/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.tsx
+++ b/packages/grafana-ui/src/components/ToolbarButton/ToolbarButton.tsx
@@ -224,6 +224,7 @@ const getStyles = (theme: GrafanaTheme2) => {
flexGrow: 1,
}),
content: css({
+ display: 'flex',
flexGrow: 1,
}),
contentWithIcon: css({
diff --git a/public/app/core/components/AppChrome/AppChrome.tsx b/public/app/core/components/AppChrome/AppChrome.tsx
index 64d9b0bb058..7e5cfd0d365 100644
--- a/public/app/core/components/AppChrome/AppChrome.tsx
+++ b/public/app/core/components/AppChrome/AppChrome.tsx
@@ -3,7 +3,7 @@ import classNames from 'classnames';
import { PropsWithChildren, useEffect } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
-import { locationSearchToObject, locationService } from '@grafana/runtime';
+import { config, locationSearchToObject, locationService } from '@grafana/runtime';
import { useStyles2, LinkButton, useTheme2 } from '@grafana/ui';
import { useGrafana } from 'app/core/context/GrafanaContext';
import { useMediaQueryChange } from 'app/core/hooks/useMediaQueryChange';
@@ -17,6 +17,7 @@ import { DOCKED_LOCAL_STORAGE_KEY, DOCKED_MENU_OPEN_LOCAL_STORAGE_KEY } from './
import { MegaMenu, MENU_WIDTH } from './MegaMenu/MegaMenu';
import { NavToolbar } from './NavToolbar/NavToolbar';
import { ReturnToPrevious } from './ReturnToPrevious/ReturnToPrevious';
+import { SingleTopBar } from './TopBar/SingleTopBar';
import { TopSearchBar } from './TopBar/TopSearchBar';
import { TOP_BAR_LEVEL_HEIGHT } from './types';
@@ -34,6 +35,7 @@ export function AppChrome({ children }: Props) {
const menuDockedAndOpen = !state.chromeless && state.megaMenuDocked && state.megaMenuOpen;
const scopesDashboardsState = useScopesDashboardsState();
const isScopesDashboardsOpen = Boolean(scopesDashboardsState?.isEnabled && scopesDashboardsState?.isPanelOpened);
+ const isSingleTopNav = config.featureToggles.singleTopNav;
useMediaQueryChange({
breakpoint: dockedMenuBreakpoint,
@@ -92,16 +94,27 @@ export function AppChrome({ children }: Props) {
Skip to main content
- {!searchBarHidden && }
-
+ {isSingleTopNav ? (
+
+ ) : (
+ <>
+ {!searchBarHidden && }
+
+ >
+ )}
>
)}
@@ -140,11 +153,12 @@ export function AppChrome({ children }: Props) {
}
const getStyles = (theme: GrafanaTheme2, searchBarHidden: boolean) => {
+ const isSingleTopNav = config.featureToggles.singleTopNav;
return {
content: css({
display: 'flex',
flexDirection: 'column',
- paddingTop: TOP_BAR_LEVEL_HEIGHT * 2,
+ paddingTop: isSingleTopNav ? TOP_BAR_LEVEL_HEIGHT : TOP_BAR_LEVEL_HEIGHT * 2,
flexGrow: 1,
height: 'auto',
}),
@@ -167,13 +181,13 @@ const getStyles = (theme: GrafanaTheme2, searchBarHidden: boolean) => {
},
{
position: 'fixed',
- height: `calc(100% - ${searchBarHidden ? TOP_BAR_LEVEL_HEIGHT : TOP_BAR_LEVEL_HEIGHT * 2}px)`,
+ height: `calc(100% - ${searchBarHidden || isSingleTopNav ? TOP_BAR_LEVEL_HEIGHT : TOP_BAR_LEVEL_HEIGHT * 2}px)`,
zIndex: 2,
}
),
scopesDashboardsContainer: css({
position: 'fixed',
- height: `calc(100% - ${searchBarHidden ? TOP_BAR_LEVEL_HEIGHT : TOP_BAR_LEVEL_HEIGHT * 2}px)`,
+ height: `calc(100% - ${searchBarHidden || isSingleTopNav ? TOP_BAR_LEVEL_HEIGHT : TOP_BAR_LEVEL_HEIGHT * 2}px)`,
zIndex: 1,
}),
scopesDashboardsContainerDocked: css({
diff --git a/public/app/core/components/AppChrome/AppChromeMenu.tsx b/public/app/core/components/AppChrome/AppChromeMenu.tsx
index fb670aff321..bf5022ec01c 100644
--- a/public/app/core/components/AppChrome/AppChromeMenu.tsx
+++ b/public/app/core/components/AppChrome/AppChromeMenu.tsx
@@ -6,6 +6,7 @@ import { useRef } from 'react';
import CSSTransition from 'react-transition-group/CSSTransition';
import { GrafanaTheme2 } from '@grafana/data';
+import { config } from '@grafana/runtime';
import { useStyles2, useTheme2 } from '@grafana/ui';
import { useGrafana } from 'app/core/context/GrafanaContext';
import { KioskMode } from 'app/types';
@@ -76,7 +77,8 @@ export function AppChromeMenu({}: Props) {
}
const getStyles = (theme: GrafanaTheme2, searchBarHidden?: boolean) => {
- const topPosition = searchBarHidden ? TOP_BAR_LEVEL_HEIGHT : TOP_BAR_LEVEL_HEIGHT * 2;
+ const topPosition =
+ searchBarHidden || config.featureToggles.singleTopNav ? TOP_BAR_LEVEL_HEIGHT : TOP_BAR_LEVEL_HEIGHT * 2;
return {
backdrop: css({
diff --git a/public/app/core/components/AppChrome/AppChromeService.tsx b/public/app/core/components/AppChrome/AppChromeService.tsx
index 5c1d88ebb41..c041574aff9 100644
--- a/public/app/core/components/AppChrome/AppChromeService.tsx
+++ b/public/app/core/components/AppChrome/AppChromeService.tsx
@@ -213,7 +213,7 @@ export class AppChromeService {
private getNextKioskMode() {
const { kioskMode, searchBarHidden } = this.state.getValue();
- if (searchBarHidden || kioskMode === KioskMode.TV) {
+ if (searchBarHidden || kioskMode === KioskMode.TV || config.featureToggles.singleTopNav) {
appEvents.emit(AppEvents.alertInfo, [t('navigation.kiosk.tv-alert', 'Press ESC to exit kiosk mode')]);
return KioskMode.Full;
}
diff --git a/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx b/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx
new file mode 100644
index 00000000000..1499882fdf9
--- /dev/null
+++ b/public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx
@@ -0,0 +1,131 @@
+import { css } from '@emotion/css';
+import { cloneDeep } from 'lodash';
+import { memo } from 'react';
+
+import { GrafanaTheme2, NavModelItem } from '@grafana/data';
+import { Dropdown, Icon, ToolbarButton, useStyles2 } from '@grafana/ui';
+import { config } from 'app/core/config';
+import { contextSrv } from 'app/core/core';
+import { HOME_NAV_ID } from 'app/core/reducers/navModel';
+import { ScopesSelector } from 'app/features/scopes';
+import { useSelector } from 'app/types';
+
+import { Branding } from '../../Branding/Branding';
+import { Breadcrumbs } from '../../Breadcrumbs/Breadcrumbs';
+import { buildBreadcrumbs } from '../../Breadcrumbs/utils';
+import { enrichHelpItem } from '../MegaMenu/utils';
+import { NewsContainer } from '../News/NewsContainer';
+import { OrganizationSwitcher } from '../OrganizationSwitcher/OrganizationSwitcher';
+import { QuickAdd } from '../QuickAdd/QuickAdd';
+import { TOP_BAR_LEVEL_HEIGHT } from '../types';
+
+import { SignInLink } from './SignInLink';
+import { TopNavBarMenu } from './TopNavBarMenu';
+import { TopSearchBarCommandPaletteTrigger } from './TopSearchBarCommandPaletteTrigger';
+import { TopSearchBarSection } from './TopSearchBarSection';
+
+interface Props {
+ sectionNav: NavModelItem;
+ pageNav?: NavModelItem;
+ onToggleMegaMenu(): void;
+ onToggleKioskMode(): void;
+}
+
+export const SingleTopBar = memo(function SingleTopBar({
+ onToggleMegaMenu,
+ onToggleKioskMode,
+ pageNav,
+ sectionNav,
+}: Props) {
+ const styles = useStyles2(getStyles);
+ 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);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {enrichedHelpNode && (
+ } placement="bottom-end">
+
+
+ )}
+ {config.newsFeedEnabled && }
+ {!contextSrv.user.isSignedIn && }
+ {profileNode && (
+ } placement="bottom-end">
+
+
+ )}
+
+
+
+
+
+ );
+});
+
+const getStyles = (theme: GrafanaTheme2) => ({
+ layout: css({
+ height: TOP_BAR_LEVEL_HEIGHT,
+ display: 'flex',
+ gap: theme.spacing(1),
+ alignItems: 'center',
+ padding: theme.spacing(0, 1),
+ borderBottom: `1px solid ${theme.colors.border.weak}`,
+ justifyContent: 'space-between',
+
+ [theme.breakpoints.up('sm')]: {
+ gridTemplateColumns: '2fr minmax(240px, 1fr)', // TODO probably change these values
+ display: 'grid',
+
+ justifyContent: 'flex-start',
+ },
+ }),
+ breadcrumbsWrapper: css({
+ display: 'flex',
+ overflow: 'hidden',
+ [theme.breakpoints.down('sm')]: {
+ minWidth: '50%',
+ },
+ }),
+ img: css({
+ alignSelf: 'center',
+ height: theme.spacing(3),
+ width: theme.spacing(3),
+ }),
+ profileButton: css({
+ padding: theme.spacing(0, 0.25),
+ img: {
+ borderRadius: theme.shape.radius.circle,
+ height: '24px',
+ marginRight: 0,
+ width: '24px',
+ },
+ }),
+ kioskToggle: css({
+ [theme.breakpoints.down('lg')]: {
+ display: 'none',
+ },
+ }),
+});
diff --git a/public/app/core/context/GrafanaContext.ts b/public/app/core/context/GrafanaContext.ts
index 068f55b48e0..424bd71ee68 100644
--- a/public/app/core/context/GrafanaContext.ts
+++ b/public/app/core/context/GrafanaContext.ts
@@ -1,7 +1,7 @@
import { createContext, useCallback, useContext } from 'react';
import { GrafanaConfig } from '@grafana/data';
-import { LocationService, locationService, BackendSrv } from '@grafana/runtime';
+import { LocationService, locationService, BackendSrv, config } from '@grafana/runtime';
import { AppChromeService } from '../components/AppChrome/AppChromeService';
import { NewFrontendAssetsChecker } from '../services/NewFrontendAssetsChecker';
@@ -50,7 +50,7 @@ export function useChromeHeaderHeight() {
if (kioskMode || chromeless) {
return 0;
- } else if (searchBarHidden) {
+ } else if (searchBarHidden || config.featureToggles.singleTopNav) {
return SINGLE_HEADER_BAR_HEIGHT;
} else {
return SINGLE_HEADER_BAR_HEIGHT * 2;