mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Security Solution][Feat] Integrate Discover Tab in timeline (#160036)
## Summary
First step for https://github.com/elastic/security-team/issues/6677
Aim of this PR is embed Discover in Security Solution. Discover must be
embedded as a complete app with certain set of capabilities working. The
set capabilities that need to working are listed here :
https://github.com/elastic/security-team/issues/6673
Release notes should be based on
https://github.com/elastic/security-team/issues/6673
### ⚠️ Note
- These changes are only available in serverless mode of security
solution behind a feature-flag called `discoverInTimeline`. Adds below
options to `serverless.security.yml`:
```yaml
# Serverless security specific options
xpack.securitySolution.enableExperimental:
- discoverInTimeline
```
You can use below command to run serverless instance of security
solution :
```bash
yarn serverless-security
```
This Implements following changes for each plugin.
### Discover
1. Exports Discover App as Lazy component.
2. Ability to override Discover Services.
3. Adds a parameter `mode` which switches off/on certain options based
on the `mode`. `Mode` has possible values of `embedded` and
`standalone`. For example, `embedded` switches off Discover breadcrumb
syncing, because consuming app may not need it.
### Unified Search
1. Ability to export a Search bar with custom depedency instances.
2. For example, today Unified Search uses a singleton global
`dataService` which store global KQL filters and queries. This
customization, let consumers of unified search to pass a new instance of
`dataService`.
4. Please see below diagram for more clarity.
### Navigation
1. Ability to export a custom stateful TopNav Menu which includes:
- DataView picker
- KQL Search Bar
- TimeRange Selector
2. Currently navigation consumes an instance of unified service which
uses a global singleton `data` service.
3. This PR creates a new instance of unified search which is then passed
to navigation to get a custom instance of `TopNav` Menu.
### Security Solution
1. Imports Discover Container Component
2. Uses customization point to pass a custom query bar.
3. Implements Custom KQL Query Bar with below customizations

---------
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Davis McPhee <davis.mcphee@elastic.co>
This commit is contained in:
parent
7143dcf2c3
commit
181eb39b70
47 changed files with 767 additions and 180 deletions
|
@ -22,3 +22,8 @@ xpack.serverless.plugin.developer.projectSwitcher.currentType: 'security'
|
|||
|
||||
# Specify in telemetry the project type
|
||||
telemetry.labels.serverless: security
|
||||
|
||||
# Serverless security specific options
|
||||
xpack.securitySolution.enableExperimental:
|
||||
- discoverInTimeline
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ import { DiscoverMainProvider } from '../../services/discover_state_provider';
|
|||
import type { SearchBarCustomization, TopNavCustomization } from '../../../../customizations';
|
||||
import type { DiscoverCustomizationId } from '../../../../customizations/customization_service';
|
||||
|
||||
import { useDiscoverCustomization } from '../../../../customizations';
|
||||
setHeaderActionMenuMounter(jest.fn());
|
||||
|
||||
jest.mock('@kbn/kibana-react-plugin/public', () => ({
|
||||
|
@ -28,6 +29,9 @@ jest.mock('@kbn/kibana-react-plugin/public', () => ({
|
|||
}),
|
||||
}));
|
||||
|
||||
const MockCustomSearchBar: typeof mockDiscoverService.navigation.ui.AggregateQueryTopNavMenu =
|
||||
() => <div data-test-subj="custom-search-bar" />;
|
||||
|
||||
const mockTopNavCustomization: TopNavCustomization = {
|
||||
id: 'top_nav',
|
||||
};
|
||||
|
@ -37,24 +41,16 @@ const mockSearchBarCustomization: SearchBarCustomization = {
|
|||
CustomDataViewPicker: jest.fn(() => <div data-test-subj="custom-data-view-picker" />),
|
||||
};
|
||||
|
||||
const mockSearchBarCustomizationWithCustomSearchBar: SearchBarCustomization = {
|
||||
id: 'search_bar',
|
||||
CustomSearchBar: MockCustomSearchBar,
|
||||
};
|
||||
|
||||
let mockUseCustomizations = false;
|
||||
|
||||
jest.mock('../../../../customizations', () => ({
|
||||
...jest.requireActual('../../../../customizations'),
|
||||
useDiscoverCustomization: jest.fn((id: DiscoverCustomizationId) => {
|
||||
if (!mockUseCustomizations) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
switch (id) {
|
||||
case 'top_nav':
|
||||
return mockTopNavCustomization;
|
||||
case 'search_bar':
|
||||
return mockSearchBarCustomization;
|
||||
default:
|
||||
throw new Error(`Unknown customization id: ${id}`);
|
||||
}
|
||||
}),
|
||||
useDiscoverCustomization: jest.fn(),
|
||||
}));
|
||||
|
||||
function getProps(savePermissions = true): DiscoverTopNavProps {
|
||||
|
@ -79,6 +75,23 @@ describe('Discover topnav component', () => {
|
|||
mockTopNavCustomization.getMenuItems = undefined;
|
||||
mockUseCustomizations = false;
|
||||
jest.clearAllMocks();
|
||||
|
||||
(useDiscoverCustomization as jest.Mock).mockImplementation(
|
||||
jest.fn((id: DiscoverCustomizationId) => {
|
||||
if (!mockUseCustomizations) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
switch (id) {
|
||||
case 'top_nav':
|
||||
return mockTopNavCustomization;
|
||||
case 'search_bar':
|
||||
return mockSearchBarCustomization;
|
||||
default:
|
||||
throw new Error(`Unknown customization id: ${id}`);
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('generated config of TopNavMenu config is correct when discover save permissions are assigned', () => {
|
||||
|
@ -175,6 +188,23 @@ describe('Discover topnav component', () => {
|
|||
});
|
||||
|
||||
describe('search bar customization', () => {
|
||||
it('should render custom Search Bar', () => {
|
||||
(useDiscoverCustomization as jest.Mock).mockImplementation((id: DiscoverCustomizationId) => {
|
||||
if (id === 'search_bar') {
|
||||
return mockSearchBarCustomizationWithCustomSearchBar;
|
||||
}
|
||||
});
|
||||
|
||||
const props = getProps();
|
||||
const component = mountWithIntl(
|
||||
<DiscoverMainProvider value={props.stateContainer}>
|
||||
<DiscoverTopNav {...props} />
|
||||
</DiscoverMainProvider>
|
||||
);
|
||||
|
||||
expect(component.find({ 'data-test-subj': 'custom-search-bar' })).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should render CustomDataViewPicker', () => {
|
||||
mockUseCustomizations = true;
|
||||
const props = getProps();
|
||||
|
|
|
@ -201,8 +201,13 @@ export const DiscoverTopNav = ({
|
|||
|
||||
const searchBarCustomization = useDiscoverCustomization('search_bar');
|
||||
|
||||
const SearchBar = useMemo(
|
||||
() => searchBarCustomization?.CustomSearchBar ?? AggregateQueryTopNavMenu,
|
||||
[searchBarCustomization?.CustomSearchBar, AggregateQueryTopNavMenu]
|
||||
);
|
||||
|
||||
return (
|
||||
<AggregateQueryTopNavMenu
|
||||
<SearchBar
|
||||
appName="discover"
|
||||
config={topNavMenu}
|
||||
indexPatterns={[dataView]}
|
||||
|
|
|
@ -18,6 +18,7 @@ import { useSavedSearchAliasMatchRedirect } from '../../hooks/saved_search_alias
|
|||
import { useSavedSearchInitial } from './services/discover_state_provider';
|
||||
import { useAdHocDataViews } from './hooks/use_adhoc_data_views';
|
||||
import { useTextBasedQueryLanguage } from './hooks/use_text_based_query_language';
|
||||
import type { DiscoverDisplayMode } from '../types';
|
||||
import { addLog } from '../../utils/add_log';
|
||||
|
||||
const DiscoverLayoutMemoized = React.memo(DiscoverLayout);
|
||||
|
@ -27,10 +28,11 @@ export interface DiscoverMainProps {
|
|||
* Central state container
|
||||
*/
|
||||
stateContainer: DiscoverStateContainer;
|
||||
mode?: DiscoverDisplayMode;
|
||||
}
|
||||
|
||||
export function DiscoverMainApp(props: DiscoverMainProps) {
|
||||
const { stateContainer } = props;
|
||||
const { stateContainer, mode = 'standalone' } = props;
|
||||
const savedSearch = useSavedSearchInitial();
|
||||
const services = useDiscoverServices();
|
||||
const { chrome, docLinks, data, spaces, history } = services;
|
||||
|
@ -60,13 +62,15 @@ export function DiscoverMainApp(props: DiscoverMainProps) {
|
|||
}, [stateContainer]);
|
||||
|
||||
/**
|
||||
* SavedSearch dependend initializing
|
||||
* SavedSearch dependent initializing
|
||||
*/
|
||||
useEffect(() => {
|
||||
const pageTitleSuffix = savedSearch.id && savedSearch.title ? `: ${savedSearch.title}` : '';
|
||||
chrome.docTitle.change(`Discover${pageTitleSuffix}`);
|
||||
setBreadcrumbsTitle({ title: savedSearch.title, services });
|
||||
}, [chrome.docTitle, savedSearch.id, savedSearch.title, services]);
|
||||
if (mode === 'standalone') {
|
||||
const pageTitleSuffix = savedSearch.id && savedSearch.title ? `: ${savedSearch.title}` : '';
|
||||
chrome.docTitle.change(`Discover${pageTitleSuffix}`);
|
||||
setBreadcrumbsTitle({ title: savedSearch.title, services });
|
||||
}
|
||||
}, [mode, chrome.docTitle, savedSearch.id, savedSearch.title, services]);
|
||||
|
||||
useEffect(() => {
|
||||
addHelpMenuToAppChrome(chrome, docLinks);
|
||||
|
|
|
@ -34,6 +34,7 @@ import {
|
|||
DiscoverCustomizationProvider,
|
||||
useDiscoverCustomizationService,
|
||||
} from '../../customizations';
|
||||
import type { DiscoverDisplayMode } from '../types';
|
||||
|
||||
const DiscoverMainAppMemoized = memo(DiscoverMainApp);
|
||||
|
||||
|
@ -44,9 +45,14 @@ interface DiscoverLandingParams {
|
|||
export interface MainRouteProps {
|
||||
customizationCallbacks: CustomizationCallback[];
|
||||
isDev: boolean;
|
||||
mode?: DiscoverDisplayMode;
|
||||
}
|
||||
|
||||
export function DiscoverMainRoute({ customizationCallbacks, isDev }: MainRouteProps) {
|
||||
export function DiscoverMainRoute({
|
||||
customizationCallbacks,
|
||||
isDev,
|
||||
mode = 'standalone',
|
||||
}: MainRouteProps) {
|
||||
const history = useHistory();
|
||||
const services = useDiscoverServices();
|
||||
const {
|
||||
|
@ -146,21 +152,21 @@ export function DiscoverMainRoute({ customizationCallbacks, isDev }: MainRoutePr
|
|||
dataView: nextDataView,
|
||||
dataViewSpec: historyLocationState?.dataViewSpec,
|
||||
});
|
||||
if (mode === 'standalone') {
|
||||
if (currentSavedSearch?.id) {
|
||||
chrome.recentlyAccessed.add(
|
||||
getSavedSearchFullPathUrl(currentSavedSearch.id),
|
||||
currentSavedSearch.title ?? '',
|
||||
currentSavedSearch.id
|
||||
);
|
||||
}
|
||||
|
||||
if (currentSavedSearch?.id) {
|
||||
chrome.recentlyAccessed.add(
|
||||
getSavedSearchFullPathUrl(currentSavedSearch.id),
|
||||
currentSavedSearch.title ?? '',
|
||||
currentSavedSearch.id
|
||||
chrome.setBreadcrumbs(
|
||||
currentSavedSearch && currentSavedSearch.title
|
||||
? getSavedSearchBreadcrumbs({ id: currentSavedSearch.title, services })
|
||||
: getRootBreadcrumbs({ services })
|
||||
);
|
||||
}
|
||||
|
||||
chrome.setBreadcrumbs(
|
||||
currentSavedSearch && currentSavedSearch.title
|
||||
? getSavedSearchBreadcrumbs({ id: currentSavedSearch.title, services })
|
||||
: getRootBreadcrumbs({ services })
|
||||
);
|
||||
|
||||
setLoading(false);
|
||||
if (services.analytics) {
|
||||
const loadSavedSearchDuration = window.performance.now() - loadSavedSearchStartTime;
|
||||
|
@ -205,6 +211,7 @@ export function DiscoverMainRoute({ customizationCallbacks, isDev }: MainRoutePr
|
|||
core.theme,
|
||||
basePath,
|
||||
toastNotifications,
|
||||
mode,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -278,8 +285,10 @@ export function DiscoverMainRoute({ customizationCallbacks, isDev }: MainRoutePr
|
|||
return (
|
||||
<DiscoverCustomizationProvider value={customizationService}>
|
||||
<DiscoverMainProvider value={stateContainer}>
|
||||
<DiscoverMainAppMemoized stateContainer={stateContainer} />
|
||||
<DiscoverMainAppMemoized stateContainer={stateContainer} mode={mode} />
|
||||
</DiscoverMainProvider>
|
||||
</DiscoverCustomizationProvider>
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default DiscoverMainRoute;
|
||||
|
|
|
@ -13,3 +13,5 @@ export enum FetchStatus {
|
|||
COMPLETE = 'complete',
|
||||
ERROR = 'error',
|
||||
}
|
||||
|
||||
export type DiscoverDisplayMode = 'embedded' | 'standalone';
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
import { DiscoverServices } from '../../build_services';
|
||||
import DiscoverContainerInternal, { DiscoverContainerInternalProps } from './discover_container';
|
||||
import { ScopedHistory } from '@kbn/core-application-browser';
|
||||
import { discoverServiceMock } from '../../__mocks__/services';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
|
||||
|
||||
const mockOverrideService = {};
|
||||
const getDiscoverServicesMock = jest.fn(
|
||||
() => new Promise<DiscoverServices>((resolve) => resolve(discoverServiceMock))
|
||||
);
|
||||
|
||||
jest.mock('../../application/main', () => {
|
||||
return {
|
||||
DiscoverMainRoute: () => <></>,
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('@kbn/kibana-react-plugin/public');
|
||||
|
||||
const { history } = discoverServiceMock;
|
||||
|
||||
const customizeMock = jest.fn();
|
||||
|
||||
const TestComponent = (props: Partial<DiscoverContainerInternalProps>) => {
|
||||
return (
|
||||
<DiscoverContainerInternal
|
||||
overrideServices={props.overrideServices ?? mockOverrideService}
|
||||
customize={props.customize ?? customizeMock}
|
||||
isDev={props.isDev ?? false}
|
||||
scopedHistory={props.scopedHistory ?? (history() as ScopedHistory<unknown>)}
|
||||
getDiscoverServices={getDiscoverServicesMock}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const TEST_IDS = {
|
||||
DISCOVER_CONTAINER_INTERNAL: 'data-container-internal-wrapper',
|
||||
};
|
||||
|
||||
describe('DiscoverContainerInternal should render properly', () => {
|
||||
beforeAll(() => {
|
||||
(KibanaContextProvider as jest.Mock).mockImplementation(() => <></>);
|
||||
});
|
||||
|
||||
afterEach(() => jest.clearAllMocks());
|
||||
|
||||
it('should render', async () => {
|
||||
const { getByTestId, queryByTestId } = render(<TestComponent />);
|
||||
expect(queryByTestId(TEST_IDS.DISCOVER_CONTAINER_INTERNAL)).not.toBeInTheDocument();
|
||||
|
||||
expect(getDiscoverServicesMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(getByTestId(TEST_IDS.DISCOVER_CONTAINER_INTERNAL)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render with overrideServices', async () => {
|
||||
const overrideServices: Partial<DiscoverServices> = {
|
||||
data: {
|
||||
...dataPluginMock.createStartContract(),
|
||||
// @ts-expect-error
|
||||
_name: 'custom',
|
||||
},
|
||||
};
|
||||
render(<TestComponent overrideServices={overrideServices} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(KibanaContextProvider as jest.Mock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
services: expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
_name: 'custom',
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
{}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import type { ScopedHistory } from '@kbn/core/public';
|
||||
import { euiStyled } from '@kbn/kibana-react-plugin/common';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { DiscoverMainRoute } from '../../application/main';
|
||||
import type { DiscoverServices } from '../../build_services';
|
||||
import type { CustomizationCallback } from '../../customizations';
|
||||
import { setHeaderActionMenuMounter, setScopedHistory } from '../../kibana_services';
|
||||
import { LoadingIndicator } from '../common/loading_indicator';
|
||||
|
||||
export interface DiscoverContainerInternalProps {
|
||||
/*
|
||||
* Any override that user of this hook
|
||||
* wants discover to use. Need to keep in mind that this
|
||||
* param is only for overrides for the services that Discover
|
||||
* already consumes.
|
||||
*/
|
||||
overrideServices: Partial<DiscoverServices>;
|
||||
getDiscoverServices: () => Promise<DiscoverServices>;
|
||||
scopedHistory: ScopedHistory;
|
||||
customize: CustomizationCallback;
|
||||
isDev: boolean;
|
||||
}
|
||||
|
||||
const DiscoverContainerWrapper = euiStyled(EuiFlexGroup)`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
// override the embedded discover page height
|
||||
// to fit in the container
|
||||
.dscPage {
|
||||
height: 100%
|
||||
}
|
||||
`;
|
||||
|
||||
export const DiscoverContainerInternal = ({
|
||||
overrideServices,
|
||||
scopedHistory,
|
||||
customize,
|
||||
isDev,
|
||||
getDiscoverServices,
|
||||
}: DiscoverContainerInternalProps) => {
|
||||
const [discoverServices, setDiscoverServices] = useState<DiscoverServices | undefined>();
|
||||
const customizationCallbacks = useMemo(() => [customize], [customize]);
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
getDiscoverServices().then((svcs) => setDiscoverServices(svcs));
|
||||
}, [getDiscoverServices]);
|
||||
|
||||
useEffect(() => {
|
||||
setScopedHistory(scopedHistory);
|
||||
setHeaderActionMenuMounter(() => {});
|
||||
setInitialized(true);
|
||||
}, [scopedHistory]);
|
||||
|
||||
const services = useMemo(() => {
|
||||
if (!discoverServices) return;
|
||||
return { ...discoverServices, ...overrideServices };
|
||||
}, [discoverServices, overrideServices]);
|
||||
|
||||
if (!initialized || !services) {
|
||||
return (
|
||||
<DiscoverContainerWrapper>
|
||||
<LoadingIndicator type="spinner" />
|
||||
</DiscoverContainerWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DiscoverContainerWrapper data-test-subj="data-container-internal-wrapper">
|
||||
<EuiFlexItem>
|
||||
<KibanaContextProvider services={services}>
|
||||
<DiscoverMainRoute
|
||||
customizationCallbacks={customizationCallbacks}
|
||||
mode="embedded"
|
||||
isDev={isDev}
|
||||
/>
|
||||
</KibanaContextProvider>
|
||||
</EuiFlexItem>
|
||||
</DiscoverContainerWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default DiscoverContainerInternal;
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { withSuspense } from '@kbn/shared-ux-utility';
|
||||
import { lazy } from 'react';
|
||||
import type { DiscoverContainerInternalProps } from './discover_container';
|
||||
|
||||
export type DiscoverContainerProps = Omit<
|
||||
DiscoverContainerInternalProps,
|
||||
'isDev' | 'getDiscoverServices'
|
||||
>;
|
||||
|
||||
export const DiscoverContainerInternal = withSuspense(lazy(() => import('./discover_container')));
|
|
@ -6,9 +6,12 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { ComponentType } from 'react';
|
||||
import type { AggregateQuery } from '@kbn/es-query';
|
||||
import type { TopNavMenuProps } from '@kbn/navigation-plugin/public';
|
||||
import type { ComponentType, ReactElement } from 'react';
|
||||
|
||||
export interface SearchBarCustomization {
|
||||
id: 'search_bar';
|
||||
CustomDataViewPicker?: ComponentType;
|
||||
CustomSearchBar?: (props: TopNavMenuProps<AggregateQuery>) => ReactElement;
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ export function plugin(initializerContext: PluginInitializerContext) {
|
|||
|
||||
export type { ISearchEmbeddable, SearchInput } from './embeddable';
|
||||
export type { DiscoverStateContainer } from './application/main/services/discover_state';
|
||||
export type { DiscoverContainerProps } from './components/discover_container';
|
||||
export type {
|
||||
CustomizationCallback,
|
||||
DiscoverProfileId,
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { sharePluginMock } from '@kbn/share-plugin/public/mocks';
|
||||
import { DiscoverSetup, DiscoverStart } from '.';
|
||||
|
||||
|
@ -25,6 +26,7 @@ const createSetupContract = (): Setup => {
|
|||
const createStartContract = (): Start => {
|
||||
const startContract: Start = {
|
||||
locator: sharePluginMock.createLocator(),
|
||||
DiscoverContainer: jest.fn().mockImplementation(() => <></>),
|
||||
registerCustomizationProfile: jest.fn(),
|
||||
};
|
||||
return startContract;
|
|
@ -7,7 +7,7 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import React, { ComponentType } from 'react';
|
||||
import { BehaviorSubject, combineLatest, map } from 'rxjs';
|
||||
import {
|
||||
AppMountParameters,
|
||||
|
@ -78,6 +78,7 @@ import {
|
|||
createProfileRegistry,
|
||||
} from './customizations/profile_registry';
|
||||
import { SEARCH_EMBEDDABLE_CELL_ACTIONS_TRIGGER } from './embeddable/constants';
|
||||
import { DiscoverContainerInternal, DiscoverContainerProps } from './components/discover_container';
|
||||
|
||||
const DocViewerLegacyTable = React.lazy(
|
||||
() => import('./services/doc_views/components/doc_viewer_table/legacy')
|
||||
|
@ -162,6 +163,7 @@ export interface DiscoverStart {
|
|||
* ```
|
||||
*/
|
||||
readonly locator: undefined | DiscoverAppLocator;
|
||||
readonly DiscoverContainer: ComponentType<DiscoverContainerProps>;
|
||||
readonly registerCustomizationProfile: RegisterCustomizationProfile;
|
||||
}
|
||||
|
||||
|
@ -420,18 +422,31 @@ export class DiscoverPlugin
|
|||
// initializeServices are assigned at start and used
|
||||
// when the application/embeddable is mounted
|
||||
|
||||
const { uiActions } = plugins;
|
||||
|
||||
uiActions.registerTrigger(SEARCH_EMBEDDABLE_CELL_ACTIONS_TRIGGER);
|
||||
|
||||
const viewSavedSearchAction = new ViewSavedSearchAction(core.application);
|
||||
uiActions.addTriggerAction('CONTEXT_MENU_TRIGGER', viewSavedSearchAction);
|
||||
setUiActions(plugins.uiActions);
|
||||
|
||||
plugins.uiActions.addTriggerAction('CONTEXT_MENU_TRIGGER', viewSavedSearchAction);
|
||||
plugins.uiActions.registerTrigger(SEARCH_EMBEDDABLE_CELL_ACTIONS_TRIGGER);
|
||||
setUiActions(plugins.uiActions);
|
||||
injectTruncateStyles(core.uiSettings.get(TRUNCATE_MAX_HEIGHT));
|
||||
|
||||
const isDev = this.initializerContext.env.mode.dev;
|
||||
|
||||
const getDiscoverServicesInternal = () => {
|
||||
return this.getDiscoverServices(core, plugins);
|
||||
};
|
||||
|
||||
return {
|
||||
locator: this.locator,
|
||||
DiscoverContainer: ({ overrideServices, ...restProps }: DiscoverContainerProps) => {
|
||||
return (
|
||||
<DiscoverContainerInternal
|
||||
overrideServices={overrideServices}
|
||||
getDiscoverServices={getDiscoverServicesInternal}
|
||||
isDev={isDev}
|
||||
{...restProps}
|
||||
/>
|
||||
);
|
||||
},
|
||||
registerCustomizationProfile: createRegisterCustomizationProfile(this.profileRegistry),
|
||||
};
|
||||
}
|
||||
|
@ -442,6 +457,23 @@ export class DiscoverPlugin
|
|||
}
|
||||
}
|
||||
|
||||
private getDiscoverServices = async (core: CoreStart, plugins: DiscoverStartPlugins) => {
|
||||
const { locator, contextLocator, singleDocLocator } = await getProfileAwareLocators({
|
||||
locator: this.locator!,
|
||||
contextLocator: this.contextLocator!,
|
||||
singleDocLocator: this.singleDocLocator!,
|
||||
});
|
||||
|
||||
return buildServices(
|
||||
core,
|
||||
plugins,
|
||||
this.initializerContext,
|
||||
locator,
|
||||
contextLocator,
|
||||
singleDocLocator
|
||||
);
|
||||
};
|
||||
|
||||
private registerEmbeddable(core: CoreSetup<DiscoverStartPlugins>, plugins: DiscoverSetupPlugins) {
|
||||
const getStartServices = async () => {
|
||||
const [coreStart, deps] = await core.getStartServices();
|
||||
|
@ -451,25 +483,12 @@ export class DiscoverPlugin
|
|||
};
|
||||
};
|
||||
|
||||
const getDiscoverServices = async () => {
|
||||
const [coreStart, discoverStartPlugins] = await core.getStartServices();
|
||||
const { locator, contextLocator, singleDocLocator } = await getProfileAwareLocators({
|
||||
locator: this.locator!,
|
||||
contextLocator: this.contextLocator!,
|
||||
singleDocLocator: this.singleDocLocator!,
|
||||
});
|
||||
|
||||
return buildServices(
|
||||
coreStart,
|
||||
discoverStartPlugins,
|
||||
this.initializerContext,
|
||||
locator,
|
||||
contextLocator,
|
||||
singleDocLocator
|
||||
);
|
||||
const getDiscoverServicesInternal = async () => {
|
||||
const [coreStart, deps] = await core.getStartServices();
|
||||
return this.getDiscoverServices(coreStart, deps);
|
||||
};
|
||||
|
||||
const factory = new SearchEmbeddableFactory(getStartServices, getDiscoverServices);
|
||||
const factory = new SearchEmbeddableFactory(getStartServices, getDiscoverServicesInternal);
|
||||
plugins.embeddable.registerEmbeddableFactory(factory.type, factory);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,13 @@
|
|||
"compilerOptions": {
|
||||
"outDir": "target/types"
|
||||
},
|
||||
"include": ["common/**/*", "public/**/*", "server/**/*", "../../../typings/**/*", ".storybook/**/*"],
|
||||
"include": [
|
||||
"common/**/*",
|
||||
"public/**/*",
|
||||
"server/**/*",
|
||||
"../../../typings/**/*",
|
||||
".storybook/**/*"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/core",
|
||||
"@kbn/charts-plugin",
|
||||
|
@ -55,9 +61,11 @@
|
|||
"@kbn/unified-field-list",
|
||||
"@kbn/core-saved-objects-api-server",
|
||||
"@kbn/cell-actions",
|
||||
"@kbn/discover-utils",
|
||||
"@kbn/shared-ux-utility",
|
||||
"@kbn/core-application-browser",
|
||||
"@kbn/discover-utils"
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
"target/**/*"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ const createStartContract = (): jest.Mocked<Start> => {
|
|||
const startContract = {
|
||||
ui: {
|
||||
TopNavMenu: jest.fn(),
|
||||
createTopNavWithCustomContext: jest.fn().mockImplementation(() => jest.fn()),
|
||||
AggregateQueryTopNavMenu: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
|
|
@ -7,12 +7,14 @@
|
|||
*/
|
||||
|
||||
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '@kbn/core/public';
|
||||
import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
|
||||
import {
|
||||
NavigationPublicPluginSetup,
|
||||
NavigationPublicPluginStart,
|
||||
NavigationPluginStartDependencies,
|
||||
} from './types';
|
||||
import { TopNavMenuExtensionsRegistry, createTopNav } from './top_nav_menu';
|
||||
import { RegisteredTopNavMenuData } from './top_nav_menu/top_nav_menu_data';
|
||||
|
||||
export class NavigationPublicPlugin
|
||||
implements Plugin<NavigationPublicPluginSetup, NavigationPublicPluginStart>
|
||||
|
@ -36,10 +38,31 @@ export class NavigationPublicPlugin
|
|||
): NavigationPublicPluginStart {
|
||||
const extensions = this.topNavMenuExtensionsRegistry.getAll();
|
||||
|
||||
/*
|
||||
*
|
||||
* This helps clients of navigation to create
|
||||
* a TopNav Search Bar which does not uses global unifiedSearch/data/query service
|
||||
*
|
||||
* Useful in creating multiple stateful SearchBar in the same app without affecting
|
||||
* global filters
|
||||
*
|
||||
* */
|
||||
const createCustomTopNav = (
|
||||
/*
|
||||
* Custom instance of unified search if it needs to be overridden
|
||||
*
|
||||
* */
|
||||
customUnifiedSearch?: UnifiedSearchPublicPluginStart,
|
||||
customExtensions?: RegisteredTopNavMenuData[]
|
||||
) => {
|
||||
return createTopNav(customUnifiedSearch ?? unifiedSearch, customExtensions ?? extensions);
|
||||
};
|
||||
|
||||
return {
|
||||
ui: {
|
||||
TopNavMenu: createTopNav(unifiedSearch, extensions),
|
||||
AggregateQueryTopNavMenu: createTopNav(unifiedSearch, extensions),
|
||||
createTopNavWithCustomContext: createCustomTopNav,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -8,7 +8,8 @@
|
|||
|
||||
import { AggregateQuery, Query } from '@kbn/es-query';
|
||||
import { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
|
||||
import { TopNavMenuProps, TopNavMenuExtensionsRegistrySetup } from './top_nav_menu';
|
||||
import { TopNavMenuProps, TopNavMenuExtensionsRegistrySetup, createTopNav } from './top_nav_menu';
|
||||
import { RegisteredTopNavMenuData } from './top_nav_menu/top_nav_menu_data';
|
||||
|
||||
export interface NavigationPublicPluginSetup {
|
||||
registerMenuItem: TopNavMenuExtensionsRegistrySetup['register'];
|
||||
|
@ -18,6 +19,10 @@ export interface NavigationPublicPluginStart {
|
|||
ui: {
|
||||
TopNavMenu: (props: TopNavMenuProps<Query>) => React.ReactElement;
|
||||
AggregateQueryTopNavMenu: (props: TopNavMenuProps<AggregateQuery>) => React.ReactElement;
|
||||
createTopNavWithCustomContext: (
|
||||
customUnifiedSearch?: UnifiedSearchPublicPluginStart,
|
||||
customExtensions?: RegisteredTopNavMenuData[]
|
||||
) => ReturnType<typeof createTopNav>;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ export type { DataViewPickerProps } from './dataview_picker';
|
|||
export type { ApplyGlobalFilterActionContext } from './actions';
|
||||
export { ACTION_GLOBAL_APPLY_FILTER, UPDATE_FILTER_REFERENCES_ACTION } from './actions';
|
||||
export { UPDATE_FILTER_REFERENCES_TRIGGER } from './triggers';
|
||||
export { createSearchBar } from './search_bar/create_search_bar';
|
||||
|
||||
/*
|
||||
* Autocomplete query suggestions:
|
||||
|
|
|
@ -34,6 +34,7 @@ const createStartContract = (): Start => {
|
|||
autocomplete: autocompleteStartMock,
|
||||
ui: {
|
||||
IndexPatternSelect: jest.fn(),
|
||||
getCustomSearchBar: jest.fn(),
|
||||
SearchBar: jest.fn().mockReturnValue(null),
|
||||
AggregateQuerySearchBar: jest.fn().mockReturnValue(null),
|
||||
FiltersBuilderLazy: jest.fn(),
|
||||
|
|
|
@ -20,6 +20,7 @@ import type {
|
|||
UnifiedSearchSetupDependencies,
|
||||
UnifiedSearchPluginSetup,
|
||||
UnifiedSearchPublicPluginStart,
|
||||
UnifiedSearchPublicPluginStartUi,
|
||||
} from './types';
|
||||
import { createFilterAction } from './actions/apply_filter_action';
|
||||
import { createUpdateFilterReferencesAction } from './actions/update_filter_references_action';
|
||||
|
@ -73,16 +74,28 @@ export class UnifiedSearchPublicPlugin
|
|||
setIndexPatterns(dataViews);
|
||||
const autocompleteStart = this.autocomplete.start();
|
||||
|
||||
const SearchBar = createSearchBar({
|
||||
core,
|
||||
data,
|
||||
storage: this.storage,
|
||||
usageCollection: this.usageCollection,
|
||||
isScreenshotMode: Boolean(screenshotMode?.isScreenshotMode()),
|
||||
unifiedSearch: {
|
||||
autocomplete: autocompleteStart,
|
||||
},
|
||||
});
|
||||
/*
|
||||
*
|
||||
* unifiedsearch uses global data service to create stateful search bar.
|
||||
* This function helps in creating a search bar with different instances of data service
|
||||
* so that it can be easy to use multiple stateful searchbars in the single applications
|
||||
*
|
||||
* */
|
||||
const getCustomSearchBar: UnifiedSearchPublicPluginStartUi['getCustomSearchBar'] = (
|
||||
customDataService
|
||||
) =>
|
||||
createSearchBar({
|
||||
core,
|
||||
data: customDataService ?? data,
|
||||
storage: this.storage,
|
||||
usageCollection: this.usageCollection,
|
||||
isScreenshotMode: Boolean(screenshotMode?.isScreenshotMode()),
|
||||
unifiedSearch: {
|
||||
autocomplete: autocompleteStart,
|
||||
},
|
||||
});
|
||||
|
||||
const SearchBar = getCustomSearchBar();
|
||||
|
||||
uiActions.attachAction(APPLY_FILTER_TRIGGER, ACTION_GLOBAL_APPLY_FILTER);
|
||||
|
||||
|
@ -92,6 +105,7 @@ export class UnifiedSearchPublicPlugin
|
|||
ui: {
|
||||
IndexPatternSelect: createIndexPatternSelect(dataViews),
|
||||
SearchBar,
|
||||
getCustomSearchBar,
|
||||
AggregateQuerySearchBar: SearchBar,
|
||||
FiltersBuilderLazy,
|
||||
},
|
||||
|
|
|
@ -23,7 +23,7 @@ import { useSavedQuery } from './lib/use_saved_query';
|
|||
import { useQueryStringManager } from './lib/use_query_string_manager';
|
||||
import type { UnifiedSearchPublicPluginStart } from '../types';
|
||||
|
||||
interface StatefulSearchBarDeps {
|
||||
export interface StatefulSearchBarDeps {
|
||||
core: CoreStart;
|
||||
data: DataPublicPluginStart;
|
||||
storage: IStorageWrapper;
|
||||
|
|
|
@ -20,6 +20,7 @@ import { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management
|
|||
import { AutocompleteSetup, AutocompleteStart } from './autocomplete';
|
||||
import type { IndexPatternSelectProps, StatefulSearchBarProps } from '.';
|
||||
import type { FiltersBuilderProps } from './filters_builder/filters_builder';
|
||||
import { StatefulSearchBarDeps } from './search_bar/create_search_bar';
|
||||
|
||||
export interface UnifiedSearchSetupDependencies {
|
||||
uiActions: UiActionsSetup;
|
||||
|
@ -39,15 +40,18 @@ export interface UnifiedSearchStartDependencies {
|
|||
screenshotMode?: ScreenshotModePluginStart;
|
||||
}
|
||||
|
||||
type AggQuerySearchBarComp = <QT extends Query | AggregateQuery = Query>(
|
||||
props: StatefulSearchBarProps<QT>
|
||||
) => React.ReactElement;
|
||||
|
||||
/**
|
||||
* Unified search plugin prewired UI components
|
||||
*/
|
||||
export interface UnifiedSearchPublicPluginStartUi {
|
||||
IndexPatternSelect: React.ComponentType<IndexPatternSelectProps>;
|
||||
getCustomSearchBar: (customDataService?: StatefulSearchBarDeps['data']) => AggQuerySearchBarComp;
|
||||
SearchBar: (props: StatefulSearchBarProps<Query>) => React.ReactElement;
|
||||
AggregateQuerySearchBar: <QT extends Query | AggregateQuery = Query>(
|
||||
props: StatefulSearchBarProps<QT>
|
||||
) => React.ReactElement;
|
||||
AggregateQuerySearchBar: AggQuerySearchBarComp;
|
||||
FiltersBuilderLazy: React.ComponentType<FiltersBuilderProps>;
|
||||
}
|
||||
|
||||
|
|
|
@ -111,6 +111,12 @@ export const allowedExperimentalValues = Object.freeze({
|
|||
* Enables experimental Entity Analytics HTTP endpoints
|
||||
*/
|
||||
riskScoringRoutesEnabled: false,
|
||||
/*
|
||||
*
|
||||
* Enables Discover embedded within timeline
|
||||
*
|
||||
* */
|
||||
discoverInTimeline: true,
|
||||
});
|
||||
|
||||
type ExperimentalConfigKeys = Array<keyof ExperimentalFeatures>;
|
||||
|
|
|
@ -37,6 +37,7 @@ export enum TimelineTabs {
|
|||
eql = 'eql',
|
||||
session = 'session',
|
||||
securityAssistant = 'securityAssistant',
|
||||
discover = 'discover',
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
USER_RISK_PREVIEW_TABLE_ROWS,
|
||||
RISK_PREVIEW_ERROR,
|
||||
RISK_PREVIEW_ERROR_BUTTON,
|
||||
LOCAL_QUERY_BAR_SELECTOR,
|
||||
} from '../../screens/entity_analytics_management';
|
||||
|
||||
import { login, visit, visitWithoutDateRange } from '../../tasks/login';
|
||||
|
@ -52,7 +53,7 @@ describe('Entity analytics management page', () => {
|
|||
cy.get(HOST_RISK_PREVIEW_TABLE_ROWS).should('have.length', 5);
|
||||
cy.get(USER_RISK_PREVIEW_TABLE_ROWS).should('have.length', 5);
|
||||
|
||||
updateDateRangeInLocalDatePickers(START_DATE, END_DATE);
|
||||
updateDateRangeInLocalDatePickers(LOCAL_QUERY_BAR_SELECTOR, START_DATE, END_DATE);
|
||||
|
||||
cy.get(HOST_RISK_PREVIEW_TABLE).contains('No items found');
|
||||
cy.get(USER_RISK_PREVIEW_TABLE).contains('No items found');
|
||||
|
@ -63,7 +64,7 @@ describe('Entity analytics management page', () => {
|
|||
cy.get(USER_RISK_PREVIEW_TABLE_ROWS).should('have.length', 5);
|
||||
|
||||
fillLocalSearchBar('host.name: "test-host1"');
|
||||
submitLocalSearch();
|
||||
submitLocalSearch(LOCAL_QUERY_BAR_SELECTOR);
|
||||
|
||||
cy.get(HOST_RISK_PREVIEW_TABLE_ROWS).should('have.length', 1);
|
||||
cy.get(HOST_RISK_PREVIEW_TABLE_ROWS).contains('test-host1');
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { getDataTestSubjectSelector } from '../helpers/common';
|
||||
import { GLOBAL_FILTERS_CONTAINER } from './date_picker';
|
||||
|
||||
export const ADD_EXCEPTION_BTN = '[data-test-subj="add-exception-menu-item"]';
|
||||
|
||||
|
@ -65,7 +66,7 @@ export const MANAGE_ALERT_DETECTION_RULES_BTN = '[data-test-subj="manage-alert-d
|
|||
|
||||
export const MARK_ALERT_ACKNOWLEDGED_BTN = '[data-test-subj="acknowledged-alert-status"]';
|
||||
|
||||
export const ALERTS_REFRESH_BTN = '[data-test-subj="querySubmitButton"]';
|
||||
export const ALERTS_REFRESH_BTN = `${GLOBAL_FILTERS_CONTAINER} [data-test-subj="querySubmitButton"]`;
|
||||
|
||||
export const ALERTS_HISTOGRAM_PANEL_LOADER = '[data-test-subj="loadingPanelAlertsHistogram"]';
|
||||
|
||||
|
|
|
@ -9,7 +9,9 @@ export const DATE_PICKER_ABSOLUTE_INPUT = '[data-test-subj="superDatePickerAbsol
|
|||
|
||||
export const LOCAL_DATE_PICKER_APPLY_BUTTON = 'button[data-test-subj="querySubmitButton"]';
|
||||
|
||||
export const DATE_PICKER_APPLY_BUTTON = `[data-test-subj="globalDatePicker"] ${LOCAL_DATE_PICKER_APPLY_BUTTON}`;
|
||||
export const GLOBAL_FILTERS_CONTAINER = `[data-test-subj="filters-global-container"]`;
|
||||
|
||||
export const DATE_PICKER_APPLY_BUTTON = `${GLOBAL_FILTERS_CONTAINER} ${LOCAL_DATE_PICKER_APPLY_BUTTON}`;
|
||||
|
||||
export const LOCAL_DATE_PICKER_APPLY_BUTTON_TIMELINE =
|
||||
'button[data-test-subj="superDatePickerApplyTimeButton"]';
|
||||
|
@ -25,7 +27,9 @@ export const DATE_PICKER_NOW_BUTTON = '[data-test-subj="superDatePickerNowButton
|
|||
export const LOCAL_DATE_PICKER_END_DATE_POPOVER_BUTTON =
|
||||
'[data-test-subj="superDatePickerendDatePopoverButton"]';
|
||||
|
||||
export const DATE_PICKER_END_DATE_POPOVER_BUTTON = `[data-test-subj="globalDatePicker"] ${LOCAL_DATE_PICKER_END_DATE_POPOVER_BUTTON}`;
|
||||
export const DATE_PICKER_END_DATE_POPOVER_BUTTON = `${GLOBAL_FILTERS_CONTAINER} ${LOCAL_DATE_PICKER_END_DATE_POPOVER_BUTTON}`;
|
||||
|
||||
export const DATE_PICKER_CONTAINER = `${GLOBAL_FILTERS_CONTAINER} .euiSuperDatePicker`;
|
||||
|
||||
export const DATE_PICKER_END_DATE_POPOVER_BUTTON_TIMELINE =
|
||||
'[data-test-subj="timeline-date-picker-container"] [data-test-subj="superDatePickerendDatePopoverButton"]';
|
||||
|
@ -33,11 +37,12 @@ export const DATE_PICKER_END_DATE_POPOVER_BUTTON_TIMELINE =
|
|||
export const LOCAL_DATE_PICKER_START_DATE_POPOVER_BUTTON =
|
||||
'button[data-test-subj="superDatePickerstartDatePopoverButton"]';
|
||||
|
||||
export const DATE_PICKER_START_DATE_POPOVER_BUTTON = `div[data-test-subj="globalDatePicker"] ${LOCAL_DATE_PICKER_START_DATE_POPOVER_BUTTON}`;
|
||||
export const DATE_PICKER_START_DATE_POPOVER_BUTTON = `${GLOBAL_FILTERS_CONTAINER} ${LOCAL_DATE_PICKER_START_DATE_POPOVER_BUTTON}`;
|
||||
|
||||
export const GLOBAL_FILTERS_CONTAINER = '[data-test-subj="globalDatePicker"]';
|
||||
export const SHOW_DATES_BUTTON = `${GLOBAL_FILTERS_CONTAINER} [data-test-subj="superDatePickerShowDatesButton"]`;
|
||||
|
||||
export const SHOW_DATES_BUTTON = '[data-test-subj="superDatePickerShowDatesButton"]';
|
||||
export const GET_LOCAL_SHOW_DATES_BUTTON = (localQueryBarSelector: string) =>
|
||||
`${localQueryBarSelector} [data-test-subj="superDatePickerShowDatesButton"]`;
|
||||
|
||||
export const DATE_PICKER_SHOW_DATE_POPOVER_BUTTON = `${GLOBAL_FILTERS_CONTAINER} ${SHOW_DATES_BUTTON}`;
|
||||
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { getDataTestSubjectSelector } from '../helpers/common';
|
||||
|
||||
export const PAGE_TITLE = '[data-test-subj="entityAnalyticsManagmentPageTitle"]';
|
||||
|
||||
export const HOST_RISK_PREVIEW_TABLE = '[data-test-subj="host-risk-preview-table"]';
|
||||
|
@ -18,3 +20,5 @@ export const USER_RISK_PREVIEW_TABLE_ROWS = '[data-test-subj="user-risk-preview-
|
|||
export const RISK_PREVIEW_ERROR = '[data-test-subj="risk-preview-error"]';
|
||||
|
||||
export const RISK_PREVIEW_ERROR_BUTTON = '[data-test-subj="risk-preview-error-button"]';
|
||||
|
||||
export const LOCAL_QUERY_BAR_SELECTOR = getDataTestSubjectSelector('risk-score-preview-search-bar');
|
||||
|
|
|
@ -5,12 +5,15 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export const GLOBAL_KQL_WRAPPER = '[data-test-subj="filters-global-container"]';
|
||||
|
||||
export const GLOBAL_SEARCH_BAR_ADD_FILTER =
|
||||
'[data-test-subj="globalDatePicker"] [data-test-subj="addFilter"]';
|
||||
|
||||
export const LOCAL_SEACH_BAR_SUBMMIT_BUTTON = '[data-test-subj="querySubmitButton"]';
|
||||
export const GLOBAL_SEARCH_BAR_SUBMIT_BUTTON = `${GLOBAL_KQL_WRAPPER} [data-test-subj="querySubmitButton"]`;
|
||||
|
||||
export const GLOBAL_SEARCH_BAR_SUBMIT_BUTTON = `[data-test-subj="globalDatePicker"] ${LOCAL_SEACH_BAR_SUBMMIT_BUTTON}`;
|
||||
export const GET_LOCAL_SEARCH_BAR_SUBMIT_BUTTON = (localSearchBarSelector: string) =>
|
||||
`${localSearchBarSelector ?? ''} [data-test-subj="querySubmitButton"]`;
|
||||
|
||||
export const ADD_FILTER_FORM_FIELD_INPUT =
|
||||
'[data-test-subj="filterFieldSuggestionList"] input[data-test-subj="comboBoxSearchInput"]';
|
||||
|
@ -36,8 +39,6 @@ export const GLOBAL_SEARCH_BAR_FILTER_ITEM_DELETE = '#popoverFor_filter0 button[
|
|||
|
||||
export const GLOBAL_SEARCH_BAR_PINNED_FILTER = '.globalFilterItem-isPinned';
|
||||
|
||||
export const GLOBAL_KQL_WRAPPER = '[data-test-subj="filters-global-container"]';
|
||||
|
||||
export const LOCAL_KQL_INPUT = `[data-test-subj="unifiedQueryInput"] textarea`;
|
||||
|
||||
export const GLOBAL_KQL_INPUT = `[data-test-subj="filters-global-container"] ${LOCAL_KQL_INPUT}`;
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { GLOBAL_KQL_WRAPPER } from './search_bar';
|
||||
|
||||
// main links
|
||||
export const DASHBOARDS = '[data-test-subj="solutionSideNavItemLink-dashboards"]';
|
||||
export const DASHBOARDS_PANEL_BTN = '[data-test-subj="solutionSideNavItemButton-dashboards"]';
|
||||
|
@ -68,9 +70,9 @@ export const EXCEPTIONS = '[data-test-subj="solutionSideNavPanelLink-exceptions"
|
|||
// other
|
||||
export const BREADCRUMBS = '[data-test-subj="breadcrumbs"] a';
|
||||
|
||||
export const KQL_INPUT = '[data-test-subj="queryInput"]';
|
||||
export const KQL_INPUT = `${GLOBAL_KQL_WRAPPER} [data-test-subj="queryInput"]`;
|
||||
|
||||
export const REFRESH_BUTTON = '[data-test-subj="querySubmitButton"]';
|
||||
export const REFRESH_BUTTON = `${GLOBAL_KQL_WRAPPER} [data-test-subj="querySubmitButton"]`;
|
||||
|
||||
export const LOADING_INDICATOR = '[data-test-subj="globalLoadingIndicator"]';
|
||||
export const LOADING_INDICATOR_HIDDEN = '[data-test-subj="globalLoadingIndicator-hidden"]';
|
||||
|
|
|
@ -9,7 +9,8 @@ import type { TimelineFilter } from '../objects/timeline';
|
|||
|
||||
export const ADD_NOTE_BUTTON = '[data-test-subj="add-note"]';
|
||||
|
||||
export const ADD_FILTER = '[data-test-subj="timeline"] [data-test-subj="addFilter"]';
|
||||
export const ADD_FILTER =
|
||||
'[data-test-subj="timeline-search-or-filter"] [data-test-subj="addFilter"]';
|
||||
|
||||
export const ATTACH_TIMELINE_TO_CASE_BUTTON = '[data-test-subj="attach-timeline-case-button"]';
|
||||
|
||||
|
|
|
@ -13,7 +13,6 @@ import {
|
|||
DATE_PICKER_END_DATE_POPOVER_BUTTON,
|
||||
DATE_PICKER_END_DATE_POPOVER_BUTTON_TIMELINE,
|
||||
DATE_PICKER_START_DATE_POPOVER_BUTTON,
|
||||
GLOBAL_FILTERS_CONTAINER,
|
||||
SHOW_DATES_BUTTON,
|
||||
DATE_PICKER_START_DATE_POPOVER_BUTTON_TIMELINE,
|
||||
DATE_PICKER_SHOW_DATE_POPOVER_BUTTON,
|
||||
|
@ -21,6 +20,8 @@ import {
|
|||
DATE_PICKER_NOW_BUTTON,
|
||||
LOCAL_DATE_PICKER_APPLY_BUTTON,
|
||||
LOCAL_DATE_PICKER_END_DATE_POPOVER_BUTTON,
|
||||
DATE_PICKER_CONTAINER,
|
||||
GET_LOCAL_SHOW_DATES_BUTTON,
|
||||
} from '../screens/date_picker';
|
||||
|
||||
export const setEndDate = (date: string) => {
|
||||
|
@ -42,8 +43,7 @@ export const setEndDateNow = () => {
|
|||
};
|
||||
|
||||
export const setStartDate = (date: string) => {
|
||||
cy.get(GLOBAL_FILTERS_CONTAINER);
|
||||
cy.get('.euiSuperDatePicker');
|
||||
cy.get(DATE_PICKER_CONTAINER).should('be.visible');
|
||||
cy.get('body').then(($container) => {
|
||||
if ($container.find(SHOW_DATES_BUTTON).length > 0) {
|
||||
cy.get(DATE_PICKER_SHOW_DATE_POPOVER_BUTTON).click({ force: true });
|
||||
|
@ -101,8 +101,12 @@ export const updateTimelineDates = () => {
|
|||
cy.get(DATE_PICKER_APPLY_BUTTON_TIMELINE).first().should('not.have.text', 'Updating');
|
||||
};
|
||||
|
||||
export const updateDateRangeInLocalDatePickers = (startDate: string, endDate: string) => {
|
||||
cy.get(SHOW_DATES_BUTTON).click();
|
||||
export const updateDateRangeInLocalDatePickers = (
|
||||
localQueryBarSelector: string,
|
||||
startDate: string,
|
||||
endDate: string
|
||||
) => {
|
||||
cy.get(GET_LOCAL_SHOW_DATES_BUTTON(localQueryBarSelector)).click();
|
||||
cy.get(DATE_PICKER_ABSOLUTE_TAB).first().click();
|
||||
|
||||
cy.get(DATE_PICKER_ABSOLUTE_INPUT).click();
|
||||
|
|
|
@ -18,7 +18,7 @@ import {
|
|||
ADD_FILTER_FORM_FILTER_VALUE_INPUT,
|
||||
GLOBAL_KQL_INPUT,
|
||||
LOCAL_KQL_INPUT,
|
||||
LOCAL_SEACH_BAR_SUBMMIT_BUTTON,
|
||||
GET_LOCAL_SEARCH_BAR_SUBMIT_BUTTON,
|
||||
} from '../screens/search_bar';
|
||||
|
||||
export const openAddFilterPopover = () => {
|
||||
|
@ -72,6 +72,6 @@ export const fillLocalSearchBar = (query: string) => {
|
|||
cy.get(LOCAL_KQL_INPUT).type(query);
|
||||
};
|
||||
|
||||
export const submitLocalSearch = () => {
|
||||
cy.get(LOCAL_SEACH_BAR_SUBMMIT_BUTTON).click();
|
||||
export const submitLocalSearch = (localSearchBarSelector: string) => {
|
||||
cy.get(GET_LOCAL_SEARCH_BAR_SUBMIT_BUTTON(localSearchBarSelector)).click();
|
||||
};
|
||||
|
|
|
@ -6,7 +6,10 @@
|
|||
"id": "securitySolution",
|
||||
"server": true,
|
||||
"browser": true,
|
||||
"configPath": ["xpack", "securitySolution"],
|
||||
"configPath": [
|
||||
"xpack",
|
||||
"securitySolution"
|
||||
],
|
||||
"requiredPlugins": [
|
||||
"actions",
|
||||
"alerting",
|
||||
|
@ -29,6 +32,7 @@
|
|||
"lens",
|
||||
"licensing",
|
||||
"maps",
|
||||
"navigation",
|
||||
"ruleRegistry",
|
||||
"sessionView",
|
||||
"spaces",
|
||||
|
@ -40,8 +44,11 @@
|
|||
"unifiedSearch",
|
||||
"files",
|
||||
"controls",
|
||||
"dataViewEditor",
|
||||
"savedObjectsManagement",
|
||||
"stackConnectors"
|
||||
"stackConnectors",
|
||||
"discover",
|
||||
"notifications"
|
||||
],
|
||||
"optionalPlugins": [
|
||||
"cloudExperiments",
|
||||
|
@ -69,6 +76,8 @@
|
|||
"unifiedSearch",
|
||||
"cloudChat"
|
||||
],
|
||||
"extraPublicDirs": ["common"]
|
||||
"extraPublicDirs": [
|
||||
"common"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,8 @@
|
|||
import React from 'react';
|
||||
import type { RecursivePartial } from '@elastic/eui/src/components/common';
|
||||
import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks';
|
||||
import { navigationPluginMock } from '@kbn/navigation-plugin/public/mocks';
|
||||
import { discoverPluginMock } from '@kbn/discover-plugin/public/mocks';
|
||||
import { coreMock, themeServiceMock } from '@kbn/core/public/mocks';
|
||||
import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public';
|
||||
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
|
||||
|
@ -102,11 +104,14 @@ export const createStartServicesMock = (
|
|||
const { storage } = createSecuritySolutionStorageMock();
|
||||
const apm = mockApm();
|
||||
const data = dataPluginMock.createStartContract();
|
||||
const customDataService = dataPluginMock.createStartContract();
|
||||
const security = securityMock.createSetup();
|
||||
const urlService = new MockUrlService();
|
||||
const locator = urlService.locators.create(new MlLocatorDefinition());
|
||||
const fleet = fleetMock.createStartMock();
|
||||
const unifiedSearch = unifiedSearchPluginMock.createStartContract();
|
||||
const navigation = navigationPluginMock.createStartContract();
|
||||
const discover = discoverPluginMock.createStartContract();
|
||||
const cases = mockCasesContract();
|
||||
const dataViewServiceMock = dataViewPluginMocks.createStartContract();
|
||||
cases.helpers.getUICapabilities.mockReturnValue(noCasesPermissions());
|
||||
|
@ -119,6 +124,8 @@ export const createStartServicesMock = (
|
|||
apm,
|
||||
cases,
|
||||
unifiedSearch,
|
||||
navigation,
|
||||
discover,
|
||||
dataViews: dataViewServiceMock,
|
||||
data: {
|
||||
...data,
|
||||
|
@ -193,6 +200,7 @@ export const createStartServicesMock = (
|
|||
guidedOnboarding,
|
||||
isSidebarEnabled$: of(true),
|
||||
upselling: new UpsellingService(),
|
||||
customDataService,
|
||||
} as unknown as StartServices;
|
||||
};
|
||||
|
||||
|
|
|
@ -156,7 +156,7 @@ export const RiskScorePreviewSection = () => {
|
|||
<EuiSpacer size={'s'} />
|
||||
<EuiText>{i18n.PREVIEW_DESCRIPTION}</EuiText>
|
||||
<EuiSpacer />
|
||||
<EuiFormRow fullWidth>
|
||||
<EuiFormRow fullWidth data-test-subj="risk-score-preview-search-bar">
|
||||
{indexPattern && (
|
||||
<SearchBar
|
||||
appName="siem"
|
||||
|
|
|
@ -171,6 +171,8 @@ export const getEndpointDetectionAlertsQueryForAgentId = (endpointAgentId: strin
|
|||
};
|
||||
|
||||
export const changeAlertsFilter = (text: string) => {
|
||||
cy.getByTestSubj('queryInput').click().type(text);
|
||||
cy.getByTestSubj('querySubmitButton').click();
|
||||
cy.getByTestSubj('filters-global-container').within(() => {
|
||||
cy.getByTestSubj('queryInput').click().type(text);
|
||||
cy.getByTestSubj('querySubmitButton').click();
|
||||
});
|
||||
};
|
||||
|
|
|
@ -16,6 +16,8 @@ import type {
|
|||
PluginInitializerContext,
|
||||
Plugin as IPlugin,
|
||||
} from '@kbn/core/public';
|
||||
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import { FilterManager, NowProvider, QueryService } from '@kbn/data-plugin/public';
|
||||
import { DEFAULT_APP_CATEGORIES, AppNavLinkStatus } from '@kbn/core/public';
|
||||
import { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
import type {
|
||||
|
@ -79,6 +81,8 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
|||
private telemetry: TelemetryService;
|
||||
|
||||
readonly experimentalFeatures: ExperimentalFeatures;
|
||||
private queryService: QueryService = new QueryService();
|
||||
private nowProvider: NowProvider = new NowProvider();
|
||||
|
||||
constructor(private readonly initializerContext: PluginInitializerContext) {
|
||||
this.config = this.initializerContext.config.get<SecuritySolutionUiConfigType>();
|
||||
|
@ -90,6 +94,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
|||
this.prebuiltRulesPackageVersion = this.config.prebuiltRulesPackageVersion;
|
||||
this.contract = new PluginContract();
|
||||
this.telemetry = new TelemetryService();
|
||||
this.storage = new Storage(window.localStorage);
|
||||
}
|
||||
private appUpdater$ = new Subject<AppUpdater>();
|
||||
|
||||
|
@ -124,6 +129,12 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
|||
};
|
||||
this.telemetry.setup({ analytics: core.analytics }, telemetryContext);
|
||||
|
||||
this.queryService?.setup({
|
||||
uiSettings: core.uiSettings,
|
||||
storage: this.storage,
|
||||
nowProvider: this.nowProvider,
|
||||
});
|
||||
|
||||
if (plugins.home) {
|
||||
plugins.home.featureCatalogue.registerSolution({
|
||||
id: APP_ID,
|
||||
|
@ -150,6 +161,23 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
|||
|
||||
const { savedObjectsTaggingOss, ...startPlugins } = startPluginsDeps;
|
||||
|
||||
const query = this.queryService.start({
|
||||
uiSettings: core.uiSettings,
|
||||
storage: this.storage,
|
||||
http: core.http,
|
||||
});
|
||||
|
||||
const filterManager = new FilterManager(core.uiSettings);
|
||||
|
||||
// used for creating a custom stateful KQL Query Bar
|
||||
const customDataService: DataPublicPluginStart = {
|
||||
...startPlugins.data,
|
||||
query: {
|
||||
...query,
|
||||
filterManager,
|
||||
},
|
||||
};
|
||||
|
||||
const services: StartServices = {
|
||||
...coreStart,
|
||||
...startPlugins,
|
||||
|
@ -165,6 +193,8 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
|||
},
|
||||
savedObjectsManagement: startPluginsDeps.savedObjectsManagement,
|
||||
telemetry: this.telemetry.start(),
|
||||
discoverFilterManager: filterManager,
|
||||
customDataService,
|
||||
};
|
||||
return services;
|
||||
};
|
||||
|
@ -295,6 +325,7 @@ export class Plugin implements IPlugin<PluginSetup, PluginStart, SetupPlugins, S
|
|||
}
|
||||
|
||||
public stop() {
|
||||
this.queryService.stop();
|
||||
licenseService.stop();
|
||||
return this.contract.getStopContract();
|
||||
}
|
||||
|
|
|
@ -45,6 +45,7 @@ describe('Pane', () => {
|
|||
<Pane timelineId={TimelineId.test} />
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(EmptyComponent.getByTestId('flyout-pane')).toHaveStyle('display: block');
|
||||
});
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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 { TestProviders } from '../../../../common/mock';
|
||||
import DiscoverTabContent from '.';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
|
||||
const TestComponent = () => {
|
||||
return (
|
||||
<TestProviders>
|
||||
<DiscoverTabContent />
|
||||
</TestProviders>
|
||||
);
|
||||
};
|
||||
|
||||
describe('Discover Tab Content', () => {
|
||||
it('renders', async () => {
|
||||
render(<TestComponent />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('timeline-embedded-discover')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* 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, { useCallback, useMemo } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import type { CustomizationCallback } from '@kbn/discover-plugin/public/customizations/types';
|
||||
import styled, { createGlobalStyle } from 'styled-components';
|
||||
import type { ScopedHistory } from '@kbn/core/public';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { useGetStatefulQueryBar } from './use_get_stateful_query_bar';
|
||||
|
||||
const HideSearchSessionIndicatorBreadcrumbIcon = createGlobalStyle`
|
||||
[data-test-subj='searchSessionIndicator'] {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
|
||||
const EmbeddedDiscoverContainer = styled.div`
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: scroll;
|
||||
display: grid,
|
||||
place-items: center
|
||||
`;
|
||||
|
||||
export const DiscoverTabContent = () => {
|
||||
const history = useHistory();
|
||||
const {
|
||||
services: { customDataService: discoverDataService, discover, discoverFilterManager },
|
||||
} = useKibana();
|
||||
|
||||
const { CustomStatefulTopNavKqlQueryBar } = useGetStatefulQueryBar();
|
||||
|
||||
const customize: CustomizationCallback = useCallback(
|
||||
({ customizations }) => {
|
||||
customizations.set({
|
||||
id: 'search_bar',
|
||||
CustomSearchBar: CustomStatefulTopNavKqlQueryBar,
|
||||
});
|
||||
},
|
||||
[CustomStatefulTopNavKqlQueryBar]
|
||||
);
|
||||
|
||||
const services = useMemo(
|
||||
() => ({
|
||||
filterManager: discoverFilterManager,
|
||||
data: discoverDataService,
|
||||
}),
|
||||
[discoverDataService, discoverFilterManager]
|
||||
);
|
||||
|
||||
const DiscoverContainer = discover.DiscoverContainer;
|
||||
|
||||
return (
|
||||
<EmbeddedDiscoverContainer data-test-subj="timeline-embedded-discover">
|
||||
<HideSearchSessionIndicatorBreadcrumbIcon />
|
||||
<DiscoverContainer
|
||||
overrideServices={services}
|
||||
scopedHistory={history as ScopedHistory}
|
||||
customize={customize}
|
||||
/>
|
||||
</EmbeddedDiscoverContainer>
|
||||
);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default DiscoverTabContent;
|
|
@ -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 { TestProviders } from '../../../../common/mock';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { useGetStatefulQueryBar } from './use_get_stateful_query_bar';
|
||||
|
||||
describe('useGetStatefulQueryBar', () => {
|
||||
it('returns custom QueryBar', async () => {
|
||||
const { result } = renderHook(() => useGetStatefulQueryBar(), {
|
||||
wrapper: TestProviders,
|
||||
});
|
||||
|
||||
expect(result.current).toHaveProperty('CustomStatefulTopNavKqlQueryBar');
|
||||
expect(result.current.CustomStatefulTopNavKqlQueryBar).not.toBeUndefined();
|
||||
});
|
||||
});
|
|
@ -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 { useMemo } from 'react';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
|
||||
export const useGetStatefulQueryBar = () => {
|
||||
const {
|
||||
services: {
|
||||
navigation: {
|
||||
ui: { createTopNavWithCustomContext },
|
||||
},
|
||||
unifiedSearch,
|
||||
customDataService,
|
||||
},
|
||||
} = useKibana();
|
||||
|
||||
const {
|
||||
ui: { getCustomSearchBar },
|
||||
} = unifiedSearch;
|
||||
|
||||
const CustomSearchBar = useMemo(
|
||||
() => getCustomSearchBar(customDataService),
|
||||
[customDataService, getCustomSearchBar]
|
||||
);
|
||||
|
||||
const CustomStatefulTopNavKqlQueryBar = useMemo(() => {
|
||||
const customUnifiedSearch = {
|
||||
...unifiedSearch,
|
||||
ui: {
|
||||
...unifiedSearch.ui,
|
||||
SearchBar: CustomSearchBar,
|
||||
AggregateQuerySearchBar: CustomSearchBar,
|
||||
},
|
||||
};
|
||||
|
||||
return createTopNavWithCustomContext(customUnifiedSearch);
|
||||
}, [CustomSearchBar, createTopNavWithCustomContext, unifiedSearch]);
|
||||
|
||||
return {
|
||||
CustomStatefulTopNavKqlQueryBar,
|
||||
};
|
||||
};
|
|
@ -9,10 +9,12 @@ import { EuiBadge, EuiSkeletonText, EuiTabs, EuiTab } from '@elastic/eui';
|
|||
import { css } from '@emotion/react';
|
||||
import { Assistant } from '@kbn/elastic-assistant';
|
||||
import { isEmpty } from 'lodash/fp';
|
||||
import type { Ref, ReactElement, ComponentType } from 'react';
|
||||
import React, { lazy, memo, Suspense, useCallback, useEffect, useMemo } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
|
||||
import { useConversationStore } from '../../../../assistant/use_conversation_store';
|
||||
import { useAssistantAvailability } from '../../../../assistant/use_assistant_availability';
|
||||
import type { SessionViewConfig } from '../../../../../common/types';
|
||||
|
@ -52,17 +54,37 @@ const HideShowContainer = styled.div.attrs<{ $isVisible: boolean; isOverflowYScr
|
|||
flex: 1;
|
||||
`;
|
||||
|
||||
/**
|
||||
* A HOC which supplies React.Suspense with a fallback component
|
||||
* @param Component A component deferred by `React.lazy`
|
||||
* @param fallback A fallback component to render while things load. Default is EuiSekeleton for all tabs
|
||||
*/
|
||||
const tabWithSuspense = <P extends {}, R = {}>(
|
||||
Component: ComponentType<P>,
|
||||
fallback: ReactElement | null = <EuiSkeletonText lines={10} />
|
||||
) => {
|
||||
const Comp = React.forwardRef((props: P, ref: Ref<R>) => (
|
||||
<Suspense fallback={fallback}>
|
||||
<Component {...props} ref={ref} />
|
||||
</Suspense>
|
||||
));
|
||||
|
||||
Comp.displayName = `${Component.displayName ?? 'Tab'}WithSuspense`;
|
||||
return Comp;
|
||||
};
|
||||
|
||||
const AssistantTabContainer = styled.div`
|
||||
overflow-y: auto;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const QueryTabContent = lazy(() => import('../query_tab_content'));
|
||||
const EqlTabContent = lazy(() => import('../eql_tab_content'));
|
||||
const GraphTabContent = lazy(() => import('../graph_tab_content'));
|
||||
const NotesTabContent = lazy(() => import('../notes_tab_content'));
|
||||
const PinnedTabContent = lazy(() => import('../pinned_tab_content'));
|
||||
const SessionTabContent = lazy(() => import('../session_tab_content'));
|
||||
const QueryTab = tabWithSuspense(lazy(() => import('../query_tab_content')));
|
||||
const EqlTab = tabWithSuspense(lazy(() => import('../eql_tab_content')));
|
||||
const GraphTab = tabWithSuspense(lazy(() => import('../graph_tab_content')));
|
||||
const NotesTab = tabWithSuspense(lazy(() => import('../notes_tab_content')));
|
||||
const PinnedTab = tabWithSuspense(lazy(() => import('../pinned_tab_content')));
|
||||
const SessionTab = tabWithSuspense(lazy(() => import('../session_tab_content')));
|
||||
const DiscoverTab = tabWithSuspense(lazy(() => import('../discover_tab_content')));
|
||||
|
||||
interface BasicTimelineTab {
|
||||
renderCellValue: (props: CellValueElementProps) => React.ReactNode;
|
||||
|
@ -75,72 +97,6 @@ interface BasicTimelineTab {
|
|||
timelineDescription: string;
|
||||
}
|
||||
|
||||
const QueryTab: React.FC<{
|
||||
renderCellValue: (props: CellValueElementProps) => React.ReactNode;
|
||||
rowRenderers: RowRenderer[];
|
||||
timelineId: TimelineId;
|
||||
}> = memo(({ renderCellValue, rowRenderers, timelineId }) => (
|
||||
<Suspense fallback={<EuiSkeletonText lines={10} />}>
|
||||
<QueryTabContent
|
||||
renderCellValue={renderCellValue}
|
||||
rowRenderers={rowRenderers}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
</Suspense>
|
||||
));
|
||||
QueryTab.displayName = 'QueryTab';
|
||||
|
||||
const EqlTab: React.FC<{
|
||||
renderCellValue: (props: CellValueElementProps) => React.ReactNode;
|
||||
rowRenderers: RowRenderer[];
|
||||
timelineId: TimelineId;
|
||||
}> = memo(({ renderCellValue, rowRenderers, timelineId }) => (
|
||||
<Suspense fallback={<EuiSkeletonText lines={10} />}>
|
||||
<EqlTabContent
|
||||
renderCellValue={renderCellValue}
|
||||
rowRenderers={rowRenderers}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
</Suspense>
|
||||
));
|
||||
EqlTab.displayName = 'EqlTab';
|
||||
|
||||
const GraphTab: React.FC<{ timelineId: TimelineId }> = memo(({ timelineId }) => (
|
||||
<Suspense fallback={<EuiSkeletonText lines={10} />}>
|
||||
<GraphTabContent timelineId={timelineId} />
|
||||
</Suspense>
|
||||
));
|
||||
GraphTab.displayName = 'GraphTab';
|
||||
|
||||
const NotesTab: React.FC<{ timelineId: TimelineId }> = memo(({ timelineId }) => (
|
||||
<Suspense fallback={<EuiSkeletonText lines={10} />}>
|
||||
<NotesTabContent timelineId={timelineId} />
|
||||
</Suspense>
|
||||
));
|
||||
NotesTab.displayName = 'NotesTab';
|
||||
|
||||
const SessionTab: React.FC<{ timelineId: TimelineId }> = memo(({ timelineId }) => (
|
||||
<Suspense fallback={<EuiSkeletonText lines={10} />}>
|
||||
<SessionTabContent timelineId={timelineId} />
|
||||
</Suspense>
|
||||
));
|
||||
SessionTab.displayName = 'SessionTab';
|
||||
|
||||
const PinnedTab: React.FC<{
|
||||
renderCellValue: (props: CellValueElementProps) => React.ReactNode;
|
||||
rowRenderers: RowRenderer[];
|
||||
timelineId: TimelineId;
|
||||
}> = memo(({ renderCellValue, rowRenderers, timelineId }) => (
|
||||
<Suspense fallback={<EuiSkeletonText lines={10} />}>
|
||||
<PinnedTabContent
|
||||
renderCellValue={renderCellValue}
|
||||
rowRenderers={rowRenderers}
|
||||
timelineId={timelineId}
|
||||
/>
|
||||
</Suspense>
|
||||
));
|
||||
PinnedTab.displayName = 'PinnedTab';
|
||||
|
||||
const AssistantTab: React.FC<{
|
||||
isAssistantEnabled: boolean;
|
||||
renderCellValue: (props: CellValueElementProps) => React.ReactNode;
|
||||
|
@ -177,6 +133,7 @@ const ActiveTimelineTab = memo<ActiveTimelineTabProps>(
|
|||
timelineType,
|
||||
showTimeline,
|
||||
}) => {
|
||||
const isDiscoverInTimelineEnabled = useIsExperimentalFeatureEnabled('discoverInTimeline');
|
||||
const { hasAssistantPrivilege, isAssistantEnabled } = useAssistantAvailability();
|
||||
const getTab = useCallback(
|
||||
(tab: TimelineTabs) => {
|
||||
|
@ -276,6 +233,14 @@ const ActiveTimelineTab = memo<ActiveTimelineTabProps>(
|
|||
)}
|
||||
</HideShowContainer>
|
||||
)}
|
||||
{isDiscoverInTimelineEnabled && (
|
||||
<HideShowContainer
|
||||
$isVisible={TimelineTabs.discover === activeTimelineTab}
|
||||
data-test-subj={`timeline-tab-content-${TimelineTabs.discover}`}
|
||||
>
|
||||
<DiscoverTab />
|
||||
</HideShowContainer>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -313,6 +278,7 @@ const TabsContentComponent: React.FC<BasicTimelineTab> = ({
|
|||
sessionViewConfig,
|
||||
timelineDescription,
|
||||
}) => {
|
||||
const isDiscoverInTimelineEnabled = useIsExperimentalFeatureEnabled('discoverInTimeline');
|
||||
const { hasAssistantPrivilege } = useAssistantAvailability();
|
||||
const dispatch = useDispatch();
|
||||
const getActiveTab = useMemo(() => getActiveTabSelector(), []);
|
||||
|
@ -388,6 +354,10 @@ const TabsContentComponent: React.FC<BasicTimelineTab> = ({
|
|||
setActiveTab(TimelineTabs.securityAssistant);
|
||||
}, [setActiveTab]);
|
||||
|
||||
const setDiscoverAsActiveTab = useCallback(() => {
|
||||
setActiveTab(TimelineTabs.discover);
|
||||
}, [setActiveTab]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!graphEventId && activeTab === TimelineTabs.graph) {
|
||||
setQueryAsActiveTab();
|
||||
|
@ -479,6 +449,17 @@ const TabsContentComponent: React.FC<BasicTimelineTab> = ({
|
|||
<span>{i18n.SECURITY_ASSISTANT}</span>
|
||||
</StyledEuiTab>
|
||||
)}
|
||||
{isDiscoverInTimelineEnabled && (
|
||||
<StyledEuiTab
|
||||
data-test-subj={`timelineTabs-${TimelineTabs.discover}`}
|
||||
onClick={setDiscoverAsActiveTab}
|
||||
isSelected={activeTab === TimelineTabs.discover}
|
||||
disabled={false}
|
||||
key={TimelineTabs.discover}
|
||||
>
|
||||
<span>{i18n.DISCOVER_IN_TIMELINE_TAB}</span>
|
||||
</StyledEuiTab>
|
||||
)}
|
||||
</EuiTabs>
|
||||
)}
|
||||
|
||||
|
|
|
@ -46,6 +46,13 @@ export const SECURITY_ASSISTANT = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const DISCOVER_IN_TIMELINE_TAB = i18n.translate(
|
||||
'xpack.securitySolution.timeline.tabs.discoverInTimeline',
|
||||
{
|
||||
defaultMessage: 'Discover',
|
||||
}
|
||||
);
|
||||
|
||||
export const SESSION_TAB = i18n.translate(
|
||||
'xpack.securitySolution.timeline.tabs.sessionTabTimelineTitle',
|
||||
{
|
||||
|
|
|
@ -9,7 +9,7 @@ import type { Observable } from 'rxjs';
|
|||
|
||||
import type { AppLeaveHandler, CoreStart } from '@kbn/core/public';
|
||||
import type { HomePublicPluginSetup } from '@kbn/home-plugin/public';
|
||||
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import type { DataPublicPluginStart, FilterManager } from '@kbn/data-plugin/public';
|
||||
import type { FieldFormatsStartCommon } from '@kbn/field-formats-plugin/common';
|
||||
import type { EmbeddableStart } from '@kbn/embeddable-plugin/public';
|
||||
import type { LensPublicStart } from '@kbn/lens-plugin/public';
|
||||
|
@ -50,6 +50,9 @@ import type { GuidedOnboardingPluginStart } from '@kbn/guided-onboarding-plugin/
|
|||
import type { DataViewsServicePublic } from '@kbn/data-views-plugin/public';
|
||||
import type { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public';
|
||||
|
||||
import type { DiscoverStart } from '@kbn/discover-plugin/public';
|
||||
import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public';
|
||||
import type { DataViewEditorStart } from '@kbn/data-view-editor-plugin/public';
|
||||
import type { ResolverPluginSetup } from './resolver/types';
|
||||
import type { Inspect } from '../common/search_strategy';
|
||||
import type { Detections } from './detections';
|
||||
|
@ -122,6 +125,9 @@ export interface StartPlugins {
|
|||
cloudExperiments?: CloudExperimentsPluginStart;
|
||||
dataViews: DataViewsServicePublic;
|
||||
fieldFormats: FieldFormatsStartCommon;
|
||||
discover: DiscoverStart;
|
||||
navigation: NavigationPublicPluginStart;
|
||||
dataViewEditor: DataViewEditorStart;
|
||||
}
|
||||
|
||||
export interface StartPluginsDependencies extends StartPlugins {
|
||||
|
@ -153,6 +159,8 @@ export type StartServices = CoreStart &
|
|||
};
|
||||
savedObjectsManagement: SavedObjectsManagementPluginStart;
|
||||
telemetry: TelemetryClientStart;
|
||||
discoverFilterManager: FilterManager;
|
||||
customDataService: DataPublicPluginStart;
|
||||
};
|
||||
|
||||
export interface PluginSetup {
|
||||
|
|
|
@ -161,6 +161,9 @@
|
|||
"@kbn/field-formats-plugin",
|
||||
"@kbn/dev-proc-runner",
|
||||
"@kbn/cloud-chat-plugin",
|
||||
"@kbn/discover-plugin",
|
||||
"@kbn/navigation-plugin",
|
||||
"@kbn/data-view-editor-plugin",
|
||||
"@kbn/alerts-ui-shared"
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue