SingleTopNav: Add `toolbar` to `Page` and replace usage of `AppChromeUpdate` (#94022)

* add page-level toolbar for actions

* handle explore

* fix panel edit sizing

* remove comments

* remove TOGGLE_BUTTON_ID

* undo alerting changes

* use fixed position header

* feature toggle Page changes

* add page context for alerting use cases

* simplify

* prettier...
pull/94188/head^2
Ashley Harrison 9 months ago committed by GitHub
parent e48d166c3e
commit dd7f45011d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 3
      public/app/core/components/AppChrome/AppChromeUpdate.tsx
  2. 52
      public/app/core/components/Page/Page.tsx
  3. 47
      public/app/core/components/Page/PageToolbarActions.tsx
  4. 2
      public/app/core/components/Page/types.ts
  5. 3
      public/app/features/alerting/unified/CloneRuleEditor.test.tsx
  6. 14
      public/app/features/alerting/unified/components/receivers/TemplateForm.tsx
  7. 42
      public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx
  8. 24
      public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx
  9. 21
      public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx
  10. 24
      public/app/features/dashboard-scene/settings/AnnotationsEditView.tsx
  11. 23
      public/app/features/dashboard-scene/settings/DashboardLinksEditView.tsx
  12. 12
      public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx
  13. 13
      public/app/features/dashboard-scene/settings/JsonModelEditView.tsx
  14. 13
      public/app/features/dashboard-scene/settings/PermissionsEditView.tsx
  15. 23
      public/app/features/dashboard-scene/settings/VariablesEditView.tsx
  16. 13
      public/app/features/dashboard-scene/settings/VersionsEditView.tsx
  17. 6
      public/app/features/explore/ExploreToolbar.tsx

@ -7,8 +7,7 @@ export interface AppChromeUpdateProps {
actions?: React.ReactNode; actions?: React.ReactNode;
} }
/** /**
* This needs to be moved to @grafana/ui or runtime. * @deprecated This component is deprecated and will be removed in a future release.
* This is the way core pages and plugins update the breadcrumbs and page toolbar actions
*/ */
export const AppChromeUpdate = React.memo<AppChromeUpdateProps>(({ actions }: AppChromeUpdateProps) => { export const AppChromeUpdate = React.memo<AppChromeUpdateProps>(({ actions }: AppChromeUpdateProps) => {
const { chrome } = useGrafana(); const { chrome } = useGrafana();

@ -1,19 +1,58 @@
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { useLayoutEffect } from 'react'; import {
createContext,
Dispatch,
ReactNode,
SetStateAction,
useContext,
useEffect,
useLayoutEffect,
useState,
} from 'react';
import { GrafanaTheme2, PageLayoutType } from '@grafana/data'; import { GrafanaTheme2, PageLayoutType } from '@grafana/data';
import { config } from '@grafana/runtime';
import { useStyles2 } from '@grafana/ui'; import { useStyles2 } from '@grafana/ui';
import { useGrafana } from 'app/core/context/GrafanaContext'; import { useGrafana } from 'app/core/context/GrafanaContext';
import { TOP_BAR_LEVEL_HEIGHT } from '../AppChrome/types';
import NativeScrollbar from '../NativeScrollbar'; import NativeScrollbar from '../NativeScrollbar';
import { PageContents } from './PageContents'; import { PageContents } from './PageContents';
import { PageHeader } from './PageHeader'; import { PageHeader } from './PageHeader';
import { PageTabs } from './PageTabs'; import { PageTabs } from './PageTabs';
import { PageToolbarActions } from './PageToolbarActions';
import { PageType } from './types'; import { PageType } from './types';
import { usePageNav } from './usePageNav'; import { usePageNav } from './usePageNav';
import { usePageTitle } from './usePageTitle'; import { usePageTitle } from './usePageTitle';
export interface PageContextType {
setToolbar: Dispatch<SetStateAction<ReactNode>>;
}
export const PageContext = createContext<PageContextType | undefined>(undefined);
function usePageContext(): PageContextType {
const context = useContext(PageContext);
if (!context) {
throw new Error('No PageContext found');
}
return context;
}
/**
* Hook to dynamically set the toolbar of a Page from a child component.
* Prefer setting the toolbar directly as a prop to Page.
* @param toolbar a ReactNode that will be rendered in a second toolbar
*/
export function usePageToolbar(toolbar?: ReactNode) {
const { setToolbar } = usePageContext();
useEffect(() => {
setToolbar(toolbar);
return () => setToolbar(undefined);
}, [setToolbar, toolbar]);
}
export const Page: PageType = ({ export const Page: PageType = ({
navId, navId,
navModel: oldNavProp, navModel: oldNavProp,
@ -24,12 +63,15 @@ export const Page: PageType = ({
subTitle, subTitle,
children, children,
className, className,
toolbar: toolbarProp,
info, info,
layout = PageLayoutType.Standard, layout = PageLayoutType.Standard,
onSetScrollRef, onSetScrollRef,
...otherProps ...otherProps
}) => { }) => {
const styles = useStyles2(getStyles); const isSingleTopNav = config.featureToggles.singleTopNav;
const [toolbar, setToolbar] = useState(toolbarProp);
const styles = useStyles2(getStyles, Boolean(isSingleTopNav && toolbar));
const navModel = usePageNav(navId, oldNavProp); const navModel = usePageNav(navId, oldNavProp);
const { chrome } = useGrafana(); const { chrome } = useGrafana();
@ -50,7 +92,9 @@ export const Page: PageType = ({
}, [navModel, pageNav, chrome, layout]); }, [navModel, pageNav, chrome, layout]);
return ( return (
<PageContext.Provider value={{ setToolbar }}>
<div className={cx(styles.wrapper, className)} {...otherProps}> <div className={cx(styles.wrapper, className)} {...otherProps}>
{isSingleTopNav && toolbar && <PageToolbarActions>{toolbar}</PageToolbarActions>}
{layout === PageLayoutType.Standard && ( {layout === PageLayoutType.Standard && (
<NativeScrollbar <NativeScrollbar
// This id is used by the image renderer to scroll through the dashboard // This id is used by the image renderer to scroll through the dashboard
@ -86,18 +130,20 @@ export const Page: PageType = ({
{layout === PageLayoutType.Custom && children} {layout === PageLayoutType.Custom && children}
</div> </div>
</PageContext.Provider>
); );
}; };
Page.Contents = PageContents; Page.Contents = PageContents;
const getStyles = (theme: GrafanaTheme2) => { const getStyles = (theme: GrafanaTheme2, hasToolbar: boolean) => {
return { return {
wrapper: css({ wrapper: css({
label: 'page-wrapper', label: 'page-wrapper',
display: 'flex', display: 'flex',
flex: '1 1 0', flex: '1 1 0',
flexDirection: 'column', flexDirection: 'column',
marginTop: hasToolbar ? TOP_BAR_LEVEL_HEIGHT : 0,
position: 'relative', position: 'relative',
}), }),
pageContent: css({ pageContent: css({

@ -0,0 +1,47 @@
import { css } from '@emotion/css';
import { PropsWithChildren } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Components } from '@grafana/e2e-selectors';
import { useChromeHeaderHeight } from '@grafana/runtime';
import { Stack, useStyles2 } from '@grafana/ui';
import { useGrafana } from 'app/core/context/GrafanaContext';
import { MENU_WIDTH } from '../AppChrome/MegaMenu/MegaMenu';
import { TOP_BAR_LEVEL_HEIGHT } from '../AppChrome/types';
export interface Props {}
export function PageToolbarActions({ children }: PropsWithChildren<Props>) {
const chromeHeaderHeight = useChromeHeaderHeight();
const { chrome } = useGrafana();
const state = chrome.useState();
const menuDockedAndOpen = !state.chromeless && state.megaMenuDocked && state.megaMenuOpen;
const styles = useStyles2(getStyles, chromeHeaderHeight ?? 0, menuDockedAndOpen);
return (
<div data-testid={Components.NavToolbar.container} className={styles.pageToolbar}>
<Stack alignItems="center" justifyContent="flex-end" flex={1} wrap="nowrap" minWidth={0}>
{children}
</Stack>
</div>
);
}
const getStyles = (theme: GrafanaTheme2, chromeHeaderHeight: number, menuDockedAndOpen: boolean) => {
return {
pageToolbar: css({
alignItems: 'center',
backgroundColor: theme.colors.background.primary,
borderBottom: `1px solid ${theme.colors.border.weak}`,
display: 'flex',
height: TOP_BAR_LEVEL_HEIGHT,
left: menuDockedAndOpen ? MENU_WIDTH : 0,
padding: theme.spacing(0, 1, 0, 2),
position: 'fixed',
top: chromeHeaderHeight,
right: 0,
zIndex: theme.zIndex.navbarFixed,
}),
};
};

@ -25,6 +25,8 @@ export interface PageProps extends HTMLAttributes<HTMLDivElement> {
layout?: PageLayoutType; layout?: PageLayoutType;
/** Can be used to get the scroll container element to access scroll position */ /** Can be used to get the scroll container element to access scroll position */
onSetScrollRef?: (ref: ScrollRefElement) => void; onSetScrollRef?: (ref: ScrollRefElement) => void;
/** Set a page-level toolbar */
toolbar?: React.ReactNode;
} }
export interface PageInfoItem { export interface PageInfoItem {

@ -5,6 +5,7 @@ import { byRole, byTestId, byText } from 'testing-library-selector';
import { selectors } from '@grafana/e2e-selectors/src'; import { selectors } from '@grafana/e2e-selectors/src';
import { setDataSourceSrv } from '@grafana/runtime'; import { setDataSourceSrv } from '@grafana/runtime';
import { PageContext } from 'app/core/components/Page/Page';
import { DashboardSearchItem, DashboardSearchItemType } from 'app/features/search/types'; import { DashboardSearchItem, DashboardSearchItemType } from 'app/features/search/types';
import { RuleWithLocation } from 'app/types/unified-alerting'; import { RuleWithLocation } from 'app/types/unified-alerting';
@ -72,7 +73,9 @@ function Wrapper({ children }: React.PropsWithChildren<{}>) {
const formApi = useForm<RuleFormValues>({ defaultValues: getDefaultFormValues() }); const formApi = useForm<RuleFormValues>({ defaultValues: getDefaultFormValues() });
return ( return (
<Providers> <Providers>
<PageContext.Provider value={{ setToolbar: jest.fn() }}>
<FormProvider {...formApi}>{children}</FormProvider> <FormProvider {...formApi}>{children}</FormProvider>
</PageContext.Provider>
</Providers> </Providers>
); );
} }

@ -1,13 +1,13 @@
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { addMinutes, subDays, subHours } from 'date-fns'; import { addMinutes, subDays, subHours } from 'date-fns';
import { Location } from 'history'; import { Location } from 'history';
import { useRef, useState } from 'react'; import { useMemo, useRef, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { useToggle } from 'react-use'; import { useToggle } from 'react-use';
import AutoSizer from 'react-virtualized-auto-sizer'; import AutoSizer from 'react-virtualized-auto-sizer';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { isFetchError, locationService } from '@grafana/runtime'; import { config as runtimeConfig, isFetchError, locationService } from '@grafana/runtime';
import { import {
Alert, Alert,
Button, Button,
@ -21,6 +21,7 @@ import {
InlineField, InlineField,
Box, Box,
} from '@grafana/ui'; } from '@grafana/ui';
import { usePageToolbar } from 'app/core/components/Page/Page';
import { useAppNotification } from 'app/core/copy/appNotification'; import { useAppNotification } from 'app/core/copy/appNotification';
import { useCleanup } from 'app/core/hooks/useCleanup'; import { useCleanup } from 'app/core/hooks/useCleanup';
import { ActiveTab as ContactPointsActiveTabs } from 'app/features/alerting/unified/components/contact-points/ContactPoints'; import { ActiveTab as ContactPointsActiveTabs } from 'app/features/alerting/unified/components/contact-points/ContactPoints';
@ -157,7 +158,8 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props)
} }
}; };
const actionButtons = ( const actionButtons = useMemo(
() => (
<Stack> <Stack>
<Button onClick={() => formRef.current?.requestSubmit()} variant="primary" size="sm" disabled={isSubmitting}> <Button onClick={() => formRef.current?.requestSubmit()} variant="primary" size="sm" disabled={isSubmitting}>
Save Save
@ -173,12 +175,16 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props)
Cancel Cancel
</LinkButton> </LinkButton>
</Stack> </Stack>
),
[alertmanager, isSubmitting]
); );
usePageToolbar(actionButtons);
return ( return (
<> <>
<FormProvider {...formApi}> <FormProvider {...formApi}>
<AppChromeUpdate actions={actionButtons} /> {!runtimeConfig.featureToggles.singleTopNav && <AppChromeUpdate actions={actionButtons} />}
<form onSubmit={handleSubmit(submit)} ref={formRef} className={styles.form} aria-label="Template form"> <form onSubmit={handleSubmit(submit)} ref={formRef} className={styles.form} aria-label="Template form">
{/* error message */} {/* error message */}
{error && ( {error && (

@ -1,5 +1,5 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { FormProvider, SubmitErrorHandler, UseFormWatch, useForm } from 'react-hook-form'; import { FormProvider, SubmitErrorHandler, UseFormWatch, useForm } from 'react-hook-form';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
@ -7,6 +7,7 @@ import { GrafanaTheme2 } from '@grafana/data';
import { config, locationService } from '@grafana/runtime'; import { config, locationService } from '@grafana/runtime';
import { Button, ConfirmModal, CustomScrollbar, Spinner, Stack, useStyles2 } from '@grafana/ui'; import { Button, ConfirmModal, CustomScrollbar, Spinner, Stack, useStyles2 } from '@grafana/ui';
import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate'; import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate';
import { usePageToolbar } from 'app/core/components/Page/Page';
import { useAppNotification } from 'app/core/copy/appNotification'; import { useAppNotification } from 'app/core/copy/appNotification';
import { contextSrv } from 'app/core/core'; import { contextSrv } from 'app/core/core';
import InfoPausedRule from 'app/features/alerting/unified/components/InfoPausedRule'; import InfoPausedRule from 'app/features/alerting/unified/components/InfoPausedRule';
@ -133,7 +134,8 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => {
}; };
// @todo why is error not propagated to form? // @todo why is error not propagated to form?
const submit = async (values: RuleFormValues, exitOnSave: boolean) => { const submit = useCallback(
async (values: RuleFormValues, exitOnSave: boolean) => {
if (conditionErrorMsg !== '') { if (conditionErrorMsg !== '') {
notifyApp.error(conditionErrorMsg); notifyApp.error(conditionErrorMsg);
return; return;
@ -141,7 +143,9 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => {
trackAlertRuleFormSaved({ formAction: existing ? 'update' : 'create', ruleType: values.type }); trackAlertRuleFormSaved({ formAction: existing ? 'update' : 'create', ruleType: values.type });
const ruleDefinition = grafanaTypeRule ? formValuesToRulerGrafanaRuleDTO(values) : formValuesToRulerRuleDTO(values); const ruleDefinition = grafanaTypeRule
? formValuesToRulerGrafanaRuleDTO(values)
: formValuesToRulerRuleDTO(values);
const ruleGroupIdentifier = existing const ruleGroupIdentifier = existing
? getRuleGroupLocationFromRuleWithLocation(existing) ? getRuleGroupLocationFromRuleWithLocation(existing)
@ -178,7 +182,18 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => {
const updatedRuleIdentifier = fromRulerRule(dataSourceName, namespaceName, groupName, ruleDefinition); const updatedRuleIdentifier = fromRulerRule(dataSourceName, namespaceName, groupName, ruleDefinition);
locationService.replace(`/alerting/${encodeURIComponent(stringifyIdentifier(updatedRuleIdentifier))}/edit`); locationService.replace(`/alerting/${encodeURIComponent(stringifyIdentifier(updatedRuleIdentifier))}/edit`);
} }
}; },
[
addRuleToRuleGroup,
conditionErrorMsg,
evaluateEvery,
existing,
grafanaTypeRule,
notifyApp,
queryParams,
updateRuleInRuleGroup,
]
);
const deleteRule = async () => { const deleteRule = async () => {
if (existing) { if (existing) {
@ -191,7 +206,8 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => {
} }
}; };
const onInvalid: SubmitErrorHandler<RuleFormValues> = (errors): void => { const onInvalid: SubmitErrorHandler<RuleFormValues> = useCallback(
(errors): void => {
trackAlertRuleFormError({ trackAlertRuleFormError({
grafana_version: config.buildInfo.version, grafana_version: config.buildInfo.version,
org_id: contextSrv.user.orgId, org_id: contextSrv.user.orgId,
@ -200,18 +216,21 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => {
formAction: existing ? 'update' : 'create', formAction: existing ? 'update' : 'create',
}); });
notifyApp.error('There are errors in the form. Please correct them and try again!'); notifyApp.error('There are errors in the form. Please correct them and try again!');
}; },
[existing, notifyApp]
);
const cancelRuleCreation = () => { const cancelRuleCreation = useCallback(() => {
logInfo(LogMessages.cancelSavingAlertRule); logInfo(LogMessages.cancelSavingAlertRule);
trackAlertRuleFormCancelled({ formAction: existing ? 'update' : 'create' }); trackAlertRuleFormCancelled({ formAction: existing ? 'update' : 'create' });
locationService.getHistory().goBack(); locationService.getHistory().goBack();
}; }, [existing]);
const evaluateEveryInForm = watch('evaluateEvery'); const evaluateEveryInForm = watch('evaluateEvery');
useEffect(() => setEvaluateEvery(evaluateEveryInForm), [evaluateEveryInForm]); useEffect(() => setEvaluateEvery(evaluateEveryInForm), [evaluateEveryInForm]);
const actionButtons = ( const actionButtons = useMemo(
() => (
<Stack justifyContent="flex-end" alignItems="center"> <Stack justifyContent="flex-end" alignItems="center">
{existing && ( {existing && (
<Button <Button
@ -255,7 +274,10 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => {
</Button> </Button>
)} )}
</Stack> </Stack>
),
[cancelRuleCreation, existing, handleSubmit, isSubmitting, onInvalid, styles.buttonSpinner, submit, watch]
); );
usePageToolbar(actionButtons);
const isPaused = existing && isGrafanaRulerRule(existing.rule) && isGrafanaRulerRulePaused(existing.rule); const isPaused = existing && isGrafanaRulerRule(existing.rule) && isGrafanaRulerRulePaused(existing.rule);
if (!type) { if (!type) {
@ -263,7 +285,7 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => {
} }
return ( return (
<FormProvider {...formAPI}> <FormProvider {...formAPI}>
<AppChromeUpdate actions={actionButtons} /> {!config.featureToggles.singleTopNav && <AppChromeUpdate actions={actionButtons} />}
<form onSubmit={(e) => e.preventDefault()} className={styles.form}> <form onSubmit={(e) => e.preventDefault()} className={styles.form}>
<div className={styles.contentOuter}> <div className={styles.contentOuter}>
{isPaused && <InfoPausedRule />} {isPaused && <InfoPausedRule />}

@ -2,7 +2,9 @@ import { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form'; import { FormProvider, useForm } from 'react-hook-form';
import { useAsync } from 'react-use'; import { useAsync } from 'react-use';
import { config } from '@grafana/runtime';
import { Button, CustomScrollbar, LinkButton, LoadingPlaceholder, Stack } from '@grafana/ui'; import { Button, CustomScrollbar, LinkButton, LoadingPlaceholder, Stack } from '@grafana/ui';
import { usePageToolbar } from 'app/core/components/Page/Page';
import { useAppNotification } from 'app/core/copy/appNotification'; import { useAppNotification } from 'app/core/copy/appNotification';
import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { useQueryParams } from 'app/core/hooks/useQueryParams';
@ -50,39 +52,47 @@ export function ModifyExportRuleForm({ ruleForm, alertUid }: ModifyExportRuleFor
const [conditionErrorMsg, setConditionErrorMsg] = useState(''); const [conditionErrorMsg, setConditionErrorMsg] = useState('');
const [evaluateEvery, setEvaluateEvery] = useState(ruleForm?.evaluateEvery ?? DEFAULT_GROUP_EVALUATION_INTERVAL); const [evaluateEvery, setEvaluateEvery] = useState(ruleForm?.evaluateEvery ?? DEFAULT_GROUP_EVALUATION_INTERVAL);
const onInvalid = (): void => { const onInvalid = useCallback((): void => {
notifyApp.error('There are errors in the form. Please correct them and try again!'); notifyApp.error('There are errors in the form. Please correct them and try again!');
}; }, [notifyApp]);
const checkAlertCondition = (msg = '') => { const checkAlertCondition = (msg = '') => {
setConditionErrorMsg(msg); setConditionErrorMsg(msg);
}; };
const submit = (exportData: RuleFormValues | undefined) => { const submit = useCallback(
(exportData: RuleFormValues | undefined) => {
if (conditionErrorMsg !== '') { if (conditionErrorMsg !== '') {
notifyApp.error(conditionErrorMsg); notifyApp.error(conditionErrorMsg);
return; return;
} }
setExportData(exportData); setExportData(exportData);
}; },
[conditionErrorMsg, notifyApp]
);
const onClose = useCallback(() => { const onClose = useCallback(() => {
setExportData(undefined); setExportData(undefined);
}, [setExportData]); }, [setExportData]);
const actionButtons = [ const actionButtons = useMemo(
() => [
<LinkButton href={returnTo} key="cancel" size="sm" variant="secondary" onClick={() => submit(undefined)}> <LinkButton href={returnTo} key="cancel" size="sm" variant="secondary" onClick={() => submit(undefined)}>
Cancel Cancel
</LinkButton>, </LinkButton>,
<Button key="export-rule" size="sm" onClick={formAPI.handleSubmit((formValues) => submit(formValues), onInvalid)}> <Button key="export-rule" size="sm" onClick={formAPI.handleSubmit((formValues) => submit(formValues), onInvalid)}>
Export Export
</Button>, </Button>,
]; ],
[formAPI, onInvalid, returnTo, submit]
);
usePageToolbar(actionButtons);
return ( return (
<> <>
<FormProvider {...formAPI}> <FormProvider {...formAPI}>
<AppChromeUpdate actions={actionButtons} /> {!config.featureToggles.singleTopNav && <AppChromeUpdate actions={actionButtons} />}
<form onSubmit={(e) => e.preventDefault()}> <form onSubmit={(e) => e.preventDefault()}>
<div> <div>
<CustomScrollbar autoHeightMin="100%" hideHorizontalTrack={true}> <CustomScrollbar autoHeightMin="100%" hideHorizontalTrack={true}>

@ -3,9 +3,10 @@ import { useEffect, useMemo } from 'react';
import { useLocation } from 'react-router-dom-v5-compat'; import { useLocation } from 'react-router-dom-v5-compat';
import { GrafanaTheme2, PageLayoutType } from '@grafana/data'; import { GrafanaTheme2, PageLayoutType } from '@grafana/data';
import { useChromeHeaderHeight } from '@grafana/runtime'; import { config, useChromeHeaderHeight } from '@grafana/runtime';
import { SceneComponentProps } from '@grafana/scenes'; import { SceneComponentProps } from '@grafana/scenes';
import { useStyles2 } from '@grafana/ui'; import { useStyles2 } from '@grafana/ui';
import { TOP_BAR_LEVEL_HEIGHT } from 'app/core/components/AppChrome/types';
import NativeScrollbar from 'app/core/components/NativeScrollbar'; import NativeScrollbar from 'app/core/components/NativeScrollbar';
import { Page } from 'app/core/components/Page/Page'; import { Page } from 'app/core/components/Page/Page';
import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound'; import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound';
@ -14,7 +15,7 @@ import DashboardEmpty from 'app/features/dashboard/dashgrid/DashboardEmpty';
import { useSelector } from 'app/types'; import { useSelector } from 'app/types';
import { DashboardScene } from './DashboardScene'; import { DashboardScene } from './DashboardScene';
import { NavToolbarActions } from './NavToolbarActions'; import { NavToolbarActions, ToolbarActions } from './NavToolbarActions';
import { PanelSearchLayout } from './PanelSearchLayout'; import { PanelSearchLayout } from './PanelSearchLayout';
import { DashboardAngularDeprecationBanner } from './angular/DashboardAngularDeprecationBanner'; import { DashboardAngularDeprecationBanner } from './angular/DashboardAngularDeprecationBanner';
@ -22,7 +23,7 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardS
const { controls, overlay, editview, editPanel, isEmpty, meta, viewPanelScene, panelSearch, panelsPerRow } = const { controls, overlay, editview, editPanel, isEmpty, meta, viewPanelScene, panelSearch, panelsPerRow } =
model.useState(); model.useState();
const headerHeight = useChromeHeaderHeight(); const headerHeight = useChromeHeaderHeight();
const styles = useStyles2(getStyles, headerHeight); const styles = useStyles2(getStyles, headerHeight ?? 0);
const location = useLocation(); const location = useLocation();
const navIndex = useSelector((state) => state.navIndex); const navIndex = useSelector((state) => state.navIndex);
const pageNav = model.getPageNav(location, navIndex); const pageNav = model.getPageNav(location, navIndex);
@ -30,6 +31,7 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardS
const navModel = getNavModel(navIndex, 'dashboards/browse'); const navModel = getNavModel(navIndex, 'dashboards/browse');
const hasControls = controls?.hasControls(); const hasControls = controls?.hasControls();
const isSettingsOpen = editview !== undefined; const isSettingsOpen = editview !== undefined;
const isSingleTopNav = config.featureToggles.singleTopNav;
// Remember scroll pos when going into view panel, edit panel or settings // Remember scroll pos when going into view panel, edit panel or settings
useMemo(() => { useMemo(() => {
@ -79,12 +81,17 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardS
} }
return ( return (
<Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Custom}> <Page
navModel={navModel}
pageNav={pageNav}
layout={PageLayoutType.Custom}
toolbar={isSingleTopNav ? <ToolbarActions dashboard={model} /> : undefined}
>
{editPanel && <editPanel.Component model={editPanel} />} {editPanel && <editPanel.Component model={editPanel} />}
{!editPanel && ( {!editPanel && (
<NativeScrollbar divId="page-scrollbar" onSetScrollRef={model.onSetScrollRef}> <NativeScrollbar divId="page-scrollbar" onSetScrollRef={model.onSetScrollRef}>
<div className={cx(styles.pageContainer, hasControls && styles.pageContainerWithControls)}> <div className={cx(styles.pageContainer, hasControls && styles.pageContainerWithControls)}>
<NavToolbarActions dashboard={model} /> {!isSingleTopNav && <NavToolbarActions dashboard={model} />}
{controls && ( {controls && (
<div className={styles.controlsWrapper}> <div className={styles.controlsWrapper}>
<controls.Component model={controls} /> <controls.Component model={controls} />
@ -99,7 +106,7 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardS
); );
} }
function getStyles(theme: GrafanaTheme2, headerHeight: number | undefined) { function getStyles(theme: GrafanaTheme2, headerHeight: number) {
return { return {
pageContainer: css({ pageContainer: css({
display: 'grid', display: 'grid',
@ -133,7 +140,7 @@ function getStyles(theme: GrafanaTheme2, headerHeight: number | undefined) {
position: 'sticky', position: 'sticky',
zIndex: theme.zIndex.activePanel, zIndex: theme.zIndex.activePanel,
background: theme.colors.background.canvas, background: theme.colors.background.canvas,
top: headerHeight, top: config.featureToggles.singleTopNav ? headerHeight + TOP_BAR_LEVEL_HEIGHT : headerHeight,
}, },
}), }),
canvasContent: css({ canvasContent: css({

@ -1,11 +1,11 @@
import { AnnotationQuery, getDataSourceRef, NavModel, NavModelItem, PageLayoutType } from '@grafana/data'; import { AnnotationQuery, getDataSourceRef, NavModel, NavModelItem, PageLayoutType } from '@grafana/data';
import { getDataSourceSrv } from '@grafana/runtime'; import { config, getDataSourceSrv } from '@grafana/runtime';
import { SceneComponentProps, SceneObjectBase, VizPanel, dataLayers } from '@grafana/scenes'; import { SceneComponentProps, SceneObjectBase, VizPanel, dataLayers } from '@grafana/scenes';
import { Page } from 'app/core/components/Page/Page'; import { Page } from 'app/core/components/Page/Page';
import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer'; import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer';
import { DashboardScene } from '../scene/DashboardScene'; import { DashboardScene } from '../scene/DashboardScene';
import { NavToolbarActions } from '../scene/NavToolbarActions'; import { NavToolbarActions, ToolbarActions } from '../scene/NavToolbarActions';
import { dataLayersToAnnotations } from '../serialization/dataLayersToAnnotations'; import { dataLayersToAnnotations } from '../serialization/dataLayersToAnnotations';
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
import { getDashboardSceneFor } from '../utils/utils'; import { getDashboardSceneFor } from '../utils/utils';
@ -133,6 +133,7 @@ function AnnotationsSettingsView({ model }: SceneComponentProps<AnnotationsEditV
const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey()); const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey());
const { editIndex } = model.useState(); const { editIndex } = model.useState();
const panels = dashboardSceneGraph.getVizPanels(dashboard); const panels = dashboardSceneGraph.getVizPanels(dashboard);
const isSingleTopNav = config.featureToggles.singleTopNav;
const annotations: AnnotationQuery[] = dataLayersToAnnotations(annotationLayers); const annotations: AnnotationQuery[] = dataLayersToAnnotations(annotationLayers);
@ -153,8 +154,13 @@ function AnnotationsSettingsView({ model }: SceneComponentProps<AnnotationsEditV
} }
return ( return (
<Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Standard}> <Page
<NavToolbarActions dashboard={dashboard} /> navModel={navModel}
pageNav={pageNav}
layout={PageLayoutType.Standard}
toolbar={isSingleTopNav ? <ToolbarActions dashboard={dashboard} /> : undefined}
>
{!isSingleTopNav && <NavToolbarActions dashboard={dashboard} />}
<AnnotationSettingsList <AnnotationSettingsList
annotations={annotations} annotations={annotations}
onNew={model.onNew} onNew={model.onNew}
@ -190,6 +196,7 @@ function AnnotationsSettingsEditView({
onDelete, onDelete,
}: AnnotationsSettingsEditViewProps) { }: AnnotationsSettingsEditViewProps) {
const { name, query } = annotationLayer.useState(); const { name, query } = annotationLayer.useState();
const isSingleTopNav = config.featureToggles.singleTopNav;
const editAnnotationPageNav = { const editAnnotationPageNav = {
text: name, text: name,
@ -197,8 +204,13 @@ function AnnotationsSettingsEditView({
}; };
return ( return (
<Page navModel={navModel} pageNav={editAnnotationPageNav} layout={PageLayoutType.Standard}> <Page
<NavToolbarActions dashboard={dashboard} /> navModel={navModel}
pageNav={editAnnotationPageNav}
layout={PageLayoutType.Standard}
toolbar={isSingleTopNav ? <ToolbarActions dashboard={dashboard} /> : undefined}
>
{!isSingleTopNav && <NavToolbarActions dashboard={dashboard} />}
<AnnotationSettingsEdit <AnnotationSettingsEdit
annotation={query} annotation={query}
editIndex={editIndex} editIndex={editIndex}

@ -1,10 +1,11 @@
import { NavModel, NavModelItem, PageLayoutType, arrayUtils } from '@grafana/data'; import { NavModel, NavModelItem, PageLayoutType, arrayUtils } from '@grafana/data';
import { config } from '@grafana/runtime';
import { SceneComponentProps, SceneObjectBase } from '@grafana/scenes'; import { SceneComponentProps, SceneObjectBase } from '@grafana/scenes';
import { DashboardLink } from '@grafana/schema'; import { DashboardLink } from '@grafana/schema';
import { Page } from 'app/core/components/Page/Page'; import { Page } from 'app/core/components/Page/Page';
import { DashboardScene } from '../scene/DashboardScene'; import { DashboardScene } from '../scene/DashboardScene';
import { NavToolbarActions } from '../scene/NavToolbarActions'; import { NavToolbarActions, ToolbarActions } from '../scene/NavToolbarActions';
import { DashboardLinkForm } from '../settings/links/DashboardLinkForm'; import { DashboardLinkForm } from '../settings/links/DashboardLinkForm';
import { DashboardLinkList } from '../settings/links/DashboardLinkList'; import { DashboardLinkList } from '../settings/links/DashboardLinkList';
import { NEW_LINK } from '../settings/links/utils'; import { NEW_LINK } from '../settings/links/utils';
@ -80,6 +81,7 @@ function DashboardLinksEditViewRenderer({ model }: SceneComponentProps<Dashboard
const { links } = dashboard.useState(); const { links } = dashboard.useState();
const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey()); const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey());
const linkToEdit = editIndex !== undefined ? links[editIndex] : undefined; const linkToEdit = editIndex !== undefined ? links[editIndex] : undefined;
const isSingleTopNav = config.featureToggles.singleTopNav;
if (linkToEdit) { if (linkToEdit) {
return ( return (
@ -95,8 +97,13 @@ function DashboardLinksEditViewRenderer({ model }: SceneComponentProps<Dashboard
} }
return ( return (
<Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Standard}> <Page
<NavToolbarActions dashboard={dashboard} /> navModel={navModel}
pageNav={pageNav}
layout={PageLayoutType.Standard}
toolbar={isSingleTopNav ? <ToolbarActions dashboard={dashboard} /> : undefined}
>
{!isSingleTopNav && <NavToolbarActions dashboard={dashboard} />}
<DashboardLinkList <DashboardLinkList
links={links} links={links}
onNew={model.onNewLink} onNew={model.onNewLink}
@ -123,10 +130,16 @@ function EditLinkView({ pageNav, link, navModel, dashboard, onChange, onGoBack }
text: 'Edit link', text: 'Edit link',
parentItem: pageNav, parentItem: pageNav,
}; };
const isSingleTopNav = config.featureToggles.singleTopNav;
return ( return (
<Page navModel={navModel} pageNav={editLinkPageNav} layout={PageLayoutType.Standard}> <Page
<NavToolbarActions dashboard={dashboard} /> navModel={navModel}
pageNav={editLinkPageNav}
layout={PageLayoutType.Standard}
toolbar={isSingleTopNav ? <ToolbarActions dashboard={dashboard} /> : undefined}
>
{!isSingleTopNav && <NavToolbarActions dashboard={dashboard} />}
<DashboardLinkForm link={link!} onUpdate={onChange} onGoBack={onGoBack} /> <DashboardLinkForm link={link!} onUpdate={onChange} onGoBack={onGoBack} />
</Page> </Page>
); );

@ -25,7 +25,7 @@ import { GenAIDashTitleButton } from 'app/features/dashboard/components/GenAI/Ge
import { updateNavModel } from '../pages/utils'; import { updateNavModel } from '../pages/utils';
import { DashboardScene } from '../scene/DashboardScene'; import { DashboardScene } from '../scene/DashboardScene';
import { NavToolbarActions } from '../scene/NavToolbarActions'; import { NavToolbarActions, ToolbarActions } from '../scene/NavToolbarActions';
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
import { getDashboardSceneFor } from '../utils/utils'; import { getDashboardSceneFor } from '../utils/utils';
@ -177,10 +177,16 @@ export class GeneralSettingsEditView
const { intervals } = model.getRefreshPicker().useState(); const { intervals } = model.getRefreshPicker().useState();
const { hideTimeControls } = model.getDashboardControls().useState(); const { hideTimeControls } = model.getDashboardControls().useState();
const { enabled: liveNow } = model.getLiveNowTimer().useState(); const { enabled: liveNow } = model.getLiveNowTimer().useState();
const isSingleTopNav = config.featureToggles.singleTopNav;
return ( return (
<Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Standard}> <Page
<NavToolbarActions dashboard={dashboard} /> navModel={navModel}
pageNav={pageNav}
layout={PageLayoutType.Standard}
toolbar={isSingleTopNav ? <ToolbarActions dashboard={dashboard} /> : undefined}
>
{!isSingleTopNav && <NavToolbarActions dashboard={dashboard} />}
<div style={{ maxWidth: '600px' }}> <div style={{ maxWidth: '600px' }}>
<Box marginBottom={5}> <Box marginBottom={5}>
<Field <Field

@ -2,6 +2,7 @@ import { css } from '@emotion/css';
import { useState } from 'react'; import { useState } from 'react';
import { GrafanaTheme2, PageLayoutType } from '@grafana/data'; import { GrafanaTheme2, PageLayoutType } from '@grafana/data';
import { config } from '@grafana/runtime';
import { SceneComponentProps, SceneObjectBase, sceneUtils } from '@grafana/scenes'; import { SceneComponentProps, SceneObjectBase, sceneUtils } from '@grafana/scenes';
import { Dashboard } from '@grafana/schema'; import { Dashboard } from '@grafana/schema';
import { Alert, Box, Button, CodeEditor, Stack, useStyles2 } from '@grafana/ui'; import { Alert, Box, Button, CodeEditor, Stack, useStyles2 } from '@grafana/ui';
@ -18,7 +19,7 @@ import {
} from '../saving/shared'; } from '../saving/shared';
import { useSaveDashboard } from '../saving/useSaveDashboard'; import { useSaveDashboard } from '../saving/useSaveDashboard';
import { DashboardScene } from '../scene/DashboardScene'; import { DashboardScene } from '../scene/DashboardScene';
import { NavToolbarActions } from '../scene/NavToolbarActions'; import { NavToolbarActions, ToolbarActions } from '../scene/NavToolbarActions';
import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene'; import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
import { transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel'; import { transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel';
import { getDashboardSceneFor } from '../utils/utils'; import { getDashboardSceneFor } from '../utils/utils';
@ -88,6 +89,7 @@ export class JsonModelEditView extends SceneObjectBase<JsonModelEditViewState> i
const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey()); const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey());
const canSave = dashboard.useState().meta.canSave; const canSave = dashboard.useState().meta.canSave;
const { jsonText } = model.useState(); const { jsonText } = model.useState();
const isSingleTopNav = config.featureToggles.singleTopNav;
const onSave = async (overwrite: boolean) => { const onSave = async (overwrite: boolean) => {
const result = await onSaveDashboard(dashboard, JSON.parse(model.state.jsonText), { const result = await onSaveDashboard(dashboard, JSON.parse(model.state.jsonText), {
@ -174,8 +176,13 @@ export class JsonModelEditView extends SceneObjectBase<JsonModelEditViewState> i
); );
} }
return ( return (
<Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Standard}> <Page
<NavToolbarActions dashboard={dashboard} /> navModel={navModel}
pageNav={pageNav}
layout={PageLayoutType.Standard}
toolbar={isSingleTopNav ? <ToolbarActions dashboard={dashboard} /> : undefined}
>
{!isSingleTopNav && <NavToolbarActions dashboard={dashboard} />}
<div className={styles.wrapper}> <div className={styles.wrapper}>
<Trans i18nKey="dashboard-settings.json-editor.subtitle"> <Trans i18nKey="dashboard-settings.json-editor.subtitle">
The JSON model below is the data structure that defines the dashboard. This includes dashboard settings, The JSON model below is the data structure that defines the dashboard. This includes dashboard settings,

@ -1,4 +1,5 @@
import { PageLayoutType } from '@grafana/data'; import { PageLayoutType } from '@grafana/data';
import { config } from '@grafana/runtime';
import { SceneComponentProps, SceneObjectBase } from '@grafana/scenes'; import { SceneComponentProps, SceneObjectBase } from '@grafana/scenes';
import { Permissions } from 'app/core/components/AccessControl'; import { Permissions } from 'app/core/components/AccessControl';
import { Page } from 'app/core/components/Page/Page'; import { Page } from 'app/core/components/Page/Page';
@ -6,7 +7,7 @@ import { contextSrv } from 'app/core/core';
import { AccessControlAction } from 'app/types'; import { AccessControlAction } from 'app/types';
import { DashboardScene } from '../scene/DashboardScene'; import { DashboardScene } from '../scene/DashboardScene';
import { NavToolbarActions } from '../scene/NavToolbarActions'; import { NavToolbarActions, ToolbarActions } from '../scene/NavToolbarActions';
import { getDashboardSceneFor } from '../utils/utils'; import { getDashboardSceneFor } from '../utils/utils';
import { DashboardEditView, DashboardEditViewState, useDashboardEditPageNav } from './utils'; import { DashboardEditView, DashboardEditViewState, useDashboardEditPageNav } from './utils';
@ -34,10 +35,16 @@ function PermissionsEditorSettings({ model }: SceneComponentProps<PermissionsEdi
const { uid } = dashboard.useState(); const { uid } = dashboard.useState();
const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey()); const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey());
const canSetPermissions = contextSrv.hasPermission(AccessControlAction.DashboardsPermissionsWrite); const canSetPermissions = contextSrv.hasPermission(AccessControlAction.DashboardsPermissionsWrite);
const isSingleTopNav = config.featureToggles.singleTopNav;
return ( return (
<Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Standard}> <Page
<NavToolbarActions dashboard={dashboard} /> navModel={navModel}
pageNav={pageNav}
layout={PageLayoutType.Standard}
toolbar={isSingleTopNav ? <ToolbarActions dashboard={dashboard} /> : undefined}
>
{!isSingleTopNav && <NavToolbarActions dashboard={dashboard} />}
<Permissions resource={'dashboards'} resourceId={uid ?? ''} canSetPermissions={canSetPermissions} /> <Permissions resource={'dashboards'} resourceId={uid ?? ''} canSetPermissions={canSetPermissions} />
</Page> </Page>
); );

@ -1,9 +1,10 @@
import { NavModel, NavModelItem, PageLayoutType } from '@grafana/data'; import { NavModel, NavModelItem, PageLayoutType } from '@grafana/data';
import { config } from '@grafana/runtime';
import { SceneComponentProps, SceneObjectBase, SceneVariable, SceneVariables, sceneGraph } from '@grafana/scenes'; import { SceneComponentProps, SceneObjectBase, SceneVariable, SceneVariables, sceneGraph } from '@grafana/scenes';
import { Page } from 'app/core/components/Page/Page'; import { Page } from 'app/core/components/Page/Page';
import { DashboardScene } from '../scene/DashboardScene'; import { DashboardScene } from '../scene/DashboardScene';
import { NavToolbarActions } from '../scene/NavToolbarActions'; import { NavToolbarActions, ToolbarActions } from '../scene/NavToolbarActions';
import { getDashboardSceneFor } from '../utils/utils'; import { getDashboardSceneFor } from '../utils/utils';
import { EditListViewSceneUrlSync } from './EditListViewSceneUrlSync'; import { EditListViewSceneUrlSync } from './EditListViewSceneUrlSync';
@ -206,6 +207,7 @@ function VariableEditorSettingsListView({ model }: SceneComponentProps<Variables
const { onDelete, onDuplicated, onOrderChanged, onEdit, onTypeChange, onGoBack, onAdd } = model; const { onDelete, onDuplicated, onOrderChanged, onEdit, onTypeChange, onGoBack, onAdd } = model;
const { variables } = model.getVariableSet().useState(); const { variables } = model.getVariableSet().useState();
const { editIndex } = model.useState(); const { editIndex } = model.useState();
const isSingleTopNav = config.featureToggles.singleTopNav;
if (editIndex !== undefined && variables[editIndex]) { if (editIndex !== undefined && variables[editIndex]) {
const variable = variables[editIndex]; const variable = variables[editIndex];
@ -226,8 +228,13 @@ function VariableEditorSettingsListView({ model }: SceneComponentProps<Variables
} }
return ( return (
<Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Standard}> <Page
<NavToolbarActions dashboard={dashboard} /> navModel={navModel}
pageNav={pageNav}
layout={PageLayoutType.Standard}
toolbar={isSingleTopNav ? <ToolbarActions dashboard={dashboard} /> : undefined}
>
{!isSingleTopNav && <NavToolbarActions dashboard={dashboard} />}
<VariableEditorList <VariableEditorList
variables={variables} variables={variables}
onDelete={onDelete} onDelete={onDelete}
@ -262,14 +269,20 @@ function VariableEditorSettingsView({
onValidateVariableName, onValidateVariableName,
}: VariableEditorSettingsEditViewProps) { }: VariableEditorSettingsEditViewProps) {
const { name } = variable.useState(); const { name } = variable.useState();
const isSingleTopNav = config.featureToggles.singleTopNav;
const editVariablePageNav = { const editVariablePageNav = {
text: name, text: name,
parentItem: pageNav, parentItem: pageNav,
}; };
return ( return (
<Page navModel={navModel} pageNav={editVariablePageNav} layout={PageLayoutType.Standard}> <Page
<NavToolbarActions dashboard={dashboard} /> navModel={navModel}
pageNav={editVariablePageNav}
layout={PageLayoutType.Standard}
toolbar={isSingleTopNav ? <ToolbarActions dashboard={dashboard} /> : undefined}
>
{!isSingleTopNav && <NavToolbarActions dashboard={dashboard} />}
<VariableEditorForm <VariableEditorForm
variable={variable} variable={variable}
onTypeChange={onTypeChange} onTypeChange={onTypeChange}

@ -1,12 +1,13 @@
import * as React from 'react'; import * as React from 'react';
import { PageLayoutType, dateTimeFormat, dateTimeFormatTimeAgo } from '@grafana/data'; import { PageLayoutType, dateTimeFormat, dateTimeFormatTimeAgo } from '@grafana/data';
import { config } from '@grafana/runtime';
import { SceneComponentProps, SceneObjectBase, sceneGraph } from '@grafana/scenes'; import { SceneComponentProps, SceneObjectBase, sceneGraph } from '@grafana/scenes';
import { Spinner, Stack } from '@grafana/ui'; import { Spinner, Stack } from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page'; import { Page } from 'app/core/components/Page/Page';
import { DashboardScene } from '../scene/DashboardScene'; import { DashboardScene } from '../scene/DashboardScene';
import { NavToolbarActions } from '../scene/NavToolbarActions'; import { NavToolbarActions, ToolbarActions } from '../scene/NavToolbarActions';
import { getDashboardSceneFor } from '../utils/utils'; import { getDashboardSceneFor } from '../utils/utils';
import { DashboardEditView, DashboardEditViewState, useDashboardEditPageNav } from './utils'; import { DashboardEditView, DashboardEditViewState, useDashboardEditPageNav } from './utils';
@ -188,6 +189,7 @@ function VersionsEditorSettingsListView({ model }: SceneComponentProps<VersionsE
const showButtons = model.versions.length > 1; const showButtons = model.versions.length > 1;
const hasMore = model.versions.length >= model.limit; const hasMore = model.versions.length >= model.limit;
const isLastPage = model.versions.find((rev) => rev.version === 1); const isLastPage = model.versions.find((rev) => rev.version === 1);
const isSingleTopNav = config.featureToggles.singleTopNav;
const viewModeCompare = ( const viewModeCompare = (
<> <>
@ -237,8 +239,13 @@ function VersionsEditorSettingsListView({ model }: SceneComponentProps<VersionsE
); );
return ( return (
<Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Standard}> <Page
<NavToolbarActions dashboard={dashboard} /> navModel={navModel}
pageNav={pageNav}
layout={PageLayoutType.Standard}
toolbar={isSingleTopNav ? <ToolbarActions dashboard={dashboard} /> : undefined}
>
{!isSingleTopNav && <NavToolbarActions dashboard={dashboard} />}
{viewMode === 'compare' ? viewModeCompare : viewModeList} {viewMode === 'compare' ? viewModeCompare : viewModeList}
</Page> </Page>
); );

@ -4,7 +4,7 @@ import { useMemo } from 'react';
import { shallowEqual } from 'react-redux'; import { shallowEqual } from 'react-redux';
import { DataSourceInstanceSettings, RawTimeRange, GrafanaTheme2 } from '@grafana/data'; import { DataSourceInstanceSettings, RawTimeRange, GrafanaTheme2 } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime'; import { config, reportInteraction } from '@grafana/runtime';
import { import {
defaultIntervals, defaultIntervals,
PageToolbar, PageToolbar,
@ -89,6 +89,7 @@ export function ExploreToolbar({ exploreId, onChangeTime, onContentOutlineToogle
const correlationDetails = useSelector(selectCorrelationDetails); const correlationDetails = useSelector(selectCorrelationDetails);
const isCorrelationsEditorMode = correlationDetails?.editorMode || false; const isCorrelationsEditorMode = correlationDetails?.editorMode || false;
const isLeftPane = useSelector(isLeftPaneSelector(exploreId)); const isLeftPane = useSelector(isLeftPaneSelector(exploreId));
const isSingleTopNav = config.featureToggles.singleTopNav;
const shouldRotateSplitIcon = useMemo( const shouldRotateSplitIcon = useMemo(
() => (isLeftPane && isLargerPane) || (!isLeftPane && !isLargerPane), () => (isLeftPane && isLargerPane) || (!isLeftPane && !isLargerPane),
@ -206,9 +207,11 @@ export function ExploreToolbar({ exploreId, onChangeTime, onContentOutlineToogle
return ( return (
<div> <div>
{refreshInterval && <SetInterval func={onRunQuery} interval={refreshInterval} loading={loading} />} {refreshInterval && <SetInterval func={onRunQuery} interval={refreshInterval} loading={loading} />}
{!isSingleTopNav && (
<div> <div>
<AppChromeUpdate actions={navBarActions} /> <AppChromeUpdate actions={navBarActions} />
</div> </div>
)}
<PageToolbar <PageToolbar
aria-label={t('explore.toolbar.aria-label', 'Explore toolbar')} aria-label={t('explore.toolbar.aria-label', 'Explore toolbar')}
leftItems={[ leftItems={[
@ -233,6 +236,7 @@ export function ExploreToolbar({ exploreId, onChangeTime, onContentOutlineToogle
hideTextValue={showSmallDataSourcePicker} hideTextValue={showSmallDataSourcePicker}
width={showSmallDataSourcePicker ? 8 : undefined} width={showSmallDataSourcePicker ? 8 : undefined}
/>, />,
isSingleTopNav && <ShortLinkButtonMenu key="share" />,
].filter(Boolean)} ].filter(Boolean)}
forceShowLeftItems forceShowLeftItems
> >

Loading…
Cancel
Save