From ec48cfd86b7fac7b0d86b7370e1bf2d1b0c2fe9a Mon Sep 17 00:00:00 2001 From: Jason Rhodes Date: Mon, 24 Sep 2018 10:03:55 -0400 Subject: [PATCH] Adds traces overview with mock data (#22628) * Adds traces overview with mock data * Updates service overview snapshots * Updates based on review feedback * Adds tests for ManagedTable and ImpactBar * Refactored transaction overview to use new managed table component * Fixes tab size * Cleans up some tests and types * Cleans up types and tests * kbn bootstrap changes to x-pack yarn.lock * Removed jsconfig file in apm * Updates snapshot tests * Reversed bogus yarn lock change in x-pack * review feedback wip * Resolves typescript issues --- package.json | 3 +- x-pack/plugins/apm/jsconfig.json | 4 - .../apm/public/components/app/Main/Home.tsx | 40 ++++ .../components/app/Main/__test__/Home.test.js | 15 ++ .../__test__/__snapshots__/Home.test.js.snap | 30 +++ .../Main/{routeConfig.js => routeConfig.tsx} | 31 ++- .../app/ServiceOverview/List/index.js | 143 ------------- .../__test__/List.test.js | 6 +- .../__test__/__snapshots__/List.test.js.snap | 110 +--------- .../{List => ServiceList}/__test__/props.json | 0 .../app/ServiceOverview/ServiceList/index.js | 96 +++++++++ .../__test__/ServiceOverview.test.js | 2 +- .../ServiceOverview.test.js.snap | 22 +- .../components/app/ServiceOverview/index.js | 6 +- .../components/app/ServiceOverview/view.js | 33 +-- .../app/TraceOverview/TraceList.tsx | 94 +++++++++ .../components/app/TraceOverview/index.tsx | 18 ++ .../components/app/TraceOverview/view.tsx | 37 ++++ .../app/TransactionOverview/List/index.js | 188 +++++++----------- .../{EmptyMessage.js => EmptyMessage.tsx} | 23 +-- .../ImpactBar/__test__/ImpactBar.test.js | 21 ++ .../__snapshots__/ImpactBar.test.js.snap | 21 ++ .../components/shared/ImpactBar/index.tsx | 19 ++ .../components/shared/KueryBar/index.js | 2 +- .../__test__/ManagedTable.test.js | 52 +++++ .../__snapshots__/ManagedTable.test.js.snap | 103 ++++++++++ .../components/shared/ManagedTable/index.tsx | 92 +++++++++ .../shared/SetupInstructionsLink.tsx | 24 +++ .../public/store/mockData/mockTraceList.json | 30 +++ .../store/reactReduxRequest/traceList.js | 52 +++++ .../apm/public/style/global_overrides.css | 9 +- yarn.lock | 21 +- 32 files changed, 893 insertions(+), 454 deletions(-) delete mode 100644 x-pack/plugins/apm/jsconfig.json create mode 100644 x-pack/plugins/apm/public/components/app/Main/Home.tsx create mode 100644 x-pack/plugins/apm/public/components/app/Main/__test__/Home.test.js create mode 100644 x-pack/plugins/apm/public/components/app/Main/__test__/__snapshots__/Home.test.js.snap rename x-pack/plugins/apm/public/components/app/Main/{routeConfig.js => routeConfig.tsx} (78%) delete mode 100644 x-pack/plugins/apm/public/components/app/ServiceOverview/List/index.js rename x-pack/plugins/apm/public/components/app/ServiceOverview/{List => ServiceList}/__test__/List.test.js (89%) rename x-pack/plugins/apm/public/components/app/ServiceOverview/{List => ServiceList}/__test__/__snapshots__/List.test.js.snap (84%) rename x-pack/plugins/apm/public/components/app/ServiceOverview/{List => ServiceList}/__test__/props.json (100%) create mode 100644 x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.js create mode 100644 x-pack/plugins/apm/public/components/app/TraceOverview/TraceList.tsx create mode 100644 x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx create mode 100644 x-pack/plugins/apm/public/components/app/TraceOverview/view.tsx rename x-pack/plugins/apm/public/components/shared/{EmptyMessage.js => EmptyMessage.tsx} (59%) create mode 100644 x-pack/plugins/apm/public/components/shared/ImpactBar/__test__/ImpactBar.test.js create mode 100644 x-pack/plugins/apm/public/components/shared/ImpactBar/__test__/__snapshots__/ImpactBar.test.js.snap create mode 100644 x-pack/plugins/apm/public/components/shared/ImpactBar/index.tsx create mode 100644 x-pack/plugins/apm/public/components/shared/ManagedTable/__test__/ManagedTable.test.js create mode 100644 x-pack/plugins/apm/public/components/shared/ManagedTable/__test__/__snapshots__/ManagedTable.test.js.snap create mode 100644 x-pack/plugins/apm/public/components/shared/ManagedTable/index.tsx create mode 100644 x-pack/plugins/apm/public/components/shared/SetupInstructionsLink.tsx create mode 100644 x-pack/plugins/apm/public/store/mockData/mockTraceList.json create mode 100644 x-pack/plugins/apm/public/store/reactReduxRequest/traceList.js diff --git a/package.json b/package.json index 5370eb9ac579c..24310c1e10996 100644 --- a/package.json +++ b/package.json @@ -236,9 +236,10 @@ "@types/node": "^8.10.20", "@types/prop-types": "^15.5.3", "@types/puppeteer": "^1.6.2", - "@types/react": "^16.3.14", + "@types/react": "16.3.14", "@types/react-dom": "^16.0.5", "@types/react-redux": "^6.0.6", + "@types/react-router-dom": "^4.3.1", "@types/react-virtualized": "^9.18.7", "@types/redux": "^3.6.31", "@types/redux-actions": "^2.2.1", diff --git a/x-pack/plugins/apm/jsconfig.json b/x-pack/plugins/apm/jsconfig.json deleted file mode 100644 index bdf4e8b91f067..0000000000000 --- a/x-pack/plugins/apm/jsconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "exclude": ["node_modules", "**/node_modules/*", "build"] -} diff --git a/x-pack/plugins/apm/public/components/app/Main/Home.tsx b/x-pack/plugins/apm/public/components/app/Main/Home.tsx new file mode 100644 index 0000000000000..9be8185fa1acf --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Main/Home.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// @ts-ignore +import { EuiTabbedContent } from '@elastic/eui'; +import React from 'react'; +// @ts-ignore +import { KueryBar } from '../../shared/KueryBar'; +import { SetupInstructionsLink } from '../../shared/SetupInstructionsLink'; +// @ts-ignore +import { HeaderContainer } from '../../shared/UIComponents'; +// @ts-ignore +import { ServiceOverview } from '../ServiceOverview'; +import { TraceOverview } from '../TraceOverview'; + +export function Home() { + return ( +
+ +

APM

+ +
+ + + }, + { id: 'traces_overview', name: 'Traces', content: } + ]} + /> +
+ ); +} diff --git a/x-pack/plugins/apm/public/components/app/Main/__test__/Home.test.js b/x-pack/plugins/apm/public/components/app/Main/__test__/Home.test.js new file mode 100644 index 0000000000000..cb856c30e19c9 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Main/__test__/Home.test.js @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { shallow } from 'enzyme'; +import React from 'react'; +import { Home } from '../Home'; + +describe('Home component', () => { + it('should render', () => { + expect(shallow()).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/apm/public/components/app/Main/__test__/__snapshots__/Home.test.js.snap b/x-pack/plugins/apm/public/components/app/Main/__test__/__snapshots__/Home.test.js.snap new file mode 100644 index 0000000000000..0383b11745400 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Main/__test__/__snapshots__/Home.test.js.snap @@ -0,0 +1,30 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Home component should render 1`] = ` +
+ +

+ APM +

+ +
+ + , + "id": "services_overview", + "name": "Services", + }, + Object { + "content": , + "id": "traces_overview", + "name": "Traces", + }, + ] + } + /> +
+`; diff --git a/x-pack/plugins/apm/public/components/app/Main/routeConfig.js b/x-pack/plugins/apm/public/components/app/Main/routeConfig.tsx similarity index 78% rename from x-pack/plugins/apm/public/components/app/Main/routeConfig.js rename to x-pack/plugins/apm/public/components/app/Main/routeConfig.tsx index 8b7bfa2c4c4aa..b3bcf516ae60b 100644 --- a/x-pack/plugins/apm/public/components/app/Main/routeConfig.js +++ b/x-pack/plugins/apm/public/components/app/Main/routeConfig.tsx @@ -6,25 +6,40 @@ import React from 'react'; import { Redirect } from 'react-router-dom'; -import ServiceOverview from '../ServiceOverview'; +// @ts-ignore +import { legacyDecodeURIComponent } from '../../../utils/url'; +// @ts-ignore import ErrorGroupDetails from '../ErrorGroupDetails'; +// @ts-ignore import ErrorGroupOverview from '../ErrorGroupOverview'; +// @ts-ignore import TransactionDetails from '../TransactionDetails'; +// @ts-ignore import TransactionOverview from '../TransactionOverview'; -import { legacyDecodeURIComponent } from '../../../utils/url'; +import { Home } from './Home'; + +interface BreadcrumbArgs { + match: { + params: StringMap; + }; +} + +interface RenderArgs { + location: StringMap; +} export const routes = [ { exact: true, path: '/', - component: ServiceOverview, + component: Home, breadcrumb: 'APM' }, { exact: true, path: '/:serviceName/errors/:groupId', component: ErrorGroupDetails, - breadcrumb: ({ match }) => match.params.groupId + breadcrumb: ({ match }: BreadcrumbArgs) => match.params.groupId }, { exact: true, @@ -44,8 +59,8 @@ export const routes = [ { exact: true, path: '/:serviceName', - breadcrumb: ({ match }) => match.params.serviceName, - render: ({ location }) => { + breadcrumb: ({ match }: BreadcrumbArgs) => match.params.serviceName, + render: ({ location }: RenderArgs) => { return ( + breadcrumb: ({ match }: BreadcrumbArgs) => legacyDecodeURIComponent(match.params.transactionType) }, { exact: true, path: '/:serviceName/transactions/:transactionType/:transactionName', component: TransactionDetails, - breadcrumb: ({ match }) => + breadcrumb: ({ match }: BreadcrumbArgs) => legacyDecodeURIComponent(match.params.transactionName) } ]; diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/List/index.js b/x-pack/plugins/apm/public/components/app/ServiceOverview/List/index.js deleted file mode 100644 index b692adfcfd2e2..0000000000000 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/List/index.js +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import orderBy from 'lodash.orderby'; -import styled from 'styled-components'; -import numeral from '@elastic/numeral'; -import { EuiBasicTable } from '@elastic/eui'; -import { RelativeLink } from '../../../../utils/url'; -import { fontSizes, truncate } from '../../../../style/variables'; -import TooltipOverlay from '../../../shared/TooltipOverlay'; -import { asMillisWithDefault } from '../../../../utils/formatters'; - -function formatNumber(value) { - if (value === 0) { - return '0'; - } - const formatted = numeral(value).format('0.0'); - return formatted <= 0.1 ? '< 0.1' : formatted; -} - -// TODO: duplicated -function paginateItems({ items, pageIndex, pageSize }) { - return items.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize); -} - -function formatString(value) { - return value || 'N/A'; -} - -const AppLink = styled(RelativeLink)` - font-size: ${fontSizes.large}; - ${truncate('100%')}; -`; - -class List extends Component { - state = { - page: { - index: 0, - size: 10 - }, - sort: { - field: 'serviceName', - direction: 'asc' - } - }; - - onTableChange = ({ page = {}, sort = {} }) => { - this.setState({ page, sort }); - }; - - render() { - const columns = [ - { - field: 'serviceName', - name: 'Name', - width: '50%', - sortable: true, - render: serviceName => ( - - - {formatString(serviceName)} - - - ) - }, - { - field: 'agentName', - name: 'Agent', - sortable: true, - render: agentName => formatString(agentName) - }, - { - field: 'avgResponseTime', - name: 'Avg. response time', - sortable: true, - dataType: 'number', - render: value => asMillisWithDefault(value) - }, - { - field: 'transactionsPerMinute', - name: 'Trans. per minute', - sortable: true, - dataType: 'number', - render: value => `${formatNumber(value)} tpm` - }, - { - field: 'errorsPerMinute', - name: 'Errors per minute', - sortable: true, - dataType: 'number', - render: value => `${formatNumber(value)} err.` - } - ]; - - const sortedItems = orderBy( - this.props.items, - this.state.sort.field, - this.state.sort.direction - ); - - const paginatedItems = paginateItems({ - items: sortedItems, - pageIndex: this.state.page.index, - pageSize: this.state.page.size - }); - - return ( - - ); - } -} - -List.propTypes = { - noItemsMessage: PropTypes.node, - items: PropTypes.array -}; - -List.defaultProps = { - items: [] -}; - -export default List; diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/List/__test__/List.test.js b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js similarity index 89% rename from x-pack/plugins/apm/public/components/app/ServiceOverview/List/__test__/List.test.js rename to x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js index 3d02fd15c137e..d8ecffa2f18b5 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/List/__test__/List.test.js +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js @@ -8,7 +8,7 @@ import React from 'react'; import { mount } from 'enzyme'; import { MemoryRouter } from 'react-router-dom'; -import List from '../index'; +import { ServiceList } from '../index'; import props from './props.json'; import { mountWithRouterAndStore, @@ -25,7 +25,7 @@ describe('ErrorGroupOverview -> List', () => { const storeState = {}; const wrapper = mount( - + , storeState ); @@ -36,7 +36,7 @@ describe('ErrorGroupOverview -> List', () => { it('should render with data', () => { const storeState = { location: {} }; const wrapper = mountWithRouterAndStore( - , + , storeState ); diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/List/__test__/__snapshots__/List.test.js.snap b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap similarity index 84% rename from x-pack/plugins/apm/public/components/app/ServiceOverview/List/__test__/__snapshots__/List.test.js.snap rename to x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap index 4661b49df1b08..4fed4bb054679 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/List/__test__/__snapshots__/List.test.js.snap +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap @@ -254,60 +254,7 @@ exports[`ErrorGroupOverview -> List should render empty state 1`] = ` >
-
-
- -
-
-
+ />
@@ -690,60 +637,7 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` >
-
-
- -
-
-
+ />
diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/List/__test__/props.json b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/props.json similarity index 100% rename from x-pack/plugins/apm/public/components/app/ServiceOverview/List/__test__/props.json rename to x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/props.json diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.js b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.js new file mode 100644 index 0000000000000..f218cd8fe7460 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.js @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import styled from 'styled-components'; +import numeral from '@elastic/numeral'; +import { RelativeLink } from '../../../../utils/url'; +import { fontSizes, truncate } from '../../../../style/variables'; +import TooltipOverlay from '../../../shared/TooltipOverlay'; +import { asMillisWithDefault } from '../../../../utils/formatters'; +import { ManagedTable } from '../../../shared/ManagedTable'; + +// TODO: Consolidate these formatting helpers centrally +function formatNumber(value) { + if (value === 0) { + return '0'; + } + const formatted = numeral(value).format('0.0'); + return formatted <= 0.1 ? '< 0.1' : formatted; +} + +function formatString(value) { + return value || 'N/A'; +} + +const AppLink = styled(RelativeLink)` + font-size: ${fontSizes.large}; + ${truncate('100%')}; +`; + +const SERVICE_COLUMNS = [ + { + field: 'serviceName', + name: 'Name', + width: '50%', + sortable: true, + render: serviceName => ( + + + {formatString(serviceName)} + + + ) + }, + { + field: 'agentName', + name: 'Agent', + sortable: true, + render: agentName => formatString(agentName) + }, + { + field: 'avgResponseTime', + name: 'Avg. response time', + sortable: true, + dataType: 'number', + render: value => asMillisWithDefault(value) + }, + { + field: 'transactionsPerMinute', + name: 'Trans. per minute', + sortable: true, + dataType: 'number', + render: value => `${formatNumber(value)} tpm` + }, + { + field: 'errorsPerMinute', + name: 'Errors per minute', + sortable: true, + dataType: 'number', + render: value => `${formatNumber(value)} err.` + } +]; + +export function ServiceList({ items, noItemsMessage }) { + return ( + + ); +} + +ServiceList.propTypes = { + noItemsMessage: PropTypes.node, + items: PropTypes.array +}; + +ServiceList.defaultProps = { + items: [] +}; diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.js b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.js index 120196541c04b..8ef92b829be9c 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.js +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.js @@ -6,7 +6,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import ServiceOverview from '../view'; +import { ServiceOverview } from '../view'; import { STATUS } from '../../../../constants'; import * as apmRestServices from '../../../../services/rest/apm'; diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.js.snap b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.js.snap index 19bf212986f01..8f437ed35630e 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.js.snap +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.js.snap @@ -2,13 +2,9 @@ exports[`Service Overview -> View should render when historical data is found 1`] = `
- -

- Services -

- -
- + @@ -20,7 +16,6 @@ Object { "items": Array [], "noItemsMessage": , } @@ -28,13 +23,9 @@ Object { exports[`Service Overview -> View should render when historical data is not found 1`] = `
- -

- Services -

- -
- + @@ -46,7 +37,6 @@ Object { "items": Array [], "noItemsMessage": - -

Services

- -
- - - + ( - + )} />
); } } - -function SetupInstructionsLink({ buttonFill = false }) { - return ( - - - Setup Instructions - - - ); -} - -export default ServiceOverview; diff --git a/x-pack/plugins/apm/public/components/app/TraceOverview/TraceList.tsx b/x-pack/plugins/apm/public/components/app/TraceOverview/TraceList.tsx new file mode 100644 index 0000000000000..193145daa50da --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/TraceOverview/TraceList.tsx @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import styled from 'styled-components'; +import { fontSizes, truncate } from '../../../style/variables'; +// @ts-ignore +import { asMillisWithDefault } from '../../../utils/formatters'; +// @ts-ignore +import { RelativeLink } from '../../../utils/url'; +import { ImpactBar } from '../../shared/ImpactBar'; +import { ManagedTable } from '../../shared/ManagedTable'; +// @ts-ignore +import TooltipOverlay from '../../shared/TooltipOverlay'; + +function formatString(value: string) { + return value || 'N/A'; +} + +const AppLink = styled(RelativeLink)` + font-size: ${fontSizes.large}; + ${truncate('100%')}; +`; + +interface TraceItem { + name: string; + serviceName: string; + averageResponseTime: number; + tracesPerMinute: number; + impact: number; +} + +const TRACE_COLUMNS = [ + { + field: 'name', + name: 'Name', + width: '40%', + sortable: true, + render: (name: string, { serviceName }: TraceItem) => ( + + + {formatString(name)} + + + ) + }, + { + field: 'serviceName', + name: 'Originating service', + sortable: true, + render: (serviceName: string) => formatString(serviceName) + }, + { + field: 'averageResponseTime', + name: 'Avg. response time', + sortable: true, + dataType: 'number', + render: (value: number) => asMillisWithDefault(value * 1000) + }, + { + field: 'tracesPerMinute', + name: 'Traces per minute', + sortable: true, + dataType: 'number', + render: (value: number) => `${value.toLocaleString()} tpm` + }, + { + field: 'impact', + name: 'Impact', + width: '20%', + align: 'right', + sortable: true, + render: (value: number) => + } +]; + +interface Props { + items: Array>; + noItemsMessage: any; +} + +export function TraceList({ items = [], noItemsMessage }: Props) { + return ( + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx b/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx new file mode 100644 index 0000000000000..2069c39855313 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/TraceOverview/index.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { connect } from 'react-redux'; +// @ts-ignore +import { getUrlParams } from '../../../store/urlParams'; +import { TraceOverview as View } from './view'; + +function mapStateToProps(state = {}) { + return { + urlParams: getUrlParams(state) + }; +} + +export const TraceOverview = connect(mapStateToProps)(View); diff --git a/x-pack/plugins/apm/public/components/app/TraceOverview/view.tsx b/x-pack/plugins/apm/public/components/app/TraceOverview/view.tsx new file mode 100644 index 0000000000000..77628d58cc6f7 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/TraceOverview/view.tsx @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiSpacer } from '@elastic/eui'; +import React from 'react'; +// @ts-ignore +import { TraceListRequest } from '../../../store/reactReduxRequest/traceList'; +import EmptyMessage from '../../shared/EmptyMessage'; +import { TraceList } from './TraceList'; + +interface Props { + urlParams: object; +} + +export function TraceOverview(props: Props) { + const { urlParams } = props; + + return ( +
+ + ( + + } + /> + )} + /> +
+ ); +} diff --git a/x-pack/plugins/apm/public/components/app/TransactionOverview/List/index.js b/x-pack/plugins/apm/public/components/app/TransactionOverview/List/index.js index 97b257275ad63..e6a66c960a7c4 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionOverview/List/index.js +++ b/x-pack/plugins/apm/public/components/app/TransactionOverview/List/index.js @@ -4,11 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; +import React from 'react'; import styled from 'styled-components'; -import { EuiBasicTable } from '@elastic/eui'; -import orderBy from 'lodash.orderby'; import TooltipOverlay from '../../../shared/TooltipOverlay'; import { RelativeLink, legacyEncodeURIComponent } from '../../../../utils/url'; import { @@ -16,9 +13,10 @@ import { asDecimal, tpmUnit } from '../../../../utils/formatters'; +import { ImpactBar } from '../../../shared/ImpactBar'; import { fontFamilyCode, truncate } from '../../../../style/variables'; -import ImpactSparkline from './ImpactSparkLine'; +import { ManagedTable } from '../../../shared/ManagedTable'; function tpmLabel(type) { return type === 'request' ? 'Req. per minute' : 'Trans. per minute'; @@ -28,129 +26,77 @@ function avgLabel(agentName) { return agentName === 'js-base' ? 'Page load time' : 'Avg. resp. time'; } -function paginateItems({ items, pageIndex, pageSize }) { - return items.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize); -} - const TransactionNameLink = styled(RelativeLink)` ${truncate('100%')}; font-family: ${fontFamilyCode}; `; -class List extends Component { - state = { - page: { - index: 0, - size: 25 +function getColumns({ agentName, serviceName, type }) { + return [ + { + field: 'name', + name: 'Name', + width: '50%', + sortable: true, + render: transactionName => { + const transactionUrl = `${serviceName}/transactions/${legacyEncodeURIComponent( + type + )}/${legacyEncodeURIComponent(transactionName)}`; + + return ( + + + {transactionName || 'N/A'} + + + ); + } + }, + { + field: 'avg', + name: avgLabel(agentName), + sortable: true, + dataType: 'number', + render: value => asMillisWithDefault(value) + }, + { + field: 'p95', + name: '95th percentile', + sortable: true, + dataType: 'number', + render: value => asMillisWithDefault(value) }, - sort: { + { + field: 'tpm', + name: tpmLabel(type), + sortable: true, + dataType: 'number', + render: value => `${asDecimal(value)} ${tpmUnit(type)}` + }, + { field: 'impactRelative', - direction: 'desc' + name: 'Impact', + sortable: true, + dataType: 'number', + render: value => } - }; - - onTableChange = ({ page = {}, sort = {} }) => { - this.setState({ page, sort }); - }; - - render() { - const { agentName, serviceName, type } = this.props; - - const columns = [ - { - field: 'name', - name: 'Name', - width: '50%', - sortable: true, - render: transactionName => { - const transactionUrl = `${serviceName}/transactions/${legacyEncodeURIComponent( - type - )}/${legacyEncodeURIComponent(transactionName)}`; - - return ( - - - {transactionName || 'N/A'} - - - ); - } - }, - { - field: 'avg', - name: avgLabel(agentName), - sortable: true, - dataType: 'number', - render: value => asMillisWithDefault(value) - }, - { - field: 'p95', - name: '95th percentile', - sortable: true, - dataType: 'number', - render: value => asMillisWithDefault(value) - }, - { - field: 'tpm', - name: tpmLabel(type), - sortable: true, - dataType: 'number', - render: value => `${asDecimal(value)} ${tpmUnit(type)}` - }, - { - field: 'impactRelative', - name: 'Impact', - sortable: true, - dataType: 'number', - render: value => - } - ]; - - const sortedItems = orderBy( - this.props.items, - this.state.sort.field, - this.state.sort.direction - ); - - const paginatedItems = paginateItems({ - items: sortedItems, - pageIndex: this.state.page.index, - pageSize: this.state.page.size - }); - - return ( - - ); - } + ]; } -List.propTypes = { - agentName: PropTypes.string, - items: PropTypes.array, - serviceName: PropTypes.string, - type: PropTypes.string -}; - -export default List; - -// const renderFooterText = () => { -// return items.length === 500 -// ? 'Showing first 500 results ordered by response time' -// : ''; -// }; +export default function TransactionList({ + items, + agentName, + serviceName, + type, + ...rest +}) { + return ( + + ); +} diff --git a/x-pack/plugins/apm/public/components/shared/EmptyMessage.js b/x-pack/plugins/apm/public/components/shared/EmptyMessage.tsx similarity index 59% rename from x-pack/plugins/apm/public/components/shared/EmptyMessage.js rename to x-pack/plugins/apm/public/components/shared/EmptyMessage.tsx index 45d1b453370cc..274d063555d14 100644 --- a/x-pack/plugins/apm/public/components/shared/EmptyMessage.js +++ b/x-pack/plugins/apm/public/components/shared/EmptyMessage.tsx @@ -4,15 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; -import PropTypes from 'prop-types'; import { EuiEmptyPrompt } from '@elastic/eui'; +import React from 'react'; -function EmptyMessage({ heading, subheading, hideSubheading }) { - if (!subheading) { - subheading = 'Try another time range or reset the search filter.'; - } - +function EmptyMessage({ + heading = 'No data found.', + subheading = 'Try another time range or reset the search filter.', + hideSubheading = false +}) { return ( { + it('should render with default values', () => { + expect(shallow()).toMatchSnapshot(); + }); + + it('should render with overridden values', () => { + expect( + shallow() + ).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/apm/public/components/shared/ImpactBar/__test__/__snapshots__/ImpactBar.test.js.snap b/x-pack/plugins/apm/public/components/shared/ImpactBar/__test__/__snapshots__/ImpactBar.test.js.snap new file mode 100644 index 0000000000000..67378b5634040 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/ImpactBar/__test__/__snapshots__/ImpactBar.test.js.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ImpactBar component should render with default values 1`] = ` + +`; + +exports[`ImpactBar component should render with overridden values 1`] = ` + +`; diff --git a/x-pack/plugins/apm/public/components/shared/ImpactBar/index.tsx b/x-pack/plugins/apm/public/components/shared/ImpactBar/index.tsx new file mode 100644 index 0000000000000..986b6f2b525bc --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/ImpactBar/index.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiProgress } from '@elastic/eui'; +import React from 'react'; + +// TODO: extend from EUI's EuiProgress prop interface +interface Props extends StringMap { + value: number; +} + +export function ImpactBar({ value, ...rest }: Props) { + return ( + + ); +} diff --git a/x-pack/plugins/apm/public/components/shared/KueryBar/index.js b/x-pack/plugins/apm/public/components/shared/KueryBar/index.js index e1a8baf74db7b..f5a621dbb2faa 100644 --- a/x-pack/plugins/apm/public/components/shared/KueryBar/index.js +++ b/x-pack/plugins/apm/public/components/shared/KueryBar/index.js @@ -5,8 +5,8 @@ */ import { connect } from 'react-redux'; -import view from './view'; import { getUrlParams } from '../../../store/urlParams'; +import view from './view'; function mapStateToProps(state = {}) { return { diff --git a/x-pack/plugins/apm/public/components/shared/ManagedTable/__test__/ManagedTable.test.js b/x-pack/plugins/apm/public/components/shared/ManagedTable/__test__/ManagedTable.test.js new file mode 100644 index 0000000000000..a775602cce600 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/ManagedTable/__test__/ManagedTable.test.js @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { shallow } from 'enzyme'; +import React from 'react'; +import { ManagedTable } from '..'; + +describe('ManagedTable component', () => { + let people; + let columns; + + beforeEach(() => { + people = [ + { name: 'Jess', age: 29 }, + { name: 'Becky', age: 43 }, + { name: 'Thomas', age: 31 } + ]; + columns = [ + { + field: 'name', + name: 'Name', + sortable: true, + render: name => `Name: ${name}` + }, + { field: 'age', name: 'Age', render: age => `Age: ${age}` } + ]; + }); + + it('should render a page-full of items, with defaults', () => { + expect( + shallow() + ).toMatchSnapshot(); + }); + + it('should render when specifying initial values', () => { + expect( + shallow( + + ) + ).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/apm/public/components/shared/ManagedTable/__test__/__snapshots__/ManagedTable.test.js.snap b/x-pack/plugins/apm/public/components/shared/ManagedTable/__test__/__snapshots__/ManagedTable.test.js.snap new file mode 100644 index 0000000000000..59679bfe11641 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/ManagedTable/__test__/__snapshots__/ManagedTable.test.js.snap @@ -0,0 +1,103 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ManagedTable component should render a page-full of items, with defaults 1`] = ` + +`; + +exports[`ManagedTable component should render when specifying initial values 1`] = ` + +`; diff --git a/x-pack/plugins/apm/public/components/shared/ManagedTable/index.tsx b/x-pack/plugins/apm/public/components/shared/ManagedTable/index.tsx new file mode 100644 index 0000000000000..b3b03ee9caa3e --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/ManagedTable/index.tsx @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// @ts-ignore +import { EuiBasicTable } from '@elastic/eui'; +import { get, sortByOrder } from 'lodash'; +import React, { Component } from 'react'; + +// TODO: this should really be imported from EUI +interface TableColumn { + field: string; + name: string; + width?: string; + sortable?: boolean; + render: (value: any, item?: any) => any; +} + +interface ManagedTableProps { + items: Array>; + columns: TableColumn[]; + initialPageIndex?: number; + initialPageSize?: number; + hidePerPageOptions?: boolean; + initialSort?: { + field: string; + direction: string; + }; + noItemsMessage?: any; +} + +export class ManagedTable extends Component { + constructor(props: ManagedTableProps) { + super(props); + + const defaultSort = { + field: get(props, 'columns[0].field', ''), + direction: 'asc' + }; + + const { + initialPageIndex = 0, + initialPageSize = 10, + initialSort = defaultSort + } = props; + + this.state = { + page: { index: initialPageIndex, size: initialPageSize }, + sort: initialSort + }; + } + + public onTableChange = ({ page = {}, sort = {} }) => { + this.setState({ page, sort }); + }; + + public getCurrentItems() { + const { items } = this.props; + const { sort = {}, page = {} } = this.state; + // TODO: Use _.orderBy once we upgrade to lodash 4+ + const sorted = sortByOrder(items, sort.field, sort.direction); + return sorted.slice(page.index * page.size, (page.index + 1) * page.size); + } + + public render() { + const { + columns, + noItemsMessage, + items, + hidePerPageOptions = true + } = this.props; + const { page, sort } = this.state; + + return ( + + ); + } +} diff --git a/x-pack/plugins/apm/public/components/shared/SetupInstructionsLink.tsx b/x-pack/plugins/apm/public/components/shared/SetupInstructionsLink.tsx new file mode 100644 index 0000000000000..e57754364fdc8 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/SetupInstructionsLink.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButton } from '@elastic/eui'; +import React from 'react'; +// @ts-ignore +import { KibanaLink } from '../../utils/url'; + +export function SetupInstructionsLink({ + buttonFill = false +}: { + buttonFill?: boolean; +}) { + return ( + + + Setup Instructions + + + ); +} diff --git a/x-pack/plugins/apm/public/store/mockData/mockTraceList.json b/x-pack/plugins/apm/public/store/mockData/mockTraceList.json new file mode 100644 index 0000000000000..4e97a030a2621 --- /dev/null +++ b/x-pack/plugins/apm/public/store/mockData/mockTraceList.json @@ -0,0 +1,30 @@ +[ + { + "name": "log", + "serviceName": "flask-server", + "averageResponseTime": 1329, + "tracesPerMinute": 3201, + "impact": 70 + }, + { + "name": "products/item", + "serviceName": "client", + "averageResponseTime": 2301, + "tracesPerMinute": 5432, + "impact": 42 + }, + { + "name": "billing/payment", + "serviceName": "client", + "averageResponseTime": 789, + "tracesPerMinute": 1201, + "impact": 14 + }, + { + "name": "user/profile", + "serviceName": "client", + "averageResponseTime": 1212, + "tracesPerMinute": 904, + "impact": 92 + } +] diff --git a/x-pack/plugins/apm/public/store/reactReduxRequest/traceList.js b/x-pack/plugins/apm/public/store/reactReduxRequest/traceList.js new file mode 100644 index 0000000000000..847720e963cf7 --- /dev/null +++ b/x-pack/plugins/apm/public/store/reactReduxRequest/traceList.js @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { Request } from 'react-redux-request'; +import { createSelector } from 'reselect'; +import mockTraceList from '../mockData/mockTraceList.json'; +import { createInitialDataSelector } from './helpers'; + +const ID = 'traceList'; +const INITIAL_DATA = []; +const withInitialData = createInitialDataSelector(INITIAL_DATA); + +const selectRRR = (state = {}) => state.reactReduxRequest; + +export const selectTraceList = createSelector( + [selectRRR], + reactReduxRequest => { + return withInitialData(reactReduxRequest[ID]); + } +); + +function loadMockTraces() { + return Promise.resolve(mockTraceList); +} + +export function TraceListRequest({ urlParams = {}, render }) { + const { start, end, kuery } = urlParams; + + if (!start || !end) { + return null; + } + + return ( + + ); +} diff --git a/x-pack/plugins/apm/public/style/global_overrides.css b/x-pack/plugins/apm/public/style/global_overrides.css index 7b032a40914de..a4d51186ab759 100644 --- a/x-pack/plugins/apm/public/style/global_overrides.css +++ b/x-pack/plugins/apm/public/style/global_overrides.css @@ -31,4 +31,11 @@ Hide default dashed gridlines in EUI chart component for all APM graphs .rv-xy-plot__grid-lines__line { stroke-opacity: 1; stroke-dasharray: 1; -} \ No newline at end of file +} + +/* +Override tab size since K6 theme makes "s" and "m" tabs both 14px +*/ +.k6Tab--large .euiTab { + font-size: 16px; +} diff --git a/yarn.lock b/yarn.lock index 4950c0988365c..6f2be98a3360e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -422,6 +422,10 @@ version "3.0.0" resolved "https://registry.yarnpkg.com/@types/has-ansi/-/has-ansi-3.0.0.tgz#636403dc4e0b2649421c4158e5c404416f3f0330" +"@types/history@*": + version "4.7.0" + resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.0.tgz#2fac51050c68f7d6f96c5aafc631132522f4aa3f" + "@types/iron@*": version "5.0.1" resolved "https://registry.yarnpkg.com/@types/iron/-/iron-5.0.1.tgz#5420bbda8623c48ee51b9a78ebad05d7305b4b24" @@ -561,6 +565,21 @@ "@types/react" "*" redux "^4.0.0" +"@types/react-router-dom@^4.3.1": + version "4.3.1" + resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-4.3.1.tgz#71fe2918f8f60474a891520def40a63997dafe04" + dependencies: + "@types/history" "*" + "@types/react" "*" + "@types/react-router" "*" + +"@types/react-router@*": + version "4.0.31" + resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-4.0.31.tgz#416bac49d746800810886c7b8582a622ed9604fc" + dependencies: + "@types/history" "*" + "@types/react" "*" + "@types/react-virtualized@^9.18.7": version "9.18.7" resolved "https://registry.yarnpkg.com/@types/react-virtualized/-/react-virtualized-9.18.7.tgz#8703d8904236819facff90b8b320f29233160c90" @@ -568,7 +587,7 @@ "@types/prop-types" "*" "@types/react" "*" -"@types/react@*", "@types/react@^16.3.14": +"@types/react@*", "@types/react@16.3.14": version "16.3.14" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.3.14.tgz#f90ac6834de172e13ecca430dcb6814744225d36" dependencies: