mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Navigation: Unify Page component (#66951)
* remove old page component * add test to check initDashboard is only called once (prevent variables loading twice) * add help node * update unit tests * remove last mentions of topnav * fix unit tests * remove unused props from ButtonRow interface * remove prop from test
This commit is contained in:
@@ -2686,8 +2686,7 @@ exports[`better eslint`] = {
|
|||||||
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
|
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
|
||||||
],
|
],
|
||||||
"public/app/features/datasources/components/ButtonRow.tsx:5381": [
|
"public/app/features/datasources/components/ButtonRow.tsx:5381": [
|
||||||
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"],
|
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
|
||||||
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "1"]
|
|
||||||
],
|
],
|
||||||
"public/app/features/datasources/components/DataSourceReadOnlyMessage.tsx:5381": [
|
"public/app/features/datasources/components/DataSourceReadOnlyMessage.tsx:5381": [
|
||||||
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
|
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ import { AppWrapper } from './AppWrapper';
|
|||||||
import appEvents from './core/app_events';
|
import appEvents from './core/app_events';
|
||||||
import { AppChromeService } from './core/components/AppChrome/AppChromeService';
|
import { AppChromeService } from './core/components/AppChrome/AppChromeService';
|
||||||
import { getAllOptionEditors, getAllStandardFieldConfigs } from './core/components/OptionsUI/registry';
|
import { getAllOptionEditors, getAllStandardFieldConfigs } from './core/components/OptionsUI/registry';
|
||||||
import { PluginPage } from './core/components/PageNew/PluginPage';
|
import { PluginPage } from './core/components/Page/PluginPage';
|
||||||
import { GrafanaContextType } from './core/context/GrafanaContext';
|
import { GrafanaContextType } from './core/context/GrafanaContext';
|
||||||
import { initializeI18n } from './core/internationalization';
|
import { initializeI18n } from './core/internationalization';
|
||||||
import { interceptLinkClicks } from './core/navigation/patch/interceptLinkClicks';
|
import { interceptLinkClicks } from './core/navigation/patch/interceptLinkClicks';
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { config } from '@grafana/runtime';
|
|||||||
import { HOME_NAV_ID } from 'app/core/reducers/navModel';
|
import { HOME_NAV_ID } from 'app/core/reducers/navModel';
|
||||||
import { DashboardQueryResult, getGrafanaSearcher, QueryResponse } from 'app/features/search/service';
|
import { DashboardQueryResult, getGrafanaSearcher, QueryResponse } from 'app/features/search/service';
|
||||||
|
|
||||||
import { Page } from '../PageNew/Page';
|
import { Page } from '../Page/Page';
|
||||||
|
|
||||||
import { AppChrome } from './AppChrome';
|
import { AppChrome } from './AppChrome';
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,28 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { TestProvider } from 'test/helpers/TestProvider';
|
import { TestProvider } from 'test/helpers/TestProvider';
|
||||||
|
import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock';
|
||||||
|
|
||||||
import { NavModelItem } from '@grafana/data';
|
import { NavModelItem, PageLayoutType } from '@grafana/data';
|
||||||
import { config } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
|
import { HOME_NAV_ID } from 'app/core/reducers/navModel';
|
||||||
|
|
||||||
import { Page } from './Page';
|
import { Page } from './Page';
|
||||||
import { PageProps } from './types';
|
import { PageProps } from './types';
|
||||||
|
|
||||||
const pageNav: NavModelItem = {
|
const pageNav: NavModelItem = {
|
||||||
text: 'Main title',
|
text: 'pageNav title',
|
||||||
children: [
|
children: [
|
||||||
{ text: 'Child1', url: '1', active: true },
|
{ text: 'pageNav child1', url: '1', active: true },
|
||||||
{ text: 'Child2', url: '2' },
|
{ text: 'pageNav child2', url: '2' },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const setup = (props: Partial<PageProps>) => {
|
const setup = (props: Partial<PageProps>) => {
|
||||||
config.bootData.navTree = [
|
config.bootData.navTree = [
|
||||||
|
{
|
||||||
|
id: HOME_NAV_ID,
|
||||||
|
text: 'Home',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: 'Section name',
|
text: 'Section name',
|
||||||
id: 'section',
|
id: 'section',
|
||||||
@@ -29,13 +34,17 @@ const setup = (props: Partial<PageProps>) => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return render(
|
const context = getGrafanaContextMock();
|
||||||
<TestProvider>
|
|
||||||
|
const renderResult = render(
|
||||||
|
<TestProvider grafanaContext={context}>
|
||||||
<Page {...props}>
|
<Page {...props}>
|
||||||
<div data-testid="page-children">Children</div>
|
<div data-testid="page-children">Children</div>
|
||||||
</Page>
|
</Page>
|
||||||
</TestProvider>
|
</TestProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return { renderResult, context };
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('Render', () => {
|
describe('Render', () => {
|
||||||
@@ -51,14 +60,25 @@ describe('Render', () => {
|
|||||||
it('should render header when pageNav supplied', async () => {
|
it('should render header when pageNav supplied', async () => {
|
||||||
setup({ pageNav });
|
setup({ pageNav });
|
||||||
|
|
||||||
expect(screen.getByRole('heading', { name: 'Main title' })).toBeInTheDocument();
|
expect(screen.getByRole('heading', { name: 'pageNav title' })).toBeInTheDocument();
|
||||||
expect(screen.getAllByRole('tab').length).toBe(2);
|
expect(screen.getAllByRole('tab').length).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should get header nav model from redux navIndex', async () => {
|
it('should update chrome with section, pageNav and layout', async () => {
|
||||||
setup({ navId: 'child1' });
|
const { context } = setup({ navId: 'child1', pageNav, layout: PageLayoutType.Canvas });
|
||||||
|
expect(context.chrome.state.getValue().sectionNav.node.id).toBe('child1');
|
||||||
|
expect(context.chrome.state.getValue().pageNav).toBe(pageNav);
|
||||||
|
expect(context.chrome.state.getValue().layout).toBe(PageLayoutType.Canvas);
|
||||||
|
});
|
||||||
|
|
||||||
expect(screen.getByRole('heading', { name: 'Section name' })).toBeInTheDocument();
|
it('should update document title', async () => {
|
||||||
expect(screen.getAllByRole('tab').length).toBe(2);
|
setup({ navId: 'child1', pageNav });
|
||||||
|
expect(document.title).toBe('pageNav title - Child1 - Section name - Grafana');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not include hideFromBreadcrumb nodes in title', async () => {
|
||||||
|
pageNav.children![0].hideFromBreadcrumbs = true;
|
||||||
|
setup({ navId: 'child1', pageNav });
|
||||||
|
expect(document.title).toBe('pageNav title - Child1 - Section name - Grafana');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,35 +1,32 @@
|
|||||||
// Libraries
|
// Libraries
|
||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useLayoutEffect } from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme2, PageLayoutType } from '@grafana/data';
|
import { GrafanaTheme2, PageLayoutType } from '@grafana/data';
|
||||||
import { config } from '@grafana/runtime';
|
|
||||||
import { CustomScrollbar, useStyles2 } from '@grafana/ui';
|
import { CustomScrollbar, useStyles2 } from '@grafana/ui';
|
||||||
import { useGrafana } from 'app/core/context/GrafanaContext';
|
import { useGrafana } from 'app/core/context/GrafanaContext';
|
||||||
|
|
||||||
import { Footer } from '../Footer/Footer';
|
|
||||||
import { PageHeader } from '../PageHeader/PageHeader';
|
|
||||||
import { Page as NewPage } from '../PageNew/Page';
|
|
||||||
|
|
||||||
import { PageContents } from './PageContents';
|
import { PageContents } from './PageContents';
|
||||||
|
import { PageHeader } from './PageHeader';
|
||||||
|
import { PageTabs } from './PageTabs';
|
||||||
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 const OldPage: PageType = ({
|
export const Page: PageType = ({
|
||||||
navId,
|
navId,
|
||||||
navModel: oldNavProp,
|
navModel: oldNavProp,
|
||||||
pageNav,
|
pageNav,
|
||||||
|
renderTitle,
|
||||||
|
actions,
|
||||||
|
subTitle,
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
toolbar,
|
|
||||||
scrollRef,
|
|
||||||
scrollTop,
|
|
||||||
layout = PageLayoutType.Standard,
|
|
||||||
renderTitle,
|
|
||||||
subTitle,
|
|
||||||
actions,
|
|
||||||
info,
|
info,
|
||||||
|
layout = PageLayoutType.Standard,
|
||||||
|
toolbar,
|
||||||
|
scrollTop,
|
||||||
|
scrollRef,
|
||||||
...otherProps
|
...otherProps
|
||||||
}) => {
|
}) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
@@ -38,48 +35,46 @@ export const OldPage: PageType = ({
|
|||||||
|
|
||||||
usePageTitle(navModel, pageNav);
|
usePageTitle(navModel, pageNav);
|
||||||
|
|
||||||
const pageHeaderNav = pageNav ?? navModel?.main;
|
const pageHeaderNav = pageNav ?? navModel?.node;
|
||||||
|
|
||||||
useEffect(() => {
|
// We use useLayoutEffect here to make sure that the chrome is updated before the page is rendered
|
||||||
|
// This prevents flickering sectionNav when going from dashbaord to settings for example
|
||||||
|
useLayoutEffect(() => {
|
||||||
if (navModel) {
|
if (navModel) {
|
||||||
// This is needed for chrome to update it's chromeless state
|
|
||||||
chrome.update({
|
chrome.update({
|
||||||
sectionNav: navModel,
|
sectionNav: navModel,
|
||||||
|
pageNav: pageNav,
|
||||||
|
layout: layout,
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
// Need to trigger a chrome state update for the route change to be processed
|
|
||||||
chrome.update({});
|
|
||||||
}
|
}
|
||||||
}, [navModel, chrome]);
|
}, [navModel, pageNav, chrome, layout]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cx(styles.wrapper, className)} {...otherProps}>
|
<div className={cx(styles.wrapper, className)} {...otherProps}>
|
||||||
{layout === PageLayoutType.Standard && (
|
{layout === PageLayoutType.Standard && (
|
||||||
<CustomScrollbar autoHeightMin={'100%'} scrollTop={scrollTop} scrollRefCallback={scrollRef}>
|
<CustomScrollbar autoHeightMin={'100%'} scrollTop={scrollTop} scrollRefCallback={scrollRef}>
|
||||||
<div className={cx('page-scrollbar-content', className)}>
|
<div className={styles.pageInner}>
|
||||||
{pageHeaderNav && (
|
{pageHeaderNav && (
|
||||||
<PageHeader
|
<PageHeader
|
||||||
actions={actions}
|
actions={actions}
|
||||||
info={info}
|
|
||||||
navItem={pageHeaderNav}
|
navItem={pageHeaderNav}
|
||||||
renderTitle={renderTitle}
|
renderTitle={renderTitle}
|
||||||
|
info={info}
|
||||||
subTitle={subTitle}
|
subTitle={subTitle}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{children}
|
{pageNav && pageNav.children && <PageTabs navItem={pageNav} />}
|
||||||
<Footer />
|
<div className={styles.pageContent}>{children}</div>
|
||||||
</div>
|
</div>
|
||||||
</CustomScrollbar>
|
</CustomScrollbar>
|
||||||
)}
|
)}
|
||||||
{layout === PageLayoutType.Canvas && (
|
{layout === PageLayoutType.Canvas && (
|
||||||
<>
|
<CustomScrollbar autoHeightMin={'100%'} scrollTop={scrollTop} scrollRefCallback={scrollRef}>
|
||||||
{toolbar}
|
<div className={styles.canvasContent}>
|
||||||
<div className={styles.scrollWrapper}>
|
{toolbar}
|
||||||
<CustomScrollbar autoHeightMin={'100%'} scrollTop={scrollTop} scrollRefCallback={scrollRef}>
|
{children}
|
||||||
<div className={cx(styles.content, !toolbar && styles.contentWithoutToolbar)}>{children}</div>
|
|
||||||
</CustomScrollbar>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</CustomScrollbar>
|
||||||
)}
|
)}
|
||||||
{layout === PageLayoutType.Custom && (
|
{layout === PageLayoutType.Custom && (
|
||||||
<>
|
<>
|
||||||
@@ -91,33 +86,46 @@ export const OldPage: PageType = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
OldPage.Contents = PageContents;
|
Page.Contents = PageContents;
|
||||||
|
|
||||||
export const Page: PageType = config.featureToggles.topnav ? NewPage : OldPage;
|
const getStyles = (theme: GrafanaTheme2) => {
|
||||||
|
return {
|
||||||
|
wrapper: css({
|
||||||
|
label: 'page-wrapper',
|
||||||
|
height: '100%',
|
||||||
|
display: 'flex',
|
||||||
|
flex: '1 1 0',
|
||||||
|
flexDirection: 'column',
|
||||||
|
minHeight: 0,
|
||||||
|
}),
|
||||||
|
pageContent: css({
|
||||||
|
label: 'page-content',
|
||||||
|
flexGrow: 1,
|
||||||
|
}),
|
||||||
|
pageInner: css({
|
||||||
|
label: 'page-inner',
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
borderRadius: theme.shape.borderRadius(1),
|
||||||
|
border: `1px solid ${theme.colors.border.weak}`,
|
||||||
|
borderBottom: 'none',
|
||||||
|
background: theme.colors.background.primary,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
flexGrow: 1,
|
||||||
|
margin: theme.spacing(0, 0, 0, 0),
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => ({
|
[theme.breakpoints.up('md')]: {
|
||||||
wrapper: css({
|
margin: theme.spacing(2, 2, 0, 1),
|
||||||
width: '100%',
|
padding: theme.spacing(3),
|
||||||
height: '100%',
|
},
|
||||||
display: 'flex',
|
}),
|
||||||
flex: '1 1 0',
|
canvasContent: css({
|
||||||
flexDirection: 'column',
|
label: 'canvas-content',
|
||||||
minHeight: 0,
|
display: 'flex',
|
||||||
}),
|
flexDirection: 'column',
|
||||||
scrollWrapper: css({
|
padding: theme.spacing(2),
|
||||||
width: '100%',
|
flexBasis: '100%',
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
minHeight: 0,
|
}),
|
||||||
display: 'flex',
|
};
|
||||||
}),
|
};
|
||||||
content: css({
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
padding: theme.spacing(0, 2, 2, 2),
|
|
||||||
flexBasis: '100%',
|
|
||||||
flexGrow: 1,
|
|
||||||
}),
|
|
||||||
contentWithoutToolbar: css({
|
|
||||||
padding: theme.spacing(2),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
// Libraries
|
// Libraries
|
||||||
import { cx } from '@emotion/css';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
// Components
|
|
||||||
import PageLoader from '../PageLoader/PageLoader';
|
import PageLoader from '../PageLoader/PageLoader';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -12,5 +10,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const PageContents = ({ isLoading, children, className }: Props) => {
|
export const PageContents = ({ isLoading, children, className }: Props) => {
|
||||||
return <div className={cx('page-container', 'page-body', className)}>{isLoading ? <PageLoader /> : children}</div>;
|
let content = className ? <div className={className}>{children}</div> : children;
|
||||||
|
|
||||||
|
return <>{isLoading ? <PageLoader /> : content}</>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ import React from 'react';
|
|||||||
import { NavModelItem, GrafanaTheme2 } from '@grafana/data';
|
import { NavModelItem, GrafanaTheme2 } from '@grafana/data';
|
||||||
import { useStyles2 } from '@grafana/ui';
|
import { useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
import { PageInfoItem } from '../Page/types';
|
|
||||||
import { PageInfo } from '../PageInfo/PageInfo';
|
import { PageInfo } from '../PageInfo/PageInfo';
|
||||||
|
|
||||||
|
import { PageInfoItem } from './types';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
navItem: NavModelItem;
|
navItem: NavModelItem;
|
||||||
renderTitle?: (title: string) => React.ReactNode;
|
renderTitle?: (title: string) => React.ReactNode;
|
||||||
@@ -3,7 +3,7 @@ import React, { useContext } from 'react';
|
|||||||
import { PluginPageProps } from '@grafana/runtime';
|
import { PluginPageProps } from '@grafana/runtime';
|
||||||
import { PluginPageContext } from 'app/features/plugins/components/PluginPageContext';
|
import { PluginPageContext } from 'app/features/plugins/components/PluginPageContext';
|
||||||
|
|
||||||
import { Page } from '../Page/Page';
|
import { Page } from './Page';
|
||||||
|
|
||||||
export function PluginPage({ actions, children, info, pageNav, layout, renderTitle, subTitle }: PluginPageProps) {
|
export function PluginPage({ actions, children, info, pageNav, layout, renderTitle, subTitle }: PluginPageProps) {
|
||||||
const context = useContext(PluginPageContext);
|
const context = useContext(PluginPageContext);
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
import { render, screen } from '@testing-library/react';
|
|
||||||
import React from 'react';
|
|
||||||
import { TestProvider } from 'test/helpers/TestProvider';
|
|
||||||
import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock';
|
|
||||||
|
|
||||||
import { NavModelItem, PageLayoutType } from '@grafana/data';
|
|
||||||
import { config } from '@grafana/runtime';
|
|
||||||
import { HOME_NAV_ID } from 'app/core/reducers/navModel';
|
|
||||||
|
|
||||||
import { PageProps } from '../Page/types';
|
|
||||||
|
|
||||||
import { Page } from './Page';
|
|
||||||
|
|
||||||
const pageNav: NavModelItem = {
|
|
||||||
text: 'pageNav title',
|
|
||||||
children: [
|
|
||||||
{ text: 'pageNav child1', url: '1', active: true },
|
|
||||||
{ text: 'pageNav child2', url: '2' },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
const setup = (props: Partial<PageProps>) => {
|
|
||||||
config.bootData.navTree = [
|
|
||||||
{
|
|
||||||
id: HOME_NAV_ID,
|
|
||||||
text: 'Home',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: 'Section name',
|
|
||||||
id: 'section',
|
|
||||||
url: 'section',
|
|
||||||
children: [
|
|
||||||
{ text: 'Child1', id: 'child1', url: 'section/child1' },
|
|
||||||
{ text: 'Child2', id: 'child2', url: 'section/child2' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const context = getGrafanaContextMock();
|
|
||||||
|
|
||||||
const renderResult = render(
|
|
||||||
<TestProvider grafanaContext={context}>
|
|
||||||
<Page {...props}>
|
|
||||||
<div data-testid="page-children">Children</div>
|
|
||||||
</Page>
|
|
||||||
</TestProvider>
|
|
||||||
);
|
|
||||||
|
|
||||||
return { renderResult, context };
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('Render', () => {
|
|
||||||
it('should render component with emtpy Page container', async () => {
|
|
||||||
setup({});
|
|
||||||
const children = await screen.findByTestId('page-children');
|
|
||||||
expect(children).toBeInTheDocument();
|
|
||||||
|
|
||||||
const pageHeader = screen.queryByRole('heading');
|
|
||||||
expect(pageHeader).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render header when pageNav supplied', async () => {
|
|
||||||
setup({ pageNav });
|
|
||||||
|
|
||||||
expect(screen.getByRole('heading', { name: 'pageNav title' })).toBeInTheDocument();
|
|
||||||
expect(screen.getAllByRole('tab').length).toBe(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should update chrome with section, pageNav and layout', async () => {
|
|
||||||
const { context } = setup({ navId: 'child1', pageNav, layout: PageLayoutType.Canvas });
|
|
||||||
expect(context.chrome.state.getValue().sectionNav.node.id).toBe('child1');
|
|
||||||
expect(context.chrome.state.getValue().pageNav).toBe(pageNav);
|
|
||||||
expect(context.chrome.state.getValue().layout).toBe(PageLayoutType.Canvas);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should update document title', async () => {
|
|
||||||
setup({ navId: 'child1', pageNav });
|
|
||||||
expect(document.title).toBe('pageNav title - Child1 - Section name - Grafana');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not include hideFromBreadcrumb nodes in title', async () => {
|
|
||||||
pageNav.children![0].hideFromBreadcrumbs = true;
|
|
||||||
setup({ navId: 'child1', pageNav });
|
|
||||||
expect(document.title).toBe('pageNav title - Child1 - Section name - Grafana');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
// Libraries
|
|
||||||
import { css, cx } from '@emotion/css';
|
|
||||||
import React, { useLayoutEffect } from 'react';
|
|
||||||
|
|
||||||
import { GrafanaTheme2, PageLayoutType } from '@grafana/data';
|
|
||||||
import { CustomScrollbar, useStyles2 } from '@grafana/ui';
|
|
||||||
import { useGrafana } from 'app/core/context/GrafanaContext';
|
|
||||||
|
|
||||||
import { PageType } from '../Page/types';
|
|
||||||
import { usePageNav } from '../Page/usePageNav';
|
|
||||||
import { usePageTitle } from '../Page/usePageTitle';
|
|
||||||
|
|
||||||
import { PageContents } from './PageContents';
|
|
||||||
import { PageHeader } from './PageHeader';
|
|
||||||
import { PageTabs } from './PageTabs';
|
|
||||||
|
|
||||||
export const Page: PageType = ({
|
|
||||||
navId,
|
|
||||||
navModel: oldNavProp,
|
|
||||||
pageNav,
|
|
||||||
renderTitle,
|
|
||||||
actions,
|
|
||||||
subTitle,
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
info,
|
|
||||||
layout = PageLayoutType.Standard,
|
|
||||||
toolbar,
|
|
||||||
scrollTop,
|
|
||||||
scrollRef,
|
|
||||||
...otherProps
|
|
||||||
}) => {
|
|
||||||
const styles = useStyles2(getStyles);
|
|
||||||
const navModel = usePageNav(navId, oldNavProp);
|
|
||||||
const { chrome } = useGrafana();
|
|
||||||
|
|
||||||
usePageTitle(navModel, pageNav);
|
|
||||||
|
|
||||||
const pageHeaderNav = pageNav ?? navModel?.node;
|
|
||||||
|
|
||||||
// We use useLayoutEffect here to make sure that the chrome is updated before the page is rendered
|
|
||||||
// This prevents flickering sectionNav when going from dashbaord to settings for example
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
if (navModel) {
|
|
||||||
chrome.update({
|
|
||||||
sectionNav: navModel,
|
|
||||||
pageNav: pageNav,
|
|
||||||
layout: layout,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [navModel, pageNav, chrome, layout]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={cx(styles.wrapper, className)} {...otherProps}>
|
|
||||||
{layout === PageLayoutType.Standard && (
|
|
||||||
<CustomScrollbar autoHeightMin={'100%'} scrollTop={scrollTop} scrollRefCallback={scrollRef}>
|
|
||||||
<div className={styles.pageInner}>
|
|
||||||
{pageHeaderNav && (
|
|
||||||
<PageHeader
|
|
||||||
actions={actions}
|
|
||||||
navItem={pageHeaderNav}
|
|
||||||
renderTitle={renderTitle}
|
|
||||||
info={info}
|
|
||||||
subTitle={subTitle}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{pageNav && pageNav.children && <PageTabs navItem={pageNav} />}
|
|
||||||
<div className={styles.pageContent}>{children}</div>
|
|
||||||
</div>
|
|
||||||
</CustomScrollbar>
|
|
||||||
)}
|
|
||||||
{layout === PageLayoutType.Canvas && (
|
|
||||||
<CustomScrollbar autoHeightMin={'100%'} scrollTop={scrollTop} scrollRefCallback={scrollRef}>
|
|
||||||
<div className={styles.canvasContent}>
|
|
||||||
{toolbar}
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</CustomScrollbar>
|
|
||||||
)}
|
|
||||||
{layout === PageLayoutType.Custom && (
|
|
||||||
<>
|
|
||||||
{toolbar}
|
|
||||||
{children}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
Page.Contents = PageContents;
|
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => {
|
|
||||||
return {
|
|
||||||
wrapper: css({
|
|
||||||
label: 'page-wrapper',
|
|
||||||
height: '100%',
|
|
||||||
display: 'flex',
|
|
||||||
flex: '1 1 0',
|
|
||||||
flexDirection: 'column',
|
|
||||||
minHeight: 0,
|
|
||||||
}),
|
|
||||||
pageContent: css({
|
|
||||||
label: 'page-content',
|
|
||||||
flexGrow: 1,
|
|
||||||
}),
|
|
||||||
pageInner: css({
|
|
||||||
label: 'page-inner',
|
|
||||||
padding: theme.spacing(2),
|
|
||||||
borderRadius: theme.shape.borderRadius(1),
|
|
||||||
border: `1px solid ${theme.colors.border.weak}`,
|
|
||||||
borderBottom: 'none',
|
|
||||||
background: theme.colors.background.primary,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
flexGrow: 1,
|
|
||||||
margin: theme.spacing(0, 0, 0, 0),
|
|
||||||
|
|
||||||
[theme.breakpoints.up('md')]: {
|
|
||||||
margin: theme.spacing(2, 2, 0, 1),
|
|
||||||
padding: theme.spacing(3),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
canvasContent: css({
|
|
||||||
label: 'canvas-content',
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
padding: theme.spacing(2),
|
|
||||||
flexBasis: '100%',
|
|
||||||
flexGrow: 1,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
// Libraries
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import PageLoader from '../PageLoader/PageLoader';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
isLoading?: boolean;
|
|
||||||
children: React.ReactNode;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PageContents = ({ isLoading, children, className }: Props) => {
|
|
||||||
let content = className ? <div className={className}>{children}</div> : children;
|
|
||||||
|
|
||||||
return <>{isLoading ? <PageLoader /> : content}</>;
|
|
||||||
};
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useLocation, useParams } from 'react-router-dom';
|
import { useLocation, useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { config } from '@grafana/runtime';
|
|
||||||
import { Page } from 'app/core/components/Page/Page';
|
import { Page } from 'app/core/components/Page/Page';
|
||||||
import { EditDataSource } from 'app/features/datasources/components/EditDataSource';
|
import { EditDataSource } from 'app/features/datasources/components/EditDataSource';
|
||||||
import { EditDataSourceActions } from 'app/features/datasources/components/EditDataSourceActions';
|
import { EditDataSourceActions } from 'app/features/datasources/components/EditDataSourceActions';
|
||||||
@@ -16,11 +15,7 @@ export function EditDataSourcePage() {
|
|||||||
const { navId, pageNav } = useDataSourceSettingsNav();
|
const { navId, pageNav } = useDataSourceSettingsNav();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page
|
<Page navId={navId} pageNav={pageNav} actions={<EditDataSourceActions uid={uid} />}>
|
||||||
navId={navId}
|
|
||||||
pageNav={pageNav}
|
|
||||||
actions={config.featureToggles.topnav ? <EditDataSourceActions uid={uid} /> : undefined}
|
|
||||||
>
|
|
||||||
<Page.Contents>
|
<Page.Contents>
|
||||||
<EditDataSource uid={uid} pageId={pageId} />
|
<EditDataSource uid={uid} pageId={pageId} />
|
||||||
</Page.Contents>
|
</Page.Contents>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { Permissions } from 'app/core/components/AccessControl';
|
import { Permissions } from 'app/core/components/AccessControl';
|
||||||
import { Page } from 'app/core/components/PageNew/Page';
|
import { Page } from 'app/core/components/Page/Page';
|
||||||
import { contextSrv } from 'app/core/core';
|
import { contextSrv } from 'app/core/core';
|
||||||
import { AccessControlAction } from 'app/types';
|
import { AccessControlAction } from 'app/types';
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { connect, ConnectedProps } from 'react-redux';
|
|||||||
|
|
||||||
import { Tooltip, Icon, Button } from '@grafana/ui';
|
import { Tooltip, Icon, Button } from '@grafana/ui';
|
||||||
import { SlideDown } from 'app/core/components/Animations/SlideDown';
|
import { SlideDown } from 'app/core/components/Animations/SlideDown';
|
||||||
import { Page } from 'app/core/components/PageNew/Page';
|
import { Page } from 'app/core/components/Page/Page';
|
||||||
import AddPermission from 'app/core/components/PermissionList/AddPermission';
|
import AddPermission from 'app/core/components/PermissionList/AddPermission';
|
||||||
import PermissionList from 'app/core/components/PermissionList/PermissionList';
|
import PermissionList from 'app/core/components/PermissionList/PermissionList';
|
||||||
import PermissionsInfo from 'app/core/components/PermissionList/PermissionsInfo';
|
import PermissionsInfo from 'app/core/components/PermissionList/PermissionsInfo';
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
|
|
||||||
import { AnnotationQuery, getDataSourceRef, NavModelItem } from '@grafana/data';
|
import { AnnotationQuery, getDataSourceRef, NavModelItem } from '@grafana/data';
|
||||||
import { getDataSourceSrv, locationService } from '@grafana/runtime';
|
import { getDataSourceSrv, locationService } from '@grafana/runtime';
|
||||||
import { Page } from 'app/core/components/PageNew/Page';
|
import { Page } from 'app/core/components/Page/Page';
|
||||||
|
|
||||||
import { DashboardModel } from '../../state';
|
import { DashboardModel } from '../../state';
|
||||||
import { AnnotationSettingsEdit, AnnotationSettingsList, newAnnotationName } from '../AnnotationSettings';
|
import { AnnotationSettingsEdit, AnnotationSettingsList, newAnnotationName } from '../AnnotationSettings';
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { selectors } from '@grafana/e2e-selectors';
|
|||||||
import { locationService } from '@grafana/runtime';
|
import { locationService } from '@grafana/runtime';
|
||||||
import { Button, ToolbarButtonRow } from '@grafana/ui';
|
import { Button, ToolbarButtonRow } from '@grafana/ui';
|
||||||
import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate';
|
import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate';
|
||||||
import { Page } from 'app/core/components/PageNew/Page';
|
import { Page } from 'app/core/components/Page/Page';
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
import { contextSrv } from 'app/core/services/context_srv';
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
import { AccessControlAction } from 'app/types';
|
import { AccessControlAction } from 'app/types';
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { connect, ConnectedProps } from 'react-redux';
|
|||||||
|
|
||||||
import { TimeZone } from '@grafana/data';
|
import { TimeZone } from '@grafana/data';
|
||||||
import { CollapsableSection, Field, Input, RadioButtonGroup, TagsInput } from '@grafana/ui';
|
import { CollapsableSection, Field, Input, RadioButtonGroup, TagsInput } from '@grafana/ui';
|
||||||
import { Page } from 'app/core/components/PageNew/Page';
|
import { Page } from 'app/core/components/Page/Page';
|
||||||
import { FolderPicker } from 'app/core/components/Select/FolderPicker';
|
import { FolderPicker } from 'app/core/components/Select/FolderPicker';
|
||||||
import { updateTimeZoneDashboard, updateWeekStartDashboard } from 'app/features/dashboard/state/actions';
|
import { updateTimeZoneDashboard, updateWeekStartDashboard } from 'app/features/dashboard/state/actions';
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import React, { useState } from 'react';
|
|||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { Button, CodeEditor, useStyles2 } from '@grafana/ui';
|
import { Button, CodeEditor, useStyles2 } from '@grafana/ui';
|
||||||
import { Page } from 'app/core/components/PageNew/Page';
|
import { Page } from 'app/core/components/Page/Page';
|
||||||
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
|
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
|
||||||
|
|
||||||
import { getDashboardSrv } from '../../services/DashboardSrv';
|
import { getDashboardSrv } from '../../services/DashboardSrv';
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React, { useState } from 'react';
|
|||||||
|
|
||||||
import { NavModelItem } from '@grafana/data';
|
import { NavModelItem } from '@grafana/data';
|
||||||
import { locationService } from '@grafana/runtime';
|
import { locationService } from '@grafana/runtime';
|
||||||
import { Page } from 'app/core/components/PageNew/Page';
|
import { Page } from 'app/core/components/Page/Page';
|
||||||
|
|
||||||
import { LinkSettingsEdit, LinkSettingsList } from '../LinksSettings';
|
import { LinkSettingsEdit, LinkSettingsList } from '../LinksSettings';
|
||||||
import { newLink } from '../LinksSettings/LinkSettingsEdit';
|
import { newLink } from '../LinksSettings/LinkSettingsEdit';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
|
|
||||||
import { Spinner, HorizontalGroup } from '@grafana/ui';
|
import { Spinner, HorizontalGroup } from '@grafana/ui';
|
||||||
import { Page } from 'app/core/components/PageNew/Page';
|
import { Page } from 'app/core/components/Page/Page';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
historySrv,
|
historySrv,
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { render, screen, waitFor } from '@testing-library/react';
|
import { render, screen, waitFor } from '@testing-library/react';
|
||||||
|
import { KBarProvider } from 'kbar';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { match, Router } from 'react-router-dom';
|
import { match, Router } from 'react-router-dom';
|
||||||
import { useEffectOnce } from 'react-use';
|
import { useEffectOnce } from 'react-use';
|
||||||
import { AutoSizerProps } from 'react-virtualized-auto-sizer';
|
import { AutoSizerProps } from 'react-virtualized-auto-sizer';
|
||||||
import { mockToolkitActionCreator } from 'test/core/redux/mocks';
|
import { mockToolkitActionCreator } from 'test/core/redux/mocks';
|
||||||
|
import { TestProvider } from 'test/helpers/TestProvider';
|
||||||
import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock';
|
import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock';
|
||||||
|
|
||||||
import { createTheme } from '@grafana/data';
|
import { createTheme } from '@grafana/data';
|
||||||
@@ -12,6 +14,7 @@ import { selectors } from '@grafana/e2e-selectors';
|
|||||||
import { config, locationService, setDataSourceSrv } from '@grafana/runtime';
|
import { config, locationService, setDataSourceSrv } from '@grafana/runtime';
|
||||||
import { Dashboard } from '@grafana/schema';
|
import { Dashboard } from '@grafana/schema';
|
||||||
import { notifyApp } from 'app/core/actions';
|
import { notifyApp } from 'app/core/actions';
|
||||||
|
import { AppChrome } from 'app/core/components/AppChrome/AppChrome';
|
||||||
import { GrafanaContext } from 'app/core/context/GrafanaContext';
|
import { GrafanaContext } from 'app/core/context/GrafanaContext';
|
||||||
import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
|
import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
|
||||||
import { RouteDescriptor } from 'app/core/navigation/types';
|
import { RouteDescriptor } from 'app/core/navigation/types';
|
||||||
@@ -58,6 +61,9 @@ jest.mock('app/core/core', () => ({
|
|||||||
return { unsubscribe: () => {} };
|
return { unsubscribe: () => {} };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
contextSrv: {
|
||||||
|
user: { orgId: 1 },
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('react-virtualized-auto-sizer', () => {
|
jest.mock('react-virtualized-auto-sizer', () => {
|
||||||
@@ -92,6 +98,10 @@ function setup(propOverrides?: Partial<Props>) {
|
|||||||
config.bootData.navTree = [
|
config.bootData.navTree = [
|
||||||
{ text: 'Dashboards', id: 'dashboards/browse' },
|
{ text: 'Dashboards', id: 'dashboards/browse' },
|
||||||
{ text: 'Home', id: HOME_NAV_ID },
|
{ text: 'Home', id: HOME_NAV_ID },
|
||||||
|
{
|
||||||
|
text: 'Help',
|
||||||
|
id: 'help',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const store = configureStore();
|
const store = configureStore();
|
||||||
@@ -177,6 +187,45 @@ describe('DashboardPage', () => {
|
|||||||
expect(document.title).toBe('My dashboard - Dashboards - Grafana');
|
expect(document.title).toBe('My dashboard - Dashboards - Grafana');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('only calls initDashboard once when wrapped in AppChrome', async () => {
|
||||||
|
const props: Props = {
|
||||||
|
...getRouteComponentProps({
|
||||||
|
match: { params: { slug: 'my-dash', uid: '11' } } as unknown as match,
|
||||||
|
route: { routeName: DashboardRoutes.Normal } as RouteDescriptor,
|
||||||
|
}),
|
||||||
|
navIndex: {
|
||||||
|
'dashboards/browse': {
|
||||||
|
text: 'Dashboards',
|
||||||
|
id: 'dashboards/browse',
|
||||||
|
parentItem: { text: 'Home', id: HOME_NAV_ID },
|
||||||
|
},
|
||||||
|
[HOME_NAV_ID]: { text: 'Home', id: HOME_NAV_ID },
|
||||||
|
},
|
||||||
|
initPhase: DashboardInitPhase.Completed,
|
||||||
|
initError: null,
|
||||||
|
initDashboard: mockInitDashboard,
|
||||||
|
notifyApp: mockToolkitActionCreator(notifyApp),
|
||||||
|
cleanUpDashboardAndVariables: mockCleanUpDashboardAndVariables,
|
||||||
|
cancelVariables: jest.fn(),
|
||||||
|
templateVarsChangedInUrl: jest.fn(),
|
||||||
|
dashboard: getTestDashboard(),
|
||||||
|
theme: createTheme(),
|
||||||
|
};
|
||||||
|
|
||||||
|
render(
|
||||||
|
<KBarProvider>
|
||||||
|
<TestProvider>
|
||||||
|
<AppChrome>
|
||||||
|
<UnthemedDashboardPage {...props} />
|
||||||
|
</AppChrome>
|
||||||
|
</TestProvider>
|
||||||
|
</KBarProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
await screen.findByText('My dashboard');
|
||||||
|
expect(mockInitDashboard).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('When going into view mode', () => {
|
describe('When going into view mode', () => {
|
||||||
|
|||||||
@@ -8,9 +8,7 @@ import { ButtonRow, Props } from './ButtonRow';
|
|||||||
const setup = (propOverrides?: object) => {
|
const setup = (propOverrides?: object) => {
|
||||||
const props: Props = {
|
const props: Props = {
|
||||||
canSave: false,
|
canSave: false,
|
||||||
canDelete: false,
|
|
||||||
onSubmit: jest.fn(),
|
onSubmit: jest.fn(),
|
||||||
onDelete: jest.fn(),
|
|
||||||
onTest: jest.fn(),
|
onTest: jest.fn(),
|
||||||
exploreUrl: '/explore',
|
exploreUrl: '/explore',
|
||||||
};
|
};
|
||||||
@@ -24,7 +22,7 @@ describe('<ButtonRow>', () => {
|
|||||||
it('should render component', () => {
|
it('should render component', () => {
|
||||||
setup();
|
setup();
|
||||||
|
|
||||||
expect(screen.getByRole('button', { name: selectors.pages.DataSource.delete })).toBeInTheDocument();
|
expect(screen.getByRole('link', { name: 'Explore' })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
it('should render save & test', () => {
|
it('should render save & test', () => {
|
||||||
setup({ canSave: true });
|
setup({ canSave: true });
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { config } from '@grafana/runtime';
|
|
||||||
import { Button, LinkButton } from '@grafana/ui';
|
import { Button, LinkButton } from '@grafana/ui';
|
||||||
import { contextSrv } from 'app/core/core';
|
import { contextSrv } from 'app/core/core';
|
||||||
import { AccessControlAction } from 'app/types';
|
import { AccessControlAction } from 'app/types';
|
||||||
@@ -9,36 +8,18 @@ import { AccessControlAction } from 'app/types';
|
|||||||
export interface Props {
|
export interface Props {
|
||||||
exploreUrl: string;
|
exploreUrl: string;
|
||||||
canSave: boolean;
|
canSave: boolean;
|
||||||
canDelete: boolean;
|
|
||||||
onDelete: () => void;
|
|
||||||
onSubmit: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
onSubmit: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||||
onTest: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
onTest: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ButtonRow({ canSave, canDelete, onDelete, onSubmit, onTest, exploreUrl }: Props) {
|
export function ButtonRow({ canSave, onSubmit, onTest, exploreUrl }: Props) {
|
||||||
const canExploreDataSources = contextSrv.hasPermission(AccessControlAction.DataSourcesExplore);
|
const canExploreDataSources = contextSrv.hasPermission(AccessControlAction.DataSourcesExplore);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="gf-form-button-row">
|
<div className="gf-form-button-row">
|
||||||
{!config.featureToggles.topnav && (
|
|
||||||
<Button variant="secondary" fill="solid" type="button" onClick={() => history.back()}>
|
|
||||||
Back
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<LinkButton variant="secondary" fill="solid" href={exploreUrl} disabled={!canExploreDataSources}>
|
<LinkButton variant="secondary" fill="solid" href={exploreUrl} disabled={!canExploreDataSources}>
|
||||||
Explore
|
Explore
|
||||||
</LinkButton>
|
</LinkButton>
|
||||||
{!config.featureToggles.topnav && (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="destructive"
|
|
||||||
disabled={!canDelete}
|
|
||||||
onClick={onDelete}
|
|
||||||
aria-label={selectors.pages.DataSource.delete}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{canSave && (
|
{canSave && (
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ export function EditDataSourceView({
|
|||||||
onUpdate,
|
onUpdate,
|
||||||
}: ViewProps) {
|
}: ViewProps) {
|
||||||
const { plugin, loadError, testingStatus, loading } = dataSourceSettings;
|
const { plugin, loadError, testingStatus, loading } = dataSourceSettings;
|
||||||
const { readOnly, hasWriteRights, hasDeleteRights } = dataSourceRights;
|
const { readOnly, hasWriteRights } = dataSourceRights;
|
||||||
const hasDataSource = dataSource.id > 0;
|
const hasDataSource = dataSource.id > 0;
|
||||||
|
|
||||||
const dsi = getDataSourceSrv()?.getInstanceSettings(dataSource.uid);
|
const dsi = getDataSourceSrv()?.getInstanceSettings(dataSource.uid);
|
||||||
@@ -175,14 +175,7 @@ export function EditDataSourceView({
|
|||||||
|
|
||||||
<DataSourceTestingStatus testingStatus={testingStatus} />
|
<DataSourceTestingStatus testingStatus={testingStatus} />
|
||||||
|
|
||||||
<ButtonRow
|
<ButtonRow onSubmit={onSubmit} onTest={onTest} exploreUrl={exploreUrl} canSave={!readOnly && hasWriteRights} />
|
||||||
onSubmit={onSubmit}
|
|
||||||
onDelete={onDelete}
|
|
||||||
onTest={onTest}
|
|
||||||
exploreUrl={exploreUrl}
|
|
||||||
canSave={!readOnly && hasWriteRights}
|
|
||||||
canDelete={!readOnly && hasDeleteRights}
|
|
||||||
/>
|
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,10 +44,7 @@ describe('Render', () => {
|
|||||||
it('should render component', async () => {
|
it('should render component', async () => {
|
||||||
setup({ isSortAscending: true });
|
setup({ isSortAscending: true });
|
||||||
|
|
||||||
expect(await screen.findByRole('heading', { name: 'Configuration' })).toBeInTheDocument();
|
expect(await screen.findByRole('heading', { name: 'Data sources' })).toBeInTheDocument();
|
||||||
expect(await screen.findByRole('link', { name: 'Documentation' })).toBeInTheDocument();
|
|
||||||
expect(await screen.findByRole('link', { name: 'Support' })).toBeInTheDocument();
|
|
||||||
expect(await screen.findByRole('link', { name: 'Community' })).toBeInTheDocument();
|
|
||||||
expect(await screen.findByRole('link', { name: 'Add data source' })).toBeInTheDocument();
|
expect(await screen.findByRole('link', { name: 'Add data source' })).toBeInTheDocument();
|
||||||
|
|
||||||
// Should not show button in page header when the list is empty
|
// Should not show button in page header when the list is empty
|
||||||
@@ -62,10 +59,7 @@ describe('Render', () => {
|
|||||||
it('should disable the "Add data source" button if user has no permissions', async () => {
|
it('should disable the "Add data source" button if user has no permissions', async () => {
|
||||||
setup({ isSortAscending: true });
|
setup({ isSortAscending: true });
|
||||||
|
|
||||||
expect(await screen.findByRole('heading', { name: 'Configuration' })).toBeInTheDocument();
|
expect(await screen.findByRole('heading', { name: 'Data sources' })).toBeInTheDocument();
|
||||||
expect(await screen.findByRole('link', { name: 'Documentation' })).toBeInTheDocument();
|
|
||||||
expect(await screen.findByRole('link', { name: 'Support' })).toBeInTheDocument();
|
|
||||||
expect(await screen.findByRole('link', { name: 'Community' })).toBeInTheDocument();
|
|
||||||
expect(await screen.findByRole('link', { name: 'Add data source' })).toHaveStyle('pointer-events: none');
|
expect(await screen.findByRole('link', { name: 'Add data source' })).toHaveStyle('pointer-events: none');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -129,7 +123,7 @@ describe('Render', () => {
|
|||||||
const dataSourceItems = await screen.findAllByRole('heading');
|
const dataSourceItems = await screen.findAllByRole('heading');
|
||||||
|
|
||||||
expect(dataSourceItems).toHaveLength(6);
|
expect(dataSourceItems).toHaveLength(6);
|
||||||
expect(dataSourceItems[0]).toHaveTextContent('Configuration');
|
expect(dataSourceItems[0]).toHaveTextContent('Data sources');
|
||||||
expect(dataSourceItems[1]).toHaveTextContent('dataSource-0');
|
expect(dataSourceItems[1]).toHaveTextContent('dataSource-0');
|
||||||
expect(dataSourceItems[2]).toHaveTextContent('dataSource-1');
|
expect(dataSourceItems[2]).toHaveTextContent('dataSource-1');
|
||||||
});
|
});
|
||||||
@@ -141,7 +135,7 @@ describe('Render', () => {
|
|||||||
const dataSourceItems = await screen.findAllByRole('heading');
|
const dataSourceItems = await screen.findAllByRole('heading');
|
||||||
|
|
||||||
expect(dataSourceItems).toHaveLength(6);
|
expect(dataSourceItems).toHaveLength(6);
|
||||||
expect(dataSourceItems[0]).toHaveTextContent('Configuration');
|
expect(dataSourceItems[0]).toHaveTextContent('Data sources');
|
||||||
expect(dataSourceItems[1]).toHaveTextContent('dataSource-4');
|
expect(dataSourceItems[1]).toHaveTextContent('dataSource-4');
|
||||||
expect(dataSourceItems[2]).toHaveTextContent('dataSource-3');
|
expect(dataSourceItems[2]).toHaveTextContent('dataSource-3');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { TestProvider } from 'test/helpers/TestProvider';
|
|||||||
|
|
||||||
import { LayoutModes } from '@grafana/data';
|
import { LayoutModes } from '@grafana/data';
|
||||||
import { setAngularLoader } from '@grafana/runtime';
|
import { setAngularLoader } from '@grafana/runtime';
|
||||||
import config from 'app/core/config';
|
|
||||||
import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
|
import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
|
||||||
import { configureStore } from 'app/store/configureStore';
|
import { configureStore } from 'app/store/configureStore';
|
||||||
|
|
||||||
@@ -58,7 +57,6 @@ describe('<EditDataSourcePage>', () => {
|
|||||||
const dataSourceMeta = getMockDataSourceMeta();
|
const dataSourceMeta = getMockDataSourceMeta();
|
||||||
const dataSourceSettings = getMockDataSourceSettingsState();
|
const dataSourceSettings = getMockDataSourceSettingsState();
|
||||||
let store: Store;
|
let store: Store;
|
||||||
const topnavValue = config.featureToggles.topnav;
|
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
setAngularLoader({
|
setAngularLoader({
|
||||||
@@ -96,12 +94,7 @@ describe('<EditDataSourcePage>', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
config.featureToggles.topnav = topnavValue;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render the edit page without an issue', async () => {
|
it('should render the edit page without an issue', async () => {
|
||||||
config.featureToggles.topnav = false;
|
|
||||||
setup(uid, store);
|
setup(uid, store);
|
||||||
|
|
||||||
expect(screen.queryByText('Loading ...')).not.toBeInTheDocument();
|
expect(screen.queryByText('Loading ...')).not.toBeInTheDocument();
|
||||||
@@ -109,23 +102,8 @@ describe('<EditDataSourcePage>', () => {
|
|||||||
// Title
|
// Title
|
||||||
expect(screen.queryByText(name)).toBeVisible();
|
expect(screen.queryByText(name)).toBeVisible();
|
||||||
|
|
||||||
// Buttons
|
|
||||||
expect(screen.queryByRole('button', { name: /Back/i })).toBeVisible();
|
|
||||||
expect(screen.queryByRole('button', { name: /Delete/i })).toBeVisible();
|
|
||||||
expect(screen.queryByRole('button', { name: /Save (.*) test/i })).toBeVisible();
|
|
||||||
expect(screen.queryByRole('link', { name: /Explore/i })).toBeVisible();
|
|
||||||
|
|
||||||
// wait for the rest of the async processes to finish
|
|
||||||
expect(await screen.findByText(name)).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show updated action buttons when topnav is on', async () => {
|
|
||||||
config.featureToggles.topnav = true;
|
|
||||||
setup(uid, store);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
// Buttons
|
// Buttons
|
||||||
expect(screen.queryAllByRole('button', { name: /Back/i })).toHaveLength(0);
|
|
||||||
expect(screen.queryByRole('button', { name: /Delete/i })).toBeVisible();
|
expect(screen.queryByRole('button', { name: /Delete/i })).toBeVisible();
|
||||||
expect(screen.queryByRole('button', { name: /Save (.*) test/i })).toBeVisible();
|
expect(screen.queryByRole('button', { name: /Save (.*) test/i })).toBeVisible();
|
||||||
expect(screen.queryByRole('link', { name: /Build a dashboard/i })).toBeVisible();
|
expect(screen.queryByRole('link', { name: /Build a dashboard/i })).toBeVisible();
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { config } from '@grafana/runtime';
|
|
||||||
import { Page } from 'app/core/components/Page/Page';
|
import { Page } from 'app/core/components/Page/Page';
|
||||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||||
|
|
||||||
@@ -17,11 +16,7 @@ export function EditDataSourcePage(props: Props) {
|
|||||||
const nav = useDataSourceSettingsNav(uid, pageId);
|
const nav = useDataSourceSettingsNav(uid, pageId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page
|
<Page navId="datasources" pageNav={nav.main} actions={<EditDataSourceActions uid={uid} />}>
|
||||||
navId="datasources"
|
|
||||||
pageNav={nav.main}
|
|
||||||
actions={config.featureToggles.topnav ? <EditDataSourceActions uid={uid} /> : undefined}
|
|
||||||
>
|
|
||||||
<Page.Contents>
|
<Page.Contents>
|
||||||
<EditDataSource uid={uid} pageId={pageId} />
|
<EditDataSource uid={uid} pageId={pageId} />
|
||||||
</Page.Contents>
|
</Page.Contents>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { connect, ConnectedProps } from 'react-redux';
|
|||||||
import { bindActionCreators } from 'redux';
|
import { bindActionCreators } from 'redux';
|
||||||
|
|
||||||
import { locationService } from '@grafana/runtime';
|
import { locationService } from '@grafana/runtime';
|
||||||
import { Page } from 'app/core/components/PageNew/Page';
|
import { Page } from 'app/core/components/Page/Page';
|
||||||
import { SettingsPageProps } from 'app/features/dashboard/components/DashboardSettings/types';
|
import { SettingsPageProps } from 'app/features/dashboard/components/DashboardSettings/types';
|
||||||
|
|
||||||
import { StoreState, ThunkDispatch } from '../../../types';
|
import { StoreState, ThunkDispatch } from '../../../types';
|
||||||
|
|||||||
Reference in New Issue
Block a user