mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
* Add Kuery to APM UI * Format bool queries * Fix test * Add min-height * Updated css to vars * Added loading indicators * Update breadcrumbs * Update Discover links * Add search button * Fix readcrumb test * Remove debounce * Fix “undefined” kuery * Handle missing index pattern * Fix race condition in data fetching * Fix legacy url encoding * Filter out field-suggestions starting with "span*" * Convert KuiTable to EuiTable for service overview * Convert KuiTable to EuiTable for transaction overview * Convert KuiTable to EuiTable for error overview * Updated empty state messages * Bump CI jo * updated snapshots * Add beta tooltip and update to EuiSearch * Fixed issue with focus * Submit when clicking search button * Submit when clearing input * Handle missing index pattern * Remove query from spans # Conflicts: # yarn.lock
This commit is contained in:
parent
79a8e478cb
commit
64768253d4
53 changed files with 2632 additions and 2241 deletions
|
@ -28,6 +28,10 @@ export function isFilterable(field) {
|
|||
}
|
||||
|
||||
export function getFromSavedObject(savedObject) {
|
||||
if (!savedObject) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
fields: JSON.parse(savedObject.attributes.fields),
|
||||
title: savedObject.attributes.title,
|
||||
|
|
|
@ -132,6 +132,7 @@
|
|||
"pivotal-ui": "13.0.1",
|
||||
"pluralize": "3.1.0",
|
||||
"pngjs": "3.3.1",
|
||||
"polished": "^1.9.2",
|
||||
"prop-types": "^15.6.0",
|
||||
"puid": "1.0.5",
|
||||
"react": "^16.3.0",
|
||||
|
|
|
@ -130,7 +130,9 @@ function DetailView({ errorGroup, urlParams, location }) {
|
|||
interval: 'auto',
|
||||
query: {
|
||||
language: 'lucene',
|
||||
query: `${SERVICE_NAME}:"${serviceName}" AND ${ERROR_GROUP_ID}:${groupId}`
|
||||
query: `${SERVICE_NAME}:"${serviceName}" AND ${ERROR_GROUP_ID}:${groupId}${
|
||||
urlParams.kuery ? ` AND ${urlParams.kuery}` : ``
|
||||
}`
|
||||
},
|
||||
sort: { '@timestamp': 'desc' }
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import { get } from 'lodash';
|
|||
import { HeaderLarge } from '../../shared/UIComponents';
|
||||
import DetailView from './DetailView';
|
||||
import Distribution from './Distribution';
|
||||
import { KueryBar } from '../../shared/KueryBar';
|
||||
|
||||
import { EuiText, EuiBadge } from '@elastic/eui';
|
||||
import {
|
||||
|
@ -86,6 +87,8 @@ function ErrorGroupDetails({ urlParams, location }) {
|
|||
)}
|
||||
</HeaderLarge>
|
||||
|
||||
<KueryBar />
|
||||
|
||||
{showDetails && (
|
||||
<Titles>
|
||||
<EuiText>
|
||||
|
|
|
@ -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 (
|
||||
<KuiTableRow>
|
||||
<GroupIdCell>
|
||||
<GroupIdLink path={`/${serviceName}/errors/${groupId}`}>
|
||||
{groupId.slice(0, 5) || 'N/A'}
|
||||
</GroupIdLink>
|
||||
</GroupIdCell>
|
||||
<MessageAndCulpritCell>
|
||||
<TooltipOverlay content={message || 'N/A'}>
|
||||
<MessageLink path={`/${serviceName}/errors/${groupId}`}>
|
||||
{message || 'N/A'}
|
||||
</MessageLink>
|
||||
</TooltipOverlay>
|
||||
<TooltipOverlay content={culprit || 'N/A'}>
|
||||
<Culprit>{culprit || 'N/A'}</Culprit>
|
||||
</TooltipOverlay>
|
||||
</MessageAndCulpritCell>
|
||||
<UnhandledCell align={RIGHT_ALIGNMENT}>
|
||||
{isUnhandled && <EuiBadge color="warning">Unhandled</EuiBadge>}
|
||||
</UnhandledCell>
|
||||
<OccurrenceCell align={RIGHT_ALIGNMENT}>{count}</OccurrenceCell>
|
||||
<OccurrenceCell align={RIGHT_ALIGNMENT}>{timeAgo}</OccurrenceCell>
|
||||
</KuiTableRow>
|
||||
);
|
||||
}
|
||||
|
||||
export default ListItem;
|
File diff suppressed because it is too large
Load diff
|
@ -6,131 +6,157 @@
|
|||
|
||||
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';
|
||||
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};
|
||||
`;
|
||||
|
||||
const ITEMS_PER_PAGE = 20;
|
||||
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 { serviceName, sortDirection, sortField } = 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 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 (
|
||||
<GroupIdLink path={`/${serviceName}/errors/${groupId}`}>
|
||||
{groupId.slice(0, 5) || 'N/A'}
|
||||
</GroupIdLink>
|
||||
);
|
||||
}
|
||||
].map(({ key, sortable, label, alignRight }) => (
|
||||
<AlignmentKuiTableHeaderCell
|
||||
key={key}
|
||||
className={alignRight ? 'kuiTableHeaderCell--alignRight' : ''}
|
||||
{...(sortable
|
||||
? {
|
||||
onSort: () => this.onSort(key),
|
||||
isSorted: sortBy === key,
|
||||
isSortAscending: sortOrder === 'asc'
|
||||
}
|
||||
: {})}
|
||||
>
|
||||
{label}
|
||||
</AlignmentKuiTableHeaderCell>
|
||||
));
|
||||
|
||||
return cells;
|
||||
};
|
||||
|
||||
const renderBody = errorGroups => {
|
||||
return errorGroups.map(error => {
|
||||
return (
|
||||
<ListItem
|
||||
key={error.groupId}
|
||||
serviceName={serviceName}
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const startNumber = page * ITEMS_PER_PAGE;
|
||||
const endNumber = (page + 1) * ITEMS_PER_PAGE;
|
||||
const currentPageItems = items.slice(startNumber, endNumber);
|
||||
},
|
||||
{
|
||||
name: 'Error message and culprit',
|
||||
field: 'message',
|
||||
sortable: false,
|
||||
width: '50%',
|
||||
render: (message, item) => {
|
||||
return (
|
||||
<MessageAndCulpritCell>
|
||||
<TooltipOverlay content={message || 'N/A'}>
|
||||
<MessageLink path={`/${serviceName}/errors/${item.groupId}`}>
|
||||
{message || 'N/A'}
|
||||
</MessageLink>
|
||||
</TooltipOverlay>
|
||||
<TooltipOverlay content={item.culprit || 'N/A'}>
|
||||
<Culprit>{item.culprit || 'N/A'}</Culprit>
|
||||
</TooltipOverlay>
|
||||
</MessageAndCulpritCell>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '',
|
||||
field: 'handled',
|
||||
sortable: false,
|
||||
align: 'right',
|
||||
render: isUnhandled =>
|
||||
isUnhandled === false && (
|
||||
<EuiBadge color="warning">Unhandled</EuiBadge>
|
||||
)
|
||||
},
|
||||
{
|
||||
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 (
|
||||
<APMTable
|
||||
defaultSearchQuery={this.props.urlParams.q}
|
||||
emptyMessageHeading="No errors in the selected time range."
|
||||
items={currentPageItems}
|
||||
itemsPerPage={ITEMS_PER_PAGE}
|
||||
onClickNext={this.onClickNext}
|
||||
onClickPrev={this.onClickPrev}
|
||||
onFilter={this.onFilter}
|
||||
page={page}
|
||||
renderBody={renderBody}
|
||||
renderHead={renderHead}
|
||||
totalItems={items.length}
|
||||
<EuiBasicTable
|
||||
noItemsMessage="No errors were found"
|
||||
items={paginatedItems}
|
||||
columns={columns}
|
||||
pagination={{
|
||||
pageIndex: this.state.page.index,
|
||||
pageSize: this.state.page.size,
|
||||
totalItemCount: this.props.items.length
|
||||
}}
|
||||
sorting={{
|
||||
sort: {
|
||||
field: sortField || 'latestOccurrenceAt',
|
||||
direction: sortDirection || 'desc'
|
||||
}
|
||||
}}
|
||||
onChange={this.onTableChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import List from './List';
|
|||
import WatcherFlyout from './Watcher/WatcherFlyOut';
|
||||
import WatcherButton from './Watcher/WatcherButton';
|
||||
import { ErrorGroupDetailsRequest } from '../../../store/reactReduxRequest/errorGroupList';
|
||||
import { KueryBar } from '../../shared/KueryBar';
|
||||
|
||||
class ErrorGroupOverview extends Component {
|
||||
state = {
|
||||
|
@ -39,6 +40,8 @@ class ErrorGroupOverview extends Component {
|
|||
)}
|
||||
</HeaderContainer>
|
||||
|
||||
<KueryBar />
|
||||
|
||||
<TabNavigation />
|
||||
|
||||
<ErrorGroupDetailsRequest
|
||||
|
|
|
@ -15,7 +15,7 @@ class Breadcrumbs extends React.Component {
|
|||
|
||||
render() {
|
||||
const { breadcrumbs, location } = this.props;
|
||||
const _g = toQuery(location.search)._g;
|
||||
const { _g = '', kuery = '' } = toQuery(location.search);
|
||||
|
||||
return (
|
||||
<div className="kuiLocalBreadcrumbs">
|
||||
|
@ -38,7 +38,9 @@ class Breadcrumbs extends React.Component {
|
|||
{breadcrumb}
|
||||
</span>
|
||||
) : (
|
||||
<a href={`#${match.url}?_g=${_g}`}>{breadcrumb}</a>
|
||||
<a href={`#${match.url}?_g=${_g}&kuery=${kuery}`}>
|
||||
{breadcrumb}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -13,7 +13,7 @@ import { toJson } from '../../../../utils/testHelpers';
|
|||
|
||||
function expectBreadcrumbToMatchSnapshot(route) {
|
||||
const wrapper = mount(
|
||||
<MemoryRouter initialEntries={[`${route}?_g=`]}>
|
||||
<MemoryRouter initialEntries={[`${route}?_g=myG&kuery=myKuery`]}>
|
||||
<Breadcrumbs />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
exports[`Breadcrumbs /:serviceName 1`] = `
|
||||
Array [
|
||||
<a
|
||||
href="#/?_g="
|
||||
href="#/?_g=myG&kuery=myKuery"
|
||||
>
|
||||
APM
|
||||
</a>,
|
||||
|
@ -16,12 +16,12 @@ Array [
|
|||
exports[`Breadcrumbs /:serviceName/errors 1`] = `
|
||||
Array [
|
||||
<a
|
||||
href="#/?_g="
|
||||
href="#/?_g=myG&kuery=myKuery"
|
||||
>
|
||||
APM
|
||||
</a>,
|
||||
<a
|
||||
href="#/opbeans-node?_g="
|
||||
href="#/opbeans-node?_g=myG&kuery=myKuery"
|
||||
>
|
||||
opbeans-node
|
||||
</a>,
|
||||
|
@ -34,17 +34,17 @@ Array [
|
|||
exports[`Breadcrumbs /:serviceName/errors/:groupId 1`] = `
|
||||
Array [
|
||||
<a
|
||||
href="#/?_g="
|
||||
href="#/?_g=myG&kuery=myKuery"
|
||||
>
|
||||
APM
|
||||
</a>,
|
||||
<a
|
||||
href="#/opbeans-node?_g="
|
||||
href="#/opbeans-node?_g=myG&kuery=myKuery"
|
||||
>
|
||||
opbeans-node
|
||||
</a>,
|
||||
<a
|
||||
href="#/opbeans-node/errors?_g="
|
||||
href="#/opbeans-node/errors?_g=myG&kuery=myKuery"
|
||||
>
|
||||
Errors
|
||||
</a>,
|
||||
|
@ -57,12 +57,12 @@ Array [
|
|||
exports[`Breadcrumbs /:serviceName/transactions 1`] = `
|
||||
Array [
|
||||
<a
|
||||
href="#/?_g="
|
||||
href="#/?_g=myG&kuery=myKuery"
|
||||
>
|
||||
APM
|
||||
</a>,
|
||||
<a
|
||||
href="#/opbeans-node?_g="
|
||||
href="#/opbeans-node?_g=myG&kuery=myKuery"
|
||||
>
|
||||
opbeans-node
|
||||
</a>,
|
||||
|
@ -75,17 +75,17 @@ Array [
|
|||
exports[`Breadcrumbs /:serviceName/transactions/:transactionType 1`] = `
|
||||
Array [
|
||||
<a
|
||||
href="#/?_g="
|
||||
href="#/?_g=myG&kuery=myKuery"
|
||||
>
|
||||
APM
|
||||
</a>,
|
||||
<a
|
||||
href="#/opbeans-node?_g="
|
||||
href="#/opbeans-node?_g=myG&kuery=myKuery"
|
||||
>
|
||||
opbeans-node
|
||||
</a>,
|
||||
<a
|
||||
href="#/opbeans-node/transactions?_g="
|
||||
href="#/opbeans-node/transactions?_g=myG&kuery=myKuery"
|
||||
>
|
||||
Transactions
|
||||
</a>,
|
||||
|
@ -98,22 +98,22 @@ Array [
|
|||
exports[`Breadcrumbs /:serviceName/transactions/:transactionType/:transactionName 1`] = `
|
||||
Array [
|
||||
<a
|
||||
href="#/?_g="
|
||||
href="#/?_g=myG&kuery=myKuery"
|
||||
>
|
||||
APM
|
||||
</a>,
|
||||
<a
|
||||
href="#/:serviceName?_g="
|
||||
href="#/:serviceName?_g=myG&kuery=myKuery"
|
||||
>
|
||||
:serviceName
|
||||
</a>,
|
||||
<a
|
||||
href="#/:serviceName/transactions?_g="
|
||||
href="#/:serviceName/transactions?_g=myG&kuery=myKuery"
|
||||
>
|
||||
Transactions
|
||||
</a>,
|
||||
<a
|
||||
href="#/:serviceName/transactions/request?_g="
|
||||
href="#/:serviceName/transactions/request?_g=myG&kuery=myKuery"
|
||||
>
|
||||
request
|
||||
</a>,
|
||||
|
|
|
@ -9,12 +9,13 @@ import styled from 'styled-components';
|
|||
import { Route, Switch } from 'react-router-dom';
|
||||
import { routes } from './routeConfig';
|
||||
import ScrollToTopOnPathChange from './ScrollToTopOnPathChange';
|
||||
import { px, units, unit } from '../../../style/variables';
|
||||
import { px, units, unit, elements } from '../../../style/variables';
|
||||
import ConnectRouterToRedux from '../../shared/ConnectRouterToRedux';
|
||||
|
||||
const MainContainer = styled.div`
|
||||
min-width: ${px(unit * 50)};
|
||||
padding: ${px(units.plus)};
|
||||
min-height: calc(100vh - ${elements.topNav});
|
||||
`;
|
||||
|
||||
export default function Main() {
|
||||
|
|
|
@ -1,74 +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 { RelativeLink } from '../../../../utils/url';
|
||||
import { KuiTableRow, KuiTableRowCell } from '@kbn/ui-framework/components';
|
||||
import { fontSizes, truncate } from '../../../../style/variables';
|
||||
import TooltipOverlay from '../../../shared/TooltipOverlay';
|
||||
import { RIGHT_ALIGNMENT } from '@elastic/eui';
|
||||
import { asMillisWithDefault } from '../../../../utils/formatters';
|
||||
import numeral from '@elastic/numeral';
|
||||
|
||||
const ServiceNameCell = styled(KuiTableRowCell)`
|
||||
width: 40%;
|
||||
`;
|
||||
|
||||
const ServiceCell = styled(KuiTableRowCell)`
|
||||
width: 15%;
|
||||
`;
|
||||
|
||||
const AppLink = styled(RelativeLink)`
|
||||
font-size: ${fontSizes.large};
|
||||
${truncate('100%')};
|
||||
`;
|
||||
|
||||
function formatString(value) {
|
||||
return value || 'N/A';
|
||||
}
|
||||
|
||||
function formatNumber(value) {
|
||||
if (value === 0) {
|
||||
return '0';
|
||||
}
|
||||
const formatted = numeral(value).format('0.0');
|
||||
return formatted <= 0.1 ? '< 0.1' : formatted;
|
||||
}
|
||||
|
||||
function ListItem({ service }) {
|
||||
const {
|
||||
serviceName,
|
||||
agentName,
|
||||
transactionsPerMinute,
|
||||
errorsPerMinute,
|
||||
avgResponseTime
|
||||
} = service;
|
||||
|
||||
return (
|
||||
<KuiTableRow>
|
||||
<ServiceNameCell>
|
||||
<TooltipOverlay content={formatString(serviceName)}>
|
||||
<AppLink path={`/${serviceName}/transactions`}>
|
||||
{formatString(serviceName)}
|
||||
</AppLink>
|
||||
</TooltipOverlay>
|
||||
</ServiceNameCell>
|
||||
<ServiceCell>{formatString(agentName)}</ServiceCell>
|
||||
<ServiceCell align={RIGHT_ALIGNMENT}>
|
||||
{asMillisWithDefault(avgResponseTime)}
|
||||
</ServiceCell>
|
||||
<ServiceCell align={RIGHT_ALIGNMENT}>
|
||||
{formatNumber(transactionsPerMinute)} tpm
|
||||
</ServiceCell>
|
||||
<ServiceCell align={RIGHT_ALIGNMENT}>
|
||||
{formatNumber(errorsPerMinute)} err.
|
||||
</ServiceCell>
|
||||
</KuiTableRow>
|
||||
);
|
||||
}
|
||||
|
||||
export default ListItem;
|
|
@ -36,11 +36,7 @@ describe('ErrorGroupOverview -> List', () => {
|
|||
it('should render with data', () => {
|
||||
const storeState = { location: {} };
|
||||
const wrapper = mountWithRouterAndStore(
|
||||
<List
|
||||
items={props.items}
|
||||
changeServiceSorting={props.changeServiceSorting}
|
||||
serviceSorting={props.serviceSorting}
|
||||
/>,
|
||||
<List items={props.items} />,
|
||||
storeState
|
||||
);
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -14,6 +14,5 @@
|
|||
"errorsPerMinute": 12.6,
|
||||
"avgResponseTime": 91535.42944785276
|
||||
}
|
||||
],
|
||||
"serviceSorting": { "key": "serviceName", "descending": false }
|
||||
]
|
||||
}
|
||||
|
|
|
@ -6,80 +6,132 @@
|
|||
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import FilterableAPMTable from '../../../shared/APMTable/FilterableAPMTable';
|
||||
import { AlignmentKuiTableHeaderCell } from '../../../shared/APMTable/APMTable';
|
||||
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';
|
||||
|
||||
import ListItem from './ListItem';
|
||||
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 {
|
||||
items,
|
||||
changeServiceSorting,
|
||||
serviceSorting,
|
||||
emptyMessageHeading,
|
||||
emptyMessageSubHeading
|
||||
} = this.props;
|
||||
const columns = [
|
||||
{
|
||||
field: 'serviceName',
|
||||
name: 'Name',
|
||||
sortable: true,
|
||||
render: serviceName => (
|
||||
<TooltipOverlay content={formatString(serviceName)}>
|
||||
<AppLink path={`/${serviceName}/transactions`}>
|
||||
{formatString(serviceName)}
|
||||
</AppLink>
|
||||
</TooltipOverlay>
|
||||
)
|
||||
},
|
||||
{
|
||||
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 renderHead = () => {
|
||||
const cells = [
|
||||
{ key: 'serviceName', label: 'Name' },
|
||||
{ key: 'agentName', label: 'Agent' },
|
||||
{
|
||||
key: 'avgResponseTime',
|
||||
label: 'Avg. response time',
|
||||
alignRight: true
|
||||
},
|
||||
{
|
||||
key: 'transactionsPerMinute',
|
||||
label: 'Trans. per minute',
|
||||
alignRight: true
|
||||
},
|
||||
{
|
||||
key: 'errorsPerMinute',
|
||||
label: 'Errors per minute',
|
||||
alignRight: true
|
||||
}
|
||||
].map(({ key, label, alignRight }) => (
|
||||
<AlignmentKuiTableHeaderCell
|
||||
key={key}
|
||||
onSort={() => changeServiceSorting(key)}
|
||||
isSorted={serviceSorting.key === key}
|
||||
isSortAscending={!serviceSorting.descending}
|
||||
className={alignRight ? 'kuiTableHeaderCell--alignRight' : ''}
|
||||
>
|
||||
{label}
|
||||
</AlignmentKuiTableHeaderCell>
|
||||
));
|
||||
const sortedItems = orderBy(
|
||||
this.props.items,
|
||||
this.state.sort.field,
|
||||
this.state.sort.direction
|
||||
);
|
||||
|
||||
return cells;
|
||||
};
|
||||
|
||||
const renderBody = services => {
|
||||
return services.map(service => {
|
||||
return <ListItem key={service.serviceName} service={service} />;
|
||||
});
|
||||
};
|
||||
|
||||
const renderFooterText = () => {
|
||||
return items.length === 500 ? 'Only top 500 services are shown' : '';
|
||||
};
|
||||
const paginatedItems = paginateItems({
|
||||
items: sortedItems,
|
||||
pageIndex: this.state.page.index,
|
||||
pageSize: this.state.page.size
|
||||
});
|
||||
|
||||
return (
|
||||
<FilterableAPMTable
|
||||
searchableFields={['serviceName', 'agentName']}
|
||||
items={items}
|
||||
emptyMessageHeading={emptyMessageHeading}
|
||||
emptyMessageSubHeading={emptyMessageSubHeading}
|
||||
renderHead={renderHead}
|
||||
renderBody={renderBody}
|
||||
renderFooterText={renderFooterText}
|
||||
<EuiBasicTable
|
||||
noItemsMessage={this.props.noItemsMessage}
|
||||
items={paginatedItems}
|
||||
columns={columns}
|
||||
pagination={{
|
||||
pageIndex: this.state.page.index,
|
||||
pageSize: this.state.page.size,
|
||||
totalItemCount: this.props.items.length
|
||||
}}
|
||||
sorting={{
|
||||
sort: {
|
||||
field: this.state.sort.field,
|
||||
direction: this.state.sort.direction
|
||||
}
|
||||
}}
|
||||
onChange={this.onTableChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
List.propTypes = {
|
||||
noItemsMessage: PropTypes.node,
|
||||
items: PropTypes.array
|
||||
};
|
||||
|
||||
|
|
|
@ -8,17 +8,13 @@ import { connect } from 'react-redux';
|
|||
import ServiceOverview from './view';
|
||||
import { getServiceList } from '../../../store/reactReduxRequest/serviceList';
|
||||
import { getUrlParams } from '../../../store/urlParams';
|
||||
import sorting, { changeServiceSorting } from '../../../store/sorting';
|
||||
|
||||
function mapStateToProps(state = {}) {
|
||||
return {
|
||||
serviceList: getServiceList(state),
|
||||
urlParams: getUrlParams(state),
|
||||
serviceSorting: sorting(state, 'service').sorting.service
|
||||
urlParams: getUrlParams(state)
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
changeServiceSorting
|
||||
};
|
||||
const mapDispatchToProps = {};
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ServiceOverview);
|
||||
|
|
|
@ -12,19 +12,21 @@ import { KibanaLink } from '../../../utils/url';
|
|||
import { EuiButton } from '@elastic/eui';
|
||||
import List from './List';
|
||||
import { HeaderContainer } from '../../shared/UIComponents';
|
||||
import { KueryBar } from '../../shared/KueryBar';
|
||||
|
||||
import { ServiceListRequest } from '../../../store/reactReduxRequest/serviceList';
|
||||
import EmptyMessage from '../../shared/EmptyMessage';
|
||||
|
||||
class ServiceOverview extends Component {
|
||||
state = {
|
||||
noHistoricalDataFound: false
|
||||
historicalDataFound: true
|
||||
};
|
||||
|
||||
async checkForHistoricalData({ serviceList }) {
|
||||
if (serviceList.status === STATUS.SUCCESS && isEmpty(serviceList.data)) {
|
||||
const result = await loadAgentStatus();
|
||||
if (!result.dataFound) {
|
||||
this.setState({ noHistoricalDataFound: true });
|
||||
this.setState({ historicalDataFound: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -38,16 +40,21 @@ class ServiceOverview extends Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { changeServiceSorting, serviceSorting, urlParams } = this.props;
|
||||
const { noHistoricalDataFound } = this.state;
|
||||
const { urlParams } = this.props;
|
||||
const { historicalDataFound } = this.state;
|
||||
|
||||
const emptyMessageHeading = noHistoricalDataFound
|
||||
? "Looks like you don't have any services with APM installed. Let's add some!"
|
||||
: 'No services with data in the selected time range.';
|
||||
|
||||
const emptyMessageSubHeading = noHistoricalDataFound ? (
|
||||
<SetupInstructionsLink buttonFill />
|
||||
) : null;
|
||||
const noItemsMessage = (
|
||||
<EmptyMessage
|
||||
heading={
|
||||
historicalDataFound
|
||||
? 'No services with data in the selected time range.'
|
||||
: "Looks like you don't have any services with APM installed. Let's add some!"
|
||||
}
|
||||
subheading={
|
||||
!historicalDataFound ? <SetupInstructionsLink buttonFill /> : null
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
@ -56,16 +63,12 @@ class ServiceOverview extends Component {
|
|||
<SetupInstructionsLink />
|
||||
</HeaderContainer>
|
||||
|
||||
<KueryBar />
|
||||
|
||||
<ServiceListRequest
|
||||
urlParams={urlParams}
|
||||
render={({ data }) => (
|
||||
<List
|
||||
items={data}
|
||||
changeServiceSorting={changeServiceSorting}
|
||||
serviceSorting={serviceSorting}
|
||||
emptyMessageHeading={emptyMessageHeading}
|
||||
emptyMessageSubHeading={emptyMessageSubHeading}
|
||||
/>
|
||||
<List items={data} noItemsMessage={noItemsMessage} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -176,10 +176,10 @@ function getPrimaryType(type) {
|
|||
}
|
||||
|
||||
Spans.propTypes = {
|
||||
location: PropTypes.object.isRequired,
|
||||
agentName: PropTypes.string.isRequired,
|
||||
urlParams: PropTypes.object.isRequired,
|
||||
droppedSpans: PropTypes.number.isRequired
|
||||
droppedSpans: PropTypes.number.isRequired,
|
||||
location: PropTypes.object.isRequired,
|
||||
urlParams: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default Spans;
|
||||
|
|
|
@ -12,12 +12,15 @@ import { DetailsChartsRequest } from '../../../store/reactReduxRequest/detailsCh
|
|||
import Charts from '../../shared/charts/TransactionCharts';
|
||||
import { TransactionDistributionRequest } from '../../../store/reactReduxRequest/transactionDistribution';
|
||||
import { TransactionDetailsRequest } from '../../../store/reactReduxRequest/transactionDetails';
|
||||
import { KueryBar } from '../../shared/KueryBar';
|
||||
|
||||
function TransactionDetails({ urlParams, location }) {
|
||||
return (
|
||||
<div>
|
||||
<HeaderLarge>{urlParams.transactionName}</HeaderLarge>
|
||||
|
||||
<KueryBar />
|
||||
|
||||
<DetailsChartsRequest
|
||||
urlParams={urlParams}
|
||||
render={({ data }) => (
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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 { units, borderRadius, px, colors } from '../../../../style/variables';
|
||||
|
||||
const ImpactBarBackground = styled.div`
|
||||
height: ${px(units.minus)};
|
||||
border-radius: ${borderRadius};
|
||||
background: ${colors.gray4};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const ImpactBar = styled.div`
|
||||
height: ${px(units.minus)};
|
||||
background: ${colors.blue2};
|
||||
border-radius: ${borderRadius};
|
||||
`;
|
||||
|
||||
function ImpactSparkline({ impact }) {
|
||||
if (!impact && impact !== 0) {
|
||||
return <div>N/A</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ImpactBarBackground>
|
||||
<ImpactBar style={{ width: `${impact}%` }} />
|
||||
</ImpactBarBackground>
|
||||
);
|
||||
}
|
||||
|
||||
export default ImpactSparkline;
|
|
@ -1,111 +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 { KuiTableRow, KuiTableRowCell } from '@kbn/ui-framework/components';
|
||||
import {
|
||||
units,
|
||||
borderRadius,
|
||||
px,
|
||||
colors,
|
||||
fontFamilyCode,
|
||||
truncate
|
||||
} from '../../../../style/variables';
|
||||
import { RIGHT_ALIGNMENT } from '@elastic/eui';
|
||||
import { RelativeLink, legacyEncodeURIComponent } from '../../../../utils/url';
|
||||
import TooltipOverlay from '../../../shared/TooltipOverlay';
|
||||
|
||||
import { get } from 'lodash';
|
||||
import { TRANSACTION_NAME } from '../../../../../common/constants';
|
||||
import {
|
||||
asMillisWithDefault,
|
||||
asDecimal,
|
||||
tpmUnit
|
||||
} from '../../../../utils/formatters';
|
||||
|
||||
const TransactionNameCell = styled(KuiTableRowCell)`
|
||||
font-family: ${fontFamilyCode};
|
||||
width: 38%;
|
||||
`;
|
||||
|
||||
const TransactionNameLink = styled(RelativeLink)`
|
||||
${truncate('100%')};
|
||||
`;
|
||||
|
||||
const TransactionKPICell = styled(KuiTableRowCell)`
|
||||
max-width: none;
|
||||
width: 14%;
|
||||
`;
|
||||
|
||||
const TransactionSpacerCell = styled(KuiTableRowCell)`
|
||||
max-width: none;
|
||||
width: 4%;
|
||||
`;
|
||||
|
||||
const TransactionImpactCell = styled(KuiTableRowCell)`
|
||||
max-width: none;
|
||||
width: 16%;
|
||||
`;
|
||||
|
||||
const ImpactBarBackground = styled.div`
|
||||
height: ${px(units.minus)};
|
||||
border-radius: ${borderRadius};
|
||||
background: ${colors.gray4};
|
||||
`;
|
||||
|
||||
const ImpactBar = styled.div`
|
||||
height: ${px(units.minus)};
|
||||
background: ${colors.blue2};
|
||||
border-radius: ${borderRadius};
|
||||
`;
|
||||
|
||||
function ImpactSparkline({ impact }) {
|
||||
if (!impact && impact !== 0) {
|
||||
return <div>N/A</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<ImpactBarBackground>
|
||||
<ImpactBar style={{ width: `${impact}%` }} />
|
||||
</ImpactBarBackground>
|
||||
);
|
||||
}
|
||||
|
||||
function TransactionListItem({ serviceName, transaction, type, impact }) {
|
||||
const transactionName = get({ transaction }, TRANSACTION_NAME);
|
||||
const transactionUrl = `${serviceName}/transactions/${legacyEncodeURIComponent(
|
||||
type
|
||||
)}/${legacyEncodeURIComponent(transactionName)}`;
|
||||
|
||||
return (
|
||||
<KuiTableRow>
|
||||
<TransactionNameCell>
|
||||
<TooltipOverlay content={transactionName || 'N/A'}>
|
||||
<TransactionNameLink path={`/${transactionUrl}`}>
|
||||
{transactionName || 'N/A'}
|
||||
</TransactionNameLink>
|
||||
</TooltipOverlay>
|
||||
</TransactionNameCell>
|
||||
<TransactionKPICell align={RIGHT_ALIGNMENT}>
|
||||
{asMillisWithDefault(transaction.avg)}
|
||||
</TransactionKPICell>
|
||||
<TransactionKPICell align={RIGHT_ALIGNMENT}>
|
||||
{asMillisWithDefault(transaction.p95)}
|
||||
</TransactionKPICell>
|
||||
<TransactionKPICell align={RIGHT_ALIGNMENT}>
|
||||
{asDecimal(transaction.tpm)} {tpmUnit(type)}
|
||||
</TransactionKPICell>
|
||||
<TransactionSpacerCell />
|
||||
<TransactionImpactCell>
|
||||
<ImpactSparkline impact={impact} />
|
||||
</TransactionImpactCell>
|
||||
</KuiTableRow>
|
||||
);
|
||||
}
|
||||
|
||||
export default TransactionListItem;
|
|
@ -6,18 +6,19 @@
|
|||
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { get } from 'lodash';
|
||||
import { TRANSACTION_ID } from '../../../../../common/constants';
|
||||
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 {
|
||||
asMillisWithDefault,
|
||||
asDecimal,
|
||||
tpmUnit
|
||||
} from '../../../../utils/formatters';
|
||||
|
||||
import { KuiTableHeaderCell } from '@kbn/ui-framework/components';
|
||||
import { AlignmentKuiTableHeaderCell } from '../../../shared/APMTable/APMTable';
|
||||
|
||||
import FilterableAPMTable from '../../../shared/APMTable/FilterableAPMTable';
|
||||
import ListItem from './ListItem';
|
||||
import ImpactTooltip from './ImpactTooltip';
|
||||
|
||||
const getRelativeImpact = (impact, impactMin, impactMax) =>
|
||||
Math.max((impact - impactMin) / Math.max(impactMax - impactMin, 1) * 100, 1);
|
||||
import { fontFamilyCode, truncate } from '../../../../style/variables';
|
||||
import ImpactSparkline from './ImpactSparkLine';
|
||||
|
||||
function tpmLabel(type) {
|
||||
return type === 'request' ? 'Req. per minute' : 'Trans. per minute';
|
||||
|
@ -27,102 +28,112 @@ 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
|
||||
},
|
||||
sort: {
|
||||
field: 'impactRelative',
|
||||
direction: 'desc'
|
||||
}
|
||||
};
|
||||
|
||||
onTableChange = ({ page = {}, sort = {} }) => {
|
||||
this.setState({ page, sort });
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
agentName,
|
||||
changeTransactionSorting,
|
||||
items,
|
||||
serviceName,
|
||||
transactionSorting,
|
||||
type
|
||||
} = this.props;
|
||||
const { agentName, serviceName, type } = this.props;
|
||||
|
||||
const renderHead = () => {
|
||||
const cells = [
|
||||
{ key: 'name', sortable: true, label: 'Name' },
|
||||
{
|
||||
key: 'avg',
|
||||
sortable: true,
|
||||
alignRight: true,
|
||||
label: avgLabel(agentName)
|
||||
},
|
||||
{
|
||||
key: 'p95',
|
||||
sortable: true,
|
||||
alignRight: true,
|
||||
label: '95th percentile'
|
||||
},
|
||||
{
|
||||
key: 'tpm',
|
||||
sortable: true,
|
||||
alignRight: true,
|
||||
label: tpmLabel(type)
|
||||
},
|
||||
{ key: 'spacer', sortable: false, label: '' }
|
||||
].map(({ key, sortable, label, alignRight }) => (
|
||||
<AlignmentKuiTableHeaderCell
|
||||
key={key}
|
||||
className={alignRight ? 'kuiTableHeaderCell--alignRight' : ''}
|
||||
{...(sortable
|
||||
? {
|
||||
onSort: () => changeTransactionSorting(key),
|
||||
isSorted: transactionSorting.key === key,
|
||||
isSortAscending: !transactionSorting.descending
|
||||
}
|
||||
: {})}
|
||||
>
|
||||
{label}
|
||||
</AlignmentKuiTableHeaderCell>
|
||||
));
|
||||
const columns = [
|
||||
{
|
||||
field: 'name',
|
||||
name: 'Name',
|
||||
sortable: true,
|
||||
render: transactionName => {
|
||||
const transactionUrl = `${serviceName}/transactions/${legacyEncodeURIComponent(
|
||||
type
|
||||
)}/${legacyEncodeURIComponent(transactionName)}`;
|
||||
|
||||
const impactCell = (
|
||||
<KuiTableHeaderCell
|
||||
key={'impact'}
|
||||
onSort={() => changeTransactionSorting('impact')}
|
||||
isSorted={transactionSorting.key === 'impact'}
|
||||
isSortAscending={!transactionSorting.descending}
|
||||
>
|
||||
Impact
|
||||
<ImpactTooltip />
|
||||
</KuiTableHeaderCell>
|
||||
);
|
||||
return (
|
||||
<TooltipOverlay content={transactionName || 'N/A'}>
|
||||
<TransactionNameLink path={`/${transactionUrl}`}>
|
||||
{transactionName || 'N/A'}
|
||||
</TransactionNameLink>
|
||||
</TooltipOverlay>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
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 => <ImpactSparkline impact={value} />
|
||||
}
|
||||
];
|
||||
|
||||
return [...cells, impactCell];
|
||||
};
|
||||
const sortedItems = orderBy(
|
||||
this.props.items,
|
||||
this.state.sort.field,
|
||||
this.state.sort.direction
|
||||
);
|
||||
|
||||
const impacts = items.map(({ impact }) => impact);
|
||||
const impactMin = Math.min(...impacts);
|
||||
const impactMax = Math.max(...impacts);
|
||||
|
||||
const renderBody = transactions => {
|
||||
return transactions.map(transaction => {
|
||||
return (
|
||||
<ListItem
|
||||
key={get({ transaction }, TRANSACTION_ID)}
|
||||
serviceName={serviceName}
|
||||
type={type}
|
||||
transaction={transaction}
|
||||
impact={getRelativeImpact(transaction.impact, impactMin, impactMax)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const renderFooterText = () => {
|
||||
return items.length === 500
|
||||
? 'Showing first 500 results ordered by response time'
|
||||
: '';
|
||||
};
|
||||
const paginatedItems = paginateItems({
|
||||
items: sortedItems,
|
||||
pageIndex: this.state.page.index,
|
||||
pageSize: this.state.page.size
|
||||
});
|
||||
|
||||
return (
|
||||
<FilterableAPMTable
|
||||
searchableFields={['name']}
|
||||
items={items}
|
||||
emptyMessageHeading="No transactions in the selected time range."
|
||||
renderHead={renderHead}
|
||||
renderBody={renderBody}
|
||||
renderFooterText={renderFooterText}
|
||||
<EuiBasicTable
|
||||
noItemsMessage="No transactions found"
|
||||
items={paginatedItems}
|
||||
columns={columns}
|
||||
pagination={{
|
||||
pageIndex: this.state.page.index,
|
||||
pageSize: this.state.page.size,
|
||||
totalItemCount: this.props.items.length
|
||||
}}
|
||||
sorting={{
|
||||
sort: {
|
||||
field: this.state.sort.field,
|
||||
direction: this.state.sort.direction
|
||||
}
|
||||
}}
|
||||
onChange={this.onTableChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -130,10 +141,15 @@ class List extends Component {
|
|||
|
||||
List.propTypes = {
|
||||
agentName: PropTypes.string,
|
||||
changeTransactionSorting: PropTypes.func.isRequired,
|
||||
items: PropTypes.array,
|
||||
transactionSorting: PropTypes.object.isRequired,
|
||||
serviceName: PropTypes.string,
|
||||
type: PropTypes.string
|
||||
};
|
||||
|
||||
export default List;
|
||||
|
||||
// const renderFooterText = () => {
|
||||
// return items.length === 500
|
||||
// ? 'Showing first 500 results ordered by response time'
|
||||
// : '';
|
||||
// };
|
||||
|
|
|
@ -11,7 +11,6 @@ import { toJson } from '../../../../utils/testHelpers';
|
|||
|
||||
const setup = () => {
|
||||
const props = {
|
||||
changeTransactionSorting: () => {},
|
||||
license: {
|
||||
data: {
|
||||
features: {
|
||||
|
@ -19,7 +18,6 @@ const setup = () => {
|
|||
}
|
||||
}
|
||||
},
|
||||
transactionSorting: {},
|
||||
hasDynamicBaseline: false,
|
||||
location: {},
|
||||
urlParams: { transactionType: 'request', serviceName: 'MyServiceName' }
|
||||
|
|
|
@ -10,6 +10,7 @@ exports[`TransactionOverview should not call loadTransactionList without any pro
|
|||
onOpenFlyout={[Function]}
|
||||
/>
|
||||
</styled.div>
|
||||
<Connect(KueryBarView) />
|
||||
<DynamicBaselineFlyout
|
||||
hasDynamicBaseline={false}
|
||||
isOpen={false}
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
import { connect } from 'react-redux';
|
||||
import TransactionOverview from './view';
|
||||
import { getUrlParams } from '../../../store/urlParams';
|
||||
import sorting, { changeTransactionSorting } from '../../../store/sorting';
|
||||
import { hasDynamicBaseline } from '../../../store/reactReduxRequest/overviewCharts';
|
||||
import { getLicense } from '../../../store/reactReduxRequest/license';
|
||||
|
||||
|
@ -16,14 +15,11 @@ function mapStateToProps(state = {}) {
|
|||
urlParams: getUrlParams(state),
|
||||
hasDynamicBaseline: hasDynamicBaseline(state),
|
||||
location: state.location,
|
||||
transactionSorting: sorting(state, 'transaction').sorting.transaction,
|
||||
license: getLicense(state)
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
changeTransactionSorting
|
||||
};
|
||||
const mapDispatchToProps = {};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(
|
||||
TransactionOverview
|
||||
|
|
|
@ -21,6 +21,7 @@ import { ServiceDetailsRequest } from '../../../store/reactReduxRequest/serviceD
|
|||
|
||||
import DynamicBaselineButton from './DynamicBaseline/Button';
|
||||
import DynamicBaselineFlyout from './DynamicBaseline/Flyout';
|
||||
import { KueryBar } from '../../shared/KueryBar';
|
||||
|
||||
function ServiceDetailsAndTransactionList({ urlParams, render }) {
|
||||
return (
|
||||
|
@ -62,14 +63,7 @@ class TransactionOverview extends Component {
|
|||
onCloseFlyout = () => this.setState({ isFlyoutOpen: false });
|
||||
|
||||
render() {
|
||||
const {
|
||||
changeTransactionSorting,
|
||||
hasDynamicBaseline,
|
||||
license,
|
||||
location,
|
||||
transactionSorting,
|
||||
urlParams
|
||||
} = this.props;
|
||||
const { hasDynamicBaseline, license, location, urlParams } = this.props;
|
||||
|
||||
const { serviceName, transactionType } = urlParams;
|
||||
const mlEnabled = chrome.getInjected('mlEnabled');
|
||||
|
@ -103,6 +97,8 @@ class TransactionOverview extends Component {
|
|||
)}
|
||||
</HeaderContainer>
|
||||
|
||||
<KueryBar />
|
||||
|
||||
<DynamicBaselineFlyout
|
||||
hasDynamicBaseline={hasDynamicBaseline}
|
||||
isOpen={this.state.isFlyoutOpen}
|
||||
|
@ -134,11 +130,9 @@ class TransactionOverview extends Component {
|
|||
return (
|
||||
<List
|
||||
agentName={serviceDetails.agentName}
|
||||
items={transactionList}
|
||||
serviceName={serviceName}
|
||||
type={transactionType}
|
||||
items={transactionList}
|
||||
changeTransactionSorting={changeTransactionSorting}
|
||||
transactionSorting={transactionSorting}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
|
@ -149,11 +143,9 @@ class TransactionOverview extends Component {
|
|||
}
|
||||
|
||||
TransactionOverview.propTypes = {
|
||||
changeTransactionSorting: PropTypes.func.isRequired,
|
||||
hasDynamicBaseline: PropTypes.bool.isRequired,
|
||||
license: PropTypes.object.isRequired,
|
||||
location: PropTypes.object.isRequired,
|
||||
transactionSorting: PropTypes.object.isRequired,
|
||||
urlParams: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
|
|
|
@ -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 = (
|
||||
<KuiToolBarSection>
|
||||
<KuiPager
|
||||
startNumber={startNumber}
|
||||
endNumber={Math.min(endNumber, totalItems)}
|
||||
totalItems={totalItems}
|
||||
hasNextPage={endNumber < totalItems}
|
||||
hasPreviousPage={page > 0}
|
||||
onNextPage={onClickNext}
|
||||
onPreviousPage={onClickPrev}
|
||||
/>
|
||||
</KuiToolBarSection>
|
||||
);
|
||||
|
||||
return (
|
||||
<KuiControlledTable>
|
||||
<KuiToolBar>
|
||||
<KuiToolBarSearchBox
|
||||
defaultValue={defaultSearchQuery}
|
||||
onClick={e => e.stopPropagation()}
|
||||
onFilter={onFilter}
|
||||
placeholder={inputPlaceholder}
|
||||
/>
|
||||
{pagination}
|
||||
</KuiToolBar>
|
||||
|
||||
{items.length === 0 && (
|
||||
<KuiEmptyTablePromptPanel>
|
||||
<EmptyMessage
|
||||
heading={emptyMessageHeading}
|
||||
subheading={emptyMessageSubHeading}
|
||||
/>
|
||||
</KuiEmptyTablePromptPanel>
|
||||
)}
|
||||
|
||||
{items.length > 0 && (
|
||||
<KuiTable>
|
||||
<KuiTableHeader>{renderHead()}</KuiTableHeader>
|
||||
<KuiTableBody>{renderBody(items)}</KuiTableBody>
|
||||
</KuiTable>
|
||||
)}
|
||||
|
||||
<KuiToolBarFooter>
|
||||
<KuiToolBarFooterSection>
|
||||
<FooterText>{renderFooterText()}</FooterText>
|
||||
</KuiToolBarFooterSection>
|
||||
{pagination}
|
||||
</KuiToolBarFooter>
|
||||
</KuiControlledTable>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
|
@ -1,94 +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 APMTable from './APMTable';
|
||||
|
||||
const ITEMS_PER_PAGE = 20;
|
||||
class FilterableAPMTable extends Component {
|
||||
state = { searchQuery: '', page: 0 };
|
||||
|
||||
onFilter = searchQuery => {
|
||||
this.setState({ searchQuery, page: 0 });
|
||||
};
|
||||
|
||||
onClickNext = () => {
|
||||
this.setState(state => ({
|
||||
page: state.page + 1
|
||||
}));
|
||||
};
|
||||
|
||||
onClickPrev = () => {
|
||||
this.setState(state => ({
|
||||
page: state.page - 1
|
||||
}));
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
emptyMessageHeading,
|
||||
emptyMessageSubHeading,
|
||||
items,
|
||||
renderBody,
|
||||
renderFooterText,
|
||||
renderHead,
|
||||
searchableFields
|
||||
} = this.props;
|
||||
|
||||
const startNumber = this.state.page * ITEMS_PER_PAGE;
|
||||
const endNumber = (this.state.page + 1) * ITEMS_PER_PAGE;
|
||||
const filteredItems = items.filter(item => {
|
||||
const isEmpty = this.state.searchQuery === '';
|
||||
const isMatch = searchableFields.some(property => {
|
||||
return (
|
||||
item[property] &&
|
||||
item[property]
|
||||
.toLowerCase()
|
||||
.includes(this.state.searchQuery.toLowerCase())
|
||||
);
|
||||
});
|
||||
return isEmpty || isMatch;
|
||||
});
|
||||
|
||||
const currentPageItems = filteredItems.slice(startNumber, endNumber);
|
||||
|
||||
return (
|
||||
<APMTable
|
||||
emptyMessageHeading={emptyMessageHeading}
|
||||
emptyMessageSubHeading={emptyMessageSubHeading}
|
||||
items={currentPageItems}
|
||||
itemsPerPage={ITEMS_PER_PAGE}
|
||||
onClickNext={this.onClickNext}
|
||||
onClickPrev={this.onClickPrev}
|
||||
onFilter={this.onFilter}
|
||||
inputPlaceholder="Filter..."
|
||||
page={this.state.page}
|
||||
renderBody={renderBody}
|
||||
renderHead={renderHead}
|
||||
renderFooterText={renderFooterText}
|
||||
totalItems={items.length}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FilterableAPMTable.propTypes = {
|
||||
emptyMessageHeading: PropTypes.string,
|
||||
items: PropTypes.array,
|
||||
renderBody: PropTypes.func.isRequired,
|
||||
renderFooterText: PropTypes.func,
|
||||
renderHead: PropTypes.func.isRequired,
|
||||
searchableFields: PropTypes.array
|
||||
};
|
||||
|
||||
FilterableAPMTable.defaultProps = {
|
||||
searchableFields: [],
|
||||
items: [],
|
||||
renderFooterText: () => {}
|
||||
};
|
||||
|
||||
export default FilterableAPMTable;
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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';
|
||||
|
||||
export default class ClickOutside extends Component {
|
||||
componentDidMount() {
|
||||
document.addEventListener('mousedown', this.onClick);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('mousedown', this.onClick);
|
||||
}
|
||||
|
||||
setNodeRef = node => {
|
||||
this.nodeRef = node;
|
||||
};
|
||||
|
||||
onClick = event => {
|
||||
if (this.nodeRef && !this.nodeRef.contains(event.target)) {
|
||||
this.props.onClickOutside();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { onClickOutside, ...restProps } = this.props;
|
||||
return (
|
||||
<div ref={this.setNodeRef} {...restProps}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ClickOutside.propTypes = {
|
||||
onClickOutside: PropTypes.func.isRequired
|
||||
};
|
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* 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 { EuiIcon } from '@elastic/eui';
|
||||
import {
|
||||
colors,
|
||||
fontFamilyCode,
|
||||
px,
|
||||
units,
|
||||
fontSizes,
|
||||
unit
|
||||
} from '../../../../style/variables';
|
||||
import { tint } from 'polished';
|
||||
|
||||
function getIconColor(type) {
|
||||
switch (type) {
|
||||
case 'field':
|
||||
return colors.apmOrange;
|
||||
case 'value':
|
||||
return colors.teal;
|
||||
case 'operator':
|
||||
return colors.apmBlue;
|
||||
case 'conjunction':
|
||||
return colors.apmPurple;
|
||||
case 'recentSearch':
|
||||
return colors.gray3;
|
||||
}
|
||||
}
|
||||
|
||||
const Description = styled.div`
|
||||
color: ${colors.gray2};
|
||||
|
||||
p {
|
||||
display: inline;
|
||||
|
||||
span {
|
||||
font-family: ${fontFamilyCode};
|
||||
color: ${colors.black};
|
||||
padding: 0 ${px(units.quarter)};
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const ListItem = styled.li`
|
||||
font-size: ${fontSizes.small};
|
||||
height: ${px(units.double)};
|
||||
align-items: center;
|
||||
display: flex;
|
||||
background: ${props => (props.selected ? colors.gray5 : 'initial')};
|
||||
cursor: pointer;
|
||||
border-radius: ${px(units.quarter)};
|
||||
|
||||
${Description} {
|
||||
p span {
|
||||
background: ${props => (props.selected ? colors.white : colors.gray5)};
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const Icon = styled.div`
|
||||
flex: 0 0 ${px(units.double)};
|
||||
background: ${props => tint(0.1, getIconColor(props.type))};
|
||||
color: ${props => getIconColor(props.type)};
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
line-height: ${px(units.double)};
|
||||
`;
|
||||
|
||||
const TextValue = styled.div`
|
||||
flex: 0 0 ${px(unit * 16)};
|
||||
color: ${colors.black2};
|
||||
padding: 0 ${px(units.half)};
|
||||
`;
|
||||
|
||||
function getEuiIconType(type) {
|
||||
switch (type) {
|
||||
case 'field':
|
||||
return 'kqlField';
|
||||
case 'value':
|
||||
return 'kqlValue';
|
||||
case 'recentSearch':
|
||||
return 'search';
|
||||
case 'conjunction':
|
||||
return 'kqlSelector';
|
||||
case 'operator':
|
||||
return 'kqlOperand';
|
||||
default:
|
||||
throw new Error('Unknown type', type);
|
||||
}
|
||||
}
|
||||
|
||||
function Suggestion(props) {
|
||||
return (
|
||||
<ListItem
|
||||
innerRef={props.innerRef}
|
||||
selected={props.selected}
|
||||
onClick={() => props.onClick(props.suggestion)}
|
||||
onMouseEnter={props.onMouseEnter}
|
||||
>
|
||||
<Icon type={props.suggestion.type}>
|
||||
<EuiIcon type={getEuiIconType(props.suggestion.type)} />
|
||||
</Icon>
|
||||
<TextValue>{props.suggestion.text}</TextValue>
|
||||
<Description
|
||||
dangerouslySetInnerHTML={{ __html: props.suggestion.description }}
|
||||
/>
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
|
||||
Suggestion.propTypes = {
|
||||
onClick: PropTypes.func.isRequired,
|
||||
onMouseEnter: PropTypes.func.isRequired,
|
||||
selected: PropTypes.bool,
|
||||
suggestion: PropTypes.object.isRequired,
|
||||
innerRef: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default Suggestion;
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* 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 styled from 'styled-components';
|
||||
import { isEmpty } from 'lodash';
|
||||
import Suggestion from './Suggestion';
|
||||
import { units, colors, px, unit } from '../../../../style/variables';
|
||||
|
||||
const List = styled.ul`
|
||||
width: 100%;
|
||||
border: 1px solid ${colors.gray4};
|
||||
border-radius: ${px(units.quarter)};
|
||||
box-shadow: 0px ${px(units.quarter)} ${px(units.double)} rgba(0, 0, 0, 0.1);
|
||||
position: absolute;
|
||||
background: #fff;
|
||||
z-index: 10;
|
||||
left: 0;
|
||||
max-height: ${px(unit * 20)};
|
||||
overflow: scroll;
|
||||
`;
|
||||
|
||||
class Suggestions extends Component {
|
||||
childNodes = [];
|
||||
|
||||
scrollIntoView = () => {
|
||||
const parent = this.parentNode;
|
||||
const child = this.childNodes[this.props.index];
|
||||
|
||||
if (this.props.index == null || !parent || !child) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scrollTop = Math.max(
|
||||
Math.min(parent.scrollTop, child.offsetTop),
|
||||
child.offsetTop + child.offsetHeight - parent.offsetHeight
|
||||
);
|
||||
|
||||
parent.scrollTop = scrollTop;
|
||||
};
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.index !== this.props.index) {
|
||||
this.scrollIntoView();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.props.show || isEmpty(this.props.suggestions)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const suggestions = this.props.suggestions.map((suggestion, index) => {
|
||||
const key = suggestion + '_' + index;
|
||||
return (
|
||||
<Suggestion
|
||||
innerRef={node => (this.childNodes[index] = node)}
|
||||
selected={index === this.props.index}
|
||||
suggestion={suggestion}
|
||||
onClick={this.props.onClick}
|
||||
onMouseEnter={() => this.props.onMouseEnter(index)}
|
||||
key={key}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<List innerRef={node => (this.parentNode = node)}>{suggestions}</List>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Suggestions.propTypes = {
|
||||
index: PropTypes.number,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
onMouseEnter: PropTypes.func.isRequired,
|
||||
show: PropTypes.bool,
|
||||
suggestions: PropTypes.array.isRequired
|
||||
};
|
||||
|
||||
export default Suggestions;
|
|
@ -0,0 +1,243 @@
|
|||
/*
|
||||
* 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 Suggestions from './Suggestions';
|
||||
import ClickOutside from './ClickOutside';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiFieldSearch,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiProgress,
|
||||
EuiIconTip
|
||||
} from '@elastic/eui';
|
||||
|
||||
const KEY_CODES = {
|
||||
LEFT: 37,
|
||||
UP: 38,
|
||||
RIGHT: 39,
|
||||
DOWN: 40,
|
||||
ENTER: 13,
|
||||
ESC: 27,
|
||||
TAB: 9
|
||||
};
|
||||
|
||||
export class Typeahead extends Component {
|
||||
state = {
|
||||
isSuggestionsVisible: false,
|
||||
index: null,
|
||||
value: '',
|
||||
inputIsPristine: true
|
||||
};
|
||||
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
if (state.inputIsPristine && props.initialValue) {
|
||||
return {
|
||||
value: props.initialValue
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
incrementIndex = currentIndex => {
|
||||
let nextIndex = currentIndex + 1;
|
||||
if (currentIndex === null || nextIndex >= this.props.suggestions.length) {
|
||||
nextIndex = 0;
|
||||
}
|
||||
this.setState({ index: nextIndex });
|
||||
};
|
||||
|
||||
decrementIndex = currentIndex => {
|
||||
let previousIndex = currentIndex - 1;
|
||||
if (previousIndex < 0) {
|
||||
previousIndex = null;
|
||||
}
|
||||
this.setState({ index: previousIndex });
|
||||
};
|
||||
|
||||
onKeyUp = event => {
|
||||
const { selectionStart } = event.target;
|
||||
const { value } = this.state;
|
||||
switch (event.keyCode) {
|
||||
case KEY_CODES.LEFT:
|
||||
this.setState({ isSuggestionsVisible: true });
|
||||
this.props.onChange(value, selectionStart);
|
||||
break;
|
||||
case KEY_CODES.RIGHT:
|
||||
this.setState({ isSuggestionsVisible: true });
|
||||
this.props.onChange(value, selectionStart);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
onKeyDown = event => {
|
||||
const { isSuggestionsVisible, index, value } = this.state;
|
||||
switch (event.keyCode) {
|
||||
case KEY_CODES.DOWN:
|
||||
event.preventDefault();
|
||||
if (isSuggestionsVisible) {
|
||||
this.incrementIndex(index);
|
||||
} else {
|
||||
this.setState({ isSuggestionsVisible: true, index: 0 });
|
||||
}
|
||||
break;
|
||||
case KEY_CODES.UP:
|
||||
event.preventDefault();
|
||||
if (isSuggestionsVisible) {
|
||||
this.decrementIndex(index);
|
||||
}
|
||||
break;
|
||||
case KEY_CODES.ENTER:
|
||||
event.preventDefault();
|
||||
if (isSuggestionsVisible && this.props.suggestions[index]) {
|
||||
this.selectSuggestion(this.props.suggestions[index]);
|
||||
} else {
|
||||
this.setState({ isSuggestionsVisible: false });
|
||||
this.props.onSubmit(value);
|
||||
}
|
||||
break;
|
||||
case KEY_CODES.ESC:
|
||||
event.preventDefault();
|
||||
this.setState({ isSuggestionsVisible: false });
|
||||
break;
|
||||
case KEY_CODES.TAB:
|
||||
this.setState({ isSuggestionsVisible: false });
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
selectSuggestion = suggestion => {
|
||||
const nextInputValue =
|
||||
this.state.value.substr(0, suggestion.start) +
|
||||
suggestion.text +
|
||||
this.state.value.substr(suggestion.end);
|
||||
|
||||
this.setState({ value: nextInputValue, index: null });
|
||||
this.props.onChange(nextInputValue, nextInputValue.length);
|
||||
};
|
||||
|
||||
onClickOutside = () => {
|
||||
this.setState({ isSuggestionsVisible: false });
|
||||
};
|
||||
|
||||
onChangeInputValue = event => {
|
||||
const { value, selectionStart } = event.target;
|
||||
const hasValue = Boolean(value.trim());
|
||||
this.setState({
|
||||
value,
|
||||
inputIsPristine: false,
|
||||
isSuggestionsVisible: hasValue,
|
||||
index: null
|
||||
});
|
||||
|
||||
if (!hasValue) {
|
||||
this.props.onSubmit(value);
|
||||
}
|
||||
this.props.onChange(value, selectionStart);
|
||||
};
|
||||
|
||||
onClickInput = event => {
|
||||
const { selectionStart } = event.target;
|
||||
this.props.onChange(this.state.value, selectionStart);
|
||||
};
|
||||
|
||||
onClickSuggestion = suggestion => {
|
||||
this.selectSuggestion(suggestion);
|
||||
this.inputRef.focus();
|
||||
};
|
||||
|
||||
onMouseEnterSuggestion = index => {
|
||||
this.setState({ index });
|
||||
};
|
||||
|
||||
onSubmit = () => {
|
||||
this.props.onSubmit(this.state.value);
|
||||
this.setState({ isSuggestionsVisible: false });
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ClickOutside
|
||||
onClickOutside={this.onClickOutside}
|
||||
style={{ position: 'relative' }}
|
||||
>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem style={{ position: 'relative' }}>
|
||||
<EuiFieldSearch
|
||||
fullWidth
|
||||
style={{
|
||||
backgroundImage: 'none'
|
||||
}}
|
||||
placeholder="Search transactions or errors… (i.e. transaction.duration.us => 100000)"
|
||||
inputRef={node => {
|
||||
if (node) {
|
||||
this.inputRef = node;
|
||||
}
|
||||
}}
|
||||
disabled={this.props.disabled}
|
||||
value={this.state.value}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onKeyUp={this.onKeyUp}
|
||||
onChange={this.onChangeInputValue}
|
||||
onClick={this.onClickInput}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
/>
|
||||
|
||||
{this.props.isLoading && (
|
||||
<EuiProgress
|
||||
size="xs"
|
||||
color="accent"
|
||||
position="absolute"
|
||||
style={{
|
||||
bottom: 0,
|
||||
top: 'initial'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton onClick={this.onSubmit} disabled={this.props.disabled}>
|
||||
Search
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIconTip
|
||||
content="The Query bar feature is still in beta. Help us report any issues or bugs by using the APM feedback link in the top."
|
||||
position="left"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<Suggestions
|
||||
show={this.state.isSuggestionsVisible}
|
||||
suggestions={this.props.suggestions}
|
||||
index={this.state.index}
|
||||
onClick={this.onClickSuggestion}
|
||||
onMouseEnter={this.onMouseEnterSuggestion}
|
||||
/>
|
||||
</ClickOutside>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Typeahead.propTypes = {
|
||||
initialValue: PropTypes.string,
|
||||
isLoading: PropTypes.bool,
|
||||
disabled: PropTypes.bool,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
suggestions: PropTypes.array.isRequired
|
||||
};
|
||||
|
||||
Typeahead.defaultProps = {
|
||||
isLoading: false,
|
||||
disabled: false,
|
||||
suggestions: []
|
||||
};
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* 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 {
|
||||
TRANSACTION_TYPE,
|
||||
ERROR_GROUP_ID,
|
||||
PROCESSOR_EVENT,
|
||||
TRANSACTION_NAME,
|
||||
SERVICE_NAME
|
||||
} from '../../../../common/constants';
|
||||
|
||||
export function getBoolFilter(urlParams) {
|
||||
const boolFilter = [
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: new Date(urlParams.start).getTime(),
|
||||
lte: new Date(urlParams.end).getTime(),
|
||||
format: 'epoch_millis'
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
if (urlParams.serviceName) {
|
||||
boolFilter.push({
|
||||
term: { [SERVICE_NAME]: urlParams.serviceName }
|
||||
});
|
||||
}
|
||||
|
||||
switch (urlParams.processorEvent) {
|
||||
case 'transaction':
|
||||
boolFilter.push({
|
||||
term: { [PROCESSOR_EVENT]: 'transaction' }
|
||||
});
|
||||
|
||||
if (urlParams.transactionName) {
|
||||
boolFilter.push({
|
||||
term: { [`${TRANSACTION_NAME}.keyword`]: urlParams.transactionName }
|
||||
});
|
||||
}
|
||||
|
||||
if (urlParams.transactionType) {
|
||||
boolFilter.push({
|
||||
term: { [TRANSACTION_TYPE]: urlParams.transactionType }
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
boolFilter.push({
|
||||
term: { [PROCESSOR_EVENT]: 'error' }
|
||||
});
|
||||
|
||||
if (urlParams.errorGroupId) {
|
||||
boolFilter.push({
|
||||
term: { [ERROR_GROUP_ID]: urlParams.errorGroupId }
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
boolFilter.push({
|
||||
bool: {
|
||||
should: [
|
||||
{ term: { [PROCESSOR_EVENT]: 'error' } },
|
||||
{ term: { [PROCESSOR_EVENT]: 'transaction' } }
|
||||
]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return boolFilter;
|
||||
}
|
|
@ -6,18 +6,21 @@
|
|||
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { uniqueId, startsWith } from 'lodash';
|
||||
import { EuiCallOut } from '@elastic/eui';
|
||||
import {
|
||||
history,
|
||||
fromQuery,
|
||||
toQuery,
|
||||
legacyEncodeURIComponent
|
||||
} from '../../../utils/url';
|
||||
import { debounce } from 'lodash';
|
||||
import { EuiFieldSearch } from '@elastic/eui';
|
||||
import { Typeahead } from './Typeahead';
|
||||
import { getAPMIndexPattern } from '../../../services/rest/savedObjects';
|
||||
import { convertKueryToEsQuery, getSuggestions } from '../../../services/kuery';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { getBoolFilter } from './get_bool_filter';
|
||||
|
||||
const Container = styled.div`
|
||||
margin-bottom: 10px;
|
||||
`;
|
||||
|
@ -25,36 +28,48 @@ const Container = styled.div`
|
|||
class KueryBarView extends Component {
|
||||
state = {
|
||||
indexPattern: null,
|
||||
inputValue: this.props.urlParams.kuery || ''
|
||||
suggestions: [],
|
||||
isLoadingIndexPattern: true,
|
||||
isLoadingSuggestions: false
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
getAPMIndexPattern().then(indexPattern => {
|
||||
this.setState({ indexPattern });
|
||||
});
|
||||
async componentDidMount() {
|
||||
const indexPattern = await getAPMIndexPattern();
|
||||
this.setState({ indexPattern, isLoadingIndexPattern: false });
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const kuery = nextProps.urlParams.kuery;
|
||||
if (kuery && !this.state.inputValue) {
|
||||
this.setState({ inputValue: kuery });
|
||||
}
|
||||
}
|
||||
|
||||
updateUrl = debounce(kuery => {
|
||||
const { location } = this.props;
|
||||
onChange = async (inputValue, selectionStart) => {
|
||||
const { indexPattern } = this.state;
|
||||
const { urlParams } = this.props;
|
||||
this.setState({ suggestions: [], isLoadingSuggestions: true });
|
||||
|
||||
if (!indexPattern) {
|
||||
return;
|
||||
}
|
||||
|
||||
getSuggestions(kuery, indexPattern).then(
|
||||
suggestions => console.log(suggestions.map(suggestion => suggestion.text)) // eslint-disable-line no-console
|
||||
);
|
||||
const currentRequest = uniqueId();
|
||||
this.currentRequest = currentRequest;
|
||||
|
||||
const boolFilter = getBoolFilter(urlParams);
|
||||
try {
|
||||
const res = convertKueryToEsQuery(kuery, indexPattern);
|
||||
const suggestions = (await getSuggestions(
|
||||
inputValue,
|
||||
selectionStart,
|
||||
indexPattern,
|
||||
boolFilter
|
||||
)).filter(suggestion => !startsWith(suggestion.text, 'span.'));
|
||||
|
||||
if (currentRequest !== this.currentRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ suggestions, isLoadingSuggestions: false });
|
||||
} catch (e) {
|
||||
console.error('Error while fetching suggestions', e);
|
||||
}
|
||||
};
|
||||
|
||||
onSubmit = inputValue => {
|
||||
const { indexPattern } = this.state;
|
||||
const { location } = this.props;
|
||||
try {
|
||||
const res = convertKueryToEsQuery(inputValue, indexPattern);
|
||||
if (!res) {
|
||||
return;
|
||||
}
|
||||
|
@ -63,29 +78,44 @@ class KueryBarView extends Component {
|
|||
...location,
|
||||
search: fromQuery({
|
||||
...toQuery(this.props.location.search),
|
||||
kuery: legacyEncodeURIComponent(kuery)
|
||||
kuery: legacyEncodeURIComponent(inputValue.trim())
|
||||
})
|
||||
});
|
||||
} catch (e) {
|
||||
console.log('Invalid kuery syntax'); // eslint-disable-line no-console
|
||||
}
|
||||
}, 200);
|
||||
|
||||
onChange = event => {
|
||||
const kuery = event.target.value;
|
||||
this.setState({ inputValue: kuery });
|
||||
this.updateUrl(kuery);
|
||||
};
|
||||
|
||||
render() {
|
||||
const indexPatternMissing =
|
||||
!this.state.isLoadingIndexPattern && !this.state.indexPattern;
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<EuiFieldSearch
|
||||
placeholder="Search... (Example: transaction.duration.us > 10000)"
|
||||
fullWidth
|
||||
<Typeahead
|
||||
disabled={indexPatternMissing}
|
||||
isLoading={this.state.isLoadingSuggestions}
|
||||
initialValue={this.props.urlParams.kuery}
|
||||
onChange={this.onChange}
|
||||
value={this.state.inputValue}
|
||||
onSubmit={this.onSubmit}
|
||||
suggestions={this.state.suggestions}
|
||||
/>
|
||||
|
||||
{indexPatternMissing && (
|
||||
<EuiCallOut
|
||||
style={{ display: 'inline-block', marginTop: '10px' }}
|
||||
title={
|
||||
<div>
|
||||
There's no APM index pattern available. To use the Query
|
||||
bar, please choose to import the APM index pattern in the{' '}
|
||||
<a href="/app/kibana#/home/tutorial/apm">Setup Instructions.</a>
|
||||
</div>
|
||||
}
|
||||
color="warning"
|
||||
iconType="alert"
|
||||
size="s"
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -15,18 +15,24 @@ export function convertKueryToEsQuery(kuery, indexPattern) {
|
|||
return toElasticsearchQuery(ast, indexPattern);
|
||||
}
|
||||
|
||||
export async function getSuggestions(query, apmIndexPattern) {
|
||||
export async function getSuggestions(
|
||||
query,
|
||||
selectionStart,
|
||||
apmIndexPattern,
|
||||
boolFilter
|
||||
) {
|
||||
const config = {
|
||||
get: () => true
|
||||
};
|
||||
|
||||
const getKuerySuggestions = getSuggestionsProvider({
|
||||
config,
|
||||
indexPatterns: [apmIndexPattern]
|
||||
indexPatterns: [apmIndexPattern],
|
||||
boolFilter
|
||||
});
|
||||
return getKuerySuggestions({
|
||||
query,
|
||||
selectionStart: query.length,
|
||||
selectionEnd: query.length
|
||||
selectionStart,
|
||||
selectionEnd: selectionStart
|
||||
});
|
||||
}
|
||||
|
|
|
@ -33,6 +33,11 @@ export async function getEncodedEsQuery(kuery) {
|
|||
}
|
||||
|
||||
const indexPattern = await getAPMIndexPattern();
|
||||
|
||||
if (!indexPattern) {
|
||||
return;
|
||||
}
|
||||
|
||||
const esFilterQuery = convertKueryToEsQuery(kuery, indexPattern);
|
||||
return encodeURIComponent(JSON.stringify(esFilterQuery));
|
||||
}
|
||||
|
@ -95,19 +100,12 @@ export async function loadTransactionDistribution({
|
|||
});
|
||||
}
|
||||
|
||||
export async function loadSpans({
|
||||
serviceName,
|
||||
start,
|
||||
end,
|
||||
transactionId,
|
||||
kuery
|
||||
}) {
|
||||
export async function loadSpans({ serviceName, start, end, transactionId }) {
|
||||
return callApi({
|
||||
pathname: `/api/apm/services/${serviceName}/transactions/${transactionId}/spans`,
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
esFilterQuery: await getEncodedEsQuery(kuery)
|
||||
end
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -165,9 +163,8 @@ export async function loadErrorGroupList({
|
|||
end,
|
||||
kuery,
|
||||
size,
|
||||
q,
|
||||
sortBy,
|
||||
sortOrder
|
||||
sortField,
|
||||
sortDirection
|
||||
}) {
|
||||
return callApi({
|
||||
pathname: `/api/apm/services/${serviceName}/errors`,
|
||||
|
@ -175,9 +172,8 @@ export async function loadErrorGroupList({
|
|||
start,
|
||||
end,
|
||||
size,
|
||||
q,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
sortField,
|
||||
sortDirection,
|
||||
esFilterQuery: await getEncodedEsQuery(kuery)
|
||||
}
|
||||
});
|
||||
|
|
|
@ -11,10 +11,6 @@ describe('root reducer', () => {
|
|||
expect(reducer(undefined, {})).toEqual({
|
||||
location: { hash: '', pathname: '', search: '' },
|
||||
reactReduxRequest: {},
|
||||
sorting: {
|
||||
service: { descending: false, key: 'serviceName' },
|
||||
transaction: { descending: true, key: 'impact' }
|
||||
},
|
||||
urlParams: {}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -25,6 +25,7 @@ describe('urlParams', () => {
|
|||
page: 0,
|
||||
serviceName: 'myServiceName',
|
||||
spanId: 10,
|
||||
processorEvent: 'transaction',
|
||||
transactionId: 'myTransactionId',
|
||||
transactionName: 'myTransactionName',
|
||||
detailTab: 'request',
|
||||
|
|
|
@ -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 }) {
|
|||
<Request
|
||||
id={ID}
|
||||
fn={loadErrorGroupList}
|
||||
args={[{ serviceName, start, end, q, sortBy, sortOrder, kuery }]}
|
||||
args={[{ serviceName, start, end, sortField, sortDirection, kuery }]}
|
||||
selector={getErrorGroupList}
|
||||
render={render}
|
||||
/>
|
||||
|
|
|
@ -5,8 +5,6 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import orderBy from 'lodash.orderby';
|
||||
import { createSelector } from 'reselect';
|
||||
import { loadServiceList } from '../../services/rest/apm';
|
||||
import { Request } from 'react-redux-request';
|
||||
import { createInitialDataSelector } from './helpers';
|
||||
|
@ -15,18 +13,9 @@ const ID = 'serviceList';
|
|||
const INITIAL_DATA = [];
|
||||
const withInitialData = createInitialDataSelector(INITIAL_DATA);
|
||||
|
||||
export const getServiceList = createSelector(
|
||||
state => withInitialData(state.reactReduxRequest[ID]),
|
||||
state => state.sorting.service,
|
||||
(serviceList, serviceSorting) => {
|
||||
const { key: sortKey, descending } = serviceSorting;
|
||||
|
||||
return {
|
||||
...serviceList,
|
||||
data: orderBy(serviceList.data, sortKey, descending ? 'desc' : 'asc')
|
||||
};
|
||||
}
|
||||
);
|
||||
export function getServiceList(state) {
|
||||
return withInitialData(state.reactReduxRequest[ID]);
|
||||
}
|
||||
|
||||
export function ServiceListRequest({ urlParams, render }) {
|
||||
const { start, end, kuery } = urlParams;
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import orderBy from 'lodash.orderby';
|
||||
import { createSelector } from 'reselect';
|
||||
import { Request } from 'react-redux-request';
|
||||
import { loadTransactionList } from '../../services/rest/apm';
|
||||
|
@ -15,19 +14,40 @@ const ID = 'transactionList';
|
|||
const INITIAL_DATA = [];
|
||||
const withInitialData = createInitialDataSelector(INITIAL_DATA);
|
||||
|
||||
const getRelativeImpact = (impact, impactMin, impactMax) =>
|
||||
Math.max((impact - impactMin) / Math.max(impactMax - impactMin, 1) * 100, 1);
|
||||
|
||||
function getWithRelativeImpact(items) {
|
||||
const impacts = items.map(({ impact }) => impact);
|
||||
const impactMin = Math.min(...impacts);
|
||||
const impactMax = Math.max(...impacts);
|
||||
|
||||
return items.map(item => {
|
||||
return {
|
||||
...item,
|
||||
impactRelative: getRelativeImpact(item.impact, impactMin, impactMax)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export const getTransactionList = createSelector(
|
||||
state => withInitialData(state.reactReduxRequest[ID]),
|
||||
state => state.sorting.transaction,
|
||||
(transactionList = {}, transactionSorting) => {
|
||||
const { key: sortKey, descending } = transactionSorting;
|
||||
|
||||
transactionList => {
|
||||
return {
|
||||
...transactionList,
|
||||
data: orderBy(transactionList.data, sortKey, descending ? 'desc' : 'asc')
|
||||
data: getWithRelativeImpact(transactionList.data)
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// export function getTransactionList(state) {
|
||||
// const transactionList = withInitialData(state.reactReduxRequest[ID]);
|
||||
// return {
|
||||
// ...transactionList,
|
||||
|
||||
// };
|
||||
// }
|
||||
|
||||
export function TransactionListRequest({ urlParams, render }) {
|
||||
const { serviceName, start, end, transactionType, kuery } = urlParams;
|
||||
|
||||
|
|
|
@ -6,13 +6,11 @@
|
|||
|
||||
import { combineReducers } from 'redux';
|
||||
import location from './location';
|
||||
import sorting from './sorting';
|
||||
import urlParams from './urlParams';
|
||||
import { reducer } from 'react-redux-request';
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
location,
|
||||
sorting,
|
||||
urlParams,
|
||||
reactReduxRequest: reducer
|
||||
});
|
||||
|
|
|
@ -1,57 +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.
|
||||
*/
|
||||
|
||||
export const SORTING_CHANGE = 'SORTING_CHANGE';
|
||||
|
||||
const INITIAL_STATE = {
|
||||
transaction: {
|
||||
key: 'impact',
|
||||
descending: true
|
||||
},
|
||||
service: {
|
||||
key: 'serviceName',
|
||||
descending: false
|
||||
}
|
||||
};
|
||||
|
||||
const INITIALLY_DESCENDING = {
|
||||
transaction: ['name'],
|
||||
service: ['serviceName']
|
||||
};
|
||||
|
||||
function getSorting(state, action) {
|
||||
const isInitiallyDescending = INITIALLY_DESCENDING[action.section].includes(
|
||||
action.key
|
||||
);
|
||||
const descending =
|
||||
state.key === action.key ? !state.descending : isInitiallyDescending;
|
||||
return { ...state, key: action.key, descending };
|
||||
}
|
||||
|
||||
export default function sorting(state = INITIAL_STATE, action) {
|
||||
switch (action.type) {
|
||||
case SORTING_CHANGE: {
|
||||
return {
|
||||
...state,
|
||||
[action.section]: getSorting(state[action.section], action)
|
||||
};
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export const changeTransactionSorting = key => ({
|
||||
type: SORTING_CHANGE,
|
||||
key,
|
||||
section: 'transaction'
|
||||
});
|
||||
|
||||
export const changeServiceSorting = key => ({
|
||||
type: SORTING_CHANGE,
|
||||
key,
|
||||
section: 'service'
|
||||
});
|
|
@ -26,6 +26,7 @@ function urlParams(state = {}, action) {
|
|||
switch (action.type) {
|
||||
case LOCATION_UPDATE: {
|
||||
const {
|
||||
processorEvent,
|
||||
serviceName,
|
||||
transactionType,
|
||||
transactionName,
|
||||
|
@ -37,9 +38,8 @@ function urlParams(state = {}, action) {
|
|||
detailTab,
|
||||
spanId,
|
||||
page,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
q,
|
||||
sortDirection,
|
||||
sortField,
|
||||
kuery
|
||||
} = toQuery(action.location.search);
|
||||
|
||||
|
@ -47,9 +47,8 @@ function urlParams(state = {}, action) {
|
|||
...state,
|
||||
|
||||
// query params
|
||||
q,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
sortDirection,
|
||||
sortField,
|
||||
page: toNumber(page) || 0,
|
||||
transactionId,
|
||||
detailTab,
|
||||
|
@ -57,6 +56,7 @@ function urlParams(state = {}, action) {
|
|||
kuery: legacyDecodeURIComponent(kuery),
|
||||
|
||||
// path params
|
||||
processorEvent,
|
||||
serviceName,
|
||||
transactionType: legacyDecodeURIComponent(transactionType),
|
||||
transactionName: legacyDecodeURIComponent(transactionName),
|
||||
|
@ -89,12 +89,14 @@ function getPathParams(pathname) {
|
|||
switch (pageName) {
|
||||
case 'transactions':
|
||||
return {
|
||||
processorEvent: 'transaction',
|
||||
serviceName: paths[0],
|
||||
transactionType: paths[2],
|
||||
transactionName: paths[3]
|
||||
};
|
||||
case 'errors':
|
||||
return {
|
||||
processorEvent: 'error',
|
||||
serviceName: paths[0],
|
||||
errorGroupId: paths[2]
|
||||
};
|
||||
|
|
|
@ -90,3 +90,8 @@ export function truncate(width) {
|
|||
text-overflow: ellipsis;
|
||||
`;
|
||||
}
|
||||
|
||||
// height of specific elements
|
||||
export const elements = {
|
||||
topNav: '29px'
|
||||
};
|
||||
|
|
|
@ -133,15 +133,11 @@ export const KibanaLink = withLocation(KibanaLinkComponent);
|
|||
// Angular decodes encoded url tokens like "%2F" to "/" which causes the route to change.
|
||||
// It was supposedly fixed in https://github.com/angular/angular.js/commit/1b779028fdd339febaa1fff5f3bd4cfcda46cc09 but still seeing the issue
|
||||
export function legacyEncodeURIComponent(url) {
|
||||
return (
|
||||
url && encodeURIComponent(url.replace(/\//g, '~2F').replace(/ /g, '~20'))
|
||||
);
|
||||
return url && encodeURIComponent(url).replace(/%/g, '~');
|
||||
}
|
||||
|
||||
export function legacyDecodeURIComponent(url) {
|
||||
return (
|
||||
url && decodeURIComponent(url.replace(/~2F/g, '/').replace(/~20/g, ' '))
|
||||
);
|
||||
return url && decodeURIComponent(url.replace(/~/g, '%'));
|
||||
}
|
||||
|
||||
export function ExternalLink(props) {
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
} from '../../../../common/constants';
|
||||
|
||||
async function getSpans({ transactionId, setup }) {
|
||||
const { start, end, esFilterQuery, client, config } = setup;
|
||||
const { start, end, client, config } = setup;
|
||||
|
||||
const params = {
|
||||
index: config.get('xpack.apm.indexPattern'),
|
||||
|
@ -47,10 +47,6 @@ async function getSpans({ transactionId, setup }) {
|
|||
}
|
||||
};
|
||||
|
||||
if (esFilterQuery) {
|
||||
params.body.query.bool.filter.push(esFilterQuery);
|
||||
}
|
||||
|
||||
const resp = await client('search', params);
|
||||
return {
|
||||
span_types: resp.aggregations.types.buckets.map(bucket => ({
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -5921,6 +5921,10 @@ pngjs@3.3.1, pngjs@^3.0.0:
|
|||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.3.1.tgz#8e14e6679ee7424b544334c3b2d21cea6d8c209a"
|
||||
|
||||
polished@^1.9.2:
|
||||
version "1.9.2"
|
||||
resolved "https://registry.yarnpkg.com/polished/-/polished-1.9.2.tgz#d705cac66f3a3ed1bd38aad863e2c1e269baf6b6"
|
||||
|
||||
popper.js@^1.14.1:
|
||||
version "1.14.3"
|
||||
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.3.tgz#1438f98d046acf7b4d78cd502bf418ac64d4f095"
|
||||
|
|
|
@ -10123,6 +10123,10 @@ podium@3.x.x:
|
|||
hoek "5.x.x"
|
||||
joi "13.x.x"
|
||||
|
||||
polished@^1.9.2:
|
||||
version "1.9.3"
|
||||
resolved "https://registry.yarnpkg.com/polished/-/polished-1.9.3.tgz#d61b8a0c4624efe31e2583ff24a358932b6b75e1"
|
||||
|
||||
popper.js@^1.14.1:
|
||||
version "1.14.3"
|
||||
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.3.tgz#1438f98d046acf7b4d78cd502bf418ac64d4f095"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue