From b7a3e74b6bd53f110755f2b4be76f3019a29de5e Mon Sep 17 00:00:00 2001 From: Joe Date: Mon, 29 Jan 2018 18:04:06 -0500 Subject: [PATCH] Use Ant Design instead of Semantic UI (#169) * Shift top nav to ant.design Signed-off-by: Joe Farro * Use the better string comparator Signed-off-by: Joe Farro * Search uses Ant - flow, tests, cleanup are TODO Signed-off-by: Joe Farro * Highlight currently active menu option Signed-off-by: Joe Farro * Using Ant for all pages, flow and tests are TODO Signed-off-by: Joe Farro * Fix bug with text ellipsis in FF Signed-off-by: Joe Farro * Fix flow after moving to ant design Signed-off-by: Joe Farro * Unit tests passing after shift to ant design Signed-off-by: Joe Farro * Fix loss of focus on first change in search form Signed-off-by: Joe Farro * Search page aesthetic tweaks Signed-off-by: Joe Farro * Remove basscss and semantic-ui. Signed-off-by: Joe Farro * Upgrade react-scripts to fix failing unit tests Signed-off-by: Joe Farro * Only bundle icons that are used from react-icons Signed-off-by: Joe Farro * Misc cleanup Signed-off-by: Joe Farro * Fix search form error when deselecting a service Signed-off-by: Joe Farro * Adjust the ant theme to the Jaeger teal color Signed-off-by: Joe Farro * Swith to using a variant of basscss (u-basscss) Signed-off-by: Joe Farro --- .flowconfig | 8 +- config-overrides-ant-variables.less | 21 + config-overrides.js | 33 + package.json | 22 +- src/components/App/App.css | 103 - src/components/App/NotFound.js | 14 +- src/components/App/Page.css | 13 +- src/components/App/Page.js | 17 +- src/components/App/TopNav.js | 108 +- src/components/App/TopNav.test.js | 28 +- src/components/App/TraceIDSearchInput.css | 19 + src/components/App/TraceIDSearchInput.js | 49 +- .../TraceSearchResult.css => App/index.css} | 20 +- src/components/App/index.js | 5 +- .../{DependencyGraph.css => index.css} | 9 + src/components/DependencyGraph/index.js | 77 +- src/components/DependencyGraph/index.test.js | 22 +- .../SearchTracePage/SearchDropdownInput.js | 82 - .../SearchDropdownInput.test.js | 71 - src/components/SearchTracePage/SearchForm.css | 44 + .../{TraceSearchForm.js => SearchForm.js} | 321 ++- ...rviceTag.test.js => SearchForm.markers.js} | 20 +- ...eSearchForm.test.js => SearchForm.test.js} | 33 +- .../SearchResults/ResultItem.css | 49 + .../SearchResults/ResultItem.js | 87 + .../SearchResults/ResultItem.markers.js | 16 + .../ResultItem.test.js} | 22 +- .../ScatterPlot.css} | 0 .../ScatterPlot.js} | 18 +- .../ScatterPlot.test.js} | 6 +- .../SearchTracePage/SearchResults/index.css | 35 + .../SearchTracePage/SearchResults/index.js | 123 + .../SearchResults/index.markers.js | 16 + .../SearchResults/index.test.js | 59 + .../SearchResults/react-vis.css | 1 + .../SearchTracePage/TraceSearchResult.js | 83 - src/components/SearchTracePage/index.css | 38 + src/components/SearchTracePage/index.js | 165 +- src/components/SearchTracePage/index.test.js | 39 +- src/components/SearchTracePage/react-vis.css | 1 - .../TracePage/KeyboardShortcutsHelp.css | 8 +- .../TracePage/KeyboardShortcutsHelp.js | 75 +- .../TracePage/SpanGraph/TickLabels.css | 4 +- src/components/TracePage/SpanGraph/index.js | 4 +- src/components/TracePage/TracePageHeader.css | 33 + src/components/TracePage/TracePageHeader.js | 155 +- .../TracePage/TracePageHeader.markers.js | 16 + .../TracePage/TracePageHeader.test.js | 11 +- .../TracePage/TraceTimelineViewer/SpanBar.css | 11 +- .../TraceTimelineViewer/SpanBarRow.css | 70 +- .../TraceTimelineViewer/SpanBarRow.js | 12 +- .../SpanDetail/AccordianKeyValues.css | 32 +- .../SpanDetail/AccordianKeyValues.js | 21 +- .../SpanDetail/AccordianKeyValues.markers.js | 16 + .../SpanDetail/AccordianKeyValues.test.js | 5 +- .../SpanDetail/AccordianLogs.css | 23 +- .../SpanDetail/AccordianLogs.js | 18 +- .../SpanDetail/KeyValuesTable.css | 19 +- .../SpanDetail/KeyValuesTable.js | 4 +- .../TraceTimelineViewer/SpanDetail/index.css | 4 + .../TraceTimelineViewer/SpanDetail/index.js | 43 +- .../SpanDetail/index.test.js | 29 +- .../TraceTimelineViewer/SpanDetailRow.css | 12 +- .../TraceTimelineViewer/SpanDetailRow.js | 2 +- .../TraceTimelineViewer/SpanTreeOffset.css | 15 +- .../TraceTimelineViewer/SpanTreeOffset.js | 14 +- .../TracePage/TraceTimelineViewer/Ticks.css | 12 +- .../TimelineHeaderRow/TimelineHeaderRow.css | 8 +- .../TraceTimelineViewer/TimelineRow.js | 2 +- .../VirtualizedTraceView.css | 6 +- .../TracePage/TraceTimelineViewer/index.css | 21 +- .../TracePage/TraceTimelineViewer/index.js | 3 +- src/components/TracePage/index.css | 15 +- src/components/TracePage/index.js | 33 +- src/components/TracePage/index.test.js | 9 +- src/components/common/ErrorMessage.css | 8 +- src/components/common/ErrorMessage.js | 27 +- .../TopNav.css => common/LabeledList.css} | 23 +- src/components/common/LabeledList.js | 53 + .../LoadingIndicator.css} | 14 +- .../LoadingIndicator.js} | 31 +- src/components/common/VirtSelect.css | 56 + src/components/common/VirtSelect.js | 93 + src/components/common/utils.css | 73 + src/index.js | 16 +- src/reducers/services.js | 6 +- src/utils/date.js | 22 + src/utils/redux-form-field-adapter.js | 35 + src/utils/sort.js | 6 +- src/utils/sort.test.js | 9 +- yarn.lock | 2476 +++++++++++------ 91 files changed, 3489 insertions(+), 2121 deletions(-) create mode 100644 config-overrides-ant-variables.less create mode 100644 config-overrides.js delete mode 100644 src/components/App/App.css create mode 100644 src/components/App/TraceIDSearchInput.css rename src/components/{SearchTracePage/TraceSearchResult.css => App/index.css} (69%) rename src/components/DependencyGraph/{DependencyGraph.css => index.css} (83%) delete mode 100644 src/components/SearchTracePage/SearchDropdownInput.js delete mode 100644 src/components/SearchTracePage/SearchDropdownInput.test.js create mode 100644 src/components/SearchTracePage/SearchForm.css rename src/components/SearchTracePage/{TraceSearchForm.js => SearchForm.js} (58%) rename src/components/SearchTracePage/{TraceServiceTag.test.js => SearchForm.markers.js} (59%) rename src/components/SearchTracePage/{TraceSearchForm.test.js => SearchForm.test.js} (92%) create mode 100644 src/components/SearchTracePage/SearchResults/ResultItem.css create mode 100644 src/components/SearchTracePage/SearchResults/ResultItem.js create mode 100644 src/components/SearchTracePage/SearchResults/ResultItem.markers.js rename src/components/SearchTracePage/{TraceSearchResult.test.js => SearchResults/ResultItem.test.js} (62%) rename src/components/SearchTracePage/{TraceResultsScatterPlot.css => SearchResults/ScatterPlot.css} (100%) rename src/components/SearchTracePage/{TraceResultsScatterPlot.js => SearchResults/ScatterPlot.js} (85%) rename src/components/SearchTracePage/{TraceResultsScatterPlot.test.js => SearchResults/ScatterPlot.test.js} (85%) create mode 100644 src/components/SearchTracePage/SearchResults/index.css create mode 100644 src/components/SearchTracePage/SearchResults/index.js create mode 100644 src/components/SearchTracePage/SearchResults/index.markers.js create mode 100644 src/components/SearchTracePage/SearchResults/index.test.js create mode 120000 src/components/SearchTracePage/SearchResults/react-vis.css delete mode 100644 src/components/SearchTracePage/TraceSearchResult.js create mode 100644 src/components/SearchTracePage/index.css delete mode 120000 src/components/SearchTracePage/react-vis.css create mode 100644 src/components/TracePage/TracePageHeader.css create mode 100644 src/components/TracePage/TracePageHeader.markers.js create mode 100644 src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianKeyValues.markers.js rename src/components/{App/TopNav.css => common/LabeledList.css} (68%) create mode 100644 src/components/common/LabeledList.js rename src/components/{SearchTracePage/TraceSearchForm.css => common/LoadingIndicator.css} (80%) rename src/components/{SearchTracePage/TraceServiceTag.js => common/LoadingIndicator.js} (54%) create mode 100644 src/components/common/VirtSelect.css create mode 100644 src/components/common/VirtSelect.js create mode 100644 src/components/common/utils.css create mode 100644 src/utils/redux-form-field-adapter.js diff --git a/.flowconfig b/.flowconfig index 3aecbf5a94..2f62e2b482 100644 --- a/.flowconfig +++ b/.flowconfig @@ -1,9 +1,9 @@ [ignore] # node_module packages that have a .flowconfig in conflict with this file -.*/node_modules/fbjs/.* -.*/node_modules/uber-licence/.* -.*/node_modules/redux-form/.* -.*/node_modules/react-motion/.* +/node_modules/uber-licence +/node_modules/redux-form +/node_modules/react-motion +/node_modules/draft-js [include] diff --git a/config-overrides-ant-variables.less b/config-overrides-ant-variables.less new file mode 100644 index 0000000000..53cceab91b --- /dev/null +++ b/config-overrides-ant-variables.less @@ -0,0 +1,21 @@ +@primary-color: #199; + +@font-size-base: 14px; +@text-color-dark: #e4e4e4; +@text-color-secondary-dark: #fff; + +// Layout +@layout-body-background :#fff; +@layout-header-background : #404040; +@layout-footer-background : @layout-body-background; +@layout-header-height : 64px; +@layout-header-padding : 0 50px; +@layout-footer-padding : 24px 50px; +@layout-sider-background : @layout-header-background; +@layout-trigger-height : 48px; +@layout-trigger-background : tint(@heading-color, 20%); +@layout-trigger-color : #fff; +@layout-zero-trigger-width : 36px; +@layout-zero-trigger-height : 42px; + +@menu-dark-bg: #151515; diff --git a/config-overrides.js b/config-overrides.js new file mode 100644 index 0000000000..0fb6e9a6af --- /dev/null +++ b/config-overrides.js @@ -0,0 +1,33 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* eslint-disable import/no-extraneous-dependencies */ + +const fs = require('fs'); +const { injectBabelPlugin } = require('react-app-rewired'); +const rewireLess = require('react-app-rewire-less'); +const lessToJs = require('less-vars-to-js'); + +// Read the less file in as string +const loadedVarOverrides = fs.readFileSync('config-overrides-ant-variables.less', 'utf8'); + +// Pass in file contents +const modifyVars = lessToJs(loadedVarOverrides); + +module.exports = function override(_config, env) { + let config = _config; + config = injectBabelPlugin(['import', { libraryName: 'antd', style: true }], config); + config = rewireLess.withLoaderOptions({ modifyVars })(config, env); + return config; +}; diff --git a/package.json b/package.json index 552f237c09..ece560e47d 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "homepage": null, "devDependencies": { "babel-eslint": "^7.2.3", + "babel-plugin-import": "^1.6.3", "bluebird": "^3.5.0", "enzyme": "^3.2.0", "enzyme-adapter-react-16": "^1.1.0", @@ -29,14 +30,17 @@ "eslint-plugin-jsx-a11y": "^6.0.2", "eslint-plugin-react": "^7.2.1", "husky": "^0.14.3", + "less-vars-to-js": "^1.2.1", "lint-staged": "^4.0.3", "prettier": "^1.5.3", + "react-app-rewire-less": "^2.1.0", + "react-app-rewired": "^1.4.0", "react-scripts": "^1.0.11", "react-test-renderer": "^15.6.1", "sinon": "^3.2.1" }, "dependencies": { - "basscss": "^8.0.3", + "antd": "^3.0.3", "chance": "^1.0.10", "classnames": "^2.2.5", "combokeys": "^3.0.0", @@ -59,15 +63,16 @@ "prop-types": "^15.5.10", "query-string": "^5.0.0", "react": "^16.0.0", - "react-addons-perf": "^15.4.2", "react-dimensions": "^1.3.0", "react-dom": "^16.0.0", "react-ga": "^2.2.0", "react-helmet": "^5.1.3", + "react-icons": "^2.2.7", "react-metrics": "^2.3.2", "react-redux": "^5.0.6", "react-router-dom": "^4.1.2", "react-router-redux": "5.0.0-alpha.6", + "react-virtualized-select": "^3.1.0", "react-vis": "^1.7.2", "react-vis-force": "^0.3.1", "recompose": "^0.25.0", @@ -77,18 +82,17 @@ "redux-form": "^7.0.3", "redux-promise-middleware": "^4.3.0", "reselect": "^3.0.1", - "semantic-ui-css": "^2.2.12", - "semantic-ui-react": "^0.71.4", "store": "^2.0.12", - "tween-functions": "^1.2.0" + "tween-functions": "^1.2.0", + "u-basscss": "2.0.0" }, "scripts": { - "start": "react-scripts start", + "start": "react-app-rewired start", "start:docs": "REACT_APP_DEMO=true react-scripts start", - "build": "react-scripts build", + "build": "react-app-rewired build", "eject": "react-scripts eject", - "test": "CI=1 react-scripts test --env=jsdom --color", - "test-dev": "react-scripts test --env=jsdom", + "test": "CI=1 react-app-rewired test --env=jsdom --color", + "test-dev": "react-app-rewired test --env=jsdom", "coverage": "npm run test -- --coverage", "lint": "npm run eslint && npm run prettier && npm run flow && npm run check-license", "eslint": "eslint src", diff --git a/src/components/App/App.css b/src/components/App/App.css deleted file mode 100644 index 427bf3ccd4..0000000000 --- a/src/components/App/App.css +++ /dev/null @@ -1,103 +0,0 @@ -/* -Copyright (c) 2017 Uber Technologies, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -#jaeger-ui-root { - height: 100%; -} - -.u-no-float { - float: none; -} - -.u-cursor-pointer { - cursor: pointer; -} - -/* override semantic UI webkit scrollbar styling (keep the other scroll styling) */ -body ::-webkit-scrollbar { - -webkit-appearance: none; - width: 7px; - height: 7px; -} - -a { - color: #11939a; -} - -a:hover { - color: #00474e; - cursor: pointer; -} - -.clearfix:after { - content: ' '; - visibility: hidden; - display: block; - height: 0; - clear: both; -} - -.pull-left { - float: left; -} - -.pull-right { - float: right; -} - -.hard { - padding: 0 !important; -} - -.soft { - padding: 1rem !important; -} - -.flush { - margin: 0 !important; -} - -.push { - margin: 1rem !important; -} - -.overflow-x { - overflow-x: scroll; -} - -.ui.compact.table td { - padding: 0.2em 0.5em; -} - -.ui.table td.light-grey { - background-color: #f1f1f1; -} - -.ui.table, -.ui.table thead tr:first-child > th:last-child, -.ui.table thead tr:first-child > th:first-child { - border-radius: 0; -} - -.ui.table thead th { - background-color: #f1f1f1; -} - -.ui.sortable.table thead th.sorted, -.ui.sortable.table thead th:hover, -.ui.sortable.table thead th.sorted:hover { - background-color: #d6d6d5; -} diff --git a/src/components/App/NotFound.js b/src/components/App/NotFound.js index 342340b6d6..bc2aa7f70a 100644 --- a/src/components/App/NotFound.js +++ b/src/components/App/NotFound.js @@ -26,16 +26,10 @@ type NotFoundProps = { export default function NotFound({ error }: NotFoundProps) { return ( -
-
-
-

Error

-
- {error && } -
- {'Back home'} -
-
+
+

Error

+ {error && } + {'Back home'}
); } diff --git a/src/components/App/Page.css b/src/components/App/Page.css index efc321a7ed..75775a0894 100644 --- a/src/components/App/Page.css +++ b/src/components/App/Page.css @@ -14,11 +14,14 @@ See the License for the specific language governing permissions and limitations under the License. */ -.jaeger-ui-page { - height: 100%; +.Page--topNav { + height: auto; + padding: 0; + position: fixed; + width: 100%; + z-index: 3; } -.jaeger-ui--content { - height: 100%; - padding-top: 50px; +.Page--content { + padding-top: 47px; } diff --git a/src/components/App/Page.js b/src/components/App/Page.js index 309da06982..79854f69fc 100644 --- a/src/components/App/Page.js +++ b/src/components/App/Page.js @@ -15,6 +15,7 @@ // limitations under the License. import * as React from 'react'; +import { Layout } from 'antd'; import Helmet from 'react-helmet'; import { connect } from 'react-redux'; import type { Location } from 'react-router-dom'; @@ -32,6 +33,8 @@ type PageProps = { config: Config, }; +const { Header, Content } = Layout; + // export for tests export class PageImpl extends React.Component { props: PageProps; @@ -50,14 +53,18 @@ export class PageImpl extends React.Component { } render() { - const { children, config } = this.props; + const { children, config, location } = this.props; const menu = config && config.menu; return ( -
+
- -
{children}
-
+ +
+ +
+ {children} +
+ ); } } diff --git a/src/components/App/TopNav.js b/src/components/App/TopNav.js index a51a886bef..3b545652c2 100644 --- a/src/components/App/TopNav.js +++ b/src/components/App/TopNav.js @@ -15,51 +15,22 @@ // limitations under the License. import React from 'react'; +import { Dropdown, Icon, Menu } from 'antd'; import _get from 'lodash/get'; 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 getConfig from '../../utils/config/get-config'; import prefixUrl from '../../utils/prefix-url'; -import './TopNav.css'; - type TopNavProps = { + activeKey: string, 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: 'search', to: prefixUrl('/search'), text: 'Search', }, @@ -67,37 +38,72 @@ const NAV_LINKS = [ if (_get(getConfig(), 'dependencies.menuEnabled')) { NAV_LINKS.push({ - key: 'dependencies', to: prefixUrl('/dependencies'), text: 'Dependencies', }); } +function CustomNavDropdown({ label, items }: ConfigMenuGroup) { + const menuItems = ( + + {items.map(item => { + const { label: itemLabel, url } = item; + return ( + + + {itemLabel} + + + ); + })} + + ); + return ( + + + {label} + + + ); +} + export default function TopNav(props: TopNavProps) { - const { menuConfig } = props; + const { activeKey, menuConfig } = props; const menuItems = Array.isArray(menuConfig) ? menuConfig : []; return ( - - - Jaeger UI - -
- -
- {NAV_LINKS.map(({ key, to, text }) => ( - - {text} - - ))} -
+
+ {menuItems.map(item => { if (item.items) { - return ; + return ( + + + + ); } - return ; + return ( + + + {item.label} + + + ); })} -
-
+ + + + Jaeger UI + + + + + {NAV_LINKS.map(({ to, text }) => ( + + {text} + + ))} + + ); } @@ -105,6 +111,4 @@ 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 index 9d4aa87e23..33817f0fd7 100644 --- a/src/components/App/TopNav.test.js +++ b/src/components/App/TopNav.test.js @@ -20,6 +20,7 @@ import TopNav from './TopNav'; describe('', () => { const labelGitHub = 'GitHub'; + const githubUrl = 'https://github.com/uber/jaeger'; const labelAbout = 'About Jaeger'; const dropdownItems = [ { @@ -36,7 +37,7 @@ describe('', () => { menuConfig: [ { label: labelGitHub, - url: 'https://github.com/uber/jaeger', + url: githubUrl, }, { label: labelAbout, @@ -70,27 +71,16 @@ describe('', () => { describe('renders the custom menu', () => { it('renders the top-level item', () => { - const item = wrapper.find(TopNav.CustomNavItem); + const item = wrapper.find(`[href="${githubUrl}"]`); expect(item.length).toBe(1); - expect(item.prop('label')).toBe(labelGitHub); + expect(item.text()).toMatch(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); - }); + it('renders the nested menu items', () => { + const item = wrapper.find(TopNav.CustomNavDropdown); + expect(item.length).toBe(1); + expect(item.prop('label')).toBe(labelAbout); + expect(item.prop('items')).toBe(dropdownItems); }); }); }); diff --git a/src/components/App/TraceIDSearchInput.css b/src/components/App/TraceIDSearchInput.css new file mode 100644 index 0000000000..3e1327f0f2 --- /dev/null +++ b/src/components/App/TraceIDSearchInput.css @@ -0,0 +1,19 @@ +/* +Copyright (c) 2017 Uber Technologies, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.TraceIDSearchInput--form { + line-height: inherit; +} diff --git a/src/components/App/TraceIDSearchInput.js b/src/components/App/TraceIDSearchInput.js index 83c25e80ad..1fd546578d 100644 --- a/src/components/App/TraceIDSearchInput.js +++ b/src/components/App/TraceIDSearchInput.js @@ -1,3 +1,5 @@ +// @flow + // Copyright (c) 2017 Uber Technologies, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,37 +14,38 @@ // See the License for the specific language governing permissions and // limitations under the License. -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; +import * as React from 'react'; +import { Form, Input } from 'antd'; import { withRouter } from 'react-router-dom'; +import type { RouterHistory } from 'react-router-dom'; + import prefixUrl from '../../utils/prefix-url'; -class TraceIDSearchInput extends Component { - goToTrace(e) { - this.props.history.push(prefixUrl(`/trace/${this.traceIDInput.value}`)); - e.preventDefault(); - return false; - } +import './TraceIDSearchInput.css'; + +type Props = { + history: RouterHistory, +}; + +class TraceIDSearchInput extends React.PureComponent { + props: Props; + + goToTrace = event => { + event.preventDefault(); + const value = event.target.elements.idInput.value; + if (value) { + this.props.history.push(prefixUrl(`/trace/${value}`)); + } + }; + render() { return ( -
this.goToTrace(e)}> - { - this.traceIDInput = input; - }} - /> -
+
+ +
); } } -TraceIDSearchInput.propTypes = { - history: PropTypes.shape({ - push: PropTypes.func, - }).isRequired, -}; - export default withRouter(TraceIDSearchInput); diff --git a/src/components/SearchTracePage/TraceSearchResult.css b/src/components/App/index.css similarity index 69% rename from src/components/SearchTracePage/TraceSearchResult.css rename to src/components/App/index.css index 094518c535..430743cf06 100644 --- a/src/components/SearchTracePage/TraceSearchResult.css +++ b/src/components/App/index.css @@ -14,20 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. */ -.ui.header.trace-search-result--duration { - color: #11939a; -} - -.trace-search-result:hover .ui.header.trace-search-result--duration { - color: #00474e; +/* Ant's default font-family, Helvetica Neue For Number, is inconcistent in thickness for a given weight */ +.font-reset, +html > body { + font-family: 'Helvetica Neue', Helvetica, 'Segoe UI', Roboto, Arial, sans-serif; } -.trace-search-result--spans { - display: inline-block; - margin-right: 0.5em; +a { + color: #11939a; } -.trace-search-result--erred-spans { - color: #c00; - display: inline-block; +a:hover { + color: #00474e; } diff --git a/src/components/App/index.js b/src/components/App/index.js index 47dceabc53..57337b0bf7 100644 --- a/src/components/App/index.js +++ b/src/components/App/index.js @@ -18,8 +18,6 @@ import { Provider } from 'react-redux'; import { Route, Redirect, Switch } from 'react-router-dom'; import { ConnectedRouter } from 'react-router-redux'; -import 'semantic-ui-css/semantic.min.css'; - import NotFound from './NotFound'; import Page from './Page'; import { ConnectedDependencyGraphPage } from '../DependencyGraph'; @@ -29,7 +27,8 @@ import JaegerAPI, { DEFAULT_API_ROOT } from '../../api/jaeger'; import configureStore from '../../utils/configure-store'; import prefixUrl from '../../utils/prefix-url'; -import './App.css'; +import './index.css'; +import '../common/utils.css'; const history = createHistory(); diff --git a/src/components/DependencyGraph/DependencyGraph.css b/src/components/DependencyGraph/index.css similarity index 83% rename from src/components/DependencyGraph/DependencyGraph.css rename to src/components/DependencyGraph/index.css index 544908faa5..2c84737bd4 100644 --- a/src/components/DependencyGraph/DependencyGraph.css +++ b/src/components/DependencyGraph/index.css @@ -14,6 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ +.DependencyGraph--graphWrapper { + bottom: 1rem; + left: 1rem; + overflow: hidden; + position: fixed; + right: 1rem; + top: 120px; +} + .rv-force__node { fill: #11939a; cursor: pointer; diff --git a/src/components/DependencyGraph/index.js b/src/components/DependencyGraph/index.js index df337decc2..d7b382eb05 100644 --- a/src/components/DependencyGraph/index.js +++ b/src/components/DependencyGraph/index.js @@ -12,23 +12,26 @@ // See the License for the specific language governing permissions and // limitations under the License. -import PropTypes from 'prop-types'; -import _get from 'lodash/get'; import React, { Component } from 'react'; +import { Tabs } from 'antd'; +import _get from 'lodash/get'; +import PropTypes from 'prop-types'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; -import { Menu } from 'semantic-ui-react'; import DAG from './DAG'; import DependencyForceGraph from './DependencyForceGraph'; -import NotFound from '../App/NotFound'; +import ErrorMessage from '../common/ErrorMessage'; +import LoadingIndicator from '../common/LoadingIndicator'; import * as jaegerApiActions from '../../actions/jaeger-api'; +import { FALLBACK_DAG_MAX_NUM_SERVICES } from '../../constants'; import { nodesPropTypes, linksPropTypes } from '../../propTypes/dependencies'; import { formatDependenciesAsNodesAndLinks } from '../../selectors/dependencies'; import getConfig from '../../utils/config/get-config'; -import { FALLBACK_DAG_MAX_NUM_SERVICES } from '../../constants'; -import './DependencyGraph.css'; +import './index.css'; + +const TabPane = Tabs.TabPane; // export for tests export const GRAPH_TYPES = { @@ -67,32 +70,20 @@ export default class DependencyGraphPage extends Component { this.props.fetchDependencies(); } - handleGraphTypeChange(graphType) { - this.setState({ graphType }); - } + handleGraphTypeChange = graphType => this.setState({ graphType }); render() { const { nodes, links, error, dependencies, loading } = this.props; const { graphType } = this.state; if (loading) { - return ( -
-
-
- ); + return ; } if (error) { - return ; + return ; } if (!nodes || !links) { - return ( -
-
-
No service dependencies found.
-
-
- ); + return
No service dependencies found.
; } const GRAPH_TYPE_OPTIONS = [GRAPH_TYPES.FORCE_DIRECTED]; @@ -100,33 +91,23 @@ export default class DependencyGraphPage extends Component { if (dependencies.length <= dagMaxNumServices) { GRAPH_TYPE_OPTIONS.push(GRAPH_TYPES.DAG); } + return ( -
- - {GRAPH_TYPE_OPTIONS.map(option => ( - this.handleGraphTypeChange(option.type)} - /> - ))} - -
- {graphType === 'FORCE_DIRECTED' && } - {graphType === 'DAG' && } -
-
+ + {GRAPH_TYPE_OPTIONS.map(opt => ( + +
+ {opt.type === 'FORCE_DIRECTED' && } + {opt.type === 'DAG' && } +
+
+ ))} +
); } } diff --git a/src/components/DependencyGraph/index.test.js b/src/components/DependencyGraph/index.test.js index 17578caeb1..a2b4574eae 100644 --- a/src/components/DependencyGraph/index.test.js +++ b/src/components/DependencyGraph/index.test.js @@ -13,12 +13,13 @@ // limitations under the License. import React from 'react'; +import { Tabs } from 'antd'; import { shallow } from 'enzyme'; -import { Menu } from 'semantic-ui-react'; import DAG from './DAG'; import DependencyForceGraph from './DependencyForceGraph'; import DependencyGraph, { GRAPH_TYPES, mapDispatchToProps, mapStateToProps } from './index'; +import LoadingIndicator from '../common/LoadingIndicator'; const childId = 'boomya'; const parentId = 'elder-one'; @@ -52,9 +53,9 @@ describe('', () => { }); it('shows a loading indicator when loading data', () => { - expect(wrapper.find('.loader').length).toBe(0); + expect(wrapper.find(LoadingIndicator).length).toBe(0); wrapper.setProps({ loading: true }); - expect(wrapper.find('.loader').length).toBe(1); + expect(wrapper.find(LoadingIndicator).length).toBe(1); }); it('shows an error message when passed error information', () => { @@ -72,24 +73,19 @@ describe('', () => { describe('graph types', () => { it('renders a menu with options for the graph types', () => { - expect(wrapper.find(Menu).length).toBe(1); - expect(wrapper.find(Menu.Item).length).toBe(Object.keys(GRAPH_TYPES).length); - expect(wrapper.find({ name: GRAPH_TYPES.FORCE_DIRECTED.name }).length).toBe(1); - expect(wrapper.find({ name: GRAPH_TYPES.DAG.name }).length).toBe(1); + expect(wrapper.find(Tabs.TabPane).length).toBe(Object.keys(GRAPH_TYPES).length); + expect(wrapper.find({ tab: GRAPH_TYPES.FORCE_DIRECTED.name }).length).toBe(1); + expect(wrapper.find({ tab: GRAPH_TYPES.DAG.name }).length).toBe(1); }); it('renders a force graph when FORCE_GRAPH is the selected type', () => { - const menuItem = wrapper.find({ name: GRAPH_TYPES.FORCE_DIRECTED.name }); - expect(menuItem.length).toBe(1); - menuItem.simulate('click'); + wrapper.simulate('change', GRAPH_TYPES.FORCE_DIRECTED.type); expect(wrapper.state('graphType')).toBe(GRAPH_TYPES.FORCE_DIRECTED.type); expect(wrapper.find(DependencyForceGraph).length).toBe(1); }); it('renders a DAG graph when DAG is the selected type', () => { - const forceMenuItem = wrapper.find({ name: GRAPH_TYPES.DAG.name }); - expect(forceMenuItem.length).toBe(1); - forceMenuItem.simulate('click'); + wrapper.simulate('change', GRAPH_TYPES.DAG.type); expect(wrapper.state('graphType')).toBe(GRAPH_TYPES.DAG.type); expect(wrapper.find(DAG).length).toBe(1); }); diff --git a/src/components/SearchTracePage/SearchDropdownInput.js b/src/components/SearchTracePage/SearchDropdownInput.js deleted file mode 100644 index c8880dec0c..0000000000 --- a/src/components/SearchTracePage/SearchDropdownInput.js +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright (c) 2017 Uber Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import { Dropdown } from 'semantic-ui-react'; - -import regexpEscape from '../../utils/regexp-escape'; - -/** - * We have to wrap the semantic ui component becuase it doesn't perform well - * when there are 200+ suggestions. - * - * We make sure only the props.maxResults are shown at one time to enhance usability. - * TODO: Identify if we should use this component. - */ -export default class SearchDropdownInput extends Component { - constructor(props) { - super(props); - this.state = { - currentItems: props.items.slice(0, props.maxResults), - }; - } - componentWillReceiveProps(nextProps) { - if (this.props.items.map(i => i.text).join(',') !== nextProps.items.map(i => i.text).join(',')) { - this.setState({ - currentItems: nextProps.items.slice(0, nextProps.maxResults), - }); - } - } - onSearch = (_, searchText) => { - const { items, maxResults } = this.props; - const regexStr = regexpEscape(searchText); - const regex = new RegExp(regexStr, 'i'); - return items.filter(v => regex.test(v.text)).slice(0, maxResults); - }; - render() { - const { input: { value, onChange } } = this.props; - const { currentItems } = this.state; - return ( - onChange(newValue)} - options={currentItems} - selection - scrolling - compact={false} - /> - ); - } -} - -SearchDropdownInput.defaultProps = { - maxResults: 250, - items: [], -}; -SearchDropdownInput.propTypes = { - items: PropTypes.arrayOf( - PropTypes.shape({ - text: PropTypes.string, - value: PropTypes.string, - }) - ), - input: PropTypes.shape({ - value: PropTypes.string, - onChange: PropTypes.func, - }).isRequired, - maxResults: PropTypes.number, -}; diff --git a/src/components/SearchTracePage/SearchDropdownInput.test.js b/src/components/SearchTracePage/SearchDropdownInput.test.js deleted file mode 100644 index ef505f62a6..0000000000 --- a/src/components/SearchTracePage/SearchDropdownInput.test.js +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) 2017 Uber Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import React from 'react'; -import { shallow } from 'enzyme'; -import { Dropdown } from 'semantic-ui-react'; - -import SearchDropdownInput from './SearchDropdownInput'; - -function toItem(s) { - return { text: s, value: s }; -} - -const MAX_RESULTS = 3; - -describe('', () => { - let currentItems; - let items; - let props; - let wrapper; - - beforeEach(() => { - items = ['abc', 'bcd', 'cde', 'abc', 'bcd', 'cde', 'abc', 'bcd', 'cde', ...'0123456789'].map(toItem); - currentItems = items.slice(0, MAX_RESULTS); - props = { - items, - maxResults: MAX_RESULTS, - input: { onChange: () => {}, value: null }, - }; - wrapper = shallow(); - }); - - it('does not explode', () => { - expect(wrapper).toBeDefined(); - }); - - it('limits the items via `maxResults`', () => { - const dropdown = wrapper.find(Dropdown); - const { options } = dropdown.props(); - expect(options.length).toBe(MAX_RESULTS); - expect(options).toEqual(currentItems); - }); - - it('adjusts the options when given new items', () => { - items = items.slice().reverse(); - wrapper.setProps({ items }); - const dropdown = wrapper.find(Dropdown); - const { options } = dropdown.props(); - expect(options).toEqual(items.slice(0, MAX_RESULTS)); - }); - - it('filters items by the searchText', () => { - const rx = /b/; - const dropdown = wrapper.find(Dropdown); - const { search } = dropdown.props(); - const filtered = search(null, rx.source); - const spec = items.filter(item => rx.test(item.text)).slice(0, MAX_RESULTS); - expect(filtered).toEqual(spec); - }); -}); diff --git a/src/components/SearchTracePage/SearchForm.css b/src/components/SearchTracePage/SearchForm.css new file mode 100644 index 0000000000..f3320215fa --- /dev/null +++ b/src/components/SearchTracePage/SearchForm.css @@ -0,0 +1,44 @@ +/* +Copyright (c) 2017 Uber Technologies, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.SearchForm--labelCount { + color: #999; +} + +.SearchForm--hintTrigger { + border: 1px solid #999; + border-radius: 100px; + color: #999; + cursor: pointer; + padding: 1px; +} + +.SearchForm--hintTrigger:hover { + color: #000; + border-color: #000; +} + +.SearchForm--tagsHintTitle { + margin-top: 0.5em; +} + +.SearchForm--tagsHintInfo { + padding-left: 1.7em; +} + +.SearchForm--tagsHintEg { + color: teal; +} diff --git a/src/components/SearchTracePage/TraceSearchForm.js b/src/components/SearchTracePage/SearchForm.js similarity index 58% rename from src/components/SearchTracePage/TraceSearchForm.js rename to src/components/SearchTracePage/SearchForm.js index 83f73077c0..3b4d385aed 100644 --- a/src/components/SearchTracePage/TraceSearchForm.js +++ b/src/components/SearchTracePage/SearchForm.js @@ -13,22 +13,32 @@ // limitations under the License. import React from 'react'; +import { Form, Input, Button, Popover, Select } from 'antd'; import logfmtParser from 'logfmt/lib/logfmt_parser'; import { stringify as logfmtStringify } from 'logfmt/lib/stringify'; import moment from 'moment'; import PropTypes from 'prop-types'; import queryString from 'query-string'; +import IoHelp from 'react-icons/lib/io/help'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import { Field, reduxForm, formValueSelector } from 'redux-form'; -import { Popup } from 'semantic-ui-react'; import store from 'store'; -import SearchDropdownInput from './SearchDropdownInput'; +import * as markers from './SearchForm.markers'; +import VirtSelect from '../common/VirtSelect'; import * as jaegerApiActions from '../../actions/jaeger-api'; import { formatDate, formatTime } from '../../utils/date'; +import reduxFormFieldAdapter from '../../utils/redux-form-field-adapter'; -import './TraceSearchForm.css'; +import './SearchForm.css'; + +const FormItem = Form.Item; +const Option = Select.Option; + +const AdaptedInput = reduxFormFieldAdapter(Input); +const AdaptedSelect = reduxFormFieldAdapter(Select); +const AdaptedVirtualSelect = reduxFormFieldAdapter(VirtSelect, option => (option ? option.value : null)); export function getUnixTimeStampInMSFromForm({ startDate, startDateTime, endDate, endDateTime }) { const start = `${startDate} ${startDateTime}`; @@ -137,161 +147,198 @@ export function submitForm(fields, searchTraces) { }); } -export function TraceSearchFormImpl(props) { - const { selectedService = '-', selectedLookback, handleSubmit, submitting, services } = props; +export function SearchFormImpl(props) { + const { handleSubmit, selectedLookback, selectedService = '-', services, submitting: disabled } = props; const selectedServicePayload = services.find(s => s.name === selectedService); - const operationsForService = (selectedServicePayload && selectedServicePayload.operations) || []; + const opsForSvc = (selectedServicePayload && selectedServicePayload.operations) || []; const noSelectedService = selectedService === '-' || !selectedService; const tz = selectedLookback === 'custom' ? new Date().toTimeString().replace(/^.*?GMT/, 'UTC') : null; return ( -
-
-
- - ({ text: v.name, value: v.name, key: v.name }))} - /> -
- - {!noSelectedService && ( -
- ({ text: op, value: op, key: op }))} - /> -
- )} + + + Service ({services.length}) + + } + > + ({ label: v.name, value: v.name })), + required: true, + }} + /> + + + Operation ({opsForSvc ? opsForSvc.length : 0}) + + } + > + ({ label: v, value: v })), + required: true, + }} + /> + -
- -
- + > + +
-
- -
- - - - - - - - - - - -
- - {selectedLookback === 'custom' && ( -
- + } + > + + + + + + + + + + + + + + + + + {selectedLookback === 'custom' && [ + -
- -
-
- -
+ Start Time{' '} + + Times are expressed in {tz} + + } + > + +
-
- )} - - {selectedLookback === 'custom' && ( -
- + } + > + + + , + + -
- -
-
- -
+ End Time{' '} + + Times are expressed in {tz} + + } + > + +
-
- )} + } + > + + + , + ]} -
-
- -
- -
-
-
- -
- -
-
-
+ + + -
- -
- -
-
- - - + + + + + + + + + + ); } -TraceSearchFormImpl.propTypes = { +SearchFormImpl.propTypes = { handleSubmit: PropTypes.func.isRequired, submitting: PropTypes.bool, services: PropTypes.arrayOf( @@ -304,7 +351,7 @@ TraceSearchFormImpl.propTypes = { selectedLookback: PropTypes.string, }; -TraceSearchFormImpl.defaultProps = { +SearchFormImpl.defaultProps = { services: [], submitting: false, selectedService: null, @@ -439,5 +486,5 @@ function mapDispatchToProps(dispatch) { export default connect(mapStateToProps, mapDispatchToProps)( reduxForm({ form: 'searchSideBar', - })(TraceSearchFormImpl) + })(SearchFormImpl) ); diff --git a/src/components/SearchTracePage/TraceServiceTag.test.js b/src/components/SearchTracePage/SearchForm.markers.js similarity index 59% rename from src/components/SearchTracePage/TraceServiceTag.test.js rename to src/components/SearchTracePage/SearchForm.markers.js index 8c16e84ad3..5a4d8ca475 100644 --- a/src/components/SearchTracePage/TraceServiceTag.test.js +++ b/src/components/SearchTracePage/SearchForm.markers.js @@ -12,21 +12,5 @@ // See the License for the specific language governing permissions and // limitations under the License. -import React from 'react'; -import { shallow } from 'enzyme'; - -import TraceServiceTag from './TraceServiceTag'; - -it(' tests', () => { - const wrapper = shallow( - - ); - const labelText = wrapper.find('.ui.label').first(); - - expect(labelText.text()).toBe('Service A (1)'); -}); +// eslint-disable-next-line import/prefer-default-export +export const SUBMIT_BTN = 'submit-btn'; diff --git a/src/components/SearchTracePage/TraceSearchForm.test.js b/src/components/SearchTracePage/SearchForm.test.js similarity index 92% rename from src/components/SearchTracePage/TraceSearchForm.test.js rename to src/components/SearchTracePage/SearchForm.test.js index 1ce0a39ac6..e2f3128ca0 100644 --- a/src/components/SearchTracePage/TraceSearchForm.test.js +++ b/src/components/SearchTracePage/SearchForm.test.js @@ -28,8 +28,9 @@ import { mapStateToProps, submitForm, traceIDsToQuery, - TraceSearchFormImpl as TraceSearchForm, -} from './TraceSearchForm'; + SearchFormImpl as SearchForm, +} from './SearchForm'; +import * as markers from './SearchForm.markers'; function makeDateParams(dateOffset = 0) { const date = new Date(); @@ -241,33 +242,37 @@ describe('submitForm()', () => { }); }); -describe('', () => { +describe('', () => { let wrapper; beforeEach(() => { - wrapper = shallow(); + wrapper = shallow(); }); - it('shows operations only when a service is selected', () => { - expect(wrapper.find('.search-form--operation').length).toBe(0); - - wrapper = shallow(); - expect(wrapper.find('.search-form--operation').length).toBe(1); + it('enables operations only when a service is selected', () => { + let ops = wrapper.find('[placeholder="Select An Operation"]'); + expect(ops.prop('props').disabled).toBe(true); + wrapper = shallow(); + ops = wrapper.find('[placeholder="Select An Operation"]'); + expect(ops.prop('props').disabled).toBe(false); }); it('shows custom date inputs when `props.selectedLookback` is "custom"', () => { function getDateFieldLengths(compWrapper) { - return [compWrapper.find('.js-test-start-input').length, compWrapper.find('.js-test-end-input').length]; + return [ + compWrapper.find('[placeholder="Start Date"]').length, + compWrapper.find('[placeholder="End Date"]').length, + ]; } expect(getDateFieldLengths(wrapper)).toEqual([0, 0]); - wrapper = shallow(); + wrapper = shallow(); expect(getDateFieldLengths(wrapper)).toEqual([1, 1]); }); it('disables the submit button when a service is not selected', () => { - let btn = wrapper.find('.js-test-submit-btn'); + let btn = wrapper.find(`[data-test="${markers.SUBMIT_BTN}"]`); expect(btn.prop('disabled')).toBeTruthy(); - wrapper = shallow(); - btn = wrapper.find('.js-test-submit-btn'); + wrapper = shallow(); + btn = wrapper.find(`[data-test="${markers.SUBMIT_BTN}"]`); expect(btn.prop('disabled')).toBeFalsy(); }); }); diff --git a/src/components/SearchTracePage/SearchResults/ResultItem.css b/src/components/SearchTracePage/SearchResults/ResultItem.css new file mode 100644 index 0000000000..93c0094cc6 --- /dev/null +++ b/src/components/SearchTracePage/SearchResults/ResultItem.css @@ -0,0 +1,49 @@ +/* +Copyright (c) 2017 Uber Technologies, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.ResultItem { + border: 1px solid #e6e6e6; + background: #fbfbfb; +} + +.ResultItem:hover { + background: #f5f5f5; + border-color: #d8d8d8; +} + +.ResultItem--title { + background: #ececec; + border-bottom: 1px solid #d8d8d8; + padding: 0.5rem; + position: relative; +} + +.ResultItem--durationBar { + background: #d7e7ea; + bottom: 0; + left: 0; + position: absolute; + top: 0; +} + +.ResultItem:hover > * > .ResultItem--durationBar { + background: #c5dde0; +} + +.ResultItem--serviceTag { + border-left-width: 15px; + margin: 0; +} diff --git a/src/components/SearchTracePage/SearchResults/ResultItem.js b/src/components/SearchTracePage/SearchResults/ResultItem.js new file mode 100644 index 0000000000..9dceaf0cfe --- /dev/null +++ b/src/components/SearchTracePage/SearchResults/ResultItem.js @@ -0,0 +1,87 @@ +// @flow + +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import React from 'react'; +import { Col, Divider, Row, Tag } from 'antd'; +import { sortBy } from 'lodash'; +import moment from 'moment'; + +import * as markers from './ResultItem.markers'; +import { FALLBACK_TRACE_NAME } from '../../../constants'; +import colorGenerator from '../../../utils/color-generator'; +import { formatDuration, formatRelativeDate } from '../../../utils/date'; + +import type { TraceSummary } from '../../../types/search'; + +import './ResultItem.css'; + +export default function ResultItem({ + trace, + durationPercent = 100, +}: { + trace: TraceSummary, + durationPercent: number, +}) { + const { duration, services, timestamp, numberOfErredSpans, numberOfSpans, traceName } = trace; + const mDate = moment(timestamp); + const timeStr = mDate.format('h:mm:ss a'); + const fromNow = mDate.fromNow(); + return ( +
+
+ + {formatDuration(duration * 1000)} +

{traceName || FALLBACK_TRACE_NAME}

+
+ + + + {numberOfSpans} Span{numberOfSpans > 1 && 's'} + + {Boolean(numberOfErredSpans) && ( + + {numberOfErredSpans} Error{numberOfErredSpans > 1 && 's'} + + )} + + +
    + {sortBy(services, s => s.name).map(service => { + const { name, numberOfSpans: count } = service; + return ( +
  • + + {name} ({count}) + +
  • + ); + })} +
+ + + {formatRelativeDate(timestamp)} + + {timeStr.slice(0, -3)} {timeStr.slice(-2)} +
+ {fromNow} + +
+
+ ); +} diff --git a/src/components/SearchTracePage/SearchResults/ResultItem.markers.js b/src/components/SearchTracePage/SearchResults/ResultItem.markers.js new file mode 100644 index 0000000000..8b7eaa5787 --- /dev/null +++ b/src/components/SearchTracePage/SearchResults/ResultItem.markers.js @@ -0,0 +1,16 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export const NUM_SPANS = 'num-spans'; +export const SERVICE_TAGS = 'service-tags'; diff --git a/src/components/SearchTracePage/TraceSearchResult.test.js b/src/components/SearchTracePage/SearchResults/ResultItem.test.js similarity index 62% rename from src/components/SearchTracePage/TraceSearchResult.test.js rename to src/components/SearchTracePage/SearchResults/ResultItem.test.js index b0b368b3e0..4ea7bb7ed5 100644 --- a/src/components/SearchTracePage/TraceSearchResult.test.js +++ b/src/components/SearchTracePage/SearchResults/ResultItem.test.js @@ -13,10 +13,11 @@ // limitations under the License. import React from 'react'; +import { Tag } from 'antd'; import { shallow } from 'enzyme'; -import TraceSearchResult from './TraceSearchResult'; -import TraceServiceTag from './TraceServiceTag'; +import ResultItem from './ResultItem'; +import * as markers from './ResultItem.markers'; const testTraceProps = { duration: 100, @@ -31,21 +32,22 @@ const testTraceProps = { numberOfSpans: 5, }; -it(' should render base case correctly', () => { - const wrapper = shallow(); +it(' should render base case correctly', () => { + const wrapper = shallow(); const numberOfSpanText = wrapper - .find('.trace-search-result--spans') + .find(`[data-test="${markers.NUM_SPANS}"]`) .first() + .render() .text(); - const numberOfServicesTags = wrapper.find(TraceServiceTag).length; - expect(numberOfSpanText).toBe('5 spans'); + const numberOfServicesTags = wrapper.find(`[data-test="${markers.SERVICE_TAGS}"]`).find(Tag).length; + expect(numberOfSpanText).toBe('5 Spans'); expect(numberOfServicesTags).toBe(1); }); -it(' should not render any ServiceTags when there are no services', () => { +it(' should not render any ServiceTags when there are no services', () => { const wrapper = shallow( - should not render any ServiceTags when there are no se durationPercent={50} /> ); - const numberOfServicesTags = wrapper.find(TraceServiceTag).length; + const numberOfServicesTags = wrapper.find(`[data-test="${markers.SERVICE_TAGS}"]`).find(Tag).length; expect(numberOfServicesTags).toBe(0); }); diff --git a/src/components/SearchTracePage/TraceResultsScatterPlot.css b/src/components/SearchTracePage/SearchResults/ScatterPlot.css similarity index 100% rename from src/components/SearchTracePage/TraceResultsScatterPlot.css rename to src/components/SearchTracePage/SearchResults/ScatterPlot.css diff --git a/src/components/SearchTracePage/TraceResultsScatterPlot.js b/src/components/SearchTracePage/SearchResults/ScatterPlot.js similarity index 85% rename from src/components/SearchTracePage/TraceResultsScatterPlot.js rename to src/components/SearchTracePage/SearchResults/ScatterPlot.js index 77583070bd..d8ebe65bb1 100644 --- a/src/components/SearchTracePage/TraceResultsScatterPlot.js +++ b/src/components/SearchTracePage/SearchResults/ScatterPlot.js @@ -19,13 +19,13 @@ import dimensions from 'react-dimensions'; import { XYPlot, XAxis, YAxis, MarkSeries, Hint } from 'react-vis'; import { compose, withState, withProps } from 'recompose'; -import { FALLBACK_TRACE_NAME } from '../../constants'; -import { formatDuration } from '../../utils/date'; +import { FALLBACK_TRACE_NAME } from '../../../constants'; +import { formatDuration } from '../../../utils/date'; import './react-vis.css'; -import './TraceResultsScatterPlot.css'; +import './ScatterPlot.css'; -function TraceResultsScatterPlotBase(props) { +function ScatterPlotImpl(props) { const { data, containerWidth, onValueClick, overValue, onValueOver, onValueOut } = props; return (
@@ -64,7 +64,7 @@ const valueShape = PropTypes.shape({ name: PropTypes.string, }); -TraceResultsScatterPlotBase.propTypes = { +ScatterPlotImpl.propTypes = { containerWidth: PropTypes.number, data: PropTypes.arrayOf(valueShape).isRequired, overValue: valueShape, @@ -73,17 +73,17 @@ TraceResultsScatterPlotBase.propTypes = { onValueOver: PropTypes.func.isRequired, }; -TraceResultsScatterPlotBase.defaultProps = { +ScatterPlotImpl.defaultProps = { containerWidth: null, overValue: null, }; -const TraceResultsScatterPlot = compose( +const ScatterPlot = compose( withState('overValue', 'setOverValue', null), withProps(({ setOverValue }) => ({ onValueOver: value => setOverValue(value), onValueOut: () => setOverValue(null), })) -)(TraceResultsScatterPlotBase); +)(ScatterPlotImpl); -export default dimensions()(TraceResultsScatterPlot); +export default dimensions()(ScatterPlot); diff --git a/src/components/SearchTracePage/TraceResultsScatterPlot.test.js b/src/components/SearchTracePage/SearchResults/ScatterPlot.test.js similarity index 85% rename from src/components/SearchTracePage/TraceResultsScatterPlot.test.js rename to src/components/SearchTracePage/SearchResults/ScatterPlot.test.js index 0727379433..ca74fce916 100644 --- a/src/components/SearchTracePage/TraceResultsScatterPlot.test.js +++ b/src/components/SearchTracePage/SearchResults/ScatterPlot.test.js @@ -15,11 +15,11 @@ import React from 'react'; import { shallow } from 'enzyme'; -import TraceResultsScatterPlot from './TraceResultsScatterPlot'; +import ScatterPlot from './ScatterPlot'; -it(' should render base case correctly', () => { +it(' should render base case correctly', () => { const wrapper = shallow( - void, + loading: boolean, + maxTraceDuration: number, + traces: {}[], +}; + +const Option = Select.Option; + +/** + * Contains the dropdown to sort and filter trace search results + */ +function SelectSortImpl() { + return ( + + ); +} + +const SelectSort = reduxForm({ + form: 'traceResultsSort', + initialValues: { + sortBy: orderBy.MOST_RECENT, + }, +})(SelectSortImpl); + +export const sortFormSelector = formValueSelector('traceResultsSort'); + +export default function SearchResults(props: SearchResultsProps) { + const { goToTrace, loading, maxTraceDuration, traces } = props; + if (loading) { + return ; + } + if (!Array.isArray(traces) || !traces.length) { + return ( +
+ No trace results. Try another query. +
+ ); + } + return ( +
+
+
+
+ ({ + x: t.timestamp, + y: t.duration, + traceID: t.traceID, + size: t.numberOfSpans, + name: t.traceName, + }))} + onValueClick={t => { + goToTrace(t.traceID); + }} + /> +
+
+ +

+ {traces.length} Trace{traces.length > 1 && 's'} +

+
+
+
+
+
    + {traces.map(trace => ( +
  • + + + +
  • + ))} +
+
+
+ ); +} diff --git a/src/components/SearchTracePage/SearchResults/index.markers.js b/src/components/SearchTracePage/SearchResults/index.markers.js new file mode 100644 index 0000000000..b911cc3a72 --- /dev/null +++ b/src/components/SearchTracePage/SearchResults/index.markers.js @@ -0,0 +1,16 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// eslint-disable-next-line import/prefer-default-export +export const NO_RESULTS = 'no-results'; diff --git a/src/components/SearchTracePage/SearchResults/index.test.js b/src/components/SearchTracePage/SearchResults/index.test.js new file mode 100644 index 0000000000..c8ac6bdaf6 --- /dev/null +++ b/src/components/SearchTracePage/SearchResults/index.test.js @@ -0,0 +1,59 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import React from 'react'; +import { shallow } from 'enzyme'; + +import SearchResults from './'; +import * as markers from './index.markers'; +import ResultItem from './ResultItem'; +import ScatterPlot from './ScatterPlot'; +import LoadingIndicator from '../../common/LoadingIndicator'; + +describe('', () => { + let wrapper; + let traces; + let props; + + beforeEach(() => { + traces = [{ traceID: 'a', spans: [], processes: {} }, { traceID: 'b', spans: [], processes: {} }]; + props = { + traces, + goToTrace: () => {}, + loading: false, + maxTraceDuration: 1, + }; + wrapper = shallow(); + }); + + it('shows the "no results" message when the search result is empty', () => { + wrapper.setProps({ traces: [] }); + expect(wrapper.find(`[data-test="${markers.NO_RESULTS}"]`).length).toBe(1); + }); + + it('shows a loading indicator if loading traces', () => { + wrapper.setProps({ loading: true }); + expect(wrapper.find(LoadingIndicator).length).toBe(1); + }); + + describe('search finished with results', () => { + it('shows a scatter plot', () => { + expect(wrapper.find(ScatterPlot).length).toBe(1); + }); + + it('shows a result entry for each trace', () => { + expect(wrapper.find(ResultItem).length).toBe(traces.length); + }); + }); +}); diff --git a/src/components/SearchTracePage/SearchResults/react-vis.css b/src/components/SearchTracePage/SearchResults/react-vis.css new file mode 120000 index 0000000000..8fc839f747 --- /dev/null +++ b/src/components/SearchTracePage/SearchResults/react-vis.css @@ -0,0 +1 @@ +../../../../node_modules/react-vis/dist/style.css \ No newline at end of file diff --git a/src/components/SearchTracePage/TraceSearchResult.js b/src/components/SearchTracePage/TraceSearchResult.js deleted file mode 100644 index a78bf2ad02..0000000000 --- a/src/components/SearchTracePage/TraceSearchResult.js +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) 2017 Uber Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import PropTypes from 'prop-types'; -import React from 'react'; -import { sortBy } from 'lodash'; -import moment from 'moment'; - -import TraceServiceTag from './TraceServiceTag'; -import { FALLBACK_TRACE_NAME } from '../../constants'; -import { formatDuration } from '../../utils/date'; - -import './TraceSearchResult.css'; - -const getBackgroundStyle = durationPercent => - `linear-gradient( - 90deg, rgba(17, 147, 154, .3) ${durationPercent}%, whitesmoke ${durationPercent - 100}%)`; - -export default function TraceSearchResult({ trace, durationPercent = 100 }) { - const { duration, services, timestamp, numberOfErredSpans, numberOfSpans, traceName } = trace; - return ( -
-
- {traceName || FALLBACK_TRACE_NAME} - {formatDuration(duration * 1000)} -
-
-
-
- - {numberOfSpans} span{numberOfSpans > 1 && 's'} - - {Boolean(numberOfErredSpans) && ( - - {numberOfErredSpans} error{numberOfErredSpans > 1 && 's'} - - )} -
-
- {sortBy(services, s => s.name).map(service => ( -
- -
- ))} -
-
- - {moment(timestamp).format('hh:mm:ss a')} ( - {moment(timestamp).fromNow()} - ) - -
-
-
-
- ); -} - -TraceSearchResult.propTypes = { - trace: PropTypes.shape({ - duration: PropTypes.number, - services: PropTypes.array, - timestamp: PropTypes.number, - numberOfSpans: PropTypes.number, - }).isRequired, - durationPercent: PropTypes.number.isRequired, -}; diff --git a/src/components/SearchTracePage/index.css b/src/components/SearchTracePage/index.css new file mode 100644 index 0000000000..313943627e --- /dev/null +++ b/src/components/SearchTracePage/index.css @@ -0,0 +1,38 @@ +/* +Copyright (c) 2017 Uber Technologies, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.SearchTracePage--column { + padding: 1rem 0.5rem; +} + +.SearchTracePage--column:first-child { + padding-left: 1rem; +} + +.SearchTracePage--column:last-child { + padding-right: 1rem; +} + +.SearchTracePage--find { + background-color: #f5f5f5; + border: 1px solid #e6e6e6; + padding: 1rem; +} + +.SearchTracePage--logo { + display: block; + margin: 13rem auto 0 auto; +} diff --git a/src/components/SearchTracePage/index.js b/src/components/SearchTracePage/index.js index 285601203f..8e3f52a65c 100644 --- a/src/components/SearchTracePage/index.js +++ b/src/components/SearchTracePage/index.js @@ -13,56 +13,25 @@ // limitations under the License. import React, { Component } from 'react'; +import { Col, Row } from 'antd'; import _values from 'lodash/values'; import PropTypes from 'prop-types'; import queryString from 'query-string'; import { connect } from 'react-redux'; -import { Link } from 'react-router-dom'; import { bindActionCreators } from 'redux'; -import { Field, reduxForm, formValueSelector } from 'redux-form'; import store from 'store'; -import JaegerLogo from '../../img/jaeger-logo.svg'; - import * as jaegerApiActions from '../../actions/jaeger-api'; -import TraceSearchForm from './TraceSearchForm'; -import TraceSearchResult from './TraceSearchResult'; -import TraceResultsScatterPlot from './TraceResultsScatterPlot'; +import SearchForm from './SearchForm'; +import SearchResults, { sortFormSelector } from './SearchResults'; import ErrorMessage from '../common/ErrorMessage'; -import * as orderBy from '../../model/order-by'; +import LoadingIndicator from '../common/LoadingIndicator'; import { sortTraces, getTraceSummaries } from '../../model/search'; -import { getPercentageOfDuration } from '../../utils/date'; import getLastXformCacher from '../../utils/get-last-xform-cacher'; import prefixUrl from '../../utils/prefix-url'; -/** - * Contains the dropdown to sort and filter trace search results - */ -function TraceResultsFilterFormImpl() { - return ( -
-
- - - - - - - - -
-
- ); -} - -const TraceResultsFilterForm = reduxForm({ - form: 'traceResultsFilters', - initialValues: { - sortBy: orderBy.MOST_RECENT, - }, -})(TraceResultsFilterFormImpl); - -const traceResultsFiltersFormSelector = formValueSelector('traceResultsFilters'); +import './index.css'; +import JaegerLogo from '../../img/jaeger-logo.svg'; export default class SearchTracePage extends Component { componentDidMount() { @@ -77,6 +46,10 @@ export default class SearchTracePage extends Component { } } + goToTrace = traceID => { + this.props.history.push(prefixUrl(`/trace/${traceID}`)); + }; + render() { const { errors, @@ -84,99 +57,47 @@ export default class SearchTracePage extends Component { loadingServices, loadingTraces, maxTraceDuration, - numberOfTraceResults, services, traceResults, } = this.props; const hasTraceResults = traceResults && traceResults.length > 0; + const showErrors = errors && !loadingTraces; + const showLogo = isHomepage && !hasTraceResults && !loadingTraces && !errors; return ( -
-
-
-

Find Traces

- {!loadingServices && services ? ( - - ) : ( -
-
-
- )} -
-
-
- {loadingTraces &&
} - {errors && - !loadingTraces && ( -
+
+ + +
+

Find Traces

+ {!loadingServices && services ? : } +
+ + + {showErrors && ( +

There was an error querying for traces:

{errors.map(err => )}
)} - {isHomepage && - !hasTraceResults && ( -
-
- presentation -
-
- )} - {!isHomepage && - !hasTraceResults && - !loadingTraces && - !errors && ( -
- No trace results. Try another query. -
- )} - {hasTraceResults && - !loadingTraces && ( -
-
-
-
- ({ - x: t.timestamp, - y: t.duration, - traceID: t.traceID, - size: t.numberOfSpans, - name: t.traceName, - }))} - onValueClick={t => { - this.props.history.push(prefixUrl(`/trace/${t.traceID}`)); - }} - /> -
-
-
- - {numberOfTraceResults} Trace - {numberOfTraceResults > 1 && 's'} - -
-
- -
-
-
-
-
-
    - {traceResults.map(trace => ( -
  • - - - -
  • - ))} -
-
-
+ {showLogo && ( + presentation )} -
+ {!showErrors && + !showLogo && ( + + )} + +
); } @@ -186,7 +107,6 @@ SearchTracePage.propTypes = { isHomepage: PropTypes.bool, // eslint-disable-next-line react/forbid-prop-types traceResults: PropTypes.array, - numberOfTraceResults: PropTypes.number, maxTraceDuration: PropTypes.number, loadingServices: PropTypes.bool, loadingTraces: PropTypes.bool, @@ -248,7 +168,7 @@ export function mapStateToProps(state) { if (serviceError) { errors.push(serviceError); } - const sortBy = traceResultsFiltersFormSelector(state, 'sortBy'); + const sortBy = sortFormSelector(state, 'sortBy'); sortTraces(traces, sortBy); return { @@ -258,7 +178,6 @@ export function mapStateToProps(state) { loadingServices, errors: errors.length ? errors : null, maxTraceDuration: maxDuration, - numberOfTraceResults: traces.length, sortTracesBy: sortBy, traceResults: traces, urlQueryParams: query, diff --git a/src/components/SearchTracePage/index.test.js b/src/components/SearchTracePage/index.test.js index 9229ef917b..7bd04d0975 100644 --- a/src/components/SearchTracePage/index.test.js +++ b/src/components/SearchTracePage/index.test.js @@ -32,9 +32,8 @@ import { shallow, mount } from 'enzyme'; import store from 'store'; import SearchTracePage, { mapStateToProps } from './index'; -import TraceResultsScatterPlot from './TraceResultsScatterPlot'; -import TraceSearchForm from './TraceSearchForm'; -import TraceSearchResult from './TraceSearchResult'; +import SearchForm from './SearchForm'; +import LoadingIndicator from '../common/LoadingIndicator'; import traceGenerator from '../../demo/trace-generators'; import { MOST_RECENT } from '../../model/order-by'; import transformTraceData from '../../model/transform-trace-data'; @@ -79,22 +78,15 @@ describe('', () => { store.get = oldFn; }); - describe('loading', () => { - it('shows a loading indicator if loading services', () => { - wrapper.setProps({ loadingServices: true }); - expect(wrapper.find('.js-test-search-loader').length).toBe(1); - }); - - it('shows a loading indicator if loading traces', () => { - wrapper.setProps({ loadingTraces: true }); - expect(wrapper.find('.js-test-traces-loader').length).toBe(1); - }); + it('shows a loading indicator if loading services', () => { + wrapper.setProps({ loadingServices: true }); + expect(wrapper.find(LoadingIndicator).length).toBe(1); }); it('shows a search form when services are loaded', () => { const services = [{ name: 'svc-a', operations: ['op-a'] }]; wrapper.setProps({ services }); - expect(wrapper.find(TraceSearchForm).length).toBe(1); + expect(wrapper.find(SearchForm).length).toBe(1); }); it('shows an error message if there is an error message', () => { @@ -106,25 +98,6 @@ describe('', () => { wrapper.setProps({ isHomepage: true, traceResults: [] }); expect(wrapper.find('.js-test-logo').length).toBe(1); }); - - it('shows the "no results" message when the search result is empty', () => { - wrapper.setProps({ traceResults: [] }); - expect(wrapper.find('.js-test-no-results').length).toBe(1); - }); - - describe('search finished with results', () => { - it('shows a scatter plot', () => { - expect(wrapper.find(TraceResultsScatterPlot).length).toBe(1); - }); - - it('shows the results filter form', () => { - expect(wrapper.find('TraceResultsFilterFormImpl').length).toBe(1); - }); - - it('shows a result entry for each trace', () => { - expect(wrapper.find(TraceSearchResult).length).toBe(traceResults.length); - }); - }); }); describe('mapStateToProps()', () => { diff --git a/src/components/SearchTracePage/react-vis.css b/src/components/SearchTracePage/react-vis.css deleted file mode 120000 index c874c81f39..0000000000 --- a/src/components/SearchTracePage/react-vis.css +++ /dev/null @@ -1 +0,0 @@ -../../../node_modules/react-vis/dist/style.css \ No newline at end of file diff --git a/src/components/TracePage/KeyboardShortcutsHelp.css b/src/components/TracePage/KeyboardShortcutsHelp.css index 35bc1aeb82..b7c0990072 100644 --- a/src/components/TracePage/KeyboardShortcutsHelp.css +++ b/src/components/TracePage/KeyboardShortcutsHelp.css @@ -14,11 +14,17 @@ See the License for the specific language governing permissions and limitations under the License. */ -.KeyboardShortcutsHelp kbd { +.KeyboardShortcutsHelp--cta { + font-size: 1.2rem; + margin: 0 -7px; +} + +.KeyboardShortcutsHelp--table kbd { background: #f5f5f5; border: 1px solid #e8e8e8; border-bottom: 1px solid #ddd; color: #000; margin-right: 0.4em; + font-family: monospace; padding: 0.25em 0.3em; } diff --git a/src/components/TracePage/KeyboardShortcutsHelp.js b/src/components/TracePage/KeyboardShortcutsHelp.js index 460d3461eb..bf6b9cc96d 100644 --- a/src/components/TracePage/KeyboardShortcutsHelp.js +++ b/src/components/TracePage/KeyboardShortcutsHelp.js @@ -15,12 +15,18 @@ // limitations under the License. import React from 'react'; -import { Button, Modal } from 'semantic-ui-react'; +import { Button, Modal, Table } from 'antd'; import { kbdMappings } from './keyboard-shortcuts'; import './KeyboardShortcutsHelp.css'; +type KeyboardShortcutsHelpProps = { + className: ?string, +}; + +const { Column } = Table; + const symbolConv = { up: '↑', right: '→', @@ -49,40 +55,45 @@ function convertKeys(keyConfig: string | string[]): string[][] { return config.map(str => str.split('+').map(part => symbolConv[part] || part.toUpperCase())); } -export default function KeyboardShortcutsHelp() { - const rows = []; +function helpModal() { + const data = []; Object.keys(kbdMappings).forEach(title => { const keyConfigs = convertKeys(kbdMappings[title]); - const configs = keyConfigs.map(config => ( - - {config.map(s => {s})} - {descriptions[title]} - - )); - rows.push(...configs); + data.push( + ...keyConfigs.map(config => ({ + key: String(config), + kbds: config.map(s => {s}), + description: descriptions[title], + })) + ); }); - return ( - -

⌘

- - } + + const content = ( + - Keyboard Shortcuts - - -
- - - - - - - {rows} -
Key(s)Description
- - -
+ + + + ); + + Modal.info({ + content, + maskClosable: true, + title: 'Keyboard Shortcuts', + width: '50%', + }); +} + +export default function KeyboardShortcutsHelp(props: KeyboardShortcutsHelpProps) { + const { className } = props; + return ( + ); } diff --git a/src/components/TracePage/SpanGraph/TickLabels.css b/src/components/TracePage/SpanGraph/TickLabels.css index 5bd8181c2b..3a0e789228 100644 --- a/src/components/TracePage/SpanGraph/TickLabels.css +++ b/src/components/TracePage/SpanGraph/TickLabels.css @@ -15,13 +15,13 @@ limitations under the License. */ .TickLabels { - height: 1.25rem; + height: 1rem; position: relative; } .TickLabels--label { color: #717171; - font-size: 0.8rem; + font-size: 0.7rem; position: absolute; user-select: none; } diff --git a/src/components/TracePage/SpanGraph/index.js b/src/components/TracePage/SpanGraph/index.js index de66e917aa..1dc76ed97e 100644 --- a/src/components/TracePage/SpanGraph/index.js +++ b/src/components/TracePage/SpanGraph/index.js @@ -84,9 +84,9 @@ export default class SpanGraph extends React.PureComponent +
-
+
+ + + Trace JSON + + + + + Trace JSON (unadjusted) + + + + ); + + const overviewItems = [ + { + key: 'start', + label: 'Trace Start:', + value: formatDatetime(timestamp), + }, + { + key: 'duration', + label: 'Duration:', + value: formatDuration(duration), + }, + { + key: 'svc-count', + label: 'Services:', + value: numServices, + }, + { + key: 'depth', + label: 'Depth:', + value: maxDepth, + }, + { + key: 'span-count', + label: 'Total Spans:', + value: numSpans, + }, + ]; + return ( -
-
-
-

- - - +
+ -
- -
-
-
-
- updateTextFilter(event.target.value)} - /> -
+

+ + + + + +
+ updateTextFilter(event.target.value)} + defaultValue={textFilter} + data-test={markers.IN_TRACE_SEARCH} + />
- {!slimView && ( -
- {HEADER_ITEMS.map(({ renderer, propName, title, key }) => { - let value: ?React.Node; - if (propName) { - value = props[propName]; - } else if (renderer) { - value = renderer(props); - } else { - throw new Error('Invalid HEADER_ITEM configuration'); - } - return ( -
- {title}: - {value} -
- ); - })} -
- )} + {!slimView && }
); } diff --git a/src/components/TracePage/TracePageHeader.markers.js b/src/components/TracePage/TracePageHeader.markers.js new file mode 100644 index 0000000000..ab078772d5 --- /dev/null +++ b/src/components/TracePage/TracePageHeader.markers.js @@ -0,0 +1,16 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// eslint-disable-next-line import/prefer-default-export +export const IN_TRACE_SEARCH = 'in-trace-search'; diff --git a/src/components/TracePage/TracePageHeader.test.js b/src/components/TracePage/TracePageHeader.test.js index 26920f9f49..d9a0ed857c 100644 --- a/src/components/TracePage/TracePageHeader.test.js +++ b/src/components/TracePage/TracePageHeader.test.js @@ -17,6 +17,7 @@ import sinon from 'sinon'; import { shallow, mount } from 'enzyme'; import TracePageHeader, { HEADER_ITEMS } from './TracePageHeader'; +import * as markers from './TracePageHeader.markers'; describe('', () => { const defaultProps = { @@ -42,8 +43,8 @@ describe('', () => { }); it('renders the trace title', () => { - const h2 = wrapper.find('h2').first(); - expect(h2.contains(defaultProps.name)).toBeTruthy(); + const h1 = wrapper.find('h1').first(); + expect(h1.contains(defaultProps.name)).toBeTruthy(); }); it('renders the header items', () => { @@ -53,15 +54,15 @@ describe('', () => { }); }); - it('calls the context updateTextFilter() function for onChange of the input', () => { + it('calls updateTextFilter() function for onChange of the input', () => { const updateTextFilter = sinon.spy(); const props = { ...defaultProps, updateTextFilter }; wrapper = shallow(); const event = { target: { value: 'my new value' } }; wrapper - .find('#trace-page__text-filter') + .find(`[data-test="${markers.IN_TRACE_SEARCH}"]`) .first() - .prop('onChange')(event); + .simulate('change', event); expect(updateTextFilter.calledWith('my new value')).toBeTruthy(); }); }); diff --git a/src/components/TracePage/TraceTimelineViewer/SpanBar.css b/src/components/TracePage/TraceTimelineViewer/SpanBar.css index 12dea165a0..f52b8fcd73 100644 --- a/src/components/TracePage/TraceTimelineViewer/SpanBar.css +++ b/src/components/TracePage/TraceTimelineViewer/SpanBar.css @@ -21,7 +21,6 @@ limitations under the License. right: 0; top: 0; overflow: hidden; - opacity: 0.5; z-index: 0; } @@ -34,8 +33,8 @@ limitations under the License. border-radius: 3px; min-width: 2px; position: absolute; - height: 50%; - top: 25%; + height: 36%; + top: 32%; } .SpanBar--rpc { @@ -46,6 +45,7 @@ limitations under the License. } .SpanBar--label { + color: #aaa; font-size: 12px; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; line-height: 1em; @@ -61,3 +61,8 @@ limitations under the License. .SpanBar--label.is-left { right: 100%; } + +.span-row.is-expanded .SpanBar--label, +.span-row:hover .SpanBar--label { + color: #000; +} diff --git a/src/components/TracePage/TraceTimelineViewer/SpanBarRow.css b/src/components/TracePage/TraceTimelineViewer/SpanBarRow.css index 788a0258a5..8e21e60328 100644 --- a/src/components/TracePage/TraceTimelineViewer/SpanBarRow.css +++ b/src/components/TracePage/TraceTimelineViewer/SpanBarRow.css @@ -14,9 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ +.span-row.is-filtered-out { + opacity: 0.2; +} + .span-name-column { position: relative; - text-overflow: ellipsis; white-space: nowrap; z-index: 1; } @@ -36,15 +39,16 @@ limitations under the License. } .span-name-wrapper { - background: #fafafa; + background: #f8f8f8; + line-height: 27px; overflow: hidden; - text-overflow: ellipsis; + display: flex; } .span-name-wrapper:hover { border-right: 1px solid #bbb; float: left; - min-width: 100%; + min-width: calc(100% + 1px); overflow: visible; } @@ -59,12 +63,44 @@ limitations under the License. } .span-name { - border-left: 4px solid; color: #000; cursor: pointer; - display: inline; + flex: 1 1 auto; outline: none; + overflow: hidden; + padding-left: 4px; padding-right: 0.25em; + position: relative; + text-overflow: ellipsis; +} + +.span-name::before { + content: ' '; + position: absolute; + top: 4px; + bottom: 4px; + left: 0; + border-left: 4px solid; + border-left-color: inherit; +} + +.span-name.is-detail-expanded::before { + bottom: 0; +} + +/* This is so the hit area of the span-name extends the rest of the width of the span-name column */ +.span-name::after { + background: transparent; + bottom: 0; + content: ' '; + left: 0; + position: absolute; + top: 0; + width: 1000px; +} + +.span-name:focus { + text-decoration: none; } .endpoint-name { @@ -76,7 +112,7 @@ limitations under the License. } .span-svc-name { - padding: 0.5rem; + padding: 0 0.25rem 0 0.5rem; font-size: 1.05em; } @@ -112,3 +148,23 @@ limitations under the License. right: 0%; z-index: 1; } + +.SpanBarRow--errorIcon { + background: #db2828; + border-radius: 6.5px; + color: #fff; + font-size: 0.85em; + margin-right: 0.25rem; + padding: 1px; +} + +.SpanBarRow--rpcColorMarker { + border-radius: 6.5px; + display: inline-block; + font-size: 0.85em; + height: 1em; + margin-right: 0.25rem; + padding: 1px; + width: 1em; + vertical-align: middle; +} diff --git a/src/components/TracePage/TraceTimelineViewer/SpanBarRow.js b/src/components/TracePage/TraceTimelineViewer/SpanBarRow.js index 510b9aec2d..55adc52ef3 100644 --- a/src/components/TracePage/TraceTimelineViewer/SpanBarRow.js +++ b/src/components/TracePage/TraceTimelineViewer/SpanBarRow.js @@ -15,6 +15,8 @@ // limitations under the License. import * as React from 'react'; +import IoAlert from 'react-icons/lib/io/alert'; +import IoArrowRightA from 'react-icons/lib/io/arrow-right-a'; import TimelineRow from './TimelineRow'; import SpanTreeOffset from './SpanTreeOffset'; @@ -120,7 +122,7 @@ export default class SpanBarRow extends React.PureComponent { level={depth + 1} hasChildren={isParent} childrenVisible={isChildrenExpanded} - onClick={this._childrenToggle} + onClick={isParent ? this._childrenToggle : null} /> { - {showErrorIcon &&