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. 124
      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. 5
      public/app/features/alerting/unified/CloneRuleEditor.test.tsx
  6. 44
      public/app/features/alerting/unified/components/receivers/TemplateForm.tsx
  7. 212
      public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx
  8. 46
      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. 12
      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,54 +92,58 @@ export const Page: PageType = ({
}, [navModel, pageNav, chrome, layout]); }, [navModel, pageNav, chrome, layout]);
return ( return (
<div className={cx(styles.wrapper, className)} {...otherProps}> <PageContext.Provider value={{ setToolbar }}>
{layout === PageLayoutType.Standard && ( <div className={cx(styles.wrapper, className)} {...otherProps}>
<NativeScrollbar {isSingleTopNav && toolbar && <PageToolbarActions>{toolbar}</PageToolbarActions>}
// This id is used by the image renderer to scroll through the dashboard {layout === PageLayoutType.Standard && (
divId="page-scrollbar" <NativeScrollbar
onSetScrollRef={onSetScrollRef} // This id is used by the image renderer to scroll through the dashboard
> divId="page-scrollbar"
<div className={styles.pageInner}> onSetScrollRef={onSetScrollRef}
{pageHeaderNav && ( >
<PageHeader <div className={styles.pageInner}>
actions={actions} {pageHeaderNav && (
onEditTitle={onEditTitle} <PageHeader
navItem={pageHeaderNav} actions={actions}
renderTitle={renderTitle} onEditTitle={onEditTitle}
info={info} navItem={pageHeaderNav}
subTitle={subTitle} renderTitle={renderTitle}
/> info={info}
)} subTitle={subTitle}
{pageNav && pageNav.children && <PageTabs navItem={pageNav} />} />
<div className={styles.pageContent}>{children}</div> )}
</div> {pageNav && pageNav.children && <PageTabs navItem={pageNav} />}
</NativeScrollbar> <div className={styles.pageContent}>{children}</div>
)} </div>
</NativeScrollbar>
{layout === PageLayoutType.Canvas && ( )}
<NativeScrollbar
// This id is used by the image renderer to scroll through the dashboard {layout === PageLayoutType.Canvas && (
divId="page-scrollbar" <NativeScrollbar
onSetScrollRef={onSetScrollRef} // This id is used by the image renderer to scroll through the dashboard
> divId="page-scrollbar"
<div className={styles.canvasContent}>{children}</div> onSetScrollRef={onSetScrollRef}
</NativeScrollbar> >
)} <div className={styles.canvasContent}>{children}</div>
</NativeScrollbar>
{layout === PageLayoutType.Custom && children} )}
</div>
{layout === PageLayoutType.Custom && children}
</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>
<FormProvider {...formApi}>{children}</FormProvider> <PageContext.Provider value={{ setToolbar: jest.fn() }}>
<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,28 +158,33 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props)
} }
}; };
const actionButtons = ( const actionButtons = useMemo(
<Stack> () => (
<Button onClick={() => formRef.current?.requestSubmit()} variant="primary" size="sm" disabled={isSubmitting}> <Stack>
Save <Button onClick={() => formRef.current?.requestSubmit()} variant="primary" size="sm" disabled={isSubmitting}>
</Button> Save
<LinkButton </Button>
disabled={isSubmitting} <LinkButton
href={makeAMLink('alerting/notifications', alertmanager, { disabled={isSubmitting}
tab: ContactPointsActiveTabs.NotificationTemplates, href={makeAMLink('alerting/notifications', alertmanager, {
})} tab: ContactPointsActiveTabs.NotificationTemplates,
variant="secondary" })}
size="sm" variant="secondary"
> size="sm"
Cancel >
</LinkButton> Cancel
</Stack> </LinkButton>
</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,52 +134,66 @@ 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(
if (conditionErrorMsg !== '') { async (values: RuleFormValues, exitOnSave: boolean) => {
notifyApp.error(conditionErrorMsg); if (conditionErrorMsg !== '') {
return; notifyApp.error(conditionErrorMsg);
} return;
}
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
const ruleGroupIdentifier = existing ? formValuesToRulerGrafanaRuleDTO(values)
? getRuleGroupLocationFromRuleWithLocation(existing) : formValuesToRulerRuleDTO(values);
: getRuleGroupLocationFromFormValues(values);
const ruleGroupIdentifier = existing
// @TODO move this to a hook too to make sure the logic here is tested for regressions? ? getRuleGroupLocationFromRuleWithLocation(existing)
if (!existing) { : getRuleGroupLocationFromFormValues(values);
// when creating a new rule, we save the manual routing setting , and editorSettings.simplifiedQueryEditor to the local storage
storeInLocalStorageValues(values); // @TODO move this to a hook too to make sure the logic here is tested for regressions?
await addRuleToRuleGroup.execute(ruleGroupIdentifier, ruleDefinition, evaluateEvery); if (!existing) {
} else { // when creating a new rule, we save the manual routing setting , and editorSettings.simplifiedQueryEditor to the local storage
const ruleIdentifier = fromRulerRuleAndRuleGroupIdentifier(ruleGroupIdentifier, existing.rule); storeInLocalStorageValues(values);
const targetRuleGroupIdentifier = getRuleGroupLocationFromFormValues(values); await addRuleToRuleGroup.execute(ruleGroupIdentifier, ruleDefinition, evaluateEvery);
await updateRuleInRuleGroup.execute( } else {
ruleGroupIdentifier, const ruleIdentifier = fromRulerRuleAndRuleGroupIdentifier(ruleGroupIdentifier, existing.rule);
ruleIdentifier, const targetRuleGroupIdentifier = getRuleGroupLocationFromFormValues(values);
ruleDefinition, await updateRuleInRuleGroup.execute(
targetRuleGroupIdentifier, ruleGroupIdentifier,
evaluateEvery ruleIdentifier,
); ruleDefinition,
} targetRuleGroupIdentifier,
evaluateEvery
const { dataSourceName, namespaceName, groupName } = ruleGroupIdentifier; );
if (exitOnSave) { }
const returnTo = queryParams.get('returnTo') || getReturnToUrl(ruleGroupIdentifier, ruleDefinition);
const { dataSourceName, namespaceName, groupName } = ruleGroupIdentifier;
locationService.push(returnTo); if (exitOnSave) {
return; const returnTo = queryParams.get('returnTo') || getReturnToUrl(ruleGroupIdentifier, ruleDefinition);
}
locationService.push(returnTo);
// Cloud Ruler rules identifier changes on update due to containing rule name and hash components return;
// After successful update we need to update the URL to avoid displaying 404 errors }
if (isCloudRulerRule(ruleDefinition)) {
const updatedRuleIdentifier = fromRulerRule(dataSourceName, namespaceName, groupName, ruleDefinition); // Cloud Ruler rules identifier changes on update due to containing rule name and hash components
locationService.replace(`/alerting/${encodeURIComponent(stringifyIdentifier(updatedRuleIdentifier))}/edit`); // After successful update we need to update the URL to avoid displaying 404 errors
} if (isCloudRulerRule(ruleDefinition)) {
}; const updatedRuleIdentifier = fromRulerRule(dataSourceName, namespaceName, groupName, ruleDefinition);
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,71 +206,78 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => {
} }
}; };
const onInvalid: SubmitErrorHandler<RuleFormValues> = (errors): void => { const onInvalid: SubmitErrorHandler<RuleFormValues> = useCallback(
trackAlertRuleFormError({ (errors): void => {
grafana_version: config.buildInfo.version, trackAlertRuleFormError({
org_id: contextSrv.user.orgId, grafana_version: config.buildInfo.version,
user_id: contextSrv.user.id, org_id: contextSrv.user.orgId,
error: Object.keys(errors).toString(), user_id: contextSrv.user.id,
formAction: existing ? 'update' : 'create', error: Object.keys(errors).toString(),
}); 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"> () => (
{existing && ( <Stack justifyContent="flex-end" alignItems="center">
{existing && (
<Button
variant="primary"
type="button"
size="sm"
onClick={handleSubmit((values) => submit(values, false), onInvalid)}
disabled={isSubmitting}
>
{isSubmitting && <Spinner className={styles.buttonSpinner} inline={true} />}
Save rule
</Button>
)}
<Button <Button
variant="primary" variant="primary"
type="button" type="button"
size="sm" size="sm"
onClick={handleSubmit((values) => submit(values, false), onInvalid)} onClick={handleSubmit((values) => submit(values, true), onInvalid)}
disabled={isSubmitting} disabled={isSubmitting}
> >
{isSubmitting && <Spinner className={styles.buttonSpinner} inline={true} />} {isSubmitting && <Spinner className={styles.buttonSpinner} inline={true} />}
Save rule Save rule and exit
</Button>
)}
<Button
variant="primary"
type="button"
size="sm"
onClick={handleSubmit((values) => submit(values, true), onInvalid)}
disabled={isSubmitting}
>
{isSubmitting && <Spinner className={styles.buttonSpinner} inline={true} />}
Save rule and exit
</Button>
<Button variant="secondary" disabled={isSubmitting} type="button" onClick={cancelRuleCreation} size="sm">
Cancel
</Button>
{existing ? (
<Button fill="outline" variant="destructive" type="button" onClick={() => setShowDeleteModal(true)} size="sm">
Delete
</Button> </Button>
) : null} <Button variant="secondary" disabled={isSubmitting} type="button" onClick={cancelRuleCreation} size="sm">
{existing && isCortexLokiOrRecordingRule(watch) && ( Cancel
<Button
variant="secondary"
type="button"
onClick={() => setShowEditYaml(true)}
disabled={isSubmitting}
size="sm"
>
Edit YAML
</Button> </Button>
)} {existing ? (
</Stack> <Button fill="outline" variant="destructive" type="button" onClick={() => setShowDeleteModal(true)} size="sm">
Delete
</Button>
) : null}
{existing && isCortexLokiOrRecordingRule(watch) && (
<Button
variant="secondary"
type="button"
onClick={() => setShowEditYaml(true)}
disabled={isSubmitting}
size="sm"
>
Edit YAML
</Button>
)}
</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(
if (conditionErrorMsg !== '') { (exportData: RuleFormValues | undefined) => {
notifyApp.error(conditionErrorMsg); if (conditionErrorMsg !== '') {
return; notifyApp.error(conditionErrorMsg);
} 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)}> () => [
Cancel <LinkButton href={returnTo} key="cancel" size="sm" variant="secondary" onClick={() => submit(undefined)}>
</LinkButton>, Cancel
<Button key="export-rule" size="sm" onClick={formAPI.handleSubmit((formValues) => submit(formValues), onInvalid)}> </LinkButton>,
Export <Button key="export-rule" size="sm" onClick={formAPI.handleSubmit((formValues) => submit(formValues), onInvalid)}>
</Button>, Export
]; </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} />}
<div> {!isSingleTopNav && (
<AppChromeUpdate actions={navBarActions} /> <div>
</div> <AppChromeUpdate actions={navBarActions} />
</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