Skip to content

Commit

Permalink
Merge branch 'main' into 140996-reactive-host-view-setting
Browse files Browse the repository at this point in the history
  • Loading branch information
kibanamachine authored Oct 4, 2022
2 parents 0ccc1b7 + 10884e6 commit c38d50b
Show file tree
Hide file tree
Showing 22 changed files with 296 additions and 130 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { EventsQueryTabBody, ALERTS_EVENTS_HISTOGRAM_ID } from './events_query_t
import { useGlobalFullScreen } from '../../containers/use_full_screen';
import * as tGridActions from '@kbn/timelines-plugin/public/store/t_grid/actions';
import { licenseService } from '../../hooks/use_license';
import { mockHistory } from '../../mock/router';

const mockGetDefaultControlColumn = jest.fn();
jest.mock('../../../timelines/components/timeline/body/control_columns', () => ({
Expand All @@ -39,6 +40,11 @@ jest.mock('../../lib/kibana', () => {
};
});

jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useHistory: () => mockHistory,
}));

const FakeStatefulEventsViewer = ({ additionalFilters }: { additionalFilters: JSX.Element }) => (
<div>
{additionalFilters}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ import { useLicense } from '../../hooks/use_license';
import { useUiSetting$ } from '../../lib/kibana';
import { defaultAlertsFilters } from '../events_viewer/external_alerts_filter';

import {
useGetInitialUrlParamValue,
useReplaceUrlParams,
} from '../../utils/global_query_string/helpers';

export const ALERTS_EVENTS_HISTOGRAM_ID = 'alertsOrEventsHistogramQuery';

type QueryTabBodyProps = UserQueryTabBodyProps | HostQueryTabBodyProps | NetworkQueryTabBodyProps;
Expand All @@ -55,6 +60,8 @@ export type EventsQueryTabBodyComponentProps = QueryTabBodyProps & {
timelineId: TimelineId;
};

const EXTERNAL_ALERTS_URL_PARAM = 'onlyExternalAlerts';

const EventsQueryTabBodyComponent: React.FC<EventsQueryTabBodyComponentProps> = ({
deleteQuery,
endDate,
Expand All @@ -70,14 +77,21 @@ const EventsQueryTabBodyComponent: React.FC<EventsQueryTabBodyComponentProps> =
const { globalFullScreen } = useGlobalFullScreen();
const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled');
const [defaultNumberFormat] = useUiSetting$<string>(DEFAULT_NUMBER_FORMAT);
const [showExternalAlerts, setShowExternalAlerts] = useState(false);
const isEnterprisePlus = useLicense().isEnterprise();
const ACTION_BUTTON_COUNT = isEnterprisePlus ? 5 : 4;
const leadingControlColumns = useMemo(
() => getDefaultControlColumn(ACTION_BUTTON_COUNT),
[ACTION_BUTTON_COUNT]
);

const showExternalAlertsInitialUrlState = useExternalAlertsInitialUrlState();

const [showExternalAlerts, setShowExternalAlerts] = useState(
showExternalAlertsInitialUrlState ?? false
);

useSyncExternalAlertsUrlState(showExternalAlerts);

const toggleExternalAlerts = useCallback(() => setShowExternalAlerts((s) => !s), []);
const getHistogramSubtitle = useMemo(
() => getSubtitleFunction(defaultNumberFormat, showExternalAlerts),
Expand Down Expand Up @@ -178,3 +192,43 @@ EventsQueryTabBodyComponent.displayName = 'EventsQueryTabBodyComponent';
export const EventsQueryTabBody = React.memo(EventsQueryTabBodyComponent);

EventsQueryTabBody.displayName = 'EventsQueryTabBody';

const useExternalAlertsInitialUrlState = () => {
const replaceUrlParams = useReplaceUrlParams();

const getInitialUrlParamValue = useGetInitialUrlParamValue<boolean>(EXTERNAL_ALERTS_URL_PARAM);

const { decodedParam: showExternalAlertsInitialUrlState } = useMemo(
() => getInitialUrlParamValue(),
[getInitialUrlParamValue]
);

useEffect(() => {
// Only called on component unmount
return () => {
replaceUrlParams([
{
key: EXTERNAL_ALERTS_URL_PARAM,
value: null,
},
]);
};
}, [replaceUrlParams]);

return showExternalAlertsInitialUrlState;
};

/**
* Update URL state when showExternalAlerts value changes
*/
const useSyncExternalAlertsUrlState = (showExternalAlerts: boolean) => {
const replaceUrlParams = useReplaceUrlParams();
useEffect(() => {
replaceUrlParams([
{
key: EXTERNAL_ALERTS_URL_PARAM,
value: showExternalAlerts ? 'true' : null,
},
]);
}, [showExternalAlerts, replaceUrlParams]);
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@
* 2.0.
*/

import { parse } from 'query-string';
import { decode, encode } from 'rison-node';
import type { ParsedQuery } from 'query-string';
import { parse, stringify } from 'query-string';
import { url } from '@kbn/kibana-utils-plugin/public';
import { useHistory } from 'react-router-dom';
import { useCallback } from 'react';
import { SecurityPageName } from '../../../app/types';

export const isDetectionsPages = (pageName: string) =>
Expand Down Expand Up @@ -40,3 +44,59 @@ export const getParamFromQueryString = (

return Array.isArray(queryParam) ? queryParam[0] : queryParam;
};

/**
*
* Gets the value of the URL param from the query string.
* It doesn't update when the URL changes.
*
*/
export const useGetInitialUrlParamValue = <State>(urlParamKey: string) => {
// window.location.search provides the most updated representation of the url search.
// It also guarantees that we don't overwrite URL param managed outside react-router.
const getInitialUrlParamValue = useCallback(() => {
const param = getParamFromQueryString(
getQueryStringFromLocation(window.location.search),
urlParamKey
);

const decodedParam = decodeRisonUrlState<State>(param ?? undefined);

return { param, decodedParam };
}, [urlParamKey]);

return getInitialUrlParamValue;
};

export const encodeQueryString = (urlParams: ParsedQuery<string>): string =>
stringify(url.encodeQuery(urlParams), { sort: false, encode: false });

export const useReplaceUrlParams = () => {
const history = useHistory();

const replaceUrlParams = useCallback(
(params: Array<{ key: string; value: string | null }>) => {
// window.location.search provides the most updated representation of the url search.
// It prevents unnecessary re-renders which useLocation would create because 'replaceUrlParams' does update the location.
// window.location.search also guarantees that we don't overwrite URL param managed outside react-router.
const search = window.location.search;
const urlParams = parse(search, { sort: false });

params.forEach(({ key, value }) => {
if (value == null || value === '') {
delete urlParams[key];
} else {
urlParams[key] = value;
}
});

const newSearch = encodeQueryString(urlParams);

if (getQueryStringFromLocation(search) !== newSearch) {
history.replace({ search: newSearch });
}
},
[history]
);
return replaceUrlParams;
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import React from 'react';
import { renderHook } from '@testing-library/react-hooks';
import { act, renderHook } from '@testing-library/react-hooks';
import {
useInitializeUrlParam,
useGlobalQueryString,
Expand Down Expand Up @@ -296,5 +296,33 @@ describe('global query string', () => {

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

it('deletes unregistered URL params', async () => {
const urlParamKey = 'testKey';
const value = '123';
window.location.search = `?${urlParamKey}=${value}`;
const globalUrlParam = {
[urlParamKey]: value,
};
const store = makeStore(globalUrlParam);

const { waitForNextUpdate } = renderHook(() => useSyncGlobalQueryString(), {
wrapper: ({ children }: { children: React.ReactElement }) => (
<TestProviders store={store}>{children}</TestProviders>
),
});

mockHistory.replace.mockClear();

act(() => {
store.dispatch(globalUrlParamActions.deregisterUrlParam({ key: urlParamKey }));
});

waitForNextUpdate();

expect(mockHistory.replace).toHaveBeenCalledWith({
search: ``,
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,15 @@
* 2.0.
*/

import type * as H from 'history';
import type { ParsedQuery } from 'query-string';
import { parse, stringify } from 'query-string';
import { useCallback, useEffect, useMemo } from 'react';

import { url } from '@kbn/kibana-utils-plugin/public';
import { isEmpty, pickBy } from 'lodash/fp';
import { useHistory } from 'react-router-dom';
import { difference, isEmpty, pickBy } from 'lodash/fp';
import { useDispatch } from 'react-redux';
import usePrevious from 'react-use/lib/usePrevious';
import {
decodeRisonUrlState,
encodeQueryString,
encodeRisonUrlState,
getParamFromQueryString,
getQueryStringFromLocation,
useGetInitialUrlParamValue,
useReplaceUrlParams,
} from './helpers';
import { useShallowEqualSelector } from '../../hooks/use_selector';
import { globalUrlParamActions, globalUrlParamSelectors } from '../../store/global_url_param';
Expand All @@ -43,13 +38,10 @@ export const useInitializeUrlParam = <State>(
) => {
const dispatch = useDispatch();

const getInitialUrlParamValue = useGetInitialUrlParamValue<State>(urlParamKey);

useEffect(() => {
// window.location.search provides the most updated representation of the url search.
// It also guarantees that we don't overwrite URL param managed outside react-router.
const initialValue = getParamFromQueryString(
getQueryStringFromLocation(window.location.search),
urlParamKey
);
const { param: initialValue, decodedParam: decodedInitialValue } = getInitialUrlParamValue();

dispatch(
globalUrlParamActions.registerUrlParam({
Expand All @@ -59,7 +51,7 @@ export const useInitializeUrlParam = <State>(
);

// execute consumer initialization
onInitialize(decodeRisonUrlState<State>(initialValue ?? undefined));
onInitialize(decodedInitialValue);

return () => {
dispatch(globalUrlParamActions.deregisterUrlParam({ key: urlParamKey }));
Expand Down Expand Up @@ -103,9 +95,16 @@ export const useGlobalQueryString = (): string => {
* - It updates the URL when globalUrlParam store updates.
*/
export const useSyncGlobalQueryString = () => {
const history = useHistory();
const [{ pageName }] = useRouteSpy();
const globalUrlParam = useShallowEqualSelector(globalUrlParamSelectors.selectGlobalUrlParam);
const previousGlobalUrlParams = usePrevious(globalUrlParam);
const replaceUrlParams = useReplaceUrlParams();

// Url params that got deleted from GlobalUrlParams
const unregisteredKeys = useMemo(
() => difference(Object.keys(previousGlobalUrlParams ?? {}), Object.keys(globalUrlParam)),
[previousGlobalUrlParams, globalUrlParam]
);

useEffect(() => {
const linkInfo = getLinkInfo(pageName) ?? { skipUrlState: true };
Expand All @@ -114,36 +113,16 @@ export const useSyncGlobalQueryString = () => {
value: linkInfo.skipUrlState ? null : value,
}));

if (params.length > 0) {
// window.location.search provides the most updated representation of the url search.
// It prevents unnecessary re-renders which useLocation would create because 'replaceUrlParams' does update the location.
// window.location.search also guarantees that we don't overwrite URL param managed outside react-router.
replaceUrlParams(params, history, window.location.search);
}
}, [globalUrlParam, pageName, history]);
};

const encodeQueryString = (urlParams: ParsedQuery<string>): string =>
stringify(url.encodeQuery(urlParams), { sort: false, encode: false });

const replaceUrlParams = (
params: Array<{ key: string; value: string | null }>,
history: H.History,
search: string
) => {
const urlParams = parse(search, { sort: false });
// Delete unregistered Url params
unregisteredKeys.forEach((key) => {
params.push({
key,
value: null,
});
});

params.forEach(({ key, value }) => {
if (value == null || value === '') {
delete urlParams[key];
} else {
urlParams[key] = value;
if (params.length > 0) {
replaceUrlParams(params);
}
});

const newSearch = encodeQueryString(urlParams);

if (getQueryStringFromLocation(search) !== newSearch) {
history.replace({ search: newSearch });
}
}, [globalUrlParam, pageName, unregisteredKeys, replaceUrlParams]);
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@
* 2.0.
*/

import type { IHttpFetchError } from '@kbn/core-http-browser';
import { createAction } from '@reduxjs/toolkit';
import { StatesIndexStatus } from '../../../../../common/runtime_types';
import { IHttpSerializedFetchError } from '../utils/http_error';

export const getIndexStatus = createAction<void>('[INDEX STATUS] GET');
export const getIndexStatusSuccess = createAction<StatesIndexStatus>('[INDEX STATUS] GET SUCCESS');
export const getIndexStatusFail =
createAction<IHttpSerializedFetchError>('[INDEX STATUS] GET FAIL');
export const getIndexStatusFail = createAction<IHttpFetchError>('[INDEX STATUS] GET FAIL');
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import { createReducer } from '@reduxjs/toolkit';
import { IHttpSerializedFetchError } from '../utils/http_error';
import { IHttpSerializedFetchError, serializeHttpFetchError } from '../utils/http_error';
import { StatesIndexStatus } from '../../../../../common/runtime_types';

import { getIndexStatus, getIndexStatusSuccess, getIndexStatusFail } from './actions';
Expand All @@ -33,7 +33,7 @@ export const indexStatusReducer = createReducer(initialState, (builder) => {
state.loading = false;
})
.addCase(getIndexStatusFail, (state, action) => {
state.error = action.payload;
state.error = serializeHttpFetchError(action.payload);
state.loading = false;
});
});
Expand Down
Loading

0 comments on commit c38d50b

Please sign in to comment.