mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[APM] Fix Datepicker double loading and move date parsing to urlParams (#33560)
This commit is contained in:
parent
721161f3d1
commit
ed2874bf54
12 changed files with 347 additions and 329 deletions
|
@ -54,10 +54,17 @@ export class MachineLearningFlyout extends Component<FlyoutProps, FlyoutState> {
|
|||
hasMLJob: false,
|
||||
selectedTransactionType: this.props.urlParams.transactionType
|
||||
};
|
||||
public willUnmount = false;
|
||||
|
||||
public componentWillUnmount() {
|
||||
this.willUnmount = true;
|
||||
}
|
||||
|
||||
public async componentDidMount() {
|
||||
const indexPattern = await getAPMIndexPattern();
|
||||
this.setState({ hasIndexPattern: !!indexPattern });
|
||||
if (!this.willUnmount) {
|
||||
this.setState({ hasIndexPattern: !!indexPattern });
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: This should use `getDerivedStateFromProps`
|
||||
|
|
|
@ -5,15 +5,13 @@
|
|||
*/
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { getServiceDetails } from 'x-pack/plugins/apm/public/store/reactReduxRequest/serviceDetails';
|
||||
import { IReduxState } from 'x-pack/plugins/apm/public/store/rootReducer';
|
||||
import { selectIsMLAvailable } from 'x-pack/plugins/apm/public/store/selectors/license';
|
||||
import { ServiceIntegrationsView } from './view';
|
||||
|
||||
function mapStateToProps(state = {} as IReduxState) {
|
||||
return {
|
||||
mlAvailable: selectIsMLAvailable(state),
|
||||
serviceDetails: getServiceDetails(state).data
|
||||
mlAvailable: selectIsMLAvailable(state)
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -22,9 +22,7 @@ import { WatcherFlyout } from './WatcherFlyout';
|
|||
export interface ServiceIntegrationProps {
|
||||
mlAvailable: boolean;
|
||||
location: Location;
|
||||
serviceDetails: {
|
||||
types: string[];
|
||||
};
|
||||
transactionTypes: string[];
|
||||
urlParams: IUrlParams;
|
||||
}
|
||||
interface ServiceIntegrationState {
|
||||
|
@ -171,7 +169,7 @@ export class ServiceIntegrationsView extends React.Component<
|
|||
isOpen={this.state.activeFlyout === 'ML'}
|
||||
onClose={this.closeFlyouts}
|
||||
urlParams={this.props.urlParams}
|
||||
serviceTransactionTypes={this.props.serviceDetails.types}
|
||||
serviceTransactionTypes={this.props.transactionTypes}
|
||||
/>
|
||||
<WatcherFlyout
|
||||
location={this.props.location}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
import { Location } from 'history';
|
||||
import React from 'react';
|
||||
import React, { Fragment } from 'react';
|
||||
import { ServiceDetailsRequest } from 'x-pack/plugins/apm/public/store/reactReduxRequest/serviceDetails';
|
||||
import { IUrlParams } from 'x-pack/plugins/apm/public/store/urlParams';
|
||||
// @ts-ignore
|
||||
|
@ -23,36 +23,39 @@ export class ServiceDetailsView extends React.Component<ServiceDetailsProps> {
|
|||
public render() {
|
||||
const { urlParams, location } = this.props;
|
||||
return (
|
||||
<React.Fragment>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="l">
|
||||
<h1>{urlParams.serviceName}</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<ServiceIntegrations
|
||||
location={this.props.location}
|
||||
urlParams={urlParams}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<ServiceDetailsRequest
|
||||
urlParams={urlParams}
|
||||
render={({ data }) => {
|
||||
return (
|
||||
<Fragment>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="l">
|
||||
<h1>{urlParams.serviceName}</h1>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<ServiceIntegrations
|
||||
transactionTypes={data.types}
|
||||
location={this.props.location}
|
||||
urlParams={urlParams}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer />
|
||||
<EuiSpacer />
|
||||
|
||||
<FilterBar />
|
||||
<FilterBar />
|
||||
|
||||
<ServiceDetailsRequest
|
||||
urlParams={urlParams}
|
||||
render={({ data }) => (
|
||||
<ServiceDetailTabs
|
||||
location={location}
|
||||
urlParams={urlParams}
|
||||
transactionTypes={data.types}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</React.Fragment>
|
||||
<ServiceDetailTabs
|
||||
location={location}
|
||||
urlParams={urlParams}
|
||||
transactionTypes={data.types}
|
||||
/>
|
||||
</Fragment>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,94 +4,53 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import datemath from '@elastic/datemath';
|
||||
import { EuiSuperDatePicker, EuiSuperDatePickerProps } 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 {
|
||||
TIMEPICKER_DEFAULTS,
|
||||
toBoolean,
|
||||
toNumber,
|
||||
updateTimePicker
|
||||
getUrlParams,
|
||||
IUrlParams,
|
||||
refreshTimeRange
|
||||
} from '../../../store/urlParams';
|
||||
import { fromQuery, toQuery } from '../Links/url_helpers';
|
||||
|
||||
export interface DatePickerProps extends RouteComponentProps {
|
||||
dispatchUpdateTimePicker: typeof updateTimePicker;
|
||||
dispatchRefreshTimeRange: typeof refreshTimeRange;
|
||||
urlParams: IUrlParams;
|
||||
}
|
||||
|
||||
export class DatePickerComponent extends React.Component<DatePickerProps> {
|
||||
public refreshTimeoutId = 0;
|
||||
|
||||
public getParamsFromSearch = (search: string) => {
|
||||
const { rangeFrom, rangeTo, refreshPaused, refreshInterval } = {
|
||||
...TIMEPICKER_DEFAULTS,
|
||||
...toQuery(search)
|
||||
};
|
||||
return {
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
refreshPaused: toBoolean(refreshPaused),
|
||||
refreshInterval: toNumber(refreshInterval)
|
||||
};
|
||||
};
|
||||
|
||||
public componentDidMount() {
|
||||
this.dispatchTimeRangeUpdate();
|
||||
}
|
||||
|
||||
public componentDidUpdate() {
|
||||
this.dispatchTimeRangeUpdate();
|
||||
}
|
||||
|
||||
public dispatchTimeRangeUpdate() {
|
||||
const { rangeFrom, rangeTo } = this.getParamsFromSearch(
|
||||
this.props.location.search
|
||||
);
|
||||
const parsed = {
|
||||
from: datemath.parse(rangeFrom),
|
||||
// roundUp: true is required for the quick select relative date values to work properly
|
||||
to: datemath.parse(rangeTo, { roundUp: true })
|
||||
};
|
||||
if (!parsed.from || !parsed.to) {
|
||||
return;
|
||||
}
|
||||
const min = parsed.from.toISOString();
|
||||
const max = parsed.to.toISOString();
|
||||
this.props.dispatchUpdateTimePicker({ min, max });
|
||||
}
|
||||
|
||||
public updateUrl(nextQuery: {
|
||||
rangeFrom?: string;
|
||||
rangeTo?: string;
|
||||
refreshPaused?: boolean;
|
||||
refreshInterval?: number;
|
||||
}) {
|
||||
const currentQuery = toQuery(this.props.location.search);
|
||||
const nextSearch = fromQuery({ ...currentQuery, ...nextQuery });
|
||||
|
||||
// Compare the encoded versions of current and next search string, and if they're the same,
|
||||
// use replace instead of push to prevent an unnecessary stack entry which breaks the back button.
|
||||
const currentSearch = fromQuery(currentQuery);
|
||||
const { push, replace } = this.props.history;
|
||||
const update = currentSearch === nextSearch ? replace : push;
|
||||
|
||||
update({ ...this.props.location, search: nextSearch });
|
||||
const { history, location } = this.props;
|
||||
history.push({
|
||||
...location,
|
||||
search: fromQuery({ ...toQuery(location.search), ...nextQuery })
|
||||
});
|
||||
}
|
||||
|
||||
public handleRefreshChange: EuiSuperDatePickerProps['onRefreshChange'] = ({
|
||||
public onRefreshChange: EuiSuperDatePickerProps['onRefreshChange'] = ({
|
||||
isPaused,
|
||||
refreshInterval
|
||||
}) => {
|
||||
this.updateUrl({
|
||||
refreshPaused: isPaused,
|
||||
refreshInterval
|
||||
});
|
||||
this.updateUrl({ refreshPaused: isPaused, refreshInterval });
|
||||
};
|
||||
|
||||
public handleTimeChange = (options: { start: string; end: string }) => {
|
||||
this.updateUrl({ rangeFrom: options.start, rangeTo: options.end });
|
||||
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() {
|
||||
|
@ -100,26 +59,31 @@ export class DatePickerComponent extends React.Component<DatePickerProps> {
|
|||
rangeTo,
|
||||
refreshPaused,
|
||||
refreshInterval
|
||||
} = this.getParamsFromSearch(this.props.location.search);
|
||||
} = this.props.urlParams;
|
||||
return (
|
||||
<EuiSuperDatePicker
|
||||
start={rangeFrom}
|
||||
end={rangeTo}
|
||||
isPaused={refreshPaused}
|
||||
refreshInterval={refreshInterval}
|
||||
onTimeChange={this.handleTimeChange}
|
||||
onRefresh={this.handleTimeChange}
|
||||
onRefreshChange={this.handleRefreshChange}
|
||||
onTimeChange={this.onTimeChange}
|
||||
onRefresh={this.onRefresh}
|
||||
onRefreshChange={this.onRefreshChange}
|
||||
showUpdateButton={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: IReduxState) => ({
|
||||
urlParams: getUrlParams(state)
|
||||
});
|
||||
const mapDispatchToProps = { dispatchRefreshTimeRange: refreshTimeRange };
|
||||
|
||||
const DatePicker = withRouter(
|
||||
connect(
|
||||
null,
|
||||
{ dispatchUpdateTimePicker: updateTimePicker }
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(DatePickerComponent)
|
||||
);
|
||||
|
||||
|
|
|
@ -5,144 +5,120 @@
|
|||
*/
|
||||
|
||||
import { mount, shallow } from 'enzyme';
|
||||
import { History, Location } from 'history';
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { MemoryRouter, RouteComponentProps } from 'react-router-dom';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { Store } from 'redux';
|
||||
// @ts-ignore
|
||||
import configureStore from 'x-pack/plugins/apm/public/store/config/configureStore';
|
||||
import { mockNow } from 'x-pack/plugins/apm/public/utils/testHelpers';
|
||||
import { DatePicker, DatePickerComponent } from '../DatePicker';
|
||||
|
||||
function mountPicker(search?: string) {
|
||||
const store = configureStore();
|
||||
let path = '/whatever';
|
||||
if (search) {
|
||||
path += `?${search}`;
|
||||
}
|
||||
const mounted = mount(
|
||||
function mountPicker(initialState = {}) {
|
||||
const store = configureStore(initialState);
|
||||
const wrapper = mount(
|
||||
<Provider store={store}>
|
||||
<MemoryRouter initialEntries={[path]}>
|
||||
<MemoryRouter>
|
||||
<DatePicker />
|
||||
</MemoryRouter>
|
||||
</Provider>
|
||||
);
|
||||
return { mounted, store };
|
||||
return { wrapper, store };
|
||||
}
|
||||
|
||||
describe('DatePicker', () => {
|
||||
describe('date calculations', () => {
|
||||
let restoreNow: () => void;
|
||||
|
||||
beforeAll(() => {
|
||||
restoreNow = mockNow('2019-02-15T12:00:00.000Z');
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
restoreNow();
|
||||
});
|
||||
|
||||
it('should initialize with APM default date range', () => {
|
||||
const { store } = mountPicker();
|
||||
expect(store.getState().urlParams).toEqual({
|
||||
start: '2019-02-14T12:00:00.000Z',
|
||||
end: '2019-02-15T12:00:00.000Z'
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse "last 15 minutes" from URL params', () => {
|
||||
const { store } = mountPicker('rangeFrom=now-15m&rangeTo=now');
|
||||
expect(store.getState().urlParams).toEqual({
|
||||
start: '2019-02-15T11:45:00.000Z',
|
||||
end: '2019-02-15T12:00:00.000Z'
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse "last 7 days" from URL params', () => {
|
||||
const { store } = mountPicker('rangeFrom=now-7d&rangeTo=now');
|
||||
expect(store.getState().urlParams).toEqual({
|
||||
start: '2019-02-08T12:00:00.000Z',
|
||||
end: '2019-02-15T12:00:00.000Z'
|
||||
});
|
||||
});
|
||||
|
||||
it('should parse absolute dates from URL params', () => {
|
||||
const { store } = mountPicker(
|
||||
`rangeFrom=2019-02-03T10:00:00.000Z&rangeTo=2019-02-10T16:30:00.000Z`
|
||||
);
|
||||
expect(store.getState().urlParams).toEqual({
|
||||
start: '2019-02-03T10:00:00.000Z',
|
||||
end: '2019-02-10T16:30:00.000Z'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('url updates', () => {
|
||||
function setupTest() {
|
||||
const location = { search: '?rangeFrom=now-15m&rangeTo=now' } as Location;
|
||||
const history = {
|
||||
push: jest.fn() as History['push'],
|
||||
replace: jest.fn() as History['replace']
|
||||
} as History;
|
||||
const routerProps = { location, history } as RouteComponentProps;
|
||||
const actionMock = jest.fn();
|
||||
const props = { ...routerProps, dispatchUpdateTimePicker: actionMock };
|
||||
const routerProps = {
|
||||
location: { search: '' },
|
||||
history: { push: jest.fn() }
|
||||
} as any;
|
||||
|
||||
const wrapper = shallow<DatePickerComponent>(
|
||||
<DatePickerComponent {...props} />
|
||||
<DatePickerComponent
|
||||
{...routerProps}
|
||||
dispatchUpdateTimePicker={jest.fn()}
|
||||
urlParams={{}}
|
||||
/>
|
||||
);
|
||||
|
||||
return { history, wrapper };
|
||||
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).toHaveBeenCalledTimes(1);
|
||||
expect(history.replace).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should replace the last entry in the stack if the URL is the same', () => {
|
||||
const { history, wrapper } = setupTest();
|
||||
|
||||
wrapper.instance().updateUrl({ rangeFrom: 'now-15m', rangeTo: 'now' });
|
||||
|
||||
expect(history.replace).toHaveBeenCalledTimes(1);
|
||||
expect(history.push).not.toHaveBeenCalled();
|
||||
expect(history.push).toHaveBeenCalledWith({
|
||||
search: 'rangeFrom=now-20m&rangeTo=now'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const tick = () => new Promise(resolve => setImmediate(resolve, 0));
|
||||
|
||||
describe('refresh cycle', () => {
|
||||
let nowSpy: jest.Mock;
|
||||
beforeEach(() => {
|
||||
nowSpy = mockNow('2010');
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
nowSpy.mockRestore();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('should refresh the store once per refresh interval', async () => {
|
||||
const { store } = mountPicker(
|
||||
'rangeFrom=now-15m&rangeTo=now&refreshPaused=false&refreshInterval=200'
|
||||
);
|
||||
const listener = jest.fn();
|
||||
store.subscribe(listener);
|
||||
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;
|
||||
|
||||
jest.advanceTimersByTime(200);
|
||||
await new Promise(resolve => setImmediate(resolve, 0));
|
||||
jest.advanceTimersByTime(200);
|
||||
await new Promise(resolve => setImmediate(resolve, 0));
|
||||
jest.advanceTimersByTime(200);
|
||||
await new Promise(resolve => setImmediate(resolve, 0));
|
||||
listener = jest.fn();
|
||||
store.subscribe(listener);
|
||||
|
||||
expect(listener).toHaveBeenCalledTimes(3);
|
||||
jest.advanceTimersByTime(200);
|
||||
await tick();
|
||||
jest.advanceTimersByTime(200);
|
||||
await tick();
|
||||
jest.advanceTimersByTime(200);
|
||||
await tick();
|
||||
});
|
||||
|
||||
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 not refresh when paused', () => {
|
||||
const { store } = mountPicker(
|
||||
'rangeFrom=now-15m&rangeTo=now&refreshPaused=true&refreshInterval=200'
|
||||
);
|
||||
const { store } = mountPicker({
|
||||
urlParams: {
|
||||
rangeFrom: 'now-15m',
|
||||
rangeTo: 'now',
|
||||
refreshPaused: true,
|
||||
refreshInterval: 200
|
||||
}
|
||||
});
|
||||
|
||||
const listener = jest.fn();
|
||||
store.subscribe(listener);
|
||||
jest.advanceTimersByTime(1100);
|
||||
|
@ -151,9 +127,14 @@ describe('DatePicker', () => {
|
|||
});
|
||||
|
||||
it('should be paused by default', () => {
|
||||
const { store } = mountPicker(
|
||||
'rangeFrom=now-15m&rangeTo=now&refreshInterval=200'
|
||||
);
|
||||
const { store } = mountPicker({
|
||||
urlParams: {
|
||||
rangeFrom: 'now-15m',
|
||||
rangeTo: 'now',
|
||||
refreshInterval: 200
|
||||
}
|
||||
});
|
||||
|
||||
const listener = jest.fn();
|
||||
store.subscribe(listener);
|
||||
jest.advanceTimersByTime(1100);
|
||||
|
@ -162,12 +143,18 @@ describe('DatePicker', () => {
|
|||
});
|
||||
|
||||
it('should not attempt refreshes after unmounting', () => {
|
||||
const { store, mounted } = mountPicker(
|
||||
'rangeFrom=now-15m&rangeTo=now&refreshPaused=false&refreshInterval=200'
|
||||
);
|
||||
const { store, wrapper } = mountPicker({
|
||||
urlParams: {
|
||||
rangeFrom: 'now-15m',
|
||||
rangeTo: 'now',
|
||||
refreshPaused: false,
|
||||
refreshInterval: 200
|
||||
}
|
||||
});
|
||||
|
||||
const listener = jest.fn();
|
||||
store.subscribe(listener);
|
||||
mounted.unmount();
|
||||
wrapper.unmount();
|
||||
jest.advanceTimersByTime(1100);
|
||||
|
||||
expect(listener).not.toHaveBeenCalled();
|
||||
|
|
|
@ -39,9 +39,17 @@ class KueryBarView extends Component {
|
|||
isLoadingSuggestions: false
|
||||
};
|
||||
|
||||
willUnmount = false;
|
||||
|
||||
componentWillUnmount() {
|
||||
this.willUnmount = true;
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
const indexPattern = await getAPMIndexPatternForKuery();
|
||||
this.setState({ indexPattern, isLoadingIndexPattern: false });
|
||||
if (!this.willUnmount) {
|
||||
this.setState({ indexPattern, isLoadingIndexPattern: false });
|
||||
}
|
||||
}
|
||||
|
||||
onChange = async (inputValue, selectionStart) => {
|
||||
|
|
|
@ -6,17 +6,9 @@
|
|||
|
||||
import { rootReducer } from '../rootReducer';
|
||||
|
||||
const ISO_DATE_PATTERN = /\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)/;
|
||||
|
||||
describe('root reducer', () => {
|
||||
it('should return the initial state', () => {
|
||||
const state = rootReducer(undefined, {});
|
||||
|
||||
expect(state.urlParams.start).toMatch(ISO_DATE_PATTERN);
|
||||
expect(state.urlParams.end).toMatch(ISO_DATE_PATTERN);
|
||||
|
||||
delete state.urlParams.start;
|
||||
delete state.urlParams.end;
|
||||
const state = rootReducer(undefined, {} as any);
|
||||
|
||||
expect(state).toEqual({
|
||||
location: { hash: '', pathname: '', search: '' },
|
|
@ -1,69 +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 { urlParamsReducer, updateTimePicker } from '../urlParams';
|
||||
import { LOCATION_UPDATE } from '../location';
|
||||
|
||||
describe('urlParams', () => {
|
||||
it('should handle LOCATION_UPDATE for transactions section', () => {
|
||||
const state = urlParamsReducer(
|
||||
{},
|
||||
{
|
||||
type: LOCATION_UPDATE,
|
||||
location: {
|
||||
pathname:
|
||||
'myServiceName/transactions/myTransactionType/myTransactionName/b/c',
|
||||
search: '?transactionId=myTransactionId&detailTab=request&spanId=10'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
expect(state).toEqual({
|
||||
page: 0,
|
||||
serviceName: 'myServiceName',
|
||||
spanId: 10,
|
||||
processorEvent: 'transaction',
|
||||
transactionId: 'myTransactionId',
|
||||
transactionName: 'myTransactionName',
|
||||
detailTab: 'request',
|
||||
transactionType: 'myTransactionType'
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle LOCATION_UPDATE for error section', () => {
|
||||
const state = urlParamsReducer(
|
||||
{},
|
||||
{
|
||||
type: LOCATION_UPDATE,
|
||||
location: {
|
||||
pathname: 'myServiceName/errors/myErrorGroupId',
|
||||
search: '?detailTab=request&transactionId=myTransactionId'
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
expect(state).toEqual(
|
||||
expect.objectContaining({
|
||||
serviceName: 'myServiceName',
|
||||
errorGroupId: 'myErrorGroupId',
|
||||
detailTab: 'request',
|
||||
transactionId: 'myTransactionId'
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle TIMEPICKER_UPDATE', () => {
|
||||
const state = urlParamsReducer(
|
||||
{},
|
||||
updateTimePicker({
|
||||
min: 'minTime',
|
||||
max: 'maxTime'
|
||||
})
|
||||
);
|
||||
|
||||
expect(state).toEqual({ end: 'maxTime', start: 'minTime' });
|
||||
});
|
||||
});
|
113
x-pack/plugins/apm/public/store/__jest__/urlParams.test.tsx
Normal file
113
x-pack/plugins/apm/public/store/__jest__/urlParams.test.tsx
Normal file
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* 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'
|
||||
});
|
||||
});
|
||||
});
|
|
@ -18,7 +18,7 @@ import { getDefaultDistributionSample } from './reactReduxRequest/transactionDis
|
|||
import { IReduxState } from './rootReducer';
|
||||
|
||||
// ACTION TYPES
|
||||
export const TIMEPICKER_UPDATE = 'TIMEPICKER_UPDATE';
|
||||
export const TIME_RANGE_REFRESH = 'TIME_RANGE_REFRESH';
|
||||
export const TIMEPICKER_DEFAULTS = {
|
||||
rangeFrom: 'now-24h',
|
||||
rangeTo: 'now',
|
||||
|
@ -26,34 +26,43 @@ export const TIMEPICKER_DEFAULTS = {
|
|||
refreshInterval: '0'
|
||||
};
|
||||
|
||||
function calculateTimePickerDefaults() {
|
||||
const parsed = {
|
||||
from: datemath.parse(TIMEPICKER_DEFAULTS.rangeFrom),
|
||||
// roundUp: true is required for the quick select relative date values to work properly
|
||||
to: datemath.parse(TIMEPICKER_DEFAULTS.rangeTo, { roundUp: true })
|
||||
};
|
||||
|
||||
const result: IUrlParams = {};
|
||||
if (parsed.from) {
|
||||
result.start = parsed.from.toISOString();
|
||||
}
|
||||
if (parsed.to) {
|
||||
result.end = parsed.to.toISOString();
|
||||
}
|
||||
return result;
|
||||
interface TimeRange {
|
||||
rangeFrom: string;
|
||||
rangeTo: string;
|
||||
}
|
||||
|
||||
const INITIAL_STATE: IUrlParams = calculateTimePickerDefaults();
|
||||
|
||||
interface LocationAction {
|
||||
type: typeof LOCATION_UPDATE;
|
||||
location: Location;
|
||||
}
|
||||
interface TimepickerAction {
|
||||
type: typeof TIMEPICKER_UPDATE;
|
||||
time: { min: string; max: string };
|
||||
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;
|
||||
}
|
||||
export type APMAction = LocationAction | TimepickerAction;
|
||||
|
||||
// "urlParams" contains path and query parameters from the url, that can be easily consumed from
|
||||
// any (container) component with access to the store
|
||||
|
@ -63,7 +72,10 @@ export type APMAction = LocationAction | TimepickerAction;
|
|||
// serviceName: opbeans-backend (path param)
|
||||
// transactionType: Brewing%20Bot (path param)
|
||||
// transactionId: 1321 (query param)
|
||||
export function urlParamsReducer(state = INITIAL_STATE, action: APMAction) {
|
||||
export function urlParamsReducer(
|
||||
state: IUrlParams = {},
|
||||
action: APMAction
|
||||
): IUrlParams {
|
||||
switch (action.type) {
|
||||
case LOCATION_UPDATE: {
|
||||
const {
|
||||
|
@ -84,12 +96,24 @@ export function urlParamsReducer(state = INITIAL_STATE, action: APMAction) {
|
|||
page,
|
||||
sortDirection,
|
||||
sortField,
|
||||
kuery
|
||||
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,
|
||||
|
@ -111,11 +135,11 @@ export function urlParamsReducer(state = INITIAL_STATE, action: APMAction) {
|
|||
});
|
||||
}
|
||||
|
||||
case TIMEPICKER_UPDATE:
|
||||
case TIME_RANGE_REFRESH:
|
||||
return {
|
||||
...state,
|
||||
start: action.time.min,
|
||||
end: action.time.max
|
||||
start: getParsedDate(action.time.rangeFrom),
|
||||
end: getParsedDate(action.time.rangeTo)
|
||||
};
|
||||
|
||||
default:
|
||||
|
@ -181,14 +205,9 @@ function getPathParams(pathname: string) {
|
|||
}
|
||||
}
|
||||
|
||||
interface TimeUpdate {
|
||||
min: string;
|
||||
max: string;
|
||||
}
|
||||
|
||||
// ACTION CREATORS
|
||||
export function updateTimePicker(time: TimeUpdate) {
|
||||
return { type: TIMEPICKER_UPDATE, time };
|
||||
export function refreshTimeRange(time: TimeRange): TimeRangeRefreshAction {
|
||||
return { type: TIME_RANGE_REFRESH, time };
|
||||
}
|
||||
|
||||
// Selectors
|
||||
|
@ -216,9 +235,13 @@ export interface IUrlParams {
|
|||
errorGroupId?: string;
|
||||
flyoutDetailTab?: string;
|
||||
kuery?: string;
|
||||
rangeFrom?: string;
|
||||
rangeTo?: string;
|
||||
refreshInterval?: number;
|
||||
refreshPaused?: boolean;
|
||||
serviceName?: string;
|
||||
sortField?: string;
|
||||
sortDirection?: string;
|
||||
sortField?: string;
|
||||
start?: string;
|
||||
traceId?: string;
|
||||
transactionId?: string;
|
||||
|
|
|
@ -116,11 +116,5 @@ export async function getRenderedHref(
|
|||
|
||||
export function mockNow(date: string) {
|
||||
const fakeNow = new Date(date).getTime();
|
||||
const realDateNow = global.Date.now.bind(global.Date);
|
||||
|
||||
global.Date.now = jest.fn(() => fakeNow);
|
||||
|
||||
return () => {
|
||||
global.Date.now = realDateNow;
|
||||
};
|
||||
return jest.spyOn(Date, 'now').mockReturnValue(fakeNow);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue