From 24474dcb9cd0d1d899dafd110a07307a9b794074 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 9 Apr 2025 14:51:22 +0200 Subject: [PATCH] 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 --- .../feature-toggles/index.md | 1 + .../src/types/featureToggles.gen.ts | 5 + pkg/services/featuremgmt/registry.go | 8 ++ pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 + pkg/services/featuremgmt/toggles_gen.json | 17 +++ public/app/app.ts | 3 +- .../core/components/AppChrome/AppChrome.tsx | 24 ++-- .../components/AppChrome/AppChromeService.tsx | 20 +--- .../components/AppChrome/AppChromeUpdate.tsx | 29 +++-- .../ExtensionSidebar/ExtensionToolbarItem.tsx | 50 +++++---- .../AppChrome/MegaMenu/MegaMenu.tsx | 4 +- .../AppChrome/MegaMenu/MegaMenuHeader.tsx | 6 +- .../NavToolbar/NavToolbarSeparator.tsx | 3 + .../AppChrome/QuickAdd/QuickAdd.tsx | 37 ++----- .../AppChrome/TopBar/InviteUserButton.tsx | 7 +- .../AppChrome/TopBar/SingleTopBar.tsx | 104 +++++++++++------- .../AppChrome/TopBar/SingleTopBarActions.tsx | 45 ++++---- .../TopSearchBarCommandPaletteTrigger.tsx | 42 ++++--- .../AppChrome/TopBar/useChromeHeaderHeight.ts | 99 +++++++++++++++++ public/app/core/components/AppChrome/types.ts | 1 - public/app/core/context/GrafanaContext.ts | 11 -- .../edit-pane/DashboardEditableElement.tsx | 30 ++--- .../scene/NavToolbarActions.tsx | 26 ++--- .../scene/new-toolbar/LeftActions.tsx | 19 +--- .../scene/new-toolbar/RightActions.tsx | 35 +++--- .../actions/EditSchemaV2Button.tsx | 6 +- .../scene/new-toolbar/utils.tsx | 2 +- .../features/explore/Logs/LogsNavigation.tsx | 5 +- public/locales/en-US/grafana.json | 2 +- 30 files changed, 388 insertions(+), 258 deletions(-) create mode 100644 public/app/core/components/AppChrome/TopBar/useChromeHeaderHeight.ts 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 2ebac404107..44b77618cc9 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 4025e3944bf..ef5effc6574 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 5e629d59044..aef765565c3 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 fe37e8f3aea..1d2ac3d8262 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -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 diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index cd5b75a9d01..88a878fd9d7 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 69790dee522..86ff71f2fed 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -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", diff --git a/public/app/app.ts b/public/app/app.ts index c21cfabd4c8..ef7460001e7 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -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'; diff --git a/public/app/core/components/AppChrome/AppChrome.tsx b/public/app/core/components/AppChrome/AppChrome.tsx index d47465b7508..4cd95dd8d4e 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) && {state.actions}} )} @@ -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 f93f33c87b6..f3de83d38a5 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 8daf42dba22..692b61e2fe0 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(({ actions }: AppChromeUpdateProps) => { - const { chrome } = useGrafana(); +export const AppChromeUpdate = React.memo( + ({ 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 c56eadad7a7..d7af125d894 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 ( - { - if (isOpen) { - setDockedComponentId(undefined); - } else { - setDockedComponentId(getComponentIdFromComponentMeta(components[0].pluginId, components[0])); - } - }} - /> + <> + { + if (isOpen) { + setDockedComponentId(undefined); + } else { + setDockedComponentId(getComponentIdFromComponentMeta(components[0].pluginId, components[0])); + } + }} + /> + + ); } @@ -62,13 +67,16 @@ export function ExtensionToolbarItem() { ); return ( - - - + <> + + + + + ); } diff --git a/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx b/public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx index d7045bb5960..3dedb1ea0d5 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 92062891de7..91f476bdef1 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 d2bb32e22cd..b9c65fc8c70 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 f1fa0a35902..a7e5e6ac5fe 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) => { - + ) : 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 a788f798fc6..452e899ca47 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) ? ( - - +
- {`${modKey}+k`}
@@ -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 00000000000..fa3555741b6 --- /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 1882e24d9a8..6cf72c936f5 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 8ce1dcacd0e..1f3bf86372b 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 431dfed0d1b..a778dd364f9 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 ( - + <> + + + ); } } diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx index 003ea1b438b..e33e6acd112 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(({ dashboard }) => { const hasNewToolbar = config.featureToggles.dashboardNewLayouts && config.featureToggles.newDashboardSharingComponent; - const actions = hasNewToolbar ? ( - + return hasNewToolbar ? ( + } + actions={} + /> ) : ( - + } /> ); - return ; }); 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 ( - - {leftActionsElements.length > 0 && {leftActionsElements}} - {rightActionsElements} - - ); + return {renderActionElements(toolbarActions)}; } 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 5c083673521..ec37d446145 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 ( - - {elements} - - ); + return {elements}; }; - -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 b809a9fb90a..428906c1ab0 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 ( {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 fc18a235bdc..3b0f729f711 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) => ( - } 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 936faf0e054..aaa5fcbf6a2 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(); } diff --git a/public/app/features/explore/Logs/LogsNavigation.tsx b/public/app/features/explore/Logs/LogsNavigation.tsx index aae833cffc6..64a306c8192 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 f3e36bbbf61..468ef976a60 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -5837,7 +5837,7 @@ "title": "Scenes" }, "search": { - "placeholderCommandPalette": "Search or jump to..." + "placeholderCommandPalette": "Search..." }, "search-dashboards": { "title": "Search dashboards"