From fdf35768bc11a9cb01cecff61faf97834ed3af0e Mon Sep 17 00:00:00 2001 From: Joe Farro Date: Thu, 21 Sep 2017 04:28:23 -0400 Subject: [PATCH] Custom menu via /api/config with friendly default --- src/actions/jaeger-api.js | 2 + src/api/jaeger.js | 3 + src/components/App/Page.js | 27 ++++++-- src/components/App/TopNav.css | 12 +++- src/components/App/TopNav.js | 60 +++++++++++++++-- src/components/App/TopNav.test.js | 102 +++++++++++++++++++++++++++++ src/components/App/index.js | 30 +++------ src/components/TracePage/index.css | 2 +- src/constants/default-config.js | 82 +++++++++++++++++++++++ src/reducers/config.js | 57 ++++++++++++++++ src/reducers/index.js | 2 + src/types/config.js | 35 ++++++++++ 12 files changed, 377 insertions(+), 37 deletions(-) create mode 100644 src/components/App/TopNav.test.js create mode 100644 src/constants/default-config.js create mode 100644 src/reducers/config.js create mode 100644 src/types/config.js diff --git a/src/actions/jaeger-api.js b/src/actions/jaeger-api.js index 6e9b024591..bf7e825088 100644 --- a/src/actions/jaeger-api.js +++ b/src/actions/jaeger-api.js @@ -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), diff --git a/src/api/jaeger.js b/src/api/jaeger.js index 266f04ad15..9373ca1450 100644 --- a/src/api/jaeger.js +++ b/src/api/jaeger.js @@ -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}`); }, diff --git a/src/components/App/Page.js b/src/components/App/Page.js index a5ae110838..1ba5f95b89 100644 --- a/src/components/App/Page.js +++ b/src/components/App/Page.js @@ -1,3 +1,5 @@ +// @flow + // Copyright (c) 2017 Uber Technologies, Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy @@ -18,19 +20,26 @@ // 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 (
- +
{children}
@@ -38,6 +47,10 @@ export default function JaegerUIPage({ children }) { ); } -JaegerUIPage.propTypes = { - children: PropTypes.node, -}; +function mapStateToProps(state, ownProps) { + const { config } = state; + const { children } = ownProps; + return { children, config }; +} + +export default connect(mapStateToProps)(JaegerUIPage); diff --git a/src/components/App/TopNav.css b/src/components/App/TopNav.css index 73315ece98..05ed71e3ff 100644 --- a/src/components/App/TopNav.css +++ b/src/components/App/TopNav.css @@ -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; +} diff --git a/src/components/App/TopNav.js b/src/components/App/TopNav.js index 835356661b..ebff68dccc 100644 --- a/src/components/App/TopNav.js +++ b/src/components/App/TopNav.js @@ -1,3 +1,5 @@ +// @flow + // Copyright (c) 2017 Uber Technologies, Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy @@ -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 ( + + {label} + + ); +} + +function CustomNavDropdown({ label, items }: ConfigMenuGroup) { + return ( + + + {items.map(item => { + const { label: itemLabel, url } = item; + return ( + + + {itemLabel} + + + ); + })} + + + ); +} + const NAV_LINKS = [ { key: 'dependencies', @@ -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 ( - + ); } + +TopNav.defaultProps = { + menuConfig: [], +}; + +// exported for tests +TopNav.CustomNavItem = CustomNavItem; +TopNav.CustomNavDropdown = CustomNavDropdown; diff --git a/src/components/App/TopNav.test.js b/src/components/App/TopNav.test.js new file mode 100644 index 0000000000..8b99ff23c3 --- /dev/null +++ b/src/components/App/TopNav.test.js @@ -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('', () => { + 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(); + }); + + 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 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 renders the links', () => { + const dropdown = shallow(); + 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); + }); + }); + }); +}); diff --git a/src/components/App/index.js b/src/components/App/index.js index 4916ecce44..edeaec38fe 100644 --- a/src/components/App/index.js +++ b/src/components/App/index.js @@ -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'; @@ -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'; @@ -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 ( - - + + diff --git a/src/components/TracePage/index.css b/src/components/TracePage/index.css index 018a9241f0..4e5953b662 100644 --- a/src/components/TracePage/index.css +++ b/src/components/TracePage/index.css @@ -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 { diff --git a/src/constants/default-config.js b/src/constants/default-config.js new file mode 100644 index 0000000000..cca38e573c --- /dev/null +++ b/src/constants/default-config.js @@ -0,0 +1,82 @@ +// 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. + +export default { + menu: [ + { + label: 'About Jaeger', + items: [ + { + label: 'GitHub', + url: 'https://github.com/uber/jaeger', + }, + { + label: 'Docs', + url: 'http://jaeger.readthedocs.io/en/latest/', + }, + { + label: 'Twitter', + url: 'https://twitter.com/JaegerTracing', + }, + { + label: 'Discussion Group', + url: 'https://groups.google.com/forum/#!forum/jaeger-tracing', + }, + { + label: 'Gitter.im', + url: 'https://gitter.im/jaegertracing/Lobby', + }, + { + label: 'Blog', + url: 'https://medium.com/jaegertracing/', + }, + ], + }, + { + label: 'Create a ticket', + items: [ + { + label: 'Jaeger Core', + url: 'https://github.com/uber/jaeger/issues', + }, + { + label: 'Go Client', + url: 'https://github.com/uber/jaeger-client-go/issues', + }, + { + label: 'Java Client', + url: 'https://github.com/uber/jaeger-client-java/issues', + }, + { + label: 'Node Client', + url: 'https://github.com/uber/jaeger-client-node/issues', + }, + { + label: 'Python Client', + url: 'https://github.com/uber/jaeger-client-python/issues', + }, + { + label: 'Jaeger UI', + url: 'https://github.com/uber/jaeger-ui/issues', + }, + ], + }, + ], +}; diff --git a/src/reducers/config.js b/src/reducers/config.js new file mode 100644 index 0000000000..4233402aaf --- /dev/null +++ b/src/reducers/config.js @@ -0,0 +1,57 @@ +// 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 { handleActions } from 'redux-actions'; + +import { fetchConfig } from '../actions/jaeger-api'; +import defaultConfig from '../constants/default-config'; + +const initialState = { + data: {}, + loading: false, + error: null, +}; + +function fetchStarted(state) { + return { ...state, loading: true }; +} + +function fetchDone(state, { payload }) { + const data = payload; + // fetchConfig action creator is set to handle rejected promises + if (data.error) { + const { message, stack } = data.error; + return { ...state, error: { message, stack }, loading: false, data: defaultConfig }; + } + return { ...state, data, error: null, loading: false }; +} + +function fetchErred(state, { payload: error }) { + return { ...state, error: error.message, loading: false, data: defaultConfig }; +} + +export default handleActions( + { + [`${fetchConfig}_PENDING`]: fetchStarted, + [`${fetchConfig}_FULFILLED`]: fetchDone, + [`${fetchConfig}_REJECTED`]: fetchErred, + }, + initialState +); diff --git a/src/reducers/index.js b/src/reducers/index.js index 3fd240269c..b406fbe604 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -20,11 +20,13 @@ import { reducer as formReducer } from 'redux-form'; +import config from './config'; import dependencies from './dependencies'; import services from './services'; import trace from './trace'; export default { + config, dependencies, services, trace, diff --git a/src/types/config.js b/src/types/config.js new file mode 100644 index 0000000000..7779b88743 --- /dev/null +++ b/src/types/config.js @@ -0,0 +1,35 @@ +// @flow + +// 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. + +export type ConfigMenuItem = { + label: string, + url: string, +}; + +export type ConfigMenuGroup = { + label: string, + items: ConfigMenuItem[], +}; + +export type Config = { + menu: (ConfigMenuGroup | ConfigMenuItem)[], +};