diff --git a/public/app/core/components/AppChrome/AppChromeUpdate.tsx b/public/app/core/components/AppChrome/AppChromeUpdate.tsx index d7f03d2da4d..ef92b81903a 100644 --- a/public/app/core/components/AppChrome/AppChromeUpdate.tsx +++ b/public/app/core/components/AppChrome/AppChromeUpdate.tsx @@ -7,8 +7,7 @@ export interface AppChromeUpdateProps { actions?: React.ReactNode; } /** - * This needs to be moved to @grafana/ui or runtime. - * This is the way core pages and plugins update the breadcrumbs and page toolbar actions + * @deprecated This component is deprecated and will be removed in a future release. */ export const AppChromeUpdate = React.memo(({ actions }: AppChromeUpdateProps) => { const { chrome } = useGrafana(); diff --git a/public/app/core/components/Page/Page.tsx b/public/app/core/components/Page/Page.tsx index cd29bbfb556..0c5aab757b5 100644 --- a/public/app/core/components/Page/Page.tsx +++ b/public/app/core/components/Page/Page.tsx @@ -1,19 +1,58 @@ 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 { config } from '@grafana/runtime'; import { useStyles2 } from '@grafana/ui'; import { useGrafana } from 'app/core/context/GrafanaContext'; +import { TOP_BAR_LEVEL_HEIGHT } from '../AppChrome/types'; import NativeScrollbar from '../NativeScrollbar'; import { PageContents } from './PageContents'; import { PageHeader } from './PageHeader'; import { PageTabs } from './PageTabs'; +import { PageToolbarActions } from './PageToolbarActions'; import { PageType } from './types'; import { usePageNav } from './usePageNav'; import { usePageTitle } from './usePageTitle'; +export interface PageContextType { + setToolbar: Dispatch>; +} + +export const PageContext = createContext(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 = ({ navId, navModel: oldNavProp, @@ -24,12 +63,15 @@ export const Page: PageType = ({ subTitle, children, className, + toolbar: toolbarProp, info, layout = PageLayoutType.Standard, onSetScrollRef, ...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 { chrome } = useGrafana(); @@ -50,54 +92,58 @@ export const Page: PageType = ({ }, [navModel, pageNav, chrome, layout]); return ( -
- {layout === PageLayoutType.Standard && ( - -
- {pageHeaderNav && ( - - )} - {pageNav && pageNav.children && } -
{children}
-
-
- )} - - {layout === PageLayoutType.Canvas && ( - -
{children}
-
- )} - - {layout === PageLayoutType.Custom && children} -
+ +
+ {isSingleTopNav && toolbar && {toolbar}} + {layout === PageLayoutType.Standard && ( + +
+ {pageHeaderNav && ( + + )} + {pageNav && pageNav.children && } +
{children}
+
+
+ )} + + {layout === PageLayoutType.Canvas && ( + +
{children}
+
+ )} + + {layout === PageLayoutType.Custom && children} +
+
); }; Page.Contents = PageContents; -const getStyles = (theme: GrafanaTheme2) => { +const getStyles = (theme: GrafanaTheme2, hasToolbar: boolean) => { return { wrapper: css({ label: 'page-wrapper', display: 'flex', flex: '1 1 0', flexDirection: 'column', + marginTop: hasToolbar ? TOP_BAR_LEVEL_HEIGHT : 0, position: 'relative', }), pageContent: css({ diff --git a/public/app/core/components/Page/PageToolbarActions.tsx b/public/app/core/components/Page/PageToolbarActions.tsx new file mode 100644 index 00000000000..20c5589333f --- /dev/null +++ b/public/app/core/components/Page/PageToolbarActions.tsx @@ -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) { + 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 ( +
+ + {children} + +
+ ); +} + +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, + }), + }; +}; diff --git a/public/app/core/components/Page/types.ts b/public/app/core/components/Page/types.ts index f83f00e77f3..eb6bd7dd4d3 100644 --- a/public/app/core/components/Page/types.ts +++ b/public/app/core/components/Page/types.ts @@ -25,6 +25,8 @@ export interface PageProps extends HTMLAttributes { layout?: PageLayoutType; /** Can be used to get the scroll container element to access scroll position */ onSetScrollRef?: (ref: ScrollRefElement) => void; + /** Set a page-level toolbar */ + toolbar?: React.ReactNode; } export interface PageInfoItem { diff --git a/public/app/features/alerting/unified/CloneRuleEditor.test.tsx b/public/app/features/alerting/unified/CloneRuleEditor.test.tsx index 38155aa917a..fec861fa12c 100644 --- a/public/app/features/alerting/unified/CloneRuleEditor.test.tsx +++ b/public/app/features/alerting/unified/CloneRuleEditor.test.tsx @@ -5,6 +5,7 @@ import { byRole, byTestId, byText } from 'testing-library-selector'; import { selectors } from '@grafana/e2e-selectors/src'; import { setDataSourceSrv } from '@grafana/runtime'; +import { PageContext } from 'app/core/components/Page/Page'; import { DashboardSearchItem, DashboardSearchItemType } from 'app/features/search/types'; import { RuleWithLocation } from 'app/types/unified-alerting'; @@ -72,7 +73,9 @@ function Wrapper({ children }: React.PropsWithChildren<{}>) { const formApi = useForm({ defaultValues: getDefaultFormValues() }); return ( - {children} + + {children} + ); } diff --git a/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx b/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx index eeb6890e744..c306701172b 100644 --- a/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx +++ b/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx @@ -1,13 +1,13 @@ import { css, cx } from '@emotion/css'; import { addMinutes, subDays, subHours } from 'date-fns'; import { Location } from 'history'; -import { useRef, useState } from 'react'; +import { useMemo, useRef, useState } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import { useToggle } from 'react-use'; import AutoSizer from 'react-virtualized-auto-sizer'; import { GrafanaTheme2 } from '@grafana/data'; -import { isFetchError, locationService } from '@grafana/runtime'; +import { config as runtimeConfig, isFetchError, locationService } from '@grafana/runtime'; import { Alert, Button, @@ -21,6 +21,7 @@ import { InlineField, Box, } from '@grafana/ui'; +import { usePageToolbar } from 'app/core/components/Page/Page'; import { useAppNotification } from 'app/core/copy/appNotification'; import { useCleanup } from 'app/core/hooks/useCleanup'; 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 = ( - - - - Cancel - - + const actionButtons = useMemo( + () => ( + + + + Cancel + + + ), + [alertmanager, isSubmitting] ); + usePageToolbar(actionButtons); + return ( <> - + {!runtimeConfig.featureToggles.singleTopNav && }
{/* error message */} {error && ( diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx index 31942393121..8805b40cf1e 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx @@ -1,5 +1,5 @@ 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 { useParams } from 'react-router-dom'; @@ -7,6 +7,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { config, locationService } from '@grafana/runtime'; import { Button, ConfirmModal, CustomScrollbar, Spinner, Stack, useStyles2 } from '@grafana/ui'; import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate'; +import { usePageToolbar } from 'app/core/components/Page/Page'; import { useAppNotification } from 'app/core/copy/appNotification'; import { contextSrv } from 'app/core/core'; 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? - const submit = async (values: RuleFormValues, exitOnSave: boolean) => { - if (conditionErrorMsg !== '') { - notifyApp.error(conditionErrorMsg); - return; - } - - trackAlertRuleFormSaved({ formAction: existing ? 'update' : 'create', ruleType: values.type }); - - const ruleDefinition = grafanaTypeRule ? formValuesToRulerGrafanaRuleDTO(values) : formValuesToRulerRuleDTO(values); - - const ruleGroupIdentifier = existing - ? getRuleGroupLocationFromRuleWithLocation(existing) - : getRuleGroupLocationFromFormValues(values); - - // @TODO move this to a hook too to make sure the logic here is tested for regressions? - if (!existing) { - // when creating a new rule, we save the manual routing setting , and editorSettings.simplifiedQueryEditor to the local storage - storeInLocalStorageValues(values); - await addRuleToRuleGroup.execute(ruleGroupIdentifier, ruleDefinition, evaluateEvery); - } else { - const ruleIdentifier = fromRulerRuleAndRuleGroupIdentifier(ruleGroupIdentifier, existing.rule); - const targetRuleGroupIdentifier = getRuleGroupLocationFromFormValues(values); - await updateRuleInRuleGroup.execute( - ruleGroupIdentifier, - ruleIdentifier, - ruleDefinition, - targetRuleGroupIdentifier, - evaluateEvery - ); - } - - const { dataSourceName, namespaceName, groupName } = ruleGroupIdentifier; - if (exitOnSave) { - const returnTo = queryParams.get('returnTo') || getReturnToUrl(ruleGroupIdentifier, ruleDefinition); - - locationService.push(returnTo); - return; - } - - // 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 - if (isCloudRulerRule(ruleDefinition)) { - const updatedRuleIdentifier = fromRulerRule(dataSourceName, namespaceName, groupName, ruleDefinition); - locationService.replace(`/alerting/${encodeURIComponent(stringifyIdentifier(updatedRuleIdentifier))}/edit`); - } - }; + const submit = useCallback( + async (values: RuleFormValues, exitOnSave: boolean) => { + if (conditionErrorMsg !== '') { + notifyApp.error(conditionErrorMsg); + return; + } + + trackAlertRuleFormSaved({ formAction: existing ? 'update' : 'create', ruleType: values.type }); + + const ruleDefinition = grafanaTypeRule + ? formValuesToRulerGrafanaRuleDTO(values) + : formValuesToRulerRuleDTO(values); + + const ruleGroupIdentifier = existing + ? getRuleGroupLocationFromRuleWithLocation(existing) + : getRuleGroupLocationFromFormValues(values); + + // @TODO move this to a hook too to make sure the logic here is tested for regressions? + if (!existing) { + // when creating a new rule, we save the manual routing setting , and editorSettings.simplifiedQueryEditor to the local storage + storeInLocalStorageValues(values); + await addRuleToRuleGroup.execute(ruleGroupIdentifier, ruleDefinition, evaluateEvery); + } else { + const ruleIdentifier = fromRulerRuleAndRuleGroupIdentifier(ruleGroupIdentifier, existing.rule); + const targetRuleGroupIdentifier = getRuleGroupLocationFromFormValues(values); + await updateRuleInRuleGroup.execute( + ruleGroupIdentifier, + ruleIdentifier, + ruleDefinition, + targetRuleGroupIdentifier, + evaluateEvery + ); + } + + const { dataSourceName, namespaceName, groupName } = ruleGroupIdentifier; + if (exitOnSave) { + const returnTo = queryParams.get('returnTo') || getReturnToUrl(ruleGroupIdentifier, ruleDefinition); + + locationService.push(returnTo); + return; + } + + // 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 + 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 () => { if (existing) { @@ -191,71 +206,78 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => { } }; - const onInvalid: SubmitErrorHandler = (errors): void => { - trackAlertRuleFormError({ - grafana_version: config.buildInfo.version, - org_id: contextSrv.user.orgId, - user_id: contextSrv.user.id, - error: Object.keys(errors).toString(), - formAction: existing ? 'update' : 'create', - }); - notifyApp.error('There are errors in the form. Please correct them and try again!'); - }; + const onInvalid: SubmitErrorHandler = useCallback( + (errors): void => { + trackAlertRuleFormError({ + grafana_version: config.buildInfo.version, + org_id: contextSrv.user.orgId, + user_id: contextSrv.user.id, + error: Object.keys(errors).toString(), + formAction: existing ? 'update' : 'create', + }); + notifyApp.error('There are errors in the form. Please correct them and try again!'); + }, + [existing, notifyApp] + ); - const cancelRuleCreation = () => { + const cancelRuleCreation = useCallback(() => { logInfo(LogMessages.cancelSavingAlertRule); trackAlertRuleFormCancelled({ formAction: existing ? 'update' : 'create' }); locationService.getHistory().goBack(); - }; + }, [existing]); const evaluateEveryInForm = watch('evaluateEvery'); useEffect(() => setEvaluateEvery(evaluateEveryInForm), [evaluateEveryInForm]); - const actionButtons = ( - - {existing && ( + const actionButtons = useMemo( + () => ( + + {existing && ( + + )} - )} - - - {existing ? ( - - ) : null} - {existing && isCortexLokiOrRecordingRule(watch) && ( - - )} - + {existing ? ( + + ) : null} + {existing && isCortexLokiOrRecordingRule(watch) && ( + + )} + + ), + [cancelRuleCreation, existing, handleSubmit, isSubmitting, onInvalid, styles.buttonSpinner, submit, watch] ); + usePageToolbar(actionButtons); const isPaused = existing && isGrafanaRulerRule(existing.rule) && isGrafanaRulerRulePaused(existing.rule); if (!type) { @@ -263,7 +285,7 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => { } return ( - + {!config.featureToggles.singleTopNav && } e.preventDefault()} className={styles.form}>
{isPaused && } diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx index 79eef183128..ee890b033bc 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx @@ -2,7 +2,9 @@ import { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import { useAsync } from 'react-use'; +import { config } from '@grafana/runtime'; 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 { useQueryParams } from 'app/core/hooks/useQueryParams'; @@ -50,39 +52,47 @@ export function ModifyExportRuleForm({ ruleForm, alertUid }: ModifyExportRuleFor const [conditionErrorMsg, setConditionErrorMsg] = useState(''); 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]); const checkAlertCondition = (msg = '') => { setConditionErrorMsg(msg); }; - const submit = (exportData: RuleFormValues | undefined) => { - if (conditionErrorMsg !== '') { - notifyApp.error(conditionErrorMsg); - return; - } - setExportData(exportData); - }; + const submit = useCallback( + (exportData: RuleFormValues | undefined) => { + if (conditionErrorMsg !== '') { + notifyApp.error(conditionErrorMsg); + return; + } + setExportData(exportData); + }, + [conditionErrorMsg, notifyApp] + ); const onClose = useCallback(() => { setExportData(undefined); }, [setExportData]); - const actionButtons = [ - submit(undefined)}> - Cancel - , - , - ]; + const actionButtons = useMemo( + () => [ + submit(undefined)}> + Cancel + , + , + ], + [formAPI, onInvalid, returnTo, submit] + ); + + usePageToolbar(actionButtons); return ( <> - + {!config.featureToggles.singleTopNav && } e.preventDefault()}>
diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx b/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx index 32780129248..d338a64be03 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx @@ -3,9 +3,10 @@ import { useEffect, useMemo } from 'react'; import { useLocation } from 'react-router-dom-v5-compat'; import { GrafanaTheme2, PageLayoutType } from '@grafana/data'; -import { useChromeHeaderHeight } from '@grafana/runtime'; +import { config, useChromeHeaderHeight } from '@grafana/runtime'; import { SceneComponentProps } from '@grafana/scenes'; import { useStyles2 } from '@grafana/ui'; +import { TOP_BAR_LEVEL_HEIGHT } from 'app/core/components/AppChrome/types'; import NativeScrollbar from 'app/core/components/NativeScrollbar'; import { Page } from 'app/core/components/Page/Page'; 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 { DashboardScene } from './DashboardScene'; -import { NavToolbarActions } from './NavToolbarActions'; +import { NavToolbarActions, ToolbarActions } from './NavToolbarActions'; import { PanelSearchLayout } from './PanelSearchLayout'; import { DashboardAngularDeprecationBanner } from './angular/DashboardAngularDeprecationBanner'; @@ -22,7 +23,7 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps state.navIndex); const pageNav = model.getPageNav(location, navIndex); @@ -30,6 +31,7 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps { @@ -79,12 +81,17 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps + : undefined} + > {editPanel && } {!editPanel && (
- + {!isSingleTopNav && } {controls && (
@@ -99,7 +106,7 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps - + : undefined} + > + {!isSingleTopNav && } - + : undefined} + > + {!isSingleTopNav && } - + : undefined} + > + {!isSingleTopNav && } - + : undefined} + > + {!isSingleTopNav && } ); diff --git a/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx b/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx index 73b1d8cdceb..1540c2fbbd0 100644 --- a/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx +++ b/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx @@ -25,7 +25,7 @@ import { GenAIDashTitleButton } from 'app/features/dashboard/components/GenAI/Ge import { updateNavModel } from '../pages/utils'; import { DashboardScene } from '../scene/DashboardScene'; -import { NavToolbarActions } from '../scene/NavToolbarActions'; +import { NavToolbarActions, ToolbarActions } from '../scene/NavToolbarActions'; import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { getDashboardSceneFor } from '../utils/utils'; @@ -177,10 +177,16 @@ export class GeneralSettingsEditView const { intervals } = model.getRefreshPicker().useState(); const { hideTimeControls } = model.getDashboardControls().useState(); const { enabled: liveNow } = model.getLiveNowTimer().useState(); + const isSingleTopNav = config.featureToggles.singleTopNav; return ( - - + : undefined} + > + {!isSingleTopNav && }
i const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey()); const canSave = dashboard.useState().meta.canSave; const { jsonText } = model.useState(); + const isSingleTopNav = config.featureToggles.singleTopNav; const onSave = async (overwrite: boolean) => { const result = await onSaveDashboard(dashboard, JSON.parse(model.state.jsonText), { @@ -174,8 +176,13 @@ export class JsonModelEditView extends SceneObjectBase i ); } return ( - - + : undefined} + > + {!isSingleTopNav && }
The JSON model below is the data structure that defines the dashboard. This includes dashboard settings, diff --git a/public/app/features/dashboard-scene/settings/PermissionsEditView.tsx b/public/app/features/dashboard-scene/settings/PermissionsEditView.tsx index 5dfa1a49659..727122e9ecb 100644 --- a/public/app/features/dashboard-scene/settings/PermissionsEditView.tsx +++ b/public/app/features/dashboard-scene/settings/PermissionsEditView.tsx @@ -1,4 +1,5 @@ import { PageLayoutType } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { SceneComponentProps, SceneObjectBase } from '@grafana/scenes'; import { Permissions } from 'app/core/components/AccessControl'; import { Page } from 'app/core/components/Page/Page'; @@ -6,7 +7,7 @@ import { contextSrv } from 'app/core/core'; import { AccessControlAction } from 'app/types'; import { DashboardScene } from '../scene/DashboardScene'; -import { NavToolbarActions } from '../scene/NavToolbarActions'; +import { NavToolbarActions, ToolbarActions } from '../scene/NavToolbarActions'; import { getDashboardSceneFor } from '../utils/utils'; import { DashboardEditView, DashboardEditViewState, useDashboardEditPageNav } from './utils'; @@ -34,10 +35,16 @@ function PermissionsEditorSettings({ model }: SceneComponentProps - + : undefined} + > + {!isSingleTopNav && } ); diff --git a/public/app/features/dashboard-scene/settings/VariablesEditView.tsx b/public/app/features/dashboard-scene/settings/VariablesEditView.tsx index ffdfe4e7856..993bcd0fb10 100644 --- a/public/app/features/dashboard-scene/settings/VariablesEditView.tsx +++ b/public/app/features/dashboard-scene/settings/VariablesEditView.tsx @@ -1,9 +1,10 @@ import { NavModel, NavModelItem, PageLayoutType } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { SceneComponentProps, SceneObjectBase, SceneVariable, SceneVariables, sceneGraph } from '@grafana/scenes'; import { Page } from 'app/core/components/Page/Page'; import { DashboardScene } from '../scene/DashboardScene'; -import { NavToolbarActions } from '../scene/NavToolbarActions'; +import { NavToolbarActions, ToolbarActions } from '../scene/NavToolbarActions'; import { getDashboardSceneFor } from '../utils/utils'; import { EditListViewSceneUrlSync } from './EditListViewSceneUrlSync'; @@ -206,6 +207,7 @@ function VariableEditorSettingsListView({ model }: SceneComponentProps - + : undefined} + > + {!isSingleTopNav && } - + : undefined} + > + {!isSingleTopNav && } 1; const hasMore = model.versions.length >= model.limit; const isLastPage = model.versions.find((rev) => rev.version === 1); + const isSingleTopNav = config.featureToggles.singleTopNav; const viewModeCompare = ( <> @@ -237,8 +239,13 @@ function VersionsEditorSettingsListView({ model }: SceneComponentProps - + : undefined} + > + {!isSingleTopNav && } {viewMode === 'compare' ? viewModeCompare : viewModeList} ); diff --git a/public/app/features/explore/ExploreToolbar.tsx b/public/app/features/explore/ExploreToolbar.tsx index a97aa3a46d9..1a08b894354 100644 --- a/public/app/features/explore/ExploreToolbar.tsx +++ b/public/app/features/explore/ExploreToolbar.tsx @@ -4,7 +4,7 @@ import { useMemo } from 'react'; import { shallowEqual } from 'react-redux'; import { DataSourceInstanceSettings, RawTimeRange, GrafanaTheme2 } from '@grafana/data'; -import { reportInteraction } from '@grafana/runtime'; +import { config, reportInteraction } from '@grafana/runtime'; import { defaultIntervals, PageToolbar, @@ -89,6 +89,7 @@ export function ExploreToolbar({ exploreId, onChangeTime, onContentOutlineToogle const correlationDetails = useSelector(selectCorrelationDetails); const isCorrelationsEditorMode = correlationDetails?.editorMode || false; const isLeftPane = useSelector(isLeftPaneSelector(exploreId)); + const isSingleTopNav = config.featureToggles.singleTopNav; const shouldRotateSplitIcon = useMemo( () => (isLeftPane && isLargerPane) || (!isLeftPane && !isLargerPane), @@ -206,9 +207,11 @@ export function ExploreToolbar({ exploreId, onChangeTime, onContentOutlineToogle return (
{refreshInterval && } -
- -
+ {!isSingleTopNav && ( +
+ +
+ )} , + isSingleTopNav && , ].filter(Boolean)} forceShowLeftItems >