diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/ListItem.js b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/ListItem.js deleted file mode 100644 index 069823906535..000000000000 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/ListItem.js +++ /dev/null @@ -1,99 +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 from 'react'; -import styled from 'styled-components'; -import { - unit, - px, - fontFamilyCode, - fontSizes, - truncate -} from '../../../../style/variables'; -import { RelativeLink } from '../../../../utils/url'; -import { KuiTableRow, KuiTableRowCell } from '@kbn/ui-framework/components'; -import { RIGHT_ALIGNMENT, EuiBadge } from '@elastic/eui'; -import TooltipOverlay from '../../../shared/TooltipOverlay'; -import numeral from '@elastic/numeral'; -import moment from 'moment'; - -const GroupIdCell = styled(KuiTableRowCell)` - max-width: none; - width: 100px; -`; - -const GroupIdLink = styled(RelativeLink)` - font-family: ${fontFamilyCode}; -`; - -const MessageAndCulpritCell = styled(KuiTableRowCell)` - ${truncate(px(unit * 32))}; -`; - -const MessageLink = styled(RelativeLink)` - font-family: ${fontFamilyCode}; - font-size: ${fontSizes.large}; - ${truncate('100%')}; -`; - -const Culprit = styled.div` - font-family: ${fontFamilyCode}; -`; - -const UnhandledCell = styled(KuiTableRowCell)` - max-width: none; - width: 100px; -`; - -const OccurrenceCell = styled(KuiTableRowCell)` - max-width: none; -`; - -function ListItem({ error, serviceName }) { - const { - groupId, - culprit, - message, - handled, - occurrenceCount, - latestOccurrenceAt - } = error; - - const isUnhandled = handled === false; - const count = occurrenceCount - ? numeral(occurrenceCount).format('0.[0]a') - : 'N/A'; - const timeAgo = latestOccurrenceAt - ? moment(latestOccurrenceAt).fromNow() - : 'N/A'; - - return ( - - - - {groupId.slice(0, 5) || 'N/A'} - - - - - - {message || 'N/A'} - - - - {culprit || 'N/A'} - - - - {isUnhandled && Unhandled} - - {count} - {timeAgo} - - ); -} - -export default ListItem; diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.js.snap b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.js.snap index f7c76ba0072b..91ffea0a3507 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.js.snap +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/__test__/__snapshots__/List.test.js.snap @@ -1,228 +1,281 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ErrorGroupOverview -> List should render empty state 1`] = ` -.c0 { - text-align: center; - font-size: 16px; -} - -.c1 { - text-align: center; - font-size: 14px; - margin-top: 8px; -} - -.c2 { - font-size: 12px; - color: #999999; -} -
-
-
-
-
- -
-
+
- 0 - – - 0 - of - 0 -
-
-
-
-
-
- No errors in the selected time range. -
- - Oops! You should try another time range. If that's no good, there's always the - - documentation. - - -
-
+ + + +
+ + Group ID + +
+ + +
+ + Error message and culprit + +
+ + +
+ +
+ + + + + + + + + + + + +
+ + No items found + +
+ + + +
-
+
-
-
+ className="euiSpacer euiSpacer--m" + />
- 0 - – - 0 - of - 0 -
-
-
+
+ +
`; exports[`ErrorGroupOverview -> List should render with data 1`] = ` -.c8 { - font-size: 12px; - color: #999999; -} - .c0 { - max-width: none; -} - -.c0.kuiTableHeaderCell--alignRight > button > span { - -webkit-box-pack: end; - -webkit-justify-content: flex-end; - -ms-flex-pack: end; - justify-content: flex-end; -} - -.c1 { - max-width: none; - width: 100px; -} - -.c2 { font-family: "SFMono-Regular",Consolas,"Liberation Mono",Menlo,Courier,monospace; } -.c3 { +.c1 { max-width: 512px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.c4 { +.c2 { font-family: "SFMono-Regular",Consolas,"Liberation Mono",Menlo,Courier,monospace; font-size: 16px; max-width: 100%; @@ -231,520 +284,573 @@ exports[`ErrorGroupOverview -> List should render with data 1`] = ` text-overflow: ellipsis; } -.c5 { +.c3 { font-family: "SFMono-Regular",Consolas,"Liberation Mono",Menlo,Courier,monospace; } -.c6 { - max-width: none; - width: 100px; -} - -.c7 { - max-width: none; -} -
-
+
- -
-
-
-
-
- 0 - – - 4 - of - 4 -
-
-
-
- - - - - - - + + - + + - - - - - + + + + + - - - - - - - + + + - - - - - - + + - + + + - - - - - - - + + + + - - - - - -
-
- Group ID -
-
-
- Error message and culprit -
-
-
-
-
- - Occurrences - - - - - Latest occurrence + + +
+ - - -
-
+ +
- - a0ce2 - - - - -
+ + Occurrences + + + + +
- - About to blow up! - + + + Latest occurrence + + + + + + + + + +
- elasticapm.contrib.django.client.capture + + a0ce2 +
- -
-
-
-
- 75 -
-
-
- 1515578797 -
-
-
+
- - f3ac9 - - - -
+ + About to blow up! + +
+ elasticapm.contrib.django.client.capture +
+
+ +
- + +
- AssertionError: - + 75 +
+
- opbeans.views.oopsie + 1515578797
- -
-
-
-
- 75 -
-
-
- 1515578797 -
-
-
+
- - e9086 - - - -
+ f3ac9 + +
+
- - AssertionError: Bad luck! - +
+ + AssertionError: + +
+ opbeans.views.oopsie +
+
+ +
+
+
- opbeans.tasks.update_stats + 75
- -
-
-
-
- 24 -
-
-
- 1515578796 -
-
-
+
- - 8673d - - - -
+
- - Customer with ID 8517 not found - + + e9086 + + +
- opbeans.views.customer +
+ + AssertionError: Bad luck! + +
+ opbeans.tasks.update_stats +
+
- -
-
-
-
- 15 -
-
-
- 1515578773 -
-
-
+ + +
+ + +
+ 24 +
+ + +
+ 1515578796 +
+ + + + + + + +
+
+ + Customer with ID 8517 not found + +
+ opbeans.views.customer +
+
+
+ + +
+ + +
+ 15 +
+ + +
+ 1515578773 +
+ + + + +
+
-
-
+ className="euiSpacer euiSpacer--m" + />
- 0 - – - 4 - of - 4 -
-
-
+
+ +
diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/index.js b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/index.js index af2f87f9c3b7..a49ca9a12d6c 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/index.js +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupOverview/List/index.js @@ -6,131 +6,156 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import { toQuery, fromQuery, history } from '../../../../utils/url'; -import { debounce } from 'lodash'; -import APMTable, { - AlignmentKuiTableHeaderCell -} from '../../../shared/APMTable/APMTable'; -import ListItem from './ListItem'; - -const ITEMS_PER_PAGE = 20; +import { EuiBasicTable, EuiBadge } from '@elastic/eui'; +import numeral from '@elastic/numeral'; +import moment from 'moment'; +import { + toQuery, + fromQuery, + history, + RelativeLink +} from '../../../../utils/url'; +import TooltipOverlay from '../../../shared/TooltipOverlay'; +import styled from 'styled-components'; +import { + unit, + px, + fontFamilyCode, + fontSizes, + truncate +} from '../../../../style/variables'; + +function paginateItems({ items, pageIndex, pageSize }) { + return items.slice(pageIndex * pageSize, (pageIndex + 1) * pageSize); +} + +const GroupIdLink = styled(RelativeLink)` + font-family: ${fontFamilyCode}; +`; + +const MessageAndCulpritCell = styled.div` + ${truncate(px(unit * 32))}; +`; + +const MessageLink = styled(RelativeLink)` + font-family: ${fontFamilyCode}; + font-size: ${fontSizes.large}; + ${truncate('100%')}; +`; + +const Culprit = styled.div` + font-family: ${fontFamilyCode}; +`; + class List extends Component { - updateQuery = getNextQuery => { + state = { + page: { + index: 0, + size: 25 + } + }; + + onTableChange = ({ page = {}, sort = {} }) => { + this.setState({ page }); + const { location } = this.props; - const prevQuery = toQuery(location.search); history.push({ ...location, - search: fromQuery(getNextQuery(prevQuery)) + search: fromQuery({ + ...toQuery(location.search), + sortField: sort.field, + sortDirection: sort.direction + }) }); }; - onClickNext = () => { - const { page } = this.props.urlParams; - this.updateQuery(prevQuery => ({ - ...prevQuery, - page: page + 1 - })); - }; - - onClickPrev = () => { - const { page } = this.props.urlParams; - this.updateQuery(prevQuery => ({ - ...prevQuery, - page: page - 1 - })); - }; - - onFilter = debounce(q => { - this.updateQuery(prevQuery => ({ - ...prevQuery, - page: 0, - q - })); - }, 300); - - onSort = key => { - this.updateQuery(prevQuery => ({ - ...prevQuery, - sortBy: key, - sortOrder: this.props.urlParams.sortOrder === 'asc' ? 'desc' : 'asc' - })); - }; - render() { const { items } = this.props; - const { - sortBy = 'latestOccurrenceAt', - sortOrder = 'desc', - page, - serviceName - } = this.props.urlParams; - - const renderHead = () => { - const cells = [ - { key: 'groupId', sortable: false, label: 'Group ID' }, - { key: 'message', sortable: false, label: 'Error message and culprit' }, - { key: 'handled', sortable: false, label: '', alignRight: true }, - { - key: 'occurrenceCount', - sortable: true, - label: 'Occurrences', - alignRight: true - }, - { - key: 'latestOccurrenceAt', - sortable: true, - label: 'Latest occurrence', - alignRight: true + const { serviceName, sortDirection, sortField } = this.props.urlParams; + + const paginatedItems = paginateItems({ + items, + pageIndex: this.state.page.index, + pageSize: this.state.page.size + }); + + const columns = [ + { + name: 'Group ID', + field: 'groupId', + sortable: false, + width: px(unit * 6), + render: groupId => { + return ( + + {groupId.slice(0, 5) || 'N/A'} + + ); + } + }, + { + name: 'Error message and culprit', + field: 'message', + sortable: false, + width: '50%', + render: (message, item) => { + return ( + + + + {message || 'N/A'} + + + + {item.culprit || 'N/A'} + + + ); } - ].map(({ key, sortable, label, alignRight }) => ( - this.onSort(key), - isSorted: sortBy === key, - isSortAscending: sortOrder === 'asc' - } - : {})} - > - {label} - - )); - - return cells; - }; - - const renderBody = errorGroups => { - return errorGroups.map(error => { - return ( - - ); - }); - }; - - const startNumber = page * ITEMS_PER_PAGE; - const endNumber = (page + 1) * ITEMS_PER_PAGE; - const currentPageItems = items.slice(startNumber, endNumber); + }, + { + name: '', + field: 'handled', + sortable: false, + align: 'right', + render: isUnhandled => + isUnhandled === false && ( + Unhandled + ) + }, + { + name: 'Occurrences', + field: 'occurrenceCount', + sortable: true, + dataType: 'number', + render: value => (value ? numeral(value).format('0.[0]a') : 'N/A') + }, + { + field: 'latestOccurrenceAt', + sortable: true, + name: 'Latest occurrence', + align: 'right', + render: value => (value ? moment(value).fromNow() : 'N/A') + } + ]; 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 4a3ccc717983..55a91a3b3a81 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 @@ -16,7 +16,6 @@ import { asDecimal, tpmUnit } from '../../../../utils/formatters'; -// import ImpactTooltip from './ImpactTooltip'; import { fontFamilyCode, truncate } from '../../../../style/variables'; import ImpactSparkline from './ImpactSparkLine'; @@ -38,7 +37,7 @@ const TransactionNameLink = styled(RelativeLink)` font-family: ${fontFamilyCode}; `; -export class List extends Component { +class List extends Component { state = { page: { index: 0, diff --git a/x-pack/plugins/apm/public/components/shared/APMTable/APMTable.js b/x-pack/plugins/apm/public/components/shared/APMTable/APMTable.js deleted file mode 100644 index 9987d8888121..000000000000 --- a/x-pack/plugins/apm/public/components/shared/APMTable/APMTable.js +++ /dev/null @@ -1,135 +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 from 'react'; -import styled from 'styled-components'; -import PropTypes from 'prop-types'; - -import { - KuiControlledTable, - KuiEmptyTablePromptPanel, - KuiPager, - KuiTable, - KuiTableBody, - KuiTableHeader, - KuiTableHeaderCell, - KuiToolBar, - KuiToolBarFooter, - KuiToolBarFooterSection, - KuiToolBarSearchBox, - KuiToolBarSection -} from '@kbn/ui-framework/components'; - -import EmptyMessage from '../EmptyMessage'; -import { fontSizes, colors } from '../../../style/variables'; - -export const FooterText = styled.div` - font-size: ${fontSizes.small}; - color: ${colors.gray3}; -`; - -export const AlignmentKuiTableHeaderCell = styled(KuiTableHeaderCell)` - max-width: none; - - &.kuiTableHeaderCell--alignRight > button > span { - justify-content: flex-end; - } -`; // Fixes alignment for sortable KuiTableHeaderCell children - -function APMTable({ - defaultSearchQuery, - emptyMessageHeading, - emptyMessageSubHeading, - items, - itemsPerPage, - onClickNext, - onClickPrev, - onFilter, - inputPlaceholder, - page, - renderBody, - renderFooterText, - renderHead, - totalItems -}) { - const startNumber = page * itemsPerPage; - const endNumber = (page + 1) * itemsPerPage; - - const pagination = ( - - 0} - onNextPage={onClickNext} - onPreviousPage={onClickPrev} - /> - - ); - - return ( - - - e.stopPropagation()} - onFilter={onFilter} - placeholder={inputPlaceholder} - /> - {pagination} - - - {items.length === 0 && ( - - - - )} - - {items.length > 0 && ( - - {renderHead()} - {renderBody(items)} - - )} - - - - {renderFooterText()} - - {pagination} - - - ); -} - -APMTable.propTypes = { - defaultSearchQuery: PropTypes.string, - emptyMessageHeading: PropTypes.string, - items: PropTypes.array, - itemsPerPage: PropTypes.number.isRequired, - onClickNext: PropTypes.func.isRequired, - onClickPrev: PropTypes.func.isRequired, - onFilter: PropTypes.func.isRequired, - page: PropTypes.number.isRequired, - renderBody: PropTypes.func.isRequired, - renderFooterText: PropTypes.func, - renderHead: PropTypes.func.isRequired, - totalItems: PropTypes.number.isRequired -}; - -APMTable.defaultProps = { - items: [], - page: 0, - renderFooterText: () => {}, - totalItems: 0 -}; - -export default APMTable; diff --git a/x-pack/plugins/apm/public/services/rest/apm.js b/x-pack/plugins/apm/public/services/rest/apm.js index 6ac23c408443..5681ec139d57 100644 --- a/x-pack/plugins/apm/public/services/rest/apm.js +++ b/x-pack/plugins/apm/public/services/rest/apm.js @@ -165,9 +165,8 @@ export async function loadErrorGroupList({ end, kuery, size, - q, - sortBy, - sortOrder + sortField, + sortDirection }) { return callApi({ pathname: `/api/apm/services/${serviceName}/errors`, @@ -175,9 +174,8 @@ export async function loadErrorGroupList({ start, end, size, - q, - sortBy, - sortOrder, + sortField, + sortDirection, esFilterQuery: await getEncodedEsQuery(kuery) } }); diff --git a/x-pack/plugins/apm/public/store/reactReduxRequest/errorGroupList.js b/x-pack/plugins/apm/public/store/reactReduxRequest/errorGroupList.js index 4ae6dd135de6..3d566940f704 100644 --- a/x-pack/plugins/apm/public/store/reactReduxRequest/errorGroupList.js +++ b/x-pack/plugins/apm/public/store/reactReduxRequest/errorGroupList.js @@ -18,7 +18,14 @@ export function getErrorGroupList(state) { } export function ErrorGroupDetailsRequest({ urlParams, render }) { - const { serviceName, start, end, q, sortBy, sortOrder, kuery } = urlParams; + const { + serviceName, + start, + end, + sortField, + sortDirection, + kuery + } = urlParams; if (!(serviceName && start && end)) { return null; @@ -28,7 +35,7 @@ export function ErrorGroupDetailsRequest({ urlParams, render }) { diff --git a/x-pack/plugins/apm/public/store/urlParams.js b/x-pack/plugins/apm/public/store/urlParams.js index bd3f48bdb931..23a8313f6b90 100644 --- a/x-pack/plugins/apm/public/store/urlParams.js +++ b/x-pack/plugins/apm/public/store/urlParams.js @@ -38,9 +38,8 @@ function urlParams(state = {}, action) { detailTab, spanId, page, - sortBy, - sortOrder, - q, + sortDirection, + sortField, kuery } = toQuery(action.location.search); @@ -48,9 +47,8 @@ function urlParams(state = {}, action) { ...state, // query params - q, - sortBy, - sortOrder, + sortDirection, + sortField, page: toNumber(page) || 0, transactionId, detailTab, diff --git a/x-pack/plugins/apm/server/lib/errors/get_error_groups.js b/x-pack/plugins/apm/server/lib/errors/get_error_groups.js index 28d6afb059bc..c2cfd84daab9 100644 --- a/x-pack/plugins/apm/server/lib/errors/get_error_groups.js +++ b/x-pack/plugins/apm/server/lib/errors/get_error_groups.js @@ -17,9 +17,8 @@ import { get } from 'lodash'; export async function getErrorGroups({ serviceName, - q, - sortBy, - sortOrder = 'desc', + sortField, + sortDirection = 'desc', setup }) { const { start, end, esFilterQuery, client, config } = setup; @@ -50,7 +49,7 @@ export async function getErrorGroups({ terms: { field: ERROR_GROUP_ID, size: 500, - order: { _count: sortOrder } + order: { _count: sortDirection } }, aggs: { sample: { @@ -78,9 +77,9 @@ export async function getErrorGroups({ } // sort buckets by last occurence of error - if (sortBy === 'latestOccurrenceAt') { + if (sortField === 'latestOccurrenceAt') { params.body.aggs.error_groups.terms.order = { - max_timestamp: sortOrder + max_timestamp: sortDirection }; params.body.aggs.error_groups.aggs.max_timestamp = { @@ -88,23 +87,6 @@ export async function getErrorGroups({ }; } - // match query against error fields - if (q) { - params.body.query.bool.must = [ - { - simple_query_string: { - fields: [ - ERROR_EXC_MESSAGE, - ERROR_LOG_MESSAGE, - ERROR_CULPRIT, - ERROR_GROUP_ID - ], - query: q - } - } - ]; - } - const resp = await client('search', params); const hits = get(resp, 'aggregations.error_groups.buckets', []).map( bucket => { diff --git a/x-pack/plugins/apm/server/routes/errors.js b/x-pack/plugins/apm/server/routes/errors.js index 70bc4fb78bb2..8ff6abd41cc5 100644 --- a/x-pack/plugins/apm/server/routes/errors.js +++ b/x-pack/plugins/apm/server/routes/errors.js @@ -28,22 +28,20 @@ export function initErrorsApi(server) { pre, validate: { query: withDefaultValidators({ - q: Joi.string().allow(''), - sortBy: Joi.string(), - sortOrder: Joi.string() + sortField: Joi.string(), + sortDirection: Joi.string() }) } }, handler: (req, reply) => { const { setup } = req.pre; const { serviceName } = req.params; - const { q, sortBy, sortOrder } = req.query; + const { sortField, sortDirection } = req.query; return getErrorGroups({ serviceName, - q, - sortBy, - sortOrder, + sortField, + sortDirection, setup }) .then(reply)