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:
Ashley Harrison 2023-04-24 16:41:32 +01:00 committed by GitHub
parent 67ca91ece3
commit 4e492ae725
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 177 additions and 399 deletions

View File

@ -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"]

View File

@ -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';

View File

@ -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';

View File

@ -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');
});
});

View File

@ -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,
}),
};
};

View File

@ -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}</>;
};

View File

@ -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;

View File

@ -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);

View File

@ -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');
});
});

View File

@ -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,
}),
};
};

View File

@ -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}</>;
};

View File

@ -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>

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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,

View File

@ -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', () => {

View File

@ -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 });

View File

@ -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"

View File

@ -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>
);
}

View File

@ -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');
});

View File

@ -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();

View File

@ -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>

View File

@ -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';