[APM] Fix Datepicker double loading and move date parsing to urlParams (#33560)

This commit is contained in:
Søren Louv-Jansen 2019-03-22 15:56:57 +01:00 committed by GitHub
parent 721161f3d1
commit ed2874bf54
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 347 additions and 329 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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: '' },

View file

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

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

View file

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

View file

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