Replace api calls with ReduxRequest (#18815)

This commit is contained in:
Søren Louv-Jansen 2018-05-04 20:29:00 +02:00 committed by GitHub
parent 3028a6fd80
commit 14876364c7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
74 changed files with 1337 additions and 1754 deletions

View file

@ -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;

View file

@ -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;

View file

@ -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);

View file

@ -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;

View file

@ -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
};

View file

@ -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);

View file

@ -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;

View file

@ -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 = {}) {

View file

@ -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" />}
/>
);
};

View file

@ -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;

View file

@ -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;

View file

@ -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) => {

View file

@ -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;

View file

@ -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);

View file

@ -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;

View file

@ -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);

View file

@ -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);

View file

@ -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;

View file

@ -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);

View file

@ -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;

View file

@ -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);

View file

@ -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;

View file

@ -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>
);
}

View file

@ -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);

View file

@ -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;

View file

@ -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();
});
});

View file

@ -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>
`;

View file

@ -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
};

View file

@ -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']);

View file

@ -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] || {}
};

View file

@ -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;
}
}

View 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
};

View file

@ -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;

View file

@ -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>

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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')
);

View file

@ -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,

View file

@ -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",
],
}
`;

View file

@ -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
});
});
});
});
});

View file

@ -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();
});
});

View file

@ -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
});
});
});

View file

@ -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": {}
}
]

View file

@ -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);
};

View file

@ -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
);

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
};

View file

@ -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;

View file

@ -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
);

View file

@ -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>');
});
});
});

View file

@ -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}
/>
);
}

View 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 { 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}
/>
);
}

View 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}
/>
);
}

View 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 { 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}
/>
);
}

View 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
};
}

View 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}
/>
);
}

View file

@ -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}
/>
);
}

View file

@ -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
};

View file

@ -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}
/>
);
}

View 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}
/>
);
}

View 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 { 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}
/>
);
}

View file

@ -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}
/>
);
}

View file

@ -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}
/>
);
}

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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,

View file

@ -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() {