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
This commit is contained in:
@@ -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
|
||||
|
||||
|
@@ -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",
|
||||
|
||||
+2
-1
@@ -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) {
|
||||
import { getChromeHeaderLevelHeight } from './useChromeHeaderHeight';
|
||||
|
||||
export interface Props {
|
||||
actions?: React.ReactNode;
|
||||
breadcrumbActions?: React.ReactNode;
|
||||
scopes?: ScopesContextValue | undefined;
|
||||
}
|
||||
|
||||
export function SingleTopBarActions({ actions, breadcrumbActions, scopes }: Props) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const scopes = useScopes();
|
||||
|
||||
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;
|
||||
|
||||
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) }),
|
||||
});
|
||||
|
||||
+4
-2
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user