[Security Solution][Timeline] - ESQL in timeline (#166764)

## Summary

This PR leverages the work done
[here](https://github.com/elastic/kibana/pull/165596) to introduce ES|QL
into timeline. The goal of this PR is to provide security users easy
access to ESQL from within the security solution. It will be released in
`technical preview` for the 8.11 release.

<img width="1725" alt="image"
src="0e275cf7-bbce-476b-b1dc-8936427ad14f">

### Checklist

Delete any items that are not applicable to this PR.

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: Jatin Kathuria <jatin.kathuria@elastic.co>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Jatin Kathuria <jtn.kathuria@gmail.com>
This commit is contained in:
Michael Olorunnisola 2023-10-04 02:34:00 -04:00 committed by GitHub
parent 471ae6a858
commit 96a1ef4581
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 248 additions and 246 deletions

View file

@ -35,7 +35,6 @@ xpack.fleet.internal.registry.spec.max: '3.0'
# Serverless security specific options
xpack.securitySolution.enableExperimental:
- discoverInTimeline
- esqlRulesDisabled
xpack.ml.ad.enabled: true

View file

@ -46,6 +46,11 @@ const mockSearchBarCustomizationWithCustomSearchBar: SearchBarCustomization = {
CustomSearchBar: MockCustomSearchBar,
};
const mockSearchBarCustomizationWithHiddenDataViewPicker: SearchBarCustomization = {
id: 'search_bar',
hideDataViewPicker: true,
};
let mockUseCustomizations = false;
jest.mock('../../../../customizations', () => ({
@ -256,5 +261,23 @@ describe('Discover topnav component', () => {
).find(mockSearchBarCustomization.CustomDataViewPicker!);
expect(dataViewPickerOverride.length).toBe(1);
});
it('should not render the dataView picker when hideDataViewPicker is true', () => {
(useDiscoverCustomization as jest.Mock).mockImplementation((id: DiscoverCustomizationId) => {
if (id === 'search_bar') {
return mockSearchBarCustomizationWithHiddenDataViewPicker;
}
});
const props = getProps();
const component = mountWithIntl(
<DiscoverMainProvider value={props.stateContainer}>
<DiscoverTopNav {...props} />
</DiscoverMainProvider>
);
const topNav = component.find(mockDiscoverService.navigation.ui.AggregateQueryTopNavMenu);
expect(topNav.prop('dataViewPickerComponentProps')).toBeUndefined();
});
});
});

View file

@ -114,6 +114,7 @@ export const DiscoverTopNav = ({
}, [dataViewEditor, stateContainer]);
const topNavCustomization = useDiscoverCustomization('top_nav');
const topNavMenu = useMemo(
() =>
getTopNavLinks({
@ -171,6 +172,17 @@ export const DiscoverTopNav = ({
if (isESQLModeEnabled) {
supportedTextBasedLanguages.push('ESQL');
}
const searchBarCustomization = useDiscoverCustomization('search_bar');
const SearchBar = useMemo(
() => searchBarCustomization?.CustomSearchBar ?? AggregateQueryTopNavMenu,
[searchBarCustomization?.CustomSearchBar, AggregateQueryTopNavMenu]
);
const shouldHideDefaultDataviewPicker =
!!searchBarCustomization?.CustomDataViewPicker || !!searchBarCustomization?.hideDataViewPicker;
const dataViewPickerProps: DataViewPickerProps = {
trigger: {
label: dataView?.getName() || '',
@ -201,13 +213,6 @@ export const DiscoverTopNav = ({
[services, stateContainer]
);
const searchBarCustomization = useDiscoverCustomization('search_bar');
const SearchBar = useMemo(
() => searchBarCustomization?.CustomSearchBar ?? AggregateQueryTopNavMenu,
[searchBarCustomization?.CustomSearchBar, AggregateQueryTopNavMenu]
);
return (
<SearchBar
appName="discover"
@ -231,7 +236,7 @@ export const DiscoverTopNav = ({
) : undefined
}
dataViewPickerComponentProps={
searchBarCustomization?.CustomDataViewPicker ? undefined : dataViewPickerProps
shouldHideDefaultDataviewPicker ? undefined : dataViewPickerProps
}
displayStyle="detached"
textBasedLanguageModeErrors={

View file

@ -15,4 +15,5 @@ export interface SearchBarCustomization {
CustomDataViewPicker?: ComponentType;
PrependFilterBar?: ComponentType;
CustomSearchBar?: ComponentType<TopNavMenuProps<AggregateQuery>>;
hideDataViewPicker?: boolean;
}

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { LIVE_QUERY_EDITOR } from '../../screens/live_query';
import { OSQUERY_FLYOUT_BODY_EDITOR } from '../../screens/live_query';
import {
cleanupCase,
cleanupPack,
@ -79,7 +79,7 @@ describe(
cy.getBySel('osquery-action-item').click();
cy.contains(/^\d+ agen(t|ts) selected/);
cy.contains('Run a set of queries in a pack').click();
cy.get(LIVE_QUERY_EDITOR).should('not.exist');
cy.get(OSQUERY_FLYOUT_BODY_EDITOR).should('not.exist');
cy.getBySel('select-live-pack').click().type(`${packName}{downArrow}{enter}`);
submitQuery();
cy.get('[aria-label="Add to Case"]').first().click();

View file

@ -14,6 +14,7 @@ import {
takeOsqueryActionWithParams,
} from '../../tasks/live_query';
import { ServerlessRoleName } from '../../support/roles';
import { OSQUERY_FLYOUT_BODY_EDITOR } from '../../screens/live_query';
describe(
'Alert Event Details - dynamic params',
@ -43,12 +44,14 @@ describe(
it('should substitute parameters in investigation guide', () => {
cy.getBySel('expand-event').first().click();
cy.getBySel('securitySolutionFlyoutInvestigationGuideButton').click();
cy.contains('Get processes').click();
cy.getBySel('flyout-body-osquery').within(() => {
cy.contains("SELECT * FROM os_version where name='Ubuntu';");
cy.contains('host.os.platform');
cy.contains('platform');
});
// Flakes at times if the button is only clicked once
cy.contains('Get processes').should('be.visible').dblclick({ force: true });
// Cypress can properly reads the fields when the codeEditor is interacted with first
// This is probably due to the tokenization of the fields when it's inactive
cy.get(OSQUERY_FLYOUT_BODY_EDITOR).click();
cy.getBySel('flyout-body-osquery').contains("SELECT * FROM os_version where name='Ubuntu';");
cy.getBySel('flyout-body-osquery').contains('host.os.platform');
cy.getBySel('flyout-body-osquery').contains('platform');
});
// response-actions-notification doesn't exist in expandable flyout

View file

@ -8,6 +8,8 @@
export const AGENT_FIELD = '[data-test-subj="comboBoxInput"]';
export const ALL_AGENTS_OPTION = '[title="All agents"]';
export const LIVE_QUERY_EDITOR = '.kibanaCodeEditor';
export const OSQUERY_FLYOUT_BODY_EDITOR =
'[data-test-subj="flyout-body-osquery"] .kibanaCodeEditor';
export const SUBMIT_BUTTON = '#submit-button';
export const RESULTS_TABLE = 'osqueryResultsTable';

View file

@ -108,7 +108,7 @@ export const allowedExperimentalValues = Object.freeze({
* Enables Discover embedded within timeline
*
* */
discoverInTimeline: false,
discoverInTimeline: true,
/**
* disables ES|QL rules

View file

@ -7,22 +7,22 @@
export const useDiscoverInTimelineActions = () => {
return {
resetDiscoverAppState: jest.fn(),
resetDiscoverAppState: jest.fn().mockResolvedValue(true),
restoreDiscoverAppStateFromSavedSearch: jest.fn(),
updateSavedSearch: jest.fn(),
getAppStateFromSavedSearch: jest.fn(),
defaultDiscoverAppState: {
getDefaultDiscoverAppState: () => ({
query: {
query: '',
language: 'kuery',
language: 'esql',
},
sort: [['@timestamp', 'desc']],
columns: [],
index: 'security-solution-default',
interval: 'auto',
filters: [],
hideChart: true,
hideChart: false,
grid: {},
},
}),
};
};

View file

@ -25,8 +25,12 @@ import { createStore } from '../../store';
import { TimelineId } from '../../../../common/types';
import type { ComponentType, FC, PropsWithChildren } from 'react';
import React from 'react';
import type { DataView } from '@kbn/data-views-plugin/common';
import TestRenderer from 'react-test-renderer';
const mockDiscoverStateContainerRef = {
const { act } = TestRenderer;
let mockDiscoverStateContainerRef = {
current: discoverPluginMock.getDiscoverStateMock({}),
};
@ -64,6 +68,9 @@ const getTestProviderWithCustomState = (state: State = mockState) => {
};
const renderTestHook = (customWrapper: ComponentType = getTestProviderWithCustomState()) => {
mockDiscoverStateContainerRef = {
current: discoverPluginMock.getDiscoverStateMock({}),
};
return renderHook(() => useDiscoverInTimelineActions(mockDiscoverStateContainerRef), {
wrapper: customWrapper,
});
@ -120,6 +127,13 @@ export const savedSearchMock = {
const startServicesMock = createStartServicesMock();
startServicesMock.dataViews.get = jest.fn(
async () =>
({
getIndexPattern: jest.fn(),
} as unknown as DataView)
);
describe('useDiscoverInTimelineActions', () => {
beforeEach(() => {
(useKibana as jest.Mock).mockImplementation(() => ({
@ -188,15 +202,15 @@ describe('useDiscoverInTimelineActions', () => {
describe('resetDiscoverAppState', () => {
it('should reset Discover AppState to a default state', async () => {
const { result, waitFor } = renderTestHook();
result.current.resetDiscoverAppState();
await result.current.resetDiscoverAppState();
await waitFor(() => {
const appState = mockDiscoverStateContainerRef.current.appState.getState();
expect(appState).toMatchObject(result.current.defaultDiscoverAppState);
expect(appState).toMatchObject(result.current.getDefaultDiscoverAppState());
});
});
it('should reset Discover time to a default state', async () => {
const { result, waitFor } = renderTestHook();
result.current.resetDiscoverAppState();
await result.current.resetDiscoverAppState();
await waitFor(() => {
const globalState = mockDiscoverStateContainerRef.current.globalState.get();
expect(globalState).toMatchObject({ time: { from: 'now-15m', to: 'now' } });
@ -206,7 +220,9 @@ describe('useDiscoverInTimelineActions', () => {
describe('updateSavedSearch', () => {
it('should add defaults to the savedSearch before updating saved search', async () => {
const { result } = renderTestHook();
await result.current.updateSavedSearch(savedSearchMock, TimelineId.active);
await act(async () => {
await result.current.updateSavedSearch(savedSearchMock, TimelineId.active);
});
expect(startServicesMock.savedSearch.save).toHaveBeenNthCalledWith(
1,
@ -242,7 +258,9 @@ describe('useDiscoverInTimelineActions', () => {
const LocalTestProvider = getTestProviderWithCustomState(localMockState);
const { result } = renderTestHook(LocalTestProvider);
await result.current.updateSavedSearch(savedSearchMock, TimelineId.active);
await act(async () => {
await result.current.updateSavedSearch(savedSearchMock, TimelineId.active);
});
expect(startServicesMock.savedSearch.save).toHaveBeenNthCalledWith(
1,

View file

@ -40,7 +40,11 @@ export const useDiscoverInTimelineActions = (
const { addError } = useAppToasts();
const {
services: { customDataService: discoverDataService, savedSearch: savedSearchService },
services: {
customDataService: discoverDataService,
savedSearch: savedSearchService,
dataViews: dataViewService,
},
} = useKibana();
const dispatch = useDispatch();
@ -69,18 +73,23 @@ export const useDiscoverInTimelineActions = (
},
});
const defaultDiscoverAppState: DiscoverAppState = useMemo(() => {
const getDefaultDiscoverAppState: () => Promise<DiscoverAppState> = useCallback(async () => {
const localDataViewId = dataViewId ?? 'security-solution-default';
const dataView = await dataViewService.get(localDataViewId);
return {
query: discoverDataService.query.queryString.getDefaultQuery(),
query: {
esql: dataView ? `from ${dataView.getIndexPattern()} | limit 10` : '',
},
sort: [['@timestamp', 'desc']],
columns: [],
index: dataViewId ?? 'security-solution-default',
interval: 'auto',
filters: [],
hideChart: true,
grid: {},
};
}, [discoverDataService, dataViewId]);
}, [dataViewService, dataViewId]);
/*
* generates Appstate from a given saved Search object
@ -123,13 +132,14 @@ export const useDiscoverInTimelineActions = (
* resets discover state to a default value
*
* */
const resetDiscoverAppState = useCallback(() => {
const resetDiscoverAppState = useCallback(async () => {
const defaultDiscoverAppState = await getDefaultDiscoverAppState();
discoverStateContainer.current?.appState.set(defaultDiscoverAppState);
discoverStateContainer.current?.globalState.set({
...discoverStateContainer.current?.globalState.get(),
time: defaultDiscoverTimeRange,
});
}, [defaultDiscoverAppState, discoverStateContainer]);
}, [getDefaultDiscoverAppState, discoverStateContainer]);
const persistSavedSearch = useCallback(
async (savedSearch: SavedSearch, savedSearchOption: SaveSavedSearchOptions) => {
@ -220,14 +230,14 @@ export const useDiscoverInTimelineActions = (
restoreDiscoverAppStateFromSavedSearch,
updateSavedSearch,
getAppStateFromSavedSearch,
defaultDiscoverAppState,
getDefaultDiscoverAppState,
}),
[
resetDiscoverAppState,
restoreDiscoverAppStateFromSavedSearch,
updateSavedSearch,
getAppStateFromSavedSearch,
defaultDiscoverAppState,
getDefaultDiscoverAppState,
]
);

View file

@ -6,15 +6,18 @@
*/
import type { CustomizationCallback } from '@kbn/discover-plugin/public';
import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features';
import { useGetStatefulQueryBar } from '../use_get_stateful_query_bar';
export const useSearchBarCustomizations = () => {
const { CustomStatefulTopNavKqlQueryBar } = useGetStatefulQueryBar();
const isDiscoverInTimelineEnabled = useIsExperimentalFeatureEnabled('discoverInTimeline');
const setSearchBarCustomizations: CustomizationCallback = ({ customizations }) => {
customizations.set({
id: 'search_bar',
CustomSearchBar: CustomStatefulTopNavKqlQueryBar,
hideDataViewPicker: isDiscoverInTimelineEnabled,
});
};

View file

@ -11,7 +11,6 @@ import { useSearchBarCustomizations } from './use_search_bar_customizations';
export const useSetDiscoverCustomizationCallbacks = (): CustomizationCallback[] => {
const searchBarCustomizationCallback = useSearchBarCustomizations();
const histogramCustomizationCallback = useHistogramCustomization();
return [searchBarCustomizationCallback, histogramCustomizationCallback];

View file

@ -72,6 +72,7 @@ export const DiscoverTabContent: FC<DiscoverTabContentProps> = ({ timelineId })
updateSavedSearch,
restoreDiscoverAppStateFromSavedSearch,
resetDiscoverAppState,
getDefaultDiscoverAppState,
} = useDiscoverInTimelineContext();
const {
@ -126,9 +127,10 @@ export const DiscoverTabContent: FC<DiscoverTabContentProps> = ({ timelineId })
if (!savedSearchById) {
// nothing to restore if savedSearchById is null
if (status === 'draft') {
resetDiscoverAppState();
resetDiscoverAppState().then(() => {
setSavedSearchLoaded(true);
});
}
setSavedSearchLoaded(true);
return;
}
restoreDiscoverAppStateFromSavedSearch(savedSearchById);
@ -227,15 +229,13 @@ export const DiscoverTabContent: FC<DiscoverTabContentProps> = ({ timelineId })
savedSearchAppState = getAppStateFromSavedSearch(localSavedSearch);
}
const finalAppState = savedSearchAppState?.appState ?? discoverAppState;
const defaultDiscoverAppState = await getDefaultDiscoverAppState();
if (finalAppState) {
stateContainer.appState.set(finalAppState);
await stateContainer.appState.replaceUrlState(finalAppState);
} else {
// set initial dataView Id
if (dataView) stateContainer.actions.setDataView(dataView);
}
const finalAppState =
savedSearchAppState?.appState ?? discoverAppState ?? defaultDiscoverAppState;
stateContainer.appState.set(finalAppState);
await stateContainer.appState.replaceUrlState(finalAppState);
const unsubscribeState = stateContainer.appState.state$.subscribe({
next: setDiscoverAppState,
@ -272,12 +272,12 @@ export const DiscoverTabContent: FC<DiscoverTabContentProps> = ({ timelineId })
setDiscoverSavedSearchState,
setDiscoverInternalState,
setDiscoverAppState,
dataView,
setDiscoverStateContainer,
getAppStateFromSavedSearch,
discoverDataService.query.timefilter.timefilter,
savedSearchId,
savedSearchService,
getDefaultDiscoverAppState,
]
);

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { EuiBadge, EuiSkeletonText, EuiTabs, EuiTab } from '@elastic/eui';
import { EuiBadge, EuiSkeletonText, EuiTabs, EuiTab, EuiBetaBadge } from '@elastic/eui';
import { css } from '@emotion/react';
import { Assistant } from '@kbn/elastic-assistant';
import { isEmpty } from 'lodash/fp';
@ -14,6 +14,7 @@ import React, { lazy, memo, Suspense, useCallback, useEffect, useMemo, useState
import { useDispatch } from 'react-redux';
import styled from 'styled-components';
import { FormattedMessage } from '@kbn/i18n-react';
import { useAssistantTelemetry } from '../../../../assistant/use_assistant_telemetry';
import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features';
import { useConversationStore } from '../../../../assistant/use_conversation_store';
@ -44,6 +45,7 @@ import * as i18n from './translations';
import { useLicense } from '../../../../common/hooks/use_license';
import { TIMELINE_CONVERSATION_TITLE } from '../../../../assistant/content/conversations/translations';
import { initializeTimelineSettings } from '../../../store/timeline/actions';
import { DISCOVER_ESQL_IN_TIMELINE_TECHNICAL_PREVIEW } from './translations';
const HideShowContainer = styled.div.attrs<{ $isVisible: boolean; isOverflowYScroll: boolean }>(
({ $isVisible = false, isOverflowYScroll = false }) => ({
@ -248,8 +250,18 @@ const CountBadge = styled(EuiBadge)`
margin-left: ${({ theme }) => theme.eui.euiSizeS};
`;
const StyledEuiBetaBadge = styled(EuiBetaBadge)`
vertical-align: middle;
margin-left: ${({ theme }) => theme.eui.euiSizeS};
&:hover {
cursor: pointer;
}
`;
const StyledEuiTab = styled(EuiTab)`
.euiTab__content {
align-items: center;
display: flex;
flex-direction: row;
white-space: pre;
@ -388,6 +400,28 @@ const TabsContentComponent: React.FC<BasicTimelineTab> = ({
<span>{i18n.QUERY_TAB}</span>
{showTimeline && <TimelineEventsCountBadge />}
</StyledEuiTab>
{isDiscoverInTimelineEnabled && (
<StyledEuiTab
data-test-subj={`timelineTabs-${TimelineTabs.discover}`}
onClick={setDiscoverAsActiveTab}
isSelected={activeTab === TimelineTabs.discover}
disabled={false}
key={TimelineTabs.discover}
>
<span>{i18n.DISCOVER_ESQL_IN_TIMELINE_TAB}</span>
<StyledEuiBetaBadge
label={DISCOVER_ESQL_IN_TIMELINE_TECHNICAL_PREVIEW}
size="s"
iconType="beaker"
tooltipContent={
<FormattedMessage
id="xpack.securitySolution.timeline.tabs.discoverEsqlInTimeline.technicalPreviewTooltip"
defaultMessage="This functionality is in technical preview and may be changed or removed completely in a future release. Elastic will take a best effort approach to fix any issues, but features in technical preview are not subject to the support SLA of official GA features."
/>
}
/>
</StyledEuiTab>
)}
{timelineType === TimelineType.default && (
<StyledEuiTab
data-test-subj={`timelineTabs-${TimelineTabs.eql}`}
@ -459,17 +493,6 @@ 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>
)}

View file

@ -46,10 +46,17 @@ export const SECURITY_ASSISTANT = i18n.translate(
}
);
export const DISCOVER_IN_TIMELINE_TAB = i18n.translate(
'xpack.securitySolution.timeline.tabs.discoverInTimeline',
export const DISCOVER_ESQL_IN_TIMELINE_TAB = i18n.translate(
'xpack.securitySolution.timeline.tabs.discoverEsqlInTimeline',
{
defaultMessage: 'Discover',
defaultMessage: 'ES|QL',
}
);
export const DISCOVER_ESQL_IN_TIMELINE_TECHNICAL_PREVIEW = i18n.translate(
'xpack.securitySolution.timeline.tabs.discoverEsqlInTimeline.technicalPreviewLabel',
{
defaultMessage: 'Technical Preview',
}
);

View file

@ -9,7 +9,6 @@ import { grantClipboardReadPerm } from '../../../../tasks/common/clipboard';
import {
DISCOVER_CELL_ACTIONS,
DISCOVER_CONTAINER,
DISCOVER_FILTER_BADGES,
GET_DISCOVER_DATA_GRID_CELL,
} from '../../../../screens/discover';
import { waitForDiscoverGridToLoad } from '../../../../tasks/discover';
@ -28,7 +27,7 @@ describe.skip(
`Discover Datagrid Cell Actions`,
{
env: { ftrConfig: { enableExperimental: ['discoverInTimeline'] } },
tags: ['@serverless', '@brokenInServerless'],
tags: ['@ess'],
},
() => {
beforeEach(() => {
@ -39,35 +38,6 @@ describe.skip(
updateDateRangeInLocalDatePickers(DISCOVER_CONTAINER, INITIAL_START_DATE, INITIAL_END_DATE);
waitForDiscoverGridToLoad();
});
it('Filter for', () => {
cy.get(GET_DISCOVER_DATA_GRID_CELL(TIMESTAMP_COLUMN_NAME, 0)).then((sub) => {
const selectedTimestamp = sub.text();
cy.get(GET_DISCOVER_DATA_GRID_CELL(TIMESTAMP_COLUMN_NAME, 0)).realHover();
cy.get(DISCOVER_CELL_ACTIONS.FILTER_FOR).should('be.visible').trigger('click');
cy.get(DISCOVER_FILTER_BADGES).should('have.length', 1);
cy.get(DISCOVER_FILTER_BADGES)
.first()
.should(
'have.text',
`${TIMESTAMP_COLUMN_NAME}: ${selectedTimestamp} to ${selectedTimestamp}`
);
});
});
it('Filter out', { tags: ['@brokenInServerless'] }, () => {
cy.get(GET_DISCOVER_DATA_GRID_CELL(TIMESTAMP_COLUMN_NAME, 0)).then((sub) => {
const selectedTimestamp = sub.text();
cy.get(GET_DISCOVER_DATA_GRID_CELL(TIMESTAMP_COLUMN_NAME, 0)).realHover();
cy.get(DISCOVER_CELL_ACTIONS.FILTER_OUT).should('be.visible').trigger('click');
cy.get(DISCOVER_FILTER_BADGES).should('have.length', 1);
cy.get(DISCOVER_FILTER_BADGES)
.first()
.should(
'have.text',
`NOT ${TIMESTAMP_COLUMN_NAME}: ${selectedTimestamp} to ${selectedTimestamp}`
);
});
});
// @TODO: copy is incredibly flaky although it is written same strategy as above tests
// Need to see what is the reaosn for that. Trusting that above tests prove that `Copy`
// will also work correctly.

View file

@ -5,19 +5,15 @@
* 2.0.
*/
import { fillAddFilterForm } from '../../../../tasks/search_bar';
import {
addDiscoverKqlQuery,
addDiscoverEsqlQuery,
addFieldToTable,
openAddDiscoverFilterPopover,
submitDiscoverSearchBar,
switchDataViewTo,
verifyDiscoverEsqlQuery,
} from '../../../../tasks/discover';
import { navigateFromHeaderTo } from '../../../../tasks/security_header';
import {
DISCOVER_CONTAINER,
DISCOVER_QUERY_INPUT,
DISCOVER_FILTER_BADGES,
DISCOVER_DATA_VIEW_SWITCHER,
GET_DISCOVER_DATA_GRID_CELL_HEADER,
} from '../../../../screens/discover';
@ -34,14 +30,14 @@ import { ALERTS, CSP_FINDINGS } from '../../../../screens/security_header';
const INITIAL_START_DATE = 'Jan 18, 2021 @ 20:33:29.186';
const INITIAL_END_DATE = 'Jan 19, 2024 @ 20:33:29.186';
const DEFAULT_ESQL_QUERY =
'from .alerts-security.alerts-default,apm-*-transaction*,auditbeat-*,endgame-*,filebeat-*,logs-*,packetbeat-*,traces-apm*,winlogbeat-*,-*elastic-cloud-logs-* | limit 10';
// FLAKY: https://github.com/elastic/kibana/issues/165663
// FLAKY: https://github.com/elastic/kibana/issues/165747
describe(
'Discover State',
{
env: { ftrConfig: { enableExperimental: ['discoverInTimeline'] } },
tags: ['@ess', '@serverless', '@brokenInServerless'],
tags: ['@ess'],
},
() => {
beforeEach(() => {
@ -51,36 +47,22 @@ describe(
gotToDiscoverTab();
updateDateRangeInLocalDatePickers(DISCOVER_CONTAINER, INITIAL_START_DATE, INITIAL_END_DATE);
});
it('should remember kql query when navigating away and back to discover ', () => {
const kqlQuery = '_id:*';
addDiscoverKqlQuery(kqlQuery);
it('should not allow the dataview to be changed', () => {
cy.get(DISCOVER_DATA_VIEW_SWITCHER.BTN).should('not.exist');
});
it('should have the default esql query on load', () => {
verifyDiscoverEsqlQuery(DEFAULT_ESQL_QUERY);
});
it('should remember esql query when navigating away and back to discover ', () => {
const esqlQuery = 'from auditbeat-* | limit 5';
addDiscoverEsqlQuery(esqlQuery);
submitDiscoverSearchBar();
navigateFromHeaderTo(CSP_FINDINGS);
navigateFromHeaderTo(ALERTS);
openActiveTimeline();
gotToDiscoverTab();
cy.get(DISCOVER_QUERY_INPUT).should('have.text', kqlQuery);
});
it('should remember filters when navigating away and back to discover ', () => {
openAddDiscoverFilterPopover();
fillAddFilterForm({
key: 'agent.type',
value: 'winlogbeat',
});
navigateFromHeaderTo(CSP_FINDINGS);
navigateFromHeaderTo(ALERTS);
openActiveTimeline();
gotToDiscoverTab();
cy.get(DISCOVER_FILTER_BADGES).should('have.length', 1);
});
it.skip('should remember dataView when navigating away and back to discover ', () => {
const dataviewName = '.kibana-event-log';
switchDataViewTo(dataviewName);
navigateFromHeaderTo(CSP_FINDINGS);
navigateFromHeaderTo(ALERTS);
openActiveTimeline();
gotToDiscoverTab();
cy.get(DISCOVER_DATA_VIEW_SWITCHER.BTN).should('contain.text', dataviewName);
verifyDiscoverEsqlQuery(esqlQuery);
});
it('should remember columns when navigating away and back to discover ', () => {
addFieldToTable('host.name');

View file

@ -13,13 +13,10 @@ import {
navigateFromKibanaCollapsibleTo,
openKibanaNavigation,
} from '../../../../tasks/kibana_navigation';
import { fillAddFilterForm } from '../../../../tasks/search_bar';
import {
addDiscoverKqlQuery,
addDiscoverEsqlQuery,
addFieldToTable,
openAddDiscoverFilterPopover,
switchDataViewTo,
switchDataViewToESQL,
verifyDiscoverEsqlQuery,
} from '../../../../tasks/discover';
import {
GET_LOCAL_DATE_PICKER_START_DATE_POPOVER_BUTTON,
@ -29,8 +26,6 @@ import { ALERTS_URL } from '../../../../urls/navigation';
import {
DISCOVER_CONTAINER,
DISCOVER_DATA_VIEW_SWITCHER,
DISCOVER_FILTER_BADGES,
DISCOVER_QUERY_INPUT,
GET_DISCOVER_DATA_GRID_CELL_HEADER,
} from '../../../../screens/discover';
import { updateDateRangeInLocalDatePickers } from '../../../../tasks/date_picker';
@ -63,6 +58,7 @@ const TIMELINE_PATCH_REQ = 'TIMELINE_PATCH_REQ';
const TIMELINE_RESPONSE_SAVED_OBJECT_ID_PATH =
'response.body.data.persistTimeline.timeline.savedObjectId';
const esqlQuery = 'from auditbeat-* | where ecs.version == "8.0.0"';
describe(
'Discover Timeline State Integration',
@ -126,19 +122,11 @@ describe(
);
});
it('should save/restore discover dataview/timerange/filter/query/columns when saving/resoring timeline', () => {
const dataviewName = '.kibana-event-log';
const timelineSuffix = Date.now();
const timelineName = `DataView timeline-${timelineSuffix}`;
const kqlQuery = '_id:*';
const column1 = 'event.category';
const column2 = 'ecs.version';
switchDataViewTo(dataviewName);
addDiscoverKqlQuery(kqlQuery);
openAddDiscoverFilterPopover();
fillAddFilterForm({
key: 'ecs.version',
value: '1.8.0',
});
addDiscoverEsqlQuery(esqlQuery);
addFieldToTable(column1);
addFieldToTable(column2);
@ -155,10 +143,7 @@ describe(
openTimelineById(timelineId);
cy.get(LOADING_INDICATOR).should('not.exist');
gotToDiscoverTab();
cy.get(DISCOVER_DATA_VIEW_SWITCHER.BTN).should('contain.text', dataviewName);
cy.get(DISCOVER_QUERY_INPUT).should('have.text', kqlQuery);
cy.get(DISCOVER_FILTER_BADGES).should('have.length', 1);
cy.get(DISCOVER_FILTER_BADGES).should('contain.text', 'ecs.version: 1.8.0');
verifyDiscoverEsqlQuery(esqlQuery);
cy.get(GET_DISCOVER_DATA_GRID_CELL_HEADER(column1)).should('exist');
cy.get(GET_DISCOVER_DATA_GRID_CELL_HEADER(column2)).should('exist');
cy.get(GET_LOCAL_DATE_PICKER_START_DATE_POPOVER_BUTTON(DISCOVER_CONTAINER)).should(
@ -168,19 +153,11 @@ describe(
});
});
it('should save/restore discover dataview/timerange/filter/query/columns when timeline is opened via url', () => {
const dataviewName = '.kibana-event-log';
const timelineSuffix = Date.now();
const timelineName = `DataView timeline-${timelineSuffix}`;
const kqlQuery = '_id:*';
const column1 = 'event.category';
const column2 = 'ecs.version';
switchDataViewTo(dataviewName);
addDiscoverKqlQuery(kqlQuery);
openAddDiscoverFilterPopover();
fillAddFilterForm({
key: 'ecs.version',
value: '1.8.0',
});
addDiscoverEsqlQuery(esqlQuery);
addFieldToTable(column1);
addFieldToTable(column2);
@ -192,10 +169,7 @@ describe(
cy.wait(`@${TIMELINE_REQ_WITH_SAVED_SEARCH}`);
// reload the page with the exact url
cy.reload();
cy.get(DISCOVER_DATA_VIEW_SWITCHER.BTN).should('contain.text', dataviewName);
cy.get(DISCOVER_QUERY_INPUT).should('have.text', kqlQuery);
cy.get(DISCOVER_FILTER_BADGES).should('have.length', 1);
cy.get(DISCOVER_FILTER_BADGES).should('contain.text', 'ecs.version: 1.8.0');
verifyDiscoverEsqlQuery(esqlQuery);
cy.get(GET_DISCOVER_DATA_GRID_CELL_HEADER(column1)).should('exist');
cy.get(GET_DISCOVER_DATA_GRID_CELL_HEADER(column2)).should('exist');
cy.get(GET_LOCAL_DATE_PICKER_START_DATE_POPOVER_BUTTON(DISCOVER_CONTAINER)).should(
@ -207,7 +181,6 @@ describe(
it('should save/restore discover ES|QL when saving timeline', () => {
const timelineSuffix = Date.now();
const timelineName = `ES|QL timeline-${timelineSuffix}`;
switchDataViewToESQL();
addNameToTimeline(timelineName);
cy.wait(`@${TIMELINE_PATCH_REQ}`)
.its(TIMELINE_RESPONSE_SAVED_OBJECT_ID_PATH)
@ -220,7 +193,7 @@ describe(
openTimelineById(timelineId);
cy.get(LOADING_INDICATOR).should('not.exist');
gotToDiscoverTab();
cy.get(DISCOVER_DATA_VIEW_SWITCHER.BTN).should('contain.text', 'ES|QL');
cy.get(DISCOVER_DATA_VIEW_SWITCHER.BTN).should('not.exist');
});
});
});
@ -235,8 +208,7 @@ describe(
it('should save discover saved search with `Security Solution` tag', () => {
const timelineSuffix = Date.now();
const timelineName = `SavedObject timeline-${timelineSuffix}`;
const kqlQuery = '_id: *';
addDiscoverKqlQuery(kqlQuery);
addDiscoverEsqlQuery(esqlQuery);
addNameToTimeline(timelineName);
cy.wait(`@${TIMELINE_REQ_WITH_SAVED_SEARCH}`);
openKibanaNavigation();
@ -257,8 +229,7 @@ describe(
it('should rename the saved search on timeline rename', () => {
const timelineSuffix = Date.now();
const timelineName = `Rename timeline-${timelineSuffix}`;
const kqlQuery = '_id: *';
addDiscoverKqlQuery(kqlQuery);
addDiscoverEsqlQuery(esqlQuery);
addNameToTimeline(timelineName);
cy.wait(`@${TIMELINE_PATCH_REQ}`)

View file

@ -6,7 +6,6 @@
*/
import { GET_LOCAL_DATE_PICKER_START_DATE_POPOVER_BUTTON } from '../../../../screens/date_picker';
import { fillAddFilterForm, fillAddFilterFormAsQueryDSL } from '../../../../tasks/search_bar';
import {
setStartDate,
updateDateRangeInLocalDatePickers,
@ -14,20 +13,15 @@ import {
} from '../../../../tasks/date_picker';
import {
DISCOVER_CONTAINER,
DISCOVER_NO_RESULTS,
DISCOVER_RESULT_HITS,
DISCOVER_FILTER_BADGES,
DISCOVER_QUERY_INPUT,
GET_DISCOVER_DATA_GRID_CELL_HEADER,
DISCOVER_DATA_VIEW_SWITCHER,
DISCOVER_ESQL_INPUT_TEXT_CONTAINER,
} from '../../../../screens/discover';
import {
addDiscoverKqlQuery,
switchDataViewTo,
addDiscoverEsqlQuery,
submitDiscoverSearchBar,
openAddDiscoverFilterPopover,
addFieldToTable,
createAdHocDataView,
convertNBSPToSP,
} from '../../../../tasks/discover';
import { createNewTimeline, gotToDiscoverTab } from '../../../../tasks/timeline';
import { login } from '../../../../tasks/login';
@ -37,13 +31,14 @@ import { ALERTS_URL } from '../../../../urls/navigation';
const INITIAL_START_DATE = 'Jan 18, 2021 @ 20:33:29.186';
const INITIAL_END_DATE = 'Jan 19, 2024 @ 20:33:29.186';
const NEW_START_DATE = 'Jan 18, 2023 @ 20:33:29.186';
const esqlQuery = 'from auditbeat-* | where ecs.version == "8.0.0"';
// Failing: See https://github.com/elastic/kibana/issues/167186
describe.skip(
describe(
'Basic discover search and filter operations',
{
env: { ftrConfig: { enableExperimental: ['discoverInTimeline'] } },
tags: ['@ess', '@serverless'],
tags: ['@ess'],
},
() => {
beforeEach(() => {
@ -53,76 +48,34 @@ describe.skip(
gotToDiscoverTab();
updateDateRangeInLocalDatePickers(DISCOVER_CONTAINER, INITIAL_START_DATE, INITIAL_END_DATE);
});
it('should change data when dataView is changed', () => {
switchDataViewTo('.kibana-event-log');
cy.get(DISCOVER_RESULT_HITS).should('have.text', '1');
});
it('should show data according to kql query', () => {
const kqlQuery = '_id:"invalid"';
addDiscoverKqlQuery(kqlQuery);
it('should show data according to esql query', () => {
addDiscoverEsqlQuery(`${esqlQuery} | limit 1`);
submitDiscoverSearchBar();
cy.get(DISCOVER_NO_RESULTS).should('be.visible');
cy.get(DISCOVER_RESULT_HITS).should('have.text', 1);
});
it('should show correct data according to filter applied', () => {
openAddDiscoverFilterPopover();
fillAddFilterForm({
key: 'agent.type',
value: 'winlogbeat',
});
cy.get(DISCOVER_FILTER_BADGES).should('have.length', 1);
cy.get(DISCOVER_RESULT_HITS).should('have.text', '1');
});
it('should show correct data according to query DSL', () => {
const query = {
bool: {
filter: [
{
term: {
'agent.type': 'winlogbeat',
},
},
],
},
};
openAddDiscoverFilterPopover();
fillAddFilterFormAsQueryDSL(JSON.stringify(query));
cy.get(DISCOVER_FILTER_BADGES).should('have.length', 1);
cy.get(DISCOVER_RESULT_HITS).should('have.text', '1');
});
it('should be able to create ad-hoc dataview without saving', () => {
const adHocDVName = 'adHocDataView';
const indexPattern = 'audit';
createAdHocDataView(adHocDVName, indexPattern);
cy.get(DISCOVER_DATA_VIEW_SWITCHER.BTN).should('contain.text', adHocDVName);
});
it('should be able to add fields to the table', () => {
addFieldToTable('host.name');
cy.get(GET_DISCOVER_DATA_GRID_CELL_HEADER('host.name')).should('be.visible');
addFieldToTable('user.name');
cy.get(GET_DISCOVER_DATA_GRID_CELL_HEADER('host.name')).should('be.visible');
cy.get(GET_DISCOVER_DATA_GRID_CELL_HEADER('user.name')).should('be.visible');
});
context('navigation', () => {
it('should remove the filter when back is pressed after adding a filter', () => {
openAddDiscoverFilterPopover();
fillAddFilterForm({
key: 'agent.type',
value: 'winlogbeat',
});
cy.get(DISCOVER_FILTER_BADGES).should('have.length', 1);
cy.go('back');
cy.get(DISCOVER_FILTER_BADGES).should('not.exist');
});
it('should removed the query when back is pressed after adding a query', () => {
const kqlQuery = '_id:"invalid"';
addDiscoverKqlQuery(kqlQuery);
addDiscoverEsqlQuery(esqlQuery);
submitDiscoverSearchBar();
cy.get(DISCOVER_QUERY_INPUT).should('have.text', kqlQuery);
cy.get(DISCOVER_ESQL_INPUT_TEXT_CONTAINER).then((subj) => {
const currentQuery = subj.text();
const sanitizedQuery = convertNBSPToSP(currentQuery);
expect(sanitizedQuery).to.eq(esqlQuery);
});
cy.go('back');
cy.get(DISCOVER_QUERY_INPUT).should('not.have.text', kqlQuery);
cy.get(DISCOVER_ESQL_INPUT_TEXT_CONTAINER).then((subj) => {
const currentQuery = subj.text();
const sanitizedQuery = convertNBSPToSP(currentQuery);
expect(sanitizedQuery).to.not.eq(esqlQuery);
});
});
it(`should changed the timerange to ${INITIAL_START_DATE} when back is pressed after modifying timerange from ${INITIAL_START_DATE} to ${NEW_START_DATE} `, () => {

View file

@ -26,10 +26,14 @@ export const DISCOVER_DATA_VIEW_EDITOR_FLYOUT = {
SAVE_DATA_VIEW_BTN: getDataTestSubjectSelector('saveIndexPatternButton'),
};
export const DISCOVER_QUERY_INPUT = `${DISCOVER_CONTAINER} ${getDataTestSubjectSelector(
'unifiedQueryInput'
export const DISCOVER_ESQL_INPUT = `${DISCOVER_CONTAINER} ${getDataTestSubjectSelector(
'kibanaCodeEditor'
)}`;
export const DISCOVER_ESQL_INPUT_TEXT_CONTAINER = `${DISCOVER_ESQL_INPUT} .view-lines`;
export const DISCOVER_ESQL_EDITABLE_INPUT = `${DISCOVER_ESQL_INPUT} textarea:first`;
export const DISCOVER_ADD_FILTER = `${DISCOVER_CONTAINER} ${getDataTestSubjectSelector(
'addFilter'
)}`;

View file

@ -162,7 +162,7 @@ export const setEnrichmentDates = (from?: string, to?: string) => {
cy.get(ENRICHMENT_QUERY_END_INPUT).type(`{selectall}${to}{enter}`);
}
});
cy.get(UPDATE_ENRICHMENT_RANGE_BUTTON).click();
cy.get(UPDATE_ENRICHMENT_RANGE_BUTTON).click({ force: true });
};
export const refreshAlertPageFilter = () => {

View file

@ -10,11 +10,12 @@ import {
DISCOVER_CONTAINER,
DISCOVER_DATA_GRID_UPDATING,
DISCOVER_DATA_VIEW_SWITCHER,
DISCOVER_QUERY_INPUT,
DISCOVER_ESQL_INPUT,
GET_DISCOVER_COLUMN_TOGGLE_BTN,
DISCOVER_FIELD_SEARCH,
DISCOVER_DATA_VIEW_EDITOR_FLYOUT,
DISCOVER_FIELD_LIST_LOADING,
DISCOVER_ESQL_EDITABLE_INPUT,
} from '../screens/discover';
import { GET_LOCAL_SEARCH_BAR_SUBMIT_BUTTON } from '../screens/search_bar';
@ -43,8 +44,35 @@ export const waitForDiscoverGridToLoad = () => {
cy.get(DISCOVER_FIELD_LIST_LOADING).should('not.exist');
};
export const addDiscoverKqlQuery = (kqlQuery: string) => {
cy.get(DISCOVER_QUERY_INPUT).type(`${kqlQuery}{enter}`);
export const selectCurrentDiscoverEsqlQuery = (discoverEsqlInput = DISCOVER_ESQL_INPUT) => {
cy.get(discoverEsqlInput).click();
cy.get(discoverEsqlInput).focused();
cy.get(discoverEsqlInput).type(Cypress.platform === 'darwin' ? '{cmd+a}' : '{ctrl+a}');
};
export const addDiscoverEsqlQuery = (esqlQuery: string) => {
// ESQL input uses the monaco editor which doesn't allow for traditional input updates
selectCurrentDiscoverEsqlQuery(DISCOVER_ESQL_EDITABLE_INPUT);
cy.get(DISCOVER_ESQL_EDITABLE_INPUT).clear();
cy.get(DISCOVER_ESQL_EDITABLE_INPUT).type(`${esqlQuery}`);
cy.get(DISCOVER_ESQL_EDITABLE_INPUT).blur();
cy.get(GET_LOCAL_SEARCH_BAR_SUBMIT_BUTTON(DISCOVER_CONTAINER)).realClick();
};
export const convertNBSPToSP = (str: string) => {
return str.replaceAll(String.fromCharCode(160), ' ');
};
export const verifyDiscoverEsqlQuery = (esqlQueryToVerify: string) => {
// We select the query first as multi-line queries do not render fully unless all the text is selected
selectCurrentDiscoverEsqlQuery();
/**
* When selected all visual spaces actually render the middot character, so we replace the spaces with the middot
* If testing without selecting first you can replace with a Non-breaking space character
* https://github.com/cypress-io/cypress/issues/15863#issuecomment-816746693
*/
const unicodeReplacedQuery = esqlQueryToVerify.replaceAll(' ', '\u00b7');
cy.get(DISCOVER_ESQL_INPUT).should('include.text', unicodeReplacedQuery);
};
export const submitDiscoverSearchBar = () => {
@ -69,6 +97,7 @@ export const clearFieldSearch = () => {
export const addFieldToTable = (fieldId: string) => {
searchForField(fieldId);
cy.get(GET_DISCOVER_COLUMN_TOGGLE_BTN(fieldId)).first().should('exist');
cy.get(GET_DISCOVER_COLUMN_TOGGLE_BTN(fieldId)).first().trigger('click');
clearFieldSearch();
};

View file

@ -102,7 +102,7 @@ export const addDescriptionToTimeline = (
if (!modalAlreadyOpen) {
cy.get(TIMELINE_EDIT_MODAL_OPEN_BUTTON).first().click();
}
cy.get(TIMELINE_DESCRIPTION_INPUT).type(description);
cy.get(TIMELINE_DESCRIPTION_INPUT).should('not.be.disabled').type(description);
cy.get(TIMELINE_DESCRIPTION_INPUT).invoke('val').should('equal', description);
cy.get(TIMELINE_EDIT_MODAL_SAVE_BUTTON).click();
cy.get(TIMELINE_TITLE_INPUT).should('not.exist');
@ -112,7 +112,7 @@ export const addNameToTimeline = (name: string, modalAlreadyOpen: boolean = fals
if (!modalAlreadyOpen) {
cy.get(TIMELINE_EDIT_MODAL_OPEN_BUTTON).first().click();
}
cy.get(TIMELINE_TITLE_INPUT).type(`${name}{enter}`);
cy.get(TIMELINE_TITLE_INPUT).should('not.be.disabled').type(`${name}{enter}`);
cy.get(TIMELINE_TITLE_INPUT).should('have.attr', 'value', name);
cy.get(TIMELINE_EDIT_MODAL_SAVE_BUTTON).click();
cy.get(TIMELINE_TITLE_INPUT).should('not.exist');
@ -315,7 +315,7 @@ export const createNewTimeline = () => {
cy.get(TIMELINE_SETTINGS_ICON).should('be.visible');
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(300);
cy.get(CREATE_NEW_TIMELINE).eq(0).click();
cy.get(CREATE_NEW_TIMELINE).eq(0).should('be.visible').click();
};
export const openCreateTimelineOptionsPopover = () => {