Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor use url state hook #112675

Merged
merged 6 commits into from
Sep 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { Location } from 'history';
import { UrlInputsModel } from '../../store/inputs/model';
import { TimelineUrl } from '../../../timelines/store/timeline/model';
import { CONSTANTS } from '../url_state/constants';
import { URL_STATE_KEYS, KeyUrlState, UrlState } from '../url_state/types';
import { KeyUrlState, UrlState, isAdministration, ALL_URL_STATE_KEYS } from '../url_state/types';
import {
replaceQueryStringInLocation,
replaceStateKeyInQueryString,
Expand All @@ -23,8 +23,8 @@ import { SearchNavTab } from './types';
import { SourcererScopePatterns } from '../../store/sourcerer/model';

export const getSearch = (tab: SearchNavTab, urlState: UrlState): string => {
if (tab && tab.urlKey != null && URL_STATE_KEYS[tab.urlKey] != null) {
return URL_STATE_KEYS[tab.urlKey].reduce<Location>(
if (tab && tab.urlKey != null && !isAdministration(tab.urlKey)) {
return ALL_URL_STATE_KEYS.reduce<Location>(
(myLocation: Location, urlKey: KeyUrlState) => {
let urlStateToReplace:
| Filter[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,27 @@

import { navTabs } from '../../../app/home/home_navigations';
import { getTitle } from './helpers';
import { HostsType } from '../../../hosts/store/model';

describe('Helpers Url_State', () => {
describe('getTitle', () => {
test('host page name', () => {
const result = getTitle('hosts', undefined, navTabs);
const result = getTitle('hosts', navTabs);
expect(result).toEqual('Hosts');
});
test('network page name', () => {
const result = getTitle('network', undefined, navTabs);
const result = getTitle('network', navTabs);
expect(result).toEqual('Network');
});
test('overview page name', () => {
const result = getTitle('overview', undefined, navTabs);
const result = getTitle('overview', navTabs);
expect(result).toEqual('Overview');
});
test('timelines page name', () => {
const result = getTitle('timelines', undefined, navTabs);
const result = getTitle('timelines', navTabs);
expect(result).toEqual('Timelines');
});
test('details page name', () => {
const result = getTitle('hosts', HostsType.details, navTabs);
expect(result).toEqual(HostsType.details);
});
test('Not existing', () => {
const result = getTitle('IamHereButNotReally', undefined, navTabs);
const result = getTitle('IamHereButNotReally', navTabs);
expect(result).toEqual('');
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { timelineSelectors } from '../../../timelines/store/timeline';
import { formatDate } from '../super_date_picker';
import { NavTab } from '../navigation/types';
import { CONSTANTS, UrlStateType } from './constants';
import { ReplaceStateInLocation, UpdateUrlStateString } from './types';
import { ReplaceStateInLocation, KeyUrlState, ValueUrlState } from './types';
import { sourcererSelectors } from '../../store/sourcerer';
import { SourcererScopeName, SourcererScopePatterns } from '../../store/sourcerer/model';

Expand Down Expand Up @@ -113,12 +113,7 @@ export const getUrlType = (pageName: string): UrlStateType => {
return 'overview';
};

export const getTitle = (
pageName: string,
detailName: string | undefined,
navTabs: Record<string, NavTab>
): string => {
if (detailName != null) return detailName;
export const getTitle = (pageName: string, navTabs: Record<string, NavTab>): string => {
return navTabs[pageName] != null ? navTabs[pageName].name : '';
};

Expand Down Expand Up @@ -189,13 +184,13 @@ export const makeMapStateToProps = () => {

export const updateTimerangeUrl = (
timeRange: UrlInputsModel,
isInitializing: boolean
isFirstPageLoad: boolean
): UrlInputsModel => {
if (timeRange.global.timerange.kind === 'relative') {
timeRange.global.timerange.from = formatDate(timeRange.global.timerange.fromStr);
timeRange.global.timerange.to = formatDate(timeRange.global.timerange.toStr, { roundUp: true });
}
if (timeRange.timeline.timerange.kind === 'relative' && isInitializing) {
if (timeRange.timeline.timerange.kind === 'relative' && isFirstPageLoad) {
timeRange.timeline.timerange.from = formatDate(timeRange.timeline.timerange.fromStr);
timeRange.timeline.timerange.to = formatDate(timeRange.timeline.timerange.toStr, {
roundUp: true,
Expand All @@ -204,90 +199,32 @@ export const updateTimerangeUrl = (
return timeRange;
};

export const updateUrlStateString = ({
isInitializing,
history,
newUrlStateString,
pathName,
search,
updateTimerange,
urlKey,
}: UpdateUrlStateString): string => {
if (urlKey === CONSTANTS.appQuery) {
const queryState = decodeRisonUrlState<Query>(newUrlStateString);
if (queryState != null && queryState.query === '') {
return replaceStateInLocation({
history,
pathName,
search,
urlStateToReplace: '',
urlStateKey: urlKey,
});
}
} else if (urlKey === CONSTANTS.timerange && updateTimerange) {
const queryState = decodeRisonUrlState<UrlInputsModel>(newUrlStateString);
if (queryState != null && queryState.global != null) {
return replaceStateInLocation({
history,
pathName,
search,
urlStateToReplace: updateTimerangeUrl(queryState, isInitializing),
urlStateKey: urlKey,
});
}
} else if (urlKey === CONSTANTS.sourcerer) {
const sourcererState = decodeRisonUrlState<SourcererScopePatterns>(newUrlStateString);
if (sourcererState != null && Object.keys(sourcererState).length > 0) {
return replaceStateInLocation({
history,
pathName,
search,
urlStateToReplace: sourcererState,
urlStateKey: urlKey,
});
}
} else if (urlKey === CONSTANTS.filters) {
const queryState = decodeRisonUrlState<Filter[]>(newUrlStateString);
if (isEmpty(queryState)) {
return replaceStateInLocation({
history,
pathName,
search,
urlStateToReplace: '',
urlStateKey: urlKey,
});
}
} else if (urlKey === CONSTANTS.timeline) {
const queryState = decodeRisonUrlState<TimelineUrl>(newUrlStateString);
if (queryState != null && queryState.id === '') {
return replaceStateInLocation({
history,
pathName,
search,
urlStateToReplace: '',
urlStateKey: urlKey,
});
}
}
return search;
};
export const isQueryStateEmpty = (queryState: ValueUrlState | null, urlKey: KeyUrlState) =>
queryState === null ||
(urlKey === CONSTANTS.appQuery && isEmpty((queryState as Query).query)) ||
(urlKey === CONSTANTS.filters && isEmpty(queryState)) ||
(urlKey === CONSTANTS.timeline && (queryState as TimelineUrl).id === '');

export const replaceStatesInLocation = (
states: ReplaceStateInLocation[],
pathname: string,
search: string,
history?: H.History
) => {
const location = {
hash: '',
pathname,
search,
state: '',
};

const queryString = getQueryStringFromLocation(search);
const newQueryString = states.reduce((updatedQueryString, { urlStateKey, urlStateToReplace }) => {
return replaceStateKeyInQueryString(urlStateKey, urlStateToReplace)(updatedQueryString);
}, queryString);

const newLocation = replaceQueryStringInLocation(location, newQueryString);

export const replaceStateInLocation = <T>({
history,
urlStateToReplace,
urlStateKey,
pathName,
search,
}: ReplaceStateInLocation<T>) => {
const newLocation = replaceQueryStringInLocation(
{
hash: '',
pathname: pathName,
search,
state: '',
},
replaceStateKeyInQueryString(urlStateKey, urlStateToReplace)(getQueryStringFromLocation(search))
);
if (history) {
newLocation.state = history.location.state;
history.replace(newLocation);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
mockSetAbsoluteRangeDatePicker,
mockSetRelativeRangeDatePicker,
testCases,
getMockProps,
} from './test_dependencies';
import { UrlStateContainerPropTypes } from './types';
import { useUrlStateHooks } from './use_url_state';
Expand Down Expand Up @@ -60,6 +61,14 @@ jest.mock('../../lib/kibana', () => ({
},
}));

jest.mock('react-redux', () => {
const original = jest.requireActual('react-redux');
return {
...original,
useDispatch: () => jest.fn(),
};
});

jest.mock('react-router-dom', () => {
const original = jest.requireActual('react-router-dom');

Expand Down Expand Up @@ -216,6 +225,56 @@ describe('UrlStateContainer', () => {

expect(mockHistory.replace).not.toHaveBeenCalled();
});

it('it removes empty AppQuery state from URL', () => {
mockProps = {
...getMockProps(
{
hash: '',
pathname: '/network',
search: "?query=(query:'')",
state: '',
},
CONSTANTS.networkPage,
null,
SecurityPageName.network,
undefined
),
};

(useLocation as jest.Mock).mockReturnValue({
pathname: mockProps.pathName,
});

mount(<HookWrapper hookProps={mockProps} hook={(args) => useUrlStateHooks(args)} />);

expect(mockHistory.replace.mock.calls[0][0].search).not.toContain('query=');
});

it('it removes empty timeline state from URL', () => {
mockProps = {
...getMockProps(
{
hash: '',
pathname: '/network',
search: "?timeline=(id:'',isOpen:!t)",
state: '',
},
CONSTANTS.networkPage,
null,
SecurityPageName.network,
undefined
),
};

(useLocation as jest.Mock).mockReturnValue({
pathname: mockProps.pathName,
});

mount(<HookWrapper hookProps={mockProps} hook={(args) => useUrlStateHooks(args)} />);

expect(mockHistory.replace.mock.calls[0][0].search).not.toContain('timeline=');
});
});

describe('After Initialization, keep Relative Date up to date for global only on alerts page', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,56 +6,39 @@
*/

import React from 'react';
import { compose, Dispatch } from 'redux';

import { connect } from 'react-redux';
import deepEqual from 'fast-deep-equal';

import { timelineActions } from '../../../timelines/store/timeline';
import { RouteSpyState } from '../../utils/route/types';
import { useRouteSpy } from '../../utils/route/use_route_spy';

import { UrlStateContainerPropTypes, UrlStateProps } from './types';
import { UrlStateContainerPropTypes, UrlStateProps, UrlStateStateToPropsType } from './types';
import { useUrlStateHooks } from './use_url_state';
import { dispatchUpdateTimeline } from '../../../timelines/components/open_timeline/helpers';
import { dispatchSetInitialStateFromUrl } from './initialize_redux_by_url';
import { makeMapStateToProps } from './helpers';

export const UrlStateContainer: React.FC<UrlStateContainerPropTypes> = (
props: UrlStateContainerPropTypes
) => {
useUrlStateHooks(props);
return null;
};

const mapDispatchToProps = (dispatch: Dispatch) => ({
setInitialStateFromUrl: dispatchSetInitialStateFromUrl(dispatch),
updateTimeline: dispatchUpdateTimeline(dispatch),
updateTimelineIsLoading: ({ id, isLoading }: { id: string; isLoading: boolean }) =>
dispatch(timelineActions.updateIsLoading({ id, isLoading })),
});

export const UrlStateRedux = compose<React.ComponentClass<UrlStateProps & RouteSpyState>>(
connect(makeMapStateToProps, mapDispatchToProps)
)(
React.memo(
UrlStateContainer,
(prevProps, nextProps) =>
prevProps.pathName === nextProps.pathName && deepEqual(prevProps.urlState, nextProps.urlState)
)
export const UseUrlStateMemo = React.memo(
function UrlState(props: UrlStateContainerPropTypes) {
useUrlStateHooks(props);
return null;
},
(prevProps, nextProps) =>
prevProps.pathName === nextProps.pathName &&
deepEqual(prevProps.urlState, nextProps.urlState) &&
deepEqual(prevProps.indexPattern, nextProps.indexPattern) &&
deepEqual(prevProps.navTabs, nextProps.navTabs)
);

const UseUrlStateComponent: React.FC<UrlStateProps> = (props) => {
export const UseUrlStateComponent: React.FC<UrlStateProps & UrlStateStateToPropsType> = (props) => {
const [routeProps] = useRouteSpy();
const urlStateReduxProps: RouteSpyState & UrlStateProps = {
...routeProps,
...props,
};
return <UrlStateRedux {...urlStateReduxProps} />;

return (
<UseUrlStateMemo
{...{
...routeProps,
...props,
}}
/>
);
};

export const UseUrlState = React.memo(
UseUrlStateComponent,
(prevProps, nextProps) =>
deepEqual(prevProps.indexPattern, nextProps.indexPattern) &&
deepEqual(prevProps.navTabs, nextProps.navTabs)
);
export const UseUrlState = connect(makeMapStateToProps)(UseUrlStateComponent);
Loading