mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -7,8 +7,7 @@ export interface AppChromeUpdateProps {
|
|||||||
actions?: React.ReactNode;
|
actions?: React.ReactNode;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* This needs to be moved to @grafana/ui or runtime.
|
* @deprecated This component is deprecated and will be removed in a future release.
|
||||||
* This is the way core pages and plugins update the breadcrumbs and page toolbar actions
|
|
||||||
*/
|
*/
|
||||||
export const AppChromeUpdate = React.memo<AppChromeUpdateProps>(({ actions }: AppChromeUpdateProps) => {
|
export const AppChromeUpdate = React.memo<AppChromeUpdateProps>(({ actions }: AppChromeUpdateProps) => {
|
||||||
const { chrome } = useGrafana();
|
const { chrome } = useGrafana();
|
||||||
|
|||||||
@@ -1,19 +1,58 @@
|
|||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import { useLayoutEffect } from 'react';
|
import {
|
||||||
|
createContext,
|
||||||
|
Dispatch,
|
||||||
|
ReactNode,
|
||||||
|
SetStateAction,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useLayoutEffect,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme2, PageLayoutType } from '@grafana/data';
|
import { GrafanaTheme2, PageLayoutType } from '@grafana/data';
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
import { useStyles2 } from '@grafana/ui';
|
import { useStyles2 } from '@grafana/ui';
|
||||||
import { useGrafana } from 'app/core/context/GrafanaContext';
|
import { useGrafana } from 'app/core/context/GrafanaContext';
|
||||||
|
|
||||||
|
import { TOP_BAR_LEVEL_HEIGHT } from '../AppChrome/types';
|
||||||
import NativeScrollbar from '../NativeScrollbar';
|
import NativeScrollbar from '../NativeScrollbar';
|
||||||
|
|
||||||
import { PageContents } from './PageContents';
|
import { PageContents } from './PageContents';
|
||||||
import { PageHeader } from './PageHeader';
|
import { PageHeader } from './PageHeader';
|
||||||
import { PageTabs } from './PageTabs';
|
import { PageTabs } from './PageTabs';
|
||||||
|
import { PageToolbarActions } from './PageToolbarActions';
|
||||||
import { PageType } from './types';
|
import { PageType } from './types';
|
||||||
import { usePageNav } from './usePageNav';
|
import { usePageNav } from './usePageNav';
|
||||||
import { usePageTitle } from './usePageTitle';
|
import { usePageTitle } from './usePageTitle';
|
||||||
|
|
||||||
|
export interface PageContextType {
|
||||||
|
setToolbar: Dispatch<SetStateAction<ReactNode>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PageContext = createContext<PageContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
function usePageContext(): PageContextType {
|
||||||
|
const context = useContext(PageContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('No PageContext found');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook to dynamically set the toolbar of a Page from a child component.
|
||||||
|
* Prefer setting the toolbar directly as a prop to Page.
|
||||||
|
* @param toolbar a ReactNode that will be rendered in a second toolbar
|
||||||
|
*/
|
||||||
|
export function usePageToolbar(toolbar?: ReactNode) {
|
||||||
|
const { setToolbar } = usePageContext();
|
||||||
|
useEffect(() => {
|
||||||
|
setToolbar(toolbar);
|
||||||
|
return () => setToolbar(undefined);
|
||||||
|
}, [setToolbar, toolbar]);
|
||||||
|
}
|
||||||
|
|
||||||
export const Page: PageType = ({
|
export const Page: PageType = ({
|
||||||
navId,
|
navId,
|
||||||
navModel: oldNavProp,
|
navModel: oldNavProp,
|
||||||
@@ -24,12 +63,15 @@ export const Page: PageType = ({
|
|||||||
subTitle,
|
subTitle,
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
|
toolbar: toolbarProp,
|
||||||
info,
|
info,
|
||||||
layout = PageLayoutType.Standard,
|
layout = PageLayoutType.Standard,
|
||||||
onSetScrollRef,
|
onSetScrollRef,
|
||||||
...otherProps
|
...otherProps
|
||||||
}) => {
|
}) => {
|
||||||
const styles = useStyles2(getStyles);
|
const isSingleTopNav = config.featureToggles.singleTopNav;
|
||||||
|
const [toolbar, setToolbar] = useState(toolbarProp);
|
||||||
|
const styles = useStyles2(getStyles, Boolean(isSingleTopNav && toolbar));
|
||||||
const navModel = usePageNav(navId, oldNavProp);
|
const navModel = usePageNav(navId, oldNavProp);
|
||||||
const { chrome } = useGrafana();
|
const { chrome } = useGrafana();
|
||||||
|
|
||||||
@@ -50,54 +92,58 @@ export const Page: PageType = ({
|
|||||||
}, [navModel, pageNav, chrome, layout]);
|
}, [navModel, pageNav, chrome, layout]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cx(styles.wrapper, className)} {...otherProps}>
|
<PageContext.Provider value={{ setToolbar }}>
|
||||||
{layout === PageLayoutType.Standard && (
|
<div className={cx(styles.wrapper, className)} {...otherProps}>
|
||||||
<NativeScrollbar
|
{isSingleTopNav && toolbar && <PageToolbarActions>{toolbar}</PageToolbarActions>}
|
||||||
// This id is used by the image renderer to scroll through the dashboard
|
{layout === PageLayoutType.Standard && (
|
||||||
divId="page-scrollbar"
|
<NativeScrollbar
|
||||||
onSetScrollRef={onSetScrollRef}
|
// This id is used by the image renderer to scroll through the dashboard
|
||||||
>
|
divId="page-scrollbar"
|
||||||
<div className={styles.pageInner}>
|
onSetScrollRef={onSetScrollRef}
|
||||||
{pageHeaderNav && (
|
>
|
||||||
<PageHeader
|
<div className={styles.pageInner}>
|
||||||
actions={actions}
|
{pageHeaderNav && (
|
||||||
onEditTitle={onEditTitle}
|
<PageHeader
|
||||||
navItem={pageHeaderNav}
|
actions={actions}
|
||||||
renderTitle={renderTitle}
|
onEditTitle={onEditTitle}
|
||||||
info={info}
|
navItem={pageHeaderNav}
|
||||||
subTitle={subTitle}
|
renderTitle={renderTitle}
|
||||||
/>
|
info={info}
|
||||||
)}
|
subTitle={subTitle}
|
||||||
{pageNav && pageNav.children && <PageTabs navItem={pageNav} />}
|
/>
|
||||||
<div className={styles.pageContent}>{children}</div>
|
)}
|
||||||
</div>
|
{pageNav && pageNav.children && <PageTabs navItem={pageNav} />}
|
||||||
</NativeScrollbar>
|
<div className={styles.pageContent}>{children}</div>
|
||||||
)}
|
</div>
|
||||||
|
</NativeScrollbar>
|
||||||
|
)}
|
||||||
|
|
||||||
{layout === PageLayoutType.Canvas && (
|
{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({
|
||||||
|
|||||||
47
public/app/core/components/Page/PageToolbarActions.tsx
Normal file
47
public/app/core/components/Page/PageToolbarActions.tsx
Normal 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,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -25,6 +25,8 @@ export interface PageProps extends HTMLAttributes<HTMLDivElement> {
|
|||||||
layout?: PageLayoutType;
|
layout?: PageLayoutType;
|
||||||
/** Can be used to get the scroll container element to access scroll position */
|
/** Can be used to get the scroll container element to access scroll position */
|
||||||
onSetScrollRef?: (ref: ScrollRefElement) => void;
|
onSetScrollRef?: (ref: ScrollRefElement) => void;
|
||||||
|
/** Set a page-level toolbar */
|
||||||
|
toolbar?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PageInfoItem {
|
export interface PageInfoItem {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { byRole, byTestId, byText } from 'testing-library-selector';
|
|||||||
|
|
||||||
import { selectors } from '@grafana/e2e-selectors/src';
|
import { selectors } from '@grafana/e2e-selectors/src';
|
||||||
import { setDataSourceSrv } from '@grafana/runtime';
|
import { setDataSourceSrv } from '@grafana/runtime';
|
||||||
|
import { PageContext } from 'app/core/components/Page/Page';
|
||||||
import { DashboardSearchItem, DashboardSearchItemType } from 'app/features/search/types';
|
import { DashboardSearchItem, DashboardSearchItemType } from 'app/features/search/types';
|
||||||
import { RuleWithLocation } from 'app/types/unified-alerting';
|
import { RuleWithLocation } from 'app/types/unified-alerting';
|
||||||
|
|
||||||
@@ -72,7 +73,9 @@ function Wrapper({ children }: React.PropsWithChildren<{}>) {
|
|||||||
const formApi = useForm<RuleFormValues>({ defaultValues: getDefaultFormValues() });
|
const formApi = useForm<RuleFormValues>({ defaultValues: getDefaultFormValues() });
|
||||||
return (
|
return (
|
||||||
<Providers>
|
<Providers>
|
||||||
<FormProvider {...formApi}>{children}</FormProvider>
|
<PageContext.Provider value={{ setToolbar: jest.fn() }}>
|
||||||
|
<FormProvider {...formApi}>{children}</FormProvider>
|
||||||
|
</PageContext.Provider>
|
||||||
</Providers>
|
</Providers>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import { addMinutes, subDays, subHours } from 'date-fns';
|
import { addMinutes, subDays, subHours } from 'date-fns';
|
||||||
import { Location } from 'history';
|
import { Location } from 'history';
|
||||||
import { useRef, useState } from 'react';
|
import { useMemo, useRef, useState } from 'react';
|
||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { useToggle } from 'react-use';
|
import { useToggle } from 'react-use';
|
||||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { isFetchError, locationService } from '@grafana/runtime';
|
import { config as runtimeConfig, isFetchError, locationService } from '@grafana/runtime';
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Button,
|
Button,
|
||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
InlineField,
|
InlineField,
|
||||||
Box,
|
Box,
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
|
import { usePageToolbar } from 'app/core/components/Page/Page';
|
||||||
import { useAppNotification } from 'app/core/copy/appNotification';
|
import { useAppNotification } from 'app/core/copy/appNotification';
|
||||||
import { useCleanup } from 'app/core/hooks/useCleanup';
|
import { useCleanup } from 'app/core/hooks/useCleanup';
|
||||||
import { ActiveTab as ContactPointsActiveTabs } from 'app/features/alerting/unified/components/contact-points/ContactPoints';
|
import { ActiveTab as ContactPointsActiveTabs } from 'app/features/alerting/unified/components/contact-points/ContactPoints';
|
||||||
@@ -157,28 +158,33 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props)
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const actionButtons = (
|
const actionButtons = useMemo(
|
||||||
<Stack>
|
() => (
|
||||||
<Button onClick={() => formRef.current?.requestSubmit()} variant="primary" size="sm" disabled={isSubmitting}>
|
<Stack>
|
||||||
Save
|
<Button onClick={() => formRef.current?.requestSubmit()} variant="primary" size="sm" disabled={isSubmitting}>
|
||||||
</Button>
|
Save
|
||||||
<LinkButton
|
</Button>
|
||||||
disabled={isSubmitting}
|
<LinkButton
|
||||||
href={makeAMLink('alerting/notifications', alertmanager, {
|
disabled={isSubmitting}
|
||||||
tab: ContactPointsActiveTabs.NotificationTemplates,
|
href={makeAMLink('alerting/notifications', alertmanager, {
|
||||||
})}
|
tab: ContactPointsActiveTabs.NotificationTemplates,
|
||||||
variant="secondary"
|
})}
|
||||||
size="sm"
|
variant="secondary"
|
||||||
>
|
size="sm"
|
||||||
Cancel
|
>
|
||||||
</LinkButton>
|
Cancel
|
||||||
</Stack>
|
</LinkButton>
|
||||||
|
</Stack>
|
||||||
|
),
|
||||||
|
[alertmanager, isSubmitting]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
usePageToolbar(actionButtons);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FormProvider {...formApi}>
|
<FormProvider {...formApi}>
|
||||||
<AppChromeUpdate actions={actionButtons} />
|
{!runtimeConfig.featureToggles.singleTopNav && <AppChromeUpdate actions={actionButtons} />}
|
||||||
<form onSubmit={handleSubmit(submit)} ref={formRef} className={styles.form} aria-label="Template form">
|
<form onSubmit={handleSubmit(submit)} ref={formRef} className={styles.form} aria-label="Template form">
|
||||||
{/* error message */}
|
{/* error message */}
|
||||||
{error && (
|
{error && (
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { FormProvider, SubmitErrorHandler, UseFormWatch, useForm } from 'react-hook-form';
|
import { FormProvider, SubmitErrorHandler, UseFormWatch, useForm } from 'react-hook-form';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
@@ -7,6 +7,7 @@ import { GrafanaTheme2 } from '@grafana/data';
|
|||||||
import { config, locationService } from '@grafana/runtime';
|
import { config, locationService } from '@grafana/runtime';
|
||||||
import { Button, ConfirmModal, CustomScrollbar, Spinner, Stack, useStyles2 } from '@grafana/ui';
|
import { Button, ConfirmModal, CustomScrollbar, Spinner, Stack, useStyles2 } from '@grafana/ui';
|
||||||
import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate';
|
import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate';
|
||||||
|
import { usePageToolbar } from 'app/core/components/Page/Page';
|
||||||
import { useAppNotification } from 'app/core/copy/appNotification';
|
import { useAppNotification } from 'app/core/copy/appNotification';
|
||||||
import { contextSrv } from 'app/core/core';
|
import { contextSrv } from 'app/core/core';
|
||||||
import InfoPausedRule from 'app/features/alerting/unified/components/InfoPausedRule';
|
import InfoPausedRule from 'app/features/alerting/unified/components/InfoPausedRule';
|
||||||
@@ -133,52 +134,66 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// @todo why is error not propagated to form?
|
// @todo why is error not propagated to form?
|
||||||
const submit = async (values: RuleFormValues, exitOnSave: boolean) => {
|
const submit = useCallback(
|
||||||
if (conditionErrorMsg !== '') {
|
async (values: RuleFormValues, exitOnSave: boolean) => {
|
||||||
notifyApp.error(conditionErrorMsg);
|
if (conditionErrorMsg !== '') {
|
||||||
return;
|
notifyApp.error(conditionErrorMsg);
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
trackAlertRuleFormSaved({ formAction: existing ? 'update' : 'create', ruleType: values.type });
|
trackAlertRuleFormSaved({ formAction: existing ? 'update' : 'create', ruleType: values.type });
|
||||||
|
|
||||||
const ruleDefinition = grafanaTypeRule ? formValuesToRulerGrafanaRuleDTO(values) : formValuesToRulerRuleDTO(values);
|
const ruleDefinition = grafanaTypeRule
|
||||||
|
? 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 />}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
|||||||
import { FormProvider, useForm } from 'react-hook-form';
|
import { FormProvider, useForm } from 'react-hook-form';
|
||||||
import { useAsync } from 'react-use';
|
import { useAsync } from 'react-use';
|
||||||
|
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
import { Button, CustomScrollbar, LinkButton, LoadingPlaceholder, Stack } from '@grafana/ui';
|
import { Button, CustomScrollbar, LinkButton, LoadingPlaceholder, Stack } from '@grafana/ui';
|
||||||
|
import { usePageToolbar } from 'app/core/components/Page/Page';
|
||||||
import { useAppNotification } from 'app/core/copy/appNotification';
|
import { useAppNotification } from 'app/core/copy/appNotification';
|
||||||
import { useQueryParams } from 'app/core/hooks/useQueryParams';
|
import { useQueryParams } from 'app/core/hooks/useQueryParams';
|
||||||
|
|
||||||
@@ -50,39 +52,47 @@ export function ModifyExportRuleForm({ ruleForm, alertUid }: ModifyExportRuleFor
|
|||||||
const [conditionErrorMsg, setConditionErrorMsg] = useState('');
|
const [conditionErrorMsg, setConditionErrorMsg] = useState('');
|
||||||
const [evaluateEvery, setEvaluateEvery] = useState(ruleForm?.evaluateEvery ?? DEFAULT_GROUP_EVALUATION_INTERVAL);
|
const [evaluateEvery, setEvaluateEvery] = useState(ruleForm?.evaluateEvery ?? DEFAULT_GROUP_EVALUATION_INTERVAL);
|
||||||
|
|
||||||
const onInvalid = (): void => {
|
const onInvalid = useCallback((): void => {
|
||||||
notifyApp.error('There are errors in the form. Please correct them and try again!');
|
notifyApp.error('There are errors in the form. Please correct them and try again!');
|
||||||
};
|
}, [notifyApp]);
|
||||||
|
|
||||||
const checkAlertCondition = (msg = '') => {
|
const checkAlertCondition = (msg = '') => {
|
||||||
setConditionErrorMsg(msg);
|
setConditionErrorMsg(msg);
|
||||||
};
|
};
|
||||||
|
|
||||||
const submit = (exportData: RuleFormValues | undefined) => {
|
const submit = useCallback(
|
||||||
if (conditionErrorMsg !== '') {
|
(exportData: RuleFormValues | undefined) => {
|
||||||
notifyApp.error(conditionErrorMsg);
|
if (conditionErrorMsg !== '') {
|
||||||
return;
|
notifyApp.error(conditionErrorMsg);
|
||||||
}
|
return;
|
||||||
setExportData(exportData);
|
}
|
||||||
};
|
setExportData(exportData);
|
||||||
|
},
|
||||||
|
[conditionErrorMsg, notifyApp]
|
||||||
|
);
|
||||||
|
|
||||||
const onClose = useCallback(() => {
|
const onClose = useCallback(() => {
|
||||||
setExportData(undefined);
|
setExportData(undefined);
|
||||||
}, [setExportData]);
|
}, [setExportData]);
|
||||||
|
|
||||||
const actionButtons = [
|
const actionButtons = useMemo(
|
||||||
<LinkButton href={returnTo} key="cancel" size="sm" variant="secondary" onClick={() => submit(undefined)}>
|
() => [
|
||||||
Cancel
|
<LinkButton href={returnTo} key="cancel" size="sm" variant="secondary" onClick={() => submit(undefined)}>
|
||||||
</LinkButton>,
|
Cancel
|
||||||
<Button key="export-rule" size="sm" onClick={formAPI.handleSubmit((formValues) => submit(formValues), onInvalid)}>
|
</LinkButton>,
|
||||||
Export
|
<Button key="export-rule" size="sm" onClick={formAPI.handleSubmit((formValues) => submit(formValues), onInvalid)}>
|
||||||
</Button>,
|
Export
|
||||||
];
|
</Button>,
|
||||||
|
],
|
||||||
|
[formAPI, onInvalid, returnTo, submit]
|
||||||
|
);
|
||||||
|
|
||||||
|
usePageToolbar(actionButtons);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FormProvider {...formAPI}>
|
<FormProvider {...formAPI}>
|
||||||
<AppChromeUpdate actions={actionButtons} />
|
{!config.featureToggles.singleTopNav && <AppChromeUpdate actions={actionButtons} />}
|
||||||
<form onSubmit={(e) => e.preventDefault()}>
|
<form onSubmit={(e) => e.preventDefault()}>
|
||||||
<div>
|
<div>
|
||||||
<CustomScrollbar autoHeightMin="100%" hideHorizontalTrack={true}>
|
<CustomScrollbar autoHeightMin="100%" hideHorizontalTrack={true}>
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ import { useEffect, useMemo } from 'react';
|
|||||||
import { useLocation } from 'react-router-dom-v5-compat';
|
import { useLocation } from 'react-router-dom-v5-compat';
|
||||||
|
|
||||||
import { GrafanaTheme2, PageLayoutType } from '@grafana/data';
|
import { GrafanaTheme2, PageLayoutType } from '@grafana/data';
|
||||||
import { useChromeHeaderHeight } from '@grafana/runtime';
|
import { config, useChromeHeaderHeight } from '@grafana/runtime';
|
||||||
import { SceneComponentProps } from '@grafana/scenes';
|
import { SceneComponentProps } from '@grafana/scenes';
|
||||||
import { useStyles2 } from '@grafana/ui';
|
import { useStyles2 } from '@grafana/ui';
|
||||||
|
import { TOP_BAR_LEVEL_HEIGHT } from 'app/core/components/AppChrome/types';
|
||||||
import NativeScrollbar from 'app/core/components/NativeScrollbar';
|
import NativeScrollbar from 'app/core/components/NativeScrollbar';
|
||||||
import { Page } from 'app/core/components/Page/Page';
|
import { Page } from 'app/core/components/Page/Page';
|
||||||
import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound';
|
import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound';
|
||||||
@@ -14,7 +15,7 @@ import DashboardEmpty from 'app/features/dashboard/dashgrid/DashboardEmpty';
|
|||||||
import { useSelector } from 'app/types';
|
import { useSelector } from 'app/types';
|
||||||
|
|
||||||
import { DashboardScene } from './DashboardScene';
|
import { DashboardScene } from './DashboardScene';
|
||||||
import { NavToolbarActions } from './NavToolbarActions';
|
import { NavToolbarActions, ToolbarActions } from './NavToolbarActions';
|
||||||
import { PanelSearchLayout } from './PanelSearchLayout';
|
import { PanelSearchLayout } from './PanelSearchLayout';
|
||||||
import { DashboardAngularDeprecationBanner } from './angular/DashboardAngularDeprecationBanner';
|
import { DashboardAngularDeprecationBanner } from './angular/DashboardAngularDeprecationBanner';
|
||||||
|
|
||||||
@@ -22,7 +23,7 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardS
|
|||||||
const { controls, overlay, editview, editPanel, isEmpty, meta, viewPanelScene, panelSearch, panelsPerRow } =
|
const { controls, overlay, editview, editPanel, isEmpty, meta, viewPanelScene, panelSearch, panelsPerRow } =
|
||||||
model.useState();
|
model.useState();
|
||||||
const headerHeight = useChromeHeaderHeight();
|
const headerHeight = useChromeHeaderHeight();
|
||||||
const styles = useStyles2(getStyles, headerHeight);
|
const styles = useStyles2(getStyles, headerHeight ?? 0);
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navIndex = useSelector((state) => state.navIndex);
|
const navIndex = useSelector((state) => state.navIndex);
|
||||||
const pageNav = model.getPageNav(location, navIndex);
|
const pageNav = model.getPageNav(location, navIndex);
|
||||||
@@ -30,6 +31,7 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardS
|
|||||||
const navModel = getNavModel(navIndex, 'dashboards/browse');
|
const navModel = getNavModel(navIndex, 'dashboards/browse');
|
||||||
const hasControls = controls?.hasControls();
|
const hasControls = controls?.hasControls();
|
||||||
const isSettingsOpen = editview !== undefined;
|
const isSettingsOpen = editview !== undefined;
|
||||||
|
const isSingleTopNav = config.featureToggles.singleTopNav;
|
||||||
|
|
||||||
// Remember scroll pos when going into view panel, edit panel or settings
|
// Remember scroll pos when going into view panel, edit panel or settings
|
||||||
useMemo(() => {
|
useMemo(() => {
|
||||||
@@ -79,12 +81,17 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardS
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Custom}>
|
<Page
|
||||||
|
navModel={navModel}
|
||||||
|
pageNav={pageNav}
|
||||||
|
layout={PageLayoutType.Custom}
|
||||||
|
toolbar={isSingleTopNav ? <ToolbarActions dashboard={model} /> : undefined}
|
||||||
|
>
|
||||||
{editPanel && <editPanel.Component model={editPanel} />}
|
{editPanel && <editPanel.Component model={editPanel} />}
|
||||||
{!editPanel && (
|
{!editPanel && (
|
||||||
<NativeScrollbar divId="page-scrollbar" onSetScrollRef={model.onSetScrollRef}>
|
<NativeScrollbar divId="page-scrollbar" onSetScrollRef={model.onSetScrollRef}>
|
||||||
<div className={cx(styles.pageContainer, hasControls && styles.pageContainerWithControls)}>
|
<div className={cx(styles.pageContainer, hasControls && styles.pageContainerWithControls)}>
|
||||||
<NavToolbarActions dashboard={model} />
|
{!isSingleTopNav && <NavToolbarActions dashboard={model} />}
|
||||||
{controls && (
|
{controls && (
|
||||||
<div className={styles.controlsWrapper}>
|
<div className={styles.controlsWrapper}>
|
||||||
<controls.Component model={controls} />
|
<controls.Component model={controls} />
|
||||||
@@ -99,7 +106,7 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardS
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStyles(theme: GrafanaTheme2, headerHeight: number | undefined) {
|
function getStyles(theme: GrafanaTheme2, headerHeight: number) {
|
||||||
return {
|
return {
|
||||||
pageContainer: css({
|
pageContainer: css({
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
@@ -133,7 +140,7 @@ function getStyles(theme: GrafanaTheme2, headerHeight: number | undefined) {
|
|||||||
position: 'sticky',
|
position: 'sticky',
|
||||||
zIndex: theme.zIndex.activePanel,
|
zIndex: theme.zIndex.activePanel,
|
||||||
background: theme.colors.background.canvas,
|
background: theme.colors.background.canvas,
|
||||||
top: headerHeight,
|
top: config.featureToggles.singleTopNav ? headerHeight + TOP_BAR_LEVEL_HEIGHT : headerHeight,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
canvasContent: css({
|
canvasContent: css({
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { AnnotationQuery, getDataSourceRef, NavModel, NavModelItem, PageLayoutType } from '@grafana/data';
|
import { AnnotationQuery, getDataSourceRef, NavModel, NavModelItem, PageLayoutType } from '@grafana/data';
|
||||||
import { getDataSourceSrv } from '@grafana/runtime';
|
import { config, getDataSourceSrv } from '@grafana/runtime';
|
||||||
import { SceneComponentProps, SceneObjectBase, VizPanel, dataLayers } from '@grafana/scenes';
|
import { SceneComponentProps, SceneObjectBase, VizPanel, dataLayers } from '@grafana/scenes';
|
||||||
import { Page } from 'app/core/components/Page/Page';
|
import { Page } from 'app/core/components/Page/Page';
|
||||||
|
|
||||||
import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer';
|
import { DashboardAnnotationsDataLayer } from '../scene/DashboardAnnotationsDataLayer';
|
||||||
import { DashboardScene } from '../scene/DashboardScene';
|
import { DashboardScene } from '../scene/DashboardScene';
|
||||||
import { NavToolbarActions } from '../scene/NavToolbarActions';
|
import { NavToolbarActions, ToolbarActions } from '../scene/NavToolbarActions';
|
||||||
import { dataLayersToAnnotations } from '../serialization/dataLayersToAnnotations';
|
import { dataLayersToAnnotations } from '../serialization/dataLayersToAnnotations';
|
||||||
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
|
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
|
||||||
import { getDashboardSceneFor } from '../utils/utils';
|
import { getDashboardSceneFor } from '../utils/utils';
|
||||||
@@ -133,6 +133,7 @@ function AnnotationsSettingsView({ model }: SceneComponentProps<AnnotationsEditV
|
|||||||
const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey());
|
const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey());
|
||||||
const { editIndex } = model.useState();
|
const { editIndex } = model.useState();
|
||||||
const panels = dashboardSceneGraph.getVizPanels(dashboard);
|
const panels = dashboardSceneGraph.getVizPanels(dashboard);
|
||||||
|
const isSingleTopNav = config.featureToggles.singleTopNav;
|
||||||
|
|
||||||
const annotations: AnnotationQuery[] = dataLayersToAnnotations(annotationLayers);
|
const annotations: AnnotationQuery[] = dataLayersToAnnotations(annotationLayers);
|
||||||
|
|
||||||
@@ -153,8 +154,13 @@ function AnnotationsSettingsView({ model }: SceneComponentProps<AnnotationsEditV
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Standard}>
|
<Page
|
||||||
<NavToolbarActions dashboard={dashboard} />
|
navModel={navModel}
|
||||||
|
pageNav={pageNav}
|
||||||
|
layout={PageLayoutType.Standard}
|
||||||
|
toolbar={isSingleTopNav ? <ToolbarActions dashboard={dashboard} /> : undefined}
|
||||||
|
>
|
||||||
|
{!isSingleTopNav && <NavToolbarActions dashboard={dashboard} />}
|
||||||
<AnnotationSettingsList
|
<AnnotationSettingsList
|
||||||
annotations={annotations}
|
annotations={annotations}
|
||||||
onNew={model.onNew}
|
onNew={model.onNew}
|
||||||
@@ -190,6 +196,7 @@ function AnnotationsSettingsEditView({
|
|||||||
onDelete,
|
onDelete,
|
||||||
}: AnnotationsSettingsEditViewProps) {
|
}: AnnotationsSettingsEditViewProps) {
|
||||||
const { name, query } = annotationLayer.useState();
|
const { name, query } = annotationLayer.useState();
|
||||||
|
const isSingleTopNav = config.featureToggles.singleTopNav;
|
||||||
|
|
||||||
const editAnnotationPageNav = {
|
const editAnnotationPageNav = {
|
||||||
text: name,
|
text: name,
|
||||||
@@ -197,8 +204,13 @@ function AnnotationsSettingsEditView({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page navModel={navModel} pageNav={editAnnotationPageNav} layout={PageLayoutType.Standard}>
|
<Page
|
||||||
<NavToolbarActions dashboard={dashboard} />
|
navModel={navModel}
|
||||||
|
pageNav={editAnnotationPageNav}
|
||||||
|
layout={PageLayoutType.Standard}
|
||||||
|
toolbar={isSingleTopNav ? <ToolbarActions dashboard={dashboard} /> : undefined}
|
||||||
|
>
|
||||||
|
{!isSingleTopNav && <NavToolbarActions dashboard={dashboard} />}
|
||||||
<AnnotationSettingsEdit
|
<AnnotationSettingsEdit
|
||||||
annotation={query}
|
annotation={query}
|
||||||
editIndex={editIndex}
|
editIndex={editIndex}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { NavModel, NavModelItem, PageLayoutType, arrayUtils } from '@grafana/data';
|
import { NavModel, NavModelItem, PageLayoutType, arrayUtils } from '@grafana/data';
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
import { SceneComponentProps, SceneObjectBase } from '@grafana/scenes';
|
import { SceneComponentProps, SceneObjectBase } from '@grafana/scenes';
|
||||||
import { DashboardLink } from '@grafana/schema';
|
import { DashboardLink } from '@grafana/schema';
|
||||||
import { Page } from 'app/core/components/Page/Page';
|
import { Page } from 'app/core/components/Page/Page';
|
||||||
|
|
||||||
import { DashboardScene } from '../scene/DashboardScene';
|
import { DashboardScene } from '../scene/DashboardScene';
|
||||||
import { NavToolbarActions } from '../scene/NavToolbarActions';
|
import { NavToolbarActions, ToolbarActions } from '../scene/NavToolbarActions';
|
||||||
import { DashboardLinkForm } from '../settings/links/DashboardLinkForm';
|
import { DashboardLinkForm } from '../settings/links/DashboardLinkForm';
|
||||||
import { DashboardLinkList } from '../settings/links/DashboardLinkList';
|
import { DashboardLinkList } from '../settings/links/DashboardLinkList';
|
||||||
import { NEW_LINK } from '../settings/links/utils';
|
import { NEW_LINK } from '../settings/links/utils';
|
||||||
@@ -80,6 +81,7 @@ function DashboardLinksEditViewRenderer({ model }: SceneComponentProps<Dashboard
|
|||||||
const { links } = dashboard.useState();
|
const { links } = dashboard.useState();
|
||||||
const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey());
|
const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey());
|
||||||
const linkToEdit = editIndex !== undefined ? links[editIndex] : undefined;
|
const linkToEdit = editIndex !== undefined ? links[editIndex] : undefined;
|
||||||
|
const isSingleTopNav = config.featureToggles.singleTopNav;
|
||||||
|
|
||||||
if (linkToEdit) {
|
if (linkToEdit) {
|
||||||
return (
|
return (
|
||||||
@@ -95,8 +97,13 @@ function DashboardLinksEditViewRenderer({ model }: SceneComponentProps<Dashboard
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Standard}>
|
<Page
|
||||||
<NavToolbarActions dashboard={dashboard} />
|
navModel={navModel}
|
||||||
|
pageNav={pageNav}
|
||||||
|
layout={PageLayoutType.Standard}
|
||||||
|
toolbar={isSingleTopNav ? <ToolbarActions dashboard={dashboard} /> : undefined}
|
||||||
|
>
|
||||||
|
{!isSingleTopNav && <NavToolbarActions dashboard={dashboard} />}
|
||||||
<DashboardLinkList
|
<DashboardLinkList
|
||||||
links={links}
|
links={links}
|
||||||
onNew={model.onNewLink}
|
onNew={model.onNewLink}
|
||||||
@@ -123,10 +130,16 @@ function EditLinkView({ pageNav, link, navModel, dashboard, onChange, onGoBack }
|
|||||||
text: 'Edit link',
|
text: 'Edit link',
|
||||||
parentItem: pageNav,
|
parentItem: pageNav,
|
||||||
};
|
};
|
||||||
|
const isSingleTopNav = config.featureToggles.singleTopNav;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page navModel={navModel} pageNav={editLinkPageNav} layout={PageLayoutType.Standard}>
|
<Page
|
||||||
<NavToolbarActions dashboard={dashboard} />
|
navModel={navModel}
|
||||||
|
pageNav={editLinkPageNav}
|
||||||
|
layout={PageLayoutType.Standard}
|
||||||
|
toolbar={isSingleTopNav ? <ToolbarActions dashboard={dashboard} /> : undefined}
|
||||||
|
>
|
||||||
|
{!isSingleTopNav && <NavToolbarActions dashboard={dashboard} />}
|
||||||
<DashboardLinkForm link={link!} onUpdate={onChange} onGoBack={onGoBack} />
|
<DashboardLinkForm link={link!} onUpdate={onChange} onGoBack={onGoBack} />
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import { GenAIDashTitleButton } from 'app/features/dashboard/components/GenAI/Ge
|
|||||||
|
|
||||||
import { updateNavModel } from '../pages/utils';
|
import { updateNavModel } from '../pages/utils';
|
||||||
import { DashboardScene } from '../scene/DashboardScene';
|
import { DashboardScene } from '../scene/DashboardScene';
|
||||||
import { NavToolbarActions } from '../scene/NavToolbarActions';
|
import { NavToolbarActions, ToolbarActions } from '../scene/NavToolbarActions';
|
||||||
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
|
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
|
||||||
import { getDashboardSceneFor } from '../utils/utils';
|
import { getDashboardSceneFor } from '../utils/utils';
|
||||||
|
|
||||||
@@ -177,10 +177,16 @@ export class GeneralSettingsEditView
|
|||||||
const { intervals } = model.getRefreshPicker().useState();
|
const { intervals } = model.getRefreshPicker().useState();
|
||||||
const { hideTimeControls } = model.getDashboardControls().useState();
|
const { hideTimeControls } = model.getDashboardControls().useState();
|
||||||
const { enabled: liveNow } = model.getLiveNowTimer().useState();
|
const { enabled: liveNow } = model.getLiveNowTimer().useState();
|
||||||
|
const isSingleTopNav = config.featureToggles.singleTopNav;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Standard}>
|
<Page
|
||||||
<NavToolbarActions dashboard={dashboard} />
|
navModel={navModel}
|
||||||
|
pageNav={pageNav}
|
||||||
|
layout={PageLayoutType.Standard}
|
||||||
|
toolbar={isSingleTopNav ? <ToolbarActions dashboard={dashboard} /> : undefined}
|
||||||
|
>
|
||||||
|
{!isSingleTopNav && <NavToolbarActions dashboard={dashboard} />}
|
||||||
<div style={{ maxWidth: '600px' }}>
|
<div style={{ maxWidth: '600px' }}>
|
||||||
<Box marginBottom={5}>
|
<Box marginBottom={5}>
|
||||||
<Field
|
<Field
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { css } from '@emotion/css';
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme2, PageLayoutType } from '@grafana/data';
|
import { GrafanaTheme2, PageLayoutType } from '@grafana/data';
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
import { SceneComponentProps, SceneObjectBase, sceneUtils } from '@grafana/scenes';
|
import { SceneComponentProps, SceneObjectBase, sceneUtils } from '@grafana/scenes';
|
||||||
import { Dashboard } from '@grafana/schema';
|
import { Dashboard } from '@grafana/schema';
|
||||||
import { Alert, Box, Button, CodeEditor, Stack, useStyles2 } from '@grafana/ui';
|
import { Alert, Box, Button, CodeEditor, Stack, useStyles2 } from '@grafana/ui';
|
||||||
@@ -18,7 +19,7 @@ import {
|
|||||||
} from '../saving/shared';
|
} from '../saving/shared';
|
||||||
import { useSaveDashboard } from '../saving/useSaveDashboard';
|
import { useSaveDashboard } from '../saving/useSaveDashboard';
|
||||||
import { DashboardScene } from '../scene/DashboardScene';
|
import { DashboardScene } from '../scene/DashboardScene';
|
||||||
import { NavToolbarActions } from '../scene/NavToolbarActions';
|
import { NavToolbarActions, ToolbarActions } from '../scene/NavToolbarActions';
|
||||||
import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
|
import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
|
||||||
import { transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel';
|
import { transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel';
|
||||||
import { getDashboardSceneFor } from '../utils/utils';
|
import { getDashboardSceneFor } from '../utils/utils';
|
||||||
@@ -88,6 +89,7 @@ export class JsonModelEditView extends SceneObjectBase<JsonModelEditViewState> i
|
|||||||
const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey());
|
const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey());
|
||||||
const canSave = dashboard.useState().meta.canSave;
|
const canSave = dashboard.useState().meta.canSave;
|
||||||
const { jsonText } = model.useState();
|
const { jsonText } = model.useState();
|
||||||
|
const isSingleTopNav = config.featureToggles.singleTopNav;
|
||||||
|
|
||||||
const onSave = async (overwrite: boolean) => {
|
const onSave = async (overwrite: boolean) => {
|
||||||
const result = await onSaveDashboard(dashboard, JSON.parse(model.state.jsonText), {
|
const result = await onSaveDashboard(dashboard, JSON.parse(model.state.jsonText), {
|
||||||
@@ -174,8 +176,13 @@ export class JsonModelEditView extends SceneObjectBase<JsonModelEditViewState> i
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Standard}>
|
<Page
|
||||||
<NavToolbarActions dashboard={dashboard} />
|
navModel={navModel}
|
||||||
|
pageNav={pageNav}
|
||||||
|
layout={PageLayoutType.Standard}
|
||||||
|
toolbar={isSingleTopNav ? <ToolbarActions dashboard={dashboard} /> : undefined}
|
||||||
|
>
|
||||||
|
{!isSingleTopNav && <NavToolbarActions dashboard={dashboard} />}
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
<Trans i18nKey="dashboard-settings.json-editor.subtitle">
|
<Trans i18nKey="dashboard-settings.json-editor.subtitle">
|
||||||
The JSON model below is the data structure that defines the dashboard. This includes dashboard settings,
|
The JSON model below is the data structure that defines the dashboard. This includes dashboard settings,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { PageLayoutType } from '@grafana/data';
|
import { PageLayoutType } from '@grafana/data';
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
import { SceneComponentProps, SceneObjectBase } from '@grafana/scenes';
|
import { SceneComponentProps, SceneObjectBase } from '@grafana/scenes';
|
||||||
import { Permissions } from 'app/core/components/AccessControl';
|
import { Permissions } from 'app/core/components/AccessControl';
|
||||||
import { Page } from 'app/core/components/Page/Page';
|
import { Page } from 'app/core/components/Page/Page';
|
||||||
@@ -6,7 +7,7 @@ import { contextSrv } from 'app/core/core';
|
|||||||
import { AccessControlAction } from 'app/types';
|
import { AccessControlAction } from 'app/types';
|
||||||
|
|
||||||
import { DashboardScene } from '../scene/DashboardScene';
|
import { DashboardScene } from '../scene/DashboardScene';
|
||||||
import { NavToolbarActions } from '../scene/NavToolbarActions';
|
import { NavToolbarActions, ToolbarActions } from '../scene/NavToolbarActions';
|
||||||
import { getDashboardSceneFor } from '../utils/utils';
|
import { getDashboardSceneFor } from '../utils/utils';
|
||||||
|
|
||||||
import { DashboardEditView, DashboardEditViewState, useDashboardEditPageNav } from './utils';
|
import { DashboardEditView, DashboardEditViewState, useDashboardEditPageNav } from './utils';
|
||||||
@@ -34,10 +35,16 @@ function PermissionsEditorSettings({ model }: SceneComponentProps<PermissionsEdi
|
|||||||
const { uid } = dashboard.useState();
|
const { uid } = dashboard.useState();
|
||||||
const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey());
|
const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey());
|
||||||
const canSetPermissions = contextSrv.hasPermission(AccessControlAction.DashboardsPermissionsWrite);
|
const canSetPermissions = contextSrv.hasPermission(AccessControlAction.DashboardsPermissionsWrite);
|
||||||
|
const isSingleTopNav = config.featureToggles.singleTopNav;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Standard}>
|
<Page
|
||||||
<NavToolbarActions dashboard={dashboard} />
|
navModel={navModel}
|
||||||
|
pageNav={pageNav}
|
||||||
|
layout={PageLayoutType.Standard}
|
||||||
|
toolbar={isSingleTopNav ? <ToolbarActions dashboard={dashboard} /> : undefined}
|
||||||
|
>
|
||||||
|
{!isSingleTopNav && <NavToolbarActions dashboard={dashboard} />}
|
||||||
<Permissions resource={'dashboards'} resourceId={uid ?? ''} canSetPermissions={canSetPermissions} />
|
<Permissions resource={'dashboards'} resourceId={uid ?? ''} canSetPermissions={canSetPermissions} />
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { NavModel, NavModelItem, PageLayoutType } from '@grafana/data';
|
import { NavModel, NavModelItem, PageLayoutType } from '@grafana/data';
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
import { SceneComponentProps, SceneObjectBase, SceneVariable, SceneVariables, sceneGraph } from '@grafana/scenes';
|
import { SceneComponentProps, SceneObjectBase, SceneVariable, SceneVariables, sceneGraph } from '@grafana/scenes';
|
||||||
import { Page } from 'app/core/components/Page/Page';
|
import { Page } from 'app/core/components/Page/Page';
|
||||||
|
|
||||||
import { DashboardScene } from '../scene/DashboardScene';
|
import { DashboardScene } from '../scene/DashboardScene';
|
||||||
import { NavToolbarActions } from '../scene/NavToolbarActions';
|
import { NavToolbarActions, ToolbarActions } from '../scene/NavToolbarActions';
|
||||||
import { getDashboardSceneFor } from '../utils/utils';
|
import { getDashboardSceneFor } from '../utils/utils';
|
||||||
|
|
||||||
import { EditListViewSceneUrlSync } from './EditListViewSceneUrlSync';
|
import { EditListViewSceneUrlSync } from './EditListViewSceneUrlSync';
|
||||||
@@ -206,6 +207,7 @@ function VariableEditorSettingsListView({ model }: SceneComponentProps<Variables
|
|||||||
const { onDelete, onDuplicated, onOrderChanged, onEdit, onTypeChange, onGoBack, onAdd } = model;
|
const { onDelete, onDuplicated, onOrderChanged, onEdit, onTypeChange, onGoBack, onAdd } = model;
|
||||||
const { variables } = model.getVariableSet().useState();
|
const { variables } = model.getVariableSet().useState();
|
||||||
const { editIndex } = model.useState();
|
const { editIndex } = model.useState();
|
||||||
|
const isSingleTopNav = config.featureToggles.singleTopNav;
|
||||||
|
|
||||||
if (editIndex !== undefined && variables[editIndex]) {
|
if (editIndex !== undefined && variables[editIndex]) {
|
||||||
const variable = variables[editIndex];
|
const variable = variables[editIndex];
|
||||||
@@ -226,8 +228,13 @@ function VariableEditorSettingsListView({ model }: SceneComponentProps<Variables
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Standard}>
|
<Page
|
||||||
<NavToolbarActions dashboard={dashboard} />
|
navModel={navModel}
|
||||||
|
pageNav={pageNav}
|
||||||
|
layout={PageLayoutType.Standard}
|
||||||
|
toolbar={isSingleTopNav ? <ToolbarActions dashboard={dashboard} /> : undefined}
|
||||||
|
>
|
||||||
|
{!isSingleTopNav && <NavToolbarActions dashboard={dashboard} />}
|
||||||
<VariableEditorList
|
<VariableEditorList
|
||||||
variables={variables}
|
variables={variables}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
@@ -262,14 +269,20 @@ function VariableEditorSettingsView({
|
|||||||
onValidateVariableName,
|
onValidateVariableName,
|
||||||
}: VariableEditorSettingsEditViewProps) {
|
}: VariableEditorSettingsEditViewProps) {
|
||||||
const { name } = variable.useState();
|
const { name } = variable.useState();
|
||||||
|
const isSingleTopNav = config.featureToggles.singleTopNav;
|
||||||
|
|
||||||
const editVariablePageNav = {
|
const editVariablePageNav = {
|
||||||
text: name,
|
text: name,
|
||||||
parentItem: pageNav,
|
parentItem: pageNav,
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Page navModel={navModel} pageNav={editVariablePageNav} layout={PageLayoutType.Standard}>
|
<Page
|
||||||
<NavToolbarActions dashboard={dashboard} />
|
navModel={navModel}
|
||||||
|
pageNav={editVariablePageNav}
|
||||||
|
layout={PageLayoutType.Standard}
|
||||||
|
toolbar={isSingleTopNav ? <ToolbarActions dashboard={dashboard} /> : undefined}
|
||||||
|
>
|
||||||
|
{!isSingleTopNav && <NavToolbarActions dashboard={dashboard} />}
|
||||||
<VariableEditorForm
|
<VariableEditorForm
|
||||||
variable={variable}
|
variable={variable}
|
||||||
onTypeChange={onTypeChange}
|
onTypeChange={onTypeChange}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
import { PageLayoutType, dateTimeFormat, dateTimeFormatTimeAgo } from '@grafana/data';
|
import { PageLayoutType, dateTimeFormat, dateTimeFormatTimeAgo } from '@grafana/data';
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
import { SceneComponentProps, SceneObjectBase, sceneGraph } from '@grafana/scenes';
|
import { SceneComponentProps, SceneObjectBase, sceneGraph } from '@grafana/scenes';
|
||||||
import { Spinner, Stack } from '@grafana/ui';
|
import { Spinner, Stack } from '@grafana/ui';
|
||||||
import { Page } from 'app/core/components/Page/Page';
|
import { Page } from 'app/core/components/Page/Page';
|
||||||
|
|
||||||
import { DashboardScene } from '../scene/DashboardScene';
|
import { DashboardScene } from '../scene/DashboardScene';
|
||||||
import { NavToolbarActions } from '../scene/NavToolbarActions';
|
import { NavToolbarActions, ToolbarActions } from '../scene/NavToolbarActions';
|
||||||
import { getDashboardSceneFor } from '../utils/utils';
|
import { getDashboardSceneFor } from '../utils/utils';
|
||||||
|
|
||||||
import { DashboardEditView, DashboardEditViewState, useDashboardEditPageNav } from './utils';
|
import { DashboardEditView, DashboardEditViewState, useDashboardEditPageNav } from './utils';
|
||||||
@@ -188,6 +189,7 @@ function VersionsEditorSettingsListView({ model }: SceneComponentProps<VersionsE
|
|||||||
const showButtons = model.versions.length > 1;
|
const showButtons = model.versions.length > 1;
|
||||||
const hasMore = model.versions.length >= model.limit;
|
const hasMore = model.versions.length >= model.limit;
|
||||||
const isLastPage = model.versions.find((rev) => rev.version === 1);
|
const isLastPage = model.versions.find((rev) => rev.version === 1);
|
||||||
|
const isSingleTopNav = config.featureToggles.singleTopNav;
|
||||||
|
|
||||||
const viewModeCompare = (
|
const viewModeCompare = (
|
||||||
<>
|
<>
|
||||||
@@ -237,8 +239,13 @@ function VersionsEditorSettingsListView({ model }: SceneComponentProps<VersionsE
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page navModel={navModel} pageNav={pageNav} layout={PageLayoutType.Standard}>
|
<Page
|
||||||
<NavToolbarActions dashboard={dashboard} />
|
navModel={navModel}
|
||||||
|
pageNav={pageNav}
|
||||||
|
layout={PageLayoutType.Standard}
|
||||||
|
toolbar={isSingleTopNav ? <ToolbarActions dashboard={dashboard} /> : undefined}
|
||||||
|
>
|
||||||
|
{!isSingleTopNav && <NavToolbarActions dashboard={dashboard} />}
|
||||||
{viewMode === 'compare' ? viewModeCompare : viewModeList}
|
{viewMode === 'compare' ? viewModeCompare : viewModeList}
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useMemo } from 'react';
|
|||||||
import { shallowEqual } from 'react-redux';
|
import { shallowEqual } from 'react-redux';
|
||||||
|
|
||||||
import { DataSourceInstanceSettings, RawTimeRange, GrafanaTheme2 } from '@grafana/data';
|
import { DataSourceInstanceSettings, RawTimeRange, GrafanaTheme2 } from '@grafana/data';
|
||||||
import { reportInteraction } from '@grafana/runtime';
|
import { config, reportInteraction } from '@grafana/runtime';
|
||||||
import {
|
import {
|
||||||
defaultIntervals,
|
defaultIntervals,
|
||||||
PageToolbar,
|
PageToolbar,
|
||||||
@@ -89,6 +89,7 @@ export function ExploreToolbar({ exploreId, onChangeTime, onContentOutlineToogle
|
|||||||
const correlationDetails = useSelector(selectCorrelationDetails);
|
const correlationDetails = useSelector(selectCorrelationDetails);
|
||||||
const isCorrelationsEditorMode = correlationDetails?.editorMode || false;
|
const isCorrelationsEditorMode = correlationDetails?.editorMode || false;
|
||||||
const isLeftPane = useSelector(isLeftPaneSelector(exploreId));
|
const isLeftPane = useSelector(isLeftPaneSelector(exploreId));
|
||||||
|
const isSingleTopNav = config.featureToggles.singleTopNav;
|
||||||
|
|
||||||
const shouldRotateSplitIcon = useMemo(
|
const shouldRotateSplitIcon = useMemo(
|
||||||
() => (isLeftPane && isLargerPane) || (!isLeftPane && !isLargerPane),
|
() => (isLeftPane && isLargerPane) || (!isLeftPane && !isLargerPane),
|
||||||
@@ -206,9 +207,11 @@ export function ExploreToolbar({ exploreId, onChangeTime, onContentOutlineToogle
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{refreshInterval && <SetInterval func={onRunQuery} interval={refreshInterval} loading={loading} />}
|
{refreshInterval && <SetInterval func={onRunQuery} interval={refreshInterval} loading={loading} />}
|
||||||
<div>
|
{!isSingleTopNav && (
|
||||||
<AppChromeUpdate actions={navBarActions} />
|
<div>
|
||||||
</div>
|
<AppChromeUpdate actions={navBarActions} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<PageToolbar
|
<PageToolbar
|
||||||
aria-label={t('explore.toolbar.aria-label', 'Explore toolbar')}
|
aria-label={t('explore.toolbar.aria-label', 'Explore toolbar')}
|
||||||
leftItems={[
|
leftItems={[
|
||||||
@@ -233,6 +236,7 @@ export function ExploreToolbar({ exploreId, onChangeTime, onContentOutlineToogle
|
|||||||
hideTextValue={showSmallDataSourcePicker}
|
hideTextValue={showSmallDataSourcePicker}
|
||||||
width={showSmallDataSourcePicker ? 8 : undefined}
|
width={showSmallDataSourcePicker ? 8 : undefined}
|
||||||
/>,
|
/>,
|
||||||
|
isSingleTopNav && <ShortLinkButtonMenu key="share" />,
|
||||||
].filter(Boolean)}
|
].filter(Boolean)}
|
||||||
forceShowLeftItems
|
forceShowLeftItems
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user