Changes APM home page to use route-based tabs (#25891)

* Changed home page to use route based history tabs, abstracted history tabs to component with tests

* Spreads location on tab links to preserve query string etc

* Adds ts-ignore to 'fix' problem with TS not finding EuiTab and EuiTabs modules in EUI exports

* Fixes breadcrumbs and service redirect

* Removes commented code
This commit is contained in:
Jason Rhodes 2018-11-21 11:06:03 -05:00 committed by GitHub
parent 1b3d6ae0b3
commit 453e1f1a2d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 268 additions and 31 deletions

View file

@ -5,8 +5,12 @@
*/
// @ts-ignore
import { EuiTabbedContent } from '@elastic/eui';
import { EuiTab, EuiTabs } from '@elastic/eui';
import React from 'react';
import {
HistoryTabs,
IHistoryTab
} from 'x-pack/plugins/apm/public/components/shared/HistoryTabs';
// @ts-ignore
import { KueryBar } from '../../shared/KueryBar';
import { SetupInstructionsLink } from '../../shared/SetupInstructionsLink';
@ -16,6 +20,19 @@ import { HeaderContainer } from '../../shared/UIComponents';
import { ServiceOverview } from '../ServiceOverview';
import { TraceOverview } from '../TraceOverview';
const homeTabs: IHistoryTab[] = [
{
path: '/services',
name: 'Services',
component: ServiceOverview
},
{
path: '/traces',
name: 'Traces',
component: TraceOverview
}
];
export function Home() {
return (
<div>
@ -24,17 +41,7 @@ export function Home() {
<SetupInstructionsLink />
</HeaderContainer>
<KueryBar />
<EuiTabbedContent
className="k6Tab--large"
tabs={[
{
id: 'services_overview',
name: 'Services',
content: <ServiceOverview />
},
{ id: 'traces_overview', name: 'Traces', content: <TraceOverview /> }
]}
/>
<HistoryTabs tabs={homeTabs} />
</div>
);
}

View file

@ -9,19 +9,18 @@ exports[`Home component should render 1`] = `
<SetupInstructionsLink />
</styled.div>
<Connect(KueryBarView) />
<EuiTabbedContent
className="k6Tab--large"
<withRouter(HistoryTabsWithoutRouter)
tabs={
Array [
Object {
"content": <Connect(ServiceOverview) />,
"id": "services_overview",
"component": [Function],
"name": "Services",
"path": "/services",
},
Object {
"content": <Connect(TraceOverview) />,
"id": "traces_overview",
"component": [Function],
"name": "Traces",
"path": "/traces",
},
]
}

View file

@ -20,19 +20,33 @@ import { Home } from './Home';
interface BreadcrumbArgs {
match: {
params: StringMap<any>;
params: StringMap;
};
}
interface RenderArgs {
location: StringMap<any>;
location: StringMap;
match: {
params: StringMap;
};
}
const renderAsRedirectTo = (to: string) => {
return ({ location }: RenderArgs) => (
<Redirect
to={{
...location,
pathname: to
}}
/>
);
};
export const routes = [
{
exact: true,
path: '/',
component: Home,
render: renderAsRedirectTo('/services'),
breadcrumb: 'APM'
},
{
@ -56,20 +70,26 @@ export const routes = [
breadcrumb: 'Invalid License',
render: () => <div>Invalid license</div>
},
{
exact: true,
path: '/services',
component: Home,
breadcrumb: 'Services'
},
{
exact: true,
path: '/traces',
component: Home,
breadcrumb: 'Traces'
},
{
exact: true,
path: '/:serviceName',
breadcrumb: ({ match }: BreadcrumbArgs) => match.params.serviceName,
render: ({ location }: RenderArgs) => {
return (
<Redirect
to={{
...location,
pathname: `${location.pathname}/transactions`
}}
/>
);
}
render: (props: RenderArgs) =>
renderAsRedirectTo(`/${props.match.params.serviceName}/transactions`)(
props
)
}
]
},

View file

@ -0,0 +1,92 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
// @ts-ignore otherwise TS complains "Module ''@elastic/eui'' has no exported member 'EuiTab'"
import { EuiTab } from '@elastic/eui';
import { mount, ReactWrapper, shallow, ShallowWrapper } from 'enzyme';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import {
HistoryTabs,
HistoryTabsProps,
HistoryTabsWithoutRouter,
IHistoryTab
} from '..';
describe('HistoryTabs', () => {
let mockLocation: any;
let mockHistory: any;
let testTabs: IHistoryTab[];
let testProps: HistoryTabsProps;
beforeEach(() => {
mockLocation = {
pathname: ''
};
mockHistory = {
push: jest.fn()
};
const Content = (props: { name: string }) => <div>{props.name}</div>;
testTabs = [
{
name: 'One',
path: '/one',
component: () => <Content name="one" />
},
{
name: 'Two',
path: '/two',
component: () => <Content name="two" />
},
{
name: 'Three',
path: '/three',
component: () => <Content name="three" />
}
];
testProps = ({
location: mockLocation,
history: mockHistory,
tabs: testTabs
} as unknown) as HistoryTabsProps;
});
it('should render correctly', () => {
mockLocation.pathname = '/two';
const wrapper = shallow(<HistoryTabsWithoutRouter {...testProps} />);
expect(wrapper).toMatchSnapshot();
const tabs: ShallowWrapper<EuiTab> = wrapper.find(EuiTab);
expect(tabs.at(0).props().isSelected).toEqual(false);
expect(tabs.at(1).props().isSelected).toEqual(true);
expect(tabs.at(2).props().isSelected).toEqual(false);
});
it('should change the selected item on tab click', () => {
const wrapper = mount(
<MemoryRouter initialEntries={['/two']}>
<HistoryTabs tabs={testTabs} />
</MemoryRouter>
);
expect(wrapper.find('Content')).toMatchSnapshot();
wrapper
.find(EuiTab)
.at(2)
.simulate('click');
const tabs: ReactWrapper<EuiTab> = wrapper.find(EuiTab);
expect(tabs.at(0).props().isSelected).toEqual(false);
expect(tabs.at(1).props().isSelected).toEqual(false);
expect(tabs.at(2).props().isSelected).toEqual(true);
expect(wrapper.find('Content')).toMatchSnapshot();
});
});

View file

@ -0,0 +1,70 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`HistoryTabs should change the selected item on tab click 1`] = `
<Content
name="two"
>
<div>
two
</div>
</Content>
`;
exports[`HistoryTabs should change the selected item on tab click 2`] = `
<Content
name="three"
>
<div>
three
</div>
</Content>
`;
exports[`HistoryTabs should render correctly 1`] = `
<React.Fragment>
<EuiTabs
expand={false}
size="m"
>
<EuiTab
disabled={false}
isSelected={false}
key="/one--One"
onClick={[Function]}
>
One
</EuiTab>
<EuiTab
disabled={false}
isSelected={true}
key="/two--Two"
onClick={[Function]}
>
Two
</EuiTab>
<EuiTab
disabled={false}
isSelected={false}
key="/three--Three"
onClick={[Function]}
>
Three
</EuiTab>
</EuiTabs>
<Route
component={[Function]}
key="/one"
path="/one"
/>
<Route
component={[Function]}
key="/two"
path="/two"
/>
<Route
component={[Function]}
key="/three"
path="/three"
/>
</React.Fragment>
`;

View file

@ -0,0 +1,49 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
// @ts-ignore otherwise TS complains "Module ''@elastic/eui'' has no exported member 'EuiTab'"
import { EuiTab, EuiTabs } from '@elastic/eui';
import React from 'react';
import { Route, RouteComponentProps, withRouter } from 'react-router-dom';
export interface IHistoryTab {
path: string;
name: string;
component: React.SFC | React.ComponentClass;
}
export interface HistoryTabsProps extends RouteComponentProps {
tabs: IHistoryTab[];
}
const HistoryTabsWithoutRouter = ({
tabs,
history,
location
}: HistoryTabsProps) => {
return (
<React.Fragment>
<EuiTabs>
{tabs.map(tab => (
<EuiTab
onClick={() => history.push({ ...location, pathname: tab.path })}
isSelected={location.pathname === tab.path}
key={`${tab.path}--${tab.name}`}
>
{tab.name}
</EuiTab>
))}
</EuiTabs>
{tabs.map(tab => (
<Route path={tab.path} component={tab.component} key={tab.path} />
))}
</React.Fragment>
);
};
const HistoryTabs = withRouter(HistoryTabsWithoutRouter);
export { HistoryTabsWithoutRouter, HistoryTabs };