SingleTopNav: Revert to using `AppChromeUpdate` so banners are correct (#94540)

* revert to using AppChromeUpdate

* fix dashboard settings in old arch

* remove empty interface

* fix AlertRuleForm
pull/94624/head^2
Ashley Harrison 9 months ago committed by GitHub
parent a5d72e264d
commit 4d08f44667
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 22
      public/app/core/components/AppChrome/AppChrome.tsx
  2. 2
      public/app/core/components/AppChrome/AppChromeUpdate.tsx
  3. 79
      public/app/core/components/AppChrome/MegaMenu/MegaMenu.tsx
  4. 33
      public/app/core/components/AppChrome/TopBar/SingleTopBarActions.tsx
  5. 124
      public/app/core/components/Page/Page.tsx
  6. 47
      public/app/core/components/Page/PageToolbarActions.tsx
  7. 2
      public/app/core/components/Page/types.ts
  8. 25
      public/app/core/context/GrafanaContext.ts
  9. 5
      public/app/features/alerting/unified/CloneRuleEditor.test.tsx
  10. 44
      public/app/features/alerting/unified/components/receivers/TemplateForm.tsx
  11. 208
      public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx
  12. 46
      public/app/features/alerting/unified/components/rule-editor/alert-rule-form/ModifyExportRuleForm.tsx
  13. 17
      public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx
  14. 24
      public/app/features/dashboard-scene/settings/AnnotationsEditView.tsx
  15. 23
      public/app/features/dashboard-scene/settings/DashboardLinksEditView.tsx
  16. 12
      public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx
  17. 13
      public/app/features/dashboard-scene/settings/JsonModelEditView.tsx
  18. 13
      public/app/features/dashboard-scene/settings/PermissionsEditView.tsx
  19. 23
      public/app/features/dashboard-scene/settings/VariablesEditView.tsx
  20. 13
      public/app/features/dashboard-scene/settings/VersionsEditView.tsx
  21. 16
      public/app/features/dashboard/components/DashNav/DashNav.tsx
  22. 4
      public/app/features/dashboard/components/DashboardPermissions/AccessControlDashboardPermissions.tsx
  23. 4
      public/app/features/dashboard/components/DashboardSettings/AnnotationsSettings.tsx
  24. 18
      public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx
  25. 3
      public/app/features/dashboard/components/DashboardSettings/GeneralSettings.tsx
  26. 4
      public/app/features/dashboard/components/DashboardSettings/JsonEditorSettings.tsx
  27. 4
      public/app/features/dashboard/components/DashboardSettings/LinksSettings.tsx
  28. 4
      public/app/features/dashboard/components/DashboardSettings/VersionsSettings.tsx
  29. 3
      public/app/features/dashboard/components/DashboardSettings/types.ts
  30. 16
      public/app/features/dashboard/components/PanelEditor/PanelEditor.tsx
  31. 24
      public/app/features/dashboard/containers/DashboardPage.tsx
  32. 4
      public/app/features/variables/editor/VariableEditorContainer.tsx

@ -18,6 +18,7 @@ import { MegaMenu, MENU_WIDTH } from './MegaMenu/MegaMenu';
import { NavToolbar } from './NavToolbar/NavToolbar';
import { ReturnToPrevious } from './ReturnToPrevious/ReturnToPrevious';
import { SingleTopBar } from './TopBar/SingleTopBar';
import { SingleTopBarActions } from './TopBar/SingleTopBarActions';
import { TopSearchBar } from './TopBar/TopSearchBar';
import { TOP_BAR_LEVEL_HEIGHT } from './types';
@ -28,7 +29,7 @@ export function AppChrome({ children }: Props) {
const state = chrome.useState();
const searchBarHidden = state.searchBarHidden || state.kioskMode === KioskMode.TV;
const theme = useTheme2();
const styles = useStyles2(getStyles, searchBarHidden);
const styles = useStyles2(getStyles, searchBarHidden, Boolean(state.actions));
const dockedMenuBreakpoint = theme.breakpoints.values.xl;
const dockedMenuLocalStorageState = store.getBool(DOCKED_LOCAL_STORAGE_KEY, true);
@ -99,12 +100,15 @@ export function AppChrome({ children }: Props) {
)}
<header className={cx(styles.topNav, isSingleTopNav && menuDockedAndOpen && styles.topNavMenuDocked)}>
{isSingleTopNav ? (
<SingleTopBar
sectionNav={state.sectionNav.node}
pageNav={state.pageNav}
onToggleMegaMenu={handleMegaMenu}
onToggleKioskMode={chrome.onToggleKioskMode}
/>
<>
<SingleTopBar
sectionNav={state.sectionNav.node}
pageNav={state.pageNav}
onToggleMegaMenu={handleMegaMenu}
onToggleKioskMode={chrome.onToggleKioskMode}
/>
{state.actions && <SingleTopBarActions>{state.actions}</SingleTopBarActions>}
</>
) : (
<>
{!searchBarHidden && <TopSearchBar />}
@ -156,13 +160,13 @@ export function AppChrome({ children }: Props) {
);
}
const getStyles = (theme: GrafanaTheme2, searchBarHidden: boolean) => {
const getStyles = (theme: GrafanaTheme2, searchBarHidden: boolean, hasActions: boolean) => {
const isSingleTopNav = config.featureToggles.singleTopNav;
return {
content: css({
display: 'flex',
flexDirection: 'column',
paddingTop: isSingleTopNav ? TOP_BAR_LEVEL_HEIGHT : TOP_BAR_LEVEL_HEIGHT * 2,
paddingTop: !isSingleTopNav || hasActions ? TOP_BAR_LEVEL_HEIGHT * 2 : TOP_BAR_LEVEL_HEIGHT,
flexGrow: 1,
height: 'auto',
}),

@ -7,7 +7,7 @@ export interface AppChromeUpdateProps {
actions?: React.ReactNode;
}
/**
* @deprecated This component is deprecated and will be removed in a future release.
* This is the way core pages add actions to the second chrome toolbar
*/
export const AppChromeUpdate = React.memo<AppChromeUpdateProps>(({ actions }: AppChromeUpdateProps) => {
const { chrome } = useGrafana();

@ -13,6 +13,8 @@ import { setBookmark } from 'app/core/reducers/navBarTree';
import { usePatchUserPreferencesMutation } from 'app/features/preferences/api/index';
import { useDispatch, useSelector } from 'app/types';
import { TOP_BAR_LEVEL_HEIGHT } from '../types';
import { MegaMenuHeader } from './MegaMenuHeader';
import { MegaMenuItem } from './MegaMenuItem';
import { usePinnedItems } from './hooks';
@ -166,41 +168,44 @@ export const MegaMenu = memo(
MegaMenu.displayName = 'MegaMenu';
const getStyles = (theme: GrafanaTheme2) => ({
content: css({
display: 'flex',
flexDirection: 'column',
height: '100%',
minHeight: 0,
position: 'relative',
}),
mobileHeader: css({
display: 'flex',
justifyContent: 'space-between',
padding: theme.spacing(1, 1, 1, 2),
borderBottom: `1px solid ${theme.colors.border.weak}`,
[theme.breakpoints.up('md')]: {
const getStyles = (theme: GrafanaTheme2) => {
const isSingleTopNav = config.featureToggles.singleTopNav;
return {
content: css({
display: 'flex',
flexDirection: 'column',
height: isSingleTopNav ? `calc(100% - ${TOP_BAR_LEVEL_HEIGHT}px)` : '100%',
minHeight: 0,
position: 'relative',
}),
mobileHeader: css({
display: 'flex',
justifyContent: 'space-between',
padding: theme.spacing(1, 1, 1, 2),
borderBottom: `1px solid ${theme.colors.border.weak}`,
[theme.breakpoints.up('md')]: {
display: 'none',
},
}),
itemList: css({
boxSizing: 'border-box',
display: 'flex',
flexDirection: 'column',
listStyleType: 'none',
padding: theme.spacing(1, 1, 2, 1),
[theme.breakpoints.up('md')]: {
width: MENU_WIDTH,
},
}),
dockMenuButton: css({
display: 'none',
},
}),
itemList: css({
boxSizing: 'border-box',
display: 'flex',
flexDirection: 'column',
listStyleType: 'none',
padding: theme.spacing(1, 1, 2, 1),
[theme.breakpoints.up('md')]: {
width: MENU_WIDTH,
},
}),
dockMenuButton: css({
display: 'none',
position: 'relative',
top: theme.spacing(1),
[theme.breakpoints.up('xl')]: {
display: 'inline-flex',
},
}),
});
position: 'relative',
top: theme.spacing(1),
[theme.breakpoints.up('xl')]: {
display: 'inline-flex',
},
}),
};
};

@ -0,0 +1,33 @@
import { css } from '@emotion/css';
import { PropsWithChildren } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Components } from '@grafana/e2e-selectors';
import { Stack, useStyles2 } from '@grafana/ui';
import { TOP_BAR_LEVEL_HEIGHT } from '../types';
export function SingleTopBarActions({ children }: PropsWithChildren) {
const styles = useStyles2(getStyles);
return (
<div data-testid={Components.NavToolbar.container} className={styles.actionsBar}>
<Stack alignItems="center" justifyContent="flex-end" flex={1} wrap="nowrap" minWidth={0}>
{children}
</Stack>
</div>
);
}
const getStyles = (theme: GrafanaTheme2) => {
return {
actionsBar: css({
alignItems: 'center',
backgroundColor: theme.colors.background.primary,
borderBottom: `1px solid ${theme.colors.border.weak}`,
display: 'flex',
height: TOP_BAR_LEVEL_HEIGHT,
padding: theme.spacing(0, 1, 0, 2),
}),
};
};

@ -1,58 +1,19 @@
import { css, cx } from '@emotion/css';
import {
createContext,
Dispatch,
ReactNode,
SetStateAction,
useContext,
useEffect,
useLayoutEffect,
useState,
} from 'react';
import { useLayoutEffect } 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<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 = ({
navId,
navModel: oldNavProp,
@ -63,15 +24,12 @@ export const Page: PageType = ({
subTitle,
children,
className,
toolbar: toolbarProp,
info,
layout = PageLayoutType.Standard,
onSetScrollRef,
...otherProps
}) => {
const isSingleTopNav = config.featureToggles.singleTopNav;
const [toolbar, setToolbar] = useState(toolbarProp);
const styles = useStyles2(getStyles, Boolean(isSingleTopNav && toolbar));
const styles = useStyles2(getStyles);
const navModel = usePageNav(navId, oldNavProp);
const { chrome } = useGrafana();
@ -92,58 +50,54 @@ export const Page: PageType = ({
}, [navModel, pageNav, chrome, layout]);
return (
<PageContext.Provider value={{ setToolbar }}>
<div className={cx(styles.wrapper, className)} {...otherProps}>
{isSingleTopNav && toolbar && <PageToolbarActions>{toolbar}</PageToolbarActions>}
{layout === PageLayoutType.Standard && (
<NativeScrollbar
// This id is used by the image renderer to scroll through the dashboard
divId="page-scrollbar"
onSetScrollRef={onSetScrollRef}
>
<div className={styles.pageInner}>
{pageHeaderNav && (
<PageHeader
actions={actions}
onEditTitle={onEditTitle}
navItem={pageHeaderNav}
renderTitle={renderTitle}
info={info}
subTitle={subTitle}
/>
)}
{pageNav && pageNav.children && <PageTabs navItem={pageNav} />}
<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
divId="page-scrollbar"
onSetScrollRef={onSetScrollRef}
>
<div className={styles.canvasContent}>{children}</div>
</NativeScrollbar>
)}
{layout === PageLayoutType.Custom && children}
</div>
</PageContext.Provider>
<div className={cx(styles.wrapper, className)} {...otherProps}>
{layout === PageLayoutType.Standard && (
<NativeScrollbar
// This id is used by the image renderer to scroll through the dashboard
divId="page-scrollbar"
onSetScrollRef={onSetScrollRef}
>
<div className={styles.pageInner}>
{pageHeaderNav && (
<PageHeader
actions={actions}
onEditTitle={onEditTitle}
navItem={pageHeaderNav}
renderTitle={renderTitle}
info={info}
subTitle={subTitle}
/>
)}
{pageNav && pageNav.children && <PageTabs navItem={pageNav} />}
<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
divId="page-scrollbar"
onSetScrollRef={onSetScrollRef}
>
<div className={styles.canvasContent}>{children}</div>
</NativeScrollbar>
)}
{layout === PageLayoutType.Custom && children}
</div>
);
};
Page.Contents = PageContents;
const getStyles = (theme: GrafanaTheme2, hasToolbar: boolean) => {
const getStyles = (theme: GrafanaTheme2) => {
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({

@ -1,47 +0,0 @@
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,8 +25,6 @@ export interface PageProps extends HTMLAttributes<HTMLDivElement> {
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 {

@ -4,6 +4,7 @@ import { GrafanaConfig } from '@grafana/data';
import { LocationService, locationService, BackendSrv, config } from '@grafana/runtime';
import { AppChromeService } from '../components/AppChrome/AppChromeService';
import { TOP_BAR_LEVEL_HEIGHT } from '../components/AppChrome/types';
import { NewFrontendAssetsChecker } from '../services/NewFrontendAssetsChecker';
import { KeybindingSrv } from '../services/keybindingSrv';
@ -42,17 +43,25 @@ export function useReturnToPreviousInternal() {
);
}
const SINGLE_HEADER_BAR_HEIGHT = 40;
export function useChromeHeaderHeight() {
const { chrome } = useGrafana();
const { kioskMode, searchBarHidden, chromeless } = chrome.useState();
const { actions, kioskMode, searchBarHidden, chromeless } = chrome.useState();
if (kioskMode || chromeless) {
return 0;
} else if (searchBarHidden || config.featureToggles.singleTopNav) {
return SINGLE_HEADER_BAR_HEIGHT;
if (config.featureToggles.singleTopNav) {
if (kioskMode || chromeless) {
return 0;
} else if (actions) {
return TOP_BAR_LEVEL_HEIGHT * 2;
} else {
return TOP_BAR_LEVEL_HEIGHT;
}
} else {
return SINGLE_HEADER_BAR_HEIGHT * 2;
if (kioskMode || chromeless) {
return 0;
} else if (searchBarHidden) {
return TOP_BAR_LEVEL_HEIGHT;
} else {
return TOP_BAR_LEVEL_HEIGHT * 2;
}
}
}

@ -5,7 +5,6 @@ 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';
@ -73,9 +72,7 @@ function Wrapper({ children }: React.PropsWithChildren<{}>) {
const formApi = useForm<RuleFormValues>({ defaultValues: getDefaultFormValues() });
return (
<Providers>
<PageContext.Provider value={{ setToolbar: jest.fn() }}>
<FormProvider {...formApi}>{children}</FormProvider>
</PageContext.Provider>
<FormProvider {...formApi}>{children}</FormProvider>
</Providers>
);
}

@ -1,13 +1,13 @@
import { css, cx } from '@emotion/css';
import { addMinutes, subDays, subHours } from 'date-fns';
import { Location } from 'history';
import { useMemo, useRef, useState } from 'react';
import { 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 { config as runtimeConfig, isFetchError, locationService } from '@grafana/runtime';
import { isFetchError, locationService } from '@grafana/runtime';
import {
Alert,
Button,
@ -21,7 +21,6 @@ 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';
@ -158,33 +157,28 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props)
}
};
const actionButtons = useMemo(
() => (
<Stack>
<Button onClick={() => formRef.current?.requestSubmit()} variant="primary" size="sm" disabled={isSubmitting}>
Save
</Button>
<LinkButton
disabled={isSubmitting}
href={makeAMLink('alerting/notifications', alertmanager, {
tab: ContactPointsActiveTabs.NotificationTemplates,
})}
variant="secondary"
size="sm"
>
Cancel
</LinkButton>
</Stack>
),
[alertmanager, isSubmitting]
const actionButtons = (
<Stack>
<Button onClick={() => formRef.current?.requestSubmit()} variant="primary" size="sm" disabled={isSubmitting}>
Save
</Button>
<LinkButton
disabled={isSubmitting}
href={makeAMLink('alerting/notifications', alertmanager, {
tab: ContactPointsActiveTabs.NotificationTemplates,
})}
variant="secondary"
size="sm"
>
Cancel
</LinkButton>
</Stack>
);
usePageToolbar(actionButtons);
return (
<>
<FormProvider {...formApi}>
{!runtimeConfig.featureToggles.singleTopNav && <AppChromeUpdate actions={actionButtons} />}
<AppChromeUpdate actions={actionButtons} />
<form onSubmit={handleSubmit(submit)} ref={formRef} className={styles.form} aria-label="Template form">
{/* error message */}
{error && (

@ -1,5 +1,5 @@
import { css } from '@emotion/css';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { FormProvider, SubmitErrorHandler, useForm, UseFormWatch } from 'react-hook-form';
import { useParams } from 'react-router-dom-v5-compat';
@ -7,7 +7,6 @@ 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';
@ -136,66 +135,52 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => {
};
// @todo why is error not propagated to form?
const submit = useCallback(
async (values: RuleFormValues, exitOnSave: boolean) => {
if (conditionErrorMsg !== '') {
notifyApp.error(conditionErrorMsg);
return;
}
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
);
}
trackAlertRuleFormSaved({ formAction: existing ? 'update' : 'create', ruleType: values.type });
const { dataSourceName, namespaceName, groupName } = ruleGroupIdentifier;
if (exitOnSave) {
const returnTo = queryParams.get('returnTo') || getReturnToUrl(ruleGroupIdentifier, ruleDefinition);
const ruleDefinition = grafanaTypeRule ? formValuesToRulerGrafanaRuleDTO(values) : formValuesToRulerRuleDTO(values);
locationService.push(returnTo);
return;
}
const ruleGroupIdentifier = existing
? getRuleGroupLocationFromRuleWithLocation(existing)
: getRuleGroupLocationFromFormValues(values);
// 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,
]
);
// @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 deleteRule = async () => {
if (existing) {
@ -208,80 +193,73 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => {
}
};
const onInvalid: SubmitErrorHandler<RuleFormValues> = 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 onInvalid: SubmitErrorHandler<RuleFormValues> = (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 cancelRuleCreation = useCallback(() => {
const cancelRuleCreation = () => {
logInfo(LogMessages.cancelSavingAlertRule);
trackAlertRuleFormCancelled({ formAction: existing ? 'update' : 'create' });
locationService.getHistory().goBack();
}, [existing]);
};
const evaluateEveryInForm = watch('evaluateEvery');
useEffect(() => setEvaluateEvery(evaluateEveryInForm), [evaluateEveryInForm]);
const actionButtons = useMemo(
() => (
<Stack justifyContent="flex-end" alignItems="center">
{existing && (
<Button
data-testid="save-rule"
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>
)}
const actionButtons = (
<Stack justifyContent="flex-end" alignItems="center">
{existing && (
<Button
data-testid="save-rule-and-exit"
data-testid="save-rule"
variant="primary"
type="button"
size="sm"
onClick={handleSubmit((values) => submit(values, true), onInvalid)}
onClick={handleSubmit((values) => submit(values, false), onInvalid)}
disabled={isSubmitting}
>
{isSubmitting && <Spinner className={styles.buttonSpinner} inline={true} />}
Save rule and exit
Save rule
</Button>
)}
<Button
data-testid="save-rule-and-exit"
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 variant="secondary" disabled={isSubmitting} type="button" onClick={cancelRuleCreation} size="sm">
Cancel
) : null}
{existing && isCortexLokiOrRecordingRule(watch) && (
<Button
variant="secondary"
type="button"
onClick={() => setShowEditYaml(true)}
disabled={isSubmitting}
size="sm"
>
Edit YAML
</Button>
{existing ? (
<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]
)}
</Stack>
);
usePageToolbar(actionButtons);
const isPaused = existing && isGrafanaRulerRule(existing.rule) && isGrafanaRulerRulePaused(existing.rule);
if (!type) {
@ -289,7 +267,7 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => {
}
return (
<FormProvider {...formAPI}>
{!config.featureToggles.singleTopNav && <AppChromeUpdate actions={actionButtons} />}
<AppChromeUpdate actions={actionButtons} />
<form onSubmit={(e) => e.preventDefault()} className={styles.form}>
<div className={styles.contentOuter}>
{isPaused && <InfoPausedRule />}

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

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

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

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

@ -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, ToolbarActions } from '../scene/NavToolbarActions';
import { NavToolbarActions } from '../scene/NavToolbarActions';
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
import { getDashboardSceneFor } from '../utils/utils';
@ -177,16 +177,10 @@ 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 (
<Page
navModel={navModel}
pageNav={pageNav}
layout={PageLayoutType.Standard}
toolbar={isSingleTopNav ? <ToolbarActions dashboard={dashboard} /> : undefined}
>
{!isSingleTopNav && <NavToolbarActions dashboard={dashboard} />}
<Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Standard}>
<NavToolbarActions dashboard={dashboard} />
<div style={{ maxWidth: '600px' }}>
<Box marginBottom={5}>
<Field

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

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

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

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

@ -16,6 +16,7 @@ import {
Badge,
} from '@grafana/ui';
import { updateNavIndex } from 'app/core/actions';
import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate';
import { NavToolbarSeparator } from 'app/core/components/AppChrome/NavToolbar/NavToolbarSeparator';
import config from 'app/core/config';
import { useAppNotification } from 'app/core/copy/appNotification';
@ -82,7 +83,6 @@ export const DashNav = memo<Props>((props) => {
// this ensures the component rerenders when the location changes
useLocation();
const forceUpdate = useForceUpdate();
const isSingleTopNav = config.featureToggles.singleTopNav;
// We don't really care about the event payload here only that it triggeres a re-render of this component
useBusEvent(props.dashboard.events, DashboardMetaChangedEvent);
@ -357,11 +357,15 @@ export const DashNav = memo<Props>((props) => {
};
return (
<>
{renderLeftActions()}
{!isSingleTopNav && <NavToolbarSeparator leftActionsSeparator />}
<ToolbarButtonRow alignment="right">{renderRightActions()}</ToolbarButtonRow>
</>
<AppChromeUpdate
actions={
<>
{renderLeftActions()}
<NavToolbarSeparator leftActionsSeparator />
<ToolbarButtonRow alignment="right">{renderRightActions()}</ToolbarButtonRow>
</>
}
/>
);
});

@ -5,12 +5,12 @@ import { AccessControlAction } from 'app/types';
import { SettingsPageProps } from '../DashboardSettings/types';
export const AccessControlDashboardPermissions = ({ dashboard, sectionNav, toolbar }: SettingsPageProps) => {
export const AccessControlDashboardPermissions = ({ dashboard, sectionNav }: SettingsPageProps) => {
const canSetPermissions = contextSrv.hasPermission(AccessControlAction.DashboardsPermissionsWrite);
const pageNav = sectionNav.node.parentItem;
return (
<Page navModel={sectionNav} pageNav={pageNav} toolbar={toolbar}>
<Page navModel={sectionNav} pageNav={pageNav}>
<Permissions resource={'dashboards'} resourceId={dashboard.uid} canSetPermissions={canSetPermissions} />
</Page>
);

@ -7,7 +7,7 @@ import { AnnotationSettingsEdit, AnnotationSettingsList, newAnnotationName } fro
import { SettingsPageProps } from './types';
export function AnnotationsSettings({ dashboard, editIndex, sectionNav, toolbar }: SettingsPageProps) {
export function AnnotationsSettings({ dashboard, editIndex, sectionNav }: SettingsPageProps) {
const onNew = () => {
const newAnnotation: AnnotationQuery = {
name: newAnnotationName,
@ -27,7 +27,7 @@ export function AnnotationsSettings({ dashboard, editIndex, sectionNav, toolbar
const isEditing = editIndex != null && editIndex < dashboard.annotations.list.length;
return (
<Page toolbar={toolbar} navModel={sectionNav} pageNav={getSubPageNav(dashboard, editIndex, sectionNav.node)}>
<Page navModel={sectionNav} pageNav={getSubPageNav(dashboard, editIndex, sectionNav.node)}>
{!isEditing && <AnnotationSettingsList dashboard={dashboard} onNew={onNew} onEdit={onEdit} />}
{isEditing && <AnnotationSettingsEdit dashboard={dashboard} editIdx={editIndex!} />}
</Page>

@ -4,7 +4,7 @@ import { useLocation } from 'react-router-dom-v5-compat';
import { locationUtil, NavModel, NavModelItem } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { config, locationService } from '@grafana/runtime';
import { locationService } from '@grafana/runtime';
import { Button, Stack, Text, ToolbarButtonRow } from '@grafana/ui';
import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate';
import { Page } from 'app/core/components/Page/Page';
@ -36,7 +36,6 @@ const onClose = () => locationService.partial({ editview: null, editIndex: null
export function DashboardSettings({ dashboard, editview, pageNav, sectionNav }: Props) {
const [updateId, setUpdateId] = useState(0);
const isSingleTopNav = config.featureToggles.singleTopNav;
useEffect(() => {
dashboard.events.subscribe(DashboardMetaChangedEvent, () => setUpdateId((v) => v + 1));
}, [dashboard]);
@ -82,15 +81,8 @@ export function DashboardSettings({ dashboard, editview, pageNav, sectionNav }:
return (
<>
{!isSingleTopNav && (
<AppChromeUpdate actions={<ToolbarButtonRow alignment="right">{actions}</ToolbarButtonRow>} />
)}
<currentPage.component
toolbar={isSingleTopNav ? <ToolbarButtonRow alignment="right">{actions}</ToolbarButtonRow> : undefined}
sectionNav={subSectionNav}
dashboard={dashboard}
editIndex={editIndex}
/>
<AppChromeUpdate actions={<ToolbarButtonRow alignment="right">{actions}</ToolbarButtonRow>} />
<currentPage.component sectionNav={subSectionNav} dashboard={dashboard} editIndex={editIndex} />
</>
);
}
@ -217,9 +209,9 @@ function getSectionNav(
};
}
function MakeEditable({ dashboard, sectionNav, toolbar }: SettingsPageProps) {
function MakeEditable({ dashboard, sectionNav }: SettingsPageProps) {
return (
<Page navModel={sectionNav} toolbar={toolbar}>
<Page navModel={sectionNav}>
<Stack direction="column" gap={2} alignItems="flex-start">
<Text variant="h3">Dashboard not editable</Text>
<Button type="submit" onClick={() => dashboard.makeEditable()}>

@ -39,7 +39,6 @@ export function GeneralSettingsUnconnected({
updateTimeZone,
updateWeekStart,
sectionNav,
toolbar,
}: Props): JSX.Element {
const [renderCounter, setRenderCounter] = useState(0);
const [dashboardTitle, setDashboardTitle] = useState(dashboard.title);
@ -120,7 +119,7 @@ export function GeneralSettingsUnconnected({
];
return (
<Page navModel={sectionNav} pageNav={pageNav} toolbar={toolbar}>
<Page navModel={sectionNav} pageNav={pageNav}>
<div style={{ maxWidth: '600px' }}>
<Box marginBottom={5}>
<Field

@ -11,7 +11,7 @@ import { getDashboardSrv } from '../../services/DashboardSrv';
import { SettingsPageProps } from './types';
export function JsonEditorSettings({ dashboard, sectionNav, toolbar }: SettingsPageProps) {
export function JsonEditorSettings({ dashboard, sectionNav }: SettingsPageProps) {
const dashboardSaveModel = dashboard.getSaveModelClone();
const [dashboardJson, setDashboardJson] = useState<string>(JSON.stringify(dashboardSaveModel, null, 2));
const pageNav = sectionNav.node.parentItem;
@ -24,7 +24,7 @@ export function JsonEditorSettings({ dashboard, sectionNav, toolbar }: SettingsP
const styles = useStyles2(getStyles);
return (
<Page navModel={sectionNav} pageNav={pageNav} toolbar={toolbar}>
<Page navModel={sectionNav} pageNav={pageNav}>
<div className={styles.wrapper}>
<Trans i18nKey="dashboard-settings.json-editor.subtitle">
The JSON model below is the data structure that defines the dashboard. This includes dashboard settings, panel

@ -10,7 +10,7 @@ import { SettingsPageProps } from './types';
export type LinkSettingsMode = 'list' | 'new' | 'edit';
export function LinksSettings({ dashboard, sectionNav, editIndex, toolbar }: SettingsPageProps) {
export function LinksSettings({ dashboard, sectionNav, editIndex }: SettingsPageProps) {
const [isNew, setIsNew] = useState<boolean>(false);
const onGoBack = () => {
@ -44,7 +44,7 @@ export function LinksSettings({ dashboard, sectionNav, editIndex, toolbar }: Set
}
return (
<Page navModel={sectionNav} pageNav={pageNav} toolbar={toolbar}>
<Page navModel={sectionNav} pageNav={pageNav}>
{!isEditing && <LinkSettingsList dashboard={dashboard} onNew={onNew} onEdit={onEdit} />}
{isEditing && <LinkSettingsEdit dashboard={dashboard} editLinkIdx={editIndex} onGoBack={onGoBack} />}
</Page>

@ -143,7 +143,7 @@ export class VersionsSettings extends PureComponent<Props, State> {
if (viewMode === 'compare') {
return (
<Page navModel={this.props.sectionNav} pageNav={pageNav} toolbar={this.props.toolbar}>
<Page navModel={this.props.sectionNav} pageNav={pageNav}>
<VersionHistoryHeader
onClick={this.reset}
baseVersion={baseInfo?.version}
@ -165,7 +165,7 @@ export class VersionsSettings extends PureComponent<Props, State> {
}
return (
<Page navModel={this.props.sectionNav} pageNav={pageNav} toolbar={this.props.toolbar}>
<Page navModel={this.props.sectionNav} pageNav={pageNav}>
{isLoading ? (
<VersionsHistorySpinner msg="Fetching history list&hellip;" />
) : (

@ -1,4 +1,4 @@
import { ComponentType, ReactNode } from 'react';
import { ComponentType } from 'react';
import { NavModel } from '@grafana/data';
import { IconName } from '@grafana/ui';
@ -17,5 +17,4 @@ export interface SettingsPageProps {
dashboard: DashboardModel;
sectionNav: NavModel;
editIndex?: number;
toolbar?: ReactNode;
}

@ -6,7 +6,7 @@ import { Subscription } from 'rxjs';
import { FieldConfigSource, GrafanaTheme2, NavModel, NavModelItem, PageLayoutType } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { config, locationService } from '@grafana/runtime';
import { locationService } from '@grafana/runtime';
import {
Button,
HorizontalGroup,
@ -432,7 +432,6 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
render() {
const { initDone, uiState, theme, sectionNav, pageNav, className, updatePanelEditorUIState } = this.props;
const isSingleTopNav = config.featureToggles.singleTopNav;
const styles = getStyles(theme, this.props);
if (!initDone) {
@ -446,17 +445,10 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
data-testid={selectors.components.PanelEditor.General.content}
layout={PageLayoutType.Custom}
className={className}
toolbar={
isSingleTopNav ? (
<ToolbarButtonRow alignment="right">{this.renderEditorActions()}</ToolbarButtonRow>
) : undefined
}
>
{!isSingleTopNav && (
<AppChromeUpdate
actions={<ToolbarButtonRow alignment="right">{this.renderEditorActions()}</ToolbarButtonRow>}
/>
)}
<AppChromeUpdate
actions={<ToolbarButtonRow alignment="right">{this.renderEditorActions()}</ToolbarButtonRow>}
/>
<div className={styles.wrapper}>
<div className={styles.verticalSplitPanesWrapper}>
{!uiState.isPanelOptionsVisible ? (

@ -7,7 +7,6 @@ import { selectors } from '@grafana/e2e-selectors';
import { config, locationService } from '@grafana/runtime';
import { Themeable2, withTheme2 } from '@grafana/ui';
import { notifyApp } from 'app/core/actions';
import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate';
import { ScrollRefElement } from 'app/core/components/NativeScrollbar';
import { Page } from 'app/core/components/Page/Page';
import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound';
@ -362,7 +361,6 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
const { editPanel, viewPanel, pageNav, sectionNav } = this.state;
const kioskMode = getKioskMode(this.props.queryParams);
const styles = getStyles(theme);
const isSingleTopNav = config.featureToggles.singleTopNav;
if (!dashboard || !pageNav || !sectionNav) {
return <DashboardLoading initPhase={this.props.initPhase} />;
@ -438,8 +436,9 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
layout={PageLayoutType.Canvas}
className={pageClassName}
onSetScrollRef={this.setScrollRef}
toolbar={
isSingleTopNav ? (
>
{showToolbar && (
<header data-testid={selectors.pages.Dashboard.DashNav.navV2}>
<DashNav
dashboard={dashboard}
title={dashboard.title}
@ -448,23 +447,6 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
kioskMode={kioskMode}
hideTimePicker={dashboard.timepicker.hidden}
/>
) : undefined
}
>
{showToolbar && (
<header data-testid={selectors.pages.Dashboard.DashNav.navV2}>
<AppChromeUpdate
actions={
<DashNav
dashboard={dashboard}
title={dashboard.title}
folderTitle={dashboard.meta.folderTitle}
isFullscreen={!!viewPanel}
kioskMode={kioskMode}
hideTimePicker={dashboard.timepicker.hidden}
/>
}
/>
</header>
)}
<DashboardPrompt dashboard={dashboard} />

@ -105,14 +105,14 @@ class VariableEditorContainerUnconnected extends PureComponent<Props, State> {
};
render() {
const { editIndex, variables, sectionNav, toolbar } = this.props;
const { editIndex, variables, sectionNav } = this.props;
const variableToEdit = editIndex != null ? variables[editIndex] : undefined;
const node = sectionNav.node;
const parentItem = node.parentItem;
const subPageNav = variableToEdit ? { text: variableToEdit.name, parentItem } : parentItem;
return (
<Page toolbar={toolbar} navModel={this.props.sectionNav} pageNav={subPageNav}>
<Page navModel={this.props.sectionNav} pageNav={subPageNav}>
{!variableToEdit && (
<VariableEditorList
variables={this.props.variables}

Loading…
Cancel
Save