TopNav: New page layouts (#51510)

* First stab at new page layouts behind feature toggle

* Simplifying PageHeader

* Progress on a new model that can more easily support new and old page layouts

* Progress

* rename folder

* Progress

* Minor change

* fixes

* Fixing tests

* Make breadcrumbs work

* Add tests for old Page component

* Adding tests for new Page component and behavior

* fixing page header test

* Fixed test

* AppChrome outside route

* Renaming folder

* Minor fix

* Updated

* Fixing StoragePage

* Fix for banners

Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com>
This commit is contained in:
Torkel Ödegaard
2022-07-06 17:00:56 +02:00
committed by GitHub
parent 663f3fcd2a
commit 1e85a6f4fd
106 changed files with 927 additions and 347 deletions

View File

@@ -5202,6 +5202,12 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"]
],
"public/app/core/components/PageNew/PageTabs.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/core/components/PageNew/SectionNav.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/core/components/PanelTypeFilter/PanelTypeFilter.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
@@ -7897,6 +7903,9 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"]
],
"public/app/features/teams/CreateTeam.test.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/features/teams/TeamGroupSync.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]

View File

@@ -56,10 +56,6 @@ export interface NavModel {
* This is the current active tab/navigation.
*/
node: NavModelItem;
/**
* Describes breadcrumbs that are used in places such as data source settings., folder page and plugins page.
*/
breadcrumbs?: NavModelItem[];
}
export interface NavModelBreadcrumb {

View File

@@ -0,0 +1,96 @@
import { css, cx } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { useStyles2 } from '../../themes/ThemeContext';
import { Icon } from '../Icon/Icon';
import { Counter } from './Counter';
import { TabProps } from './Tab';
export const VerticalTab = React.forwardRef<HTMLAnchorElement, TabProps>(
({ label, active, icon, counter, className, suffix: Suffix, onChangeTab, href, ...otherProps }, ref) => {
const tabsStyles = useStyles2(getTabStyles);
const content = () => (
<>
{icon && <Icon name={icon} />}
{label}
{typeof counter === 'number' && <Counter value={counter} />}
{Suffix && <Suffix className={tabsStyles.suffix} />}
</>
);
const linkClass = cx(tabsStyles.link, active && tabsStyles.activeStyle);
return (
<li className={tabsStyles.item}>
<a
href={href}
className={linkClass}
{...otherProps}
onClick={onChangeTab}
aria-label={otherProps['aria-label'] || selectors.components.Tab.title(label)}
role="tab"
aria-selected={active}
ref={ref}
>
{content()}
</a>
</li>
);
}
);
VerticalTab.displayName = 'Tab';
const getTabStyles = (theme: GrafanaTheme2) => {
return {
item: css`
list-style: none;
margin-right: ${theme.spacing(2)};
position: relative;
display: block;
margin-bottom: 4px;
`,
link: css`
padding: 6px 12px;
display: block;
height: 100%;
cursor: pointer;
color: ${theme.colors.text.primary};
svg {
margin-right: ${theme.spacing(1)};
}
&:hover,
&:focus {
text-decoration: underline;
}
`,
activeStyle: css`
label: activeTabStyle;
color: ${theme.colors.text.maxContrast};
font-weight: 500;
overflow: hidden;
&::before {
display: block;
content: ' ';
position: absolute;
left: 0;
width: 4px;
bottom: 0;
top: 0;
border-radius: 2px;
background-image: linear-gradient(0deg, #f05a28 30%, #fbca0a 99%);
}
`,
suffix: css`
margin-left: ${theme.spacing(1)};
`,
};
};

View File

@@ -79,6 +79,7 @@ export { TableCellDisplayMode, TableSortByFieldState } from './Table/types';
export { TableInputCSV } from './TableInputCSV/TableInputCSV';
export { TabsBar } from './Tabs/TabsBar';
export { Tab } from './Tabs/Tab';
export { VerticalTab } from './Tabs/VerticalTab';
export { TabContent } from './Tabs/TabContent';
export { Counter } from './Tabs/Counter';

View File

@@ -12,6 +12,7 @@ import { store } from 'app/store/store';
import { AngularRoot } from './angular/AngularRoot';
import { loadAndInitAngularIfEnabled } from './angular/loadAndInitAngularIfEnabled';
import { GrafanaApp } from './app';
import { AppChrome } from './core/components/AppChrome/AppChrome';
import { AppNotificationList } from './core/components/AppNotifications/AppNotificationList';
import { NavBar } from './core/components/NavBar/NavBar';
import { I18nProvider } from './core/localisation';
@@ -125,7 +126,7 @@ export class AppWrapper extends React.Component<AppWrapperProps, AppWrapperState
<div className="grafana-app">
<Router history={locationService.getHistory()}>
{this.renderNavBar()}
<main className="main-view">
<AppChrome>
{pageBanners.map((Banner, index) => (
<Banner key={index.toString()} />
))}
@@ -137,7 +138,7 @@ export class AppWrapper extends React.Component<AppWrapperProps, AppWrapperState
{bodyRenderHooks.map((Hook, index) => (
<Hook key={index.toString()} />
))}
</main>
</AppChrome>
</Router>
</div>
<LiveConnectionWarning />

View File

@@ -17,7 +17,7 @@ import { QueryEditor as CloudMonitoringQueryEditor } from 'app/plugins/datasourc
import EmptyListCTA from '../core/components/EmptyListCTA/EmptyListCTA';
import { Footer } from '../core/components/Footer/Footer';
import PageHeader from '../core/components/PageHeader/PageHeader';
import { PageHeader } from '../core/components/PageHeader/PageHeader';
import { MetricSelect } from '../core/components/Select/MetricSelect';
import { TagFilter } from '../core/components/TagFilter/TagFilter';
import { HelpModal } from '../core/components/help/HelpModal';

View File

@@ -71,7 +71,6 @@ export function getWarningNav(text: string, subTitle?: string): NavModel {
icon: 'exclamation-triangle',
};
return {
breadcrumbs: [node],
node: node,
main: node,
};

View File

@@ -1,75 +1,64 @@
import { css, cx } from '@emotion/css';
import React, { PropsWithChildren, useState } from 'react';
import { useSelector } from 'react-redux';
import { useObservable, useToggle } from 'react-use';
import { createSelector } from 'reselect';
import { useToggle } from 'react-use';
import { GrafanaTheme2 } from '@grafana/data';
import { config } from '@grafana/runtime';
import { useStyles2 } from '@grafana/ui';
import { getNavModel } from 'app/core/selectors/navModel';
import { StoreState } from 'app/types';
import { MegaMenu } from '../MegaMenu/MegaMenu';
import { appChromeService } from './AppChromeService';
import { NavToolbar } from './NavToolbar';
import { topNavDefaultProps, topNavUpdates } from './TopNavUpdate';
import { TopSearchBar } from './TopSearchBar';
import { TOP_BAR_LEVEL_HEIGHT } from './types';
export interface Props extends PropsWithChildren<{}> {
/** This is nav tree id provided by route.
* It's not enough for item navigation. For that pages will need provide an item nav model as well via TopNavUpdate
*/
navId?: string;
}
export interface Props extends PropsWithChildren<{}> {}
export function TopNavPage({ children, navId }: Props) {
export function AppChrome({ children }: Props) {
const styles = useStyles2(getStyles);
const [searchBarHidden, toggleSearchBar] = useToggle(false); // repace with local storage
const props = useObservable(topNavUpdates, topNavDefaultProps);
const navModel = useSelector(createSelector(getNavIndex, (navIndex) => getNavModel(navIndex, navId ?? 'home')));
const [megaMenuOpen, setMegaMenuOpen] = useState(false);
const state = appChromeService.useState();
if (state.chromeless || !config.featureToggles.topnav) {
return <main className="main-view">{children} </main>;
}
return (
<div className={styles.viewport}>
<main className="main-view">
<div className={styles.topNav}>
{!searchBarHidden && <TopSearchBar />}
<NavToolbar
{...props}
searchBarHidden={searchBarHidden}
sectionNav={state.sectionNav}
pageNav={state.pageNav}
actions={state.actions}
onToggleSearchBar={toggleSearchBar}
onToggleMegaMenu={() => setMegaMenuOpen(!megaMenuOpen)}
sectionNav={navModel.node}
/>
</div>
<div className={cx(styles.content, searchBarHidden && styles.contentNoSearchBar)}>{children}</div>
{megaMenuOpen && <MegaMenu searchBarHidden={searchBarHidden} onClose={() => setMegaMenuOpen(false)} />}
</div>
</main>
);
}
function getNavIndex(store: StoreState) {
return store.navIndex;
}
const getStyles = (theme: GrafanaTheme2) => {
const shadow = theme.isDark
? `0 0.6px 1.5px rgb(0 0 0), 0 2px 4px rgb(0 0 0 / 40%), 0 5px 10px rgb(0 0 0 / 23%)`
: '0 0.6px 1.5px rgb(0 0 0 / 8%), 0 2px 4px rgb(0 0 0 / 6%), 0 5px 10px rgb(0 0 0 / 5%)';
return {
viewport: css({
content: css({
display: 'flex',
flexDirection: 'column',
paddingTop: TOP_BAR_LEVEL_HEIGHT * 2,
flexGrow: 1,
height: '100%',
}),
content: css({
display: 'flex',
paddingTop: TOP_BAR_LEVEL_HEIGHT * 2 + 16,
flexGrow: 1,
}),
contentNoSearchBar: css({
paddingTop: TOP_BAR_LEVEL_HEIGHT + 16,
paddingTop: TOP_BAR_LEVEL_HEIGHT,
}),
topNav: css({
display: 'flex',

View File

@@ -0,0 +1,51 @@
import { useObservable } from 'react-use';
import { BehaviorSubject } from 'rxjs';
import { NavModelItem } from '@grafana/data';
import { isShallowEqual } from 'app/core/utils/isShallowEqual';
import { RouteDescriptor } from '../../navigation/types';
export interface AppChromeState {
chromeless: boolean;
sectionNav: NavModelItem;
pageNav?: NavModelItem;
actions?: React.ReactNode;
}
const defaultSection: NavModelItem = { text: 'Grafana' };
export class AppChromeService {
readonly state = new BehaviorSubject<AppChromeState>({
chromeless: true, // start out hidden to not flash it on pages without chrome
sectionNav: defaultSection,
});
routeMounted(route: RouteDescriptor) {
this.update({
chromeless: route.chromeless === true,
sectionNav: defaultSection,
pageNav: undefined,
actions: undefined,
});
}
update(state: Partial<AppChromeState>) {
const current = this.state.getValue();
const newState: AppChromeState = {
...current,
...state,
};
if (!isShallowEqual(current, newState)) {
this.state.next(newState);
}
}
useState() {
// eslint-disable-next-line react-hooks/rules-of-hooks
return useObservable(this.state, this.state.getValue());
}
}
export const appChromeService = new AppChromeService();

View File

@@ -0,0 +1,22 @@
import React, { useEffect } from 'react';
import { NavModelItem } from '@grafana/data';
import { appChromeService } from './AppChromeService';
export interface AppChromeUpdateProps {
pageNav?: NavModelItem;
actions?: React.ReactNode;
}
/**
* This needs to be moved to @grafana/ui or runtime.
* This is the way core pages and plugins update the breadcrumbs and page toolbar actions
*/
export const AppChromeUpdate = React.memo<AppChromeUpdateProps>(({ pageNav, actions }: AppChromeUpdateProps) => {
useEffect(() => {
appChromeService.update({ pageNav, actions });
});
return null;
});
AppChromeUpdate.displayName = 'TopNavUpdate';

View File

@@ -4,11 +4,9 @@ import React from 'react';
import { GrafanaTheme2, NavModelItem } from '@grafana/data';
import { useStyles2, Icon, IconName } from '@grafana/ui';
import { TopNavProps } from './TopNavUpdate';
export interface Props extends TopNavProps {
export interface Props {
sectionNav: NavModelItem;
subNav?: NavModelItem;
pageNav?: NavModelItem;
}
export interface Breadcrumb {
@@ -17,9 +15,9 @@ export interface Breadcrumb {
href?: string;
}
export function Breadcrumbs({ sectionNav, subNav }: Props) {
export function Breadcrumbs({ sectionNav, pageNav }: Props) {
const styles = useStyles2(getStyles);
const crumbs: Breadcrumb[] = [{ icon: 'home', href: '/' }];
const crumbs: Breadcrumb[] = [{ icon: 'home-alt', href: '/' }];
function addCrumbs(node: NavModelItem) {
if (node.parentItem) {
@@ -31,8 +29,8 @@ export function Breadcrumbs({ sectionNav, subNav }: Props) {
addCrumbs(sectionNav);
if (subNav) {
addCrumbs(subNav);
if (pageNav) {
addCrumbs(pageNav);
}
return (

View File

@@ -5,22 +5,22 @@ import { GrafanaTheme2, NavModelItem } from '@grafana/data';
import { IconButton, ToolbarButton, useStyles2 } from '@grafana/ui';
import { Breadcrumbs } from './Breadcrumbs';
import { TopNavProps } from './TopNavUpdate';
import { TOP_BAR_LEVEL_HEIGHT } from './types';
export interface Props extends TopNavProps {
export interface Props {
onToggleSearchBar(): void;
onToggleMegaMenu(): void;
searchBarHidden?: boolean;
sectionNav: NavModelItem;
subNav?: NavModelItem;
pageNav?: NavModelItem;
actions: React.ReactNode;
}
export function NavToolbar({
actions,
searchBarHidden,
sectionNav,
subNav,
pageNav,
onToggleMegaMenu,
onToggleSearchBar,
}: Props) {
@@ -31,7 +31,7 @@ export function NavToolbar({
<div className={styles.menuButton}>
<IconButton name="bars" tooltip="Toggle menu" tooltipPlacement="bottom" size="xl" onClick={onToggleMegaMenu} />
</div>
<Breadcrumbs sectionNav={sectionNav} subNav={subNav} />
<Breadcrumbs sectionNav={sectionNav} pageNav={pageNav} />
<div className={styles.leftActions}></div>
<div className={styles.rightActions}>
{actions}

View File

@@ -0,0 +1,8 @@
import { NavModelItem } from '@grafana/data';
export const TOP_BAR_LEVEL_HEIGHT = 40;
export interface ToolbarUpdateProps {
pageNav?: NavModelItem;
actions?: React.ReactNode;
}

View File

@@ -7,7 +7,7 @@ import { Icon } from '@grafana/ui';
import { getNavModel } from 'app/core/selectors/navModel';
import { StoreState } from 'app/types';
import Page from '../Page/Page';
import { Page } from '../Page/Page';
interface ConnectedProps {
navModel: NavModel;

View File

@@ -9,9 +9,9 @@ import { GrafanaTheme2, NavModelItem } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime';
import { CustomScrollbar, Icon, IconButton, useTheme2 } from '@grafana/ui';
import { TOP_BAR_LEVEL_HEIGHT } from '../AppChrome/types';
import { NavItem } from '../NavBar/NavBarMenu';
import { NavBarToggle } from '../NavBar/NavBarToggle';
import { TOP_BAR_LEVEL_HEIGHT } from '../TopNav/types';
const MENU_WIDTH = '350px';

View File

@@ -0,0 +1,67 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { Provider } from 'react-redux';
import { NavModelItem } from '@grafana/data';
import { config } from '@grafana/runtime';
import { configureStore } from 'app/store/configureStore';
import { Page } from './Page';
import { PageProps } from './types';
const pageNav: NavModelItem = {
text: 'Main title',
children: [
{ text: 'Child1', url: '1', active: true },
{ text: 'Child2', url: '2' },
],
};
const setup = (props: Partial<PageProps>) => {
config.bootData.navTree = [
{
text: 'Section name',
id: 'section',
url: 'section',
children: [
{ text: 'Child1', id: 'child1', url: 'section/child1' },
{ text: 'Child2', id: 'child2', url: 'section/child2' },
],
},
];
const store = configureStore();
return render(
<Provider store={store}>
<Page {...props}>
<div data-testid="page-children">Children</div>
</Page>
</Provider>
);
};
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: 'Main title' })).toBeInTheDocument();
expect(screen.getAllByRole('tab').length).toBe(2);
});
it('should get header nav model from redux navIndex', async () => {
setup({ navId: 'child1' });
expect(screen.getByRole('heading', { name: 'Section name' })).toBeInTheDocument();
expect(screen.getAllByRole('tab').length).toBe(2);
});
});

View File

@@ -1,45 +1,33 @@
// Libraries
import { css, cx } from '@emotion/css';
import React, { FC, HTMLAttributes, useEffect } from 'react';
import React from 'react';
import { GrafanaTheme2, NavModel } from '@grafana/data';
import { GrafanaTheme2 } from '@grafana/data';
import { config } from '@grafana/runtime';
import { CustomScrollbar, useStyles2 } from '@grafana/ui';
import { getTitleFromNavModel } from 'app/core/selectors/navModel';
// Components
import { Branding } from '../Branding/Branding';
import { Footer } from '../Footer/Footer';
import PageHeader from '../PageHeader/PageHeader';
import { PageHeader } from '../PageHeader/PageHeader';
import { Page as NewPage } from '../PageNew/Page';
import { PageContents } from './PageContents';
import { PageType } from './types';
import { usePageNav } from './usePageNav';
import { usePageTitle } from './usePageTitle';
interface Props extends HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
navModel?: NavModel;
}
export interface PageType extends FC<Props> {
Header: typeof PageHeader;
Contents: typeof PageContents;
}
export const Page: PageType = ({ navModel, children, className, ...otherProps }) => {
export const OldPage: PageType = ({ navId, navModel: oldNavProp, pageNav, children, className, ...otherProps }) => {
const styles = useStyles2(getStyles);
const navModel = usePageNav(navId, oldNavProp);
useEffect(() => {
if (navModel) {
const title = getTitleFromNavModel(navModel);
document.title = title ? `${title} - ${Branding.AppTitle}` : Branding.AppTitle;
} else {
document.title = Branding.AppTitle;
}
}, [navModel]);
usePageTitle(navModel, pageNav);
const pageHeaderNav = pageNav ?? navModel?.main;
return (
<div {...otherProps} className={cx(styles.wrapper, className)}>
<CustomScrollbar autoHeightMin={'100%'}>
<div className="page-scrollbar-content">
{navModel && <PageHeader model={navModel} />}
{pageHeaderNav && <PageHeader navItem={pageHeaderNav} />}
{children}
<Footer />
</div>
@@ -48,12 +36,12 @@ export const Page: PageType = ({ navModel, children, className, ...otherProps })
);
};
Page.Header = PageHeader;
Page.Contents = PageContents;
OldPage.Header = PageHeader;
OldPage.Contents = PageContents;
export default Page;
export const Page: PageType = config.featureToggles.topnav ? NewPage : OldPage;
const getStyles = (theme: GrafanaTheme2) => ({
const getStyles = (_: GrafanaTheme2) => ({
wrapper: css`
width: 100%;
flex-grow: 1;

View File

@@ -0,0 +1,19 @@
import { FC, HTMLAttributes } from 'react';
import { NavModel, NavModelItem } from '@grafana/data';
import { PageHeader } from '../PageHeader/PageHeader';
import { PageContents } from './PageContents';
export interface PageProps extends HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
navId?: string;
navModel?: NavModel;
pageNav?: NavModelItem;
}
export interface PageType extends FC<PageProps> {
Header: typeof PageHeader;
Contents: typeof PageContents;
}

View File

@@ -0,0 +1,29 @@
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import { NavModel } from '@grafana/data';
import { getNavModel } from 'app/core/selectors/navModel';
import { store } from 'app/store/store';
import { StoreState } from 'app/types';
export function usePageNav(navId?: string, oldProp?: NavModel): NavModel | undefined {
if (oldProp) {
return oldProp;
}
if (!navId) {
return;
}
// Page component is used in so many tests, this simplifies not having to initialize a full redux store
if (!store) {
return;
}
// eslint-disable-next-line react-hooks/rules-of-hooks
return useSelector(createSelector(getNavIndex, (navIndex) => getNavModel(navIndex, navId ?? 'home')));
}
function getNavIndex(store: StoreState) {
return store.navIndex;
}

View File

@@ -0,0 +1,32 @@
import { useEffect } from 'react';
import { NavModel, NavModelItem } from '@grafana/data';
import { Branding } from '../Branding/Branding';
export function usePageTitle(navModel?: NavModel, pageNav?: NavModelItem) {
useEffect(() => {
const parts: string[] = [];
if (pageNav) {
if (pageNav.children) {
const activePage = pageNav.children.find((x) => x.active);
if (activePage) {
parts.push(activePage.text);
}
}
parts.push(pageNav.text);
}
if (navModel) {
if (navModel.node !== navModel.main) {
parts.push(navModel.node.text);
}
parts.push(navModel.main.text);
}
parts.push(Branding.AppTitle);
document.title = parts.join(' - ');
}, [navModel, pageNav]);
}

View File

@@ -1,23 +1,20 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import PageHeader from './PageHeader';
import { PageHeader } from './PageHeader';
describe('PageHeader', () => {
describe('when the nav tree has a node with a title', () => {
it('should render the title', async () => {
const nav = {
main: {
icon: 'folder-open',
id: 'node',
subTitle: 'node subtitle',
url: '',
text: 'node',
},
node: {},
icon: 'folder-open',
id: 'node',
subTitle: 'node subtitle',
url: '',
text: 'node',
};
render(<PageHeader model={nav as any} />);
render(<PageHeader navItem={nav as any} />);
expect(screen.getByRole('heading', { name: 'node' })).toBeInTheDocument();
});
@@ -26,18 +23,15 @@ describe('PageHeader', () => {
describe('when the nav tree has a node with breadcrumbs and a title', () => {
it('should render the title with breadcrumbs first and then title last', async () => {
const nav = {
main: {
icon: 'folder-open',
id: 'child',
subTitle: 'child subtitle',
url: '',
text: 'child',
breadcrumbs: [{ title: 'Parent', url: 'parentUrl' }],
},
node: {},
icon: 'folder-open',
id: 'child',
subTitle: 'child subtitle',
url: '',
text: 'child',
breadcrumbs: [{ title: 'Parent', url: 'parentUrl' }],
};
render(<PageHeader model={nav as any} />);
render(<PageHeader navItem={nav as any} />);
expect(screen.getByRole('heading', { name: 'Parent / child' })).toBeInTheDocument();
expect(screen.getByRole('link', { name: 'Parent' })).toBeInTheDocument();

View File

@@ -1,14 +1,14 @@
import { css } from '@emotion/css';
import React, { FC } from 'react';
import { NavModel, NavModelItem, NavModelBreadcrumb, GrafanaTheme2 } from '@grafana/data';
import { NavModelItem, NavModelBreadcrumb, GrafanaTheme2 } from '@grafana/data';
import { Tab, TabsBar, Icon, IconName, useStyles2 } from '@grafana/ui';
import { PanelHeaderMenuItem } from 'app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem';
import { ProBadge } from '../Upgrade/ProBadge';
export interface Props {
model: NavModel;
navItem: NavModelItem;
}
const SelectNav = ({ children, customCss }: { children: NavModelItem[]; customCss: string }) => {
@@ -75,21 +75,19 @@ const Navigation = ({ children }: { children: NavModelItem[] }) => {
);
};
export const PageHeader: FC<Props> = ({ model }) => {
export const PageHeader: FC<Props> = ({ navItem: model }) => {
const styles = useStyles2(getStyles);
if (!model) {
return null;
}
const main = model.main;
const children = main.children;
return (
<div className={styles.headerCanvas}>
<div className="page-container">
<div className="page-header">
{renderHeaderTitle(main)}
{children && children.length && <Navigation>{children}</Navigation>}
{renderHeaderTitle(model)}
{model.children && model.children.length > 0 && <Navigation>{model.children}</Navigation>}
</div>
</div>
</div>
@@ -157,5 +155,3 @@ const getStyles = (theme: GrafanaTheme2) => ({
background: ${theme.colors.background.canvas};
`,
});
export default PageHeader;

View File

@@ -0,0 +1,79 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { Provider } from 'react-redux';
import { NavModelItem } from '@grafana/data';
import { config } from '@grafana/runtime';
import { configureStore } from 'app/store/configureStore';
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 = [
{
text: 'Section name',
id: 'section',
url: 'section',
children: [
{ text: 'Child1', id: 'child1', url: 'section/child1' },
{ text: 'Child2', id: 'child2', url: 'section/child2' },
],
},
];
const store = configureStore();
return render(
<Provider store={store}>
<Page {...props}>
<div data-testid="page-children">Children</div>
</Page>
</Provider>
);
};
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 render section nav model based on navId', async () => {
setup({ navId: 'child1' });
expect(screen.getByRole('heading', { name: 'Section name' })).toBeInTheDocument();
expect(screen.getByRole('heading', { name: 'Child1' })).toBeInTheDocument();
expect(screen.getByRole('tab', { name: 'Tab Child1' })).toBeInTheDocument();
expect(screen.getAllByRole('tab').length).toBe(2);
});
it('should render section nav model based on navId and item page nav', async () => {
setup({ navId: 'child1', pageNav });
expect(screen.getByRole('heading', { name: 'Section name' })).toBeInTheDocument();
expect(screen.getByRole('heading', { name: 'pageNav title' })).toBeInTheDocument();
expect(screen.getByRole('tab', { name: 'Tab Child1' })).toBeInTheDocument();
expect(screen.getByRole('tab', { name: 'Tab pageNav child1' })).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,93 @@
// Libraries
import { css, cx } from '@emotion/css';
import React, { useEffect } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { CustomScrollbar, useStyles2 } from '@grafana/ui';
// Components
import { appChromeService } from '../AppChrome/AppChromeService';
import { Footer } from '../Footer/Footer';
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';
import { SectionNav } from './SectionNav';
export const Page: PageType = ({ navId, navModel: oldNavProp, pageNav, children, className, ...otherProps }) => {
const styles = useStyles2(getStyles);
const navModel = usePageNav(navId, oldNavProp);
usePageTitle(navModel, pageNav);
const pageHeaderNav = pageNav ?? navModel?.node;
useEffect(() => {
if (navModel || pageNav) {
appChromeService.update({ sectionNav: navModel?.node, pageNav });
}
}, [navModel, pageNav]);
return (
<div {...otherProps} className={cx(styles.wrapper, className)}>
<div className={styles.panes}>
{navModel && navModel.main.children && <SectionNav model={navModel} />}
<div className={styles.pageContent}>
<CustomScrollbar autoHeightMin={'100%'}>
<div className={styles.pageInner}>
{pageHeaderNav && <PageHeader navItem={pageHeaderNav} />}
{pageNav && pageNav.children && <PageTabs navItem={pageNav} />}
{children}
</div>
<Footer />
</CustomScrollbar>
</div>
</div>
</div>
);
};
Page.Header = PageHeader;
Page.Contents = PageContents;
const getStyles = (theme: GrafanaTheme2) => {
const shadow = theme.isDark
? `0 0.6px 1.5px -1px rgb(0 0 0),0 2px 4px -1px rgb(0 0 0 / 40%),0 5px 10px -1px rgb(0 0 0 / 23%)`
: '0 0.6px 1.5px -1px rgb(0 0 0 / 8%),0 2px 4px rgb(0 0 0 / 6%),0 5px 10px -1px rgb(0 0 0 / 5%)';
return {
wrapper: css`
height: 100%;
display: flex;
flex: 1 1 0;
flex-direction: column;
min-height: 0;
`,
panes: css({
display: 'flex',
height: '100%',
width: '100%',
flexGrow: 1,
minHeight: 0,
flexDirection: 'column',
[theme.breakpoints.up('md')]: {
flexDirection: 'row',
},
}),
pageContent: css({
flexGrow: 1,
}),
pageInner: css({
padding: theme.spacing(3),
boxShadow: shadow,
background: theme.colors.background.primary,
margin: theme.spacing(2, 2, 2, 1),
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
}),
};
};

View File

@@ -0,0 +1,14 @@
// Libraries
import React, { FC } from 'react';
import PageLoader from '../PageLoader/PageLoader';
interface Props {
isLoading?: boolean;
children: React.ReactNode;
className?: string;
}
export const PageContents: FC<Props> = ({ isLoading, children }) => {
return <>{isLoading ? <PageLoader /> : children}</>;
};

View File

@@ -0,0 +1,43 @@
import { css } from '@emotion/css';
import React from 'react';
import { NavModelItem, GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '@grafana/ui';
export interface Props {
navItem: NavModelItem;
}
export function PageHeader({ navItem }: Props) {
const styles = useStyles2(getStyles);
return (
<>
<h1 className={styles.pageTitle}>
{navItem.img && <img className={styles.pageImg} src={navItem.img} alt={`logo for ${navItem.text}`} />}
{navItem.text}
</h1>
{navItem.subTitle && <div className={styles.pageSubTitle}>{navItem.subTitle}</div>}
</>
);
}
const getStyles = (theme: GrafanaTheme2) => {
return {
pageTitle: css({
display: 'flex',
marginBottom: theme.spacing(3),
}),
pageSubTitle: css({
marginBottom: theme.spacing(2),
position: 'relative',
top: theme.spacing(-1),
color: theme.colors.text.secondary,
}),
pageImg: css({
width: '32px',
height: '32px',
marginRight: theme.spacing(2),
}),
};
};

View File

@@ -0,0 +1,42 @@
import { css } from '@emotion/css';
import React from 'react';
import { NavModelItem, GrafanaTheme2 } from '@grafana/data';
import { IconName, useStyles2, TabsBar, Tab } from '@grafana/ui';
export interface Props {
navItem: NavModelItem;
}
export function PageTabs({ navItem }: Props) {
const styles = useStyles2(getStyles);
return (
<div className={styles.tabsWrapper}>
<TabsBar>
{navItem.children!.map((child, index) => {
return (
!child.hideFromTabs && (
<Tab
label={child.text}
active={child.active}
key={`${child.url}-${index}`}
icon={child.icon as IconName}
href={child.url}
suffix={child.tabSuffix}
/>
)
);
})}
</TabsBar>
</div>
);
}
const getStyles = (theme: GrafanaTheme2) => {
return {
tabsWrapper: css({
paddingBottom: theme.spacing(3),
}),
};
};

View File

@@ -0,0 +1,92 @@
import { css } from '@emotion/css';
import React from 'react';
import { NavModel, GrafanaTheme2 } from '@grafana/data';
import { IconName, useStyles2, Icon, VerticalTab } from '@grafana/ui';
export interface Props {
model: NavModel;
}
export function SectionNav(props: Props) {
const styles = useStyles2(getStyles);
const main = props.model.main;
const directChildren = props.model.main.children!.filter((x) => !x.hideFromTabs && !x.children);
const nestedItems = props.model.main.children!.filter((x) => x.children && x.children.length);
return (
<nav className={styles.nav}>
<h2 className={styles.sectionName}>
{main.icon && <Icon name={main.icon as IconName} size="lg" />}
{main.img && <img className="page-header__img" src={main.img} alt={`logo of ${main.text}`} />}
{props.model.main.text}
</h2>
<div className={styles.items}>
{directChildren.map((child, index) => {
return (
!child.hideFromTabs &&
!child.children && (
<VerticalTab
label={child.text}
active={child.active}
key={`${child.url}-${index}`}
// icon={child.icon as IconName}
href={child.url}
/>
)
);
})}
{nestedItems.map((child) => (
<>
<div className={styles.subSection}>{child.text}</div>
{child.children!.map((child, index) => {
return (
!child.hideFromTabs &&
!child.children && (
<VerticalTab
label={child.text}
active={child.active}
key={`${child.url}-${index}`}
// icon={child.icon as IconName}
href={child.url}
/>
)
);
})}
</>
))}
</div>
</nav>
);
}
const getStyles = (theme: GrafanaTheme2) => {
return {
nav: css({
display: 'flex',
flexDirection: 'column',
background: theme.colors.background.canvas,
padding: theme.spacing(3, 2),
flexShrink: 0,
[theme.breakpoints.up('md')]: {
width: '250px',
},
}),
sectionName: css({
display: 'flex',
gap: theme.spacing(1),
padding: theme.spacing(0.5, 0, 3, 0.25),
fontSize: theme.typography.h4.fontSize,
margin: 0,
}),
items: css({
// paddingLeft: '9px',
}),
subSection: css({
padding: theme.spacing(3, 0, 1, 1),
fontWeight: 500,
fontSize: '16px',
}),
};
};

View File

@@ -1,23 +0,0 @@
import { useEffect } from 'react';
import { Subject } from 'rxjs';
import { NavModelItem } from '@grafana/data';
export interface TopNavProps {
subNav?: NavModelItem;
actions?: React.ReactNode;
}
export const topNavUpdates = new Subject<TopNavProps>();
export const topNavDefaultProps: TopNavProps = {};
/**
* This needs to be moved to @grafana/ui or runtime.
* This is the way core pages and plugins update the breadcrumbs and page toolbar actions
*/
export function TopNavUpdate(props: TopNavProps) {
useEffect(() => {
topNavUpdates.next(props);
});
return null;
}

View File

@@ -1 +0,0 @@
export const TOP_BAR_LEVEL_HEIGHT = 40;

View File

@@ -2,9 +2,9 @@ import React from 'react';
// @ts-ignore
import Drop from 'tether-drop';
import { config, locationSearchToObject, navigationLogger, reportPageview } from '@grafana/runtime';
import { locationSearchToObject, navigationLogger, reportPageview } from '@grafana/runtime';
import { TopNavPage } from '../components/TopNav/TopNavPage';
import { appChromeService } from '../components/AppChrome/AppChromeService';
import { keybindingSrv } from '../services/keybindingSrv';
import { GrafanaRouteComponentProps } from './types';
@@ -13,6 +13,8 @@ export interface Props extends Omit<GrafanaRouteComponentProps, 'queryParams'> {
export class GrafanaRoute extends React.Component<Props> {
componentDidMount() {
appChromeService.routeMounted(this.props.route);
this.updateBodyClassNames();
this.cleanupDOM();
// unbinds all and re-bind global keybindins
@@ -71,12 +73,6 @@ export class GrafanaRoute extends React.Component<Props> {
navigationLogger('GrafanaRoute', false, 'Rendered', props.route);
const RouteComponent = props.route.component;
const routeElement = <RouteComponent {...props} queryParams={locationSearchToObject(props.location.search)} />;
if (config.featureToggles.topnav && !props.route.navHidden) {
return <TopNavPage navId={props.route.navId}>{routeElement}</TopNavPage>;
}
return routeElement;
return <RouteComponent {...props} queryParams={locationSearchToObject(props.location.search)} />;
}
}

View File

@@ -17,7 +17,6 @@ export interface RouteDescriptor {
pageClass?: string;
/** Can be used like an id for the route if the same component is used by many routes */
routeName?: string;
navHidden?: boolean;
chromeless?: boolean;
exact?: boolean;
navId?: string;
}

View File

@@ -32,7 +32,6 @@ function buildWarningNav(text: string, subTitle?: string): NavModel {
icon: 'exclamation-triangle',
};
return {
breadcrumbs: [node],
node: node,
main: node,
};

View File

@@ -0,0 +1,29 @@
// From https://github.com/streamich/fast-shallow-equal
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function isShallowEqual(a: any, b: any) {
if (a === b) {
return true;
}
if (!(a instanceof Object) || !(b instanceof Object)) {
return false;
}
var keys = Object.keys(a);
var length = keys.length;
for (let i = 0; i < length; i++) {
if (!(keys[i] in b)) {
return false;
}
}
for (let i = 0; i < length; i++) {
if (a[keys[i]] !== b[keys[i]]) {
return false;
}
}
return length === Object.keys(b).length;
}

View File

@@ -6,7 +6,7 @@ import { useAsyncFn } from 'react-use';
import { UrlQueryValue } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime';
import { Form, Field, Input, Button, Legend, Alert } from '@grafana/ui';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import { contextSrv } from 'app/core/core';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import { getNavModel } from 'app/core/selectors/navModel';

View File

@@ -4,7 +4,7 @@ import useAsyncFn from 'react-use/lib/useAsyncFn';
import { getBackendSrv } from '@grafana/runtime';
import { LinkButton } from '@grafana/ui';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import { getNavModel } from 'app/core/selectors/navModel';
import { contextSrv } from 'app/core/services/context_srv';
import { AccessControlAction } from 'app/types';

View File

@@ -4,7 +4,7 @@ import { useAsync } from 'react-use';
import { NavModel } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import { getNavModel } from 'app/core/selectors/navModel';
import { StoreState } from 'app/types';

View File

@@ -4,8 +4,8 @@ import { connect } from 'react-redux';
import { GrafanaTheme2, NavModel } from '@grafana/data';
import { LinkButton, useStyles2 } from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page';
import Page from '../../core/components/Page/Page';
import { getNavModel } from '../../core/selectors/navModel';
import { StoreState } from '../../types';

View File

@@ -3,7 +3,7 @@ import { connect, ConnectedProps } from 'react-redux';
import { NavModel } from '@grafana/data';
import { featureEnabled } from '@grafana/runtime';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import { contextSrv } from 'app/core/core';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import { getNavModel } from 'app/core/selectors/navModel';

View File

@@ -5,7 +5,7 @@ import { useHistory } from 'react-router-dom';
import { NavModel } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime';
import { Form, Button, Input, Field } from '@grafana/ui';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import { getNavModel } from '../../core/selectors/navModel';
import { StoreState } from '../../types';

View File

@@ -13,7 +13,7 @@ import {
useStyles2,
FilterInput,
} from '@grafana/ui';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import { TagBadge } from 'app/core/components/TagFilter/TagBadge';
import { contextSrv } from 'app/core/core';

View File

@@ -5,7 +5,7 @@ import { NavModel } from '@grafana/data';
import { featureEnabled } from '@grafana/runtime';
import { Alert, Button, LegacyForms } from '@grafana/ui';
const { FormField } = LegacyForms;
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import { contextSrv } from 'app/core/core';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import { getNavModel } from 'app/core/selectors/navModel';

View File

@@ -5,7 +5,7 @@ import { SelectableValue } from '@grafana/data';
import { config, locationService } from '@grafana/runtime';
import { Button, FilterInput, LinkButton, Select, VerticalGroup } from '@grafana/ui';
import appEvents from 'app/core/app_events';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import { getNavModel } from 'app/core/selectors/navModel';
import { AlertRule, StoreState } from 'app/types';

View File

@@ -4,7 +4,7 @@ import { MapDispatchToProps, MapStateToProps } from 'react-redux';
import { NavModel } from '@grafana/data';
import { config } from '@grafana/runtime';
import { Form, Spinner } from '@grafana/ui';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import { connectWithCleanUp } from 'app/core/components/connectWithCleanUp';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import { getNavModel } from 'app/core/selectors/navModel';

View File

@@ -1,6 +1,6 @@
import React from 'react';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import { useNavModel } from 'app/core/hooks/useNavModel';
export default function FeatureTogglePage() {

View File

@@ -3,7 +3,7 @@ import { connect, ConnectedProps } from 'react-redux';
import { config } from '@grafana/runtime';
import { Form } from '@grafana/ui';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import { getNavModel } from 'app/core/selectors/navModel';
import { NotificationChannelDTO, StoreState } from '../../types';

View File

@@ -4,7 +4,7 @@ import { useAsyncFn } from 'react-use';
import { getBackendSrv } from '@grafana/runtime';
import { HorizontalGroup, Button, LinkButton } from '@grafana/ui';
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import { appEvents } from 'app/core/core';
import { useNavModel } from 'app/core/hooks/useNavModel';
import { AlertNotification } from 'app/types/alerting';

View File

@@ -5,7 +5,7 @@ import { useAsync } from 'react-use';
import { GrafanaTheme2 } from '@grafana/data';
import { Alert, LinkButton, LoadingPlaceholder, useStyles2, withErrorBoundary } from '@grafana/ui';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import { useCleanup } from 'app/core/hooks/useCleanup';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import { RuleIdentifier } from 'app/types/unified-alerting';

View File

@@ -1,7 +1,7 @@
import React, { FC } from 'react';
import { useSelector } from 'react-redux';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import { getNavModel } from 'app/core/selectors/navModel';
import { StoreState } from 'app/types/store';

View File

@@ -6,7 +6,7 @@ import { rangeUtil } from '@grafana/data';
import { InlineField, InlineSwitch, VerticalGroup } from '@grafana/ui';
import appEvents from 'app/core/app_events';
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import config from 'app/core/config';
import { contextSrv } from 'app/core/core';
import { getNavModel } from 'app/core/selectors/navModel';

View File

@@ -5,7 +5,7 @@ import { useLocation } from 'react-router-dom';
import { locationUtil, textUtil } from '@grafana/data';
import { locationService } from '@grafana/runtime';
import { ButtonGroup, ModalsController, ToolbarButton, PageToolbar, useForceUpdate } from '@grafana/ui';
import { TopNavUpdate } from 'app/core/components/TopNav/TopNavUpdate';
import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate';
import config from 'app/core/config';
import { toggleKioskMode } from 'app/core/navigation/kiosk';
import { DashboardCommentsModal } from 'app/features/dashboard/components/DashboardComments/DashboardCommentsModal';
@@ -277,8 +277,8 @@ export const DashNav = React.memo<Props>((props) => {
if (config.featureToggles.topnav) {
return (
<TopNavUpdate
subNav={{ text: title }}
<AppChromeUpdate
pageNav={{ text: title }}
actions={<ToolbarButton onClick={onOpenSettings} icon="cog"></ToolbarButton>}
/>
);

View File

@@ -5,7 +5,7 @@ import { connect, ConnectedProps } from 'react-redux';
import { GrafanaTheme2, TimeRange } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { locationService } from '@grafana/runtime';
import { config, locationService } from '@grafana/runtime';
import { CustomScrollbar, stylesFactory, Themeable2, withTheme2 } from '@grafana/ui';
import { notifyApp } from 'app/core/actions';
import { Branding } from 'app/core/components/Branding/Branding';
@@ -387,7 +387,8 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
* Styles
*/
export const getStyles = stylesFactory((theme: GrafanaTheme2, kioskMode: KioskMode) => {
const contentPadding = kioskMode !== KioskMode.Full ? theme.spacing(0, 2, 2) : theme.spacing(2);
const contentPadding =
kioskMode === KioskMode.Full || config.featureToggles.topnav ? theme.spacing(2) : theme.spacing(0, 2, 2);
return {
dashboardContainer: css`
width: 100%;

View File

@@ -1,7 +1,7 @@
import React, { PureComponent } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import { getNavModel } from 'app/core/selectors/navModel';
import { PluginDashboard, StoreState } from 'app/types';

View File

@@ -3,7 +3,7 @@ import { connect, ConnectedProps } from 'react-redux';
import { IconName } from '@grafana/ui';
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import PageActionBar from 'app/core/components/PageActionBar/PageActionBar';
import { contextSrv } from 'app/core/core';
import { getNavModel } from 'app/core/selectors/navModel';

View File

@@ -5,7 +5,7 @@ import { connect, ConnectedProps } from 'react-redux';
import { DataSourcePluginMeta, GrafanaTheme2, NavModel } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { Card, LinkButton, List, PluginSignatureBadge, FilterInput, useStyles2 } from '@grafana/ui';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import { StoreState } from 'app/types';
import { PluginsErrorsInfo } from '../plugins/components/PluginsErrorsInfo';

View File

@@ -6,7 +6,7 @@ import { selectors } from '@grafana/e2e-selectors';
import { Alert, Button } from '@grafana/ui';
import { cleanUpAction } from 'app/core/actions/cleanUp';
import appEvents from 'app/core/app_events';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import { contextSrv } from 'app/core/core';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import { getNavModel } from 'app/core/selectors/navModel';

View File

@@ -3,7 +3,7 @@ import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '@grafana/ui';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
export default function FeatureTogglePage() {
const styles = useStyles2(

View File

@@ -2,7 +2,7 @@ import React, { useEffect } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { Permissions } from 'app/core/components/AccessControl';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import { contextSrv } from 'app/core/core';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import { getNavModel } from 'app/core/selectors/navModel';

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useAsync } from 'react-use';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import { getNavModel } from 'app/core/selectors/navModel';
import { StoreState } from 'app/types';

View File

@@ -2,7 +2,8 @@ import React, { useState } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { useAsync } from 'react-use';
import Page from '../../core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import { GrafanaRouteComponentProps } from '../../core/navigation/types';
import { getNavModel } from '../../core/selectors/navModel';
import { StoreState } from '../../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/Page/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

@@ -4,7 +4,7 @@ import { connect, ConnectedProps } from 'react-redux';
import { Button, LegacyForms } from '@grafana/ui';
const { Input } = LegacyForms;
import appEvents from 'app/core/app_events';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import { getNavModel } from 'app/core/selectors/navModel';
import { StoreState } from 'app/types';

View File

@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should enable save button 1`] = `
<Page
<OldPage
navModel={Object {}}
>
<PageContents
@@ -54,11 +54,11 @@ exports[`Render should enable save button 1`] = `
</form>
</div>
</PageContents>
</Page>
</OldPage>
`;
exports[`Render should render component 1`] = `
<Page
<OldPage
navModel={Object {}}
>
<PageContents
@@ -111,5 +111,5 @@ exports[`Render should render component 1`] = `
</form>
</div>
</PageContents>
</Page>
</OldPage>
`;

View File

@@ -2,7 +2,7 @@ import React, { PureComponent } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { Button, Input, Form, Field } from '@grafana/ui';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import { getNavModel } from 'app/core/selectors/navModel';
import { StoreState } from 'app/types';

View File

@@ -3,7 +3,7 @@ import { useAsync } from 'react-use';
import { getBackendSrv } from '@grafana/runtime';
import { Button, Field, Form, Input } from '@grafana/ui';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import { getConfig } from 'app/core/config';
import { contextSrv } from 'app/core/core';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';

View File

@@ -1,7 +1,8 @@
import React, { FC, useState } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import Page from '../../core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import { GrafanaRouteComponentProps } from '../../core/navigation/types';
import { getNavModel } from '../../core/selectors/navModel';
import { StoreState } from '../../types';

View File

@@ -4,7 +4,7 @@ import React, { useEffect, useState } from 'react';
import { GrafanaTheme } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime';
import { useStyles } from '@grafana/ui';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import { useNavModel } from 'app/core/hooks/useNavModel';
import { GrafanaCloudBackend } from './types';

View File

@@ -1,6 +1,6 @@
import React from 'react';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import { useNavModel } from 'app/core/hooks/useNavModel';
export default function FeatureTogglePage() {

View File

@@ -1,6 +1,6 @@
import React from 'react';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import { useNavModel } from 'app/core/hooks/useNavModel';
export default function CloudAdminPage() {

View File

@@ -2,7 +2,7 @@ import React, { useEffect, useState, ChangeEvent } from 'react';
import { getBackendSrv } from '@grafana/runtime';
import { Input } from '@grafana/ui';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import { useNavModel } from 'app/core/hooks/useNavModel';
import { AddNewRule } from './AddNewRule';

View File

@@ -20,7 +20,7 @@ import {
withTheme2,
} from '@grafana/ui';
import appEvents from 'app/core/app_events';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import { getNavModel } from 'app/core/selectors/navModel';
import { StoreState } from 'app/types';

View File

@@ -2,7 +2,7 @@ import React, { FC } from 'react';
import { MapStateToProps, connect } from 'react-redux';
import { NavModel } from '@grafana/data';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import { getNavModel } from 'app/core/selectors/navModel';
import { StoreState } from 'app/types';

View File

@@ -1,7 +1,8 @@
import React from 'react';
import { connect, ConnectedProps } from 'react-redux';
import Page from '../../core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import { GrafanaRouteComponentProps } from '../../core/navigation/types';
import { getNavModel } from '../../core/selectors/navModel';
import { StoreState } from '../../types';

View File

@@ -2,7 +2,7 @@ import React, { FC } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { Button, Input, Field, Form } from '@grafana/ui';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import { getConfig } from 'app/core/config';
import { StoreState } from 'app/types';

View File

@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
import { NavModel } from '@grafana/data';
import { VerticalGroup } from '@grafana/ui';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import SharedPreferences from 'app/core/components/SharedPreferences/SharedPreferences';
import { contextSrv } from 'app/core/core';
import { getNavModel } from 'app/core/selectors/navModel';

View File

@@ -4,7 +4,7 @@ import { useEffectOnce } from 'react-use';
import { config } from '@grafana/runtime';
import { Button, HorizontalGroup } from '@grafana/ui';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import { StoreState, UserOrg } from 'app/types';
import { getUserOrganizations, setUserOrganization } from './state/actions';

View File

@@ -2,7 +2,7 @@ import React, { FC } from 'react';
import { connect } from 'react-redux';
import { NavModel } from '@grafana/data';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import { contextSrv } from 'app/core/core';
import { getNavModel } from 'app/core/selectors/navModel';
import { StoreState } from 'app/types/store';

View File

@@ -4,7 +4,7 @@ import { connect, MapStateToProps } from 'react-redux';
import { NavModel } from '@grafana/data';
import { locationService } from '@grafana/runtime';
import { useStyles2 } from '@grafana/ui';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import { getNavModel } from 'app/core/selectors/navModel';
import { StoreState } from 'app/types';

View File

@@ -4,7 +4,7 @@ import { connect, MapStateToProps } from 'react-redux';
import { NavModel } from '@grafana/data';
import { locationService } from '@grafana/runtime';
import { useStyles2 } from '@grafana/ui';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import { getNavModel } from 'app/core/selectors/navModel';
import { StoreState } from 'app/types';

View File

@@ -4,7 +4,7 @@ import { useDebounce } from 'react-use';
import { NavModel } from '@grafana/data';
import { ConfirmModal } from '@grafana/ui';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import PageActionBar from 'app/core/components/PageActionBar/PageActionBar';
import { getNavModel } from 'app/core/selectors/navModel';
import { contextSrv } from 'app/core/services/context_srv';

View File

@@ -7,16 +7,19 @@ import { PluginAdminRoutes } from './types';
const DEFAULT_ROUTES = [
{
path: '/plugins',
navId: 'plugins',
routeName: PluginAdminRoutes.Home,
component: SafeDynamicImport(() => import(/* webpackChunkName: "PluginListPage" */ './pages/Browse')),
},
{
path: '/plugins/browse',
navId: 'plugins',
routeName: PluginAdminRoutes.Browse,
component: SafeDynamicImport(() => import(/* webpackChunkName: "PluginListPage" */ './pages/Browse')),
},
{
path: '/plugins/:pluginId/',
navId: 'plugins',
routeName: PluginAdminRoutes.Details,
component: SafeDynamicImport(() => import(/* webpackChunkName: "PluginPage" */ './pages/PluginDetails')),
},
@@ -25,16 +28,19 @@ const DEFAULT_ROUTES = [
const ADMIN_ROUTES = [
{
path: '/admin/plugins',
navId: 'admin-plugins',
routeName: PluginAdminRoutes.HomeAdmin,
component: SafeDynamicImport(() => import(/* webpackChunkName: "PluginListPage" */ './pages/Browse')),
},
{
path: '/admin/plugins/browse',
navId: 'admin-plugins',
routeName: PluginAdminRoutes.BrowseAdmin,
component: SafeDynamicImport(() => import(/* webpackChunkName: "PluginListPage" */ './pages/Browse')),
},
{
path: '/admin/plugins/:pluginId/',
navId: 'admin-plugins',
routeName: PluginAdminRoutes.DetailsAdmin,
component: SafeDynamicImport(() => import(/* webpackChunkName: "PluginPage" */ './pages/PluginDetails')),
},

View File

@@ -3,7 +3,7 @@ import { useLocation, useParams } from 'react-router-dom';
import { NavModel } from '@grafana/data';
import { getWarningNav } from 'app/angular/services/nav_model_srv';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import PageLoader from 'app/core/components/PageLoader/PageLoader';
import { useImportAppPlugin } from '../hooks/useImportAppPlugin';
@@ -23,13 +23,13 @@ export const AppPluginLoader = ({ id, basePath }: AppPluginLoaderProps) => {
const { pathname } = useLocation();
if (error) {
return <Page.Header model={getWarningNav(error.message, error.stack)} />;
return <Page.Header navItem={getWarningNav(error.message, error.stack).main} />;
}
return (
<>
{loading && <PageLoader />}
{nav && <Page.Header model={nav} />}
{nav && <Page.Header navItem={nav.main} />}
{!loading && plugin && plugin.root && (
<plugin.root
meta={plugin.meta}

View File

@@ -4,7 +4,7 @@ import { createHtmlPortalNode, InPortal, OutPortal, HtmlPortalNode } from 'react
import { AppEvents, AppPlugin, AppPluginMeta, KeyValue, NavModel, PluginType } from '@grafana/data';
import { getNotFoundNav, getWarningNav, getExceptionNav } from 'app/angular/services/nav_model_srv';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import PageLoader from 'app/core/components/PageLoader/PageLoader';
import { appEvents } from 'app/core/core';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';

View File

@@ -3,7 +3,7 @@ import { connect, ConnectedProps } from 'react-redux';
import { useMount } from 'react-use';
import { NavModel } from '@grafana/data';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import { getNavModel } from 'app/core/selectors/navModel';
import { StoreState } from 'app/types';

View File

@@ -1,6 +1,6 @@
import React from 'react';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import { useNavModel } from 'app/core/hooks/useNavModel';
export default function FeatureTogglePage() {

View File

@@ -4,7 +4,7 @@ import { useMount } from 'react-use';
import { NavModel } from '@grafana/data';
import { VerticalGroup } from '@grafana/ui';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import SharedPreferences from 'app/core/components/SharedPreferences/SharedPreferences';
import { getNavModel } from 'app/core/selectors/navModel';
import { StoreState } from 'app/types';

View File

@@ -11,11 +11,11 @@ import {
PanelData,
} from '@grafana/data';
import { Button, Table } from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page';
import { config } from 'app/core/config';
import { useAppNotification } from 'app/core/copy/appNotification';
import { QueryGroupOptions } from 'app/types';
import Page from '../../core/components/Page/Page';
import { PanelRenderer } from '../panel/components/PanelRenderer';
import { QueryGroup } from '../query/components/QueryGroup';
import { PanelQueryRunner } from '../query/state/PanelQueryRunner';

View File

@@ -5,7 +5,7 @@ import { useAsync } from 'react-use';
import { NavModel, locationUtil } from '@grafana/data';
import { locationService } from '@grafana/runtime';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import { getNavModel } from 'app/core/selectors/navModel';
import { FolderDTO, StoreState } from 'app/types';

View File

@@ -2,11 +2,12 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { ServiceAccountCreatePageUnconnected, Props } from './ServiceAccountCreatePage';
import { ServiceAccountCreatePage, Props } from './ServiceAccountCreatePage';
const postMock = jest.fn().mockResolvedValue({});
const patchMock = jest.fn().mockResolvedValue({});
const putMock = jest.fn().mockResolvedValue({});
jest.mock('@grafana/runtime', () => ({
getBackendSrv: () => ({
post: postMock,
@@ -26,6 +27,7 @@ jest.mock('@grafana/runtime', () => ({
licenseUrl: '',
},
appSubUrl: '',
featureToggles: {},
},
}));
@@ -52,7 +54,7 @@ const setup = (propOverrides: Partial<Props>) => {
Object.assign(props, propOverrides);
render(<ServiceAccountCreatePageUnconnected {...props} />);
render(<ServiceAccountCreatePage {...props} />);
};
describe('ServiceAccountCreatePage tests', () => {

View File

@@ -1,34 +1,24 @@
import React, { useCallback, useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { useHistory } from 'react-router-dom';
import { NavModel } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime';
import { Form, Button, Input, Field } from '@grafana/ui';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker';
import { fetchBuiltinRoles, fetchRoleOptions, updateUserRoles } from 'app/core/components/RolePicker/api';
import { contextSrv } from 'app/core/core';
import { AccessControlAction, OrgRole, Role, ServiceAccountCreateApiResponse, ServiceAccountDTO } from 'app/types';
import { getNavModel } from '../../core/selectors/navModel';
import { StoreState } from '../../types';
import { OrgRolePicker } from '../admin/OrgRolePicker';
export interface Props {
navModel: NavModel;
}
const mapStateToProps = (state: StoreState) => ({
navModel: getNavModel(state.navIndex, 'serviceaccounts'),
});
export interface Props {}
const createServiceAccount = async (sa: ServiceAccountDTO) => getBackendSrv().post('/api/serviceaccounts/', sa);
const updateServiceAccount = async (id: number, sa: ServiceAccountDTO) =>
getBackendSrv().patch(`/api/serviceaccounts/${id}`, sa);
export const ServiceAccountCreatePageUnconnected = ({ navModel }: Props): JSX.Element => {
export const ServiceAccountCreatePage = ({}: Props): JSX.Element => {
const [roleOptions, setRoleOptions] = useState<Role[]>([]);
const [builtinRoles, setBuiltinRoles] = useState<{ [key: string]: Role[] }>({});
const [pendingRoles, setPendingRoles] = useState<Role[]>([]);
@@ -109,7 +99,7 @@ export const ServiceAccountCreatePageUnconnected = ({ navModel }: Props): JSX.El
};
return (
<Page navModel={navModel}>
<Page navId="serviceaccounts">
<Page.Contents>
<h1>Create service account</h1>
<Form onSubmit={onSubmit} validateOn="onSubmit">
@@ -152,4 +142,4 @@ export const ServiceAccountCreatePageUnconnected = ({ navModel }: Props): JSX.El
);
};
export default connect(mapStateToProps)(ServiceAccountCreatePageUnconnected);
export default ServiceAccountCreatePage;

View File

@@ -23,14 +23,6 @@ const setup = (propOverrides: Partial<Props>) => {
const updateServiceAccountMock = jest.fn();
const props: Props = {
navModel: {
main: {
text: 'Configuration',
},
node: {
text: 'Service accounts',
},
},
serviceAccount: {} as ServiceAccountDTO,
tokens: [],
builtInRoles: {},

View File

@@ -2,12 +2,11 @@ import { css } from '@emotion/css';
import React, { useEffect, useState } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { getTimeZone, GrafanaTheme2, NavModel } from '@grafana/data';
import { getTimeZone, GrafanaTheme2 } from '@grafana/data';
import { Button, ConfirmModal, IconButton, useStyles2 } from '@grafana/ui';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import { contextSrv } from 'app/core/core';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import { getNavModel } from 'app/core/selectors/navModel';
import { AccessControlAction, ApiKey, Role, ServiceAccountDTO, StoreState } from 'app/types';
import { CreateTokenModal, ServiceAccountToken } from './components/CreateTokenModal';
@@ -24,7 +23,6 @@ import {
} from './state/actionsServiceAccountPage';
interface OwnProps extends GrafanaRouteComponentProps<{ id: string }> {
navModel: NavModel;
serviceAccount?: ServiceAccountDTO;
tokens: ApiKey[];
isLoading: boolean;
@@ -34,7 +32,6 @@ interface OwnProps extends GrafanaRouteComponentProps<{ id: string }> {
function mapStateToProps(state: StoreState) {
return {
navModel: getNavModel(state.navIndex, 'serviceaccounts'),
serviceAccount: state.serviceAccountProfile.serviceAccount,
tokens: state.serviceAccountProfile.tokens,
isLoading: state.serviceAccountProfile.isLoading,
@@ -58,7 +55,6 @@ const connector = connect(mapStateToProps, mapDispatchToProps);
export type Props = OwnProps & ConnectedProps<typeof connector>;
export const ServiceAccountPageUnconnected = ({
navModel,
match,
serviceAccount,
tokens,
@@ -131,7 +127,7 @@ export const ServiceAccountPageUnconnected = ({
};
return (
<Page navModel={navModel}>
<Page navId="serviceaccounts">
<Page.Contents isLoading={isLoading}>
{serviceAccount && (
<div className={styles.headerContainer}>

View File

@@ -26,14 +26,6 @@ const setup = (propOverrides: Partial<Props>) => {
const getApiKeysMigrationInfoMock = jest.fn();
const closeApiKeysMigrationInfoMock = jest.fn();
const props: Props = {
navModel: {
main: {
text: 'Configuration',
},
node: {
text: 'Service accounts',
},
},
builtInRoles: {},
isLoading: false,
page: 0,

View File

@@ -6,10 +6,9 @@ import { connect, ConnectedProps } from 'react-redux';
import { GrafanaTheme2, OrgRole } from '@grafana/data';
import { Alert, ConfirmModal, FilterInput, Icon, LinkButton, RadioButtonGroup, Tooltip, useStyles2 } from '@grafana/ui';
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import PageLoader from 'app/core/components/PageLoader/PageLoader';
import { contextSrv } from 'app/core/core';
import { getNavModel } from 'app/core/selectors/navModel';
import { StoreState, ServiceAccountDTO, AccessControlAction, ServiceAccountStateFilter } from 'app/types';
import { CreateTokenModal, ServiceAccountToken } from './components/CreateTokenModal';
@@ -33,7 +32,6 @@ export type Props = OwnProps & ConnectedProps<typeof connector>;
function mapStateToProps(state: StoreState) {
return {
navModel: getNavModel(state.navIndex, 'serviceaccounts'),
...state.serviceAccounts,
};
}
@@ -54,7 +52,6 @@ const mapDispatchToProps = {
const connector = connect(mapStateToProps, mapDispatchToProps);
export const ServiceAccountsListPageUnconnected = ({
navModel,
serviceAccounts,
isLoading,
roleOptions,
@@ -169,7 +166,7 @@ export const ServiceAccountsListPageUnconnected = ({
};
return (
<Page navModel={navModel}>
<Page navId="serviceaccounts">
<Page.Contents>
{apiKeysMigrated && showApiKeysMigrationInfo && (
<Alert

View File

@@ -5,7 +5,7 @@ import { useAsync } from 'react-use';
import { DataFrame, GrafanaTheme2, isDataFrame, ValueLinkConfig } from '@grafana/data';
import { locationService } from '@grafana/runtime';
import { useStyles2, IconName, Spinner, TabsBar, Tab, Button, HorizontalGroup } from '@grafana/ui';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import { useNavModel } from 'app/core/hooks/useNavModel';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';

View File

@@ -2,9 +2,9 @@ import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { NavModel } from '@grafana/data';
import { BackendSrv, setBackendSrv } from '@grafana/runtime';
import { CreateTeam, Props } from './CreateTeam';
import { CreateTeam } from './CreateTeam';
beforeEach(() => {
jest.clearAllMocks();
@@ -14,23 +14,12 @@ const mockPost = jest.fn(() => {
return Promise.resolve({});
});
jest.mock('@grafana/runtime', () => ({
getBackendSrv: () => {
return {
post: mockPost,
};
},
config: {
buildInfo: {},
licenseInfo: {},
},
}));
setBackendSrv({
post: mockPost,
} as any as BackendSrv);
const setup = () => {
const props: Props = {
navModel: { node: {}, main: {} } as NavModel,
};
return render(<CreateTeam {...props} />);
return render(<CreateTeam />);
};
describe('Create team', () => {

View File

@@ -1,24 +1,16 @@
import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import { NavModel } from '@grafana/data';
import { getBackendSrv, locationService } from '@grafana/runtime';
import { Button, Form, Field, Input, FieldSet } from '@grafana/ui';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import { contextSrv } from 'app/core/core';
import { getNavModel } from 'app/core/selectors/navModel';
import { StoreState } from 'app/types';
export interface Props {
navModel: NavModel;
}
interface TeamDTO {
name: string;
email: string;
}
export class CreateTeam extends PureComponent<Props> {
export class CreateTeam extends PureComponent {
create = async (formModel: TeamDTO) => {
const result = await getBackendSrv().post('/api/teams', formModel);
if (result.teamId) {
@@ -27,10 +19,8 @@ export class CreateTeam extends PureComponent<Props> {
}
};
render() {
const { navModel } = this.props;
return (
<Page navModel={navModel}>
<Page navId="teams">
<Page.Contents>
<Form onSubmit={this.create}>
{({ register, errors }) => (
@@ -58,10 +48,4 @@ export class CreateTeam extends PureComponent<Props> {
}
}
function mapStateToProps(state: StoreState) {
return {
navModel: getNavModel(state.navIndex, 'teams'),
};
}
export default connect(mapStateToProps)(CreateTeam);
export default CreateTeam;

View File

@@ -3,7 +3,6 @@ import userEvent from '@testing-library/user-event';
import React from 'react';
import { mockToolkitActionCreator } from 'test/core/redux/mocks';
import { NavModel } from '@grafana/data';
import { contextSrv, User } from 'app/core/services/context_srv';
import { OrgRole, Team } from '../../types';
@@ -20,14 +19,6 @@ jest.mock('app/core/config', () => {
const setup = (propOverrides?: object) => {
const props: Props = {
navModel: {
main: {
text: 'Configuration',
},
node: {
text: 'Team List',
},
} as NavModel,
teams: [] as Team[],
loadTeams: jest.fn(),
deleteTeam: jest.fn(),

View File

@@ -1,13 +1,11 @@
import React, { PureComponent } from 'react';
import { NavModel } from '@grafana/data';
import { DeleteButton, LinkButton, FilterInput, VerticalGroup, HorizontalGroup, Pagination } from '@grafana/ui';
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
import Page from 'app/core/components/Page/Page';
import { Page } from 'app/core/components/Page/Page';
import { TeamRolePicker } from 'app/core/components/RolePicker/TeamRolePicker';
import { fetchRoleOptions } from 'app/core/components/RolePicker/api';
import { config } from 'app/core/config';
import { getNavModel } from 'app/core/selectors/navModel';
import { contextSrv, User } from 'app/core/services/context_srv';
import { AccessControlAction, Role, StoreState, Team } from 'app/types';
@@ -20,7 +18,6 @@ import { getSearchQuery, getTeams, getTeamsCount, getTeamsSearchPage, isPermissi
const pageLimit = 30;
export interface Props {
navModel: NavModel;
teams: Team[];
searchQuery: string;
searchPage: number;
@@ -224,10 +221,10 @@ export class TeamList extends PureComponent<Props, State> {
}
render() {
const { hasFetched, navModel } = this.props;
const { hasFetched } = this.props;
return (
<Page navModel={navModel}>
<Page navId="teams">
<Page.Contents isLoading={!hasFetched}>{this.renderList()}</Page.Contents>
</Page>
);
@@ -236,7 +233,6 @@ export class TeamList extends PureComponent<Props, State> {
function mapStateToProps(state: StoreState) {
return {
navModel: getNavModel(state.navIndex, 'teams'),
teams: getTeams(state.teams),
searchQuery: getSearchQuery(state.teams),
searchPage: getTeamsSearchPage(state.teams),

Some files were not shown because too many files have changed in this diff Show More