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:
parent
67ca91ece3
commit
4e492ae725
@ -2686,8 +2686,7 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
|
||||
],
|
||||
"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", "1"]
|
||||
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
|
||||
],
|
||||
"public/app/features/datasources/components/DataSourceReadOnlyMessage.tsx:5381": [
|
||||
[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 { AppChromeService } from './core/components/AppChrome/AppChromeService';
|
||||
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 { initializeI18n } from './core/internationalization';
|
||||
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 { DashboardQueryResult, getGrafanaSearcher, QueryResponse } from 'app/features/search/service';
|
||||
|
||||
import { Page } from '../PageNew/Page';
|
||||
import { Page } from '../Page/Page';
|
||||
|
||||
import { AppChrome } from './AppChrome';
|
||||
|
||||
|
@ -1,23 +1,28 @@
|
||||
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 } from '@grafana/data';
|
||||
import { NavModelItem, PageLayoutType } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { HOME_NAV_ID } from 'app/core/reducers/navModel';
|
||||
|
||||
import { Page } from './Page';
|
||||
import { PageProps } from './types';
|
||||
|
||||
const pageNav: NavModelItem = {
|
||||
text: 'Main title',
|
||||
text: 'pageNav title',
|
||||
children: [
|
||||
{ text: 'Child1', url: '1', active: true },
|
||||
{ text: 'Child2', url: '2' },
|
||||
{ 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',
|
||||
@ -29,13 +34,17 @@ const setup = (props: Partial<PageProps>) => {
|
||||
},
|
||||
];
|
||||
|
||||
return render(
|
||||
<TestProvider>
|
||||
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', () => {
|
||||
@ -51,14 +60,25 @@ describe('Render', () => {
|
||||
it('should render header when pageNav supplied', async () => {
|
||||
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);
|
||||
});
|
||||
|
||||
it('should get header nav model from redux navIndex', async () => {
|
||||
setup({ navId: 'child1' });
|
||||
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);
|
||||
});
|
||||
|
||||
expect(screen.getByRole('heading', { name: 'Section name' })).toBeInTheDocument();
|
||||
expect(screen.getAllByRole('tab').length).toBe(2);
|
||||
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,35 +1,32 @@
|
||||
// Libraries
|
||||
import { css, cx } from '@emotion/css';
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useLayoutEffect } from 'react';
|
||||
|
||||
import { GrafanaTheme2, PageLayoutType } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { CustomScrollbar, useStyles2 } from '@grafana/ui';
|
||||
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 { PageHeader } from './PageHeader';
|
||||
import { PageTabs } from './PageTabs';
|
||||
import { PageType } from './types';
|
||||
import { usePageNav } from './usePageNav';
|
||||
import { usePageTitle } from './usePageTitle';
|
||||
|
||||
export const OldPage: PageType = ({
|
||||
export const Page: PageType = ({
|
||||
navId,
|
||||
navModel: oldNavProp,
|
||||
pageNav,
|
||||
renderTitle,
|
||||
actions,
|
||||
subTitle,
|
||||
children,
|
||||
className,
|
||||
toolbar,
|
||||
scrollRef,
|
||||
scrollTop,
|
||||
layout = PageLayoutType.Standard,
|
||||
renderTitle,
|
||||
subTitle,
|
||||
actions,
|
||||
info,
|
||||
layout = PageLayoutType.Standard,
|
||||
toolbar,
|
||||
scrollTop,
|
||||
scrollRef,
|
||||
...otherProps
|
||||
}) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
@ -38,48 +35,46 @@ export const OldPage: PageType = ({
|
||||
|
||||
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) {
|
||||
// This is needed for chrome to update it's chromeless state
|
||||
chrome.update({
|
||||
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 (
|
||||
<div className={cx(styles.wrapper, className)} {...otherProps}>
|
||||
{layout === PageLayoutType.Standard && (
|
||||
<CustomScrollbar autoHeightMin={'100%'} scrollTop={scrollTop} scrollRefCallback={scrollRef}>
|
||||
<div className={cx('page-scrollbar-content', className)}>
|
||||
<div className={styles.pageInner}>
|
||||
{pageHeaderNav && (
|
||||
<PageHeader
|
||||
actions={actions}
|
||||
info={info}
|
||||
navItem={pageHeaderNav}
|
||||
renderTitle={renderTitle}
|
||||
info={info}
|
||||
subTitle={subTitle}
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
<Footer />
|
||||
{pageNav && pageNav.children && <PageTabs navItem={pageNav} />}
|
||||
<div className={styles.pageContent}>{children}</div>
|
||||
</div>
|
||||
</CustomScrollbar>
|
||||
)}
|
||||
{layout === PageLayoutType.Canvas && (
|
||||
<>
|
||||
{toolbar}
|
||||
<div className={styles.scrollWrapper}>
|
||||
<CustomScrollbar autoHeightMin={'100%'} scrollTop={scrollTop} scrollRefCallback={scrollRef}>
|
||||
<div className={cx(styles.content, !toolbar && styles.contentWithoutToolbar)}>{children}</div>
|
||||
</CustomScrollbar>
|
||||
<CustomScrollbar autoHeightMin={'100%'} scrollTop={scrollTop} scrollRefCallback={scrollRef}>
|
||||
<div className={styles.canvasContent}>
|
||||
{toolbar}
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
</CustomScrollbar>
|
||||
)}
|
||||
{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) => ({
|
||||
wrapper: css({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flex: '1 1 0',
|
||||
flexDirection: 'column',
|
||||
minHeight: 0,
|
||||
}),
|
||||
scrollWrapper: css({
|
||||
width: '100%',
|
||||
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),
|
||||
}),
|
||||
});
|
||||
[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,8 +1,6 @@
|
||||
// Libraries
|
||||
import { cx } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
// Components
|
||||
import PageLoader from '../PageLoader/PageLoader';
|
||||
|
||||
interface Props {
|
||||
@ -12,5 +10,7 @@ interface 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 { useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { PageInfoItem } from '../Page/types';
|
||||
import { PageInfo } from '../PageInfo/PageInfo';
|
||||
|
||||
import { PageInfoItem } from './types';
|
||||
|
||||
export interface Props {
|
||||
navItem: NavModelItem;
|
||||
renderTitle?: (title: string) => React.ReactNode;
|
@ -3,7 +3,7 @@ import React, { useContext } from 'react';
|
||||
import { PluginPageProps } from '@grafana/runtime';
|
||||
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) {
|
||||
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 { useLocation, useParams } from 'react-router-dom';
|
||||
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import { EditDataSource } from 'app/features/datasources/components/EditDataSource';
|
||||
import { EditDataSourceActions } from 'app/features/datasources/components/EditDataSourceActions';
|
||||
@ -16,11 +15,7 @@ export function EditDataSourcePage() {
|
||||
const { navId, pageNav } = useDataSourceSettingsNav();
|
||||
|
||||
return (
|
||||
<Page
|
||||
navId={navId}
|
||||
pageNav={pageNav}
|
||||
actions={config.featureToggles.topnav ? <EditDataSourceActions uid={uid} /> : undefined}
|
||||
>
|
||||
<Page navId={navId} pageNav={pageNav} actions={<EditDataSourceActions uid={uid} />}>
|
||||
<Page.Contents>
|
||||
<EditDataSource uid={uid} pageId={pageId} />
|
||||
</Page.Contents>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
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 { AccessControlAction } from 'app/types';
|
||||
|
||||
|
@ -3,7 +3,7 @@ import { connect, ConnectedProps } from 'react-redux';
|
||||
|
||||
import { Tooltip, Icon, Button } from '@grafana/ui';
|
||||
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 PermissionList from 'app/core/components/PermissionList/PermissionList';
|
||||
import PermissionsInfo from 'app/core/components/PermissionList/PermissionsInfo';
|
||||
|
@ -2,7 +2,7 @@ import React from 'react';
|
||||
|
||||
import { AnnotationQuery, getDataSourceRef, NavModelItem } from '@grafana/data';
|
||||
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 { AnnotationSettingsEdit, AnnotationSettingsList, newAnnotationName } from '../AnnotationSettings';
|
||||
|
@ -7,7 +7,7 @@ import { selectors } from '@grafana/e2e-selectors';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { Button, ToolbarButtonRow } from '@grafana/ui';
|
||||
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 { contextSrv } from 'app/core/services/context_srv';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
|
@ -3,7 +3,7 @@ import { connect, ConnectedProps } from 'react-redux';
|
||||
|
||||
import { TimeZone } from '@grafana/data';
|
||||
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 { updateTimeZoneDashboard, updateWeekStartDashboard } from 'app/features/dashboard/state/actions';
|
||||
|
||||
|
@ -3,7 +3,7 @@ import React, { useState } from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
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 { getDashboardSrv } from '../../services/DashboardSrv';
|
||||
|
@ -2,7 +2,7 @@ import React, { useState } from 'react';
|
||||
|
||||
import { NavModelItem } from '@grafana/data';
|
||||
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 { newLink } from '../LinksSettings/LinkSettingsEdit';
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
import { Spinner, HorizontalGroup } from '@grafana/ui';
|
||||
import { Page } from 'app/core/components/PageNew/Page';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
|
||||
import {
|
||||
historySrv,
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { KBarProvider } from 'kbar';
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { match, Router } from 'react-router-dom';
|
||||
import { useEffectOnce } from 'react-use';
|
||||
import { AutoSizerProps } from 'react-virtualized-auto-sizer';
|
||||
import { mockToolkitActionCreator } from 'test/core/redux/mocks';
|
||||
import { TestProvider } from 'test/helpers/TestProvider';
|
||||
import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock';
|
||||
|
||||
import { createTheme } from '@grafana/data';
|
||||
@ -12,6 +14,7 @@ import { selectors } from '@grafana/e2e-selectors';
|
||||
import { config, locationService, setDataSourceSrv } from '@grafana/runtime';
|
||||
import { Dashboard } from '@grafana/schema';
|
||||
import { notifyApp } from 'app/core/actions';
|
||||
import { AppChrome } from 'app/core/components/AppChrome/AppChrome';
|
||||
import { GrafanaContext } from 'app/core/context/GrafanaContext';
|
||||
import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
|
||||
import { RouteDescriptor } from 'app/core/navigation/types';
|
||||
@ -58,6 +61,9 @@ jest.mock('app/core/core', () => ({
|
||||
return { unsubscribe: () => {} };
|
||||
},
|
||||
},
|
||||
contextSrv: {
|
||||
user: { orgId: 1 },
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('react-virtualized-auto-sizer', () => {
|
||||
@ -92,6 +98,10 @@ function setup(propOverrides?: Partial<Props>) {
|
||||
config.bootData.navTree = [
|
||||
{ text: 'Dashboards', id: 'dashboards/browse' },
|
||||
{ text: 'Home', id: HOME_NAV_ID },
|
||||
{
|
||||
text: 'Help',
|
||||
id: 'help',
|
||||
},
|
||||
];
|
||||
|
||||
const store = configureStore();
|
||||
@ -177,6 +187,45 @@ describe('DashboardPage', () => {
|
||||
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', () => {
|
||||
|
@ -8,9 +8,7 @@ import { ButtonRow, Props } from './ButtonRow';
|
||||
const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
canSave: false,
|
||||
canDelete: false,
|
||||
onSubmit: jest.fn(),
|
||||
onDelete: jest.fn(),
|
||||
onTest: jest.fn(),
|
||||
exploreUrl: '/explore',
|
||||
};
|
||||
@ -24,7 +22,7 @@ describe('<ButtonRow>', () => {
|
||||
it('should render component', () => {
|
||||
setup();
|
||||
|
||||
expect(screen.getByRole('button', { name: selectors.pages.DataSource.delete })).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: 'Explore' })).toBeInTheDocument();
|
||||
});
|
||||
it('should render save & test', () => {
|
||||
setup({ canSave: true });
|
||||
|
@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Button, LinkButton } from '@grafana/ui';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
@ -9,36 +8,18 @@ import { AccessControlAction } from 'app/types';
|
||||
export interface Props {
|
||||
exploreUrl: string;
|
||||
canSave: boolean;
|
||||
canDelete: boolean;
|
||||
onDelete: () => void;
|
||||
onSubmit: (event: React.MouseEvent<HTMLButtonElement>) => 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);
|
||||
|
||||
return (
|
||||
<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}>
|
||||
Explore
|
||||
</LinkButton>
|
||||
{!config.featureToggles.topnav && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
disabled={!canDelete}
|
||||
onClick={onDelete}
|
||||
aria-label={selectors.pages.DataSource.delete}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
{canSave && (
|
||||
<Button
|
||||
type="submit"
|
||||
|
@ -108,7 +108,7 @@ export function EditDataSourceView({
|
||||
onUpdate,
|
||||
}: ViewProps) {
|
||||
const { plugin, loadError, testingStatus, loading } = dataSourceSettings;
|
||||
const { readOnly, hasWriteRights, hasDeleteRights } = dataSourceRights;
|
||||
const { readOnly, hasWriteRights } = dataSourceRights;
|
||||
const hasDataSource = dataSource.id > 0;
|
||||
|
||||
const dsi = getDataSourceSrv()?.getInstanceSettings(dataSource.uid);
|
||||
@ -175,14 +175,7 @@ export function EditDataSourceView({
|
||||
|
||||
<DataSourceTestingStatus testingStatus={testingStatus} />
|
||||
|
||||
<ButtonRow
|
||||
onSubmit={onSubmit}
|
||||
onDelete={onDelete}
|
||||
onTest={onTest}
|
||||
exploreUrl={exploreUrl}
|
||||
canSave={!readOnly && hasWriteRights}
|
||||
canDelete={!readOnly && hasDeleteRights}
|
||||
/>
|
||||
<ButtonRow onSubmit={onSubmit} onTest={onTest} exploreUrl={exploreUrl} canSave={!readOnly && hasWriteRights} />
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
@ -44,10 +44,7 @@ describe('Render', () => {
|
||||
it('should render component', async () => {
|
||||
setup({ isSortAscending: true });
|
||||
|
||||
expect(await screen.findByRole('heading', { name: 'Configuration' })).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('heading', { name: 'Data sources' })).toBeInTheDocument();
|
||||
expect(await screen.findByRole('link', { name: 'Add data source' })).toBeInTheDocument();
|
||||
|
||||
// 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 () => {
|
||||
setup({ isSortAscending: true });
|
||||
|
||||
expect(await screen.findByRole('heading', { name: 'Configuration' })).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('heading', { name: 'Data sources' })).toBeInTheDocument();
|
||||
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');
|
||||
|
||||
expect(dataSourceItems).toHaveLength(6);
|
||||
expect(dataSourceItems[0]).toHaveTextContent('Configuration');
|
||||
expect(dataSourceItems[0]).toHaveTextContent('Data sources');
|
||||
expect(dataSourceItems[1]).toHaveTextContent('dataSource-0');
|
||||
expect(dataSourceItems[2]).toHaveTextContent('dataSource-1');
|
||||
});
|
||||
@ -141,7 +135,7 @@ describe('Render', () => {
|
||||
const dataSourceItems = await screen.findAllByRole('heading');
|
||||
|
||||
expect(dataSourceItems).toHaveLength(6);
|
||||
expect(dataSourceItems[0]).toHaveTextContent('Configuration');
|
||||
expect(dataSourceItems[0]).toHaveTextContent('Data sources');
|
||||
expect(dataSourceItems[1]).toHaveTextContent('dataSource-4');
|
||||
expect(dataSourceItems[2]).toHaveTextContent('dataSource-3');
|
||||
});
|
||||
|
@ -5,7 +5,6 @@ import { TestProvider } from 'test/helpers/TestProvider';
|
||||
|
||||
import { LayoutModes } from '@grafana/data';
|
||||
import { setAngularLoader } from '@grafana/runtime';
|
||||
import config from 'app/core/config';
|
||||
import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
|
||||
@ -58,7 +57,6 @@ describe('<EditDataSourcePage>', () => {
|
||||
const dataSourceMeta = getMockDataSourceMeta();
|
||||
const dataSourceSettings = getMockDataSourceSettingsState();
|
||||
let store: Store;
|
||||
const topnavValue = config.featureToggles.topnav;
|
||||
|
||||
beforeAll(() => {
|
||||
setAngularLoader({
|
||||
@ -96,12 +94,7 @@ describe('<EditDataSourcePage>', () => {
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
config.featureToggles.topnav = topnavValue;
|
||||
});
|
||||
|
||||
it('should render the edit page without an issue', async () => {
|
||||
config.featureToggles.topnav = false;
|
||||
setup(uid, store);
|
||||
|
||||
expect(screen.queryByText('Loading ...')).not.toBeInTheDocument();
|
||||
@ -109,23 +102,8 @@ describe('<EditDataSourcePage>', () => {
|
||||
// Title
|
||||
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(() => {
|
||||
// Buttons
|
||||
expect(screen.queryAllByRole('button', { name: /Back/i })).toHaveLength(0);
|
||||
expect(screen.queryByRole('button', { name: /Delete/i })).toBeVisible();
|
||||
expect(screen.queryByRole('button', { name: /Save (.*) test/i })).toBeVisible();
|
||||
expect(screen.queryByRole('link', { name: /Build a dashboard/i })).toBeVisible();
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||
|
||||
@ -17,11 +16,7 @@ export function EditDataSourcePage(props: Props) {
|
||||
const nav = useDataSourceSettingsNav(uid, pageId);
|
||||
|
||||
return (
|
||||
<Page
|
||||
navId="datasources"
|
||||
pageNav={nav.main}
|
||||
actions={config.featureToggles.topnav ? <EditDataSourceActions uid={uid} /> : undefined}
|
||||
>
|
||||
<Page navId="datasources" pageNav={nav.main} actions={<EditDataSourceActions uid={uid} />}>
|
||||
<Page.Contents>
|
||||
<EditDataSource uid={uid} pageId={pageId} />
|
||||
</Page.Contents>
|
||||
|
@ -3,7 +3,7 @@ import { connect, ConnectedProps } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
|
||||
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 { StoreState, ThunkDispatch } from '../../../types';
|
||||
|
Loading…
Reference in New Issue
Block a user