mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[TIP] Show loading when integration availability is checked (#139537)
This commit is contained in:
parent
b70775d57e
commit
62092e2e05
18 changed files with 450 additions and 183 deletions
|
@ -14,13 +14,12 @@ import type { SourcererDataView } from '@kbn/threat-intelligence-plugin/public/t
|
|||
import { useKibana } from '../common/lib/kibana';
|
||||
import { FiltersGlobal } from '../common/components/filters_global';
|
||||
import { SpyRoute } from '../common/utils/route/spy_routes';
|
||||
import { SecuritySolutionPageWrapper } from '../common/components/page_wrapper';
|
||||
import { useIsExperimentalFeatureEnabled } from '../common/hooks/use_experimental_features';
|
||||
import { licenseService } from '../common/hooks/use_license';
|
||||
import { SecurityPageName } from '../app/types';
|
||||
import type { SecuritySubPluginRoutes } from '../app/types';
|
||||
import { useSourcererDataView } from '../common/containers/sourcerer';
|
||||
import { PluginTemplateWrapper } from '../common/components/plugin_template_wrapper';
|
||||
import { SecuritySolutionPageWrapper } from '../common/components/page_wrapper';
|
||||
|
||||
const ThreatIntelligence = memo(() => {
|
||||
const { threatIntelligence } = useKibana().services;
|
||||
|
@ -35,18 +34,15 @@ const ThreatIntelligence = memo(() => {
|
|||
|
||||
const securitySolutionContext: SecuritySolutionPluginContext = {
|
||||
getFiltersGlobalComponent: () => FiltersGlobal,
|
||||
getPageWrapper: () => SecuritySolutionPageWrapper,
|
||||
licenseService,
|
||||
sourcererDataView: sourcererDataView as unknown as SourcererDataView,
|
||||
};
|
||||
|
||||
return (
|
||||
<TrackApplicationView viewId="threat_intelligence">
|
||||
<PluginTemplateWrapper>
|
||||
<SecuritySolutionPageWrapper noPadding>
|
||||
<ThreatIntelligencePlugin securitySolutionContext={securitySolutionContext} />
|
||||
<SpyRoute pageName={SecurityPageName.threatIntelligenceIndicators} />
|
||||
</SecuritySolutionPageWrapper>
|
||||
</PluginTemplateWrapper>
|
||||
<ThreatIntelligencePlugin securitySolutionContext={securitySolutionContext} />
|
||||
<SpyRoute pageName={SecurityPageName.threatIntelligenceIndicators} />
|
||||
</TrackApplicationView>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -13,6 +13,10 @@ export const getSecuritySolutionContextMock = (): SecuritySolutionPluginContext
|
|||
() =>
|
||||
({ children }) =>
|
||||
<div>{children}</div>,
|
||||
getPageWrapper:
|
||||
() =>
|
||||
({ children }) =>
|
||||
<div>{children}</div>,
|
||||
licenseService: {
|
||||
isEnterprise() {
|
||||
return true;
|
||||
|
@ -22,5 +26,6 @@ export const getSecuritySolutionContextMock = (): SecuritySolutionPluginContext
|
|||
browserFields: {},
|
||||
selectedPatterns: [],
|
||||
indexPattern: { fields: [], title: '' },
|
||||
loading: false,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -110,6 +110,13 @@ export const mockedServices = {
|
|||
getFieldBrowser: jest.fn().mockReturnValue(null),
|
||||
},
|
||||
timelines: timelinesServiceMock,
|
||||
securityLayout: {
|
||||
getPluginWrapper:
|
||||
() =>
|
||||
({ children }: any) => {
|
||||
return <>{children}</>;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const TestProvidersComponent: FC = ({ children }) => (
|
||||
|
|
|
@ -9,11 +9,12 @@ import { render, screen } from '@testing-library/react';
|
|||
import React from 'react';
|
||||
import { DefaultPageLayout, TITLE_TEST_ID } from './layout';
|
||||
import '@testing-library/jest-dom';
|
||||
import { TestProvidersComponent } from '../../common/mocks/test_providers';
|
||||
|
||||
describe('<Layout />', () => {
|
||||
describe('when pageTitle is not specified', () => {
|
||||
beforeEach(() => {
|
||||
render(<DefaultPageLayout />);
|
||||
render(<DefaultPageLayout />, { wrapper: TestProvidersComponent });
|
||||
});
|
||||
|
||||
it('should not render secondary heading', () => {
|
||||
|
@ -23,7 +24,9 @@ describe('<Layout />', () => {
|
|||
|
||||
describe('when pageTitle is passed, it should be rendered as secondary heading', () => {
|
||||
beforeEach(() => {
|
||||
render(<DefaultPageLayout pageTitle="Stranger Threats" />);
|
||||
render(<DefaultPageLayout pageTitle="Stranger Threats" />, {
|
||||
wrapper: TestProvidersComponent,
|
||||
});
|
||||
});
|
||||
|
||||
it('should render secondary heading', () => {
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { EuiPageHeader, EuiPageHeaderSection, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import React, { FC } from 'react';
|
||||
import { SecuritySolutionPageWrapper } from '../../containers/security_solution_page_wrapper';
|
||||
|
||||
export interface LayoutProps {
|
||||
pageTitle?: string;
|
||||
|
@ -17,7 +18,7 @@ export const TITLE_TEST_ID = 'tiDefaultPageLayoutTitle';
|
|||
|
||||
export const DefaultPageLayout: FC<LayoutProps> = ({ children, pageTitle, border = true }) => {
|
||||
return (
|
||||
<>
|
||||
<SecuritySolutionPageWrapper>
|
||||
<EuiPageHeader alignItems="center" bottomBorder={border}>
|
||||
<EuiPageHeaderSection>
|
||||
{pageTitle && (
|
||||
|
@ -29,6 +30,6 @@ export const DefaultPageLayout: FC<LayoutProps> = ({ children, pageTitle, border
|
|||
</EuiPageHeader>
|
||||
<EuiSpacer size="l" />
|
||||
{children}
|
||||
</>
|
||||
</SecuritySolutionPageWrapper>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -10,6 +10,7 @@ import { FC } from 'react';
|
|||
import { Paywall } from '../../components/paywall';
|
||||
import { useKibana } from '../../hooks/use_kibana';
|
||||
import { useSecurityContext } from '../../hooks/use_security_context';
|
||||
import { SecuritySolutionPluginTemplateWrapper } from '../security_solution_plugin_template_wrapper';
|
||||
|
||||
export const EnterpriseGuard: FC = ({ children }) => {
|
||||
const { licenseService } = useSecurityContext();
|
||||
|
@ -22,8 +23,10 @@ export const EnterpriseGuard: FC = ({ children }) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<Paywall
|
||||
licenseManagementHref={http.basePath.prepend('/app/management/stack/license_management')}
|
||||
/>
|
||||
<SecuritySolutionPluginTemplateWrapper template="noData">
|
||||
<Paywall
|
||||
licenseManagementHref={http.basePath.prepend('/app/management/stack/license_management')}
|
||||
/>
|
||||
</SecuritySolutionPluginTemplateWrapper>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`checking if the page should be visible (based on indicator count) when indicator count is being loaded should render nothing at all: loading 1`] = `
|
||||
<DocumentFragment>
|
||||
<span
|
||||
aria-label="Loading"
|
||||
class="euiLoadingLogo emotion-euiLoadingLogo-xl"
|
||||
role="progressbar"
|
||||
>
|
||||
<span
|
||||
class="emotion-euiLoadingLogo__icon"
|
||||
>
|
||||
<span
|
||||
data-euiicon-type="logoSecurity"
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`checking if the page should be visible (based on indicator count) when indicator count is loaded and there are no indicators should render empty page when no indicators are found: no indicators 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="euiPanel euiPanel--transparent euiEmptyPrompt euiEmptyPrompt--horizontal euiEmptyPrompt--paddingLarge emotion-euiPanel-m-transparent"
|
||||
data-test-subj="tiEmptyPage"
|
||||
>
|
||||
<div
|
||||
class="euiEmptyPrompt__main"
|
||||
>
|
||||
<div
|
||||
class="euiEmptyPrompt__icon"
|
||||
>
|
||||
<figure
|
||||
class="euiImageWrapper emotion-euiImageWrapper-fullWidth"
|
||||
>
|
||||
<img
|
||||
alt="Enable Threat Intelligence Integrations"
|
||||
class="euiImage emotion-euiImage-fullWidth"
|
||||
src="test-file-stub"
|
||||
/>
|
||||
</figure>
|
||||
</div>
|
||||
<div
|
||||
class="euiEmptyPrompt__content"
|
||||
>
|
||||
<div
|
||||
class="euiEmptyPrompt__contentInner"
|
||||
>
|
||||
<h3
|
||||
class="euiTitle emotion-euiTitle-s"
|
||||
>
|
||||
Get started with Elastic Threat Intelligence
|
||||
</h3>
|
||||
<div
|
||||
class="euiSpacer euiSpacer--m emotion-euiSpacer-m"
|
||||
/>
|
||||
<div
|
||||
class="euiText emotion-euiText-m-euiTextColor-subdued"
|
||||
>
|
||||
<p>
|
||||
Elastic Threat Intelligence makes it easy to analyze and investigate potential security threats by aggregating data from multiple sources in one place.
|
||||
</p>
|
||||
<p>
|
||||
You’ll be able to view data from all activated threat intelligence feeds and take action from this page.
|
||||
</p>
|
||||
<p>
|
||||
To get started with Elastic Threat Intelligence, enable one or more Threat Intelligence Integrations from the Integrations page or ingest data using filebeat. For more information, view the
|
||||
<a
|
||||
class="euiLink emotion-euiLink-primary"
|
||||
data-test-subj="tiEmptyPageDocsLink"
|
||||
href=""
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Security app documentation
|
||||
<span
|
||||
class="emotion-euiLink__externalIcon"
|
||||
data-euiicon-type="popout"
|
||||
>
|
||||
External link
|
||||
</span>
|
||||
<span
|
||||
class="emotion-euiScreenReaderOnly-euiLink__screenReaderText"
|
||||
>
|
||||
(opens in a new tab or window)
|
||||
</span>
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="euiSpacer euiSpacer--l emotion-euiSpacer-l"
|
||||
/>
|
||||
<button
|
||||
class="euiButton euiButton--primary euiButton--fill"
|
||||
data-test-subj="tiEmptyPageIntegrationsPageLink"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="euiButtonContent euiButton__content"
|
||||
>
|
||||
<span
|
||||
class="euiButtonContent__icon"
|
||||
color="inherit"
|
||||
data-euiicon-type="plusInCircle"
|
||||
/>
|
||||
<span
|
||||
class="euiButton__text"
|
||||
>
|
||||
Add Integrations
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`checking if the page should be visible (based on indicator count) when loading is done and we have some indicators should render indicators table: indicators are present 1`] = `
|
||||
<DocumentFragment>
|
||||
should be restricted
|
||||
</DocumentFragment>
|
||||
`;
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from './integrations_guard';
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* 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 { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { IntegrationsGuard } from '.';
|
||||
import { TestProvidersComponent } from '../../common/mocks/test_providers';
|
||||
import { useTIDocumentationLink } from '../../hooks/use_documentation_link';
|
||||
import { useIntegrationsPageLink } from '../../hooks/use_integrations_page_link';
|
||||
import { useIndicatorsTotalCount } from '../../modules/indicators/hooks/use_indicators_total_count';
|
||||
|
||||
jest.mock('../../modules/indicators/hooks/use_indicators_total_count');
|
||||
jest.mock('../../hooks/use_integrations_page_link');
|
||||
jest.mock('../../hooks/use_documentation_link');
|
||||
|
||||
describe('checking if the page should be visible (based on indicator count)', () => {
|
||||
describe('when indicator count is being loaded', () => {
|
||||
it('should render nothing at all', () => {
|
||||
(
|
||||
useIndicatorsTotalCount as jest.MockedFunction<typeof useIndicatorsTotalCount>
|
||||
).mockReturnValue({
|
||||
count: 0,
|
||||
isLoading: true,
|
||||
});
|
||||
(
|
||||
useIntegrationsPageLink as jest.MockedFunction<typeof useIntegrationsPageLink>
|
||||
).mockReturnValue('');
|
||||
(
|
||||
useTIDocumentationLink as jest.MockedFunction<typeof useTIDocumentationLink>
|
||||
).mockReturnValue('');
|
||||
|
||||
const { asFragment } = render(<IntegrationsGuard>should be restricted</IntegrationsGuard>, {
|
||||
wrapper: TestProvidersComponent,
|
||||
});
|
||||
|
||||
expect(asFragment()).toMatchSnapshot('loading');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when indicator count is loaded and there are no indicators', () => {
|
||||
it('should render empty page when no indicators are found', async () => {
|
||||
(
|
||||
useIndicatorsTotalCount as jest.MockedFunction<typeof useIndicatorsTotalCount>
|
||||
).mockReturnValue({
|
||||
count: 0,
|
||||
isLoading: false,
|
||||
});
|
||||
(
|
||||
useIntegrationsPageLink as jest.MockedFunction<typeof useIntegrationsPageLink>
|
||||
).mockReturnValue('');
|
||||
(
|
||||
useTIDocumentationLink as jest.MockedFunction<typeof useTIDocumentationLink>
|
||||
).mockReturnValue('');
|
||||
|
||||
const { asFragment } = render(<IntegrationsGuard>should be restricted</IntegrationsGuard>, {
|
||||
wrapper: TestProvidersComponent,
|
||||
});
|
||||
expect(asFragment()).toMatchSnapshot('no indicators');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when loading is done and we have some indicators', () => {
|
||||
it('should render indicators table', async () => {
|
||||
(
|
||||
useIndicatorsTotalCount as jest.MockedFunction<typeof useIndicatorsTotalCount>
|
||||
).mockReturnValue({
|
||||
count: 7,
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
const { asFragment } = render(<IntegrationsGuard>should be restricted</IntegrationsGuard>, {
|
||||
wrapper: TestProvidersComponent,
|
||||
});
|
||||
expect(asFragment()).toMatchSnapshot('indicators are present');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 { EuiLoadingLogo } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { FC } from 'react';
|
||||
import { EmptyPage } from '../../modules/empty_page';
|
||||
import { useIndicatorsTotalCount } from '../../modules/indicators/hooks/use_indicators_total_count';
|
||||
import { SecuritySolutionPluginTemplateWrapper } from '../security_solution_plugin_template_wrapper';
|
||||
|
||||
/**
|
||||
* Renders children only if TI integrations are enabled
|
||||
*/
|
||||
export const IntegrationsGuard: FC = ({ children }) => {
|
||||
const { count: indicatorsTotalCount, isLoading: isIndicatorsTotalCountLoading } =
|
||||
useIndicatorsTotalCount();
|
||||
|
||||
if (isIndicatorsTotalCountLoading) {
|
||||
return (
|
||||
<SecuritySolutionPluginTemplateWrapper template="noData">
|
||||
<EuiLoadingLogo logo="logoSecurity" size="xl" />
|
||||
</SecuritySolutionPluginTemplateWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
const showEmptyPage = indicatorsTotalCount === 0;
|
||||
|
||||
return showEmptyPage ? <EmptyPage /> : <>{children}</>;
|
||||
};
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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 { FC } from 'react';
|
||||
import { useSecurityContext } from '../hooks/use_security_context';
|
||||
|
||||
/**
|
||||
* Security solution page wrapper, with some extra styling etc.
|
||||
*/
|
||||
export const SecuritySolutionPageWrapper: FC = ({ children }) => {
|
||||
const contextValue = useSecurityContext();
|
||||
|
||||
const Component = contextValue.getPageWrapper();
|
||||
|
||||
return <Component>{children}</Component>;
|
||||
};
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 type { FC } from 'react';
|
||||
import type { KibanaPageTemplateProps } from '@kbn/shared-ux-page-kibana-template';
|
||||
import { useKibana } from '../hooks/use_kibana';
|
||||
|
||||
interface SecuritySolutionPluginTemplateWrapperProps {
|
||||
/**
|
||||
* Accepts all the values from KibanaPageTemplate, as well as `noData` which centers the page contents.
|
||||
*/
|
||||
template?: KibanaPageTemplateProps['template'] | 'noData';
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses securityLayout service to retrieve shared plugin wrapper component and renders plugin routes / children inside of it.
|
||||
*
|
||||
* The `template` prop can be used to alter the page layout for a given plugin route / all routes within a plugin - depending on the nesting.
|
||||
*/
|
||||
export const SecuritySolutionPluginTemplateWrapper: FC<
|
||||
SecuritySolutionPluginTemplateWrapperProps
|
||||
> = ({ children, template }) => {
|
||||
const {
|
||||
services: {
|
||||
securityLayout: { getPluginWrapper },
|
||||
},
|
||||
} = useKibana();
|
||||
|
||||
const Wrapper = getPluginWrapper();
|
||||
|
||||
return <Wrapper template={template}>{children}</Wrapper>;
|
||||
};
|
|
@ -13,6 +13,7 @@ import { FormattedMessage } from '@kbn/i18n-react';
|
|||
import { useTIDocumentationLink } from '../../hooks/use_documentation_link';
|
||||
import { useIntegrationsPageLink } from '../../hooks/use_integrations_page_link';
|
||||
import illustration from './integrations_light.svg';
|
||||
import { SecuritySolutionPluginTemplateWrapper } from '../../containers/security_solution_plugin_template_wrapper';
|
||||
|
||||
export const DOCS_LINK_TEST_ID = 'tiEmptyPageDocsLink';
|
||||
export const EMPTY_PROMPT_TEST_ID = 'tiEmptyPage';
|
||||
|
@ -23,82 +24,84 @@ export const EmptyPage: VFC = () => {
|
|||
const documentationLink = useTIDocumentationLink();
|
||||
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
icon={
|
||||
<EuiImage
|
||||
size="fullWidth"
|
||||
alt={i18n.translate('xpack.threatIntelligence.common.emptyPage.imgAlt', {
|
||||
defaultMessage: 'Enable Threat Intelligence Integrations',
|
||||
})}
|
||||
src={illustration}
|
||||
/>
|
||||
}
|
||||
title={
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.threatIntelligence.common.emptyPage.title"
|
||||
defaultMessage="Get started with Elastic Threat Intelligence"
|
||||
<SecuritySolutionPluginTemplateWrapper template="noData">
|
||||
<EuiEmptyPrompt
|
||||
icon={
|
||||
<EuiImage
|
||||
size="fullWidth"
|
||||
alt={i18n.translate('xpack.threatIntelligence.common.emptyPage.imgAlt', {
|
||||
defaultMessage: 'Enable Threat Intelligence Integrations',
|
||||
})}
|
||||
src={illustration}
|
||||
/>
|
||||
</h3>
|
||||
}
|
||||
titleSize="s"
|
||||
layout="horizontal"
|
||||
color="transparent"
|
||||
body={
|
||||
<>
|
||||
<p>
|
||||
}
|
||||
title={
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id="xpack.threatIntelligence.common.emptyPage.body1"
|
||||
defaultMessage="Elastic Threat Intelligence makes it easy to analyze and investigate potential security
|
||||
id="xpack.threatIntelligence.common.emptyPage.title"
|
||||
defaultMessage="Get started with Elastic Threat Intelligence"
|
||||
/>
|
||||
</h3>
|
||||
}
|
||||
titleSize="s"
|
||||
layout="horizontal"
|
||||
color="transparent"
|
||||
body={
|
||||
<>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.threatIntelligence.common.emptyPage.body1"
|
||||
defaultMessage="Elastic Threat Intelligence makes it easy to analyze and investigate potential security
|
||||
threats by aggregating data from multiple sources in one place."
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.threatIntelligence.common.emptyPage.body2"
|
||||
defaultMessage="You’ll be able to view data from all activated threat intelligence feeds and take action
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.threatIntelligence.common.emptyPage.body2"
|
||||
defaultMessage="You’ll be able to view data from all activated threat intelligence feeds and take action
|
||||
from this page."
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.threatIntelligence.common.emptyPage.body3"
|
||||
defaultMessage="To get started with Elastic Threat Intelligence, enable one or more Threat Intelligence
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.threatIntelligence.common.emptyPage.body3"
|
||||
defaultMessage="To get started with Elastic Threat Intelligence, enable one or more Threat Intelligence
|
||||
Integrations from the Integrations page or ingest data using filebeat. For more
|
||||
information, view the {docsLink}."
|
||||
values={{
|
||||
docsLink: (
|
||||
<EuiLink
|
||||
href={documentationLink}
|
||||
target="_blank"
|
||||
data-test-subj={DOCS_LINK_TEST_ID}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.threatIntelligence.common.emptyPage.docsLinkText"
|
||||
defaultMessage="Security app documentation"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
values={{
|
||||
docsLink: (
|
||||
<EuiLink
|
||||
href={documentationLink}
|
||||
target="_blank"
|
||||
data-test-subj={DOCS_LINK_TEST_ID}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.threatIntelligence.common.emptyPage.docsLinkText"
|
||||
defaultMessage="Security app documentation"
|
||||
/>
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
actions={
|
||||
<EuiButton
|
||||
data-test-subj={INTEGRATION_LINK_ID}
|
||||
href={integrationsPageLink}
|
||||
color="primary"
|
||||
iconType="plusInCircle"
|
||||
fill
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.threatIntelligence.common.emptyPage.buttonText"
|
||||
defaultMessage="Add Integrations"
|
||||
/>
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
actions={
|
||||
<EuiButton
|
||||
data-test-subj={INTEGRATION_LINK_ID}
|
||||
href={integrationsPageLink}
|
||||
color="primary"
|
||||
iconType="plusInCircle"
|
||||
fill
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.threatIntelligence.common.emptyPage.buttonText"
|
||||
defaultMessage="Add Integrations"
|
||||
/>
|
||||
</EuiButton>
|
||||
}
|
||||
data-test-subj={EMPTY_PROMPT_TEST_ID}
|
||||
/>
|
||||
</EuiButton>
|
||||
}
|
||||
data-test-subj={EMPTY_PROMPT_TEST_ID}
|
||||
/>
|
||||
</SecuritySolutionPluginTemplateWrapper>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
isCompleteResponse,
|
||||
} from '@kbn/data-plugin/common';
|
||||
import { useKibana } from '../../../hooks/use_kibana';
|
||||
import { RawIndicatorsResponse } from './use_indicators';
|
||||
import type { RawIndicatorsResponse } from './use_indicators';
|
||||
import { useSourcererDataView } from './use_sourcerer_data_view';
|
||||
|
||||
export const useIndicatorsTotalCount = () => {
|
||||
|
@ -24,7 +24,7 @@ export const useIndicatorsTotalCount = () => {
|
|||
const [count, setCount] = useState<number>(0);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
|
||||
const { selectedPatterns } = useSourcererDataView();
|
||||
const { selectedPatterns, loading: loadingDataView } = useSourcererDataView();
|
||||
|
||||
useEffect(() => {
|
||||
const query = {
|
||||
|
@ -71,5 +71,5 @@ export const useIndicatorsTotalCount = () => {
|
|||
});
|
||||
}, [searchService, selectedPatterns]);
|
||||
|
||||
return { count, isLoading };
|
||||
return { count, isLoading: isLoading || loadingDataView };
|
||||
};
|
||||
|
|
|
@ -7,28 +7,16 @@
|
|||
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { TestProvidersComponent } from '../../common/mocks/test_providers';
|
||||
import { IndicatorsPage } from './indicators_page';
|
||||
import { useIndicators } from './hooks/use_indicators';
|
||||
import { useIndicatorsTotalCount } from './hooks/use_indicators_total_count';
|
||||
import { useAggregatedIndicators } from './hooks/use_aggregated_indicators';
|
||||
|
||||
import {
|
||||
TABLE_TEST_ID as INDICATORS_TABLE_TEST_ID,
|
||||
TABLE_TEST_ID,
|
||||
} from './components/indicators_table/indicators_table';
|
||||
import { EMPTY_PROMPT_TEST_ID } from '../empty_page';
|
||||
import { useIntegrationsPageLink } from '../../hooks/use_integrations_page_link';
|
||||
import { useTIDocumentationLink } from '../../hooks/use_documentation_link';
|
||||
import { useFilters } from './hooks/use_filters';
|
||||
import moment from 'moment';
|
||||
import { TestProvidersComponent } from '../../common/mocks/test_providers';
|
||||
import { TABLE_TEST_ID } from './components/indicators_table';
|
||||
|
||||
jest.mock('./hooks/use_indicators');
|
||||
jest.mock('./hooks/use_indicators_total_count');
|
||||
jest.mock('./hooks/use_filters');
|
||||
|
||||
jest.mock('../../hooks/use_integrations_page_link');
|
||||
jest.mock('../../hooks/use_documentation_link');
|
||||
jest.mock('./hooks/use_aggregated_indicators');
|
||||
|
||||
const stub = () => {};
|
||||
|
@ -64,65 +52,19 @@ describe('<IndicatorsPage />', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('checking if the page should be visible (based on indicator count)', () => {
|
||||
describe('when indicator count is being loaded', () => {
|
||||
it('should render nothing at all', () => {
|
||||
(
|
||||
useIndicatorsTotalCount as jest.MockedFunction<typeof useIndicatorsTotalCount>
|
||||
).mockReturnValue({
|
||||
count: 0,
|
||||
isLoading: true,
|
||||
});
|
||||
(
|
||||
useIntegrationsPageLink as jest.MockedFunction<typeof useIntegrationsPageLink>
|
||||
).mockReturnValue('');
|
||||
(
|
||||
useTIDocumentationLink as jest.MockedFunction<typeof useTIDocumentationLink>
|
||||
).mockReturnValue('');
|
||||
it('should render the table', () => {
|
||||
const { queryByTestId } = render(<IndicatorsPage />, { wrapper: TestProvidersComponent });
|
||||
|
||||
const { queryByTestId } = render(<IndicatorsPage />, { wrapper: TestProvidersComponent });
|
||||
|
||||
expect(queryByTestId(EMPTY_PROMPT_TEST_ID)).not.toBeInTheDocument();
|
||||
expect(queryByTestId(TABLE_TEST_ID)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when indicator count is loaded and there are no indicators', () => {
|
||||
it('should render empty page when no indicators are found', async () => {
|
||||
(
|
||||
useIndicatorsTotalCount as jest.MockedFunction<typeof useIndicatorsTotalCount>
|
||||
).mockReturnValue({
|
||||
count: 0,
|
||||
isLoading: false,
|
||||
});
|
||||
(
|
||||
useIntegrationsPageLink as jest.MockedFunction<typeof useIntegrationsPageLink>
|
||||
).mockReturnValue('');
|
||||
(
|
||||
useTIDocumentationLink as jest.MockedFunction<typeof useTIDocumentationLink>
|
||||
).mockReturnValue('');
|
||||
|
||||
const { queryByTestId } = render(<IndicatorsPage />, { wrapper: TestProvidersComponent });
|
||||
|
||||
expect(queryByTestId(TABLE_TEST_ID)).not.toBeInTheDocument();
|
||||
expect(queryByTestId(EMPTY_PROMPT_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
expect(queryByTestId(TABLE_TEST_ID)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('when loading is done and we have some indicators', () => {
|
||||
it('should render indicators table', async () => {
|
||||
(
|
||||
useIndicatorsTotalCount as jest.MockedFunction<typeof useIndicatorsTotalCount>
|
||||
).mockReturnValue({
|
||||
count: 7,
|
||||
isLoading: false,
|
||||
});
|
||||
it('should render the query input', () => {
|
||||
const { queryByTestId } = render(<IndicatorsPage />, { wrapper: TestProvidersComponent });
|
||||
expect(queryByTestId('iocListPageQueryInput')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const { queryByTestId } = render(<IndicatorsPage />, { wrapper: TestProvidersComponent });
|
||||
|
||||
expect(queryByTestId(INDICATORS_TABLE_TEST_ID)).toBeInTheDocument();
|
||||
expect(queryByTestId(EMPTY_PROMPT_TEST_ID)).not.toBeInTheDocument();
|
||||
});
|
||||
it('should render stack by selector', () => {
|
||||
const { queryByText } = render(<IndicatorsPage />, { wrapper: TestProvidersComponent });
|
||||
expect(queryByText('Stack by')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,8 +9,6 @@ import React, { VFC } from 'react';
|
|||
import { IndicatorsBarChartWrapper } from './components/indicators_barchart_wrapper/indicators_barchart_wrapper';
|
||||
import { IndicatorsTable } from './components/indicators_table/indicators_table';
|
||||
import { useIndicators } from './hooks/use_indicators';
|
||||
import { EmptyPage } from '../empty_page';
|
||||
import { useIndicatorsTotalCount } from './hooks/use_indicators_total_count';
|
||||
import { DefaultPageLayout } from '../../components/layout';
|
||||
import { useFilters } from './hooks/use_filters';
|
||||
import { FiltersGlobal } from '../../containers/filters_global';
|
||||
|
@ -18,9 +16,6 @@ import QueryBar from './components/query_bar';
|
|||
import { useSourcererDataView } from './hooks/use_sourcerer_data_view';
|
||||
|
||||
export const IndicatorsPage: VFC = () => {
|
||||
const { count: indicatorsTotalCount, isLoading: isIndicatorsTotalCountLoading } =
|
||||
useIndicatorsTotalCount();
|
||||
|
||||
const { browserFields, indexPattern } = useSourcererDataView();
|
||||
|
||||
const {
|
||||
|
@ -40,18 +35,7 @@ export const IndicatorsPage: VFC = () => {
|
|||
timeRange,
|
||||
});
|
||||
|
||||
// This prevents indicators table flash when total count is loading.
|
||||
// TODO: Improve this with custom loader component. It would require changes to security solutions' template wrapper - to allow
|
||||
// 'template' overrides.
|
||||
if (isIndicatorsTotalCountLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const showEmptyPage = indicatorsTotalCount === 0;
|
||||
|
||||
return showEmptyPage ? (
|
||||
<EmptyPage />
|
||||
) : (
|
||||
return (
|
||||
<DefaultPageLayout pageTitle="Indicators">
|
||||
<FiltersGlobal>
|
||||
<QueryBar
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { CoreStart, Plugin } from '@kbn/core/public';
|
||||
import { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
import React, { Suspense } from 'react';
|
||||
import React, { Suspense, VFC } from 'react';
|
||||
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
|
||||
import { KibanaContextProvider } from './hooks/use_kibana';
|
||||
import {
|
||||
|
@ -19,6 +19,8 @@ import {
|
|||
} from './types';
|
||||
import { SecuritySolutionContext } from './containers/security_solution_context';
|
||||
import { EnterpriseGuard } from './containers/enterprise_guard';
|
||||
import { SecuritySolutionPluginTemplateWrapper } from './containers/security_solution_plugin_template_wrapper';
|
||||
import { IntegrationsGuard } from './containers/integrations_guard';
|
||||
|
||||
interface AppProps {
|
||||
securitySolutionContext: SecuritySolutionPluginContext;
|
||||
|
@ -26,6 +28,14 @@ interface AppProps {
|
|||
|
||||
const LazyIndicatorsPage = React.lazy(() => import('./modules/indicators/indicators_page'));
|
||||
|
||||
const IndicatorsPage: VFC = () => (
|
||||
<SecuritySolutionPluginTemplateWrapper>
|
||||
<Suspense fallback={<div />}>
|
||||
<LazyIndicatorsPage />
|
||||
</Suspense>
|
||||
</SecuritySolutionPluginTemplateWrapper>
|
||||
);
|
||||
|
||||
/**
|
||||
* This is used here:
|
||||
* x-pack/plugins/security_solution/public/threat_intelligence/pages/threat_intelligence.tsx
|
||||
|
@ -37,13 +47,13 @@ export const createApp =
|
|||
(
|
||||
<IntlProvider>
|
||||
<SecuritySolutionContext.Provider value={securitySolutionContext}>
|
||||
<EnterpriseGuard>
|
||||
<KibanaContextProvider services={services}>
|
||||
<Suspense fallback={<div />}>
|
||||
<LazyIndicatorsPage />
|
||||
</Suspense>
|
||||
</KibanaContextProvider>
|
||||
</EnterpriseGuard>
|
||||
<KibanaContextProvider services={services}>
|
||||
<EnterpriseGuard>
|
||||
<IntegrationsGuard>
|
||||
<IndicatorsPage />
|
||||
</IntegrationsGuard>
|
||||
</EnterpriseGuard>
|
||||
</KibanaContextProvider>
|
||||
</SecuritySolutionContext.Provider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
|
|
@ -42,6 +42,7 @@ export type Services = {
|
|||
dataViews: DataViewsPublicPluginStart;
|
||||
triggersActionsUi: TriggersActionsStart;
|
||||
timelines: TimelinesUIStart;
|
||||
securityLayout: any;
|
||||
} & CoreStart;
|
||||
|
||||
export interface LicenseAware {
|
||||
|
@ -54,6 +55,7 @@ export interface SourcererDataView {
|
|||
indexPattern: SecuritySolutionDataViewBase;
|
||||
browserFields: BrowserFields;
|
||||
selectedPatterns: string[];
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -64,6 +66,12 @@ export interface SecuritySolutionPluginContext {
|
|||
* Gets the `FiltersGlobal` component for embedding a filter bar in the security solution application.
|
||||
* */
|
||||
getFiltersGlobalComponent: () => ComponentType<{ children: ReactNode }>;
|
||||
|
||||
/**
|
||||
* Gets the `PageWrapper` component for embedding a filter bar in the security solution application.
|
||||
* */
|
||||
getPageWrapper: () => ComponentType<{ children: ReactNode }>;
|
||||
|
||||
/**
|
||||
* Get the user's license to drive the Threat Intelligence plugin's visibility.
|
||||
*/
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue