mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
Replace api calls with ReduxRequest (#18815)
This commit is contained in:
parent
3028a6fd80
commit
14876364c7
74 changed files with 1337 additions and 1754 deletions
|
@ -4,22 +4,50 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import Distribution from './view';
|
||||
import { getUrlParams } from '../../../../store/urlParams';
|
||||
import {
|
||||
loadErrorDistribution,
|
||||
getErrorDistribution
|
||||
} from '../../../../store/errorDistribution';
|
||||
import React from 'react';
|
||||
import Histogram from '../../../shared/charts/Histogram';
|
||||
import EmptyMessage from '../../../shared/EmptyMessage';
|
||||
import { HeaderSmall } from '../../../shared/UIComponents';
|
||||
|
||||
function mapStateToProps(state = {}) {
|
||||
return {
|
||||
urlParams: getUrlParams(state),
|
||||
distribution: getErrorDistribution(state)
|
||||
};
|
||||
export function getFormattedBuckets(buckets, bucketSize) {
|
||||
if (!buckets) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return buckets.map(({ count, key }) => {
|
||||
return {
|
||||
x0: key,
|
||||
x: key + bucketSize,
|
||||
y: count
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
loadErrorDistribution
|
||||
};
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Distribution);
|
||||
function Distribution({ distribution }) {
|
||||
const buckets = getFormattedBuckets(
|
||||
distribution.buckets,
|
||||
distribution.bucketSize
|
||||
);
|
||||
|
||||
const isEmpty = distribution.totalHits === 0;
|
||||
|
||||
if (isEmpty) {
|
||||
return <EmptyMessage heading="No errors in the selected time range." />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<HeaderSmall>Occurrences</HeaderSmall>
|
||||
<Histogram
|
||||
verticalLineHover={bucket => bucket.x}
|
||||
xType="time"
|
||||
buckets={buckets}
|
||||
bucketSize={distribution.bucketSize}
|
||||
formatYShort={value => `${value} occ.`}
|
||||
formatYLong={value => `${value} occurrences`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Distribution;
|
||||
|
|
|
@ -1,76 +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 Histogram from '../../../shared/charts/Histogram';
|
||||
import EmptyMessage from '../../../shared/EmptyMessage';
|
||||
import { HeaderSmall } from '../../../shared/UIComponents';
|
||||
import { getKey } from '../../../../store/apiHelpers';
|
||||
|
||||
export function getFormattedBuckets(buckets, bucketSize) {
|
||||
if (!buckets) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return buckets.map(({ count, key }) => {
|
||||
return {
|
||||
x0: key,
|
||||
x: key + bucketSize,
|
||||
y: count
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function maybeLoadErrorDistribution(props) {
|
||||
const { serviceName, start, end, errorGroupId } = props.urlParams;
|
||||
const keyArgs = { serviceName, start, end, errorGroupId };
|
||||
const key = getKey(keyArgs);
|
||||
|
||||
//TODO what about load status? `props.distribution.status`
|
||||
if (key && props.distribution.key !== key) {
|
||||
props.loadErrorDistribution(keyArgs);
|
||||
}
|
||||
}
|
||||
|
||||
class Distribution extends Component {
|
||||
componentDidMount() {
|
||||
maybeLoadErrorDistribution(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
maybeLoadErrorDistribution(nextProps);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { distribution } = this.props;
|
||||
const buckets = getFormattedBuckets(
|
||||
distribution.data.buckets,
|
||||
distribution.data.bucketSize
|
||||
);
|
||||
|
||||
const isEmpty = distribution.data.totalHits === 0;
|
||||
|
||||
if (isEmpty) {
|
||||
return <EmptyMessage heading="No errors in the selected time range." />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<HeaderSmall>Occurrences</HeaderSmall>
|
||||
<Histogram
|
||||
verticalLineHover={bucket => bucket.x}
|
||||
xType="time"
|
||||
buckets={buckets}
|
||||
bucketSize={distribution.data.bucketSize}
|
||||
formatYShort={value => `${value} occ.`}
|
||||
formatYLong={value => `${value} occurrences`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Distribution;
|
|
@ -7,18 +7,14 @@
|
|||
import { connect } from 'react-redux';
|
||||
import ErrorGroupDetails from './view';
|
||||
import { getUrlParams } from '../../../store/urlParams';
|
||||
import { getErrorGroup, loadErrorGroup } from '../../../store/errorGroup';
|
||||
|
||||
function mapStateToProps(state = {}) {
|
||||
return {
|
||||
urlParams: getUrlParams(state),
|
||||
errorGroup: getErrorGroup(state),
|
||||
location: state.location
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
loadErrorGroup
|
||||
};
|
||||
const mapDispatchToProps = {};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ErrorGroupDetails);
|
||||
|
|
|
@ -4,11 +4,10 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { Fragment, Component } from 'react';
|
||||
import React, { Fragment } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import PropTypes from 'prop-types';
|
||||
import { get } from 'lodash';
|
||||
import withErrorHandler from '../../shared/withErrorHandler';
|
||||
import { HeaderLarge } from '../../shared/UIComponents';
|
||||
import DetailView from './DetailView';
|
||||
import Distribution from './Distribution';
|
||||
|
@ -28,7 +27,8 @@ import {
|
|||
ERROR_EXC_MESSAGE,
|
||||
ERROR_EXC_HANDLED
|
||||
} from '../../../../common/constants';
|
||||
import { getKey } from '../../../store/apiHelpers';
|
||||
import { ErrorDistributionRequest } from '../../../store/reduxRequest/errorDistribution';
|
||||
import { ErrorGroupDetailsRequest } from '../../../store/reduxRequest/errorGroup';
|
||||
|
||||
const Titles = styled.div`
|
||||
margin-bottom: ${px(units.plus)};
|
||||
|
@ -56,14 +56,6 @@ const Culprit = styled.div`
|
|||
font-family: ${fontFamilyCode};
|
||||
`;
|
||||
|
||||
function loadErrorGroup(props) {
|
||||
const { serviceName, errorGroupId, start, end } = props.urlParams;
|
||||
const key = getKey({ serviceName, errorGroupId, start, end });
|
||||
if (key && props.errorGroup.key !== key) {
|
||||
props.loadErrorGroup({ serviceName, errorGroupId, start, end });
|
||||
}
|
||||
}
|
||||
|
||||
function getShortGroupId(errorGroupId) {
|
||||
if (!errorGroupId) {
|
||||
return 'N/A';
|
||||
|
@ -72,65 +64,64 @@ function getShortGroupId(errorGroupId) {
|
|||
return errorGroupId.slice(0, 5);
|
||||
}
|
||||
|
||||
class ErrorGroupDetails extends Component {
|
||||
componentDidMount() {
|
||||
loadErrorGroup(this.props);
|
||||
}
|
||||
function ErrorGroupDetails({ urlParams, location }) {
|
||||
return (
|
||||
<ErrorGroupDetailsRequest
|
||||
urlParams={urlParams}
|
||||
render={errorGroup => {
|
||||
// If there are 0 occurrences, show only distribution chart w. empty message
|
||||
const showDetails = errorGroup.data.occurrencesCount !== 0;
|
||||
const logMessage = get(errorGroup.data.error, ERROR_LOG_MESSAGE);
|
||||
const excMessage = get(errorGroup.data.error, ERROR_EXC_MESSAGE);
|
||||
const culprit = get(errorGroup.data.error, ERROR_CULPRIT);
|
||||
const isUnhandled =
|
||||
get(errorGroup.data.error, ERROR_EXC_HANDLED) === false;
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
loadErrorGroup(nextProps);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { errorGroup, urlParams, location } = this.props;
|
||||
|
||||
// If there are 0 occurrences, show only distribution chart w. empty message
|
||||
const showDetails = errorGroup.data.occurrencesCount !== 0;
|
||||
const logMessage = get(errorGroup.data.error, ERROR_LOG_MESSAGE);
|
||||
const excMessage = get(errorGroup.data.error, ERROR_EXC_MESSAGE);
|
||||
const culprit = get(errorGroup.data.error, ERROR_CULPRIT);
|
||||
const isUnhandled = get(errorGroup.data.error, ERROR_EXC_HANDLED) === false;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<HeaderLarge>
|
||||
Error group {getShortGroupId(urlParams.errorGroupId)}
|
||||
{isUnhandled && (
|
||||
<UnhandledBadge color="warning">Unhandled</UnhandledBadge>
|
||||
)}
|
||||
</HeaderLarge>
|
||||
|
||||
{showDetails && (
|
||||
<Titles>
|
||||
<EuiText>
|
||||
{logMessage && (
|
||||
<Fragment>
|
||||
<Label>Log message</Label>
|
||||
<Message>{logMessage}</Message>
|
||||
</Fragment>
|
||||
return (
|
||||
<div>
|
||||
<HeaderLarge>
|
||||
Error group {getShortGroupId(urlParams.errorGroupId)}
|
||||
{isUnhandled && (
|
||||
<UnhandledBadge color="warning">Unhandled</UnhandledBadge>
|
||||
)}
|
||||
<Label>Exception message</Label>
|
||||
<Message>{excMessage || 'N/A'}</Message>
|
||||
<Label>Culprit</Label>
|
||||
<Culprit>{culprit || 'N/A'}</Culprit>
|
||||
</EuiText>
|
||||
</Titles>
|
||||
)}
|
||||
<Distribution />
|
||||
{showDetails && (
|
||||
<DetailView
|
||||
errorGroup={errorGroup}
|
||||
urlParams={urlParams}
|
||||
location={location}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
</HeaderLarge>
|
||||
|
||||
{showDetails && (
|
||||
<Titles>
|
||||
<EuiText>
|
||||
{logMessage && (
|
||||
<Fragment>
|
||||
<Label>Log message</Label>
|
||||
<Message>{logMessage}</Message>
|
||||
</Fragment>
|
||||
)}
|
||||
<Label>Exception message</Label>
|
||||
<Message>{excMessage || 'N/A'}</Message>
|
||||
<Label>Culprit</Label>
|
||||
<Culprit>{culprit || 'N/A'}</Culprit>
|
||||
</EuiText>
|
||||
</Titles>
|
||||
)}
|
||||
<ErrorDistributionRequest
|
||||
urlParams={urlParams}
|
||||
render={({ data }) => <Distribution distribution={data} />}
|
||||
/>
|
||||
{showDetails && (
|
||||
<DetailView
|
||||
errorGroup={errorGroup}
|
||||
urlParams={urlParams}
|
||||
location={location}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
ErrorGroupDetails.propTypes = {
|
||||
location: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default withErrorHandler(ErrorGroupDetails, ['errorGroup']);
|
||||
export default ErrorGroupDetails;
|
||||
|
|
|
@ -444,7 +444,7 @@ export default class WatcherFlyout extends Component {
|
|||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{this.props.isFlyoutOpen && flyout}
|
||||
{this.props.isOpen && flyout}
|
||||
<EuiGlobalToastList
|
||||
toasts={this.state.toasts}
|
||||
dismissToast={this.removeToasts}
|
||||
|
@ -456,7 +456,7 @@ export default class WatcherFlyout extends Component {
|
|||
}
|
||||
|
||||
WatcherFlyout.propTypes = {
|
||||
isFlyoutOpen: PropTypes.bool.isRequired,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
serviceName: PropTypes.string,
|
||||
onClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
|
|
@ -7,22 +7,16 @@
|
|||
import { connect } from 'react-redux';
|
||||
import ErrorGroupOverview from './view';
|
||||
import { getUrlParams } from '../../../store/urlParams';
|
||||
import {
|
||||
getErrorGroupList,
|
||||
loadErrorGroupList
|
||||
} from '../../../store/errorGroupList';
|
||||
import { getLicense } from '../../../store/reduxRequest/license';
|
||||
|
||||
function mapStateToProps(state = {}) {
|
||||
return {
|
||||
urlParams: getUrlParams(state),
|
||||
errorGroupList: getErrorGroupList(state),
|
||||
location: state.location,
|
||||
license: state.license
|
||||
license: getLicense(state)
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
loadErrorGroupList
|
||||
};
|
||||
const mapDispatchToProps = {};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ErrorGroupOverview);
|
||||
|
|
|
@ -6,30 +6,12 @@
|
|||
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import withErrorHandler from '../../shared/withErrorHandler';
|
||||
import { HeaderContainer } from '../../shared/UIComponents';
|
||||
import TabNavigation from '../../shared/TabNavigation';
|
||||
import List from './List';
|
||||
import { getKey } from '../../../store/apiHelpers';
|
||||
import WatcherFlyout from './Watcher/WatcherFlyOut';
|
||||
import OpenWatcherDialogButton from './Watcher/OpenWatcherDialogButton';
|
||||
|
||||
function maybeLoadList(props) {
|
||||
const { serviceName, start, end, q, sortBy, sortOrder } = props.urlParams;
|
||||
const keyArgs = {
|
||||
serviceName,
|
||||
start,
|
||||
end,
|
||||
q,
|
||||
sortBy,
|
||||
sortOrder
|
||||
};
|
||||
const key = getKey(keyArgs, false);
|
||||
|
||||
if (serviceName && start && end && props.errorGroupList.key !== key) {
|
||||
props.loadErrorGroupList(keyArgs);
|
||||
}
|
||||
}
|
||||
import { ErrorGroupDetailsRequest } from '../../../store/reduxRequest/errorGroupList';
|
||||
|
||||
class ErrorGroupOverview extends Component {
|
||||
state = {
|
||||
|
@ -44,17 +26,9 @@ class ErrorGroupOverview extends Component {
|
|||
this.setState({ isFlyoutOpen: false });
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
maybeLoadList(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
maybeLoadList(nextProps);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { license, location } = this.props;
|
||||
const { serviceName } = this.props.urlParams;
|
||||
const { license, location, urlParams } = this.props;
|
||||
const { serviceName } = urlParams;
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
@ -67,15 +41,16 @@ class ErrorGroupOverview extends Component {
|
|||
|
||||
<TabNavigation />
|
||||
|
||||
<List
|
||||
urlParams={this.props.urlParams}
|
||||
items={this.props.errorGroupList.data}
|
||||
location={location}
|
||||
<ErrorGroupDetailsRequest
|
||||
urlParams={urlParams}
|
||||
render={({ data }) => (
|
||||
<List urlParams={urlParams} items={data} location={location} />
|
||||
)}
|
||||
/>
|
||||
|
||||
<WatcherFlyout
|
||||
serviceName={serviceName}
|
||||
isFlyoutOpen={this.state.isFlyoutOpen}
|
||||
isOpen={this.state.isFlyoutOpen}
|
||||
onClose={this.onCloseFlyout}
|
||||
/>
|
||||
</div>
|
||||
|
@ -87,4 +62,4 @@ ErrorGroupOverview.propTypes = {
|
|||
location: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default withErrorHandler(ErrorGroupOverview, ['errorGroupList']);
|
||||
export default ErrorGroupOverview;
|
||||
|
|
|
@ -10,7 +10,10 @@ import { some, get } from 'lodash';
|
|||
import { STATUS } from '../../../../constants/index';
|
||||
|
||||
function getIsLoading(state) {
|
||||
return some(state, subState => get(subState, 'status') === STATUS.LOADING);
|
||||
return some(
|
||||
state.reduxRequest,
|
||||
subState => get(subState, 'status') === STATUS.LOADING
|
||||
);
|
||||
}
|
||||
|
||||
function mapStateToProps(state = {}) {
|
||||
|
|
|
@ -5,12 +5,14 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiProgress } from '@elastic/eui';
|
||||
import { EuiProgress, EuiDelayHide } from '@elastic/eui';
|
||||
|
||||
export default ({ isLoading }) => {
|
||||
if (!isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <EuiProgress size="xs" position="fixed" />;
|
||||
return (
|
||||
<EuiDelayHide
|
||||
hide={!isLoading}
|
||||
minimumDuration={1000}
|
||||
render={() => <EuiProgress size="xs" position="fixed" />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -3,18 +3,21 @@
|
|||
* 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 { STATUS } from '../../../../constants/index';
|
||||
import { LicenceRequest } from '../../../../store/reduxRequest/license';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import LicenseChecker from './view';
|
||||
import { loadLicense } from '../../../../store/license';
|
||||
|
||||
function mapStateToProps(state = {}) {
|
||||
return {
|
||||
license: state.license
|
||||
};
|
||||
function LicenseChecker() {
|
||||
return (
|
||||
<LicenceRequest
|
||||
render={({ data, status }) => {
|
||||
if (status === STATUS.SUCCESS && !data.license.isActive) {
|
||||
window.location = '#/invalid-license';
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
loadLicense
|
||||
};
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(LicenseChecker);
|
||||
export default LicenseChecker;
|
||||
|
|
|
@ -1,37 +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 { Component } from 'react';
|
||||
import { STATUS } from '../../../../constants/index';
|
||||
|
||||
function maybeLoadLicense(props) {
|
||||
if (!props.license.status) {
|
||||
props.loadLicense();
|
||||
}
|
||||
}
|
||||
|
||||
class LicenseChecker extends Component {
|
||||
componentDidMount() {
|
||||
maybeLoadLicense(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (
|
||||
nextProps.license.status === STATUS.SUCCESS &&
|
||||
!nextProps.license.data.license.isActive
|
||||
) {
|
||||
window.location = '#/invalid-license';
|
||||
}
|
||||
|
||||
maybeLoadLicense(nextProps);
|
||||
}
|
||||
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default LicenseChecker;
|
|
@ -8,8 +8,6 @@ import React from 'react';
|
|||
import styled from 'styled-components';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
import { routes } from './routeConfig';
|
||||
import GlobalProgess from './GlobalProgess';
|
||||
import LicenseChecker from './LicenseChecker';
|
||||
import ScrollToTopOnPathChange from './ScrollToTopOnPathChange';
|
||||
import { px, units, unit } from '../../../style/variables';
|
||||
import ConnectRouterToRedux from '../../shared/ConnectRouterToRedux';
|
||||
|
@ -22,8 +20,6 @@ const MainContainer = styled.div`
|
|||
export default function Main() {
|
||||
return (
|
||||
<MainContainer>
|
||||
<GlobalProgess />
|
||||
<LicenseChecker />
|
||||
<Route component={ConnectRouterToRedux} />
|
||||
<Route component={ScrollToTopOnPathChange} />
|
||||
{routes.map((route, i) => {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import FilterableAPMTable from '../../../shared/APMTable/FilterableAPMTable';
|
||||
import { AlignmentKuiTableHeaderCell } from '../../../shared/APMTable/APMTable';
|
||||
|
||||
|
@ -79,4 +79,12 @@ class List extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
List.propTypes = {
|
||||
items: PropTypes.array
|
||||
};
|
||||
|
||||
List.defaultProps = {
|
||||
items: []
|
||||
};
|
||||
|
||||
export default List;
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { connect } from 'react-redux';
|
||||
import ServiceOverview from './view';
|
||||
import { loadServiceList, getServiceList } from '../../../store/serviceList';
|
||||
import { getServiceList } from '../../../store/reduxRequest/serviceList';
|
||||
import { getUrlParams } from '../../../store/urlParams';
|
||||
import sorting, { changeServiceSorting } from '../../../store/sorting';
|
||||
|
||||
|
@ -19,7 +19,6 @@ function mapStateToProps(state = {}) {
|
|||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
loadServiceList,
|
||||
changeServiceSorting
|
||||
};
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ServiceOverview);
|
||||
|
|
|
@ -5,52 +5,40 @@
|
|||
*/
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import withErrorHandler from '../../shared/withErrorHandler';
|
||||
import { STATUS } from '../../../constants';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { loadAgentStatus } from '../../../services/rest';
|
||||
import { KibanaLink } from '../../../utils/url';
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
import List from './List';
|
||||
import { getKey } from '../../../store/apiHelpers';
|
||||
import { HeaderContainer } from '../../shared/UIComponents';
|
||||
|
||||
function fetchData(props) {
|
||||
const { start, end } = props.urlParams;
|
||||
const key = getKey({ start, end });
|
||||
|
||||
if (key && props.serviceList.key !== key) {
|
||||
props.loadServiceList({ start, end });
|
||||
}
|
||||
}
|
||||
import { ServiceListRequest } from '../../../store/reduxRequest/serviceList';
|
||||
|
||||
class ServiceOverview extends Component {
|
||||
state = {
|
||||
noHistoricalDataFound: false
|
||||
};
|
||||
|
||||
checkForHistoricalData({ serviceList }) {
|
||||
async checkForHistoricalData({ serviceList }) {
|
||||
if (serviceList.status === STATUS.SUCCESS && isEmpty(serviceList.data)) {
|
||||
loadAgentStatus().then(result => {
|
||||
if (!result.dataFound) {
|
||||
this.setState({ noHistoricalDataFound: true });
|
||||
}
|
||||
});
|
||||
const result = await loadAgentStatus();
|
||||
if (!result.dataFound) {
|
||||
this.setState({ noHistoricalDataFound: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
fetchData(this.props);
|
||||
this.checkForHistoricalData(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
fetchData(nextProps);
|
||||
this.checkForHistoricalData(nextProps);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { serviceList, changeServiceSorting, serviceSorting } = this.props;
|
||||
const { changeServiceSorting, serviceSorting, urlParams } = this.props;
|
||||
const { noHistoricalDataFound } = this.state;
|
||||
|
||||
const emptyMessageHeading = noHistoricalDataFound
|
||||
|
@ -68,12 +56,17 @@ class ServiceOverview extends Component {
|
|||
<SetupInstructionsLink />
|
||||
</HeaderContainer>
|
||||
|
||||
<List
|
||||
items={serviceList.data}
|
||||
changeServiceSorting={changeServiceSorting}
|
||||
serviceSorting={serviceSorting}
|
||||
emptyMessageHeading={emptyMessageHeading}
|
||||
emptyMessageSubHeading={emptyMessageSubHeading}
|
||||
<ServiceListRequest
|
||||
urlParams={urlParams}
|
||||
render={({ data }) => (
|
||||
<List
|
||||
items={data}
|
||||
changeServiceSorting={changeServiceSorting}
|
||||
serviceSorting={serviceSorting}
|
||||
emptyMessageHeading={emptyMessageHeading}
|
||||
emptyMessageSubHeading={emptyMessageSubHeading}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -90,4 +83,4 @@ function SetupInstructionsLink({ buttonFill = false }) {
|
|||
);
|
||||
}
|
||||
|
||||
export default withErrorHandler(ServiceOverview, ['serviceList']);
|
||||
export default ServiceOverview;
|
||||
|
|
|
@ -1,54 +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 { connect } from 'react-redux';
|
||||
import Charts from '../../../shared/charts/TransactionCharts';
|
||||
import { getUrlParams } from '../../../../store/urlParams';
|
||||
import {
|
||||
getDetailsCharts,
|
||||
loadDetailsCharts
|
||||
} from '../../../../store/detailsCharts';
|
||||
import { getKey } from '../../../../store/apiHelpers';
|
||||
|
||||
function mapStateToProps(state = {}) {
|
||||
return {
|
||||
urlParams: getUrlParams(state),
|
||||
charts: getDetailsCharts(state)
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
loadCharts: props => {
|
||||
const {
|
||||
serviceName,
|
||||
start,
|
||||
end,
|
||||
transactionType,
|
||||
transactionName
|
||||
} = props.urlParams;
|
||||
const key = getKey({
|
||||
serviceName,
|
||||
start,
|
||||
end,
|
||||
transactionType,
|
||||
transactionName
|
||||
});
|
||||
|
||||
if (key && props.charts.key !== key) {
|
||||
dispatch(
|
||||
loadDetailsCharts({
|
||||
serviceName,
|
||||
start,
|
||||
end,
|
||||
transactionType,
|
||||
transactionName
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Charts);
|
|
@ -7,21 +7,13 @@
|
|||
import { connect } from 'react-redux';
|
||||
import Distribution from './view';
|
||||
import { getUrlParams } from '../../../../store/urlParams';
|
||||
import {
|
||||
loadTransactionDistribution,
|
||||
getTransactionDistribution
|
||||
} from '../../../../store/transactionDistribution';
|
||||
|
||||
function mapStateToProps(state = {}) {
|
||||
return {
|
||||
urlParams: getUrlParams(state),
|
||||
distribution: getTransactionDistribution(state),
|
||||
location: state.location
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
loadTransactionDistribution
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {};
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Distribution);
|
||||
|
|
|
@ -13,7 +13,6 @@ import { HeaderSmall } from '../../../shared/UIComponents';
|
|||
import EmptyMessage from '../../../shared/EmptyMessage';
|
||||
import { getTimeFormatter, timeUnit } from '../../../../utils/formatters';
|
||||
import SamplingTooltip from './SamplingTooltip';
|
||||
import { getKey } from '../../../../store/apiHelpers';
|
||||
|
||||
export function getFormattedBuckets(buckets, bucketSize) {
|
||||
if (!buckets) {
|
||||
|
@ -32,29 +31,7 @@ export function getFormattedBuckets(buckets, bucketSize) {
|
|||
});
|
||||
}
|
||||
|
||||
function loadTransactionDistribution(props) {
|
||||
const { serviceName, start, end, transactionName } = props.urlParams;
|
||||
const key = getKey({ serviceName, start, end, transactionName });
|
||||
|
||||
if (key && props.distribution.key !== key) {
|
||||
props.loadTransactionDistribution({
|
||||
serviceName,
|
||||
start,
|
||||
end,
|
||||
transactionName
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class Distribution extends Component {
|
||||
componentDidMount() {
|
||||
loadTransactionDistribution(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
loadTransactionDistribution(nextProps);
|
||||
}
|
||||
|
||||
formatYShort = t => {
|
||||
return `${t} ${unitShort(this.props.urlParams.transactionType)}`;
|
||||
};
|
||||
|
@ -67,11 +44,11 @@ class Distribution extends Component {
|
|||
const { location, distribution } = this.props;
|
||||
|
||||
const buckets = getFormattedBuckets(
|
||||
distribution.data.buckets,
|
||||
distribution.data.bucketSize
|
||||
distribution.buckets,
|
||||
distribution.bucketSize
|
||||
);
|
||||
|
||||
const isEmpty = distribution.data.totalHits === 0;
|
||||
const isEmpty = distribution.totalHits === 0;
|
||||
const xMax = d3.max(buckets, d => d.x);
|
||||
const timeFormatter = getTimeFormatter(xMax);
|
||||
const unit = timeUnit(xMax);
|
||||
|
@ -99,7 +76,7 @@ class Distribution extends Component {
|
|||
</HeaderSmall>
|
||||
<Histogram
|
||||
buckets={buckets}
|
||||
bucketSize={distribution.data.bucketSize}
|
||||
bucketSize={distribution.bucketSize}
|
||||
bucketIndex={bucketIndex}
|
||||
onClick={bucket => {
|
||||
if (bucket.sampled && bucket.y > 0) {
|
||||
|
@ -143,7 +120,9 @@ function unitLong(type, count) {
|
|||
}
|
||||
|
||||
Distribution.propTypes = {
|
||||
location: PropTypes.object.isRequired
|
||||
urlParams: PropTypes.object.isRequired,
|
||||
location: PropTypes.object.isRequired,
|
||||
distribution: PropTypes.object
|
||||
};
|
||||
|
||||
export default Distribution;
|
||||
|
|
|
@ -7,17 +7,13 @@
|
|||
import { connect } from 'react-redux';
|
||||
import Spans from './view';
|
||||
import { getUrlParams } from '../../../../../store/urlParams';
|
||||
import { loadSpans, getSpans } from '../../../../../store/spans';
|
||||
|
||||
function mapStateToProps(state = {}) {
|
||||
return {
|
||||
urlParams: getUrlParams(state),
|
||||
spans: getSpans(state),
|
||||
location: state.location
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
loadSpans
|
||||
};
|
||||
const mapDispatchToProps = {};
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Spans);
|
||||
|
|
|
@ -17,7 +17,7 @@ import Timeline from '../../../../shared/charts/Timeline';
|
|||
import EmptyMessage from '../../../../shared/EmptyMessage';
|
||||
import { getFeatureDocs } from '../../../../../utils/documentation';
|
||||
import { ExternalLink } from '../../../../../utils/url';
|
||||
import { getKey } from '../../../../../store/apiHelpers';
|
||||
import { SpansRequest } from '../../../../../store/reduxRequest/spans';
|
||||
|
||||
const Container = styled.div`
|
||||
transition: 0.1s padding ease;
|
||||
|
@ -42,94 +42,85 @@ const TIMELINE_MARGINS = {
|
|||
};
|
||||
|
||||
class Spans extends PureComponent {
|
||||
componentDidMount() {
|
||||
loadSpans(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
loadSpans(nextProps);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { spans, agentName, urlParams, location } = this.props;
|
||||
if (isEmpty(spans.data.spans)) {
|
||||
return (
|
||||
<EmptyMessage
|
||||
heading="No spans available for this transaction."
|
||||
hideSubheading
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const spanTypes = uniq(
|
||||
spans.data.spanTypes.map(({ type }) => getPrimaryType(type))
|
||||
);
|
||||
|
||||
const getSpanColor = getColorByType(spanTypes);
|
||||
|
||||
const totalDuration = spans.data.duration;
|
||||
const spanContainerHeight = 58;
|
||||
const timelineHeight = spanContainerHeight * spans.data.spans.length;
|
||||
|
||||
const { agentName, urlParams, location, droppedSpans } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<Container>
|
||||
<StickyContainer>
|
||||
<Timeline
|
||||
header={
|
||||
<TimelineHeader
|
||||
legends={spanTypes.map(type => ({
|
||||
label: getSpanLabel(type),
|
||||
color: getSpanColor(type)
|
||||
}))}
|
||||
transactionName={urlParams.transactionName}
|
||||
/>
|
||||
}
|
||||
duration={totalDuration}
|
||||
height={timelineHeight}
|
||||
margins={TIMELINE_MARGINS}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
paddingTop: TIMELINE_MARGINS.top
|
||||
}}
|
||||
>
|
||||
{spans.data.spans.map(span => (
|
||||
<Span
|
||||
location={location}
|
||||
timelineMargins={TIMELINE_MARGINS}
|
||||
key={get({ span }, SPAN_ID)}
|
||||
color={getSpanColor(getPrimaryType(span.type))}
|
||||
span={span}
|
||||
spanTypeLabel={getSpanLabel(getPrimaryType(span.type))}
|
||||
totalDuration={totalDuration}
|
||||
isSelected={get({ span }, SPAN_ID) === urlParams.spanId}
|
||||
/>
|
||||
))}
|
||||
<SpansRequest
|
||||
urlParams={urlParams}
|
||||
render={spans => {
|
||||
if (isEmpty(spans.data.spans)) {
|
||||
return (
|
||||
<EmptyMessage
|
||||
heading="No spans available for this transaction."
|
||||
hideSubheading
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const spanTypes = uniq(
|
||||
spans.data.spanTypes.map(({ type }) => getPrimaryType(type))
|
||||
);
|
||||
|
||||
const getSpanColor = getColorByType(spanTypes);
|
||||
|
||||
const totalDuration = spans.data.duration;
|
||||
const spanContainerHeight = 58;
|
||||
const timelineHeight = spanContainerHeight * spans.data.spans.length;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Container>
|
||||
<StickyContainer>
|
||||
<Timeline
|
||||
header={
|
||||
<TimelineHeader
|
||||
legends={spanTypes.map(type => ({
|
||||
label: getSpanLabel(type),
|
||||
color: getSpanColor(type)
|
||||
}))}
|
||||
transactionName={urlParams.transactionName}
|
||||
/>
|
||||
}
|
||||
duration={totalDuration}
|
||||
height={timelineHeight}
|
||||
margins={TIMELINE_MARGINS}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
paddingTop: TIMELINE_MARGINS.top
|
||||
}}
|
||||
>
|
||||
{spans.data.spans.map(span => (
|
||||
<Span
|
||||
location={location}
|
||||
timelineMargins={TIMELINE_MARGINS}
|
||||
key={get({ span }, SPAN_ID)}
|
||||
color={getSpanColor(getPrimaryType(span.type))}
|
||||
span={span}
|
||||
spanTypeLabel={getSpanLabel(getPrimaryType(span.type))}
|
||||
totalDuration={totalDuration}
|
||||
isSelected={get({ span }, SPAN_ID) === urlParams.spanId}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</StickyContainer>
|
||||
</Container>
|
||||
|
||||
{droppedSpans > 0 && (
|
||||
<DroppedSpansContainer>
|
||||
{droppedSpans} spans dropped due to limit of{' '}
|
||||
{spans.data.spans.length}.{' '}
|
||||
<DroppedSpansDocsLink agentName={agentName} />
|
||||
</DroppedSpansContainer>
|
||||
)}
|
||||
</div>
|
||||
</StickyContainer>
|
||||
</Container>
|
||||
|
||||
{this.props.droppedSpans > 0 && (
|
||||
<DroppedSpansContainer>
|
||||
{this.props.droppedSpans} spans dropped due to limit of{' '}
|
||||
{spans.data.spans.length}.{' '}
|
||||
<DroppedSpansDocsLink agentName={agentName} />
|
||||
</DroppedSpansContainer>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function loadSpans(props) {
|
||||
const { serviceName, start, end, transactionId } = props.urlParams;
|
||||
const key = getKey({ serviceName, start, end, transactionId });
|
||||
if (key && props.spans.key !== key) {
|
||||
props.loadSpans({ serviceName, start, end, transactionId });
|
||||
}
|
||||
}
|
||||
|
||||
function DroppedSpansDocsLink({ agentName }) {
|
||||
const docs = getFeatureDocs('dropped-spans', agentName);
|
||||
|
||||
|
@ -185,7 +176,10 @@ function getPrimaryType(type) {
|
|||
}
|
||||
|
||||
Spans.propTypes = {
|
||||
location: PropTypes.object.isRequired
|
||||
location: PropTypes.object.isRequired,
|
||||
agentName: PropTypes.string.isRequired,
|
||||
urlParams: PropTypes.object.isRequired,
|
||||
droppedSpans: PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
export default Spans;
|
||||
|
|
|
@ -6,18 +6,12 @@
|
|||
|
||||
import { connect } from 'react-redux';
|
||||
import Transaction from './view';
|
||||
import { getUrlParams } from '../../../../store/urlParams';
|
||||
import { loadTransaction, getTransaction } from '../../../../store/transaction';
|
||||
|
||||
function mapStateToProps(state = {}) {
|
||||
return {
|
||||
urlParams: getUrlParams(state),
|
||||
transaction: getTransaction(state),
|
||||
location: state.location
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
loadTransaction
|
||||
};
|
||||
const mapDispatchToProps = {};
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Transaction);
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
unit,
|
||||
|
@ -31,16 +32,6 @@ import {
|
|||
import { fromQuery, toQuery, history } from '../../../../utils/url';
|
||||
import { asTime } from '../../../../utils/formatters';
|
||||
import EmptyMessage from '../../../shared/EmptyMessage';
|
||||
import { getKey } from '../../../../store/apiHelpers';
|
||||
|
||||
function loadTransaction(props) {
|
||||
const { serviceName, start, end, transactionId } = props.urlParams;
|
||||
const key = getKey({ serviceName, start, end, transactionId });
|
||||
|
||||
if (key && props.transaction.key !== key) {
|
||||
props.loadTransaction({ serviceName, start, end, transactionId });
|
||||
}
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
position: relative;
|
||||
|
@ -81,131 +72,129 @@ function getTabs(transactionData) {
|
|||
return getLevelOneProps(dynamicProps);
|
||||
}
|
||||
|
||||
class Transaction extends Component {
|
||||
componentDidMount() {
|
||||
loadTransaction(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
loadTransaction(nextProps);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { transaction, location } = this.props;
|
||||
const { transactionId } = this.props.urlParams;
|
||||
|
||||
if (isEmpty(transaction.data)) {
|
||||
return (
|
||||
<EmptyMessage
|
||||
heading="No transaction sample available for this time range."
|
||||
subheading="Please select another time range or another bucket from the distribution histogram."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const timestamp = get(transaction, 'data.@timestamp');
|
||||
const url = get(transaction.data, 'context.request.url.full', 'N/A');
|
||||
const duration = get(transaction.data, 'transaction.duration.us');
|
||||
const stickyProperties = [
|
||||
{
|
||||
label: 'Duration',
|
||||
fieldName: 'transaction.duration.us',
|
||||
val: duration ? asTime(duration) : 'N/A'
|
||||
},
|
||||
{
|
||||
label: 'Result',
|
||||
fieldName: 'transaction.result',
|
||||
val: get(transaction.data, 'transaction.result', 'N/A')
|
||||
},
|
||||
{
|
||||
label: 'User ID',
|
||||
fieldName: 'context.user.id',
|
||||
val: get(transaction.data, 'context.user.id', 'N/A')
|
||||
}
|
||||
];
|
||||
|
||||
const agentName = get(transaction.data, SERVICE_AGENT_NAME);
|
||||
const tabs = getTabs(transaction.data);
|
||||
const currentTab = getCurrentTab(tabs, this.props.urlParams.detailTab);
|
||||
|
||||
const discoverQuery = {
|
||||
_a: {
|
||||
interval: 'auto',
|
||||
query: {
|
||||
language: 'lucene',
|
||||
query: `${PROCESSOR_EVENT}:transaction AND ${TRANSACTION_ID}:${transactionId}`
|
||||
},
|
||||
sort: { '@timestamp': 'desc' }
|
||||
}
|
||||
};
|
||||
function Transaction({ transaction, location, urlParams }) {
|
||||
const { transactionId } = urlParams;
|
||||
|
||||
if (isEmpty(transaction)) {
|
||||
return (
|
||||
<Container>
|
||||
<HeaderContainer>
|
||||
<HeaderMedium
|
||||
css={`
|
||||
margin-top: ${px(units.quarter)};
|
||||
margin-bottom: 0;
|
||||
`}
|
||||
>
|
||||
Transaction sample
|
||||
</HeaderMedium>
|
||||
<DiscoverButton query={discoverQuery}>
|
||||
{`View transaction in Discover`}
|
||||
</DiscoverButton>
|
||||
</HeaderContainer>
|
||||
|
||||
<ContextProperties
|
||||
timestamp={timestamp}
|
||||
url={url}
|
||||
stickyProperties={stickyProperties}
|
||||
/>
|
||||
|
||||
<TabContainer>
|
||||
{[DEFAULT_TAB, ...tabs].map(key => {
|
||||
return (
|
||||
<Tab
|
||||
onClick={() => {
|
||||
history.replace({
|
||||
...location,
|
||||
search: fromQuery({
|
||||
...toQuery(location.search),
|
||||
detailTab: key
|
||||
})
|
||||
});
|
||||
}}
|
||||
selected={currentTab === key}
|
||||
key={key}
|
||||
>
|
||||
{capitalize(key)}
|
||||
</Tab>
|
||||
);
|
||||
})}
|
||||
</TabContainer>
|
||||
|
||||
<TabContentContainer>
|
||||
{currentTab === DEFAULT_TAB ? (
|
||||
<Spans
|
||||
agentName={agentName}
|
||||
droppedSpans={get(
|
||||
transaction.data,
|
||||
'transaction.spanCount.dropped.total',
|
||||
0
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<PropertiesTableContainer>
|
||||
<PropertiesTable
|
||||
propData={get(transaction.data.context, currentTab)}
|
||||
propKey={currentTab}
|
||||
agentName={agentName}
|
||||
/>
|
||||
</PropertiesTableContainer>
|
||||
)}
|
||||
</TabContentContainer>
|
||||
</Container>
|
||||
<EmptyMessage
|
||||
heading="No transaction sample available for this time range."
|
||||
subheading="Please select another time range or another bucket from the distribution histogram."
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const timestamp = get(transaction, '@timestamp');
|
||||
const url = get(transaction, 'context.request.url.full', 'N/A');
|
||||
const duration = get(transaction, 'transaction.duration.us');
|
||||
const stickyProperties = [
|
||||
{
|
||||
label: 'Duration',
|
||||
fieldName: 'transaction.duration.us',
|
||||
val: duration ? asTime(duration) : 'N/A'
|
||||
},
|
||||
{
|
||||
label: 'Result',
|
||||
fieldName: 'transaction.result',
|
||||
val: get(transaction, 'transaction.result', 'N/A')
|
||||
},
|
||||
{
|
||||
label: 'User ID',
|
||||
fieldName: 'context.user.id',
|
||||
val: get(transaction, 'context.user.id', 'N/A')
|
||||
}
|
||||
];
|
||||
|
||||
const agentName = get(transaction, SERVICE_AGENT_NAME);
|
||||
const tabs = getTabs(transaction);
|
||||
const currentTab = getCurrentTab(tabs, urlParams.detailTab);
|
||||
|
||||
const discoverQuery = {
|
||||
_a: {
|
||||
interval: 'auto',
|
||||
query: {
|
||||
language: 'lucene',
|
||||
query: `${PROCESSOR_EVENT}:transaction AND ${TRANSACTION_ID}:${transactionId}`
|
||||
},
|
||||
sort: { '@timestamp': 'desc' }
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<HeaderContainer>
|
||||
<HeaderMedium
|
||||
css={`
|
||||
margin-top: ${px(units.quarter)};
|
||||
margin-bottom: 0;
|
||||
`}
|
||||
>
|
||||
Transaction sample
|
||||
</HeaderMedium>
|
||||
<DiscoverButton query={discoverQuery}>
|
||||
{`View transaction in Discover`}
|
||||
</DiscoverButton>
|
||||
</HeaderContainer>
|
||||
|
||||
<ContextProperties
|
||||
timestamp={timestamp}
|
||||
url={url}
|
||||
stickyProperties={stickyProperties}
|
||||
/>
|
||||
|
||||
<TabContainer>
|
||||
{[DEFAULT_TAB, ...tabs].map(key => {
|
||||
return (
|
||||
<Tab
|
||||
onClick={() => {
|
||||
history.replace({
|
||||
...location,
|
||||
search: fromQuery({
|
||||
...toQuery(location.search),
|
||||
detailTab: key
|
||||
})
|
||||
});
|
||||
}}
|
||||
selected={currentTab === key}
|
||||
key={key}
|
||||
>
|
||||
{capitalize(key)}
|
||||
</Tab>
|
||||
);
|
||||
})}
|
||||
</TabContainer>
|
||||
|
||||
<TabContentContainer>
|
||||
{currentTab === DEFAULT_TAB ? (
|
||||
<Spans
|
||||
agentName={agentName}
|
||||
droppedSpans={get(
|
||||
transaction,
|
||||
'transaction.spanCount.dropped.total',
|
||||
0
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<PropertiesTableContainer>
|
||||
<PropertiesTable
|
||||
propData={get(transaction.context, currentTab)}
|
||||
propKey={currentTab}
|
||||
agentName={agentName}
|
||||
/>
|
||||
</PropertiesTableContainer>
|
||||
)}
|
||||
</TabContentContainer>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
Transaction.propTypes = {
|
||||
urlParams: PropTypes.object.isRequired,
|
||||
transaction: PropTypes.object
|
||||
};
|
||||
|
||||
Transaction.defaultProps = {
|
||||
transaction: {}
|
||||
};
|
||||
|
||||
export default Transaction;
|
||||
|
|
|
@ -8,15 +8,34 @@ import React from 'react';
|
|||
import { HeaderLarge } from '../../shared/UIComponents';
|
||||
import Transaction from './Transaction';
|
||||
import Distribution from './Distribution';
|
||||
import Charts from './Charts';
|
||||
import { DetailsChartsRequest } from '../../../store/reduxRequest/detailsCharts';
|
||||
import Charts from '../../shared/charts/TransactionCharts';
|
||||
import { TransactionDistributionRequest } from '../../../store/reduxRequest/transactionDistribution';
|
||||
import { TransactionDetailsRequest } from '../../../store/reduxRequest/transactionDetails';
|
||||
|
||||
function TransactionDetails({ urlParams }) {
|
||||
return (
|
||||
<div>
|
||||
<HeaderLarge>{urlParams.transactionName}</HeaderLarge>
|
||||
<Charts />
|
||||
<Distribution />
|
||||
<Transaction />
|
||||
|
||||
<DetailsChartsRequest
|
||||
urlParams={urlParams}
|
||||
render={({ data }) => <Charts charts={data} urlParams={urlParams} />}
|
||||
/>
|
||||
|
||||
<TransactionDistributionRequest
|
||||
urlParams={urlParams}
|
||||
render={({ data }) => (
|
||||
<Distribution distribution={data} urlParams={urlParams} />
|
||||
)}
|
||||
/>
|
||||
|
||||
<TransactionDetailsRequest
|
||||
urlParams={urlParams}
|
||||
render={({ data }) => (
|
||||
<Transaction transaction={data} urlParams={urlParams} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,36 +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 { connect } from 'react-redux';
|
||||
import Charts from '../../../shared/charts/TransactionCharts';
|
||||
import { getUrlParams } from '../../../../store/urlParams';
|
||||
import {
|
||||
getOverviewCharts,
|
||||
loadOverviewCharts
|
||||
} from '../../../../store/overviewCharts';
|
||||
import { getKey } from '../../../../store/apiHelpers';
|
||||
|
||||
function mapStateToProps(state = {}) {
|
||||
return {
|
||||
urlParams: getUrlParams(state),
|
||||
charts: getOverviewCharts(state)
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
loadCharts: props => {
|
||||
const { serviceName, start, end, transactionType } = props.urlParams;
|
||||
const key = getKey({ serviceName, start, end, transactionType });
|
||||
|
||||
if (key && props.charts.key !== key) {
|
||||
dispatch(
|
||||
loadOverviewCharts({ serviceName, start, end, transactionType })
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Charts);
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import { get } from 'lodash';
|
||||
import { TRANSACTION_ID } from '../../../../../common/constants';
|
||||
|
||||
|
@ -15,7 +15,6 @@ import { AlignmentKuiTableHeaderCell } from '../../../shared/APMTable/APMTable';
|
|||
import FilterableAPMTable from '../../../shared/APMTable/FilterableAPMTable';
|
||||
import ListItem from './ListItem';
|
||||
import ImpactTooltip from './ImpactTooltip';
|
||||
import withService from '../../../shared/withService';
|
||||
|
||||
const getRelativeImpact = (impact, impactMin, impactMax) =>
|
||||
Math.max((impact - impactMin) / Math.max(impactMax - impactMin, 1) * 100, 1);
|
||||
|
@ -31,12 +30,12 @@ function avgLabel(agentName) {
|
|||
class List extends Component {
|
||||
render() {
|
||||
const {
|
||||
serviceName,
|
||||
service,
|
||||
type,
|
||||
items,
|
||||
agentName,
|
||||
changeTransactionSorting,
|
||||
transactionSorting
|
||||
items,
|
||||
serviceName,
|
||||
transactionSorting,
|
||||
type
|
||||
} = this.props;
|
||||
|
||||
const renderHead = () => {
|
||||
|
@ -46,7 +45,7 @@ class List extends Component {
|
|||
key: 'avg',
|
||||
sortable: true,
|
||||
alignRight: true,
|
||||
label: avgLabel(service.data.agentName)
|
||||
label: avgLabel(agentName)
|
||||
},
|
||||
{
|
||||
key: 'p95',
|
||||
|
@ -129,4 +128,12 @@ class List extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
export default withService(List);
|
||||
List.propTypes = {
|
||||
agentName: PropTypes.string,
|
||||
changeTransactionSorting: PropTypes.func.isRequired,
|
||||
items: PropTypes.array,
|
||||
transactionSorting: PropTypes.object.isRequired,
|
||||
type: PropTypes.string
|
||||
};
|
||||
|
||||
export default List;
|
||||
|
|
|
@ -6,20 +6,13 @@
|
|||
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { TransactionOverview } from '../view';
|
||||
import { getKey } from '../../../../store/apiHelpers';
|
||||
import TransactionOverview from '../view';
|
||||
import { toJson } from '../../../../utils/testHelpers';
|
||||
jest.mock('../../../../utils/timepicker', () => {});
|
||||
|
||||
const setup = () => {
|
||||
const props = {
|
||||
service: {
|
||||
data: {}
|
||||
},
|
||||
transactionList: {
|
||||
data: []
|
||||
},
|
||||
urlParams: {},
|
||||
loadTransactionList: jest.fn()
|
||||
urlParams: { transactionType: 'request', serviceName: 'MyServiceName' }
|
||||
};
|
||||
|
||||
const wrapper = shallow(<TransactionOverview {...props} />);
|
||||
|
@ -28,55 +21,7 @@ const setup = () => {
|
|||
|
||||
describe('TransactionOverview', () => {
|
||||
it('should not call loadTransactionList without any props', () => {
|
||||
const { props } = setup();
|
||||
expect(props.loadTransactionList).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call loadTransactionList when props are given, and list is not loading', () => {
|
||||
const { props, wrapper } = setup();
|
||||
|
||||
wrapper.setProps({
|
||||
urlParams: {
|
||||
serviceName: 'myServiceName',
|
||||
start: 'myStart',
|
||||
end: 'myEnd',
|
||||
transactionType: 'myTransactionType'
|
||||
},
|
||||
transactionList: {
|
||||
data: [],
|
||||
status: undefined
|
||||
}
|
||||
});
|
||||
|
||||
expect(props.loadTransactionList).toHaveBeenCalledWith({
|
||||
serviceName: 'myServiceName',
|
||||
end: 'myEnd',
|
||||
start: 'myStart',
|
||||
transactionType: 'myTransactionType'
|
||||
});
|
||||
});
|
||||
|
||||
it('should not call loadTransactionList, if list is already loading', () => {
|
||||
const { props, wrapper } = setup();
|
||||
wrapper.setProps({
|
||||
urlParams: {
|
||||
serviceName: 'myServiceName',
|
||||
start: 'myStart',
|
||||
end: 'myEnd',
|
||||
transactionType: 'myTransactionType'
|
||||
},
|
||||
transactionList: {
|
||||
key: getKey({
|
||||
serviceName: 'myServiceName',
|
||||
start: 'myStart',
|
||||
end: 'myEnd',
|
||||
transactionType: 'myTransactionType'
|
||||
}),
|
||||
data: [],
|
||||
status: 'LOADING'
|
||||
}
|
||||
});
|
||||
|
||||
expect(props.loadTransactionList).not.toHaveBeenCalled();
|
||||
const { wrapper } = setup();
|
||||
expect(toJson(wrapper)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`TransactionOverview should not call loadTransactionList without any props 1`] = `
|
||||
<div>
|
||||
<styled.h1>
|
||||
MyServiceName
|
||||
</styled.h1>
|
||||
<Connect(TabNavigation) />
|
||||
<OverviewChartsRequest
|
||||
render={[Function]}
|
||||
urlParams={
|
||||
Object {
|
||||
"serviceName": "MyServiceName",
|
||||
"transactionType": "request",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<styled.h2>
|
||||
Request
|
||||
</styled.h2>
|
||||
<ServiceDetailsAndTransactionList
|
||||
render={[Function]}
|
||||
urlParams={
|
||||
Object {
|
||||
"serviceName": "MyServiceName",
|
||||
"transactionType": "request",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
`;
|
|
@ -8,21 +8,16 @@ import { connect } from 'react-redux';
|
|||
import TransactionOverview from './view';
|
||||
import { getUrlParams } from '../../../store/urlParams';
|
||||
import sorting, { changeTransactionSorting } from '../../../store/sorting';
|
||||
import {
|
||||
getTransactionList,
|
||||
loadTransactionList
|
||||
} from '../../../store/transactionList';
|
||||
|
||||
function mapStateToProps(state = {}) {
|
||||
return {
|
||||
urlParams: getUrlParams(state),
|
||||
transactionList: getTransactionList(state),
|
||||
|
||||
transactionSorting: sorting(state, 'transaction').sorting.transaction
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
loadTransactionList,
|
||||
changeTransactionSorting
|
||||
};
|
||||
|
||||
|
|
|
@ -4,58 +4,79 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import withErrorHandler from '../../shared/withErrorHandler';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { HeaderLarge, HeaderMedium } from '../../shared/UIComponents';
|
||||
import TabNavigation from '../../shared/TabNavigation';
|
||||
import Charts from './Charts';
|
||||
import Charts from '../../shared/charts/TransactionCharts';
|
||||
import List from './List';
|
||||
import { getKey } from '../../../store/apiHelpers';
|
||||
import { OverviewChartsRequest } from '../../../store/reduxRequest/overviewCharts';
|
||||
import { TransactionListRequest } from '../../../store/reduxRequest/transactionList';
|
||||
import { ServiceDetailsRequest } from '../../../store/reduxRequest/serviceDetails';
|
||||
|
||||
function loadTransactionList(props) {
|
||||
const { serviceName, start, end, transactionType } = props.urlParams;
|
||||
const key = getKey({ serviceName, start, end, transactionType });
|
||||
|
||||
if (key && props.transactionList.key !== key) {
|
||||
props.loadTransactionList({ serviceName, start, end, transactionType });
|
||||
}
|
||||
function ServiceDetailsAndTransactionList({ urlParams, render }) {
|
||||
return (
|
||||
<ServiceDetailsRequest
|
||||
urlParams={urlParams}
|
||||
render={serviceDetails => {
|
||||
return (
|
||||
<TransactionListRequest
|
||||
urlParams={urlParams}
|
||||
render={transactionList => {
|
||||
return render({
|
||||
transactionList: transactionList.data,
|
||||
serviceDetails: serviceDetails.data
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export class TransactionOverview extends Component {
|
||||
componentDidMount() {
|
||||
loadTransactionList(this.props);
|
||||
}
|
||||
export default function TransactionOverview({
|
||||
changeTransactionSorting,
|
||||
transactionSorting,
|
||||
urlParams
|
||||
}) {
|
||||
const { serviceName, transactionType } = urlParams;
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
loadTransactionList(nextProps);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<HeaderLarge>{serviceName}</HeaderLarge>
|
||||
<TabNavigation />
|
||||
|
||||
render() {
|
||||
const { serviceName, transactionType } = this.props.urlParams;
|
||||
const {
|
||||
changeTransactionSorting,
|
||||
transactionSorting,
|
||||
transactionList
|
||||
} = this.props;
|
||||
<OverviewChartsRequest
|
||||
urlParams={urlParams}
|
||||
render={({ data }) => <Charts charts={data} urlParams={urlParams} />}
|
||||
/>
|
||||
|
||||
return (
|
||||
<div>
|
||||
<HeaderLarge>{serviceName}</HeaderLarge>
|
||||
<TabNavigation />
|
||||
<Charts />
|
||||
<HeaderMedium>{transactionTypeLabel(transactionType)}</HeaderMedium>
|
||||
<List
|
||||
serviceName={serviceName}
|
||||
type={transactionType}
|
||||
items={transactionList.data}
|
||||
changeTransactionSorting={changeTransactionSorting}
|
||||
transactionSorting={transactionSorting}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
<HeaderMedium>{transactionTypeLabel(transactionType)}</HeaderMedium>
|
||||
|
||||
<ServiceDetailsAndTransactionList
|
||||
urlParams={urlParams}
|
||||
render={({ serviceDetails, transactionList }) => {
|
||||
return (
|
||||
<List
|
||||
agentName={serviceDetails.agentName}
|
||||
serviceName={serviceName}
|
||||
type={transactionType}
|
||||
items={transactionList}
|
||||
changeTransactionSorting={changeTransactionSorting}
|
||||
transactionSorting={transactionSorting}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
TransactionOverview.propTypes = {
|
||||
urlParams: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
// TODO: This is duplicated in TabNavigation
|
||||
function transactionTypeLabel(type) {
|
||||
switch (type) {
|
||||
|
@ -67,5 +88,3 @@ function transactionTypeLabel(type) {
|
|||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
export default withErrorHandler(TransactionOverview, ['transactionList']);
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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 PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { ReduxRequestView } from './view';
|
||||
import hash from 'object-hash/index';
|
||||
export { reduxRequestReducer } from './reducer';
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
const { args, id, selector } = ownProps;
|
||||
const hashedArgs = hash(args);
|
||||
let result;
|
||||
try {
|
||||
result = selector(state, { id });
|
||||
} catch (e) {
|
||||
console.error(`The selector for "ReduxRequest#${id}" threw an error:\n`, e);
|
||||
return {
|
||||
hashedArgs,
|
||||
hasError: true
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
prevHashedArgs: result.hashedArgs,
|
||||
hashedArgs,
|
||||
result
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => ({
|
||||
dispatch
|
||||
});
|
||||
|
||||
export const ReduxRequest = connect(mapStateToProps, mapDispatchToProps)(
|
||||
ReduxRequestView
|
||||
);
|
||||
|
||||
ReduxRequest.propTypes = {
|
||||
args: PropTypes.array,
|
||||
id: PropTypes.string.isRequired,
|
||||
selector: PropTypes.func
|
||||
};
|
||||
|
||||
ReduxRequest.defaultProps = {
|
||||
args: [],
|
||||
selector: (state, props) => state.reduxRequest[props.id] || {}
|
||||
};
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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 { get } from 'lodash';
|
||||
|
||||
export const ACTION_TYPES = {
|
||||
LOADING: 'REDUX_REQUEST_LOADING',
|
||||
SUCCESS: 'REDUX_REQUEST_SUCCESS',
|
||||
FAILURE: 'REDUX_REQUEST_FAILURE'
|
||||
};
|
||||
|
||||
export const STATUS = {
|
||||
LOADING: 'LOADING',
|
||||
SUCCESS: 'SUCCESS',
|
||||
FAILURE: 'FAILURE'
|
||||
};
|
||||
|
||||
function getStatus(type) {
|
||||
switch (type) {
|
||||
case ACTION_TYPES.LOADING:
|
||||
return STATUS.LOADING;
|
||||
case ACTION_TYPES.SUCCESS:
|
||||
return STATUS.SUCCESS;
|
||||
case ACTION_TYPES.FAILURE:
|
||||
return STATUS.FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
export function reduxRequestReducer(state = {}, action) {
|
||||
switch (action.type) {
|
||||
case ACTION_TYPES.LOADING:
|
||||
case ACTION_TYPES.SUCCESS:
|
||||
case ACTION_TYPES.FAILURE: {
|
||||
const { id, data, error, hashedArgs } = action;
|
||||
return {
|
||||
...state,
|
||||
[id]: {
|
||||
status: getStatus(action.type),
|
||||
data: data || get(state[id], 'data'),
|
||||
error: error || get(state[id], 'error'),
|
||||
hashedArgs
|
||||
}
|
||||
};
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
116
x-pack/plugins/apm/public/components/shared/ReduxRequest/view.js
Normal file
116
x-pack/plugins/apm/public/components/shared/ReduxRequest/view.js
Normal file
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* 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 _ from 'lodash';
|
||||
import { ACTION_TYPES } from './reducer';
|
||||
|
||||
async function maybeFetchData(
|
||||
{
|
||||
args,
|
||||
dispatch,
|
||||
hasError,
|
||||
fn,
|
||||
hashedArgs,
|
||||
id,
|
||||
prevHashedArgs,
|
||||
shouldInvoke
|
||||
},
|
||||
ctx = {}
|
||||
) {
|
||||
const shouldFetchData =
|
||||
shouldInvoke && prevHashedArgs !== hashedArgs && !hasError;
|
||||
|
||||
if (!shouldFetchData) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({
|
||||
id,
|
||||
hashedArgs,
|
||||
type: ACTION_TYPES.LOADING
|
||||
});
|
||||
const fetchId = (ctx.fetchId = _.uniqueId());
|
||||
try {
|
||||
const data = await fn(...args);
|
||||
if (fetchId === ctx.fetchId) {
|
||||
dispatch({
|
||||
data,
|
||||
hashedArgs,
|
||||
id,
|
||||
type: ACTION_TYPES.SUCCESS
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (fetchId === ctx.fetchId) {
|
||||
console.error(error);
|
||||
dispatch({
|
||||
error,
|
||||
hashedArgs,
|
||||
id,
|
||||
type: ACTION_TYPES.FAILURE
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class ReduxRequestView extends React.Component {
|
||||
componentWillMount() {
|
||||
maybeFetchData(this.props, this);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
maybeFetchData(nextProps, this);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return this.props.result !== nextProps.result;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.fetchId = null;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.hasError) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { status, data, error } = this.props.result;
|
||||
try {
|
||||
return this.props.render({ status, data, error });
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`The render method of "ReduxRequest#${
|
||||
this.props.id
|
||||
}" threw an error:\n`,
|
||||
e
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ReduxRequestView.propTypes = {
|
||||
args: PropTypes.array,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
fn: PropTypes.func.isRequired,
|
||||
hasError: PropTypes.bool,
|
||||
hashedArgs: PropTypes.string.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
prevHashedArgs: PropTypes.string,
|
||||
render: PropTypes.func,
|
||||
result: PropTypes.object,
|
||||
shouldInvoke: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
ReduxRequestView.defaultProps = {
|
||||
args: [],
|
||||
hasError: false,
|
||||
render: () => {},
|
||||
result: {},
|
||||
shouldInvoke: true
|
||||
};
|
|
@ -8,7 +8,6 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import { TabLink } from '../UIComponents';
|
||||
import styled from 'styled-components';
|
||||
import withService from '../withService';
|
||||
import {
|
||||
unit,
|
||||
units,
|
||||
|
@ -20,6 +19,7 @@ import {
|
|||
import { isEmpty } from 'lodash';
|
||||
|
||||
import TooltipOverlay from '../../shared/TooltipOverlay';
|
||||
import { ServiceDetailsRequest } from '../../../store/reduxRequest/serviceDetails';
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
|
@ -59,37 +59,47 @@ function transactionTypeLabel(type) {
|
|||
}
|
||||
}
|
||||
|
||||
function TabNavigation({ urlParams, location, service }) {
|
||||
function TabNavigation({ urlParams, location }) {
|
||||
const { serviceName, transactionType } = urlParams;
|
||||
const errorsSelected = location.pathname.includes('/errors');
|
||||
const { types } = service.data;
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{types.map(type => {
|
||||
const label = transactionTypeLabel(type);
|
||||
return (
|
||||
<TooltipOverlay
|
||||
content={
|
||||
<span>
|
||||
Transaction type:<br />
|
||||
{label}
|
||||
</span>
|
||||
}
|
||||
key={type}
|
||||
>
|
||||
<NavLink
|
||||
path={`${serviceName}/transactions/${encodeURIComponent(type)}`}
|
||||
selected={transactionType === type && !errorsSelected}
|
||||
>
|
||||
{label}
|
||||
</NavLink>
|
||||
</TooltipOverlay>
|
||||
);
|
||||
})}
|
||||
{isEmpty(types) && (
|
||||
<EmptyMessage>No transactions available.</EmptyMessage>
|
||||
)}
|
||||
<ServiceDetailsRequest
|
||||
urlParams={urlParams}
|
||||
render={serviceDetails => {
|
||||
const { types } = serviceDetails.data;
|
||||
|
||||
if (isEmpty(types)) {
|
||||
return <EmptyMessage>No transactions available.</EmptyMessage>;
|
||||
}
|
||||
|
||||
return types.map(type => {
|
||||
const label = transactionTypeLabel(type);
|
||||
return (
|
||||
<TooltipOverlay
|
||||
content={
|
||||
<span>
|
||||
Transaction type:<br />
|
||||
{label}
|
||||
</span>
|
||||
}
|
||||
key={type}
|
||||
>
|
||||
<NavLink
|
||||
path={`${serviceName}/transactions/${encodeURIComponent(
|
||||
type
|
||||
)}`}
|
||||
selected={transactionType === type && !errorsSelected}
|
||||
>
|
||||
{label}
|
||||
</NavLink>
|
||||
</TooltipOverlay>
|
||||
);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<Divider />
|
||||
<TabLink path={`${serviceName}/errors`} selected={errorsSelected}>
|
||||
Errors
|
||||
|
@ -102,4 +112,4 @@ TabNavigation.propTypes = {
|
|||
location: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default withService(TabNavigation);
|
||||
export default TabNavigation;
|
||||
|
|
|
@ -39,14 +39,6 @@ export class Charts extends Component {
|
|||
hoverIndex: null
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadCharts(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
nextProps.loadCharts(nextProps);
|
||||
}
|
||||
|
||||
onHover = hoverIndex => this.setState({ hoverIndex });
|
||||
onMouseLeave = () => this.setState({ hoverIndex: null });
|
||||
onSelectionEnd = selection => {
|
||||
|
@ -56,11 +48,11 @@ export class Charts extends Component {
|
|||
};
|
||||
|
||||
getResponseTimeTickFormatter = t => {
|
||||
return this.props.charts.data.noHits ? '- ms' : asMillis(t);
|
||||
return this.props.charts.noHits ? '- ms' : asMillis(t);
|
||||
};
|
||||
|
||||
getResponseTimeTooltipFormatter = t => {
|
||||
if (this.props.charts.data.noHits) {
|
||||
if (this.props.charts.noHits) {
|
||||
return '- ms';
|
||||
} else {
|
||||
return t == null ? 'N/A' : asMillis(t);
|
||||
|
@ -70,12 +62,12 @@ export class Charts extends Component {
|
|||
getTPMFormatter = t => {
|
||||
const { urlParams, charts } = this.props;
|
||||
const unit = tpmUnit(urlParams.transactionType);
|
||||
return charts.data.noHits ? `- ${unit}` : `${asInteger(t)} ${unit}`;
|
||||
return charts.noHits ? `- ${unit}` : `${asInteger(t)} ${unit}`;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { charts, urlParams } = this.props;
|
||||
const { noHits, responseTimeSeries, tpmSeries } = charts.data;
|
||||
const { noHits, responseTimeSeries, tpmSeries } = charts;
|
||||
|
||||
return (
|
||||
<ChartsWrapper>
|
||||
|
|
|
@ -1,31 +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 { STATUS } from '../../../constants';
|
||||
import ErrorHandler from './view';
|
||||
import { getDisplayName } from '../HOCUtils';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
function withErrorHandler(WrappedComponent, dataNames) {
|
||||
function HOC(props) {
|
||||
const unavailableNames = dataNames.filter(
|
||||
name => props[name].status === STATUS.FAILURE
|
||||
);
|
||||
|
||||
if (!isEmpty(unavailableNames)) {
|
||||
return <ErrorHandler names={unavailableNames} />;
|
||||
}
|
||||
|
||||
return <WrappedComponent {...props} />;
|
||||
}
|
||||
|
||||
HOC.displayName = `WithErrorHandler(${getDisplayName(WrappedComponent)})`;
|
||||
|
||||
return HOC;
|
||||
}
|
||||
|
||||
export default withErrorHandler;
|
|
@ -1,25 +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 { fontSizes } from '../../../style/variables';
|
||||
|
||||
function ErrorHandler({ names }) {
|
||||
const ErrorWrap = styled.div`
|
||||
font-size: ${fontSizes.large};
|
||||
`;
|
||||
|
||||
return (
|
||||
<ErrorWrap>
|
||||
<h1>Error</h1>
|
||||
<p>Failed to load data for: {names.join('\n')}</p>
|
||||
<p>Please check the console or the server output.</p>
|
||||
</ErrorWrap>
|
||||
);
|
||||
}
|
||||
|
||||
export default ErrorHandler;
|
|
@ -1,32 +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 { connect } from 'react-redux';
|
||||
import { getUrlParams } from '../../../store/urlParams';
|
||||
import { loadService, getService } from '../../../store/service';
|
||||
import getComponentWithService from './view';
|
||||
import { getDisplayName } from '../HOCUtils';
|
||||
|
||||
function withService(WrappedComponent) {
|
||||
function mapStateToProps(state = {}, props) {
|
||||
return {
|
||||
service: getService(state),
|
||||
urlParams: getUrlParams(state),
|
||||
originalProps: props
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
loadService
|
||||
};
|
||||
|
||||
const HOC = getComponentWithService(WrappedComponent);
|
||||
HOC.displayName = `WithService(${getDisplayName(WrappedComponent)})`;
|
||||
|
||||
return connect(mapStateToProps, mapDispatchToProps)(HOC);
|
||||
}
|
||||
|
||||
export default withService;
|
|
@ -1,40 +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 { getKey } from '../../../store/apiHelpers';
|
||||
|
||||
function maybeLoadService(props) {
|
||||
const { serviceName, start, end } = props.urlParams;
|
||||
const key = getKey({ serviceName, start, end });
|
||||
|
||||
if (key && props.service.key !== key) {
|
||||
props.loadService({ serviceName, start, end });
|
||||
}
|
||||
}
|
||||
|
||||
function getComponentWithService(WrappedComponent) {
|
||||
return class extends Component {
|
||||
componentDidMount() {
|
||||
maybeLoadService(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
maybeLoadService(nextProps);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<WrappedComponent
|
||||
{...this.props.originalProps}
|
||||
service={this.props.service}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default getComponentWithService;
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { uiModules } from 'ui/modules'; // eslint-disable-line no-unused-vars
|
||||
import chrome from 'ui/chrome';
|
||||
import React from 'react';
|
||||
import React, { Fragment } from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Router } from 'react-router-dom';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
@ -21,6 +21,8 @@ import Breadcrumbs from './components/app/Main/Breadcrumbs';
|
|||
|
||||
import { initTimepicker } from './utils/timepicker';
|
||||
import configureStore from './store/config/configureStore';
|
||||
import GlobalProgess from './components/app/Main/GlobalProgess';
|
||||
import LicenseChecker from './components/app/Main/LicenseChecker';
|
||||
|
||||
import { history } from './utils/url';
|
||||
|
||||
|
@ -38,9 +40,13 @@ initTimepicker(history, store.dispatch, () => {
|
|||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<Router history={history}>
|
||||
<Main />
|
||||
</Router>
|
||||
<Fragment>
|
||||
<GlobalProgess />
|
||||
<LicenseChecker />
|
||||
<Router history={history}>
|
||||
<Main />
|
||||
</Router>
|
||||
</Fragment>
|
||||
</Provider>,
|
||||
document.getElementById('react-apm-root')
|
||||
);
|
||||
|
|
|
@ -11,7 +11,7 @@ import _ from 'lodash';
|
|||
import chrome from 'ui/chrome';
|
||||
|
||||
async function callApi(options) {
|
||||
const { pathname, query, camelcase, compact, ...urlOptions } = {
|
||||
const { pathname, query, camelcase, compact, ...requestOptions } = {
|
||||
compact: true, // remove empty query args
|
||||
camelcase: true,
|
||||
credentials: 'same-origin',
|
||||
|
@ -29,7 +29,7 @@ async function callApi(options) {
|
|||
});
|
||||
|
||||
try {
|
||||
const response = await fetch(fullUrl, urlOptions);
|
||||
const response = await fetch(fullUrl, requestOptions);
|
||||
const json = await response.json();
|
||||
if (!response.ok) {
|
||||
throw new Error(JSON.stringify(json, null, 4));
|
||||
|
@ -65,18 +65,17 @@ export async function loadAgentStatus() {
|
|||
});
|
||||
}
|
||||
|
||||
export async function loadServiceList({ start, end, query }) {
|
||||
export async function loadServiceList({ start, end }) {
|
||||
return callApi({
|
||||
pathname: chrome.addBasePath(`/api/apm/services`),
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
query
|
||||
end
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function loadService({ start, end, serviceName }) {
|
||||
export async function loadServiceDetails({ start, end, serviceName }) {
|
||||
return callApi({
|
||||
pathname: chrome.addBasePath(`/api/apm/services/${serviceName}`),
|
||||
query: {
|
||||
|
@ -199,7 +198,7 @@ export async function loadErrorGroupList({
|
|||
});
|
||||
}
|
||||
|
||||
export async function loadErrorGroup({
|
||||
export async function loadErrorGroupDetails({
|
||||
serviceName,
|
||||
errorGroupId,
|
||||
start,
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`root reducer should return default data as state 1`] = `
|
||||
Object {
|
||||
"data": Array [],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`root reducer should return incoming data as state 1`] = `
|
||||
Object {
|
||||
"data": Array [
|
||||
"a",
|
||||
"b",
|
||||
],
|
||||
}
|
||||
`;
|
|
@ -1,218 +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 { createStore } from 'redux';
|
||||
import { STATUS } from '../../constants';
|
||||
import {
|
||||
createActionTypes,
|
||||
createReducer,
|
||||
createAction,
|
||||
getKey
|
||||
} from '../apiHelpers';
|
||||
|
||||
describe('apiHelpers', () => {
|
||||
describe('Reducers should not be prone to race conditions', () => {
|
||||
it('should return the response for the action that was initiated (started loading) last', () => {
|
||||
const actionTypes = createActionTypes('MY_ACTION');
|
||||
const [ACTION_LOADING, ACTION_SUCCESS] = actionTypes;
|
||||
const reducer = createReducer(actionTypes, {});
|
||||
const store = createStore(reducer);
|
||||
|
||||
store.dispatch({
|
||||
key: 'first',
|
||||
type: ACTION_LOADING
|
||||
});
|
||||
|
||||
store.dispatch({
|
||||
key: 'second',
|
||||
type: ACTION_LOADING
|
||||
});
|
||||
|
||||
store.dispatch({
|
||||
key: 'second',
|
||||
response: 'response from second',
|
||||
type: ACTION_SUCCESS
|
||||
});
|
||||
|
||||
store.dispatch({
|
||||
key: 'first',
|
||||
response: 'response from first',
|
||||
type: ACTION_SUCCESS
|
||||
});
|
||||
|
||||
expect(store.getState()).toEqual({
|
||||
data: 'response from second',
|
||||
key: 'second',
|
||||
status: 'SUCCESS'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createActionTypes', () => {
|
||||
it('should return 3 action types', () => {
|
||||
expect(createActionTypes('MY_ACTION')).toEqual([
|
||||
'MY_ACTION_LOADING',
|
||||
'MY_ACTION_SUCCESS',
|
||||
'MY_ACTION_FAILURE'
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createReducer', () => {
|
||||
const actionTypes = createActionTypes('MY_ACTION_TYPE');
|
||||
const [
|
||||
MY_ACTION_TYPE_LOADING,
|
||||
MY_ACTION_TYPE_SUCCESS,
|
||||
MY_ACTION_TYPE_FAILURE
|
||||
] = actionTypes;
|
||||
|
||||
const initialData = { foo: 'intitial data' };
|
||||
|
||||
it('should return loading state with initial data', () => {
|
||||
expect(
|
||||
createReducer(actionTypes, initialData)(undefined, {
|
||||
type: MY_ACTION_TYPE_LOADING
|
||||
})
|
||||
).toEqual({
|
||||
status: STATUS.LOADING,
|
||||
data: { foo: 'intitial data' }
|
||||
});
|
||||
});
|
||||
|
||||
it('should return loading state with store data', () => {
|
||||
expect(
|
||||
createReducer(actionTypes, initialData)(
|
||||
{ data: 'previous data' },
|
||||
{
|
||||
type: MY_ACTION_TYPE_LOADING
|
||||
}
|
||||
)
|
||||
).toEqual({
|
||||
data: 'previous data',
|
||||
status: STATUS.LOADING
|
||||
});
|
||||
});
|
||||
|
||||
it('should return success state', () => {
|
||||
expect(
|
||||
createReducer(actionTypes, initialData)(undefined, {
|
||||
response: { user: 1337 },
|
||||
type: MY_ACTION_TYPE_SUCCESS
|
||||
})
|
||||
).toEqual({
|
||||
data: { user: 1337 },
|
||||
status: STATUS.SUCCESS
|
||||
});
|
||||
});
|
||||
|
||||
it('should return failure state', () => {
|
||||
console.error = jest.fn();
|
||||
|
||||
expect(
|
||||
createReducer(actionTypes, initialData)(undefined, {
|
||||
errorResponse: { msg: 'Something failed :(' },
|
||||
type: MY_ACTION_TYPE_FAILURE
|
||||
})
|
||||
).toEqual({
|
||||
error: { msg: 'Something failed :(' },
|
||||
data: { foo: 'intitial data' },
|
||||
status: STATUS.FAILURE
|
||||
});
|
||||
});
|
||||
|
||||
it('should return default state', () => {
|
||||
console.error = jest.fn();
|
||||
|
||||
expect(
|
||||
createReducer(actionTypes, initialData)(undefined, {
|
||||
type: 'NON_MATCHING_TYPE'
|
||||
})
|
||||
).toEqual({
|
||||
data: { foo: 'intitial data' }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createAction', () => {
|
||||
const actionTypes = createActionTypes('MY_ACTION_TYPE');
|
||||
const [
|
||||
MY_ACTION_TYPE_LOADING,
|
||||
MY_ACTION_TYPE_SUCCESS,
|
||||
MY_ACTION_TYPE_FAILURE
|
||||
] = actionTypes;
|
||||
|
||||
describe('succesful request', () => {
|
||||
let key;
|
||||
let dispatchMock;
|
||||
let apiMock;
|
||||
let keyArgs;
|
||||
beforeEach(async () => {
|
||||
dispatchMock = jest.fn();
|
||||
apiMock = jest.fn(() => Promise.resolve('foo'));
|
||||
keyArgs = { a: 'aa', b: 'bb' };
|
||||
key = getKey(keyArgs);
|
||||
await createAction(actionTypes, apiMock)(keyArgs)(dispatchMock);
|
||||
});
|
||||
|
||||
it('should dispatch loading action', () => {
|
||||
expect(dispatchMock).toHaveBeenCalledWith({
|
||||
keyArgs,
|
||||
key,
|
||||
type: MY_ACTION_TYPE_LOADING
|
||||
});
|
||||
});
|
||||
|
||||
it('should call apiMock with keyArgs', () => {
|
||||
expect(apiMock).toHaveBeenCalledWith(keyArgs);
|
||||
});
|
||||
|
||||
it('should dispatch success action', () => {
|
||||
expect(dispatchMock).toHaveBeenCalledWith({
|
||||
keyArgs,
|
||||
key,
|
||||
response: 'foo',
|
||||
type: MY_ACTION_TYPE_SUCCESS
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('unsuccesful request', () => {
|
||||
it('should dispatch error action', async () => {
|
||||
const dispatchMock = jest.fn();
|
||||
const apiMock = jest.fn(() =>
|
||||
Promise.reject(new Error('an error occured :('))
|
||||
);
|
||||
const keyArgs = { a: 'aa', b: 'bb' };
|
||||
const key = getKey(keyArgs);
|
||||
await createAction(actionTypes, apiMock)(keyArgs)(dispatchMock);
|
||||
|
||||
expect(dispatchMock).toHaveBeenCalledWith({
|
||||
keyArgs,
|
||||
key,
|
||||
errorResponse: expect.any(Error),
|
||||
type: MY_ACTION_TYPE_FAILURE
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('without arguments', () => {
|
||||
it('should dispatch success action', async () => {
|
||||
const dispatchMock = jest.fn();
|
||||
const apiMock = jest.fn(() => Promise.resolve('foobar'));
|
||||
const keyArgs = undefined;
|
||||
const key = getKey(keyArgs);
|
||||
await createAction(actionTypes, apiMock)(keyArgs)(dispatchMock);
|
||||
|
||||
expect(dispatchMock).toHaveBeenCalledWith({
|
||||
keyArgs: {},
|
||||
key,
|
||||
response: 'foobar',
|
||||
type: MY_ACTION_TYPE_SUCCESS
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -9,55 +9,13 @@ import reducer from '../rootReducer';
|
|||
describe('root reducer', () => {
|
||||
it('should return the initial state', () => {
|
||||
expect(reducer(undefined, {})).toEqual({
|
||||
detailsCharts: {
|
||||
data: {
|
||||
dates: [],
|
||||
responseTimes: {},
|
||||
totalHits: 0,
|
||||
tpmBuckets: [],
|
||||
weightedAverage: null
|
||||
}
|
||||
},
|
||||
overviewCharts: {
|
||||
data: {
|
||||
dates: [],
|
||||
responseTimes: {},
|
||||
totalHits: 0,
|
||||
tpmBuckets: [],
|
||||
weightedAverage: null
|
||||
}
|
||||
},
|
||||
errorDistribution: { data: { buckets: [], totalHits: 0 } },
|
||||
errorGroup: { data: {} },
|
||||
errorGroupList: { data: [] },
|
||||
license: {
|
||||
data: {
|
||||
features: { watcher: { isAvailable: false } },
|
||||
license: { isActive: false }
|
||||
}
|
||||
},
|
||||
location: { hash: '', pathname: '', search: '' },
|
||||
service: { data: { types: [] } },
|
||||
serviceList: { data: [] },
|
||||
reduxRequest: {},
|
||||
sorting: {
|
||||
service: { descending: false, key: 'serviceName' },
|
||||
transaction: { descending: true, key: 'impact' }
|
||||
},
|
||||
spans: { data: {} },
|
||||
transaction: { data: {} },
|
||||
transactionDistribution: { data: { buckets: [], totalHits: 0 } },
|
||||
transactionList: { data: [] },
|
||||
urlParams: {}
|
||||
});
|
||||
});
|
||||
|
||||
it('should return incoming data as state', () => {
|
||||
expect(
|
||||
reducer({ errorGroupList: { data: ['a', 'b'] } }, {}).errorGroupList
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should return default data as state', () => {
|
||||
expect(reducer(undefined, {}).errorGroupList).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,55 +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 {
|
||||
loadServiceList,
|
||||
SERVICE_LIST_LOADING,
|
||||
SERVICE_LIST_SUCCESS
|
||||
} from '../serviceList';
|
||||
import { getKey } from '../apiHelpers';
|
||||
import fetchMock from 'fetch-mock';
|
||||
import response from './services-response.json';
|
||||
|
||||
describe('loadServiceList', () => {
|
||||
const key = getKey({ start: 'myStart', end: 'myEnd' });
|
||||
const dispatch = jest.fn();
|
||||
const matcherName = /\/api\/apm\/services/;
|
||||
|
||||
beforeEach(() => {
|
||||
fetchMock.get(matcherName, response);
|
||||
return loadServiceList({
|
||||
start: 'myStart',
|
||||
end: 'myEnd'
|
||||
})(dispatch);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fetchMock.restore();
|
||||
});
|
||||
|
||||
it('should make a http request', () => {
|
||||
expect(fetchMock.lastUrl(matcherName)).toContain(
|
||||
'/api/apm/services?start=myStart&end=myEnd'
|
||||
);
|
||||
});
|
||||
|
||||
it('should dispatch SERVICE_LIST_LOADING', () => {
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
keyArgs: { start: 'myStart', end: 'myEnd' },
|
||||
type: SERVICE_LIST_LOADING,
|
||||
key
|
||||
});
|
||||
});
|
||||
|
||||
it('should dispatch SERVICE_LIST_SUCCESS with http response', () => {
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
keyArgs: { start: 'myStart', end: 'myEnd' },
|
||||
response,
|
||||
type: SERVICE_LIST_SUCCESS,
|
||||
key
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,20 +0,0 @@
|
|||
[
|
||||
{
|
||||
"serviceName": "opbeans-python",
|
||||
"overallAvg": 163335.74660633484,
|
||||
"types": ["request"],
|
||||
"chart": {}
|
||||
},
|
||||
{
|
||||
"serviceName": "opbeans-backend",
|
||||
"overallAvg": 370627.24742268043,
|
||||
"types": ["request", "Brewing Bot"],
|
||||
"chart": {}
|
||||
},
|
||||
{
|
||||
"serviceName": "opbeat-stage",
|
||||
"overallAvg": 6740834.058047493,
|
||||
"types": ["transaction.celery", "request"],
|
||||
"chart": {}
|
||||
}
|
||||
]
|
|
@ -1,97 +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 { STATUS } from '../constants';
|
||||
import _ from 'lodash';
|
||||
const hash = require('object-hash/index');
|
||||
|
||||
export function createActionTypes(actionName) {
|
||||
return [
|
||||
`${actionName}_LOADING`,
|
||||
`${actionName}_SUCCESS`,
|
||||
`${actionName}_FAILURE`
|
||||
];
|
||||
}
|
||||
|
||||
export function createReducer(actionTypes, initialData) {
|
||||
const [LOADING, SUCCESS, FAILURE] = actionTypes;
|
||||
|
||||
return (state, action) => {
|
||||
switch (action.type) {
|
||||
case LOADING:
|
||||
return {
|
||||
key: action.key,
|
||||
keyArgs: action.keyArgs,
|
||||
data: _.get(state, 'data', initialData),
|
||||
status: STATUS.LOADING
|
||||
};
|
||||
|
||||
case SUCCESS:
|
||||
// This should avoid race-conditions so a slow response doesn't overwrite a newer response (faster) response
|
||||
if (action.key !== _.get(state, 'key')) {
|
||||
return state;
|
||||
}
|
||||
|
||||
return {
|
||||
key: action.key,
|
||||
keyArgs: action.keyArgs,
|
||||
data: action.response, // TODO: rename 'data' to 'response'
|
||||
status: STATUS.SUCCESS
|
||||
};
|
||||
|
||||
case FAILURE:
|
||||
return {
|
||||
key: action.key,
|
||||
keyArgs: action.keyArgs,
|
||||
data: initialData,
|
||||
status: STATUS.FAILURE,
|
||||
error: action.errorResponse // TODO: rename 'error' to 'errorResponse'
|
||||
};
|
||||
default:
|
||||
return state || { data: initialData };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function createAction(actionTypes, callApi) {
|
||||
const [LOADING, SUCCESS, FAILURE] = actionTypes;
|
||||
|
||||
return (keyArgs = {}) => {
|
||||
return async dispatch => {
|
||||
const key = hash(keyArgs);
|
||||
dispatch({ key, keyArgs, type: LOADING });
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await callApi(keyArgs);
|
||||
} catch (errorResponse) {
|
||||
console.error(errorResponse);
|
||||
return dispatch({
|
||||
key,
|
||||
keyArgs,
|
||||
errorResponse,
|
||||
type: FAILURE
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
return dispatch({
|
||||
key,
|
||||
keyArgs,
|
||||
response,
|
||||
type: SUCCESS
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const hasEmptyProps = obj => _.some(obj, value => value == null);
|
||||
export const getKey = (obj = {}, notNull = true) => {
|
||||
return notNull && hasEmptyProps(obj) ? null : hash(obj);
|
||||
};
|
|
@ -1,30 +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 { createSelector } from 'reselect';
|
||||
import * as rest from '../services/rest';
|
||||
import { createActionTypes, createAction, createReducer } from './apiHelpers';
|
||||
import { getCharts } from './selectors/chartSelectors';
|
||||
import { getUrlParams } from './urlParams';
|
||||
|
||||
const actionTypes = createActionTypes('DETAILS_CHARTS');
|
||||
|
||||
const INITIAL_DATA = {
|
||||
totalHits: 0,
|
||||
dates: [],
|
||||
responseTimes: {},
|
||||
tpmBuckets: [],
|
||||
weightedAverage: null
|
||||
};
|
||||
|
||||
export default createReducer(actionTypes, INITIAL_DATA);
|
||||
export const loadDetailsCharts = createAction(actionTypes, rest.loadCharts);
|
||||
|
||||
export const getDetailsCharts = createSelector(
|
||||
getUrlParams,
|
||||
state => state.detailsCharts,
|
||||
getCharts
|
||||
);
|
|
@ -1,28 +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 * as rest from '../services/rest';
|
||||
import { createActionTypes, createAction, createReducer } from './apiHelpers';
|
||||
|
||||
const actionTypes = createActionTypes('ERROR_DISTRIBUTION');
|
||||
export const [
|
||||
ERROR_DISTRIBUTION_LOADING,
|
||||
ERROR_DISTRIBUTION_SUCCESS,
|
||||
ERROR_DISTRIBUTION_FAILURE
|
||||
] = actionTypes;
|
||||
|
||||
const INITIAL_DATA = { buckets: [], totalHits: 0 };
|
||||
|
||||
export default createReducer(actionTypes, INITIAL_DATA);
|
||||
|
||||
export const loadErrorDistribution = createAction(
|
||||
actionTypes,
|
||||
rest.loadErrorDistribution
|
||||
);
|
||||
|
||||
export function getErrorDistribution(state) {
|
||||
return state.errorDistribution;
|
||||
}
|
|
@ -1,25 +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 * as rest from '../services/rest';
|
||||
import { createActionTypes, createAction, createReducer } from './apiHelpers';
|
||||
|
||||
const actionTypes = createActionTypes('ERROR_GROUP');
|
||||
export const [
|
||||
ERROR_GROUP_LOADING,
|
||||
ERROR_GROUP_SUCCESS,
|
||||
ERROR_GROUP_FAILURE
|
||||
] = actionTypes;
|
||||
|
||||
const INITIAL_DATA = {};
|
||||
|
||||
export default createReducer(actionTypes, INITIAL_DATA);
|
||||
|
||||
export const loadErrorGroup = createAction(actionTypes, rest.loadErrorGroup);
|
||||
|
||||
export function getErrorGroup(state) {
|
||||
return state.errorGroup;
|
||||
}
|
|
@ -1,27 +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 * as rest from '../services/rest';
|
||||
import { createActionTypes, createAction, createReducer } from './apiHelpers';
|
||||
|
||||
const actionTypes = createActionTypes('ERROR_GROUP_LIST');
|
||||
export const [
|
||||
ERROR_GROUP_LIST_LOADING,
|
||||
ERROR_GROUP_LIST_SUCCESS,
|
||||
ERROR_GROUP_LIST_FAILURE
|
||||
] = actionTypes;
|
||||
|
||||
const INITIAL_DATA = [];
|
||||
export default createReducer(actionTypes, INITIAL_DATA);
|
||||
|
||||
export const loadErrorGroupList = createAction(
|
||||
actionTypes,
|
||||
rest.loadErrorGroupList
|
||||
);
|
||||
|
||||
export const getErrorGroupList = state => {
|
||||
return state.errorGroupList;
|
||||
};
|
|
@ -1,20 +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 * as rest from '../services/rest';
|
||||
import { createActionTypes, createAction, createReducer } from './apiHelpers';
|
||||
|
||||
const actionTypes = createActionTypes('LICENSE');
|
||||
export const [LICENSE_LOADING, LICENSE_SUCCESS, LICENSE_FAILURE] = actionTypes;
|
||||
|
||||
const INITIAL_DATA = {
|
||||
features: { watcher: { isAvailable: false } },
|
||||
license: { isActive: false }
|
||||
};
|
||||
const license = createReducer(actionTypes, INITIAL_DATA);
|
||||
export const loadLicense = createAction(actionTypes, rest.loadLicense);
|
||||
|
||||
export default license;
|
|
@ -1,30 +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 { createSelector } from 'reselect';
|
||||
import * as rest from '../services/rest';
|
||||
import { createActionTypes, createAction, createReducer } from './apiHelpers';
|
||||
import { getCharts } from './selectors/chartSelectors';
|
||||
import { getUrlParams } from './urlParams';
|
||||
|
||||
const actionTypes = createActionTypes('OVERVIEW_CHARTS');
|
||||
|
||||
const INITIAL_DATA = {
|
||||
totalHits: 0,
|
||||
dates: [],
|
||||
responseTimes: {},
|
||||
tpmBuckets: [],
|
||||
weightedAverage: null
|
||||
};
|
||||
|
||||
export default createReducer(actionTypes, INITIAL_DATA);
|
||||
export const loadOverviewCharts = createAction(actionTypes, rest.loadCharts);
|
||||
|
||||
export const getOverviewCharts = createSelector(
|
||||
getUrlParams,
|
||||
state => state.overviewCharts,
|
||||
getCharts
|
||||
);
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* 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 * as rest from '../../../services/rest';
|
||||
import { getServiceList, ServiceListRequest } from '../serviceList';
|
||||
import { mountWithStore } from '../../../utils/testHelpers';
|
||||
|
||||
describe('serviceList', () => {
|
||||
describe('getServiceList', () => {
|
||||
it('should return default value when empty', () => {
|
||||
const state = { reduxRequest: {}, sorting: { service: {} } };
|
||||
expect(getServiceList(state)).toEqual({ data: [] });
|
||||
});
|
||||
|
||||
it('should return serviceList when not empty', () => {
|
||||
const state = {
|
||||
reduxRequest: { serviceList: { data: [{ foo: 'bar' }] } },
|
||||
sorting: { service: {} }
|
||||
};
|
||||
expect(getServiceList(state)).toEqual({ data: [{ foo: 'bar' }] });
|
||||
});
|
||||
});
|
||||
|
||||
describe('ServiceListRequest', () => {
|
||||
let loadSpy;
|
||||
let renderSpy;
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
const state = {
|
||||
reduxRequest: {
|
||||
serviceList: { status: 'my-status', data: [{ foo: 'bar' }] }
|
||||
},
|
||||
sorting: { service: {} }
|
||||
};
|
||||
|
||||
loadSpy = jest.spyOn(rest, 'loadServiceList').mockReturnValue();
|
||||
renderSpy = jest.fn().mockReturnValue(<div>rendered</div>);
|
||||
|
||||
wrapper = mountWithStore(
|
||||
<ServiceListRequest
|
||||
urlParams={{ start: 'myStart', end: 'myEnd' }}
|
||||
render={renderSpy}
|
||||
/>,
|
||||
state
|
||||
);
|
||||
});
|
||||
|
||||
it('should call render method', () => {
|
||||
expect(renderSpy).toHaveBeenCalledWith({
|
||||
data: [{ foo: 'bar' }],
|
||||
status: 'my-status'
|
||||
});
|
||||
});
|
||||
|
||||
it('should call "loadServiceList"', () => {
|
||||
expect(loadSpy).toHaveBeenCalledWith({
|
||||
start: 'myStart',
|
||||
end: 'myEnd'
|
||||
});
|
||||
});
|
||||
|
||||
it('should render component', () => {
|
||||
expect(wrapper.html()).toEqual('<div>rendered</div>');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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 { createSelector } from 'reselect';
|
||||
import { getCharts } from '../selectors/chartSelectors';
|
||||
import { getUrlParams } from '../urlParams';
|
||||
import { withInitialData } from './helpers';
|
||||
import { ReduxRequest } from '../../components/shared/ReduxRequest';
|
||||
import { loadCharts } from '../../services/rest';
|
||||
|
||||
const ID = 'detailsCharts';
|
||||
const INITIAL_DATA = {
|
||||
totalHits: 0,
|
||||
dates: [],
|
||||
responseTimes: {},
|
||||
tpmBuckets: [],
|
||||
weightedAverage: null
|
||||
};
|
||||
|
||||
export const getDetailsCharts = createSelector(
|
||||
getUrlParams,
|
||||
state => withInitialData(state.reduxRequest[ID], INITIAL_DATA),
|
||||
getCharts
|
||||
);
|
||||
|
||||
export function DetailsChartsRequest({ urlParams, render }) {
|
||||
const {
|
||||
serviceName,
|
||||
start,
|
||||
end,
|
||||
transactionType,
|
||||
transactionName,
|
||||
kuery
|
||||
} = urlParams;
|
||||
|
||||
return (
|
||||
<ReduxRequest
|
||||
id={ID}
|
||||
fn={loadCharts}
|
||||
shouldInvoke={Boolean(
|
||||
serviceName && start && end && transactionType && transactionName
|
||||
)}
|
||||
args={[
|
||||
{ serviceName, start, end, transactionType, transactionName, kuery }
|
||||
]}
|
||||
selector={getDetailsCharts}
|
||||
render={render}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 { ReduxRequest } from '../../components/shared/ReduxRequest';
|
||||
import { loadErrorDistribution } from '../../services/rest';
|
||||
import { withInitialData } from './helpers';
|
||||
|
||||
const ID = 'errorDistribution';
|
||||
const INITIAL_DATA = { buckets: [], totalHits: 0 };
|
||||
|
||||
export function getErrorDistribution(state) {
|
||||
return withInitialData(state.reduxRequest[ID], INITIAL_DATA);
|
||||
}
|
||||
|
||||
export function ErrorDistributionRequest({ urlParams, render }) {
|
||||
const { serviceName, start, end, errorGroupId, kuery } = urlParams;
|
||||
|
||||
return (
|
||||
<ReduxRequest
|
||||
id={ID}
|
||||
fn={loadErrorDistribution}
|
||||
shouldInvoke={Boolean(serviceName, start, end, errorGroupId)}
|
||||
args={[{ serviceName, start, end, errorGroupId, kuery }]}
|
||||
selector={getErrorDistribution}
|
||||
render={render}
|
||||
/>
|
||||
);
|
||||
}
|
32
x-pack/plugins/apm/public/store/reduxRequest/errorGroup.js
Normal file
32
x-pack/plugins/apm/public/store/reduxRequest/errorGroup.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 { withInitialData } from './helpers';
|
||||
import { ReduxRequest } from '../../components/shared/ReduxRequest';
|
||||
import { loadErrorGroupDetails } from '../../services/rest';
|
||||
|
||||
const ID = 'errorGroupDetails';
|
||||
const INITIAL_DATA = {};
|
||||
|
||||
export function getErrorGroupDetails(state) {
|
||||
return withInitialData(state.reduxRequest[ID], INITIAL_DATA);
|
||||
}
|
||||
|
||||
export function ErrorGroupDetailsRequest({ urlParams, render }) {
|
||||
const { serviceName, errorGroupId, start, end, kuery } = urlParams;
|
||||
|
||||
return (
|
||||
<ReduxRequest
|
||||
id={ID}
|
||||
fn={loadErrorGroupDetails}
|
||||
shouldInvoke={Boolean(serviceName && start && end && errorGroupId)}
|
||||
args={[{ serviceName, start, end, errorGroupId, kuery }]}
|
||||
selector={getErrorGroupDetails}
|
||||
render={render}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 { withInitialData } from './helpers';
|
||||
import { ReduxRequest } from '../../components/shared/ReduxRequest';
|
||||
import { loadErrorGroupList } from '../../services/rest';
|
||||
|
||||
const ID = 'errorGroupList';
|
||||
const INITIAL_DATA = [];
|
||||
|
||||
export function getErrorGroupList(state) {
|
||||
return withInitialData(state.reduxRequest[ID], INITIAL_DATA);
|
||||
}
|
||||
|
||||
export function ErrorGroupDetailsRequest({ urlParams, render }) {
|
||||
const { serviceName, start, end, q, sortBy, sortOrder, kuery } = urlParams;
|
||||
|
||||
return (
|
||||
<ReduxRequest
|
||||
id={ID}
|
||||
fn={loadErrorGroupList}
|
||||
shouldInvoke={Boolean(serviceName && start && end)}
|
||||
args={[{ serviceName, start, end, q, sortBy, sortOrder, kuery }]}
|
||||
selector={getErrorGroupList}
|
||||
render={render}
|
||||
/>
|
||||
);
|
||||
}
|
12
x-pack/plugins/apm/public/store/reduxRequest/helpers.js
Normal file
12
x-pack/plugins/apm/public/store/reduxRequest/helpers.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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 function withInitialData(state = {}, initialData) {
|
||||
return {
|
||||
...state,
|
||||
data: state.data || initialData
|
||||
};
|
||||
}
|
30
x-pack/plugins/apm/public/store/reduxRequest/license.js
Normal file
30
x-pack/plugins/apm/public/store/reduxRequest/license.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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 { withInitialData } from './helpers';
|
||||
import { ReduxRequest } from '../../components/shared/ReduxRequest';
|
||||
import { loadLicense } from '../../services/rest';
|
||||
|
||||
const ID = 'license';
|
||||
const INITIAL_DATA = {
|
||||
features: { watcher: { isAvailable: false } },
|
||||
license: { isActive: false }
|
||||
};
|
||||
|
||||
export function getLicense(state) {
|
||||
return withInitialData(state.reduxRequest[ID], INITIAL_DATA);
|
||||
}
|
||||
|
||||
export function LicenceRequest({ render }) {
|
||||
return (
|
||||
<ReduxRequest
|
||||
id={ID}
|
||||
fn={loadLicense}
|
||||
selector={getLicense}
|
||||
render={render}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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 { createSelector } from 'reselect';
|
||||
import { getCharts } from '../selectors/chartSelectors';
|
||||
import { getUrlParams } from '../urlParams';
|
||||
import { withInitialData } from './helpers';
|
||||
import { ReduxRequest } from '../../components/shared/ReduxRequest';
|
||||
import { loadCharts } from '../../services/rest';
|
||||
|
||||
const ID = 'overviewCharts';
|
||||
const INITIAL_DATA = {
|
||||
totalHits: 0,
|
||||
dates: [],
|
||||
responseTimes: {},
|
||||
tpmBuckets: [],
|
||||
weightedAverage: null
|
||||
};
|
||||
|
||||
export const getOverviewCharts = createSelector(
|
||||
getUrlParams,
|
||||
state => withInitialData(state.reduxRequest[ID], INITIAL_DATA),
|
||||
getCharts
|
||||
);
|
||||
|
||||
export function OverviewChartsRequest({ urlParams, render }) {
|
||||
const { serviceName, start, end, transactionType, kuery } = urlParams;
|
||||
return (
|
||||
<ReduxRequest
|
||||
id={ID}
|
||||
shouldInvoke={Boolean(serviceName && start && end && transactionType)}
|
||||
fn={loadCharts}
|
||||
args={[{ serviceName, start, end, transactionType, kuery }]}
|
||||
selector={getOverviewCharts}
|
||||
render={render}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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 _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withInitialData } from './helpers';
|
||||
import { ReduxRequest } from '../../components/shared/ReduxRequest';
|
||||
import { loadServiceDetails } from '../../services/rest';
|
||||
|
||||
const ID = 'serviceDetails';
|
||||
const INITIAL_DATA = { types: [] };
|
||||
|
||||
export function getServiceDetails(state) {
|
||||
return withInitialData(state.reduxRequest[ID], INITIAL_DATA);
|
||||
}
|
||||
|
||||
export function getDefaultTransactionType(state) {
|
||||
const types = _.get(state.reduxRequest.serviceDetails, 'data.types');
|
||||
return _.first(types);
|
||||
}
|
||||
|
||||
export function ServiceDetailsRequest({ urlParams, render }) {
|
||||
const { serviceName, start, end, kuery } = urlParams;
|
||||
return (
|
||||
<ReduxRequest
|
||||
id={ID}
|
||||
shouldInvoke={Boolean(serviceName && start && end)}
|
||||
fn={loadServiceDetails}
|
||||
args={[{ serviceName, start, end, kuery }]}
|
||||
selector={getServiceDetails}
|
||||
render={render}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
ServiceDetailsRequest.propTypes = {
|
||||
urlParams: PropTypes.object.isRequired,
|
||||
render: PropTypes.func.isRequired
|
||||
};
|
|
@ -4,27 +4,18 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import * as rest from '../services/rest';
|
||||
import React from 'react';
|
||||
import orderBy from 'lodash.orderby';
|
||||
import { createSelector } from 'reselect';
|
||||
import { createActionTypes, createAction, createReducer } from './apiHelpers';
|
||||
|
||||
const actionTypes = createActionTypes('SERVICE_LIST');
|
||||
export const [
|
||||
SERVICE_LIST_LOADING,
|
||||
SERVICE_LIST_SUCCESS,
|
||||
SERVICE_LIST_FAILURE
|
||||
] = actionTypes;
|
||||
import { loadServiceList } from '../../services/rest';
|
||||
import { ReduxRequest } from '../../components/shared/ReduxRequest';
|
||||
import { withInitialData } from './helpers';
|
||||
|
||||
const ID = 'serviceList';
|
||||
const INITIAL_DATA = [];
|
||||
|
||||
const serviceList = createReducer(actionTypes, INITIAL_DATA);
|
||||
|
||||
export const loadServiceList = createAction(actionTypes, rest.loadServiceList);
|
||||
|
||||
// SELECTORS
|
||||
export const getServiceList = createSelector(
|
||||
state => state.serviceList,
|
||||
state => withInitialData(state.reduxRequest[ID], INITIAL_DATA),
|
||||
state => state.sorting.service,
|
||||
(serviceList, serviceSorting) => {
|
||||
const { key: sortKey, descending } = serviceSorting;
|
||||
|
@ -36,4 +27,15 @@ export const getServiceList = createSelector(
|
|||
}
|
||||
);
|
||||
|
||||
export default serviceList;
|
||||
export function ServiceListRequest({ urlParams, render }) {
|
||||
const { start, end, kuery } = urlParams;
|
||||
return (
|
||||
<ReduxRequest
|
||||
id={ID}
|
||||
fn={loadServiceList}
|
||||
args={[{ start, end, kuery }]}
|
||||
selector={getServiceList}
|
||||
render={render}
|
||||
/>
|
||||
);
|
||||
}
|
31
x-pack/plugins/apm/public/store/reduxRequest/spans.js
Normal file
31
x-pack/plugins/apm/public/store/reduxRequest/spans.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 { withInitialData } from './helpers';
|
||||
import { ReduxRequest } from '../../components/shared/ReduxRequest';
|
||||
import { loadSpans } from '../../services/rest';
|
||||
|
||||
const ID = 'spans';
|
||||
const INITIAL_DATA = {};
|
||||
|
||||
export function getSpans(state) {
|
||||
return withInitialData(state.reduxRequest[ID], INITIAL_DATA);
|
||||
}
|
||||
|
||||
export function SpansRequest({ urlParams, render }) {
|
||||
const { serviceName, start, end, transactionId, kuery } = urlParams;
|
||||
return (
|
||||
<ReduxRequest
|
||||
id={ID}
|
||||
shouldInvoke={Boolean(serviceName && start && end && transactionId)}
|
||||
fn={loadSpans}
|
||||
selector={getSpans}
|
||||
args={[{ serviceName, start, end, transactionId, kuery }]}
|
||||
render={render}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* 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 { withInitialData } from './helpers';
|
||||
import { ReduxRequest } from '../../components/shared/ReduxRequest';
|
||||
import { loadTransaction } from '../../services/rest';
|
||||
|
||||
const ID = 'transactionDetails';
|
||||
const INITIAL_DATA = {};
|
||||
|
||||
export function getTransactionDetails(state) {
|
||||
return withInitialData(state.reduxRequest[ID], INITIAL_DATA);
|
||||
}
|
||||
|
||||
export function TransactionDetailsRequest({ urlParams, render }) {
|
||||
const { serviceName, start, end, transactionId, kuery } = urlParams;
|
||||
return (
|
||||
<ReduxRequest
|
||||
id={ID}
|
||||
shouldInvoke={Boolean(serviceName && start && end && transactionId)}
|
||||
fn={loadTransaction}
|
||||
selector={getTransactionDetails}
|
||||
args={[{ serviceName, start, end, transactionId, kuery }]}
|
||||
render={render}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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 { withInitialData } from './helpers';
|
||||
import { ReduxRequest } from '../../components/shared/ReduxRequest';
|
||||
import { loadTransactionDistribution } from '../../services/rest';
|
||||
|
||||
const INITIAL_DATA = { buckets: [], totalHits: 0 };
|
||||
|
||||
export function getTransactionDistribution(state) {
|
||||
return withInitialData(
|
||||
state.reduxRequest.transactionDistribution,
|
||||
INITIAL_DATA
|
||||
);
|
||||
}
|
||||
|
||||
export function getDefaultTransactionId(state) {
|
||||
const _distribution = getTransactionDistribution(state);
|
||||
return _distribution.data.defaultTransactionId;
|
||||
}
|
||||
|
||||
export function TransactionDistributionRequest({ urlParams, render }) {
|
||||
const { serviceName, start, end, transactionName, kuery } = urlParams;
|
||||
return (
|
||||
<ReduxRequest
|
||||
id="transactionDistribution"
|
||||
shouldInvoke={Boolean(serviceName && start && end && transactionName)}
|
||||
fn={loadTransactionDistribution}
|
||||
args={[{ serviceName, start, end, transactionName, kuery }]}
|
||||
selector={getTransactionDistribution}
|
||||
render={render}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 orderBy from 'lodash.orderby';
|
||||
import { createSelector } from 'reselect';
|
||||
import { ReduxRequest } from '../../components/shared/ReduxRequest';
|
||||
import { loadTransactionList } from '../../services/rest';
|
||||
|
||||
const ID = 'transactionList';
|
||||
const INITIAL_DATA = [];
|
||||
|
||||
export const getTransactionList = createSelector(
|
||||
state => state.reduxRequest[ID],
|
||||
state => state.sorting.transaction,
|
||||
(transactionList = {}, transactionSorting) => {
|
||||
const { key: sortKey, descending } = transactionSorting;
|
||||
|
||||
return {
|
||||
...transactionList,
|
||||
data: orderBy(
|
||||
transactionList.data || INITIAL_DATA,
|
||||
sortKey,
|
||||
descending ? 'desc' : 'asc'
|
||||
)
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export function TransactionListRequest({ urlParams, render }) {
|
||||
const { serviceName, start, end, transactionType, kuery } = urlParams;
|
||||
return (
|
||||
<ReduxRequest
|
||||
id={ID}
|
||||
shouldInvoke={Boolean(serviceName && start && end && transactionType)}
|
||||
fn={loadTransactionList}
|
||||
args={[
|
||||
{
|
||||
serviceName,
|
||||
start,
|
||||
end,
|
||||
transactionType,
|
||||
kuery
|
||||
}
|
||||
]}
|
||||
selector={getTransactionList}
|
||||
render={render}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -5,39 +5,16 @@
|
|||
*/
|
||||
|
||||
import { combineReducers } from 'redux';
|
||||
|
||||
import detailsCharts from './detailsCharts';
|
||||
import errorDistribution from './errorDistribution';
|
||||
import errorGroup from './errorGroup';
|
||||
import errorGroupList from './errorGroupList';
|
||||
import license from './license';
|
||||
import location from './location';
|
||||
import overviewCharts from './overviewCharts';
|
||||
import service from './service';
|
||||
import serviceList from './serviceList';
|
||||
import sorting from './sorting';
|
||||
import spans from './spans';
|
||||
import transaction from './transaction';
|
||||
import transactionDistribution from './transactionDistribution';
|
||||
import transactionList from './transactionList';
|
||||
import urlParams from './urlParams';
|
||||
import { reduxRequestReducer } from '../components/shared/ReduxRequest';
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
detailsCharts,
|
||||
errorDistribution,
|
||||
errorGroup,
|
||||
errorGroupList,
|
||||
license,
|
||||
location,
|
||||
overviewCharts,
|
||||
service,
|
||||
serviceList,
|
||||
sorting,
|
||||
spans,
|
||||
transaction,
|
||||
transactionDistribution,
|
||||
transactionList,
|
||||
urlParams
|
||||
urlParams,
|
||||
reduxRequest: reduxRequestReducer
|
||||
});
|
||||
|
||||
export default rootReducer;
|
||||
|
|
|
@ -1,27 +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 _ from 'lodash';
|
||||
import * as rest from '../services/rest';
|
||||
import { createActionTypes, createAction, createReducer } from './apiHelpers';
|
||||
|
||||
const actionTypes = createActionTypes('SERVICE');
|
||||
export const [SERVICE_LOADING, SERVICE_SUCCESS, SERVICE_FAILURE] = actionTypes;
|
||||
|
||||
const INITIAL_DATA = { types: [] };
|
||||
const service = createReducer(actionTypes, INITIAL_DATA);
|
||||
export const loadService = createAction(actionTypes, rest.loadService);
|
||||
|
||||
export function getService(state) {
|
||||
return state.service;
|
||||
}
|
||||
|
||||
export function getDefaultTransactionType(state) {
|
||||
const types = _.get(state.service, 'data.types');
|
||||
return _.first(types);
|
||||
}
|
||||
|
||||
export default service;
|
|
@ -1,21 +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 * as rest from '../services/rest';
|
||||
import { createActionTypes, createAction, createReducer } from './apiHelpers';
|
||||
|
||||
const actionTypes = createActionTypes('SPANS');
|
||||
export const [SPANS_LOADING, SPANS_SUCCESS, SPANS_FAILURE] = actionTypes;
|
||||
|
||||
const INITIAL_DATA = {};
|
||||
const spans = createReducer(actionTypes, INITIAL_DATA);
|
||||
export const loadSpans = createAction(actionTypes, rest.loadSpans);
|
||||
|
||||
export function getSpans(state) {
|
||||
return state.spans;
|
||||
}
|
||||
|
||||
export default spans;
|
|
@ -1,26 +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 * as rest from '../services/rest';
|
||||
import { createActionTypes, createAction, createReducer } from './apiHelpers';
|
||||
|
||||
const actionTypes = createActionTypes('TRANSACTION');
|
||||
export const [
|
||||
TRANSACTION_LOADING,
|
||||
TRANSACTION_SUCCESS,
|
||||
TRANSACTION_FAILURE
|
||||
] = actionTypes;
|
||||
|
||||
const INITIAL_DATA = {};
|
||||
const transaction = createReducer(actionTypes, INITIAL_DATA);
|
||||
|
||||
export const loadTransaction = createAction(actionTypes, rest.loadTransaction);
|
||||
|
||||
export function getTransaction(state) {
|
||||
return state.transaction;
|
||||
}
|
||||
|
||||
export default transaction;
|
|
@ -1,34 +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 * as rest from '../services/rest';
|
||||
import { createActionTypes, createAction, createReducer } from './apiHelpers';
|
||||
|
||||
const actionTypes = createActionTypes('TRANSACTION_DISTRIBUTION');
|
||||
export const [
|
||||
TRANSACTION_DISTRIBUTION_LOADING,
|
||||
TRANSACTION_DISTRIBUTION_SUCCESS,
|
||||
TRANSACTION_DISTRIBUTION_FAILURE
|
||||
] = actionTypes;
|
||||
|
||||
const INITIAL_DATA = { buckets: [], totalHits: 0 };
|
||||
const transactionDistribution = createReducer(actionTypes, INITIAL_DATA);
|
||||
|
||||
export const loadTransactionDistribution = createAction(
|
||||
actionTypes,
|
||||
rest.loadTransactionDistribution
|
||||
);
|
||||
|
||||
export function getTransactionDistribution(state) {
|
||||
return state.transactionDistribution;
|
||||
}
|
||||
|
||||
export function getDefaultTransactionId(state) {
|
||||
const _distribution = getTransactionDistribution(state);
|
||||
return _distribution.data.defaultTransactionId;
|
||||
}
|
||||
|
||||
export default transactionDistribution;
|
|
@ -1,40 +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 orderBy from 'lodash.orderby';
|
||||
import { createSelector } from 'reselect';
|
||||
import * as rest from '../services/rest';
|
||||
import { createActionTypes, createAction, createReducer } from './apiHelpers';
|
||||
|
||||
const actionTypes = createActionTypes('TRANSACTIONS_LIST');
|
||||
export const [
|
||||
TRANSACTIONS_LIST_LOADING,
|
||||
TRANSACTIONS_LIST_SUCCESS,
|
||||
TRANSACTIONS_LIST_FAILURE
|
||||
] = actionTypes;
|
||||
|
||||
const INITIAL_DATA = [];
|
||||
const transactionList = createReducer(actionTypes, INITIAL_DATA);
|
||||
|
||||
export const loadTransactionList = createAction(
|
||||
actionTypes,
|
||||
rest.loadTransactionList
|
||||
);
|
||||
|
||||
export const getTransactionList = createSelector(
|
||||
state => state.transactionList,
|
||||
state => state.sorting.transaction,
|
||||
(transactionList, transactionSorting) => {
|
||||
const { key: sortKey, descending } = transactionSorting;
|
||||
|
||||
return {
|
||||
...transactionList,
|
||||
data: orderBy(transactionList.data, sortKey, descending ? 'desc' : 'asc')
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export default transactionList;
|
|
@ -8,8 +8,8 @@ import _ from 'lodash';
|
|||
import { createSelector } from 'reselect';
|
||||
import { LOCATION_UPDATE } from './location';
|
||||
import { toQuery, legacyDecodeURIComponent } from '../utils/url';
|
||||
import { getDefaultTransactionId } from './transactionDistribution';
|
||||
import { getDefaultTransactionType } from './service';
|
||||
import { getDefaultTransactionId } from './reduxRequest/transactionDistribution';
|
||||
import { getDefaultTransactionType } from './reduxRequest/serviceDetails';
|
||||
|
||||
// ACTION TYPES
|
||||
export const TIMEPICKER_UPDATE = 'TIMEPICKER_UPDATE';
|
||||
|
@ -39,7 +39,8 @@ function urlParams(state = {}, action) {
|
|||
page,
|
||||
sortBy,
|
||||
sortOrder,
|
||||
q
|
||||
q,
|
||||
kuery
|
||||
} = toQuery(action.location.search);
|
||||
|
||||
return {
|
||||
|
@ -53,6 +54,7 @@ function urlParams(state = {}, action) {
|
|||
transactionId,
|
||||
detailTab,
|
||||
spanId: toNumber(spanId),
|
||||
kuery: legacyDecodeURIComponent(kuery),
|
||||
|
||||
// path params
|
||||
serviceName,
|
||||
|
|
|
@ -51,6 +51,21 @@ export function mountWithRouterAndStore(
|
|||
return mount(Component, options);
|
||||
}
|
||||
|
||||
export function mountWithStore(Component, storeState = {}) {
|
||||
const store = createMockStore(storeState);
|
||||
|
||||
const options = {
|
||||
context: {
|
||||
store
|
||||
},
|
||||
childContextTypes: {
|
||||
store: PropTypes.object.isRequired
|
||||
}
|
||||
};
|
||||
|
||||
return mount(Component, options);
|
||||
}
|
||||
|
||||
export function mockMoment() {
|
||||
// avoid timezone issues
|
||||
jest.spyOn(moment.prototype, 'format').mockImplementation(function() {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue