mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -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"]
|
||||
|
@@ -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 {
|
||||
|
96
packages/grafana-ui/src/components/Tabs/VerticalTab.tsx
Normal file
96
packages/grafana-ui/src/components/Tabs/VerticalTab.tsx
Normal 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)};
|
||||
`,
|
||||
};
|
||||
};
|
@@ -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';
|
||||
|
||||
|
@@ -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 />
|
||||
|
@@ -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';
|
||||
|
@@ -71,7 +71,6 @@ export function getWarningNav(text: string, subTitle?: string): NavModel {
|
||||
icon: 'exclamation-triangle',
|
||||
};
|
||||
return {
|
||||
breadcrumbs: [node],
|
||||
node: node,
|
||||
main: node,
|
||||
};
|
||||
|
@@ -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',
|
51
public/app/core/components/AppChrome/AppChromeService.tsx
Normal file
51
public/app/core/components/AppChrome/AppChromeService.tsx
Normal 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();
|
22
public/app/core/components/AppChrome/AppChromeUpdate.tsx
Normal file
22
public/app/core/components/AppChrome/AppChromeUpdate.tsx
Normal 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';
|
@@ -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 (
|
@@ -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}
|
8
public/app/core/components/AppChrome/types.ts
Normal file
8
public/app/core/components/AppChrome/types.ts
Normal 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;
|
||||
}
|
@@ -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;
|
||||
|
@@ -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';
|
||||
|
||||
|
67
public/app/core/components/Page/Page.test.tsx
Normal file
67
public/app/core/components/Page/Page.test.tsx
Normal 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);
|
||||
});
|
||||
});
|
@@ -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;
|
||||
|
19
public/app/core/components/Page/types.ts
Normal file
19
public/app/core/components/Page/types.ts
Normal 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;
|
||||
}
|
29
public/app/core/components/Page/usePageNav.ts
Normal file
29
public/app/core/components/Page/usePageNav.ts
Normal 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;
|
||||
}
|
32
public/app/core/components/Page/usePageTitle.ts
Normal file
32
public/app/core/components/Page/usePageTitle.ts
Normal 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]);
|
||||
}
|
@@ -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();
|
||||
|
@@ -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;
|
||||
|
79
public/app/core/components/PageNew/Page.test.tsx
Normal file
79
public/app/core/components/PageNew/Page.test.tsx
Normal 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();
|
||||
});
|
||||
});
|
93
public/app/core/components/PageNew/Page.tsx
Normal file
93
public/app/core/components/PageNew/Page.tsx
Normal 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,
|
||||
}),
|
||||
};
|
||||
};
|
14
public/app/core/components/PageNew/PageContents.tsx
Normal file
14
public/app/core/components/PageNew/PageContents.tsx
Normal 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}</>;
|
||||
};
|
43
public/app/core/components/PageNew/PageHeader.tsx
Normal file
43
public/app/core/components/PageNew/PageHeader.tsx
Normal 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),
|
||||
}),
|
||||
};
|
||||
};
|
42
public/app/core/components/PageNew/PageTabs.tsx
Normal file
42
public/app/core/components/PageNew/PageTabs.tsx
Normal 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),
|
||||
}),
|
||||
};
|
||||
};
|
92
public/app/core/components/PageNew/SectionNav.tsx
Normal file
92
public/app/core/components/PageNew/SectionNav.tsx
Normal 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',
|
||||
}),
|
||||
};
|
||||
};
|
@@ -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;
|
||||
}
|
@@ -1 +0,0 @@
|
||||
export const TOP_BAR_LEVEL_HEIGHT = 40;
|
@@ -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)} />;
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -32,7 +32,6 @@ function buildWarningNav(text: string, subTitle?: string): NavModel {
|
||||
icon: 'exclamation-triangle',
|
||||
};
|
||||
return {
|
||||
breadcrumbs: [node],
|
||||
node: node,
|
||||
main: node,
|
||||
};
|
||||
|
29
public/app/core/utils/isShallowEqual.ts
Normal file
29
public/app/core/utils/isShallowEqual.ts
Normal 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;
|
||||
}
|
@@ -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';
|
||||
|
@@ -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';
|
||||
|
@@ -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';
|
||||
|
||||
|
@@ -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';
|
||||
|
||||
|
@@ -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';
|
||||
|
@@ -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';
|
||||
|
@@ -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';
|
||||
|
||||
|
@@ -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';
|
||||
|
@@ -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';
|
||||
|
@@ -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';
|
||||
|
@@ -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() {
|
||||
|
@@ -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';
|
||||
|
@@ -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';
|
||||
|
@@ -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';
|
||||
|
@@ -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';
|
||||
|
||||
|
@@ -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';
|
||||
|
@@ -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>}
|
||||
/>
|
||||
);
|
||||
|
@@ -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%;
|
||||
|
@@ -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';
|
||||
|
@@ -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';
|
||||
|
@@ -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';
|
||||
|
@@ -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';
|
||||
|
@@ -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(
|
||||
|
@@ -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';
|
||||
|
@@ -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';
|
||||
|
@@ -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';
|
||||
|
@@ -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';
|
||||
|
@@ -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';
|
||||
|
@@ -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>
|
||||
`;
|
||||
|
@@ -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';
|
||||
|
||||
|
@@ -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';
|
||||
|
@@ -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';
|
||||
|
@@ -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';
|
||||
|
@@ -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() {
|
||||
|
@@ -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() {
|
||||
|
@@ -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';
|
||||
|
@@ -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';
|
||||
|
@@ -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';
|
||||
|
||||
|
@@ -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';
|
||||
|
@@ -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';
|
||||
|
||||
|
@@ -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';
|
||||
|
@@ -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';
|
||||
|
@@ -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';
|
||||
|
@@ -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';
|
||||
|
||||
|
@@ -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';
|
||||
|
||||
|
@@ -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';
|
||||
|
@@ -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')),
|
||||
},
|
||||
|
@@ -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}
|
||||
|
@@ -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';
|
||||
|
@@ -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';
|
||||
|
||||
|
@@ -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() {
|
||||
|
@@ -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';
|
||||
|
@@ -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';
|
||||
|
@@ -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';
|
||||
|
||||
|
@@ -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', () => {
|
||||
|
@@ -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;
|
||||
|
@@ -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: {},
|
||||
|
@@ -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}>
|
||||
|
@@ -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,
|
||||
|
@@ -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
|
||||
|
@@ -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';
|
||||
|
||||
|
@@ -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', () => {
|
||||
|
@@ -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;
|
||||
|
@@ -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(),
|
||||
|
@@ -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
Reference in New Issue
Block a user