[Security Solution] Improve url state management (#134210)

* Implement global query string POC
This commit is contained in:
Pablo Machado 2022-06-27 12:02:59 +02:00 committed by GitHub
parent 44ba4bcecc
commit 819c7e5d8b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 979 additions and 212 deletions

View file

@ -121,6 +121,7 @@ export enum SecurityPageName {
kubernetes = 'kubernetes',
exploreLanding = 'explore',
dashboardsLanding = 'dashboards',
noPage = '',
}
export const EXPLORE_PATH = '/explore' as const;

View file

@ -184,7 +184,7 @@ describe('url state', () => {
cy.get(NETWORK).should(
'have.attr',
'href',
`/app/security/network?query=(language:kuery,query:'source.ip:%20%2210.142.0.9%22%20')&sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')))`
`/app/security/network?query=(language:kuery,query:'source.ip:%20%2210.142.0.9%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))`
);
});
@ -197,12 +197,12 @@ describe('url state', () => {
cy.get(HOSTS).should(
'have.attr',
'href',
`/app/security/hosts?query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')))`
`/app/security/hosts?query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))`
);
cy.get(NETWORK).should(
'have.attr',
'href',
`/app/security/network?query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')))`
`/app/security/network?query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))`
);
cy.get(HOSTS_NAMES).first().should('have.text', 'siem-kibana');
@ -213,21 +213,22 @@ describe('url state', () => {
cy.get(ANOMALIES_TAB).should(
'have.attr',
'href',
"/app/security/hosts/siem-kibana/anomalies?sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')))&query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')"
"/app/security/hosts/siem-kibana/anomalies?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')"
);
cy.get(BREADCRUMBS)
.eq(1)
.should(
'have.attr',
'href',
`/app/security/hosts?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')))`
`/app/security/hosts?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))`
);
cy.get(BREADCRUMBS)
.eq(2)
.should(
'have.attr',
'href',
`/app/security/hosts/siem-kibana?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')))`
`/app/security/hosts/siem-kibana?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))`
);
});

View file

@ -23,7 +23,7 @@ import { useUpgradeSecurityPackages } from '../../common/hooks/use_upgrade_secur
import { GlobalHeader } from './global_header';
import { SecuritySolutionTemplateWrapper } from './template_wrapper';
import { ConsoleManager } from '../../management/components/console/components/console_manager';
import { useSyncGlobalQueryString } from '../../common/utils/global_query_string';
interface HomePageProps {
children: React.ReactNode;
onAppLeave: (handler: AppLeaveHandler) => void;
@ -36,7 +36,7 @@ const HomePageComponent: React.FC<HomePageProps> = ({
setHeaderActionMenu,
}) => {
const { pathname } = useLocation();
useSyncGlobalQueryString();
useInitSourcerer(getScopeFromPath(pathname));
const { browserFields, indexPattern } = useSourcererDataView(getScopeFromPath(pathname));

View file

@ -16,7 +16,7 @@ import { AdministrationSubTab } from '../../../../management/types';
import { renderHook } from '@testing-library/react-hooks';
import { TestProviders } from '../../../mock';
import { GetSecuritySolutionUrl } from '../../link_to';
import { APP_UI_ID } from '../../../../../common/constants';
import { APP_UI_ID, SecurityPageName } from '../../../../../common/constants';
import { useDeepEqualSelector } from '../../../hooks/use_selector';
import { useIsGroupedNavigationEnabled } from '../helpers';
import { navTabs } from '../../../../app/home/home_navigations';
@ -55,7 +55,7 @@ const mockDefaultTab = (pageName: string): SiemRouteType | undefined => {
};
const getMockObject = (
pageName: string,
pageName: SecurityPageName,
pathName: string,
detailName: string | undefined
): RouteSpyState & ObjectWithNavTabs => ({
@ -100,7 +100,6 @@ const getMockObject = (
},
},
},
sourcerer: {},
},
};
});
@ -191,7 +190,7 @@ describe('Navigation Breadcrumbs', () => {
describe('getBreadcrumbsForRoute', () => {
test('should return Overview breadcrumbs when supplied overview pageName', () => {
const breadcrumbs = getBreadcrumbsForRoute(
getMockObject('overview', '/', undefined),
getMockObject(SecurityPageName.overview, '/', undefined),
getSecuritySolutionUrl,
false
);
@ -206,7 +205,7 @@ describe('Navigation Breadcrumbs', () => {
test('should return Host breadcrumbs when supplied hosts pageName', () => {
const breadcrumbs = getBreadcrumbsForRoute(
getMockObject('hosts', '/', undefined),
getMockObject(SecurityPageName.hosts, '/', undefined),
getSecuritySolutionUrl,
false
);
@ -222,7 +221,7 @@ describe('Navigation Breadcrumbs', () => {
test('should return Network breadcrumbs when supplied network pageName', () => {
const breadcrumbs = getBreadcrumbsForRoute(
getMockObject('network', '/', undefined),
getMockObject(SecurityPageName.network, '/', undefined),
getSecuritySolutionUrl,
false
);
@ -238,7 +237,7 @@ describe('Navigation Breadcrumbs', () => {
test('should return Timelines breadcrumbs when supplied timelines pageName', () => {
const breadcrumbs = getBreadcrumbsForRoute(
getMockObject('timelines', '/', undefined),
getMockObject(SecurityPageName.timelines, '/', undefined),
getSecuritySolutionUrl,
false
);
@ -253,7 +252,7 @@ describe('Navigation Breadcrumbs', () => {
test('should return Host Details breadcrumbs when supplied a pathname with hostName', () => {
const breadcrumbs = getBreadcrumbsForRoute(
getMockObject('hosts', '/', hostName),
getMockObject(SecurityPageName.hosts, '/', hostName),
getSecuritySolutionUrl,
false
);
@ -270,7 +269,7 @@ describe('Navigation Breadcrumbs', () => {
test('should return IP Details breadcrumbs when supplied pathname with ipv4', () => {
const breadcrumbs = getBreadcrumbsForRoute(
getMockObject('network', '/', ipv4),
getMockObject(SecurityPageName.network, '/', ipv4),
getSecuritySolutionUrl,
false
);
@ -287,7 +286,7 @@ describe('Navigation Breadcrumbs', () => {
test('should return IP Details breadcrumbs when supplied pathname with ipv6', () => {
const breadcrumbs = getBreadcrumbsForRoute(
getMockObject('network', '/', ipv6Encoded),
getMockObject(SecurityPageName.network, '/', ipv6Encoded),
getSecuritySolutionUrl,
false
);
@ -304,7 +303,7 @@ describe('Navigation Breadcrumbs', () => {
test('should return Alerts breadcrumbs when supplied alerts pageName', () => {
const breadcrumbs = getBreadcrumbsForRoute(
getMockObject('alerts', '/alerts', undefined),
getMockObject(SecurityPageName.alerts, '/alerts', undefined),
getSecuritySolutionUrl,
false
);
@ -319,7 +318,7 @@ describe('Navigation Breadcrumbs', () => {
test('should return Exceptions breadcrumbs when supplied exceptions pageName', () => {
const breadcrumbs = getBreadcrumbsForRoute(
getMockObject('exceptions', '/exceptions', undefined),
getMockObject(SecurityPageName.exceptions, '/exceptions', undefined),
getSecuritySolutionUrl,
false
);
@ -334,7 +333,7 @@ describe('Navigation Breadcrumbs', () => {
test('should return Rules breadcrumbs when supplied rules pageName', () => {
const breadcrumbs = getBreadcrumbsForRoute(
getMockObject('rules', '/rules', undefined),
getMockObject(SecurityPageName.rules, '/rules', undefined),
getSecuritySolutionUrl,
false
);
@ -349,7 +348,7 @@ describe('Navigation Breadcrumbs', () => {
test('should return Rules breadcrumbs when supplied rules Creation pageName', () => {
const breadcrumbs = getBreadcrumbsForRoute(
getMockObject('rules', '/rules/create', undefined),
getMockObject(SecurityPageName.rules, '/rules/create', undefined),
getSecuritySolutionUrl,
false
);
@ -368,7 +367,7 @@ describe('Navigation Breadcrumbs', () => {
const mockRuleName = 'ALERT_RULE_NAME';
const breadcrumbs = getBreadcrumbsForRoute(
{
...getMockObject('rules', `/rules/id/${mockDetailName}`, undefined),
...getMockObject(SecurityPageName.rules, `/rules/id/${mockDetailName}`, undefined),
detailName: mockDetailName,
state: {
ruleName: mockRuleName,
@ -392,7 +391,7 @@ describe('Navigation Breadcrumbs', () => {
const mockRuleName = 'ALERT_RULE_NAME';
const breadcrumbs = getBreadcrumbsForRoute(
{
...getMockObject('rules', `/rules/id/${mockDetailName}/edit`, undefined),
...getMockObject(SecurityPageName.rules, `/rules/id/${mockDetailName}/edit`, undefined),
detailName: mockDetailName,
state: {
ruleName: mockRuleName,
@ -417,7 +416,7 @@ describe('Navigation Breadcrumbs', () => {
test('should return null breadcrumbs when supplied Cases pageName', () => {
const breadcrumbs = getBreadcrumbsForRoute(
getMockObject('cases', '/', undefined),
getMockObject(SecurityPageName.case, '/', undefined),
getSecuritySolutionUrl,
false
);
@ -431,7 +430,7 @@ describe('Navigation Breadcrumbs', () => {
};
const breadcrumbs = getBreadcrumbsForRoute(
{
...getMockObject('cases', `/${sampleCase.id}`, sampleCase.id),
...getMockObject(SecurityPageName.case, `/${sampleCase.id}`, sampleCase.id),
state: { caseTitle: sampleCase.name },
},
getSecuritySolutionUrl,
@ -442,7 +441,7 @@ describe('Navigation Breadcrumbs', () => {
test('should return Admin breadcrumbs when supplied endpoints pageName', () => {
const breadcrumbs = getBreadcrumbsForRoute(
getMockObject('administration', '/endpoints', undefined),
getMockObject(SecurityPageName.administration, '/endpoints', undefined),
getSecuritySolutionUrl,
false
);
@ -461,22 +460,26 @@ describe('Navigation Breadcrumbs', () => {
test('should call chrome breadcrumb service with correct breadcrumbs', () => {
const navigateToUrlMock = jest.fn();
const { result } = renderHook(() => useSetBreadcrumbs(), { wrapper: TestProviders });
result.current(getMockObject('hosts', '/', hostName), chromeMock, navigateToUrlMock);
result.current(
getMockObject(SecurityPageName.hosts, '/', hostName),
chromeMock,
navigateToUrlMock
);
expect(setBreadcrumbsMock).toBeCalledWith([
expect.objectContaining({
text: 'Security',
href: "securitySolutionUI/get_started?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
href: "securitySolutionUI/get_started?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
onClick: expect.any(Function),
}),
expect.objectContaining({
text: 'Hosts',
href: "securitySolutionUI/hosts?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
href: "securitySolutionUI/hosts?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
onClick: expect.any(Function),
}),
expect.objectContaining({
text: 'siem-kibana',
href: "securitySolutionUI/hosts/siem-kibana?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
href: "securitySolutionUI/hosts/siem-kibana?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
onClick: expect.any(Function),
}),
{
@ -496,7 +499,7 @@ describe('Navigation Breadcrumbs', () => {
describe('getBreadcrumbsForRoute', () => {
test('should return Overview breadcrumbs when supplied overview pageName', () => {
const breadcrumbs = getBreadcrumbsForRoute(
getMockObject('overview', '/', undefined),
getMockObject(SecurityPageName.overview, '/', undefined),
getSecuritySolutionUrl,
true
);
@ -515,7 +518,7 @@ describe('Navigation Breadcrumbs', () => {
test('should return Host breadcrumbs when supplied hosts pageName', () => {
const breadcrumbs = getBreadcrumbsForRoute(
getMockObject('hosts', '/', undefined),
getMockObject(SecurityPageName.hosts, '/', undefined),
getSecuritySolutionUrl,
true
);
@ -532,7 +535,7 @@ describe('Navigation Breadcrumbs', () => {
test('should return Network breadcrumbs when supplied network pageName', () => {
const breadcrumbs = getBreadcrumbsForRoute(
getMockObject('network', '/', undefined),
getMockObject(SecurityPageName.network, '/', undefined),
getSecuritySolutionUrl,
true
);
@ -549,7 +552,7 @@ describe('Navigation Breadcrumbs', () => {
test('should return Timelines breadcrumbs when supplied timelines pageName', () => {
const breadcrumbs = getBreadcrumbsForRoute(
getMockObject('timelines', '/', undefined),
getMockObject(SecurityPageName.timelines, '/', undefined),
getSecuritySolutionUrl,
true
);
@ -564,7 +567,7 @@ describe('Navigation Breadcrumbs', () => {
test('should return Host Details breadcrumbs when supplied a pathname with hostName', () => {
const breadcrumbs = getBreadcrumbsForRoute(
getMockObject('hosts', '/', hostName),
getMockObject(SecurityPageName.hosts, '/', hostName),
getSecuritySolutionUrl,
true
);
@ -582,7 +585,7 @@ describe('Navigation Breadcrumbs', () => {
test('should return IP Details breadcrumbs when supplied pathname with ipv4', () => {
const breadcrumbs = getBreadcrumbsForRoute(
getMockObject('network', '/', ipv4),
getMockObject(SecurityPageName.network, '/', ipv4),
getSecuritySolutionUrl,
true
);
@ -600,7 +603,7 @@ describe('Navigation Breadcrumbs', () => {
test('should return IP Details breadcrumbs when supplied pathname with ipv6', () => {
const breadcrumbs = getBreadcrumbsForRoute(
getMockObject('network', '/', ipv6Encoded),
getMockObject(SecurityPageName.network, '/', ipv6Encoded),
getSecuritySolutionUrl,
true
);
@ -618,7 +621,7 @@ describe('Navigation Breadcrumbs', () => {
test('should return Alerts breadcrumbs when supplied alerts pageName', () => {
const breadcrumbs = getBreadcrumbsForRoute(
getMockObject('alerts', '/alerts', undefined),
getMockObject(SecurityPageName.alerts, '/alerts', undefined),
getSecuritySolutionUrl,
true
);
@ -633,7 +636,7 @@ describe('Navigation Breadcrumbs', () => {
test('should return Exceptions breadcrumbs when supplied exceptions pageName', () => {
const breadcrumbs = getBreadcrumbsForRoute(
getMockObject('exceptions', '/exceptions', undefined),
getMockObject(SecurityPageName.exceptions, '/exceptions', undefined),
getSecuritySolutionUrl,
true
);
@ -649,7 +652,7 @@ describe('Navigation Breadcrumbs', () => {
test('should return Rules breadcrumbs when supplied rules pageName', () => {
const breadcrumbs = getBreadcrumbsForRoute(
getMockObject('rules', '/rules', undefined),
getMockObject(SecurityPageName.rules, '/rules', undefined),
getSecuritySolutionUrl,
true
);
@ -665,7 +668,7 @@ describe('Navigation Breadcrumbs', () => {
test('should return Rules breadcrumbs when supplied rules Creation pageName', () => {
const breadcrumbs = getBreadcrumbsForRoute(
getMockObject('rules', '/rules/create', undefined),
getMockObject(SecurityPageName.rules, '/rules/create', undefined),
getSecuritySolutionUrl,
true
);
@ -685,7 +688,7 @@ describe('Navigation Breadcrumbs', () => {
const mockRuleName = 'ALERT_RULE_NAME';
const breadcrumbs = getBreadcrumbsForRoute(
{
...getMockObject('rules', `/rules/id/${mockDetailName}`, undefined),
...getMockObject(SecurityPageName.rules, `/rules/id/${mockDetailName}`, undefined),
detailName: mockDetailName,
state: {
ruleName: mockRuleName,
@ -710,7 +713,7 @@ describe('Navigation Breadcrumbs', () => {
const mockRuleName = 'ALERT_RULE_NAME';
const breadcrumbs = getBreadcrumbsForRoute(
{
...getMockObject('rules', `/rules/id/${mockDetailName}/edit`, undefined),
...getMockObject(SecurityPageName.rules, `/rules/id/${mockDetailName}/edit`, undefined),
detailName: mockDetailName,
state: {
ruleName: mockRuleName,
@ -736,7 +739,7 @@ describe('Navigation Breadcrumbs', () => {
test('should return null breadcrumbs when supplied Cases pageName', () => {
const breadcrumbs = getBreadcrumbsForRoute(
getMockObject('cases', '/', undefined),
getMockObject(SecurityPageName.case, '/', undefined),
getSecuritySolutionUrl,
true
);
@ -750,7 +753,7 @@ describe('Navigation Breadcrumbs', () => {
};
const breadcrumbs = getBreadcrumbsForRoute(
{
...getMockObject('cases', `/${sampleCase.id}`, sampleCase.id),
...getMockObject(SecurityPageName.case, `/${sampleCase.id}`, sampleCase.id),
state: { caseTitle: sampleCase.name },
},
getSecuritySolutionUrl,
@ -761,7 +764,7 @@ describe('Navigation Breadcrumbs', () => {
test('should return Admin breadcrumbs when supplied endpoints pageName', () => {
const breadcrumbs = getBreadcrumbsForRoute(
getMockObject('administration', '/endpoints', undefined),
getMockObject(SecurityPageName.administration, '/endpoints', undefined),
getSecuritySolutionUrl,
true
);
@ -781,9 +784,13 @@ describe('Navigation Breadcrumbs', () => {
test('should call chrome breadcrumb service with correct breadcrumbs', () => {
const navigateToUrlMock = jest.fn();
const { result } = renderHook(() => useSetBreadcrumbs(), { wrapper: TestProviders });
result.current(getMockObject('hosts', '/', hostName), chromeMock, navigateToUrlMock);
result.current(
getMockObject(SecurityPageName.hosts, '/', hostName),
chromeMock,
navigateToUrlMock
);
const searchString =
"?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))";
"?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))";
expect(setBreadcrumbsMock).toBeCalledWith([
expect.objectContaining({

View file

@ -84,7 +84,7 @@ export const getBreadcrumbsForRoute = (
}
const newMenuLeadingBreadcrumbs = getLeadingBreadcrumbsForSecurityPage(
spyState.pageName as SecurityPageName,
spyState.pageName,
getSecuritySolutionUrl,
object.navTabs,
isGroupedNavigationEnabled

View file

@ -20,28 +20,31 @@ import {
} from '../url_state/helpers';
import { SearchNavTab } from './types';
import { SourcererUrlState } from '../../store/sourcerer/model';
import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features';
import { useUiSetting$ } from '../../lib/kibana';
import { ENABLE_GROUPED_NAVIGATION } from '../../../../common/constants';
export const getSearch = (tab: SearchNavTab, urlState: UrlState): string => {
export const getSearch = (
tab: SearchNavTab,
urlState: UrlState,
globalQueryString: string
): string => {
if (tab && tab.urlKey != null && !isAdministration(tab.urlKey)) {
return getUrlStateSearch(urlState);
// TODO: Temporary code while we are migrating all query strings to global_query_string_manager
if (globalQueryString.length > 0) {
return `${getUrlStateSearch(urlState)}&${globalQueryString}`;
} else {
return getUrlStateSearch(urlState);
}
}
return '';
};
export const getUrlStateSearch = (urlState: UrlState): string =>
ALL_URL_STATE_KEYS.reduce<Location>(
(myLocation: Location, urlKey: KeyUrlState) => {
let urlStateToReplace:
| Filter[]
| Query
| SourcererUrlState
| TimelineUrl
| UrlInputsModel
| string = '';
let urlStateToReplace: Filter[] | Query | TimelineUrl | UrlInputsModel | string = '';
if (urlKey === CONSTANTS.appQuery && urlState.query != null) {
if (urlState.query.query === '') {
@ -57,8 +60,6 @@ export const getUrlStateSearch = (urlState: UrlState): string =>
}
} else if (urlKey === CONSTANTS.timerange) {
urlStateToReplace = urlState[CONSTANTS.timerange];
} else if (urlKey === CONSTANTS.sourcerer) {
urlStateToReplace = urlState[CONSTANTS.sourcerer];
} else if (urlKey === CONSTANTS.timeline && urlState[CONSTANTS.timeline] != null) {
const timeline = urlState[CONSTANTS.timeline];
if (timeline.id === '') {

View file

@ -15,6 +15,7 @@ import { HostsTableType } from '../../../hosts/store/model';
import { RouteSpyState } from '../../utils/route/types';
import { TabNavigationComponentProps, SecuritySolutionTabNavigationProps } from './types';
import { TimelineTabs } from '../../../../common/types/timeline';
import { SecurityPageName } from '../../../app/types';
jest.mock('react-router-dom', () => {
const original = jest.requireActual('react-router-dom');
@ -61,7 +62,7 @@ describe('SIEM Navigation', () => {
const mockProps: TabNavigationComponentProps &
SecuritySolutionTabNavigationProps &
RouteSpyState = {
pageName: 'hosts',
pageName: SecurityPageName.hosts,
pathName: '/',
detailName: undefined,
search: '',
@ -92,7 +93,6 @@ describe('SIEM Navigation', () => {
},
[CONSTANTS.appQuery]: { query: '', language: 'kuery' },
[CONSTANTS.filters]: [],
[CONSTANTS.sourcerer]: {},
[CONSTANTS.timeline]: {
activeTab: TimelineTabs.query,
id: '',

View file

@ -84,7 +84,6 @@ export const TabNavigationComponent: React.FC<
navTabs={navTabs}
pageName={pageName}
pathName={pathName}
sourcerer={urlState.sourcerer}
savedQuery={urlState.savedQuery}
tabName={tabName}
timeline={urlState.timeline}

View file

@ -130,7 +130,7 @@ const useSideNavItems = () => {
const useSelectedId = (): SecurityPageName => {
const [{ pageName }] = useRouteSpy();
const selectedId = useMemo(() => {
const [rootLinkInfo] = getAncestorLinksInfo(pageName as SecurityPageName);
const [rootLinkInfo] = getAncestorLinksInfo(pageName);
return rootLinkInfo?.id ?? '';
}, [pageName]);

View file

@ -15,6 +15,7 @@ import { RouteSpyState } from '../../../utils/route/types';
import { CONSTANTS } from '../../url_state/constants';
import { TabNavigationComponent } from '.';
import { TabNavigationProps } from './types';
import { SecurityPageName } from '../../../../app/types';
jest.mock('../../link_to');
jest.mock('../../../lib/kibana/kibana_react', () => {
@ -54,7 +55,7 @@ describe('Table Navigation', () => {
const mockRiskyHostEnabled = true;
const mockProps: TabNavigationProps & RouteSpyState = {
pageName: 'hosts',
pageName: SecurityPageName.hosts,
pathName: '/hosts',
detailName: hostName,
search: '',
@ -89,7 +90,6 @@ describe('Table Navigation', () => {
},
[CONSTANTS.appQuery]: { query: 'host.name:"siem-es"', language: 'kuery' },
[CONSTANTS.filters]: [],
[CONSTANTS.sourcerer]: {},
[CONSTANTS.timeline]: {
activeTab: TimelineTabs.query,
id: '',

View file

@ -8,7 +8,6 @@
import type { Filter, Query } from '@kbn/es-query';
import { UrlInputsModel } from '../../../store/inputs/model';
import { CONSTANTS } from '../../url_state/constants';
import { SourcererUrlState } from '../../../store/sourcerer/model';
import { TimelineUrl } from '../../../../timelines/store/timeline/model';
import { SecuritySolutionTabNavigationProps } from '../types';
@ -21,7 +20,6 @@ export interface TabNavigationProps extends SecuritySolutionTabNavigationProps {
[CONSTANTS.appQuery]?: Query;
[CONSTANTS.filters]?: Filter[];
[CONSTANTS.savedQuery]?: string;
[CONSTANTS.sourcerer]: SourcererUrlState;
[CONSTANTS.timerange]: UrlInputsModel;
[CONSTANTS.timeline]: TimelineUrl;
}

View file

@ -8,6 +8,7 @@
import { useCallback, useMemo } from 'react';
import { useDeepEqualSelector } from '../../hooks/use_selector';
import { useGlobalQueryString } from '../../utils/global_query_string';
import { makeMapStateToProps } from '../url_state/helpers';
import { getSearch, getUrlStateSearch } from './helpers';
import { SearchNavTab } from './types';
@ -15,13 +16,27 @@ import { SearchNavTab } from './types';
export const useGetUrlSearch = (tab?: SearchNavTab) => {
const mapState = makeMapStateToProps();
const { urlState } = useDeepEqualSelector(mapState);
const urlSearch = useMemo(() => (tab ? getSearch(tab, urlState) : ''), [tab, urlState]);
const globalQueryString = useGlobalQueryString();
const urlSearch = useMemo(
() => (tab ? getSearch(tab, urlState, globalQueryString) : ''),
[tab, urlState, globalQueryString]
);
return urlSearch;
};
export const useGetUrlStateQueryString = () => {
const mapState = makeMapStateToProps();
const { urlState } = useDeepEqualSelector(mapState);
const getUrlStateQueryString = useCallback(() => getUrlStateSearch(urlState), [urlState]);
const globalQueryString = useGlobalQueryString();
const getUrlStateQueryString = useCallback(() => {
// TODO: Temporary code while we are migrating all query strings to global_query_string_manager
if (globalQueryString.length > 0) {
return `${getUrlStateSearch(urlState)}&${globalQueryString}`;
}
return getUrlStateSearch(urlState);
}, [urlState, globalQueryString]);
return getUrlStateQueryString;
};

View file

@ -8,10 +8,10 @@ Object {
"id": "main",
"items": Array [
Object {
"data-href": "securitySolutionUI/get_started?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
"data-href": "securitySolutionUI/get_started?query=(language:kuery,query:'host.name:%22security-solution-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
"data-test-subj": "navigation-get_started",
"disabled": false,
"href": "securitySolutionUI/get_started?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
"href": "securitySolutionUI/get_started?query=(language:kuery,query:'host.name:%22security-solution-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
"id": "get_started",
"isSelected": false,
"name": "Get started",
@ -24,20 +24,20 @@ Object {
"id": "dashboards",
"items": Array [
Object {
"data-href": "securitySolutionUI/overview?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
"data-href": "securitySolutionUI/overview?query=(language:kuery,query:'host.name:%22security-solution-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
"data-test-subj": "navigation-overview",
"disabled": false,
"href": "securitySolutionUI/overview?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
"href": "securitySolutionUI/overview?query=(language:kuery,query:'host.name:%22security-solution-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
"id": "overview",
"isSelected": false,
"name": "Overview",
"onClick": [Function],
},
Object {
"data-href": "securitySolutionUI/detection_response?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
"data-href": "securitySolutionUI/detection_response?query=(language:kuery,query:'host.name:%22security-solution-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
"data-test-subj": "navigation-detection_response",
"disabled": false,
"href": "securitySolutionUI/detection_response?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
"href": "securitySolutionUI/detection_response?query=(language:kuery,query:'host.name:%22security-solution-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
"id": "detection_response",
"isSelected": false,
"name": "Detection & Response",
@ -50,30 +50,30 @@ Object {
"id": "detect",
"items": Array [
Object {
"data-href": "securitySolutionUI/alerts?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
"data-href": "securitySolutionUI/alerts?query=(language:kuery,query:'host.name:%22security-solution-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
"data-test-subj": "navigation-alerts",
"disabled": false,
"href": "securitySolutionUI/alerts?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
"href": "securitySolutionUI/alerts?query=(language:kuery,query:'host.name:%22security-solution-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
"id": "alerts",
"isSelected": false,
"name": "Alerts",
"onClick": [Function],
},
Object {
"data-href": "securitySolutionUI/rules?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
"data-href": "securitySolutionUI/rules?query=(language:kuery,query:'host.name:%22security-solution-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
"data-test-subj": "navigation-rules",
"disabled": false,
"href": "securitySolutionUI/rules?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
"href": "securitySolutionUI/rules?query=(language:kuery,query:'host.name:%22security-solution-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
"id": "rules",
"isSelected": false,
"name": "Rules",
"onClick": [Function],
},
Object {
"data-href": "securitySolutionUI/exceptions?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
"data-href": "securitySolutionUI/exceptions?query=(language:kuery,query:'host.name:%22security-solution-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
"data-test-subj": "navigation-exceptions",
"disabled": false,
"href": "securitySolutionUI/exceptions?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
"href": "securitySolutionUI/exceptions?query=(language:kuery,query:'host.name:%22security-solution-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
"id": "exceptions",
"isSelected": false,
"name": "Exception lists",
@ -86,30 +86,30 @@ Object {
"id": "explore",
"items": Array [
Object {
"data-href": "securitySolutionUI/hosts?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
"data-href": "securitySolutionUI/hosts?query=(language:kuery,query:'host.name:%22security-solution-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
"data-test-subj": "navigation-hosts",
"disabled": false,
"href": "securitySolutionUI/hosts?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
"href": "securitySolutionUI/hosts?query=(language:kuery,query:'host.name:%22security-solution-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
"id": "hosts",
"isSelected": true,
"name": "Hosts",
"onClick": [Function],
},
Object {
"data-href": "securitySolutionUI/network?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
"data-href": "securitySolutionUI/network?query=(language:kuery,query:'host.name:%22security-solution-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
"data-test-subj": "navigation-network",
"disabled": false,
"href": "securitySolutionUI/network?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
"href": "securitySolutionUI/network?query=(language:kuery,query:'host.name:%22security-solution-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
"id": "network",
"isSelected": false,
"name": "Network",
"onClick": [Function],
},
Object {
"data-href": "securitySolutionUI/users?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
"data-href": "securitySolutionUI/users?query=(language:kuery,query:'host.name:%22security-solution-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
"data-test-subj": "navigation-users",
"disabled": false,
"href": "securitySolutionUI/users?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
"href": "securitySolutionUI/users?query=(language:kuery,query:'host.name:%22security-solution-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
"id": "users",
"isSelected": false,
"name": "Users",
@ -122,10 +122,10 @@ Object {
"id": "investigate",
"items": Array [
Object {
"data-href": "securitySolutionUI/timelines?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
"data-href": "securitySolutionUI/timelines?query=(language:kuery,query:'host.name:%22security-solution-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
"data-test-subj": "navigation-timelines",
"disabled": false,
"href": "securitySolutionUI/timelines?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
"href": "securitySolutionUI/timelines?query=(language:kuery,query:'host.name:%22security-solution-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
"id": "timelines",
"isSelected": false,
"name": "Timelines",

View file

@ -32,7 +32,6 @@ describe('useSecuritySolutionNavigation', () => {
const mockUrlState = {
[CONSTANTS.appQuery]: { query: 'host.name:"security-solution-es"', language: 'kuery' },
[CONSTANTS.savedQuery]: '',
[CONSTANTS.sourcerer]: {},
[CONSTANTS.timeline]: {
activeTab: TimelineTabs.query,
id: '',
@ -165,10 +164,10 @@ describe('useSecuritySolutionNavigation', () => {
);
expect(caseNavItem).toMatchInlineSnapshot(`
Object {
"data-href": "securitySolutionUI/cases?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
"data-href": "securitySolutionUI/cases?query=(language:kuery,query:'host.name:%22security-solution-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
"data-test-subj": "navigation-cases",
"disabled": false,
"href": "securitySolutionUI/cases?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
"href": "securitySolutionUI/cases?query=(language:kuery,query:'host.name:%22security-solution-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
"id": "cases",
"isSelected": false,
"name": "Cases",

View file

@ -76,7 +76,6 @@ export const useSecuritySolutionNavigation = () => {
filters: urlState.filters,
navTabs: enabledNavTabs,
pageName,
sourcerer: urlState.sourcerer,
savedQuery: urlState.savedQuery,
tabName,
timeline: urlState.timeline,

View file

@ -18,6 +18,7 @@ import { NavTab, SecurityNavGroupKey } from '../types';
import { SecurityPageName } from '../../../../../common/constants';
import { useCanSeeHostIsolationExceptionsMenu } from '../../../../management/pages/host_isolation_exceptions/view/hooks';
import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental_features';
import { useGlobalQueryString } from '../../../utils/global_query_string';
export const usePrimaryNavigationItems = ({
navTabs,
@ -25,11 +26,13 @@ export const usePrimaryNavigationItems = ({
...urlStateProps
}: PrimaryNavigationItemsProps): Array<EuiSideNavItemType<{}>> => {
const { navigateTo, getAppUrl } = useNavigation();
const globalQueryString = useGlobalQueryString();
const getSideNav = useCallback(
(tab: NavTab) => {
const { id, name, disabled } = tab;
const isSelected = selectedTabId === id;
const urlSearch = getSearch(tab, urlStateProps);
const urlSearch = getSearch(tab, urlStateProps, globalQueryString);
const handleClick = (ev: React.MouseEvent) => {
ev.preventDefault();
@ -49,7 +52,7 @@ export const usePrimaryNavigationItems = ({
onClick: handleClick,
};
},
[getAppUrl, navigateTo, selectedTabId, urlStateProps]
[getAppUrl, navigateTo, selectedTabId, urlStateProps, globalQueryString]
);
const navItemsToDisplay = usePrimaryNavigationItemsToDisplay(navTabs);

View file

@ -24,7 +24,6 @@ export const usePrimaryNavigation = ({
navTabs,
pageName,
savedQuery,
sourcerer,
tabName,
timeline,
timerange,
@ -53,7 +52,6 @@ export const usePrimaryNavigation = ({
filters,
query,
savedQuery,
sourcerer,
timeline,
timerange,
});

View file

@ -54,6 +54,16 @@ jest.mock('@kbn/kibana-react-plugin/public', () => {
};
});
const mockUpdateUrlParam = jest.fn();
jest.mock('../../utils/global_query_string', () => {
const original = jest.requireActual('../../utils/global_query_string');
return {
...original,
useUpdateUrlParam: () => mockUpdateUrlParam,
};
});
const mockOptions = DEFAULT_INDEX_PATTERN.map((index) => ({ label: index, value: index }));
const defaultProps = {
@ -411,6 +421,50 @@ describe('Sourcerer component', () => {
})
);
});
it('onSave updates the URL param', () => {
store = createStore(
{
...mockGlobalState,
sourcerer: {
...mockGlobalState.sourcerer,
kibanaDataViews: [
mockGlobalState.sourcerer.defaultDataView,
{
...mockGlobalState.sourcerer.defaultDataView,
id: '1234',
title: 'filebeat-*',
patternList: ['filebeat-*'],
},
],
sourcererScopes: {
...mockGlobalState.sourcerer.sourcererScopes,
[SourcererScopeName.default]: {
...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default],
selectedDataViewId: id,
selectedPatterns: patternListNoSignals.slice(0, 2),
},
},
},
},
SUB_PLUGINS_REDUCER,
kibanaObservable,
storage
);
const wrapper = mount(
<TestProviders store={store}>
<Sourcerer {...defaultProps} />
</TestProviders>
);
wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click');
wrapper.find(`[data-test-subj="comboBoxInput"]`).first().simulate('click');
wrapper.find(`[data-test-subj="sourcerer-combo-option"]`).first().simulate('click');
wrapper.find(`[data-test-subj="sourcerer-save"]`).first().simulate('click');
expect(mockUpdateUrlParam).toHaveBeenCalledTimes(1);
});
it('resets to default index pattern', async () => {
const wrapper = mount(
<TestProviders store={store}>

View file

@ -20,7 +20,7 @@ import { useDispatch } from 'react-redux';
import * as i18n from './translations';
import { sourcererActions, sourcererModel, sourcererSelectors } from '../../store/sourcerer';
import { useDeepEqualSelector } from '../../hooks/use_selector';
import { SourcererScopeName } from '../../store/sourcerer/model';
import { SourcererScopeName, SourcererUrlState } from '../../store/sourcerer/model';
import { usePickIndexPatterns } from './use_pick_index_patterns';
import { FormRow, PopoverContent, StyledButton, StyledFormRow } from './helpers';
import { TemporarySourcerer } from './temporary';
@ -29,6 +29,8 @@ import { useUpdateDataView } from './use_update_data_view';
import { Trigger } from './trigger';
import { AlertsCheckbox, SaveButtons, SourcererCallout } from './sub_components';
import { useSignalHelpers } from '../../containers/sourcerer/use_signal_helpers';
import { useUpdateUrlParam } from '../../utils/global_query_string';
import { CONSTANTS } from '../url_state/constants';
export interface SourcererComponentProps {
scope: sourcererModel.SourcererScopeName;
@ -38,6 +40,8 @@ export const Sourcerer = React.memo<SourcererComponentProps>(({ scope: scopeId }
const dispatch = useDispatch();
const isDetectionsSourcerer = scopeId === SourcererScopeName.detections;
const isTimelineSourcerer = scopeId === SourcererScopeName.timeline;
const isDefaultSourcerer = scopeId === SourcererScopeName.default;
const updateUrlParam = useUpdateUrlParam<SourcererUrlState>(CONSTANTS.sourcerer);
const sourcererScopeSelector = useMemo(() => sourcererSelectors.getSourcererScopeSelector(), []);
const {
@ -144,8 +148,17 @@ export const Sourcerer = React.memo<SourcererComponentProps>(({ scope: scopeId }
shouldValidateSelectedPatterns,
})
);
if (isDefaultSourcerer) {
updateUrlParam({
[SourcererScopeName.default]: {
id: newSelectedDataView,
selectedPatterns: newSelectedPatterns,
},
});
}
},
[dispatch, scopeId]
[dispatch, scopeId, isDefaultSourcerer, updateUrlParam]
);
const onChangeDataView = useCallback(

View file

@ -8,6 +8,7 @@
import { navTabs } from '../../../app/home/home_navigations';
import { getTitle, isQueryStateEmpty } from './helpers';
import { CONSTANTS } from './constants';
import { ValueUrlState } from './types';
describe('Helpers Url_State', () => {
describe('getTitle', () => {
@ -45,7 +46,7 @@ describe('Helpers Url_State', () => {
});
test('returns true if url key is "query" and queryState is empty string', () => {
const result = isQueryStateEmpty({}, CONSTANTS.appQuery);
const result = isQueryStateEmpty('', CONSTANTS.appQuery);
expect(result).toBeTruthy();
});
@ -72,7 +73,7 @@ describe('Helpers Url_State', () => {
// TODO: Is this a bug, or intended?
test('returns false if url key is "timeline" and queryState is empty', () => {
const result = isQueryStateEmpty({}, CONSTANTS.timeline);
const result = isQueryStateEmpty({} as ValueUrlState, CONSTANTS.timeline);
expect(result).toBeFalsy();
});

View file

@ -24,8 +24,6 @@ import { formatDate } from '../super_date_picker';
import { NavTab } from '../navigation/types';
import { CONSTANTS, UrlStateType } from './constants';
import { ReplaceStateInLocation, KeyUrlState, ValueUrlState } from './types';
import { sourcererSelectors } from '../../store/sourcerer';
import { SourcererScopeName, SourcererUrlState } from '../../store/sourcerer/model';
export const isDetectionsPages = (pageName: string) =>
pageName === SecurityPageName.alerts ||
@ -49,7 +47,10 @@ export const encodeRisonUrlState = (state: any) => encode(state);
export const getQueryStringFromLocation = (search: string) => search.substring(1);
export const getParamFromQueryString = (queryString: string, key: string) => {
export const getParamFromQueryString = (
queryString: string,
key: string
): string | undefined | null => {
const parsedQueryString = parse(queryString, { sort: false });
const queryParam = parsedQueryString[key];
@ -128,7 +129,6 @@ export const makeMapStateToProps = () => {
const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector();
const getGlobalSavedQuerySelector = inputsSelectors.globalSavedQuerySelector();
const getTimeline = timelineSelectors.getTimelineByIdSelector();
const getSourcererScopes = sourcererSelectors.scopesSelector();
const mapStateToProps = (state: State) => {
const inputState = getInputsSelector(state);
const { linkTo: globalLinkTo, timerange: globalTimerange } = inputState.global;
@ -159,25 +159,10 @@ export const makeMapStateToProps = () => {
[CONSTANTS.savedQuery]: savedQuery.id,
};
}
const sourcerer = getSourcererScopes(state);
const activeScopes: SourcererScopeName[] = Object.keys(sourcerer) as SourcererScopeName[];
const selectedPatterns: SourcererUrlState = activeScopes
.filter((scope) => scope === SourcererScopeName.default)
.reduce(
(acc, scope) => ({
...acc,
[scope]: {
id: sourcerer[scope]?.selectedDataViewId,
selectedPatterns: sourcerer[scope]?.selectedPatterns,
},
}),
{}
);
return {
urlState: {
...searchAttr,
[CONSTANTS.sourcerer]: selectedPatterns,
[CONSTANTS.timerange]: {
global: {
[CONSTANTS.timerange]: globalTimerange,

View file

@ -125,6 +125,7 @@ describe('UrlStateContainer', () => {
(useLocation as jest.Mock).mockReturnValue({
pathname: mockProps.pathName,
search: mockProps.search,
});
mount(<HookWrapper hookProps={mockProps} hook={(args) => useUrlStateHooks(args)} />);
@ -159,6 +160,7 @@ describe('UrlStateContainer', () => {
(useLocation as jest.Mock).mockReturnValue({
pathname: mockProps.pathName,
search: mockProps.search,
});
mount(<HookWrapper hookProps={mockProps} hook={(args) => useUrlStateHooks(args)} />);
@ -189,6 +191,7 @@ describe('UrlStateContainer', () => {
(useLocation as jest.Mock).mockReturnValue({
pathname: mockProps.pathName,
search: mockProps.search,
});
mount(<HookWrapper hookProps={mockProps} hook={(args) => useUrlStateHooks(args)} />);
@ -218,6 +221,7 @@ describe('UrlStateContainer', () => {
(useLocation as jest.Mock).mockReturnValue({
pathname: mockProps.pathName,
search: mockProps.search,
});
mount(<HookWrapper hookProps={mockProps} hook={(args) => useUrlStateHooks(args)} />);
@ -227,7 +231,7 @@ describe('UrlStateContainer', () => {
).toEqual({
hash: '',
pathname: examplePath,
search: `?query=(language:kuery,query:'host.name:%22siem-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))`,
search: `?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))`,
state: '',
});
}
@ -246,6 +250,7 @@ describe('UrlStateContainer', () => {
(useLocation as jest.Mock).mockReturnValue({
pathname: 'out of sync path',
search: mockProps.search,
});
mount(<HookWrapper hookProps={mockProps} hook={(args) => useUrlStateHooks(args)} />);
@ -265,6 +270,7 @@ describe('UrlStateContainer', () => {
(useLocation as jest.Mock).mockReturnValue({
pathname: mockProps.pathName,
search: mockProps.search,
});
mount(<HookWrapper hookProps={mockProps} hook={(args) => useUrlStateHooks(args)} />);
@ -284,6 +290,7 @@ describe('UrlStateContainer', () => {
(useLocation as jest.Mock).mockReturnValue({
pathname: mockProps.pathName,
search: mockProps.search,
});
mount(<HookWrapper hookProps={mockProps} hook={(args) => useUrlStateHooks(args)} />);
@ -309,6 +316,7 @@ describe('UrlStateContainer', () => {
(useLocation as jest.Mock).mockReturnValue({
pathname: mockProps.pathName,
search: mockProps.search,
});
mount(<HookWrapper hookProps={mockProps} hook={(args) => useUrlStateHooks(args)} />);
@ -334,6 +342,7 @@ describe('UrlStateContainer', () => {
(useLocation as jest.Mock).mockReturnValue({
pathname: mockProps.pathName,
search: mockProps.search,
});
mount(<HookWrapper hookProps={mockProps} hook={(args) => useUrlStateHooks(args)} />);
@ -356,6 +365,7 @@ describe('UrlStateContainer', () => {
(useLocation as jest.Mock).mockReturnValue({
pathname: mockProps.pathName,
search: mockProps.search,
});
const wrapper = mount(

View file

@ -88,6 +88,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () =>
(useLocation as jest.Mock).mockReturnValue({
pathname: mockProps.pathName,
search: mockProps.search,
});
const wrapper = mount(
@ -126,7 +127,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () =>
hash: '',
pathname: '/network',
search:
"?query=(language:kuery,query:'host.name:%22siem-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
"?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))",
state: '',
});
});
@ -142,6 +143,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () =>
(useLocation as jest.Mock).mockReturnValue({
pathname: mockProps.pathName,
search: mockProps.search,
});
const wrapper = mount(
@ -160,7 +162,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () =>
hash: '',
pathname: '/network',
search:
"?query=(language:kuery,query:'host.name:%22siem-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
"?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
state: '',
});
});
@ -176,6 +178,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () =>
(useLocation as jest.Mock).mockReturnValue({
pathname: mockProps.pathName,
search: mockProps.search,
});
const wrapper = mount(
@ -195,42 +198,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () =>
hash: '',
pathname: '/network',
search:
"?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))&timeline=(id:hello_timeline_id,isOpen:!t)",
state: '',
});
});
test('sourcerer redux state updates the url', () => {
mockProps = getMockPropsObj({
page: CONSTANTS.networkPage,
examplePath: '/network',
namespaceLower: 'network',
pageName: SecurityPageName.network,
detailName: undefined,
}).noSearch.undefinedQuery;
(useLocation as jest.Mock).mockReturnValue({
pathname: mockProps.pathName,
});
const wrapper = mount(
<HookWrapper hookProps={mockProps} hook={(args) => useUrlStateHooks(args)} />
);
const newUrlState = {
...mockProps.urlState,
sourcerer: ['cool', 'patterns'],
};
wrapper.setProps({
hookProps: { ...mockProps, urlState: newUrlState, isInitializing: false },
});
wrapper.update();
expect(mockHistory.replace.mock.calls[1][0]).toStrictEqual({
hash: '',
pathname: '/network',
search:
"?sourcerer=!(cool,patterns)&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
"?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))&timeline=(id:hello_timeline_id,isOpen:!t)",
state: '',
});
});
@ -286,6 +254,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () =>
(useLocation as jest.Mock).mockReturnValue({
pathname: mockProps.pathName,
search: mockProps.search,
});
const wrapper = mount(
@ -297,6 +266,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () =>
(useLocation as jest.Mock).mockReturnValue({
pathname: updatedMockProps.pathName,
search: mockProps.search,
});
wrapper.setProps({
@ -307,7 +277,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () =>
expect(mockHistory.replace.mock.calls[1][0]).toStrictEqual({
hash: '',
pathname: MANAGEMENT_PATH,
search: '?',
search: mockProps.search,
state: '',
});
});
@ -363,6 +333,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () =>
(useLocation as jest.Mock).mockReturnValue({
pathname: mockProps.pathName,
search: mockProps.search,
});
const wrapper = mount(
@ -374,6 +345,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () =>
(useLocation as jest.Mock).mockReturnValue({
pathname: updatedMockProps.pathName,
search: mockProps.search,
});
wrapper.setProps({
@ -384,7 +356,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () =>
expect(mockHistory.replace.mock.calls[1][0]).toStrictEqual({
hash: '',
pathname: DASHBOARDS_PATH,
search: '?',
search: mockProps.search,
state: '',
});
});
@ -401,6 +373,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () =>
(useLocation as jest.Mock).mockReturnValue({
pathname: mockProps.pathName,
search: mockProps.search,
});
mount(<HookWrapper hookProps={mockProps} hook={(args) => useUrlStateHooks(args)} />);
@ -409,7 +382,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () =>
hash: '',
pathname: examplePath,
search:
"?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
"?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))",
state: '',
});
}
@ -433,6 +406,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () =>
(useLocation as jest.Mock).mockReturnValue({
pathname: mockProps.pathName,
search: mockProps.search,
});
const wrapper = mount(
@ -440,11 +414,12 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () =>
);
expect(mockHistory.replace.mock.calls[0][0].search).toEqual(
"?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))"
"?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))"
);
(useLocation as jest.Mock).mockReturnValue({
pathname: updatedProps.pathName,
search: mockProps.search,
});
wrapper.setProps({ hookProps: updatedProps });
@ -452,7 +427,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () =>
wrapper.update();
expect(mockHistory.replace.mock.calls[1][0].search).toEqual(
"?query=(language:kuery,query:'host.name:%22siem-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))"
"?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))"
);
});
@ -478,6 +453,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () =>
(useLocation as jest.Mock).mockReturnValue({
pathname: mockProps.pathName,
search: mockProps.search,
});
const wrapper = mount(
@ -485,11 +461,12 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () =>
);
expect(mockHistory.replace.mock.calls[0][0].search).toEqual(
"?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))"
"?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))"
);
(useLocation as jest.Mock).mockReturnValue({
pathname: updatedMockProps.pathName,
search: mockProps.search,
});
wrapper.setProps({ hookProps: updatedMockProps });

View file

@ -11,7 +11,7 @@ import { Dispatch } from 'redux';
import { useCallback, useMemo } from 'react';
import { useDispatch } from 'react-redux';
import type { Filter, Query } from '@kbn/es-query';
import { inputsActions, sourcererActions } from '../../store/actions';
import { inputsActions } from '../../store/actions';
import { InputsModelId, TimeRangeKinds } from '../../store/inputs/constants';
import {
UrlInputsModel,
@ -21,14 +21,13 @@ import {
} from '../../store/inputs/model';
import { TimelineUrl } from '../../../timelines/store/timeline/model';
import { CONSTANTS } from './constants';
import { decodeRisonUrlState, isDetectionsPages } from './helpers';
import { decodeRisonUrlState } from './helpers';
import { normalizeTimeRange } from './normalize_time_range';
import { SetInitialStateFromUrl } from './types';
import {
queryTimelineById,
dispatchUpdateTimeline,
} from '../../../timelines/components/open_timeline/helpers';
import { SourcererScopeName, SourcererUrlState } from '../../store/sourcerer/model';
import { timelineActions } from '../../../timelines/store/timeline';
export const useSetInitialStateFromUrl = () => {
@ -54,23 +53,6 @@ export const useSetInitialStateFromUrl = () => {
if (urlKey === CONSTANTS.timerange) {
updateTimerange(newUrlStateString, dispatch);
}
if (urlKey === CONSTANTS.sourcerer) {
const sourcererState = decodeRisonUrlState<SourcererUrlState>(newUrlStateString);
if (sourcererState != null) {
const activeScopes: SourcererScopeName[] = Object.keys(sourcererState).filter(
(key) => !(key === SourcererScopeName.default && isDetectionsPages(pageName))
) as SourcererScopeName[];
activeScopes.forEach((scope) =>
dispatch(
sourcererActions.setSelectedDataView({
id: scope,
selectedDataViewId: sourcererState[scope]?.id ?? null,
selectedPatterns: sourcererState[scope]?.selectedPatterns ?? [],
})
)
);
}
}
if (urlKey === CONSTANTS.appQuery && indexPattern != null) {
const appQuery = decodeRisonUrlState<Query>(newUrlStateString);

View file

@ -120,7 +120,6 @@ export const defaultProps: UrlStateContainerPropTypes = {
id: '',
isOpen: false,
},
[CONSTANTS.sourcerer]: {},
},
history: {
...mockHistory,
@ -132,7 +131,7 @@ export const getMockProps = (
location = defaultLocation,
kqlQueryKey = CONSTANTS.networkPage,
kqlQueryValue: Query | null,
pageName: string,
pageName: SecurityPageName,
detailName: string | undefined
): UrlStateContainerPropTypes => ({
...defaultProps,
@ -154,7 +153,7 @@ interface GetMockPropsObj {
examplePath: string;
namespaceLower: string;
page: LocationTypes;
pageName: string;
pageName: SecurityPageName;
detailName: string | undefined;
}
@ -270,7 +269,7 @@ export const getMockPropsObj = ({ page, examplePath, pageName, detailName }: Get
// silly that this needs to be an array and not an object
// https://jestjs.io/docs/en/api#testeachtable-name-fn-timeout
export const testCases: Array<
[LocationTypes, string, string, string, string | null, string, undefined | string]
[LocationTypes, string, string, string, string | null, SecurityPageName, undefined | string]
> = [
[
/* page */ CONSTANTS.networkPage,

View file

@ -13,13 +13,11 @@ import { RouteSpyState } from '../../utils/route/types';
import { SecurityNav } from '../navigation/types';
import { CONSTANTS, UrlStateType } from './constants';
import { SourcererUrlState } from '../../store/sourcerer/model';
export const ALL_URL_STATE_KEYS: KeyUrlState[] = [
CONSTANTS.appQuery,
CONSTANTS.filters,
CONSTANTS.savedQuery,
CONSTANTS.sourcerer,
CONSTANTS.timerange,
CONSTANTS.timeline,
];
@ -43,7 +41,6 @@ export interface UrlState {
[CONSTANTS.appQuery]?: Query;
[CONSTANTS.filters]?: Filter[];
[CONSTANTS.savedQuery]?: string;
[CONSTANTS.sourcerer]: SourcererUrlState;
[CONSTANTS.timerange]: UrlInputsModel;
[CONSTANTS.timeline]: TimelineUrl;
}

View file

@ -41,7 +41,6 @@ import { TimelineUrl } from '../../../timelines/store/timeline/model';
import { UrlInputsModel } from '../../store/inputs/model';
import { queryTimelineByIdOnUrlChange } from './query_timeline_by_id_on_url_change';
import { getLinkInfo } from '../../links';
import { SecurityPageName } from '../../../app/types';
import { useIsGroupedNavigationEnabled } from '../navigation/helpers';
function usePrevious(value: PreviousLocationUrlState) {
@ -57,17 +56,16 @@ export const useUrlStateHooks = ({
navTabs,
pageName,
urlState,
search,
pathName,
history,
}: UrlStateContainerPropTypes) => {
const [isFirstPageLoad, setIsFirstPageLoad] = useState(true);
const { filterManager, savedQueries } = useKibana().services.data.query;
const { pathname: browserPathName } = useLocation();
const { pathname: browserPathName, search } = useLocation();
const prevProps = usePrevious({ pathName, pageName, urlState, search });
const isGroupedNavEnabled = useIsGroupedNavigationEnabled();
const linkInfo = pageName ? getLinkInfo(pageName as SecurityPageName) : undefined;
const linkInfo = pageName ? getLinkInfo(pageName) : undefined;
const { setInitialStateFromUrl, updateTimeline, updateTimelineIsLoading } =
useSetInitialStateFromUrl();

View file

@ -13,7 +13,11 @@ import { Provider } from 'react-redux';
import { getScopeFromPath, useInitSourcerer, useSourcererDataView } from '.';
import { mockPatterns } from './mocks';
import { RouteSpyState } from '../../utils/route/types';
import { DEFAULT_INDEX_PATTERN, SecurityPageName } from '../../../../common/constants';
import {
DEFAULT_DATA_VIEW_ID,
DEFAULT_INDEX_PATTERN,
SecurityPageName,
} from '../../../../common/constants';
import { createStore } from '../../store';
import {
useUserInfo,
@ -25,9 +29,12 @@ import {
mockGlobalState,
SUB_PLUGINS_REDUCER,
mockSourcererState,
TestProviders,
} from '../../mock';
import { SelectedDataView, SourcererScopeName } from '../../store/sourcerer/model';
import { postSourcererDataView } from './api';
import { sourcererActions } from '../../store/sourcerer';
import { useInitializeUrlParam, useUpdateUrlParam } from '../../utils/global_query_string';
const mockRouteSpy: RouteSpyState = {
pageName: SecurityPageName.overview,
@ -40,6 +47,7 @@ const mockDispatch = jest.fn();
const mockUseUserInfo = useUserInfo as jest.Mock;
jest.mock('../../../detections/components/user_info');
jest.mock('./api');
jest.mock('../../utils/global_query_string');
jest.mock('react-redux', () => {
const original = jest.requireActual('react-redux');
@ -52,6 +60,8 @@ jest.mock('../../utils/route/use_route_spy', () => ({
useRouteSpy: () => [mockRouteSpy],
}));
(useInitializeUrlParam as jest.Mock).mockImplementation((_, onInitialize) => onInitialize({}));
const mockSearch = jest.fn();
const mockAddWarning = jest.fn();
@ -188,6 +198,50 @@ describe('Sourcerer Hooks', () => {
});
});
it('initilizes dataview with data from query string', async () => {
const selectedPatterns = ['testPattern-*'];
const selectedDataViewId = 'security-solution-default';
(useInitializeUrlParam as jest.Mock).mockImplementation((_, onInitialize) =>
onInitialize({
[SourcererScopeName.default]: {
id: selectedDataViewId,
selectedPatterns,
},
})
);
renderHook<string, void>(() => useInitSourcerer(), {
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
});
expect(mockDispatch).toHaveBeenCalledWith(
sourcererActions.setSelectedDataView({
id: SourcererScopeName.default,
selectedDataViewId,
selectedPatterns,
})
);
});
it('sets default selected patterns to the URL when there is no sorcerer URL param in the query string', async () => {
const updateUrlParam = jest.fn();
(useUpdateUrlParam as jest.Mock).mockReturnValue(updateUrlParam);
(useInitializeUrlParam as jest.Mock).mockImplementation((_, onInitialize) =>
onInitialize(null)
);
renderHook<string, void>(() => useInitSourcerer(), {
wrapper: ({ children }) => <TestProviders>{children}</TestProviders>,
});
expect(updateUrlParam).toHaveBeenCalledWith({
[SourcererScopeName.default]: {
id: DEFAULT_DATA_VIEW_ID,
selectedPatterns: DEFAULT_INDEX_PATTERN,
},
});
});
it('calls addWarning if defaultDataView has an error', async () => {
store = createStore(
{

View file

@ -14,17 +14,18 @@ import {
SelectedDataView,
SourcererDataView,
SourcererScopeName,
SourcererUrlState,
} from '../../store/sourcerer/model';
import { useUserInfo } from '../../../detections/components/user_info';
import { timelineSelectors } from '../../../timelines/store/timeline';
import {
ALERTS_PATH,
CASES_PATH,
HOSTS_PATH,
USERS_PATH,
NETWORK_PATH,
OVERVIEW_PATH,
RULES_PATH,
CASES_PATH,
} from '../../../../common/constants';
import { TimelineId } from '../../../../common/types';
import { useDeepEqualSelector } from '../../hooks/use_selector';
@ -37,6 +38,8 @@ import { useAppToasts } from '../../hooks/use_app_toasts';
import { postSourcererDataView } from './api';
import { useDataView } from '../source/use_data_view';
import { useFetchIndex } from '../source';
import { useInitializeUrlParam, useUpdateUrlParam } from '../../utils/global_query_string';
import { CONSTANTS } from '../../components/url_state/constants';
export const useInitSourcerer = (
scopeId: SourcererScopeName.default | SourcererScopeName.detections = SourcererScopeName.default
@ -46,6 +49,7 @@ export const useInitSourcerer = (
const initialTimelineSourcerer = useRef(true);
const initialDetectionSourcerer = useRef(true);
const { loading: loadingSignalIndex, isSignalIndexExists, signalIndexName } = useUserInfo();
const updateUrlParam = useUpdateUrlParam<SourcererUrlState>(CONSTANTS.sourcerer);
const getDataViewsSelector = useMemo(
() => sourcererSelectors.getSourcererDataViewsSelector(),
@ -88,6 +92,41 @@ export const useInitSourcerer = (
} = useDeepEqualSelector((state) => scopeIdSelector(state, SourcererScopeName.timeline));
const { indexFieldsSearch } = useDataView();
const onInitializeUrlParam = useCallback(
(initialState: SourcererUrlState | null) => {
// Initialize the store with value from UrlParam.
if (initialState != null) {
(Object.keys(initialState) as SourcererScopeName[]).forEach((scope) => {
if (
!(scope === SourcererScopeName.default && scopeId === SourcererScopeName.detections)
) {
dispatch(
sourcererActions.setSelectedDataView({
id: scope,
selectedDataViewId: initialState[scope]?.id ?? null,
selectedPatterns: initialState[scope]?.selectedPatterns ?? [],
})
);
}
});
} else {
// Initialize the UrlParam with values from the store.
// It isn't strictly necessary but I am keeping it for compatibility with the previous implementation.
if (scopeDataViewId) {
updateUrlParam({
[SourcererScopeName.default]: {
id: scopeDataViewId,
selectedPatterns,
},
});
}
}
},
[dispatch, scopeDataViewId, scopeId, selectedPatterns, updateUrlParam]
);
useInitializeUrlParam<SourcererUrlState>(CONSTANTS.sourcerer, onInitializeUrlParam);
/*
* Note for future engineer:
* we changed the logic to not fetch all the index fields for every data view on the loading of the app

View file

@ -388,6 +388,7 @@ export const mockGlobalState: State = {
},
},
},
globalUrlParam: {},
/**
* These state's are wrapped in `Immutable`, but for compatibility with the overall app architecture,
* they are cast to mutable versions here.

View file

@ -0,0 +1,20 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import actionCreatorFactory from 'typescript-fsa';
const actionCreator = actionCreatorFactory('x-pack/security_solution/local/global_url_param');
export const registerUrlParam = actionCreator<{ key: string; initialValue: string | null }>(
'REGISTER_URL_PARAM'
);
export const deregisterUrlParam = actionCreator<{ key: string }>('DEREGISTER_URL_PARAM');
export const updateUrlParam = actionCreator<{ key: string; value: string | null }>(
'UPDATE_URL_PARAM'
);

View 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
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import * as globalUrlParamActions from './actions';
import * as globalUrlParamSelectors from './selectors';
export { globalUrlParamActions, globalUrlParamSelectors };
export * from './reducer';

View file

@ -0,0 +1,89 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { deregisterUrlParam, registerUrlParam, updateUrlParam } from './actions';
import { globalUrlParamReducer, initialGlobalUrlParam } from './reducer';
const error = jest.spyOn(console, 'error').mockImplementation(() => {});
describe('globalUrlParamReducer', () => {
describe('#registerUrlParam', () => {
it('registers the URL param', () => {
const key = 'testKey';
const initialValue = 'testValue';
const state = globalUrlParamReducer(
initialGlobalUrlParam,
registerUrlParam({ key, initialValue })
);
expect(state).toEqual({ [key]: initialValue });
});
it('throws exception when a key is register twice', () => {
const key = 'testKey';
const initialValue = 'testValue';
const newState = globalUrlParamReducer(
initialGlobalUrlParam,
registerUrlParam({ key, initialValue })
);
globalUrlParamReducer(newState, registerUrlParam({ key, initialValue }));
expect(error).toHaveBeenCalledWith("Url param key 'testKey' is already being used.");
});
});
describe('#deregisterUrlParam', () => {
it('deregisters the URL param', () => {
const key = 'testKey';
const initialValue = 'testValue';
let state = globalUrlParamReducer(
initialGlobalUrlParam,
registerUrlParam({ key, initialValue })
);
expect(state).toEqual({ [key]: initialValue });
state = globalUrlParamReducer(initialGlobalUrlParam, deregisterUrlParam({ key }));
expect(state).toEqual({});
});
});
describe('#updateUrlParam', () => {
it('updates URL param', () => {
const key = 'testKey';
const value = 'new test value';
const state = globalUrlParamReducer(
{ [key]: 'old test value' },
updateUrlParam({ key, value })
);
expect(state).toEqual({ [key]: value });
});
it("doesn't update the URL param if key isn't registered", () => {
const key = 'testKey';
const value = 'testValue';
const state = globalUrlParamReducer(initialGlobalUrlParam, updateUrlParam({ key, value }));
expect(state).toEqual(initialGlobalUrlParam);
});
it("doesn't update the state if new value is equal to store value", () => {
const key = 'testKey';
const value = 'testValue';
const intialState = { [key]: value };
const state = globalUrlParamReducer(intialState, updateUrlParam({ key, value }));
expect(state).toBe(intialState);
});
});
});

View file

@ -0,0 +1,47 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { reducerWithInitialState } from 'typescript-fsa-reducers';
import { registerUrlParam, updateUrlParam, deregisterUrlParam } from './actions';
export type GlobalUrlParam = Record<string, string | null>;
export const initialGlobalUrlParam: GlobalUrlParam = {};
export const globalUrlParamReducer = reducerWithInitialState(initialGlobalUrlParam)
.case(registerUrlParam, (state, { key, initialValue }) => {
// It doesn't allow the query param to be used twice
if (state[key] !== undefined) {
// eslint-disable-next-line no-console
console.error(`Url param key '${key}' is already being used.`);
return state;
}
return {
...state,
[key]: initialValue,
};
})
.case(deregisterUrlParam, (state, { key }) => {
const nextState = { ...state };
delete nextState[key];
return nextState;
})
.case(updateUrlParam, (state, { key, value }) => {
// Only update the URL after the query param is registered and if the current value is different than the previous value
if (state[key] === undefined || state[key] === value) {
return state;
}
return {
...state,
[key]: value,
};
})
.build();

View file

@ -0,0 +1,11 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { GlobalUrlParam } from '.';
import { State } from '../types';
export const selectGlobalUrlParam = (state: State): GlobalUrlParam => state.globalUrlParam;

View file

@ -24,6 +24,7 @@ import { AppAction } from './actions';
import { initDataView, SourcererModel, SourcererScopeName } from './sourcerer/model';
import { ExperimentalFeatures } from '../../../common/experimental_features';
import { getScopePatternListSelection } from './sourcerer/helpers';
import { globalUrlParamReducer, initialGlobalUrlParam } from './global_url_param';
export type SubPluginsInitReducer = HostsPluginReducer &
UsersPluginReducer &
@ -36,7 +37,7 @@ export type SubPluginsInitReducer = HostsPluginReducer &
export const createInitialState = (
pluginsInitState: Omit<
SecuritySubPlugins['store']['initialState'],
'app' | 'dragAndDrop' | 'inputs' | 'sourcerer'
'app' | 'dragAndDrop' | 'inputs' | 'sourcerer' | 'globalUrlParam'
>,
{
defaultDataView,
@ -100,6 +101,7 @@ export const createInitialState = (
kibanaDataViews: kibanaDataViews.map((dataView) => ({ ...initDataView, ...dataView })),
signalIndexName,
},
globalUrlParam: initialGlobalUrlParam,
};
return preloadedState;
@ -116,5 +118,6 @@ export const createReducer: (
dragAndDrop: dragAndDropReducer,
inputs: inputsReducer,
sourcerer: sourcererReducer,
globalUrlParam: globalUrlParamReducer,
...pluginsReducer,
});

View file

@ -20,6 +20,7 @@ import { TimelinePluginState } from '../../timelines/store/timeline';
import { NetworkPluginState } from '../../network/store';
import { ManagementPluginState } from '../../management';
import { UsersPluginState } from '../../users/store';
import { GlobalUrlParam } from './global_url_param';
export type StoreState = HostsPluginState &
UsersPluginState &
@ -31,6 +32,7 @@ export type StoreState = HostsPluginState &
dragAndDrop: DragAndDropState;
inputs: InputsState;
sourcerer: SourcererState;
globalUrlParam: GlobalUrlParam;
};
/**
* The redux `State` type for the Security App.

View file

@ -0,0 +1,303 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { renderHook } from '@testing-library/react-hooks';
import {
useInitializeUrlParam,
useGlobalQueryString,
useSyncGlobalQueryString,
useUpdateUrlParam,
} from '.';
import { GlobalUrlParam, globalUrlParamActions } from '../../store/global_url_param';
import { mockHistory } from '../route/mocks';
import {
createSecuritySolutionStorageMock,
kibanaObservable,
mockGlobalState,
SUB_PLUGINS_REDUCER,
TestProviders,
} from '../../mock';
import { createStore } from '../../store';
import { LinkInfo } from '../../links';
import { SecurityPageName } from '../../../app/types';
const mockDispatch = jest.fn();
jest.mock('react-redux', () => {
const original = jest.requireActual('react-redux');
return {
...original,
useDispatch: () => mockDispatch,
};
});
const mockLocation = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useHistory: () => mockHistory,
useLocation: () => mockLocation(),
}));
const defaultLinkInfo: LinkInfo = {
id: SecurityPageName.alerts,
path: '/test',
title: 'test title',
skipUrlState: false,
};
const mockLinkInfo = jest.fn().mockResolvedValue(defaultLinkInfo);
jest.mock('../../links', () => ({
...jest.requireActual('../../links'),
getLinkInfo: () => mockLinkInfo(),
}));
describe('global query string', () => {
const { storage } = createSecuritySolutionStorageMock();
const makeStore = (globalUrlParam: GlobalUrlParam) =>
createStore(
{
...mockGlobalState,
globalUrlParam,
},
SUB_PLUGINS_REDUCER,
kibanaObservable,
storage
);
const makeWrapper = (globalUrlParam?: GlobalUrlParam) => {
const wrapper = ({ children }: { children: React.ReactElement }) => (
<TestProviders store={makeStore(globalUrlParam ?? {})}>{children}</TestProviders>
);
return wrapper;
};
beforeAll(() => {
// allow window.location.search to be redefined
Object.defineProperty(window, 'location', {
value: {
search: '?',
},
});
});
beforeEach(() => {
jest.clearAllMocks();
window.location.search = '?';
});
describe('useInitializeUrlParam', () => {
it('calls onInitialize with decoded URL param value', () => {
const urlParamKey = 'testKey';
mockLocation.mockReturnValue({ search: '?testKey=(test:(value:123))' });
const onInitialize = jest.fn();
renderHook(() => useInitializeUrlParam(urlParamKey, onInitialize), {
wrapper: makeWrapper(),
});
expect(onInitialize).toHaveBeenCalledWith({ test: { value: 123 } });
});
it('deregister during unmount', () => {
const urlParamKey = 'testKey';
mockLocation.mockReturnValue({ search: "?testKey='123'" });
const { unmount } = renderHook(() => useInitializeUrlParam(urlParamKey, () => {}), {
wrapper: makeWrapper(),
});
unmount();
expect(mockDispatch).toBeCalledWith(
globalUrlParamActions.deregisterUrlParam({
key: urlParamKey,
})
);
});
it('calls registerUrlParam global URL param action', () => {
const urlParamKey = 'testKey';
const initialValue = 123;
mockLocation.mockReturnValue({ search: `?testKey=${initialValue}` });
renderHook(() => useInitializeUrlParam(urlParamKey, () => {}), {
wrapper: makeWrapper(),
});
expect(mockDispatch).toBeCalledWith(
globalUrlParamActions.registerUrlParam({
key: urlParamKey,
initialValue: initialValue.toString(),
})
);
});
});
describe('updateUrlParam', () => {
it('dispatch updateUrlParam action', () => {
const urlParamKey = 'testKey';
const value = { test: 123 };
const encodedVaue = '(test:123)';
const globalUrlParam = {
[urlParamKey]: 'oldValue',
};
const {
result: { current: updateUrlParam },
} = renderHook(() => useUpdateUrlParam(urlParamKey), {
wrapper: makeWrapper(globalUrlParam),
});
updateUrlParam(value);
expect(mockDispatch).toBeCalledWith(
globalUrlParamActions.updateUrlParam({
key: urlParamKey,
value: encodedVaue,
})
);
});
it('dispatch updateUrlParam action with null value', () => {
const urlParamKey = 'testKey';
const {
result: { current: updateUrlParam },
} = renderHook(() => useUpdateUrlParam(urlParamKey), {
wrapper: makeWrapper(),
});
updateUrlParam(null);
expect(mockDispatch).toBeCalledWith(
globalUrlParamActions.updateUrlParam({
key: urlParamKey,
value: null,
})
);
});
});
describe('useGlobalQueryString', () => {
it('returns global query string', () => {
const store = createStore(
{
...mockGlobalState,
globalUrlParam: {
testNumber: '123',
testObject: '(test:321)',
testNull: null,
testEmpty: '',
},
},
SUB_PLUGINS_REDUCER,
kibanaObservable,
storage
);
const wrapper = ({ children }: { children: React.ReactElement }) => (
<TestProviders store={store}>{children}</TestProviders>
);
const { result } = renderHook(() => useGlobalQueryString(), { wrapper });
expect(result.current).toEqual('testNumber=123&testObject=(test:321)');
});
});
describe('useSyncGlobalQueryString', () => {
it("doesn't delete other URL params when updating one", async () => {
const urlParamKey = 'testKey';
const value = '123';
const globalUrlParam = {
[urlParamKey]: value,
};
window.location.search = `?firstKey=111&${urlParamKey}=oldValue&lastKey=999`;
renderHook(() => useSyncGlobalQueryString(), { wrapper: makeWrapper(globalUrlParam) });
expect(mockHistory.replace).toHaveBeenCalledWith({
search: `firstKey=111&${urlParamKey}=${value}&lastKey=999`,
});
});
it('updates URL params', async () => {
const urlParamKey1 = 'testKey1';
const value1 = '1111';
const urlParamKey2 = 'testKey2';
const value2 = '2222';
const globalUrlParam = {
[urlParamKey1]: value1,
[urlParamKey2]: value2,
};
window.location.search = `?`;
renderHook(() => useSyncGlobalQueryString(), { wrapper: makeWrapper(globalUrlParam) });
expect(mockHistory.replace).toHaveBeenCalledWith({
search: `${urlParamKey1}=${value1}&${urlParamKey2}=${value2}`,
});
});
it('deletes URL param when value is null', async () => {
const urlParamKey = 'testKey';
const globalUrlParam = {
[urlParamKey]: null,
};
window.location.search = `?${urlParamKey}=oldValue`;
renderHook(() => useSyncGlobalQueryString(), { wrapper: makeWrapper(globalUrlParam) });
expect(mockHistory.replace).toHaveBeenCalledWith({
search: '',
});
});
it('deletes URL param when page has skipUrlState=true', async () => {
const urlParamKey = 'testKey';
const value = 'testValue';
const globalUrlParam = {
[urlParamKey]: value,
};
window.location.search = `?${urlParamKey}=${value}`;
mockLinkInfo.mockReturnValue({ ...defaultLinkInfo, skipUrlState: true });
renderHook(() => useSyncGlobalQueryString(), { wrapper: makeWrapper(globalUrlParam) });
expect(mockHistory.replace).toHaveBeenCalledWith({
search: '',
});
});
it('does not replace URL param when the value does not change', async () => {
const urlParamKey = 'testKey';
const value = 'testValue';
const globalUrlParam = {
[urlParamKey]: value,
};
window.location.search = `?${urlParamKey}=${value}`;
renderHook(() => useSyncGlobalQueryString(), { wrapper: makeWrapper(globalUrlParam) });
expect(mockHistory.replace).not.toHaveBeenCalledWith();
});
it('does not replace URL param when the page doe not exist', async () => {
const urlParamKey = 'testKey';
const value = 'testValue';
const globalUrlParam = {
[urlParamKey]: value,
};
window.location.search = `?${urlParamKey}=oldValue`;
mockLinkInfo.mockReturnValue(undefined);
renderHook(() => useSyncGlobalQueryString(), { wrapper: makeWrapper(globalUrlParam) });
expect(mockHistory.replace).not.toHaveBeenCalledWith();
});
});
});

View file

@ -0,0 +1,144 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import * as H from 'history';
import { parse, ParsedQuery, 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, useLocation } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import {
decodeRisonUrlState,
encodeRisonUrlState,
getParamFromQueryString,
getQueryStringFromLocation,
} from '../../components/url_state/helpers';
import { useShallowEqualSelector } from '../../hooks/use_selector';
import { globalUrlParamActions, globalUrlParamSelectors } from '../../store/global_url_param';
import { useRouteSpy } from '../route/use_route_spy';
import { getLinkInfo } from '../../links';
/**
* Adds urlParamKey and the initial value to redux store.
*
* Please call this hook at the highest possible level of the rendering tree.
* So it is only called when the application starts instead of on every page.
*
* @param urlParamKey Must not change.
* @param onInitialize Called once when initializing.
*/
export const useInitializeUrlParam = <State>(
urlParamKey: string,
/**
* @param state Decoded URL param value.
*/
onInitialize: (state: State | null) => void
) => {
const dispatch = useDispatch();
const { search } = useLocation();
useEffect(() => {
const initialValue = getParamFromQueryString(getQueryStringFromLocation(search), urlParamKey);
dispatch(
globalUrlParamActions.registerUrlParam({
key: urlParamKey,
initialValue: initialValue ?? null,
})
);
// execute consumer initialization
onInitialize(decodeRisonUrlState<State>(initialValue ?? undefined));
return () => {
dispatch(globalUrlParamActions.deregisterUrlParam({ key: urlParamKey }));
};
// eslint-disable-next-line react-hooks/exhaustive-deps -- It must run only once when the application is initializing.
}, []);
};
/**
* Updates URL parameters in the url.
*
* Make sure to call `useInitializeUrlParam` before calling this function.
*/
export const useUpdateUrlParam = <State>(urlParamKey: string) => {
const dispatch = useDispatch();
const updateUrlParam = useCallback(
(value: State | null) => {
const encodedValue = value !== null ? encodeRisonUrlState(value) : null;
dispatch(globalUrlParamActions.updateUrlParam({ key: urlParamKey, value: encodedValue }));
},
[dispatch, urlParamKey]
);
return updateUrlParam;
};
export const useGlobalQueryString = (): string => {
const globalUrlParam = useShallowEqualSelector(globalUrlParamSelectors.selectGlobalUrlParam);
const globalQueryString = useMemo(
() => encodeQueryString(pickBy((value) => !isEmpty(value), globalUrlParam)),
[globalUrlParam]
);
return globalQueryString;
};
/**
* - It hides / shows the global query depending on the page.
* - It updates the URL when globalUrlParam store updates.
*/
export const useSyncGlobalQueryString = () => {
const history = useHistory();
const [{ pageName }] = useRouteSpy();
const globalUrlParam = useShallowEqualSelector(globalUrlParamSelectors.selectGlobalUrlParam);
useEffect(() => {
const linkInfo = getLinkInfo(pageName) ?? { skipUrlState: true };
const params = Object.entries(globalUrlParam).map(([key, value]) => ({
key,
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 });
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 });
}
};

View file

@ -7,11 +7,12 @@
import { noop } from 'lodash/fp';
import { createContext, Dispatch } from 'react';
import { SecurityPageName } from '../../../app/types';
import { RouteSpyState, RouteSpyAction } from './types';
export const initRouteSpy: RouteSpyState = {
pageName: '',
pageName: SecurityPageName.noPage,
detailName: undefined,
tabName: undefined,
search: '',

View file

@ -13,6 +13,7 @@ import { ManageRoutesSpy } from './manage_spy_routes';
import { SpyRouteComponent } from './spy_routes';
import { useRouteSpy } from './use_route_spy';
import { generateHistoryMock, generateRoutesMock } from './mocks';
import { SecurityPageName } from '../../../app/types';
const mockUseRouteSpy: jest.Mock = useRouteSpy as jest.Mock;
jest.mock('./use_route_spy', () => ({
@ -81,7 +82,7 @@ describe('Spy Routes', () => {
flowTarget: undefined,
},
}}
pageName="hosts"
pageName={SecurityPageName.hosts}
/>
</ManageRoutesSpy>
);
@ -127,7 +128,7 @@ describe('Spy Routes', () => {
flowTarget: undefined,
},
}}
pageName="hosts"
pageName={SecurityPageName.hosts}
/>
);
@ -146,7 +147,7 @@ describe('Spy Routes', () => {
path: newPathname,
url: newPathname,
params: {
pageName: 'hosts',
pageName: SecurityPageName.hosts,
detailName: undefined,
tabName: HostsTableType.authentications,
search: '',
@ -162,7 +163,7 @@ describe('Spy Routes', () => {
route: {
detailName: undefined,
history: mockHistoryValue,
pageName: 'hosts',
pageName: SecurityPageName.hosts,
pathName: newPathname,
tabName: HostsTableType.authentications,
search: '?updated="true"',

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { SecurityPageName } from '../../../app/types';
import { RouteSpyState } from './types';
type Action = 'PUSH' | 'POP' | 'REPLACE';
@ -35,7 +36,7 @@ export const generateHistoryMock = () => ({
export const mockHistory = generateHistoryMock();
export const generateRoutesMock = (): RouteSpyState => ({
pageName: '',
pageName: SecurityPageName.noPage,
detailName: undefined,
tabName: undefined,
search: '',

View file

@ -15,7 +15,7 @@ import { useRouteSpy } from './use_route_spy';
import { SecurityPageName } from '../../../../common/constants';
export const SpyRouteComponent = memo<
SpyRouteProps & { location: H.Location; pageName: string | undefined }
SpyRouteProps & { location: H.Location; pageName: SecurityPageName | undefined }
>(
({
location: { pathname, search },

View file

@ -16,6 +16,7 @@ import { NetworkRouteType } from '../../../network/pages/navigation/types';
import { AdministrationSubTab as AdministrationType } from '../../../management/types';
import { FlowTarget } from '../../../../common/search_strategy';
import { UsersTableType } from '../../../users/store/model';
import { SecurityPageName } from '../../../app/types';
export type SiemRouteType =
| HostsTableType
@ -24,7 +25,7 @@ export type SiemRouteType =
| AdministrationType
| UsersTableType;
export interface RouteSpyState {
pageName: string;
pageName: SecurityPageName;
detailName: string | undefined;
tabName: SiemRouteType | undefined;
search: string;

View file

@ -146,7 +146,7 @@ exports[`Field Renderers #hostIdRenderer it renders correctly against snapshot 1
<a
class="euiLink css-102pf9n-euiLink-primary"
data-test-subj="host-details-button"
href="securitySolutionUI/hosts/raspberrypi?sourcerer=(default:(id:security-solution,selectedPatterns:!('apm-*-transaction*','auditbeat-*','endgame-*','filebeat-*','logs-*','packetbeat-*','traces-apm*','winlogbeat-*','-*elastic-cloud-logs-*')))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now%2Fd,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now%2Fd)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now%2Fd,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now%2Fd)))"
href="securitySolutionUI/hosts/raspberrypi?timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now%2Fd,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now%2Fd)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now%2Fd,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now%2Fd)))"
rel="noreferrer"
>
raspberrypi
@ -201,7 +201,7 @@ exports[`Field Renderers #hostNameRenderer it renders correctly against snapshot
<a
class="euiLink css-102pf9n-euiLink-primary"
data-test-subj="host-details-button"
href="securitySolutionUI/hosts/raspberrypi?sourcerer=(default:(id:security-solution,selectedPatterns:!('apm-*-transaction*','auditbeat-*','endgame-*','filebeat-*','logs-*','packetbeat-*','traces-apm*','winlogbeat-*','-*elastic-cloud-logs-*')))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now%2Fd,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now%2Fd)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now%2Fd,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now%2Fd)))"
href="securitySolutionUI/hosts/raspberrypi?timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now%2Fd,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now%2Fd)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now%2Fd,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now%2Fd)))"
rel="noreferrer"
>
raspberrypi