diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.js b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.js index e90ea54069b3d..cd5e32eb1caae 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.js +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.js @@ -6,7 +6,7 @@ import React from 'react'; import Histogram from '../../../shared/charts/Histogram'; -import EmptyMessage from '../../../shared/EmptyMessage'; +import { EmptyMessage } from '../../../shared/EmptyMessage'; import { HeaderSmall } from '../../../shared/UIComponents'; export function getFormattedBuckets(buckets, bucketSize) { diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js index d8ecffa2f18b5..dbeb321a18319 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js @@ -5,16 +5,10 @@ */ import React from 'react'; -import { mount } from 'enzyme'; - -import { MemoryRouter } from 'react-router-dom'; -import { ServiceList } from '../index'; +import { shallow } from 'enzyme'; +import { ServiceList, SERVICE_COLUMNS } from '../index'; import props from './props.json'; -import { - mountWithRouterAndStore, - mockMoment, - toJson -} from '../../../../../utils/testHelpers'; +import { mockMoment } from '../../../../../utils/testHelpers'; describe('ErrorGroupOverview -> List', () => { beforeAll(() => { @@ -22,24 +16,32 @@ describe('ErrorGroupOverview -> List', () => { }); it('should render empty state', () => { - const storeState = {}; - const wrapper = mount( - - - , - storeState - ); - - expect(toJson(wrapper)).toMatchSnapshot(); + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); }); it('should render with data', () => { - const storeState = { location: {} }; - const wrapper = mountWithRouterAndStore( - , - storeState - ); + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); - expect(toJson(wrapper)).toMatchSnapshot(); + it('should render columns correctly', () => { + const service = { + serviceName: 'opbeans-python', + agentName: 'python', + transactionsPerMinute: 86.93333333333334, + errorsPerMinute: 12.6, + avgResponseTime: 91535.42944785276 + }; + const renderedColumns = SERVICE_COLUMNS.map(c => + c.render(service[c.field], service) + ); + expect(renderedColumns[0]).toMatchSnapshot(); + expect(renderedColumns.slice(1)).toEqual([ + 'python', + '92 ms', + '86.9 tpm', + '12.6 err.' + ]); }); }); diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap index 4fed4bb054679..35b87eaf6eff6 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap @@ -1,649 +1,133 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ErrorGroupOverview -> List should render empty state 1`] = ` -
List should render columns correctly 1`] = ` + -
-
-
-
-
- -
-
-
-
- - - - - - - - - - - - - - - - -
- Below is a table of - 0 - items. -
- - - - - - - - - -
-
- - No items found - -
-
-
-
-
-
-
-
- -
-
-
-
+ + opbeans-python + + `; -exports[`ErrorGroupOverview -> List should render with data 1`] = ` -.c0 { - font-size: 16px; - max-width: 100%; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} +exports[`ErrorGroupOverview -> List should render empty state 1`] = ` + +`; -
-
-
-
-
-
- -
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Below is a table of - 2 - items. -
- - - - - - - - - -
- - -
- nodejs -
-
-
- N/A -
-
-
- 0 tpm -
-
-
- 46.1 err. -
-
- - -
- python -
-
-
- 92 ms -
-
-
- 86.9 tpm -
-
-
- 12.6 err. -
-
-
-
-
-
-
-
- -
-
-
-
+exports[`ErrorGroupOverview -> List should render with data 1`] = ` + `; 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.tsx similarity index 65% rename from x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.js rename to x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx index 76317a51fa6f2..5ac5530540466 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.js +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx @@ -4,16 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiToolTip } from '@elastic/eui'; import React from 'react'; -import PropTypes from 'prop-types'; import styled from 'styled-components'; -import { RelativeLink } from '../../../../utils/url'; +import { IServiceListItem } from 'x-pack/plugins/apm/server/lib/services/get_services'; import { fontSizes, truncate } from '../../../../style/variables'; -import TooltipOverlay from '../../../shared/TooltipOverlay'; -import { asMillis, asDecimal } from '../../../../utils/formatters'; +import { asDecimal, asMillis } from '../../../../utils/formatters'; +import { RelativeLink } from '../../../../utils/url'; import { ManagedTable } from '../../../shared/ManagedTable'; -function formatNumber(value) { +interface Props { + items: IServiceListItem[]; + noItemsMessage?: React.ReactNode; +} + +function formatNumber(value: number) { if (value === 0) { return '0'; } else if (value <= 0.1) { @@ -23,7 +28,7 @@ function formatNumber(value) { } } -function formatString(value) { +function formatString(value?: string | null) { return value || 'N/A'; } @@ -32,50 +37,50 @@ const AppLink = styled(RelativeLink)` ${truncate('100%')}; `; -const SERVICE_COLUMNS = [ +export const SERVICE_COLUMNS = [ { field: 'serviceName', name: 'Name', width: '50%', sortable: true, - render: serviceName => ( - + render: (serviceName: string) => ( + {formatString(serviceName)} - + ) }, { field: 'agentName', name: 'Agent', sortable: true, - render: agentName => formatString(agentName) + render: (agentName: string) => formatString(agentName) }, { field: 'avgResponseTime', name: 'Avg. response time', sortable: true, dataType: 'number', - render: value => asMillis(value) + render: (value: number) => asMillis(value) }, { field: 'transactionsPerMinute', name: 'Trans. per minute', sortable: true, dataType: 'number', - render: value => `${formatNumber(value)} tpm` + render: (value: number) => `${formatNumber(value)} tpm` }, { field: 'errorsPerMinute', name: 'Errors per minute', sortable: true, dataType: 'number', - render: value => `${formatNumber(value)} err.` + render: (value: number) => `${formatNumber(value)} err.` } ]; -export function ServiceList({ items, noItemsMessage }) { +export function ServiceList({ items = [], noItemsMessage }: Props) { 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 8ef92b829be9c..2f93eb0b2f1f0 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 @@ -13,11 +13,21 @@ import * as apmRestServices from '../../../../services/rest/apm'; jest.mock('../../../../services/rest/apm'); describe('Service Overview -> View', () => { + let mockAgentStatus; let wrapper; let instance; beforeEach(() => { - wrapper = shallow(); + mockAgentStatus = { + dataFound: true + }; + + // eslint-disable-next-line import/namespace + apmRestServices.loadAgentStatus = jest.fn(() => + Promise.resolve(mockAgentStatus) + ); + + wrapper = shallow(); instance = wrapper.instance(); }); @@ -40,53 +50,10 @@ describe('Service Overview -> View', () => { expect(List.props).toMatchSnapshot(); }); - describe('checking for historical data', () => { - let mockAgentStatus; - - beforeEach(() => { - mockAgentStatus = { - dataFound: true - }; - // eslint-disable-next-line import/namespace - apmRestServices.loadAgentStatus = jest.fn(() => - Promise.resolve(mockAgentStatus) - ); - }); - - it('should happen if service list status is success and data is empty', async () => { - const props = { - serviceList: { - status: STATUS.SUCCESS, - data: [] - } - }; - await instance.checkForHistoricalData(props); - expect(apmRestServices.loadAgentStatus).toHaveBeenCalledTimes(1); - }); - - it('should not happen if sevice list status is not success', async () => { - const props = { - serviceList: { - status: STATUS.FAILURE, - data: [] - } - }; - await instance.checkForHistoricalData(props); - expect(apmRestServices.loadAgentStatus).not.toHaveBeenCalled(); - }); - - it('should not happen if service list data is not empty', async () => { - const props = { - serviceList: { - status: STATUS.SUCCESS, - data: [1, 2, 3] - } - }; - await instance.checkForHistoricalData(props); - expect(apmRestServices.loadAgentStatus).not.toHaveBeenCalled(); - }); + it('should check for historical data once', () => {}); - it('should leave historical data state as true if data is found', async () => { + describe('checking for historical data', () => { + it('should set historical data to true if data is found', async () => { const props = { serviceList: { status: STATUS.SUCCESS, diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/view.js b/x-pack/plugins/apm/public/components/app/ServiceOverview/view.tsx similarity index 51% rename from x-pack/plugins/apm/public/components/app/ServiceOverview/view.js rename to x-pack/plugins/apm/public/components/app/ServiceOverview/view.tsx index 9e0759bb735a1..2bd9373b94c3c 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/view.js +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/view.tsx @@ -4,40 +4,39 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiSpacer } from '@elastic/eui'; import React, { Component } from 'react'; -import { STATUS } from '../../../constants'; -import { isEmpty } from 'lodash'; +import { RRRRenderResponse } from 'react-redux-request'; +import { IUrlParams } from 'x-pack/plugins/apm/public/store/urlParams'; +import { IServiceListItem } from 'x-pack/plugins/apm/server/lib/services/get_services'; import { loadAgentStatus } from '../../../services/rest/apm'; -import { ServiceList } from './ServiceList'; -import { EuiSpacer } from '@elastic/eui'; import { ServiceListRequest } from '../../../store/reactReduxRequest/serviceList'; -import EmptyMessage from '../../shared/EmptyMessage'; +import { EmptyMessage } from '../../shared/EmptyMessage'; import { SetupInstructionsLink } from '../../shared/SetupInstructionsLink'; +import { ServiceList } from './ServiceList'; -export class ServiceOverview extends Component { - state = { - historicalDataFound: true - }; +interface Props { + urlParams: IUrlParams; + serviceList: RRRRenderResponse; +} - async checkForHistoricalData({ serviceList }) { - if (serviceList.status === STATUS.SUCCESS && isEmpty(serviceList.data)) { - const result = await loadAgentStatus(); - if (!result.dataFound) { - this.setState({ historicalDataFound: false }); - } - } - } +interface State { + historicalDataFound: boolean; +} + +export class ServiceOverview extends Component { + public state = { historicalDataFound: true }; - componentDidMount() { - this.checkForHistoricalData(this.props); + public async checkForHistoricalData() { + const result = await loadAgentStatus(); + this.setState({ historicalDataFound: result.dataFound }); } - componentDidUpdate() { - // QUESTION: Do we want to check on ANY update, or only if serviceList status/data have changed? - this.checkForHistoricalData(this.props); + public componentDidMount() { + this.checkForHistoricalData(); } - render() { + public render() { const { urlParams } = this.props; const { historicalDataFound } = this.state; @@ -54,13 +53,19 @@ export class ServiceOverview extends Component { /> ); + // Render method here uses this.props.serviceList instead of received "data" from RRR + // to make it easier to test -- mapStateToProps uses the RRR selector so the data + // is the same either way return (
( - + render={() => ( + )} />
diff --git a/x-pack/plugins/apm/public/components/app/TraceOverview/view.tsx b/x-pack/plugins/apm/public/components/app/TraceOverview/view.tsx index 3d14db033bd41..b0c8cba467055 100644 --- a/x-pack/plugins/apm/public/components/app/TraceOverview/view.tsx +++ b/x-pack/plugins/apm/public/components/app/TraceOverview/view.tsx @@ -10,7 +10,7 @@ import { RRRRenderResponse } from 'react-redux-request'; import { ITransactionGroup } from '../../../../typings/TransactionGroup'; // @ts-ignore import { TraceListRequest } from '../../../store/reactReduxRequest/traceList'; -import EmptyMessage from '../../shared/EmptyMessage'; +import { EmptyMessage } from '../../shared/EmptyMessage'; import { TraceList } from './TraceList'; interface Props { diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx index f7321df130660..90439a4194179 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/Distribution/index.tsx @@ -14,7 +14,7 @@ import { getTimeFormatter, timeUnit } from '../../../../utils/formatters'; import { fromQuery, history, toQuery } from '../../../../utils/url'; // @ts-ignore import Histogram from '../../../shared/charts/Histogram'; -import EmptyMessage from '../../../shared/EmptyMessage'; +import { EmptyMessage } from '../../../shared/EmptyMessage'; interface IChartPoint { sample?: IBucket['sample']; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/view.tsx b/x-pack/plugins/apm/public/components/app/TransactionDetails/view.tsx index 97dcc67da090b..2f7c5a64a37cd 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/view.tsx +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/view.tsx @@ -15,7 +15,7 @@ import { WaterfallRequest } from '../../../store/reactReduxRequest/waterfall'; import { IUrlParams } from '../../../store/urlParams'; // @ts-ignore import TransactionCharts from '../../shared/charts/TransactionCharts'; -import EmptyMessage from '../../shared/EmptyMessage'; +import { EmptyMessage } from '../../shared/EmptyMessage'; // @ts-ignore import { KueryBar } from '../../shared/KueryBar'; // @ts-ignore diff --git a/x-pack/plugins/apm/public/components/shared/EmptyMessage.tsx b/x-pack/plugins/apm/public/components/shared/EmptyMessage.tsx index 274d063555d14..33a0c012c16b8 100644 --- a/x-pack/plugins/apm/public/components/shared/EmptyMessage.tsx +++ b/x-pack/plugins/apm/public/components/shared/EmptyMessage.tsx @@ -4,14 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiEmptyPrompt } from '@elastic/eui'; +import { EuiEmptyPrompt, EuiEmptyPromptProps } from '@elastic/eui'; import React from 'react'; -function EmptyMessage({ +interface Props { + heading?: string; + subheading?: EuiEmptyPromptProps['body']; + hideSubheading?: boolean; +} + +const EmptyMessage: React.SFC = ({ heading = 'No data found.', subheading = 'Try another time range or reset the search filter.', hideSubheading = false -}) { +}) => { return ( ); -} +}; -// tslint:disable-next-line:no-default-export -export default EmptyMessage; +export { EmptyMessage }; diff --git a/x-pack/plugins/apm/public/components/shared/Stacktrace/index.js b/x-pack/plugins/apm/public/components/shared/Stacktrace/index.js index 0bec4149914d4..b7a7f5f567f7b 100644 --- a/x-pack/plugins/apm/public/components/shared/Stacktrace/index.js +++ b/x-pack/plugins/apm/public/components/shared/Stacktrace/index.js @@ -10,7 +10,7 @@ import { isEmpty, get } from 'lodash'; import CodePreview from '../../shared/CodePreview'; import { Ellipsis } from '../../shared/Icons'; import { units, px } from '../../../style/variables'; -import EmptyMessage from '../../shared/EmptyMessage'; +import { EmptyMessage } from '../../shared/EmptyMessage'; import { EuiLink, EuiTitle } from '@elastic/eui'; const LibraryFrameToggle = styled.div` diff --git a/x-pack/plugins/apm/public/services/rest/apm.ts b/x-pack/plugins/apm/public/services/rest/apm.ts index 3a390492f8c72..d065be4fa28a2 100644 --- a/x-pack/plugins/apm/public/services/rest/apm.ts +++ b/x-pack/plugins/apm/public/services/rest/apm.ts @@ -7,7 +7,7 @@ // @ts-ignore import { camelizeKeys } from 'humps'; import { ServiceResponse } from 'x-pack/plugins/apm/server/lib/services/get_service'; -import { ServiceListItemResponse } from 'x-pack/plugins/apm/server/lib/services/get_services'; +import { IServiceListItem } from 'x-pack/plugins/apm/server/lib/services/get_services'; import { IDistributionResponse } from 'x-pack/plugins/apm/server/lib/transactions/distribution/get_distribution'; import { Span } from 'x-pack/plugins/apm/typings/Span'; import { Transaction } from 'x-pack/plugins/apm/typings/Transaction'; @@ -58,7 +58,7 @@ export async function loadServiceList({ start, end, kuery -}: IUrlParams): Promise { +}: IUrlParams): Promise { return callApi({ pathname: `/api/apm/services`, query: { diff --git a/x-pack/plugins/apm/public/store/reactReduxRequest/serviceList.js b/x-pack/plugins/apm/public/store/reactReduxRequest/serviceList.tsx similarity index 59% rename from x-pack/plugins/apm/public/store/reactReduxRequest/serviceList.js rename to x-pack/plugins/apm/public/store/reactReduxRequest/serviceList.tsx index 10175d3c507bb..ef5e59649b767 100644 --- a/x-pack/plugins/apm/public/store/reactReduxRequest/serviceList.js +++ b/x-pack/plugins/apm/public/store/reactReduxRequest/serviceList.tsx @@ -5,19 +5,31 @@ */ import React from 'react'; +import { Request, RRRRender, RRRRenderResponse } from 'react-redux-request'; +import { IServiceListItem } from 'x-pack/plugins/apm/server/lib/services/get_services'; import { loadServiceList } from '../../services/rest/apm'; -import { Request } from 'react-redux-request'; +import { IReduxState } from '../rootReducer'; +import { IUrlParams } from '../urlParams'; +// @ts-ignore import { createInitialDataSelector } from './helpers'; const ID = 'serviceList'; -const INITIAL_DATA = []; +const INITIAL_DATA: IServiceListItem[] = []; const withInitialData = createInitialDataSelector(INITIAL_DATA); -export function getServiceList(state) { +export function getServiceList( + state: IReduxState +): RRRRenderResponse { return withInitialData(state.reactReduxRequest[ID]); } -export function ServiceListRequest({ urlParams, render }) { +export function ServiceListRequest({ + urlParams, + render +}: { + urlParams: IUrlParams; + render: RRRRender; +}) { const { start, end, kuery } = urlParams; if (!(start && end)) { diff --git a/x-pack/plugins/apm/server/lib/services/get_services.ts b/x-pack/plugins/apm/server/lib/services/get_services.ts index eeb39c256e8a3..6d2019e64c1db 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services.ts @@ -14,17 +14,15 @@ import { } from '../../../common/constants'; import { Setup } from '../helpers/setup_request'; -export interface ServiceListItemResponse { - service_name: string; - agent_name: string | undefined; - transactions_per_minute: number; - errors_per_minute: number; - avg_response_time: number; +export interface IServiceListItem { + serviceName: string; + agentName: string | undefined; + transactionsPerMinute: number; + errorsPerMinute: number; + avgResponseTime: number; } -export async function getServices( - setup: Setup -): Promise { +export async function getServices(setup: Setup): Promise { const { start, end, esFilterQuery, client, config } = setup; const params = { @@ -118,11 +116,11 @@ export async function getServices( const errorsPerMinute = totalErrors / deltaAsMinutes; return { - service_name: bucket.key, - agent_name: oc(bucket).agents.buckets[0].key(), - transactions_per_minute: transactionsPerMinute, - errors_per_minute: errorsPerMinute, - avg_response_time: bucket.avg.value + serviceName: bucket.key, + agentName: oc(bucket).agents.buckets[0].key(), + transactionsPerMinute, + errorsPerMinute, + avgResponseTime: bucket.avg.value }; }); }