mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[APM] useUrlParams as separate hook + removes redux 🎉 (#34792)
* Exploring a useUrlParams hook separate from useLocation
* Refactors url params hook into context + hook
* Fixes unmount async bugs in some components
* Rewrites datepicker tests
* Fixes some import reference problems
* Moves urlParams logic out of store and into context directory, updates references
* Makes urlParams context update itself on location changes
* Adds url params tests
* Removed left-over debug additions
* Small improvement for types based on review feedback 👍
* Converts url params to hooks for ErrorGroupDetails component
* Converts url params to hooks for ServiceDetails component
* Converts url params to hooks for ServiceOverview component
* Converts url params to hooks for TraceOverview component
* Converts url params to hooks for TransactionDetails component
* Converts url params to hooks for KueryBar component (plus TS convert)
* Cleanup removing redux
* Fixes tests after removing redux
* Updates type reference to use ui/ import
* Moves selectors folder into public and deletes store folder
* Removes unnecessary mountWithRouterAndStore
* Correctly orders imports to satisfy lint gods
* Removes unused imports
This commit is contained in:
parent
4f05920f53
commit
1794c8b01c
78 changed files with 927 additions and 1373 deletions
|
@ -20,10 +20,11 @@ import { first } from 'lodash';
|
|||
import { idx } from '@kbn/elastic-idx';
|
||||
import { ErrorGroupAPIResponse } from '../../../../../server/lib/errors/get_error_group';
|
||||
import { APMError } from '../../../../../typings/es_schemas/ui/APMError';
|
||||
import { IUrlParams } from '../../../../store/urlParams';
|
||||
import { IUrlParams } from '../../../../context/UrlParamsContext/types';
|
||||
import { px, unit } from '../../../../style/variables';
|
||||
import { DiscoverErrorLink } from '../../../shared/Links/DiscoverLinks/DiscoverErrorLink';
|
||||
import { fromQuery, history, toQuery } from '../../../shared/Links/url_helpers';
|
||||
import { fromQuery, toQuery } from '../../../shared/Links/url_helpers';
|
||||
import { history } from '../../../../utils/history';
|
||||
import { ErrorMetadata } from '../../../shared/MetadataTable/ErrorMetadata';
|
||||
import { Stacktrace } from '../../../shared/Stacktrace';
|
||||
import {
|
||||
|
|
|
@ -1,24 +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 { IReduxState } from '../../../store/rootReducer';
|
||||
import { getUrlParams } from '../../../store/urlParams';
|
||||
import { ErrorGroupDetailsView } from './view';
|
||||
|
||||
function mapStateToProps(state = {} as IReduxState) {
|
||||
return {
|
||||
urlParams: getUrlParams(state),
|
||||
location: state.location
|
||||
};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {};
|
||||
|
||||
export const ErrorGroupDetails = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(ErrorGroupDetailsView);
|
|
@ -15,7 +15,6 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import theme from '@elastic/eui/dist/eui_theme_light.json';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Location } from 'history';
|
||||
import React, { Fragment } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { idx } from '@kbn/elastic-idx';
|
||||
|
@ -25,12 +24,12 @@ import {
|
|||
loadErrorDistribution,
|
||||
loadErrorGroupDetails
|
||||
} from '../../../services/rest/apm/error_groups';
|
||||
import { IUrlParams } from '../../../store/urlParams';
|
||||
import { fontFamilyCode, fontSizes, px, units } from '../../../style/variables';
|
||||
// @ts-ignore
|
||||
import { FilterBar } from '../../shared/FilterBar';
|
||||
import { DetailView } from './DetailView';
|
||||
import { ErrorDistribution } from './Distribution';
|
||||
import { useLocation } from '../../../hooks/useLocation';
|
||||
import { useUrlParams } from '../../../hooks/useUrlParams';
|
||||
|
||||
const Titles = styled.div`
|
||||
margin-bottom: ${px(units.plus)};
|
||||
|
@ -61,12 +60,9 @@ function getShortGroupId(errorGroupId?: string) {
|
|||
return errorGroupId.slice(0, 5);
|
||||
}
|
||||
|
||||
interface Props {
|
||||
urlParams: IUrlParams;
|
||||
location: Location;
|
||||
}
|
||||
|
||||
export function ErrorGroupDetailsView({ urlParams, location }: Props) {
|
||||
export function ErrorGroupDetails() {
|
||||
const location = useLocation();
|
||||
const { urlParams } = useUrlParams();
|
||||
const { serviceName, start, end, errorGroupId } = urlParams;
|
||||
|
||||
const { data: errorGroupData } = useFetcher(
|
|
@ -6,12 +6,8 @@
|
|||
|
||||
import { mount } from 'enzyme';
|
||||
import { Location } from 'history';
|
||||
import createHistory from 'history/createHashHistory';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
// @ts-ignore
|
||||
import { createMockStore } from 'redux-test-utils';
|
||||
import { mockMoment, toJson } from '../../../../../utils/testHelpers';
|
||||
import { ErrorGroupList } from '../index';
|
||||
import props from './props.json';
|
||||
|
@ -38,39 +34,8 @@ describe('ErrorGroupOverview -> List', () => {
|
|||
});
|
||||
|
||||
it('should render with data', () => {
|
||||
const storeState = { location: {} };
|
||||
const wrapper = mountWithRouterAndStore(
|
||||
<ErrorGroupList {...props} />,
|
||||
storeState
|
||||
);
|
||||
const wrapper = mount(<ErrorGroupList {...props} />);
|
||||
|
||||
expect(toJson(wrapper)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
export function mountWithRouterAndStore(
|
||||
Component: React.ReactElement,
|
||||
storeState = {}
|
||||
) {
|
||||
const store = createMockStore(storeState);
|
||||
const history = createHistory();
|
||||
|
||||
const options = {
|
||||
context: {
|
||||
store,
|
||||
router: {
|
||||
history,
|
||||
route: {
|
||||
match: { path: '/', url: '/', params: {}, isExact: true },
|
||||
location: { pathname: '/', search: '', hash: '', key: '4yyjf5' }
|
||||
}
|
||||
}
|
||||
},
|
||||
childContextTypes: {
|
||||
store: PropTypes.object.isRequired,
|
||||
router: PropTypes.object.isRequired
|
||||
}
|
||||
};
|
||||
|
||||
return mount(Component, options);
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import React, { Component } from 'react';
|
|||
import styled from 'styled-components';
|
||||
import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n';
|
||||
import { ErrorGroupListAPIResponse } from '../../../../../server/lib/errors/get_error_groups';
|
||||
import { IUrlParams } from '../../../../store/urlParams';
|
||||
import { IUrlParams } from '../../../../context/UrlParamsContext/types';
|
||||
import {
|
||||
fontFamilyCode,
|
||||
fontSizes,
|
||||
|
@ -22,7 +22,8 @@ import {
|
|||
unit
|
||||
} from '../../../../style/variables';
|
||||
import { APMLink } from '../../../shared/Links/APMLink';
|
||||
import { fromQuery, history, toQuery } from '../../../shared/Links/url_helpers';
|
||||
import { fromQuery, toQuery } from '../../../shared/Links/url_helpers';
|
||||
import { history } from '../../../../utils/history';
|
||||
|
||||
function paginateItems({
|
||||
items,
|
||||
|
|
|
@ -19,7 +19,7 @@ import {
|
|||
loadErrorDistribution,
|
||||
loadErrorGroupList
|
||||
} from '../../../services/rest/apm/error_groups';
|
||||
import { IUrlParams } from '../../../store/urlParams';
|
||||
import { IUrlParams } from '../../../context/UrlParamsContext/types';
|
||||
import { ErrorDistribution } from '../ErrorGroupDetails/Distribution';
|
||||
import { ErrorGroupList } from './List';
|
||||
|
||||
|
|
|
@ -20,14 +20,14 @@ const homeTabs: IHistoryTab[] = [
|
|||
name: i18n.translate('xpack.apm.home.servicesTabLabel', {
|
||||
defaultMessage: 'Services'
|
||||
}),
|
||||
render: props => <ServiceOverview {...props} />
|
||||
render: () => <ServiceOverview />
|
||||
},
|
||||
{
|
||||
path: '/traces',
|
||||
name: i18n.translate('xpack.apm.home.tracesTabLabel', {
|
||||
defaultMessage: 'Traces'
|
||||
}),
|
||||
render: props => <TraceOverview {...props} />
|
||||
render: () => <TraceOverview />
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ exports[`Home component should render 1`] = `
|
|||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
<FilterBar />
|
||||
<withRouter(HistoryTabsWithoutRouter)
|
||||
<HistoryTabs
|
||||
tabs={
|
||||
Array [
|
||||
Object {
|
||||
|
|
|
@ -8,8 +8,6 @@ import React from 'react';
|
|||
import { Route, Switch } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
import { px, topNavHeight, unit, units } from '../../../style/variables';
|
||||
// @ts-ignore
|
||||
import ConnectRouterToRedux from '../../shared/ConnectRouterToRedux';
|
||||
import { GlobalFetchIndicator } from './GlobalFetchIndicator';
|
||||
import { LicenseCheck } from './LicenseCheck';
|
||||
import { routes } from './routeConfig';
|
||||
|
@ -27,7 +25,6 @@ export function Main() {
|
|||
<GlobalFetchIndicator>
|
||||
<MainContainer data-test-subj="apmMainContainer">
|
||||
<UpdateBreadcrumbs />
|
||||
<Route component={ConnectRouterToRedux} />
|
||||
<Route component={ScrollToTopOnPathChange} />
|
||||
<LicenseCheck>
|
||||
<Switch>
|
||||
|
|
|
@ -8,7 +8,7 @@ import { EuiTitle } from '@elastic/eui';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { Coordinate } from '../../../../typings/timeseries';
|
||||
import { CPUMetricSeries } from '../../../store/selectors/chartSelectors';
|
||||
import { CPUMetricSeries } from '../../../selectors/chartSelectors';
|
||||
import { asPercent } from '../../../utils/formatters';
|
||||
// @ts-ignore
|
||||
import CustomPlot from '../../shared/charts/CustomPlot';
|
||||
|
|
|
@ -8,7 +8,7 @@ import { EuiTitle } from '@elastic/eui';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { Coordinate } from '../../../../typings/timeseries';
|
||||
import { MemoryMetricSeries } from '../../../store/selectors/chartSelectors';
|
||||
import { MemoryMetricSeries } from '../../../selectors/chartSelectors';
|
||||
import { asPercent } from '../../../utils/formatters';
|
||||
// @ts-ignore
|
||||
import CustomPlot from '../../shared/charts/CustomPlot';
|
||||
|
|
|
@ -5,61 +5,62 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Location } from 'history';
|
||||
import React from 'react';
|
||||
import { IUrlParams } from '../../../store/urlParams';
|
||||
import { IUrlParams } from '../../../context/UrlParamsContext/types';
|
||||
import { HistoryTabs } from '../../shared/HistoryTabs';
|
||||
import { ErrorGroupOverview } from '../ErrorGroupOverview';
|
||||
import { TransactionOverview } from '../TransactionOverview';
|
||||
import { ServiceMetrics } from './ServiceMetrics';
|
||||
import { useLocation } from '../../../hooks/useLocation';
|
||||
|
||||
interface TabsProps {
|
||||
interface Props {
|
||||
transactionTypes: string[];
|
||||
urlParams: IUrlParams;
|
||||
location: Location;
|
||||
isRumAgent?: boolean;
|
||||
}
|
||||
|
||||
export class ServiceDetailTabs extends React.Component<TabsProps> {
|
||||
public render() {
|
||||
const { transactionTypes, urlParams, location, isRumAgent } = this.props;
|
||||
const { serviceName } = urlParams;
|
||||
const headTransactionType = transactionTypes[0];
|
||||
const transactionsTab = {
|
||||
name: i18n.translate('xpack.apm.serviceDetails.transactionsTabLabel', {
|
||||
defaultMessage: 'Transactions'
|
||||
}),
|
||||
path: headTransactionType
|
||||
? `/${serviceName}/transactions/${headTransactionType}`
|
||||
: `/${serviceName}/transactions`,
|
||||
routePath: `/${serviceName}/transactions/:transactionType?`,
|
||||
render: () => (
|
||||
<TransactionOverview
|
||||
urlParams={urlParams}
|
||||
serviceTransactionTypes={transactionTypes}
|
||||
/>
|
||||
)
|
||||
};
|
||||
const errorsTab = {
|
||||
name: i18n.translate('xpack.apm.serviceDetails.errorsTabLabel', {
|
||||
defaultMessage: 'Errors'
|
||||
}),
|
||||
path: `/${serviceName}/errors`,
|
||||
render: () => {
|
||||
return <ErrorGroupOverview urlParams={urlParams} location={location} />;
|
||||
}
|
||||
};
|
||||
const metricsTab = {
|
||||
name: i18n.translate('xpack.apm.serviceDetails.metricsTabLabel', {
|
||||
defaultMessage: 'Metrics'
|
||||
}),
|
||||
path: `/${serviceName}/metrics`,
|
||||
render: () => <ServiceMetrics urlParams={urlParams} location={location} />
|
||||
};
|
||||
const tabs = isRumAgent
|
||||
? [transactionsTab, errorsTab]
|
||||
: [transactionsTab, errorsTab, metricsTab];
|
||||
export function ServiceDetailTabs({
|
||||
transactionTypes,
|
||||
urlParams,
|
||||
isRumAgent
|
||||
}: Props) {
|
||||
const location = useLocation();
|
||||
const { serviceName } = urlParams;
|
||||
const headTransactionType = transactionTypes[0];
|
||||
const transactionsTab = {
|
||||
name: i18n.translate('xpack.apm.serviceDetails.transactionsTabLabel', {
|
||||
defaultMessage: 'Transactions'
|
||||
}),
|
||||
path: headTransactionType
|
||||
? `/${serviceName}/transactions/${headTransactionType}`
|
||||
: `/${serviceName}/transactions`,
|
||||
routePath: `/${serviceName}/transactions/:transactionType?`,
|
||||
render: () => (
|
||||
<TransactionOverview
|
||||
urlParams={urlParams}
|
||||
serviceTransactionTypes={transactionTypes}
|
||||
/>
|
||||
)
|
||||
};
|
||||
const errorsTab = {
|
||||
name: i18n.translate('xpack.apm.serviceDetails.errorsTabLabel', {
|
||||
defaultMessage: 'Errors'
|
||||
}),
|
||||
path: `/${serviceName}/errors`,
|
||||
render: () => {
|
||||
return <ErrorGroupOverview urlParams={urlParams} location={location} />;
|
||||
}
|
||||
};
|
||||
const metricsTab = {
|
||||
name: i18n.translate('xpack.apm.serviceDetails.metricsTabLabel', {
|
||||
defaultMessage: 'Metrics'
|
||||
}),
|
||||
path: `/${serviceName}/metrics`,
|
||||
render: () => <ServiceMetrics urlParams={urlParams} location={location} />
|
||||
};
|
||||
const tabs = isRumAgent
|
||||
? [transactionsTab, errorsTab]
|
||||
: [transactionsTab, errorsTab, metricsTab];
|
||||
|
||||
return <HistoryTabs tabs={tabs} />;
|
||||
}
|
||||
return <HistoryTabs tabs={tabs} />;
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import React, { Component } from 'react';
|
|||
import { toastNotifications } from 'ui/notify';
|
||||
import { startMLJob } from '../../../../../services/rest/ml';
|
||||
import { getAPMIndexPattern } from '../../../../../services/rest/savedObjects';
|
||||
import { IUrlParams } from '../../../../../store/urlParams';
|
||||
import { IUrlParams } from '../../../../../context/UrlParamsContext/types';
|
||||
import { MLJobLink } from '../../../../shared/Links/MachineLearningLinks/MLJobLink';
|
||||
import { MachineLearningFlyoutView } from './view';
|
||||
|
||||
|
@ -30,19 +30,26 @@ export class MachineLearningFlyout extends Component<Props, State> {
|
|||
isCreatingJob: false,
|
||||
hasIndexPattern: false
|
||||
};
|
||||
public willUnmount = false;
|
||||
public mounted = false;
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.willUnmount = true;
|
||||
this.mounted = false;
|
||||
}
|
||||
|
||||
public async componentDidMount() {
|
||||
this.mounted = true;
|
||||
const indexPattern = await getAPMIndexPattern();
|
||||
if (!this.willUnmount) {
|
||||
// TODO: this is causing warning from react because setState happens after
|
||||
// the component has been unmounted - dispite of the checks
|
||||
this.setState({ hasIndexPattern: !!indexPattern });
|
||||
}
|
||||
|
||||
// setTimeout:0 hack forces the state update to wait for next tick
|
||||
// in case the component is mid-unmount :/
|
||||
setTimeout(() => {
|
||||
if (!this.mounted) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
hasIndexPattern: !!indexPattern
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
|
||||
public onClickCreate = async () => {
|
||||
|
|
|
@ -32,7 +32,7 @@ import React, { Component } from 'react';
|
|||
import styled from 'styled-components';
|
||||
import chrome from 'ui/chrome';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import { IUrlParams } from '../../../../store/urlParams';
|
||||
import { IUrlParams } from '../../../../context/UrlParamsContext/types';
|
||||
import { KibanaLink } from '../../../shared/Links/KibanaLink';
|
||||
import { createErrorGroupWatch, Schedule } from './createErrorGroupWatch';
|
||||
import { ElasticDocsLink } from '../../../shared/Links/ElasticDocsLink';
|
||||
|
|
|
@ -14,7 +14,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import { memoize } from 'lodash';
|
||||
import React, { Fragment } from 'react';
|
||||
import chrome from 'ui/chrome';
|
||||
import { IUrlParams } from '../../../../store/urlParams';
|
||||
import { IUrlParams } from '../../../../context/UrlParamsContext/types';
|
||||
import { LicenseContext } from '../../Main/LicenseCheck';
|
||||
import { MachineLearningFlyout } from './MachineLearningFlyout';
|
||||
import { WatcherFlyout } from './WatcherFlyout';
|
||||
|
|
|
@ -18,7 +18,7 @@ import { useFetcher } from '../../../hooks/useFetcher';
|
|||
import { useServiceMetricCharts } from '../../../hooks/useServiceMetricCharts';
|
||||
import { useTransactionOverviewCharts } from '../../../hooks/useTransactionOverviewCharts';
|
||||
import { loadErrorDistribution } from '../../../services/rest/apm/error_groups';
|
||||
import { IUrlParams } from '../../../store/urlParams';
|
||||
import { IUrlParams } from '../../../context/UrlParamsContext/types';
|
||||
import { SyncChartGroup } from '../../shared/charts/SyncChartGroup';
|
||||
import { TransactionCharts } from '../../shared/charts/TransactionCharts';
|
||||
import { ErrorDistribution } from '../ErrorGroupDetails/Distribution';
|
||||
|
|
|
@ -4,17 +4,55 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { IReduxState } from '../../../store/rootReducer';
|
||||
import { getUrlParams } from '../../../store/urlParams';
|
||||
import { ServiceDetailsView } from './view';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { useFetcher } from '../../../hooks/useFetcher';
|
||||
import { loadServiceDetails } from '../../../services/rest/apm/services';
|
||||
import { FilterBar } from '../../shared/FilterBar';
|
||||
import { ServiceDetailTabs } from './ServiceDetailTabs';
|
||||
import { ServiceIntegrations } from './ServiceIntegrations';
|
||||
import { isRumAgentName } from '../../../../common/agent_name';
|
||||
import { useUrlParams } from '../../../hooks/useUrlParams';
|
||||
|
||||
function mapStateToProps(state = {} as IReduxState) {
|
||||
return {
|
||||
urlParams: getUrlParams(state)
|
||||
};
|
||||
export function ServiceDetails() {
|
||||
const { urlParams } = useUrlParams();
|
||||
const { serviceName, start, end, kuery } = urlParams;
|
||||
const { data: serviceDetailsData } = useFetcher(
|
||||
() => loadServiceDetails({ serviceName, start, end, kuery }),
|
||||
[serviceName, start, end, kuery]
|
||||
);
|
||||
|
||||
if (!serviceDetailsData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isRumAgent = isRumAgentName(serviceDetailsData.agentName || '');
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="l">
|
||||
<h1>{urlParams.serviceName}</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<ServiceIntegrations
|
||||
transactionTypes={serviceDetailsData.types}
|
||||
urlParams={urlParams}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer />
|
||||
|
||||
<FilterBar />
|
||||
|
||||
<ServiceDetailTabs
|
||||
urlParams={urlParams}
|
||||
transactionTypes={serviceDetailsData.types}
|
||||
isRumAgent={isRumAgent}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
const ServiceDetails = connect(mapStateToProps)(ServiceDetailsView);
|
||||
|
||||
export { ServiceDetails };
|
||||
|
|
|
@ -1,65 +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 { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
import { Location } from 'history';
|
||||
import React from 'react';
|
||||
import { useFetcher } from '../../../hooks/useFetcher';
|
||||
import { loadServiceDetails } from '../../../services/rest/apm/services';
|
||||
import { IUrlParams } from '../../../store/urlParams';
|
||||
// @ts-ignore
|
||||
import { FilterBar } from '../../shared/FilterBar';
|
||||
import { ServiceDetailTabs } from './ServiceDetailTabs';
|
||||
import { ServiceIntegrations } from './ServiceIntegrations';
|
||||
import { isRumAgentName } from '../../../../common/agent_name';
|
||||
|
||||
interface Props {
|
||||
urlParams: IUrlParams;
|
||||
location: Location;
|
||||
}
|
||||
|
||||
export function ServiceDetailsView({ urlParams, location }: Props) {
|
||||
const { serviceName, start, end, kuery } = urlParams;
|
||||
const { data: serviceDetailsData } = useFetcher(
|
||||
() => loadServiceDetails({ serviceName, start, end, kuery }),
|
||||
[serviceName, start, end, kuery]
|
||||
);
|
||||
|
||||
if (!serviceDetailsData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isRumAgent = isRumAgentName(serviceDetailsData.agentName || '');
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="l">
|
||||
<h1>{urlParams.serviceName}</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<ServiceIntegrations
|
||||
transactionTypes={serviceDetailsData.types}
|
||||
urlParams={urlParams}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer />
|
||||
|
||||
<FilterBar />
|
||||
|
||||
<ServiceDetailTabs
|
||||
location={location}
|
||||
urlParams={urlParams}
|
||||
transactionTypes={serviceDetailsData.types}
|
||||
isRumAgent={isRumAgent}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
|
@ -5,23 +5,14 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { render, wait, waitForElement } from 'react-testing-library';
|
||||
import 'react-testing-library/cleanup-after-each';
|
||||
import { toastNotifications } from 'ui/notify';
|
||||
import * as apmRestServices from '../../../../services/rest/apm/services';
|
||||
// @ts-ignore
|
||||
import configureStore from '../../../../store/config/configureStore';
|
||||
import { ServiceOverview } from '../view';
|
||||
import { ServiceOverview } from '..';
|
||||
|
||||
function renderServiceOverview() {
|
||||
const store = configureStore();
|
||||
|
||||
return render(
|
||||
<Provider store={store}>
|
||||
<ServiceOverview urlParams={{}} />
|
||||
</Provider>
|
||||
);
|
||||
return render(<ServiceOverview />);
|
||||
}
|
||||
|
||||
describe('Service Overview -> View', () => {
|
||||
|
|
|
@ -1,18 +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 { IReduxState } from '../../../store/rootReducer';
|
||||
import { getUrlParams } from '../../../store/urlParams';
|
||||
import { ServiceOverview as View } from './view';
|
||||
|
||||
function mapStateToProps(state = {} as IReduxState) {
|
||||
return {
|
||||
urlParams: getUrlParams(state)
|
||||
};
|
||||
}
|
||||
|
||||
export const ServiceOverview = connect(mapStateToProps)(View);
|
|
@ -13,13 +13,9 @@ import { toastNotifications } from 'ui/notify';
|
|||
import url from 'url';
|
||||
import { useFetcher } from '../../../hooks/useFetcher';
|
||||
import { loadServiceList } from '../../../services/rest/apm/services';
|
||||
import { IUrlParams } from '../../../store/urlParams';
|
||||
import { NoServicesMessage } from './NoServicesMessage';
|
||||
import { ServiceList } from './ServiceList';
|
||||
|
||||
interface Props {
|
||||
urlParams: IUrlParams;
|
||||
}
|
||||
import { useUrlParams } from '../../../hooks/useUrlParams';
|
||||
|
||||
const initalData = {
|
||||
items: [],
|
||||
|
@ -29,7 +25,8 @@ const initalData = {
|
|||
|
||||
let hasDisplayedToast = false;
|
||||
|
||||
export function ServiceOverview({ urlParams }: Props) {
|
||||
export function ServiceOverview() {
|
||||
const { urlParams } = useUrlParams();
|
||||
const { start, end, kuery } = urlParams;
|
||||
const { data = initalData } = useFetcher(
|
||||
() => loadServiceList({ start, end, kuery }),
|
|
@ -4,15 +4,24 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { IReduxState } from '../../../store/rootReducer';
|
||||
import { getUrlParams } from '../../../store/urlParams';
|
||||
import { TraceOverview as View } from './view';
|
||||
import { EuiPanel } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher';
|
||||
import { loadTraceList } from '../../../services/rest/apm/traces';
|
||||
import { TraceList } from './TraceList';
|
||||
import { useUrlParams } from '../../../hooks/useUrlParams';
|
||||
|
||||
function mapStateToProps(state = {} as IReduxState) {
|
||||
return {
|
||||
urlParams: getUrlParams(state)
|
||||
};
|
||||
export function TraceOverview() {
|
||||
const { urlParams } = useUrlParams();
|
||||
const { start, end, kuery } = urlParams;
|
||||
const { status, data = [] } = useFetcher(
|
||||
() => loadTraceList({ start, end, kuery }),
|
||||
[start, end, kuery]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPanel>
|
||||
<TraceList items={data} isLoading={status === FETCH_STATUS.LOADING} />
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
|
||||
export const TraceOverview = connect(mapStateToProps)(View);
|
||||
|
|
|
@ -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 { EuiPanel } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { FETCH_STATUS, useFetcher } from '../../../hooks/useFetcher';
|
||||
import { loadTraceList } from '../../../services/rest/apm/traces';
|
||||
import { IUrlParams } from '../../../store/urlParams';
|
||||
import { TraceList } from './TraceList';
|
||||
|
||||
interface Props {
|
||||
urlParams: IUrlParams;
|
||||
}
|
||||
|
||||
export function TraceOverview(props: Props) {
|
||||
const { start, end, kuery } = props.urlParams;
|
||||
const { status, data = [] } = useFetcher(
|
||||
() => loadTraceList({ start, end, kuery }),
|
||||
[start, end, kuery]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPanel>
|
||||
<TraceList items={data} isLoading={status === FETCH_STATUS.LOADING} />
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
|
@ -11,12 +11,13 @@ import { Location } from 'history';
|
|||
import React, { Component } from 'react';
|
||||
import { ITransactionDistributionAPIResponse } from '../../../../../server/lib/transactions/distribution';
|
||||
import { IBucket } from '../../../../../server/lib/transactions/distribution/get_buckets/transform';
|
||||
import { IUrlParams } from '../../../../store/urlParams';
|
||||
import { IUrlParams } from '../../../../context/UrlParamsContext/types';
|
||||
import { getTimeFormatter, timeUnit } from '../../../../utils/formatters';
|
||||
// @ts-ignore
|
||||
import Histogram from '../../../shared/charts/Histogram';
|
||||
import { EmptyMessage } from '../../../shared/EmptyMessage';
|
||||
import { fromQuery, history, toQuery } from '../../../shared/Links/url_helpers';
|
||||
import { fromQuery, toQuery } from '../../../shared/Links/url_helpers';
|
||||
import { history } from '../../../../utils/history';
|
||||
|
||||
interface IChartPoint {
|
||||
sample?: IBucket['sample'];
|
||||
|
|
|
@ -9,8 +9,9 @@ import { i18n } from '@kbn/i18n';
|
|||
import { Location } from 'history';
|
||||
import React from 'react';
|
||||
import { Transaction } from '../../../../../typings/es_schemas/ui/Transaction';
|
||||
import { IUrlParams } from '../../../../store/urlParams';
|
||||
import { fromQuery, history, toQuery } from '../../../shared/Links/url_helpers';
|
||||
import { IUrlParams } from '../../../../context/UrlParamsContext/types';
|
||||
import { fromQuery, toQuery } from '../../../shared/Links/url_helpers';
|
||||
import { history } from '../../../../utils/history';
|
||||
import { TransactionMetadata } from '../../../shared/MetadataTable/TransactionMetadata';
|
||||
import { WaterfallContainer } from './WaterfallContainer';
|
||||
import { IWaterfall } from './WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers';
|
||||
|
|
|
@ -9,15 +9,15 @@ import React, { Component } from 'react';
|
|||
// @ts-ignore
|
||||
import { StickyContainer } from 'react-sticky';
|
||||
import styled from 'styled-components';
|
||||
import { IUrlParams } from '../../../../../../store/urlParams';
|
||||
import { IUrlParams } from '../../../../../../context/UrlParamsContext/types';
|
||||
// @ts-ignore
|
||||
import Timeline from '../../../../../shared/charts/Timeline';
|
||||
import {
|
||||
APMQueryParams,
|
||||
fromQuery,
|
||||
history,
|
||||
toQuery
|
||||
} from '../../../../../shared/Links/url_helpers';
|
||||
import { history } from '../../../../../../utils/history';
|
||||
import { AgentMark } from '../get_agent_marks';
|
||||
import { SpanFlyout } from './SpanFlyout';
|
||||
import { TransactionFlyout } from './TransactionFlyout';
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import { Location } from 'history';
|
||||
import React from 'react';
|
||||
import { Transaction } from '../../../../../../typings/es_schemas/ui/Transaction';
|
||||
import { IUrlParams } from '../../../../../store/urlParams';
|
||||
import { IUrlParams } from '../../../../../context/UrlParamsContext/types';
|
||||
import { getAgentMarks } from './get_agent_marks';
|
||||
import { ServiceLegends } from './ServiceLegends';
|
||||
import { Waterfall } from './Waterfall';
|
||||
|
|
|
@ -17,7 +17,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import { Location } from 'history';
|
||||
import React from 'react';
|
||||
import { Transaction as ITransaction } from '../../../../../typings/es_schemas/ui/Transaction';
|
||||
import { IUrlParams } from '../../../../store/urlParams';
|
||||
import { IUrlParams } from '../../../../context/UrlParamsContext/types';
|
||||
import { TransactionLink } from '../../../shared/Links/TransactionLink';
|
||||
import { TransactionActionMenu } from '../../../shared/TransactionActionMenu/TransactionActionMenu';
|
||||
import { StickyTransactionProperties } from './StickyTransactionProperties';
|
||||
|
|
|
@ -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 { connect } from 'react-redux';
|
||||
import { IReduxState } from '../../../store/rootReducer';
|
||||
import { getUrlParams } from '../../../store/urlParams';
|
||||
import { TransactionDetailsView } from './view';
|
||||
|
||||
function mapStateToProps(state = {} as IReduxState) {
|
||||
return {
|
||||
location: state.location,
|
||||
urlParams: getUrlParams(state)
|
||||
};
|
||||
}
|
||||
|
||||
export const TransactionDetails = connect(mapStateToProps)(
|
||||
TransactionDetailsView
|
||||
);
|
|
@ -6,25 +6,22 @@
|
|||
|
||||
import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { Location } from 'history';
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import { useTransactionDetailsCharts } from '../../../hooks/useTransactionDetailsCharts';
|
||||
import { useTransactionDistribution } from '../../../hooks/useTransactionDistribution';
|
||||
import { useWaterfall } from '../../../hooks/useWaterfall';
|
||||
import { IUrlParams } from '../../../store/urlParams';
|
||||
import { TransactionCharts } from '../../shared/charts/TransactionCharts';
|
||||
import { EmptyMessage } from '../../shared/EmptyMessage';
|
||||
import { FilterBar } from '../../shared/FilterBar';
|
||||
import { TransactionDistribution } from './Distribution';
|
||||
import { Transaction } from './Transaction';
|
||||
import { useLocation } from '../../../hooks/useLocation';
|
||||
import { useUrlParams } from '../../../hooks/useUrlParams';
|
||||
|
||||
interface Props {
|
||||
urlParams: IUrlParams;
|
||||
location: Location;
|
||||
}
|
||||
|
||||
export function TransactionDetailsView({ urlParams, location }: Props) {
|
||||
export function TransactionDetails() {
|
||||
const location = useLocation();
|
||||
const { urlParams } = useUrlParams();
|
||||
const { data: distributionData } = useTransactionDistribution(urlParams);
|
||||
const { data: transactionDetailsChartsData } = useTransactionDetailsCharts(
|
||||
urlParams
|
|
@ -6,13 +6,10 @@
|
|||
|
||||
import createHistory from 'history/createHashHistory';
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { queryByLabelText, render } from 'react-testing-library';
|
||||
import { TransactionOverview } from '..';
|
||||
// @ts-ignore
|
||||
import configureStore from '../../../../store/config/configureStore';
|
||||
import { IUrlParams } from '../../../../store/urlParams';
|
||||
import { IUrlParams } from '../../../../context/UrlParamsContext/types';
|
||||
|
||||
// Suppress warnings about "act" until async/await syntax is supported: https://github.com/facebook/react/issues/14769
|
||||
/* eslint-disable no-console */
|
||||
|
@ -28,16 +25,13 @@ function setup(props: {
|
|||
urlParams: IUrlParams;
|
||||
serviceTransactionTypes: string[];
|
||||
}) {
|
||||
const store = configureStore();
|
||||
const history = createHistory();
|
||||
history.replace = jest.fn();
|
||||
|
||||
const { container } = render(
|
||||
<Provider store={store}>
|
||||
<Router history={history}>
|
||||
<TransactionOverview {...props} />
|
||||
</Router>
|
||||
</Provider>
|
||||
<Router history={history}>
|
||||
<TransactionOverview {...props} />
|
||||
</Router>
|
||||
);
|
||||
|
||||
return { container, history };
|
||||
|
|
|
@ -18,7 +18,7 @@ import React from 'react';
|
|||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
import { useTransactionList } from '../../../hooks/useTransactionList';
|
||||
import { useTransactionOverviewCharts } from '../../../hooks/useTransactionOverviewCharts';
|
||||
import { IUrlParams } from '../../../store/urlParams';
|
||||
import { IUrlParams } from '../../../context/UrlParamsContext/types';
|
||||
import { TransactionCharts } from '../../shared/charts/TransactionCharts';
|
||||
import { legacyEncodeURIComponent } from '../../shared/Links/url_helpers';
|
||||
import { TransactionList } from './List';
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
||||
// Initially inspired from react-router's ConnectedRouter
|
||||
// https://github.com/ReactTraining/react-router/blob/e6f9017c947b3ae49affa24cc320d0a86f765b55/packages/react-router-redux/modules/ConnectedRouter.js
|
||||
// Instead of adding a listener to `history` we passively receive props from react-router
|
||||
|
||||
// This ensures that we don't have two history listeners (one here, and one for react-router) which can cause "race-condition" type issues
|
||||
// since history.listen is sync and can result in cascading updates
|
||||
|
||||
import { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class ConnectRouterToRedux extends Component {
|
||||
static propTypes = {
|
||||
location: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.props.updateLocation(this.props.location);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
// this component is wrapped in a react-router Route to get access
|
||||
// to the location prop, so no need to check for prop change here
|
||||
this.props.updateLocation(this.props.location);
|
||||
}
|
||||
|
||||
render() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default ConnectRouterToRedux;
|
|
@ -4,87 +4,60 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { EuiSuperDatePicker, EuiSuperDatePickerProps } from '@elastic/eui';
|
||||
import { EuiSuperDatePicker } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { RouteComponentProps, withRouter } from 'react-router-dom';
|
||||
import { IReduxState } from '../../../store/rootReducer';
|
||||
import {
|
||||
getUrlParams,
|
||||
IUrlParams,
|
||||
refreshTimeRange
|
||||
} from '../../../store/urlParams';
|
||||
import { fromQuery, toQuery } from '../Links/url_helpers';
|
||||
import { history } from '../../../utils/history';
|
||||
import { useLocation } from '../../../hooks/useLocation';
|
||||
import { useUrlParams } from '../../../hooks/useUrlParams';
|
||||
|
||||
interface DatePickerProps extends RouteComponentProps {
|
||||
dispatchRefreshTimeRange: typeof refreshTimeRange;
|
||||
urlParams: IUrlParams;
|
||||
}
|
||||
export function DatePicker() {
|
||||
const location = useLocation();
|
||||
const { urlParams, refreshTimeRange } = useUrlParams();
|
||||
|
||||
export class DatePickerComponent extends React.Component<DatePickerProps> {
|
||||
public updateUrl(nextQuery: {
|
||||
function updateUrl(nextQuery: {
|
||||
rangeFrom?: string;
|
||||
rangeTo?: string;
|
||||
refreshPaused?: boolean;
|
||||
refreshInterval?: number;
|
||||
}) {
|
||||
const { history, location } = this.props;
|
||||
history.push({
|
||||
...location,
|
||||
search: fromQuery({ ...toQuery(location.search), ...nextQuery })
|
||||
search: fromQuery({
|
||||
...toQuery(location.search),
|
||||
...nextQuery
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
public onRefreshChange: EuiSuperDatePickerProps['onRefreshChange'] = ({
|
||||
function onRefreshChange({
|
||||
isPaused,
|
||||
refreshInterval
|
||||
}) => {
|
||||
this.updateUrl({ refreshPaused: isPaused, refreshInterval });
|
||||
};
|
||||
|
||||
public onTimeChange: EuiSuperDatePickerProps['onTimeChange'] = ({
|
||||
start,
|
||||
end
|
||||
}) => {
|
||||
this.updateUrl({ rangeFrom: start, rangeTo: end });
|
||||
};
|
||||
|
||||
public onRefresh: EuiSuperDatePickerProps['onRefresh'] = ({ start, end }) => {
|
||||
this.props.dispatchRefreshTimeRange({ rangeFrom: start, rangeTo: end });
|
||||
};
|
||||
|
||||
public render() {
|
||||
const {
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
refreshPaused,
|
||||
refreshInterval
|
||||
} = this.props.urlParams;
|
||||
return (
|
||||
<EuiSuperDatePicker
|
||||
start={rangeFrom}
|
||||
end={rangeTo}
|
||||
isPaused={refreshPaused}
|
||||
refreshInterval={refreshInterval}
|
||||
onTimeChange={this.onTimeChange}
|
||||
onRefresh={this.onRefresh}
|
||||
onRefreshChange={this.onRefreshChange}
|
||||
showUpdateButton={true}
|
||||
/>
|
||||
);
|
||||
}: {
|
||||
isPaused: boolean;
|
||||
refreshInterval: number;
|
||||
}) {
|
||||
updateUrl({ refreshPaused: isPaused, refreshInterval });
|
||||
}
|
||||
|
||||
function onTimeChange({ start, end }: { start: string; end: string }) {
|
||||
updateUrl({ rangeFrom: start, rangeTo: end });
|
||||
}
|
||||
|
||||
const { rangeFrom, rangeTo, refreshPaused, refreshInterval } = urlParams;
|
||||
|
||||
return (
|
||||
<EuiSuperDatePicker
|
||||
start={rangeFrom}
|
||||
end={rangeTo}
|
||||
isPaused={refreshPaused}
|
||||
refreshInterval={refreshInterval}
|
||||
onTimeChange={onTimeChange}
|
||||
onRefresh={({ start, end }) => {
|
||||
refreshTimeRange({ rangeFrom: start, rangeTo: end });
|
||||
}}
|
||||
onRefreshChange={onRefreshChange}
|
||||
showUpdateButton={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: IReduxState) => ({
|
||||
urlParams: getUrlParams(state)
|
||||
});
|
||||
const mapDispatchToProps = { dispatchRefreshTimeRange: refreshTimeRange };
|
||||
|
||||
const DatePicker = withRouter(
|
||||
connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(DatePickerComponent)
|
||||
);
|
||||
|
||||
export { DatePicker };
|
||||
|
|
|
@ -4,158 +4,87 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { mount, shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { Store } from 'redux';
|
||||
// @ts-ignore
|
||||
import configureStore from '../../../../store/config/configureStore';
|
||||
import { mockNow, tick } from '../../../../utils/testHelpers';
|
||||
import { DatePicker, DatePickerComponent } from '../DatePicker';
|
||||
import { LocationProvider } from '../../../../context/LocationContext';
|
||||
import { UrlParamsContext } from '../../../../context/UrlParamsContext';
|
||||
import { tick } from '../../../../utils/testHelpers';
|
||||
import { DatePicker } from '../DatePicker';
|
||||
import { IUrlParams } from '../../../../context/UrlParamsContext/types';
|
||||
import { history } from '../../../../utils/history';
|
||||
import { mount } from 'enzyme';
|
||||
import { EuiSuperDatePicker } from '@elastic/eui';
|
||||
|
||||
function mountPicker(initialState = {}) {
|
||||
const store = configureStore(initialState);
|
||||
const wrapper = mount(
|
||||
<Provider store={store}>
|
||||
<MemoryRouter>
|
||||
const mockHistoryPush = jest.spyOn(history, 'push');
|
||||
const mockRefreshTimeRange = jest.fn();
|
||||
const MockUrlParamsProvider: React.FC<{
|
||||
params?: IUrlParams;
|
||||
}> = ({ params = {}, children }) => (
|
||||
<UrlParamsContext.Provider
|
||||
value={{ urlParams: params, refreshTimeRange: mockRefreshTimeRange }}
|
||||
children={children}
|
||||
/>
|
||||
);
|
||||
|
||||
function mountDatePicker(params?: IUrlParams) {
|
||||
return mount(
|
||||
<LocationProvider history={history}>
|
||||
<MockUrlParamsProvider params={params}>
|
||||
<DatePicker />
|
||||
</MemoryRouter>
|
||||
</Provider>
|
||||
</MockUrlParamsProvider>
|
||||
</LocationProvider>
|
||||
);
|
||||
return { wrapper, store };
|
||||
}
|
||||
|
||||
describe('DatePicker', () => {
|
||||
describe('url updates', () => {
|
||||
function setupTest() {
|
||||
const routerProps = {
|
||||
location: { search: '' },
|
||||
history: { push: jest.fn() }
|
||||
} as any;
|
||||
|
||||
const wrapper = shallow<DatePickerComponent>(
|
||||
<DatePickerComponent
|
||||
{...routerProps}
|
||||
dispatchUpdateTimePicker={jest.fn()}
|
||||
urlParams={{}}
|
||||
/>
|
||||
);
|
||||
|
||||
return { history: routerProps.history, wrapper };
|
||||
}
|
||||
|
||||
it('should push an entry to the stack for each change', () => {
|
||||
const { history, wrapper } = setupTest();
|
||||
wrapper.instance().updateUrl({ rangeFrom: 'now-20m', rangeTo: 'now' });
|
||||
expect(history.push).toHaveBeenCalledWith({
|
||||
search: 'rangeFrom=now-20m&rangeTo=now'
|
||||
});
|
||||
});
|
||||
beforeAll(() => {
|
||||
jest.spyOn(console, 'error').mockImplementation(() => null);
|
||||
});
|
||||
|
||||
describe('refresh cycle', () => {
|
||||
let nowSpy: jest.Mock;
|
||||
beforeEach(() => {
|
||||
nowSpy = mockNow('2010');
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nowSpy.mockRestore();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('when refresh is not paused', () => {
|
||||
let listener: jest.Mock;
|
||||
let store: Store;
|
||||
beforeEach(async () => {
|
||||
const obj = mountPicker({
|
||||
urlParams: {
|
||||
rangeFrom: 'now-15m',
|
||||
rangeTo: 'now',
|
||||
refreshPaused: false,
|
||||
refreshInterval: 200
|
||||
}
|
||||
});
|
||||
store = obj.store;
|
||||
|
||||
listener = jest.fn();
|
||||
store.subscribe(listener);
|
||||
|
||||
jest.advanceTimersByTime(200);
|
||||
await tick();
|
||||
jest.advanceTimersByTime(200);
|
||||
await tick();
|
||||
jest.advanceTimersByTime(200);
|
||||
await tick();
|
||||
it('should update the URL when the date range changes', () => {
|
||||
const datePicker = mountDatePicker();
|
||||
datePicker
|
||||
.find(EuiSuperDatePicker)
|
||||
.props()
|
||||
.onTimeChange({
|
||||
start: 'updated-start',
|
||||
end: 'updated-end',
|
||||
isInvalid: false,
|
||||
isQuickSelection: true
|
||||
});
|
||||
expect(mockHistoryPush).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
search: 'rangeFrom=updated-start&rangeTo=updated-end'
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should dispatch every refresh interval', async () => {
|
||||
expect(listener).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('should update the store with the new date range', () => {
|
||||
expect(store.getState().urlParams).toEqual({
|
||||
end: '2010-01-01T00:00:00.000Z',
|
||||
rangeFrom: 'now-15m',
|
||||
rangeTo: 'now',
|
||||
refreshInterval: 200,
|
||||
refreshPaused: false,
|
||||
start: '2009-12-31T23:45:00.000Z'
|
||||
});
|
||||
});
|
||||
it('should auto-refresh when refreshPaused is false', async () => {
|
||||
jest.useFakeTimers();
|
||||
const wrapper = mountDatePicker({
|
||||
refreshPaused: false,
|
||||
refreshInterval: 1000
|
||||
});
|
||||
expect(mockRefreshTimeRange).not.toHaveBeenCalled();
|
||||
jest.advanceTimersByTime(1000);
|
||||
await tick();
|
||||
expect(mockRefreshTimeRange).toHaveBeenCalled();
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it('should not refresh when paused', () => {
|
||||
const { store } = mountPicker({
|
||||
urlParams: {
|
||||
rangeFrom: 'now-15m',
|
||||
rangeTo: 'now',
|
||||
refreshPaused: true,
|
||||
refreshInterval: 200
|
||||
}
|
||||
});
|
||||
|
||||
const listener = jest.fn();
|
||||
store.subscribe(listener);
|
||||
jest.advanceTimersByTime(1100);
|
||||
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should be paused by default', () => {
|
||||
const { store } = mountPicker({
|
||||
urlParams: {
|
||||
rangeFrom: 'now-15m',
|
||||
rangeTo: 'now',
|
||||
refreshInterval: 200
|
||||
}
|
||||
});
|
||||
|
||||
const listener = jest.fn();
|
||||
store.subscribe(listener);
|
||||
jest.advanceTimersByTime(1100);
|
||||
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not attempt refreshes after unmounting', () => {
|
||||
const { store, wrapper } = mountPicker({
|
||||
urlParams: {
|
||||
rangeFrom: 'now-15m',
|
||||
rangeTo: 'now',
|
||||
refreshPaused: false,
|
||||
refreshInterval: 200
|
||||
}
|
||||
});
|
||||
|
||||
const listener = jest.fn();
|
||||
store.subscribe(listener);
|
||||
wrapper.unmount();
|
||||
jest.advanceTimersByTime(1100);
|
||||
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
});
|
||||
it('should NOT auto-refresh when refreshPaused is true', async () => {
|
||||
jest.useFakeTimers();
|
||||
mountDatePicker({ refreshPaused: true, refreshInterval: 1000 });
|
||||
expect(mockRefreshTimeRange).not.toHaveBeenCalled();
|
||||
jest.advanceTimersByTime(1000);
|
||||
await tick();
|
||||
expect(mockRefreshTimeRange).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,15 +5,11 @@
|
|||
*/
|
||||
|
||||
import { EuiTab } from '@elastic/eui';
|
||||
import { mount, ReactWrapper, shallow, ShallowWrapper } from 'enzyme';
|
||||
import { shallow, ShallowWrapper } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import {
|
||||
HistoryTabs,
|
||||
HistoryTabsProps,
|
||||
HistoryTabsWithoutRouter,
|
||||
IHistoryTab
|
||||
} from '..';
|
||||
import { HistoryTabs, HistoryTabsProps, IHistoryTab } from '..';
|
||||
import * as hooks from '../../../../hooks/useLocation';
|
||||
import { history } from '../../../../utils/history';
|
||||
|
||||
type PropsOf<Component> = Component extends React.SFC<infer Props>
|
||||
? Props
|
||||
|
@ -22,7 +18,6 @@ type EuiTabProps = PropsOf<typeof EuiTab>;
|
|||
|
||||
describe('HistoryTabs', () => {
|
||||
let mockLocation: any;
|
||||
let mockHistory: any;
|
||||
let testTabs: IHistoryTab[];
|
||||
let testProps: HistoryTabsProps;
|
||||
|
||||
|
@ -30,9 +25,8 @@ describe('HistoryTabs', () => {
|
|||
mockLocation = {
|
||||
pathname: ''
|
||||
};
|
||||
mockHistory = {
|
||||
push: jest.fn()
|
||||
};
|
||||
|
||||
jest.spyOn(hooks, 'useLocation').mockImplementation(() => mockLocation);
|
||||
|
||||
const Content = (props: { name: string }) => <div>{props.name}</div>;
|
||||
|
||||
|
@ -55,15 +49,17 @@ describe('HistoryTabs', () => {
|
|||
];
|
||||
|
||||
testProps = {
|
||||
location: mockLocation,
|
||||
history: mockHistory,
|
||||
tabs: testTabs
|
||||
} as HistoryTabsProps;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should render correctly', () => {
|
||||
mockLocation.pathname = '/two';
|
||||
const wrapper = shallow(<HistoryTabsWithoutRouter {...testProps} />);
|
||||
const wrapper = shallow(<HistoryTabs {...testProps} />);
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
|
||||
const tabs: ShallowWrapper<EuiTabProps> = wrapper.find(EuiTab);
|
||||
|
@ -72,25 +68,15 @@ describe('HistoryTabs', () => {
|
|||
expect(tabs.at(2).props().isSelected).toEqual(false);
|
||||
});
|
||||
|
||||
it('should change the selected item on tab click', () => {
|
||||
const wrapper = mount(
|
||||
<MemoryRouter initialEntries={['/two']}>
|
||||
<HistoryTabs tabs={testTabs} />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
expect(wrapper.find('Content')).toMatchSnapshot();
|
||||
it('should push a new state onto history on tab click', () => {
|
||||
const pushSpy = jest.spyOn(history, 'push');
|
||||
const wrapper = shallow(<HistoryTabs tabs={testTabs} />);
|
||||
|
||||
wrapper
|
||||
.find(EuiTab)
|
||||
.at(2)
|
||||
.simulate('click');
|
||||
|
||||
const tabs: ReactWrapper<EuiTabProps> = wrapper.find(EuiTab);
|
||||
expect(tabs.at(0).props().isSelected).toEqual(false);
|
||||
expect(tabs.at(1).props().isSelected).toEqual(false);
|
||||
expect(tabs.at(2).props().isSelected).toEqual(true);
|
||||
|
||||
expect(wrapper.find('Content')).toMatchSnapshot();
|
||||
expect(pushSpy).toHaveBeenCalledWith({ pathname: '/three' });
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,25 +1,5 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`HistoryTabs should change the selected item on tab click 1`] = `
|
||||
<Content
|
||||
name="two"
|
||||
>
|
||||
<div>
|
||||
two
|
||||
</div>
|
||||
</Content>
|
||||
`;
|
||||
|
||||
exports[`HistoryTabs should change the selected item on tab click 2`] = `
|
||||
<Content
|
||||
name="three"
|
||||
>
|
||||
<div>
|
||||
three
|
||||
</div>
|
||||
</Content>
|
||||
`;
|
||||
|
||||
exports[`HistoryTabs should render correctly 1`] = `
|
||||
<Fragment>
|
||||
<EuiTabs
|
||||
|
|
|
@ -6,12 +6,9 @@
|
|||
|
||||
import { EuiSpacer, EuiTab, EuiTabs } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import {
|
||||
matchPath,
|
||||
Route,
|
||||
RouteComponentProps,
|
||||
withRouter
|
||||
} from 'react-router-dom';
|
||||
import { matchPath, Route, RouteComponentProps } from 'react-router-dom';
|
||||
import { useLocation } from '../../../hooks/useLocation';
|
||||
import { history } from '../../../utils/history';
|
||||
|
||||
export interface IHistoryTab {
|
||||
path: string;
|
||||
|
@ -20,7 +17,7 @@ export interface IHistoryTab {
|
|||
render?: (props: RouteComponentProps) => React.ReactNode;
|
||||
}
|
||||
|
||||
export interface HistoryTabsProps extends RouteComponentProps {
|
||||
export interface HistoryTabsProps {
|
||||
tabs: IHistoryTab[];
|
||||
}
|
||||
|
||||
|
@ -31,11 +28,8 @@ function isTabSelected(tab: IHistoryTab, currentPath: string) {
|
|||
return currentPath === tab.path;
|
||||
}
|
||||
|
||||
const HistoryTabsWithoutRouter = ({
|
||||
tabs,
|
||||
history,
|
||||
location
|
||||
}: HistoryTabsProps) => {
|
||||
export function HistoryTabs({ tabs }: HistoryTabsProps) {
|
||||
const location = useLocation();
|
||||
return (
|
||||
<React.Fragment>
|
||||
<EuiTabs>
|
||||
|
@ -61,8 +55,4 @@ const HistoryTabsWithoutRouter = ({
|
|||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const HistoryTabs = withRouter(HistoryTabsWithoutRouter);
|
||||
|
||||
export { HistoryTabsWithoutRouter, HistoryTabs };
|
||||
}
|
||||
|
|
|
@ -1,18 +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 view from './view';
|
||||
|
||||
function mapStateToProps(state = {}) {
|
||||
return {
|
||||
location: state.location,
|
||||
urlParams: getUrlParams(state)
|
||||
};
|
||||
}
|
||||
|
||||
export const KueryBar = connect(mapStateToProps)(view);
|
181
x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx
Normal file
181
x-pack/plugins/apm/public/components/shared/KueryBar/index.tsx
Normal file
|
@ -0,0 +1,181 @@
|
|||
/*
|
||||
* 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, { useState, useEffect } from 'react';
|
||||
import { uniqueId, startsWith } from 'lodash';
|
||||
import { EuiCallOut } from '@elastic/eui';
|
||||
import chrome from 'ui/chrome';
|
||||
import styled from 'styled-components';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { AutocompleteSuggestion } from 'ui/autocomplete_providers';
|
||||
import { StaticIndexPattern } from 'ui/index_patterns';
|
||||
import {
|
||||
fromQuery,
|
||||
toQuery,
|
||||
legacyEncodeURIComponent
|
||||
} from '../Links/url_helpers';
|
||||
import { KibanaLink } from '../Links/KibanaLink';
|
||||
// @ts-ignore
|
||||
import { Typeahead } from './Typeahead';
|
||||
import {
|
||||
convertKueryToEsQuery,
|
||||
getSuggestions,
|
||||
getAPMIndexPatternForKuery
|
||||
} from '../../../services/kuery';
|
||||
// @ts-ignore
|
||||
import { getBoolFilter } from './get_bool_filter';
|
||||
import { useLocation } from '../../../hooks/useLocation';
|
||||
import { useUrlParams } from '../../../hooks/useUrlParams';
|
||||
import { history } from '../../../utils/history';
|
||||
|
||||
const Container = styled.div`
|
||||
margin-bottom: 10px;
|
||||
`;
|
||||
|
||||
interface State {
|
||||
indexPattern: StaticIndexPattern | null;
|
||||
suggestions: AutocompleteSuggestion[];
|
||||
isLoadingIndexPattern: boolean;
|
||||
isLoadingSuggestions: boolean;
|
||||
}
|
||||
|
||||
export function KueryBar() {
|
||||
const [state, setState] = useState<State>({
|
||||
indexPattern: null,
|
||||
suggestions: [],
|
||||
isLoadingIndexPattern: true,
|
||||
isLoadingSuggestions: false
|
||||
});
|
||||
const { urlParams } = useUrlParams();
|
||||
const location = useLocation();
|
||||
const apmIndexPatternTitle = chrome.getInjected('apmIndexPatternTitle');
|
||||
const indexPatternMissing =
|
||||
!state.isLoadingIndexPattern && !state.indexPattern;
|
||||
let currentRequestCheck;
|
||||
|
||||
useEffect(() => {
|
||||
let didCancel = false;
|
||||
|
||||
async function loadIndexPattern() {
|
||||
setState({ ...state, isLoadingIndexPattern: true });
|
||||
const indexPattern = await getAPMIndexPatternForKuery();
|
||||
if (didCancel) {
|
||||
return;
|
||||
}
|
||||
if (!indexPattern) {
|
||||
setState({ ...state, isLoadingIndexPattern: false });
|
||||
} else {
|
||||
setState({ ...state, indexPattern, isLoadingIndexPattern: false });
|
||||
}
|
||||
}
|
||||
loadIndexPattern();
|
||||
|
||||
return () => {
|
||||
didCancel = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
async function onChange(inputValue: string, selectionStart: number) {
|
||||
const { indexPattern } = state;
|
||||
|
||||
if (indexPattern === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState({ ...state, suggestions: [], isLoadingSuggestions: true });
|
||||
|
||||
const currentRequest = uniqueId();
|
||||
currentRequestCheck = currentRequest;
|
||||
|
||||
const boolFilter = getBoolFilter(urlParams);
|
||||
try {
|
||||
const suggestions = (await getSuggestions(
|
||||
inputValue,
|
||||
selectionStart,
|
||||
indexPattern,
|
||||
boolFilter
|
||||
))
|
||||
.filter(suggestion => !startsWith(suggestion.text, 'span.'))
|
||||
.slice(0, 15);
|
||||
|
||||
if (currentRequest !== currentRequestCheck) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState({ ...state, suggestions, isLoadingSuggestions: false });
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Error while fetching suggestions', e);
|
||||
}
|
||||
}
|
||||
|
||||
function onSubmit(inputValue: string) {
|
||||
const { indexPattern } = state;
|
||||
|
||||
if (indexPattern === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = convertKueryToEsQuery(inputValue, indexPattern);
|
||||
if (!res) {
|
||||
return;
|
||||
}
|
||||
|
||||
history.replace({
|
||||
...location,
|
||||
search: fromQuery({
|
||||
...toQuery(location.search),
|
||||
kuery: legacyEncodeURIComponent(inputValue.trim())
|
||||
})
|
||||
});
|
||||
} catch (e) {
|
||||
console.log('Invalid kuery syntax'); // eslint-disable-line no-console
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Typeahead
|
||||
disabled={indexPatternMissing}
|
||||
isLoading={state.isLoadingSuggestions}
|
||||
initialValue={urlParams.kuery}
|
||||
onChange={onChange}
|
||||
onSubmit={onSubmit}
|
||||
suggestions={state.suggestions}
|
||||
/>
|
||||
|
||||
{indexPatternMissing && (
|
||||
<EuiCallOut
|
||||
style={{ display: 'inline-block', marginTop: '10px' }}
|
||||
title={
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id="xpack.apm.kueryBar.indexPatternMissingWarningMessage"
|
||||
defaultMessage="There's no APM index pattern with the title {apmIndexPatternTitle} available. To use the Query bar, please choose to import the APM index pattern via the {setupInstructionsLink}."
|
||||
values={{
|
||||
apmIndexPatternTitle: `"${apmIndexPatternTitle}"`,
|
||||
setupInstructionsLink: (
|
||||
<KibanaLink path={`/home/tutorial/apm`}>
|
||||
{i18n.translate(
|
||||
'xpack.apm.kueryBar.setupInstructionsLinkLabel',
|
||||
{ defaultMessage: 'Setup Instructions' }
|
||||
)}
|
||||
</KibanaLink>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
color="warning"
|
||||
iconType="alert"
|
||||
size="s"
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
|
@ -1,158 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { uniqueId, startsWith } from 'lodash';
|
||||
import { EuiCallOut } from '@elastic/eui';
|
||||
import {
|
||||
history,
|
||||
fromQuery,
|
||||
toQuery,
|
||||
legacyEncodeURIComponent
|
||||
} from '../Links/url_helpers';
|
||||
import { KibanaLink } from '../Links/KibanaLink';
|
||||
import { Typeahead } from './Typeahead';
|
||||
import chrome from 'ui/chrome';
|
||||
import {
|
||||
convertKueryToEsQuery,
|
||||
getSuggestions,
|
||||
getAPMIndexPatternForKuery
|
||||
} from '../../../services/kuery';
|
||||
import styled from 'styled-components';
|
||||
import { getBoolFilter } from './get_bool_filter';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
const Container = styled.div`
|
||||
margin-bottom: 10px;
|
||||
`;
|
||||
|
||||
class KueryBarView extends Component {
|
||||
state = {
|
||||
indexPattern: null,
|
||||
suggestions: [],
|
||||
isLoadingIndexPattern: true,
|
||||
isLoadingSuggestions: false
|
||||
};
|
||||
|
||||
willUnmount = false;
|
||||
|
||||
componentWillUnmount() {
|
||||
this.willUnmount = true;
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
const indexPattern = await getAPMIndexPatternForKuery();
|
||||
if (!this.willUnmount) {
|
||||
this.setState({ indexPattern, isLoadingIndexPattern: false });
|
||||
}
|
||||
}
|
||||
|
||||
onChange = async (inputValue, selectionStart) => {
|
||||
const { indexPattern } = this.state;
|
||||
const { urlParams } = this.props;
|
||||
this.setState({ suggestions: [], isLoadingSuggestions: true });
|
||||
|
||||
const currentRequest = uniqueId();
|
||||
this.currentRequest = currentRequest;
|
||||
|
||||
const boolFilter = getBoolFilter(urlParams);
|
||||
try {
|
||||
const suggestions = (await getSuggestions(
|
||||
inputValue,
|
||||
selectionStart,
|
||||
indexPattern,
|
||||
boolFilter
|
||||
))
|
||||
.filter(suggestion => !startsWith(suggestion.text, 'span.'))
|
||||
.slice(0, 15);
|
||||
|
||||
if (currentRequest !== this.currentRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ suggestions, isLoadingSuggestions: false });
|
||||
} catch (e) {
|
||||
console.error('Error while fetching suggestions', e);
|
||||
}
|
||||
};
|
||||
|
||||
onSubmit = inputValue => {
|
||||
const { indexPattern } = this.state;
|
||||
const { location } = this.props;
|
||||
try {
|
||||
const res = convertKueryToEsQuery(inputValue, indexPattern);
|
||||
if (!res) {
|
||||
return;
|
||||
}
|
||||
|
||||
history.replace({
|
||||
...location,
|
||||
search: fromQuery({
|
||||
...toQuery(this.props.location.search),
|
||||
kuery: legacyEncodeURIComponent(inputValue.trim())
|
||||
})
|
||||
});
|
||||
} catch (e) {
|
||||
console.log('Invalid kuery syntax'); // eslint-disable-line no-console
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const apmIndexPatternTitle = chrome.getInjected('apmIndexPatternTitle');
|
||||
const indexPatternMissing =
|
||||
!this.state.isLoadingIndexPattern && !this.state.indexPattern;
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Typeahead
|
||||
disabled={indexPatternMissing}
|
||||
isLoading={this.state.isLoadingSuggestions}
|
||||
initialValue={this.props.urlParams.kuery}
|
||||
onChange={this.onChange}
|
||||
onSubmit={this.onSubmit}
|
||||
suggestions={this.state.suggestions}
|
||||
/>
|
||||
|
||||
{indexPatternMissing && (
|
||||
<EuiCallOut
|
||||
style={{ display: 'inline-block', marginTop: '10px' }}
|
||||
title={
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id="xpack.apm.kueryBar.indexPatternMissingWarningMessage"
|
||||
defaultMessage="There's no APM index pattern with the title {apmIndexPatternTitle} available. To use the Query bar, please choose to import the APM index pattern via the {setupInstructionsLink}."
|
||||
values={{
|
||||
apmIndexPatternTitle: `"${apmIndexPatternTitle}"`,
|
||||
setupInstructionsLink: (
|
||||
<KibanaLink path={`/home/tutorial/apm`}>
|
||||
{i18n.translate(
|
||||
'xpack.apm.kueryBar.setupInstructionsLinkLabel',
|
||||
{ defaultMessage: 'Setup Instructions' }
|
||||
)}
|
||||
</KibanaLink>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
color="warning"
|
||||
iconType="alert"
|
||||
size="s"
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
KueryBarView.propTypes = {
|
||||
location: PropTypes.object.isRequired,
|
||||
urlParams: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default KueryBarView;
|
|
@ -10,7 +10,7 @@ import url from 'url';
|
|||
import { pick } from 'lodash';
|
||||
import { useLocation } from '../../../hooks/useLocation';
|
||||
import { APMQueryParams, toQuery, fromQuery } from './url_helpers';
|
||||
import { TIMEPICKER_DEFAULTS } from '../../../store/urlParams';
|
||||
import { TIMEPICKER_DEFAULTS } from '../../../context/UrlParamsContext/constants';
|
||||
|
||||
interface Props extends EuiLinkAnchorProps {
|
||||
path?: string;
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { Location } from 'history';
|
||||
import { TIMEPICKER_DEFAULTS } from '../../../store/urlParams';
|
||||
import { TIMEPICKER_DEFAULTS } from '../../../context/UrlParamsContext/constants';
|
||||
import { toQuery } from './url_helpers';
|
||||
|
||||
export interface TimepickerRisonData {
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import createHistory from 'history/createHashHistory';
|
||||
import qs from 'querystring';
|
||||
import { StringMap } from '../../../../typings/common';
|
||||
|
||||
|
@ -52,9 +51,3 @@ export function legacyEncodeURIComponent(rawUrl?: string) {
|
|||
export function legacyDecodeURIComponent(encodedUrl?: string) {
|
||||
return encodedUrl && decodeURIComponent(encodedUrl.replace(/~/g, '%'));
|
||||
}
|
||||
|
||||
// Make history singleton available across APM project.
|
||||
// This is not great. Other options are to use context or withRouter helper
|
||||
// React Context API is unstable and will change soon-ish (probably 16.3)
|
||||
// withRouter helper from react-router overrides several props (eg. `location`) which makes it less desireable
|
||||
export const history = createHistory();
|
||||
|
|
|
@ -16,7 +16,7 @@ import InteractivePlot from '../InteractivePlot';
|
|||
import {
|
||||
getResponseTimeSeries,
|
||||
getEmptySerie
|
||||
} from '../../../../../store/selectors/chartSelectors';
|
||||
} from '../../../../../selectors/chartSelectors';
|
||||
|
||||
function getXValueByIndex(index) {
|
||||
return responseWithData.responseTimes.avg[index].x;
|
||||
|
|
|
@ -19,8 +19,8 @@ import React, { Component } from 'react';
|
|||
import { isEmpty } from 'lodash';
|
||||
import styled from 'styled-components';
|
||||
import { Coordinate } from '../../../../../typings/timeseries';
|
||||
import { ITransactionChartData } from '../../../../store/selectors/chartSelectors';
|
||||
import { IUrlParams } from '../../../../store/urlParams';
|
||||
import { ITransactionChartData } from '../../../../selectors/chartSelectors';
|
||||
import { IUrlParams } from '../../../../context/UrlParamsContext/types';
|
||||
import { asInteger, asMillis, tpmUnit } from '../../../../utils/formatters';
|
||||
import { LicenseContext } from '../../../app/Main/LicenseCheck';
|
||||
import { MLJobLink } from '../../Links/MachineLearningLinks/MLJobLink';
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { UrlParamsContext, UrlParamsProvider } from '..';
|
||||
import { mount } from 'enzyme';
|
||||
import * as hooks from '../../../hooks/useLocation';
|
||||
import { Location } from 'history';
|
||||
import { IUrlParams } from '../types';
|
||||
|
||||
function mountParams() {
|
||||
return mount(
|
||||
<UrlParamsProvider>
|
||||
<UrlParamsContext.Consumer>
|
||||
{({ urlParams }: { urlParams: IUrlParams }) => (
|
||||
<span id="data">{JSON.stringify(urlParams, null, 2)}</span>
|
||||
)}
|
||||
</UrlParamsContext.Consumer>
|
||||
</UrlParamsProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function getDataFromOutput(wrapper: ReturnType<typeof mount>) {
|
||||
return JSON.parse(wrapper.find('#data').text());
|
||||
}
|
||||
|
||||
describe('UrlParamsContext', () => {
|
||||
let mockLocation: Location;
|
||||
|
||||
beforeEach(() => {
|
||||
mockLocation = { pathname: '/test/pathname' } as Location;
|
||||
jest.spyOn(hooks, 'useLocation').mockImplementation(() => mockLocation);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should have default params', () => {
|
||||
jest
|
||||
.spyOn(Date, 'now')
|
||||
.mockImplementation(() => new Date('2000-06-15T12:00:00Z').getTime());
|
||||
const wrapper = mountParams();
|
||||
const params = getDataFromOutput(wrapper);
|
||||
|
||||
expect(params).toEqual({
|
||||
start: '2000-06-14T12:00:00.000Z',
|
||||
end: '2000-06-15T12:00:00.000Z',
|
||||
page: 0,
|
||||
rangeFrom: 'now-24h',
|
||||
rangeTo: 'now',
|
||||
refreshInterval: 0,
|
||||
refreshPaused: true
|
||||
});
|
||||
});
|
||||
|
||||
it('should read values in from location', () => {
|
||||
mockLocation.search =
|
||||
'?rangeFrom=2010-03-15T12:00:00Z&rangeTo=2010-04-10T12:00:00Z&transactionId=123abc';
|
||||
const wrapper = mountParams();
|
||||
const params = getDataFromOutput(wrapper);
|
||||
expect(params.start).toEqual('2010-03-15T12:00:00.000Z');
|
||||
expect(params.end).toEqual('2010-04-10T12:00:00.000Z');
|
||||
});
|
||||
|
||||
it('should update param values if location has changed', () => {
|
||||
const wrapper = mountParams();
|
||||
mockLocation = {
|
||||
pathname: '/test/updated',
|
||||
search:
|
||||
'?rangeFrom=2009-03-15T12:00:00Z&rangeTo=2009-04-10T12:00:00Z&transactionId=UPDATED'
|
||||
} as Location;
|
||||
// force an update
|
||||
wrapper.setProps({ abc: 123 });
|
||||
const params = getDataFromOutput(wrapper);
|
||||
expect(params.start).toEqual('2009-03-15T12:00:00.000Z');
|
||||
expect(params.end).toEqual('2009-04-10T12:00:00.000Z');
|
||||
});
|
||||
|
||||
it('should refresh the time range with new values', () => {
|
||||
const wrapper = mount(
|
||||
<UrlParamsProvider>
|
||||
<UrlParamsContext.Consumer>
|
||||
{({ urlParams, refreshTimeRange }) => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<span id="data">{JSON.stringify(urlParams, null, 2)}</span>
|
||||
<button
|
||||
onClick={() =>
|
||||
refreshTimeRange({
|
||||
rangeFrom: '2005-09-20T12:00:00Z',
|
||||
rangeTo: '2005-10-21T12:00:00Z'
|
||||
})
|
||||
}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}}
|
||||
</UrlParamsContext.Consumer>
|
||||
</UrlParamsProvider>
|
||||
);
|
||||
wrapper.find('button').simulate('click');
|
||||
const data = getDataFromOutput(wrapper);
|
||||
expect(data.start).toEqual('2005-09-20T12:00:00.000Z');
|
||||
expect(data.end).toEqual('2005-10-21T12:00:00.000Z');
|
||||
});
|
||||
});
|
|
@ -4,14 +4,11 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import view from './view';
|
||||
import { updateLocation } from '../../../store/location';
|
||||
|
||||
const mapDispatchToProps = {
|
||||
updateLocation
|
||||
export const TIME_RANGE_REFRESH = 'TIME_RANGE_REFRESH';
|
||||
export const LOCATION_UPDATE = 'LOCATION_UPDATE';
|
||||
export const TIMEPICKER_DEFAULTS = {
|
||||
rangeFrom: 'now-24h',
|
||||
rangeTo: 'now',
|
||||
refreshPaused: 'true',
|
||||
refreshInterval: '0'
|
||||
};
|
||||
export default connect(
|
||||
null,
|
||||
mapDispatchToProps
|
||||
)(view);
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { compact, pick } from 'lodash';
|
||||
import datemath from '@elastic/datemath';
|
||||
import { IUrlParams } from './types';
|
||||
|
||||
export function getParsedDate(rawDate?: string, opts = {}) {
|
||||
if (rawDate) {
|
||||
const parsed = datemath.parse(rawDate, opts);
|
||||
if (parsed) {
|
||||
return parsed.toISOString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getStart(prevState: IUrlParams, rangeFrom?: string) {
|
||||
if (prevState.rangeFrom !== rangeFrom) {
|
||||
return getParsedDate(rangeFrom);
|
||||
}
|
||||
return prevState.start;
|
||||
}
|
||||
|
||||
export function getEnd(prevState: IUrlParams, rangeTo?: string) {
|
||||
if (prevState.rangeTo !== rangeTo) {
|
||||
return getParsedDate(rangeTo, { roundUp: true });
|
||||
}
|
||||
return prevState.end;
|
||||
}
|
||||
|
||||
export function toNumber(value?: string) {
|
||||
if (value !== undefined) {
|
||||
return parseInt(value, 10);
|
||||
}
|
||||
}
|
||||
|
||||
export function toString(value?: string) {
|
||||
if (value === '' || value === 'null' || value === 'undefined') {
|
||||
return;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function toBoolean(value?: string) {
|
||||
return value === 'true';
|
||||
}
|
||||
|
||||
export function getPathAsArray(pathname: string = '') {
|
||||
return compact(pathname.split('/'));
|
||||
}
|
||||
|
||||
export function removeUndefinedProps<T>(obj: T): Partial<T> {
|
||||
return pick(obj, value => value !== undefined);
|
||||
}
|
||||
|
||||
export function getPathParams(pathname: string = '') {
|
||||
const paths = getPathAsArray(pathname);
|
||||
const pageName = paths[1];
|
||||
|
||||
switch (pageName) {
|
||||
case 'transactions':
|
||||
return {
|
||||
processorEvent: 'transaction',
|
||||
serviceName: paths[0],
|
||||
transactionType: paths[2],
|
||||
transactionName: paths[3]
|
||||
};
|
||||
case 'errors':
|
||||
return {
|
||||
processorEvent: 'error',
|
||||
serviceName: paths[0],
|
||||
errorGroupId: paths[2]
|
||||
};
|
||||
case 'metrics':
|
||||
return {
|
||||
processorEvent: 'metric',
|
||||
serviceName: paths[0]
|
||||
};
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
84
x-pack/plugins/apm/public/context/UrlParamsContext/index.tsx
Normal file
84
x-pack/plugins/apm/public/context/UrlParamsContext/index.tsx
Normal file
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* 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, { createContext, useReducer, useEffect } from 'react';
|
||||
import { Location } from 'history';
|
||||
import { useLocation } from '../../hooks/useLocation';
|
||||
import { IUrlParams } from './types';
|
||||
import { LOCATION_UPDATE, TIME_RANGE_REFRESH } from './constants';
|
||||
import { getParsedDate } from './helpers';
|
||||
import { resolveUrlParams } from './resolveUrlParams';
|
||||
|
||||
interface TimeRange {
|
||||
rangeFrom: string;
|
||||
rangeTo: string;
|
||||
}
|
||||
|
||||
interface LocationAction {
|
||||
type: typeof LOCATION_UPDATE;
|
||||
location: Location;
|
||||
}
|
||||
|
||||
interface TimeRangeRefreshAction {
|
||||
type: typeof TIME_RANGE_REFRESH;
|
||||
time: TimeRange;
|
||||
}
|
||||
|
||||
const defaultRefresh = (time: TimeRange) => {};
|
||||
|
||||
export function urlParamsReducer(
|
||||
state: IUrlParams = {},
|
||||
action: LocationAction | TimeRangeRefreshAction
|
||||
): IUrlParams {
|
||||
switch (action.type) {
|
||||
case LOCATION_UPDATE: {
|
||||
return resolveUrlParams(action.location);
|
||||
}
|
||||
|
||||
case TIME_RANGE_REFRESH:
|
||||
return {
|
||||
...state,
|
||||
start: getParsedDate(action.time.rangeFrom),
|
||||
end: getParsedDate(action.time.rangeTo)
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
const UrlParamsContext = createContext({
|
||||
urlParams: {} as IUrlParams,
|
||||
refreshTimeRange: defaultRefresh
|
||||
});
|
||||
|
||||
const UrlParamsProvider: React.FC<{}> = ({ children }) => {
|
||||
const location = useLocation();
|
||||
const [urlParams, dispatch] = useReducer(
|
||||
urlParamsReducer,
|
||||
resolveUrlParams(location)
|
||||
);
|
||||
|
||||
function refreshTimeRange(time: TimeRange) {
|
||||
dispatch({ type: TIME_RANGE_REFRESH, time });
|
||||
}
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
dispatch({ type: LOCATION_UPDATE, location });
|
||||
},
|
||||
[location]
|
||||
);
|
||||
|
||||
return (
|
||||
<UrlParamsContext.Provider
|
||||
children={children}
|
||||
value={{ urlParams, refreshTimeRange }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { UrlParamsContext, UrlParamsProvider };
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { Location } from 'history';
|
||||
import { IUrlParams } from './types';
|
||||
import {
|
||||
getPathParams,
|
||||
removeUndefinedProps,
|
||||
getStart,
|
||||
getEnd,
|
||||
toBoolean,
|
||||
toNumber,
|
||||
toString
|
||||
} from './helpers';
|
||||
import {
|
||||
toQuery,
|
||||
legacyDecodeURIComponent
|
||||
} from '../../components/shared/Links/url_helpers';
|
||||
import { TIMEPICKER_DEFAULTS } from './constants';
|
||||
|
||||
export function resolveUrlParams(location: Location, state: IUrlParams = {}) {
|
||||
const {
|
||||
processorEvent,
|
||||
serviceName,
|
||||
transactionName,
|
||||
transactionType,
|
||||
errorGroupId
|
||||
} = getPathParams(location.pathname);
|
||||
|
||||
const {
|
||||
traceId,
|
||||
transactionId,
|
||||
detailTab,
|
||||
flyoutDetailTab,
|
||||
waterfallItemId,
|
||||
spanId,
|
||||
page,
|
||||
sortDirection,
|
||||
sortField,
|
||||
kuery,
|
||||
refreshPaused = TIMEPICKER_DEFAULTS.refreshPaused,
|
||||
refreshInterval = TIMEPICKER_DEFAULTS.refreshInterval,
|
||||
rangeFrom = TIMEPICKER_DEFAULTS.rangeFrom,
|
||||
rangeTo = TIMEPICKER_DEFAULTS.rangeTo
|
||||
} = toQuery(location.search);
|
||||
|
||||
return removeUndefinedProps({
|
||||
...state,
|
||||
// date params
|
||||
start: getStart(state, rangeFrom),
|
||||
end: getEnd(state, rangeTo),
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
refreshPaused: toBoolean(refreshPaused),
|
||||
refreshInterval: toNumber(refreshInterval),
|
||||
// query params
|
||||
sortDirection,
|
||||
sortField,
|
||||
page: toNumber(page) || 0,
|
||||
transactionId: toString(transactionId),
|
||||
traceId: toString(traceId),
|
||||
waterfallItemId: toString(waterfallItemId),
|
||||
detailTab: toString(detailTab),
|
||||
flyoutDetailTab: toString(flyoutDetailTab),
|
||||
spanId: toNumber(spanId),
|
||||
kuery: legacyDecodeURIComponent(kuery),
|
||||
// path params
|
||||
processorEvent,
|
||||
serviceName,
|
||||
transactionType: legacyDecodeURIComponent(transactionType),
|
||||
transactionName: legacyDecodeURIComponent(transactionName),
|
||||
errorGroupId
|
||||
});
|
||||
}
|
26
x-pack/plugins/apm/public/context/UrlParamsContext/types.ts
Normal file
26
x-pack/plugins/apm/public/context/UrlParamsContext/types.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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 interface IUrlParams {
|
||||
detailTab?: string;
|
||||
end?: string;
|
||||
errorGroupId?: string;
|
||||
flyoutDetailTab?: string;
|
||||
kuery?: string;
|
||||
rangeFrom?: string;
|
||||
rangeTo?: string;
|
||||
refreshInterval?: number;
|
||||
refreshPaused?: boolean;
|
||||
serviceName?: string;
|
||||
sortDirection?: string;
|
||||
sortField?: string;
|
||||
start?: string;
|
||||
traceId?: string;
|
||||
transactionId?: string;
|
||||
transactionName?: string;
|
||||
transactionType?: string;
|
||||
waterfallItemId?: string;
|
||||
}
|
|
@ -8,11 +8,8 @@ import { useMemo } from 'react';
|
|||
import { MetricsChartAPIResponse } from '../../server/lib/metrics/get_all_metrics_chart_data';
|
||||
import { MemoryChartAPIResponse } from '../../server/lib/metrics/get_memory_chart_data';
|
||||
import { loadMetricsChartDataForService } from '../services/rest/apm/metrics';
|
||||
import {
|
||||
getCPUSeries,
|
||||
getMemorySeries
|
||||
} from '../store/selectors/chartSelectors';
|
||||
import { IUrlParams } from '../store/urlParams';
|
||||
import { getCPUSeries, getMemorySeries } from '../selectors/chartSelectors';
|
||||
import { IUrlParams } from '../context/UrlParamsContext/types';
|
||||
import { useFetcher } from './useFetcher';
|
||||
|
||||
const memory: MemoryChartAPIResponse = {
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
|
||||
import { useMemo } from 'react';
|
||||
import { loadTransactionDetailsCharts } from '../services/rest/apm/transaction_groups';
|
||||
import { getTransactionCharts } from '../store/selectors/chartSelectors';
|
||||
import { IUrlParams } from '../store/urlParams';
|
||||
import { getTransactionCharts } from '../selectors/chartSelectors';
|
||||
import { IUrlParams } from '../context/UrlParamsContext/types';
|
||||
import { useFetcher } from './useFetcher';
|
||||
|
||||
export function useTransactionDetailsCharts(urlParams: IUrlParams) {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { loadTransactionDistribution } from '../services/rest/apm/transaction_groups';
|
||||
import { IUrlParams } from '../store/urlParams';
|
||||
import { IUrlParams } from '../context/UrlParamsContext/types';
|
||||
import { useFetcher } from './useFetcher';
|
||||
|
||||
const INITIAL_DATA = {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import { useMemo } from 'react';
|
||||
import { TransactionListAPIResponse } from '../../server/lib/transactions/get_top_transactions';
|
||||
import { loadTransactionList } from '../services/rest/apm/transaction_groups';
|
||||
import { IUrlParams } from '../store/urlParams';
|
||||
import { IUrlParams } from '../context/UrlParamsContext/types';
|
||||
import { useFetcher } from './useFetcher';
|
||||
|
||||
const getRelativeImpact = (
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
|
||||
import { useMemo } from 'react';
|
||||
import { loadTransactionOverviewCharts } from '../services/rest/apm/transaction_groups';
|
||||
import { getTransactionCharts } from '../store/selectors/chartSelectors';
|
||||
import { IUrlParams } from '../store/urlParams';
|
||||
import { getTransactionCharts } from '../selectors/chartSelectors';
|
||||
import { IUrlParams } from '../context/UrlParamsContext/types';
|
||||
import { useFetcher } from './useFetcher';
|
||||
|
||||
export function useTransactionOverviewCharts(urlParams: IUrlParams) {
|
||||
|
|
|
@ -4,8 +4,9 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import storeProd from './configureStore.prod';
|
||||
import storeDev from './configureStore.dev';
|
||||
import { useContext } from 'react';
|
||||
import { UrlParamsContext } from '../context/UrlParamsContext';
|
||||
|
||||
const store = process.env.NODE_ENV === 'production' ? storeProd : storeDev;
|
||||
export default store;
|
||||
export function useUrlParams() {
|
||||
return useContext(UrlParamsContext);
|
||||
}
|
|
@ -7,7 +7,7 @@
|
|||
import { useMemo } from 'react';
|
||||
import { getWaterfall } from '../components/app/TransactionDetails/Transaction/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers';
|
||||
import { loadTrace } from '../services/rest/apm/traces';
|
||||
import { IUrlParams } from '../store/urlParams';
|
||||
import { IUrlParams } from '../context/UrlParamsContext/types';
|
||||
import { useFetcher } from './useFetcher';
|
||||
|
||||
const INITIAL_DATA = { trace: [], errorsPerTransaction: {} };
|
||||
|
|
|
@ -16,8 +16,6 @@ import { I18nContext } from 'ui/i18n';
|
|||
import { uiModules } from 'ui/modules';
|
||||
import 'uiExports/autocompleteProviders';
|
||||
import { GlobalHelpExtension } from './components/app/GlobalHelpExtension';
|
||||
// @ts-ignore
|
||||
import configureStore from './store/config/configureStore';
|
||||
import { plugin } from './new-platform';
|
||||
import { REACT_APP_ROOT_ID } from './new-platform/plugin';
|
||||
import './style/global_overrides.css';
|
||||
|
|
|
@ -6,30 +6,27 @@
|
|||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { CoreSetup } from 'src/core/public';
|
||||
import { Main } from '../components/app/Main';
|
||||
import { history } from '../components/shared/Links/url_helpers';
|
||||
import { history } from '../utils/history';
|
||||
import { LocationProvider } from '../context/LocationContext';
|
||||
// @ts-ignore
|
||||
import configureStore from '../store/config/configureStore';
|
||||
import { UrlParamsProvider } from '../context/UrlParamsContext';
|
||||
|
||||
export const REACT_APP_ROOT_ID = 'react-apm-root';
|
||||
|
||||
export class Plugin {
|
||||
public setup(core: CoreSetup) {
|
||||
const { i18n } = core;
|
||||
const store = configureStore();
|
||||
ReactDOM.render(
|
||||
<i18n.Context>
|
||||
<Provider store={store}>
|
||||
<Router history={history}>
|
||||
<LocationProvider history={history}>
|
||||
<Router history={history}>
|
||||
<LocationProvider history={history}>
|
||||
<UrlParamsProvider>
|
||||
<Main />
|
||||
</LocationProvider>
|
||||
</Router>
|
||||
</Provider>
|
||||
</UrlParamsProvider>
|
||||
</LocationProvider>
|
||||
</Router>
|
||||
</i18n.Context>,
|
||||
document.getElementById(REACT_APP_ROOT_ID)
|
||||
);
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { ApmTimeSeriesResponse } from '../../../../server/lib/transactions/charts/get_timeseries_data/transform';
|
||||
import { ApmTimeSeriesResponse } from '../../../server/lib/transactions/charts/get_timeseries_data/transform';
|
||||
import {
|
||||
getAnomalyScoreSeries,
|
||||
getResponseTimeSeries,
|
|
@ -10,18 +10,13 @@ import d3 from 'd3';
|
|||
import { difference, memoize, zipObject } from 'lodash';
|
||||
import mean from 'lodash.mean';
|
||||
import { rgba } from 'polished';
|
||||
import { MetricsChartAPIResponse } from '../../../server/lib/metrics/get_all_metrics_chart_data';
|
||||
import { TimeSeriesAPIResponse } from '../../../server/lib/transactions/charts';
|
||||
import { ApmTimeSeriesResponse } from '../../../server/lib/transactions/charts/get_timeseries_data/transform';
|
||||
import { StringMap } from '../../../typings/common';
|
||||
import { Coordinate, RectCoordinate } from '../../../typings/timeseries';
|
||||
import {
|
||||
asDecimal,
|
||||
asMillis,
|
||||
asPercent,
|
||||
tpmUnit
|
||||
} from '../../utils/formatters';
|
||||
import { IUrlParams } from '../urlParams';
|
||||
import { MetricsChartAPIResponse } from '../../server/lib/metrics/get_all_metrics_chart_data';
|
||||
import { TimeSeriesAPIResponse } from '../../server/lib/transactions/charts';
|
||||
import { ApmTimeSeriesResponse } from '../../server/lib/transactions/charts/get_timeseries_data/transform';
|
||||
import { StringMap } from '../../typings/common';
|
||||
import { Coordinate, RectCoordinate } from '../../typings/timeseries';
|
||||
import { asDecimal, asMillis, asPercent, tpmUnit } from '../utils/formatters';
|
||||
import { IUrlParams } from '../context/UrlParamsContext/types';
|
||||
|
||||
export const getEmptySerie = memoize(
|
||||
(
|
|
@ -8,7 +8,7 @@ import { ErrorDistributionAPIResponse } from '../../../../server/lib/errors/dist
|
|||
import { ErrorGroupAPIResponse } from '../../../../server/lib/errors/get_error_group';
|
||||
import { ErrorGroupListAPIResponse } from '../../../../server/lib/errors/get_error_groups';
|
||||
import { MissingArgumentsError } from '../../../hooks/useFetcher';
|
||||
import { IUrlParams } from '../../../store/urlParams';
|
||||
import { IUrlParams } from '../../../context/UrlParamsContext/types';
|
||||
import { callApi } from '../callApi';
|
||||
import { getEncodedEsQuery } from './apm';
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { MetricsChartAPIResponse } from '../../../../server/lib/metrics/get_all_metrics_chart_data';
|
||||
import { IUrlParams } from '../../../store/urlParams';
|
||||
import { IUrlParams } from '../../../context/UrlParamsContext/types';
|
||||
import { callApi } from '../callApi';
|
||||
import { getEncodedEsQuery } from './apm';
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import { ServiceAPIResponse } from '../../../../server/lib/services/get_service';
|
||||
import { ServiceListAPIResponse } from '../../../../server/lib/services/get_services';
|
||||
import { MissingArgumentsError } from '../../../hooks/useFetcher';
|
||||
import { IUrlParams } from '../../../store/urlParams';
|
||||
import { IUrlParams } from '../../../context/UrlParamsContext/types';
|
||||
import { callApi } from '../callApi';
|
||||
import { getEncodedEsQuery } from './apm';
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import { TraceListAPIResponse } from '../../../../server/lib/traces/get_top_traces';
|
||||
import { TraceAPIResponse } from '../../../../server/lib/traces/get_trace';
|
||||
import { MissingArgumentsError } from '../../../hooks/useFetcher';
|
||||
import { IUrlParams } from '../../../store/urlParams';
|
||||
import { IUrlParams } from '../../../context/UrlParamsContext/types';
|
||||
import { callApi } from '../callApi';
|
||||
import { getEncodedEsQuery } from './apm';
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ import { TimeSeriesAPIResponse } from '../../../../server/lib/transactions/chart
|
|||
import { ITransactionDistributionAPIResponse } from '../../../../server/lib/transactions/distribution';
|
||||
import { TransactionListAPIResponse } from '../../../../server/lib/transactions/get_top_transactions';
|
||||
import { MissingArgumentsError } from '../../../hooks/useFetcher';
|
||||
import { IUrlParams } from '../../../store/urlParams';
|
||||
import { IUrlParams } from '../../../context/UrlParamsContext/types';
|
||||
import { callApi } from '../callApi';
|
||||
import { getEncodedEsQuery } from './apm';
|
||||
|
||||
|
|
|
@ -1,18 +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 { rootReducer } from '../rootReducer';
|
||||
|
||||
describe('root reducer', () => {
|
||||
it('should return the initial state', () => {
|
||||
const state = rootReducer(undefined, {} as any);
|
||||
|
||||
expect(state).toEqual({
|
||||
location: { hash: '', pathname: '', search: '' },
|
||||
urlParams: {}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,113 +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 { Location } from 'history';
|
||||
import { mockNow } from '../../utils/testHelpers';
|
||||
import { updateLocation } from '../location';
|
||||
import { APMAction, refreshTimeRange, urlParamsReducer } from '../urlParams';
|
||||
|
||||
describe('urlParams', () => {
|
||||
beforeEach(() => {
|
||||
mockNow('2010');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should parse "last 15 minutes"', () => {
|
||||
const action = updateLocation({
|
||||
pathname: '',
|
||||
search: '?rangeFrom=now-15m&rangeTo=now'
|
||||
} as Location) as APMAction;
|
||||
const { start, end } = urlParamsReducer({}, action);
|
||||
|
||||
expect({ start, end }).toEqual({
|
||||
start: '2009-12-31T23:45:00.000Z',
|
||||
end: '2010-01-01T00:00:00.000Z'
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse "last 7 days"', () => {
|
||||
const action = updateLocation({
|
||||
pathname: '',
|
||||
search: '?rangeFrom=now-7d&rangeTo=now'
|
||||
} as Location) as APMAction;
|
||||
const { start, end } = urlParamsReducer({}, action);
|
||||
|
||||
expect({ start, end }).toEqual({
|
||||
start: '2009-12-25T00:00:00.000Z',
|
||||
end: '2010-01-01T00:00:00.000Z'
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse absolute dates', () => {
|
||||
const action = updateLocation({
|
||||
pathname: '',
|
||||
search:
|
||||
'?rangeFrom=2019-02-03T10:00:00.000Z&rangeTo=2019-02-10T16:30:00.000Z'
|
||||
} as Location) as APMAction;
|
||||
const { start, end } = urlParamsReducer({}, action);
|
||||
|
||||
expect({ start, end }).toEqual({
|
||||
start: '2019-02-03T10:00:00.000Z',
|
||||
end: '2019-02-10T16:30:00.000Z'
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle LOCATION_UPDATE for transactions section', () => {
|
||||
const action = updateLocation({
|
||||
pathname:
|
||||
'myServiceName/transactions/myTransactionType/myTransactionName/b/c',
|
||||
search: '?transactionId=myTransactionId&detailTab=request&spanId=10'
|
||||
} as Location) as APMAction;
|
||||
const state = urlParamsReducer({}, action);
|
||||
|
||||
expect(state).toEqual({
|
||||
detailTab: 'request',
|
||||
end: '2010-01-01T00:00:00.000Z',
|
||||
page: 0,
|
||||
processorEvent: 'transaction',
|
||||
rangeFrom: 'now-24h',
|
||||
rangeTo: 'now',
|
||||
refreshInterval: 0,
|
||||
refreshPaused: true,
|
||||
serviceName: 'myServiceName',
|
||||
spanId: 10,
|
||||
start: '2009-12-31T00:00:00.000Z',
|
||||
transactionId: 'myTransactionId',
|
||||
transactionName: 'myTransactionName',
|
||||
transactionType: 'myTransactionType'
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle LOCATION_UPDATE for error section', () => {
|
||||
const action = updateLocation({
|
||||
pathname: 'myServiceName/errors/myErrorGroupId',
|
||||
search: '?detailTab=request&transactionId=myTransactionId'
|
||||
} as Location) as APMAction;
|
||||
const state = urlParamsReducer({}, action);
|
||||
|
||||
expect(state).toEqual(
|
||||
expect.objectContaining({
|
||||
serviceName: 'myServiceName',
|
||||
errorGroupId: 'myErrorGroupId',
|
||||
detailTab: 'request',
|
||||
transactionId: 'myTransactionId'
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle refreshTimeRange action', () => {
|
||||
const action = refreshTimeRange({ rangeFrom: 'now', rangeTo: 'now-15m' });
|
||||
const state = urlParamsReducer({}, action);
|
||||
|
||||
expect(state).toEqual({
|
||||
start: '2010-01-01T00:00:00.000Z',
|
||||
end: '2009-12-31T23:45:00.000Z'
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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 { createStore, applyMiddleware, compose } from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
import throttle from '../middleware/throttle';
|
||||
import { rootReducer } from '../rootReducer';
|
||||
|
||||
export default function configureStore(preloadedState) {
|
||||
const composeEnhancers =
|
||||
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
|
||||
|
||||
return createStore(
|
||||
rootReducer,
|
||||
preloadedState,
|
||||
composeEnhancers(applyMiddleware(throttle, thunk))
|
||||
);
|
||||
}
|
|
@ -1,13 +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, applyMiddleware } from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
import { rootReducer } from '../rootReducer';
|
||||
|
||||
export default function configureStore(preloadedState) {
|
||||
return createStore(rootReducer, preloadedState, applyMiddleware(thunk));
|
||||
}
|
|
@ -1,29 +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 { Location } from 'history';
|
||||
import { AnyAction } from 'redux';
|
||||
|
||||
export const LOCATION_UPDATE = 'LOCATION_UPDATE';
|
||||
|
||||
export function locationReducer(
|
||||
state = { pathname: '', search: '', hash: '' },
|
||||
action: AnyAction
|
||||
) {
|
||||
switch (action.type) {
|
||||
case LOCATION_UPDATE:
|
||||
return action.location;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export function updateLocation(nextLocation: Location) {
|
||||
return {
|
||||
type: LOCATION_UPDATE,
|
||||
location: nextLocation
|
||||
};
|
||||
}
|
|
@ -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.
|
||||
*/
|
||||
|
||||
// Throttle middleware for redux
|
||||
// Should only be used during development
|
||||
//
|
||||
// tldr: While developing, it is easy to make minor mistakes that results in infinite dispatch loops.
|
||||
// Longer Motivation: Infinite dispatch loop occurs, if a component dispatches an action, that in turn re-renders the component, which then again dispatches.
|
||||
// Normally this is guarded, but if the condition is missing/erroneous an infinite loop happens.
|
||||
// The root cause is mostly very simple to fix (update an if statement) but the infinite loop causes the browser to be unresponsive
|
||||
// and only by killing and restarting the process can development continue.
|
||||
// Solution: Block actions if more than x dispatches happens within y seconds
|
||||
|
||||
const MAX_DISPATCHES = 50;
|
||||
const INTERVAL_MS = 2000;
|
||||
let IS_THROTTLED = false;
|
||||
|
||||
let count = 0;
|
||||
setInterval(() => {
|
||||
count = 0;
|
||||
}, INTERVAL_MS);
|
||||
|
||||
function throttle() {
|
||||
return next => action => {
|
||||
count += 1;
|
||||
|
||||
if (count > MAX_DISPATCHES || IS_THROTTLED) {
|
||||
IS_THROTTLED = true;
|
||||
console.error('Action was throttled', action);
|
||||
return {};
|
||||
}
|
||||
|
||||
return next(action);
|
||||
};
|
||||
}
|
||||
|
||||
export default throttle;
|
|
@ -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 { Location } from 'history';
|
||||
import { combineReducers } from 'redux';
|
||||
import { locationReducer } from './location';
|
||||
import { IUrlParams, urlParamsReducer } from './urlParams';
|
||||
|
||||
export interface IReduxState {
|
||||
location: Location;
|
||||
urlParams: IUrlParams;
|
||||
}
|
||||
|
||||
export const rootReducer = combineReducers({
|
||||
location: locationReducer,
|
||||
urlParams: urlParamsReducer
|
||||
});
|
|
@ -1,229 +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 datemath from '@elastic/datemath';
|
||||
import { Location } from 'history';
|
||||
import { compact, pick } from 'lodash';
|
||||
import {
|
||||
legacyDecodeURIComponent,
|
||||
toQuery
|
||||
} from '../components/shared/Links/url_helpers';
|
||||
import { LOCATION_UPDATE } from './location';
|
||||
import { IReduxState } from './rootReducer';
|
||||
|
||||
// ACTION TYPES
|
||||
export const TIME_RANGE_REFRESH = 'TIME_RANGE_REFRESH';
|
||||
export const TIMEPICKER_DEFAULTS = {
|
||||
rangeFrom: 'now-24h',
|
||||
rangeTo: 'now',
|
||||
refreshPaused: 'true',
|
||||
refreshInterval: '0'
|
||||
};
|
||||
|
||||
interface TimeRange {
|
||||
rangeFrom: string;
|
||||
rangeTo: string;
|
||||
}
|
||||
|
||||
interface LocationAction {
|
||||
type: typeof LOCATION_UPDATE;
|
||||
location: Location;
|
||||
}
|
||||
interface TimeRangeRefreshAction {
|
||||
type: typeof TIME_RANGE_REFRESH;
|
||||
time: TimeRange;
|
||||
}
|
||||
export type APMAction = LocationAction | TimeRangeRefreshAction;
|
||||
|
||||
function getParsedDate(rawDate?: string, opts = {}) {
|
||||
if (rawDate) {
|
||||
const parsed = datemath.parse(rawDate, opts);
|
||||
if (parsed) {
|
||||
return parsed.toISOString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getStart(prevState: IUrlParams, rangeFrom?: string) {
|
||||
if (prevState.rangeFrom !== rangeFrom) {
|
||||
return getParsedDate(rangeFrom);
|
||||
}
|
||||
return prevState.start;
|
||||
}
|
||||
|
||||
function getEnd(prevState: IUrlParams, rangeTo?: string) {
|
||||
if (prevState.rangeTo !== rangeTo) {
|
||||
return getParsedDate(rangeTo, { roundUp: true });
|
||||
}
|
||||
return prevState.end;
|
||||
}
|
||||
|
||||
// "urlParams" contains path and query parameters from the url, that can be easily consumed from
|
||||
// any (container) component with access to the store
|
||||
|
||||
// Example:
|
||||
// url: /opbeans-backend/Brewing%20Bot?transactionId=1321
|
||||
// serviceName: opbeans-backend (path param)
|
||||
// transactionType: Brewing%20Bot (path param)
|
||||
// transactionId: 1321 (query param)
|
||||
export function urlParamsReducer(
|
||||
state: IUrlParams = {},
|
||||
action: APMAction
|
||||
): IUrlParams {
|
||||
switch (action.type) {
|
||||
case LOCATION_UPDATE: {
|
||||
const {
|
||||
processorEvent,
|
||||
serviceName,
|
||||
transactionName,
|
||||
transactionType,
|
||||
errorGroupId
|
||||
} = getPathParams(action.location.pathname);
|
||||
|
||||
const {
|
||||
traceId,
|
||||
transactionId,
|
||||
detailTab,
|
||||
flyoutDetailTab,
|
||||
waterfallItemId,
|
||||
spanId,
|
||||
page,
|
||||
sortDirection,
|
||||
sortField,
|
||||
kuery,
|
||||
refreshPaused = TIMEPICKER_DEFAULTS.refreshPaused,
|
||||
refreshInterval = TIMEPICKER_DEFAULTS.refreshInterval,
|
||||
rangeFrom = TIMEPICKER_DEFAULTS.rangeFrom,
|
||||
rangeTo = TIMEPICKER_DEFAULTS.rangeTo
|
||||
} = toQuery(action.location.search);
|
||||
|
||||
return removeUndefinedProps({
|
||||
...state,
|
||||
|
||||
// date params
|
||||
start: getStart(state, rangeFrom),
|
||||
end: getEnd(state, rangeTo),
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
refreshPaused: toBoolean(refreshPaused),
|
||||
refreshInterval: toNumber(refreshInterval),
|
||||
|
||||
// query params
|
||||
sortDirection,
|
||||
sortField,
|
||||
page: toNumber(page) || 0,
|
||||
transactionId: toString(transactionId),
|
||||
traceId: toString(traceId),
|
||||
waterfallItemId: toString(waterfallItemId),
|
||||
detailTab: toString(detailTab),
|
||||
flyoutDetailTab: toString(flyoutDetailTab),
|
||||
spanId: toNumber(spanId),
|
||||
kuery: legacyDecodeURIComponent(kuery),
|
||||
|
||||
// path params
|
||||
processorEvent,
|
||||
serviceName,
|
||||
transactionType: legacyDecodeURIComponent(transactionType),
|
||||
transactionName: legacyDecodeURIComponent(transactionName),
|
||||
errorGroupId
|
||||
});
|
||||
}
|
||||
|
||||
case TIME_RANGE_REFRESH:
|
||||
return {
|
||||
...state,
|
||||
start: getParsedDate(action.time.rangeFrom),
|
||||
end: getParsedDate(action.time.rangeTo)
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export function toNumber(value?: string) {
|
||||
if (value !== undefined) {
|
||||
return parseInt(value, 10);
|
||||
}
|
||||
}
|
||||
|
||||
function toString(value?: string) {
|
||||
if (value === '' || value === 'null' || value === 'undefined') {
|
||||
return;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function toBoolean(value?: string) {
|
||||
return value === 'true';
|
||||
}
|
||||
|
||||
function getPathAsArray(pathname: string) {
|
||||
return compact(pathname.split('/'));
|
||||
}
|
||||
|
||||
function removeUndefinedProps<T>(obj: T): Partial<T> {
|
||||
return pick(obj, value => value !== undefined);
|
||||
}
|
||||
|
||||
function getPathParams(pathname: string) {
|
||||
const paths = getPathAsArray(pathname);
|
||||
const pageName = paths[1];
|
||||
|
||||
switch (pageName) {
|
||||
case 'transactions':
|
||||
return {
|
||||
processorEvent: 'transaction',
|
||||
serviceName: paths[0],
|
||||
transactionType: paths[2],
|
||||
transactionName: paths[3]
|
||||
};
|
||||
case 'errors':
|
||||
return {
|
||||
processorEvent: 'error',
|
||||
serviceName: paths[0],
|
||||
errorGroupId: paths[2]
|
||||
};
|
||||
case 'metrics':
|
||||
return {
|
||||
processorEvent: 'metric',
|
||||
serviceName: paths[0]
|
||||
};
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// ACTION CREATORS
|
||||
export function refreshTimeRange(time: TimeRange): TimeRangeRefreshAction {
|
||||
return { type: TIME_RANGE_REFRESH, time };
|
||||
}
|
||||
|
||||
// Selectors
|
||||
export function getUrlParams(state: IReduxState) {
|
||||
return state.urlParams;
|
||||
}
|
||||
|
||||
export interface IUrlParams {
|
||||
detailTab?: string;
|
||||
end?: string;
|
||||
errorGroupId?: string;
|
||||
flyoutDetailTab?: string;
|
||||
kuery?: string;
|
||||
rangeFrom?: string;
|
||||
rangeTo?: string;
|
||||
refreshInterval?: number;
|
||||
refreshPaused?: boolean;
|
||||
serviceName?: string;
|
||||
sortDirection?: string;
|
||||
sortField?: string;
|
||||
start?: string;
|
||||
traceId?: string;
|
||||
transactionId?: string;
|
||||
transactionName?: string;
|
||||
transactionType?: string;
|
||||
waterfallItemId?: string;
|
||||
}
|
13
x-pack/plugins/apm/public/utils/history.ts
Normal file
13
x-pack/plugins/apm/public/utils/history.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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 createHistory from 'history/createHashHistory';
|
||||
|
||||
// Make history singleton available across APM project
|
||||
// TODO: Explore using React context or hook instead?
|
||||
const history = createHistory();
|
||||
|
||||
export { history };
|
Loading…
Add table
Add a link
Reference in a new issue