Skip to content

Commit

Permalink
Custom menu via /api/config with friendly default
Browse files Browse the repository at this point in the history
  • Loading branch information
tiffon committed Sep 21, 2017
1 parent f77ea5b commit fdf3576
Show file tree
Hide file tree
Showing 12 changed files with 377 additions and 37 deletions.
2 changes: 2 additions & 0 deletions src/actions/jaeger-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import JaegerAPI from '../api/jaeger';
* async wrapper to get the api object in case we're in demo mode.
*/

export const fetchConfig = createAction('@JAEGER_API/FETCH_CONFIG', () => JaegerAPI.fetchConfig());

export const fetchTrace = createAction(
'@JAEGER_API/FETCH_TRACE',
id => JaegerAPI.fetchTrace(id),
Expand Down
3 changes: 3 additions & 0 deletions src/api/jaeger.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ export const DEFAULT_DEPENDENCY_LOOKBACK = moment.duration(1, 'weeks').asMillise

const JaegerAPI = {
apiRoot: DEFAULT_API_ROOT,
fetchConfig() {
return getJSON(`${this.apiRoot}config`).catch(err => ({ error: err }));
},
fetchTrace(id) {
return getJSON(`${this.apiRoot}traces/${id}`);
},
Expand Down
27 changes: 20 additions & 7 deletions src/components/App/Page.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// @flow

// Copyright (c) 2017 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
Expand All @@ -18,26 +20,37 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

import PropTypes from 'prop-types';
import React from 'react';
import * as React from 'react';
import Helmet from 'react-helmet';
import { connect } from 'react-redux';

import TopNav from './TopNav';
import type { Config } from '../../types/config';

import './Page.css';

export default function JaegerUIPage({ children }) {
type JaegerUIPageProps = {
children: React.Node,
config: { data: Config },
};

function JaegerUIPage({ config, children }: JaegerUIPageProps) {
const menu = config && config.data && config.data.menu;
return (
<section className="jaeger-ui-page" id="jaeger-ui">
<Helmet title="Jaeger UI" />
<TopNav />
<TopNav menuConfig={menu} />
<div className="jaeger-ui--content">
{children}
</div>
</section>
);
}

JaegerUIPage.propTypes = {
children: PropTypes.node,
};
function mapStateToProps(state, ownProps) {
const { config } = state;
const { children } = ownProps;
return { children, config };
}

export default connect(mapStateToProps)(JaegerUIPage);
12 changes: 10 additions & 2 deletions src/components/App/TopNav.css
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,18 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/

.ui.menu.jaeger-ui--topnav {
border-radius: 0;
.TopNav {
/* !important is to override styles from semantic UI */
border-radius: 0 !important;
margin: 0 !important;
padding: 5px;
position: fixed;
width: 100%;
z-index: 1000;
}

.TopNav--DropdownItem {
display: block;
margin: -1em;
padding: 1em;
}
60 changes: 55 additions & 5 deletions src/components/App/TopNav.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// @flow

// Copyright (c) 2017 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
Expand All @@ -20,12 +22,45 @@

import React from 'react';
import { Link } from 'react-router-dom';
import { Dropdown, Menu } from 'semantic-ui-react';

import TraceIDSearchInput from './TraceIDSearchInput';
import type { ConfigMenuItem, ConfigMenuGroup } from '../../types/config';
import prefixUrl from '../../utils/prefix-url';

import './TopNav.css';

type TopNavProps = {
menuConfig: (ConfigMenuItem | ConfigMenuGroup)[],
};

function CustomNavItem({ label, url }: ConfigMenuItem) {
return (
<a href={url} className="header item" target="_blank">
{label}
</a>
);
}

function CustomNavDropdown({ label, items }: ConfigMenuGroup) {
return (
<Dropdown text={label} pointing className="link item">
<Dropdown.Menu>
{items.map(item => {
const { label: itemLabel, url } = item;
return (
<Dropdown.Item key={itemLabel}>
<a href={url} className="ui TopNav--DropdownItem" target="_blank">
{itemLabel}
</a>
</Dropdown.Item>
);
})}
</Dropdown.Menu>
</Dropdown>
);
}

const NAV_LINKS = [
{
key: 'dependencies',
Expand All @@ -39,13 +74,20 @@ const NAV_LINKS = [
},
];

export default function TopNav() {
export default function TopNav(props: TopNavProps) {
const { menuConfig } = props;
const menuItems = Array.isArray(menuConfig) ? menuConfig : [];
return (
<nav className="ui top inverted menu jaeger-ui--topnav">
<Menu inverted className="TopNav">
<Link to={prefixUrl('/')} className="header item">
{'Jaeger UI'}
Jaeger UI
</Link>

{menuItems.map(item => {
if (item.items) {
return <CustomNavDropdown key={item.label} {...item} />;
}
return <CustomNavItem key={item.label} {...item} />;
})}
<div className="right menu">
<div className="ui input">
<TraceIDSearchInput />
Expand All @@ -56,6 +98,14 @@ export default function TopNav() {
</Link>
)}
</div>
</nav>
</Menu>
);
}

TopNav.defaultProps = {
menuConfig: [],
};

// exported for tests
TopNav.CustomNavItem = CustomNavItem;
TopNav.CustomNavDropdown = CustomNavDropdown;
102 changes: 102 additions & 0 deletions src/components/App/TopNav.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

import React from 'react';
import { shallow } from 'enzyme';
import { Link } from 'react-router-dom';

import TopNav from './TopNav';

describe('<TopNav>', () => {
const labelGitHub = 'GitHub';
const labelAbout = 'About Jaeger';
const dropdownItems = [
{
label: 'Docs',
url: 'http://jaeger.readthedocs.io/en/latest/',
},
{
label: 'Twitter',
url: 'https://twitter.com/JaegerTracing',
},
];

const defaultProps = {
menuConfig: [
{
label: labelGitHub,
url: 'https://github.com/uber/jaeger',
},
{
label: labelAbout,
items: dropdownItems,
},
],
};

let wrapper;

beforeEach(() => {
wrapper = shallow(<TopNav {...defaultProps} />);
});

describe('renders the default menu options', () => {
it('renders the "Jaeger UI" button', () => {
const items = wrapper.find(Link).findWhere(link => /Jaeger UI/.test(link.text()));
expect(items.length).toBe(1);
});

it('renders the "Search" button', () => {
const items = wrapper.find(Link).findWhere(link => /Search/.test(link.text()));
expect(items.length).toBe(1);
});

it('renders the "Dependencies" button', () => {
const items = wrapper.find(Link).findWhere(link => /Dependencies/.test(link.text()));
expect(items.length).toBe(1);
});
});

describe('renders the custom menu', () => {
it('renders the top-level item', () => {
const item = wrapper.find(TopNav.CustomNavItem);
expect(item.length).toBe(1);
expect(item.prop('label')).toBe(labelGitHub);
});

describe('renders the nested menu items', () => {
it('renders the <CustomNavDropdown> component', () => {
const item = wrapper.find(TopNav.CustomNavDropdown);
expect(item.length).toBe(1);
expect(item.prop('label')).toBe(labelAbout);
expect(item.prop('items')).toBe(dropdownItems);
});

it('the <CustomNavDropdown> renders the links', () => {
const dropdown = shallow(<TopNav.CustomNavDropdown label={labelAbout} items={dropdownItems} />);
const links = dropdown.find('a');
expect(links.length).toBe(2);
const linkTexts = links.map(node => node.text()).sort();
const expectTexts = dropdownItems.map(item => item.label).sort();
expect(expectTexts).toEqual(linkTexts);
});
});
});
});
30 changes: 8 additions & 22 deletions src/components/App/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@

import React, { Component } from 'react';
import createHistory from 'history/createBrowserHistory';
import PropTypes from 'prop-types';
import { metrics } from 'react-metrics';
import { Provider } from 'react-redux';
import { Route, Redirect, Switch } from 'react-router-dom';
Expand All @@ -33,6 +32,7 @@ import NotFound from './NotFound';
import { ConnectedDependencyGraphPage } from '../DependencyGraph';
import { ConnectedSearchTracePage } from '../SearchTracePage';
import { ConnectedTracePage } from '../TracePage';
import { fetchConfig } from '../../actions/jaeger-api';
import JaegerAPI, { DEFAULT_API_ROOT } from '../../api/jaeger';
import configureStore from '../../utils/configure-store';
import metricConfig from '../../utils/metrics';
Expand All @@ -45,31 +45,17 @@ const PageWithMetrics = metrics(metricConfig)(Page);
const defaultHistory = createHistory();

export default class JaegerUIApp extends Component {
static get propTypes() {
return {
history: PropTypes.object,
apiRoot: PropTypes.string,
};
}

static get defaultProps() {
return {
history: defaultHistory,
apiRoot: DEFAULT_API_ROOT,
};
}

componentDidMount() {
const { apiRoot } = this.props;
JaegerAPI.apiRoot = apiRoot;
constructor(props) {
super(props);
this.store = configureStore(defaultHistory);
JaegerAPI.apiRoot = DEFAULT_API_ROOT;
this.store.dispatch(fetchConfig());
}

render() {
const { history } = this.props;
const store = configureStore(history);
return (
<Provider store={store}>
<ConnectedRouter history={history}>
<Provider store={this.store}>
<ConnectedRouter history={defaultHistory}>
<PageWithMetrics>
<Switch>
<Route path={prefixUrl('/search')} component={ConnectedSearchTracePage} />
Expand Down
2 changes: 1 addition & 1 deletion src/components/TracePage/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ THE SOFTWARE.
padding: 0.5em 0.5em 0 0.5em;
position: fixed;
width: 100%;
z-index: 1000001;
z-index: 3;
}

.trace-timeline-section {
Expand Down
Loading

0 comments on commit fdf3576

Please sign in to comment.