AppChrome: Improved responsive use of vertical space (#103488)

* Progress

* Progress

* Update

* Update

* Update

* Update

* Responsive improvements

* Update

* Update

* Update

* Revert height change

* Update

* add missing testid

* update

* Fixes

* update

* Refactoring

* fix bug in app chrome service

* Fixed e2e tests

* fix

* fix

* Update

* Update

* fix bookmarks e2e

* Update

* improve responsiveness on small screens for breadcrumbs

* Always use two levels when menu is docked

* Adding kiosk toggle button to dashboards only

* update

* Update

* Update

* when menu is docked and no actions use 1 level

* removed formatting change that caused unnessary diff in PR

* remove extra separator line after merge with main

* Fix double separators

* Update

* remove temp change

* Update
pull/103710/head
Torkel Ödegaard 1 month ago committed by GitHub
parent 927ce79dcf
commit 24474dcb9c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
  2. 5
      packages/grafana-data/src/types/featureToggles.gen.ts
  3. 8
      pkg/services/featuremgmt/registry.go
  4. 1
      pkg/services/featuremgmt/toggles_gen.csv
  5. 4
      pkg/services/featuremgmt/toggles_gen.go
  6. 17
      pkg/services/featuremgmt/toggles_gen.json
  7. 3
      public/app/app.ts
  8. 24
      public/app/core/components/AppChrome/AppChrome.tsx
  9. 20
      public/app/core/components/AppChrome/AppChromeService.tsx
  10. 29
      public/app/core/components/AppChrome/AppChromeUpdate.tsx
  11. 50
      public/app/core/components/AppChrome/ExtensionSidebar/ExtensionToolbarItem.tsx
  12. 4
      public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx
  13. 6
      public/app/core/components/AppChrome/MegaMenu/MegaMenuHeader.tsx
  14. 3
      public/app/core/components/AppChrome/NavToolbar/NavToolbarSeparator.tsx
  15. 37
      public/app/core/components/AppChrome/QuickAdd/QuickAdd.tsx
  16. 7
      public/app/core/components/AppChrome/TopBar/InviteUserButton.tsx
  17. 104
      public/app/core/components/AppChrome/TopBar/SingleTopBar.tsx
  18. 45
      public/app/core/components/AppChrome/TopBar/SingleTopBarActions.tsx
  19. 42
      public/app/core/components/AppChrome/TopBar/TopSearchBarCommandPaletteTrigger.tsx
  20. 99
      public/app/core/components/AppChrome/TopBar/useChromeHeaderHeight.ts
  21. 1
      public/app/core/components/AppChrome/types.ts
  22. 11
      public/app/core/context/GrafanaContext.ts
  23. 30
      public/app/features/dashboard-scene/edit-pane/DashboardEditableElement.tsx
  24. 26
      public/app/features/dashboard-scene/scene/NavToolbarActions.tsx
  25. 19
      public/app/features/dashboard-scene/scene/new-toolbar/LeftActions.tsx
  26. 35
      public/app/features/dashboard-scene/scene/new-toolbar/RightActions.tsx
  27. 6
      public/app/features/dashboard-scene/scene/new-toolbar/actions/EditSchemaV2Button.tsx
  28. 2
      public/app/features/dashboard-scene/scene/new-toolbar/utils.tsx
  29. 5
      public/app/features/explore/Logs/LogsNavigation.tsx
  30. 2
      public/locales/en-US/grafana.json

@ -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

@ -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
*/

@ -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",

@ -232,5 +232,6 @@ 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
metricsFromProfiles,experimental,@grafana/observability-traces-and-profiling,false,false,true

1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
232 xrayApplicationSignals experimental @grafana/aws-datasources false false true
233 multiTenantTempCredentials experimental @grafana/aws-datasources false false false
234 localizationForPlugins experimental @grafana/plugins-platform-backend false false false
235 unifiedNavbars GA @grafana/plugins-platform-backend false false true
236 logsPanelControls privatePreview @grafana/observability-logs false false true
237 metricsFromProfiles experimental @grafana/observability-traces-and-profiling false false true

@ -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"

@ -3069,6 +3069,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",

@ -55,10 +55,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';

@ -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,
}),

@ -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;

@ -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';

@ -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 />
</>
);
}

@ -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({

@ -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',

@ -30,6 +30,9 @@ const getStyles = (theme: GrafanaTheme2) => {
height: 24,
flexShrink: 0,
flexGrow: 0,
[theme.breakpoints.down('sm')]: {
display: 'none',
},
}),
};
};

@ -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',
},
}),
});

@ -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;
}

@ -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',

@ -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),
}),
};

@ -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([

@ -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;
}

@ -1,5 +1,4 @@
import { NavModelItem } from '@grafana/data';
export const TOP_BAR_LEVEL_HEIGHT = 40;
export interface ToolbarUpdateProps {
pageNav?: NavModelItem;

@ -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);
}

@ -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>
</>
);
}
}

@ -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[]) {

@ -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,
}),
});

@ -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) }),
});

@ -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()}

@ -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`} />);
}

@ -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,

@ -5837,7 +5837,7 @@
"title": "Scenes"
},
"search": {
"placeholderCommandPalette": "Search or jump to..."
"placeholderCommandPalette": "Search..."
},
"search-dashboards": {
"title": "Search dashboards"

Loading…
Cancel
Save