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...
This commit is contained in:
Ashley Harrison
2024-10-03 09:52:01 +01:00
committed by GitHub
parent e48d166c3e
commit dd7f45011d
17 changed files with 415 additions and 204 deletions

View File

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

View File

@@ -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 && ( {layout === PageLayoutType.Canvas && (
<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
divId="page-scrollbar" divId="page-scrollbar"
onSetScrollRef={onSetScrollRef} onSetScrollRef={onSetScrollRef}
> >
<div className={styles.canvasContent}>{children}</div> <div className={styles.canvasContent}>{children}</div>
</NativeScrollbar> </NativeScrollbar>
)} )}
{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({

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
? formValuesToRulerGrafanaRuleDTO(values)
: formValuesToRulerRuleDTO(values);
const ruleGroupIdentifier = existing const ruleGroupIdentifier = existing
? getRuleGroupLocationFromRuleWithLocation(existing) ? getRuleGroupLocationFromRuleWithLocation(existing)
: getRuleGroupLocationFromFormValues(values); : getRuleGroupLocationFromFormValues(values);
// @TODO move this to a hook too to make sure the logic here is tested for regressions? // @TODO move this to a hook too to make sure the logic here is tested for regressions?
if (!existing) { if (!existing) {
// when creating a new rule, we save the manual routing setting , and editorSettings.simplifiedQueryEditor to the local storage // when creating a new rule, we save the manual routing setting , and editorSettings.simplifiedQueryEditor to the local storage
storeInLocalStorageValues(values); storeInLocalStorageValues(values);
await addRuleToRuleGroup.execute(ruleGroupIdentifier, ruleDefinition, evaluateEvery); await addRuleToRuleGroup.execute(ruleGroupIdentifier, ruleDefinition, evaluateEvery);
} else { } else {
const ruleIdentifier = fromRulerRuleAndRuleGroupIdentifier(ruleGroupIdentifier, existing.rule); const ruleIdentifier = fromRulerRuleAndRuleGroupIdentifier(ruleGroupIdentifier, existing.rule);
const targetRuleGroupIdentifier = getRuleGroupLocationFromFormValues(values); const targetRuleGroupIdentifier = getRuleGroupLocationFromFormValues(values);
await updateRuleInRuleGroup.execute( await updateRuleInRuleGroup.execute(
ruleGroupIdentifier, ruleGroupIdentifier,
ruleIdentifier, ruleIdentifier,
ruleDefinition, ruleDefinition,
targetRuleGroupIdentifier, targetRuleGroupIdentifier,
evaluateEvery evaluateEvery
); );
} }
const { dataSourceName, namespaceName, groupName } = ruleGroupIdentifier; const { dataSourceName, namespaceName, groupName } = ruleGroupIdentifier;
if (exitOnSave) { if (exitOnSave) {
const returnTo = queryParams.get('returnTo') || getReturnToUrl(ruleGroupIdentifier, ruleDefinition); const returnTo = queryParams.get('returnTo') || getReturnToUrl(ruleGroupIdentifier, ruleDefinition);
locationService.push(returnTo); locationService.push(returnTo);
return; return;
} }
// Cloud Ruler rules identifier changes on update due to containing rule name and hash components // Cloud Ruler rules identifier changes on update due to containing rule name and hash components
// After successful update we need to update the URL to avoid displaying 404 errors // After successful update we need to update the URL to avoid displaying 404 errors
if (isCloudRulerRule(ruleDefinition)) { if (isCloudRulerRule(ruleDefinition)) {
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,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>
)} <Button variant="secondary" disabled={isSubmitting} type="button" onClick={cancelRuleCreation} size="sm">
<Button Cancel
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} {existing ? (
{existing && isCortexLokiOrRecordingRule(watch) && ( <Button fill="outline" variant="destructive" type="button" onClick={() => setShowDeleteModal(true)} size="sm">
<Button Delete
variant="secondary" </Button>
type="button" ) : null}
onClick={() => setShowEditYaml(true)} {existing && isCortexLokiOrRecordingRule(watch) && (
disabled={isSubmitting} <Button
size="sm" variant="secondary"
> type="button"
Edit YAML onClick={() => setShowEditYaml(true)}
</Button> disabled={isSubmitting}
)} size="sm"
</Stack> >
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 />}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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