[TIP] Unify filters with the rest of Security Solution (#142336)

This commit is contained in:
Luke Gmys 2022-10-03 18:26:10 +02:00 committed by GitHub
parent 5d82869ca0
commit 824b7f307c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 73 additions and 787 deletions

View file

@ -11,8 +11,9 @@ import type { SecuritySolutionPluginContext } from '@kbn/threat-intelligence-plu
import { THREAT_INTELLIGENCE_BASE_PATH } from '@kbn/threat-intelligence-plugin/public';
import type { SourcererDataView } from '@kbn/threat-intelligence-plugin/public/types';
import type { Store } from 'redux';
import { useSelector } from 'react-redux';
import { useInvestigateInTimeline } from './use_investigate_in_timeline';
import { getStore } from '../common/store';
import { getStore, inputsSelectors } from '../common/store';
import { useKibana } from '../common/lib/kibana';
import { FiltersGlobal } from '../common/components/filters_global';
import { SpyRoute } from '../common/utils/route/spy_routes';
@ -21,6 +22,8 @@ import { SecurityPageName } from '../app/types';
import type { SecuritySubPluginRoutes } from '../app/types';
import { useSourcererDataView } from '../common/containers/sourcerer';
import { SecuritySolutionPageWrapper } from '../common/components/page_wrapper';
import { SiemSearchBar } from '../common/components/search_bar';
import { useGlobalTime } from '../common/containers/use_global_time';
const ThreatIntelligence = memo(() => {
const { threatIntelligence } = useKibana().services;
@ -37,6 +40,12 @@ const ThreatIntelligence = memo(() => {
sourcererDataView: sourcererDataView as unknown as SourcererDataView,
getSecuritySolutionStore: securitySolutionStore,
getUseInvestigateInTimeline: useInvestigateInTimeline,
useQuery: () => useSelector(inputsSelectors.globalQuerySelector()),
useFilters: () => useSelector(inputsSelectors.globalFiltersQuerySelector()),
useGlobalTime,
SiemSearchBar,
};
return (

View file

@ -12,17 +12,11 @@
"requiredPlugins": [
"data",
"dataViews",
"unifiedSearch",
"kibanaUtils",
"navigation",
"kibanaReact",
"triggersActionsUi",
"inspector"
],
"requiredBundles": [
"data",
"unifiedSearch",
"kibanaUtils",
"kibanaReact"
]
"requiredBundles": ["data", "kibanaUtils", "kibanaReact"]
}

View file

@ -8,6 +8,8 @@
import { FilterManager } from '@kbn/data-plugin/public';
import { IndicatorsFiltersContextValue } from '../../modules/indicators/containers/indicators_filters/context';
export const mockTimeRange = { from: '2022-10-03T07:48:31.498Z', to: '2022-10-03T07:48:31.498Z' };
export const mockIndicatorsFiltersContext: IndicatorsFiltersContextValue = {
filterManager: {
getFilters: () => [],
@ -18,7 +20,5 @@ export const mockIndicatorsFiltersContext: IndicatorsFiltersContextValue = {
language: 'kuery',
query: '',
},
handleSavedQuery: () => {},
handleSubmitQuery: () => {},
handleSubmitTimeRange: () => {},
timeRange: mockTimeRange,
};

View file

@ -36,4 +36,12 @@ export const getSecuritySolutionContextMock = (): SecuritySolutionPluginContext
({ dataProviders, from, to }) =>
() =>
new Promise((resolve) => window.alert('investigate in timeline')),
SiemSearchBar: () => <div data-test-subj="SiemSearchBar">mock siem search</div>,
useFilters: () => [],
useGlobalTime: () => ({ from: '', to: '' }),
useQuery: () => ({ language: 'kuery', query: '' }),
});

View file

@ -16,7 +16,6 @@ import { DataPublicPluginStart } from '@kbn/data-plugin/public';
import { IUiSettingsClient } from '@kbn/core/public';
import { StoryProvidersComponent } from '../../../../common/mocks/story_providers';
import { mockKibanaTimelinesService } from '../../../../common/mocks/mock_kibana_timelines_service';
import { DEFAULT_TIME_RANGE } from '../../../query_bar/hooks/use_filters/utils';
import { IndicatorsBarChartWrapper } from '.';
import { Aggregation, AGGREGATION_NAME, ChartSeries } from '../../services';
@ -25,7 +24,7 @@ export default {
title: 'IndicatorsBarChartWrapper',
};
const mockTimeRange: TimeRange = DEFAULT_TIME_RANGE;
const mockTimeRange: TimeRange = { from: '', to: '' };
const mockIndexPattern: DataView = {
fields: [
@ -161,16 +160,6 @@ InitialLoad.decorators = [(story) => <MemoryRouter>{story()}</MemoryRouter>];
export const UpdatingData: Story<void> = () => {
const mockIndicators: ChartSeries[] = [
{
x: '1 Jan 2022 00:00:00 GMT',
y: 2,
g: '[Filebeat] AbuseCH Malware',
},
{
x: '1 Jan 2022 00:00:00 GMT',
y: 10,
g: '[Filebeat] AbuseCH MalwareBazaar',
},
{
x: '1 Jan 2022 06:00:00 GMT',
y: 0,

View file

@ -11,7 +11,6 @@ import { TimeRange } from '@kbn/es-query';
import { DataView, DataViewField } from '@kbn/data-views-plugin/common';
import { TestProvidersComponent } from '../../../../common/mocks/test_providers';
import { CHART_UPDATE_PROGRESS_TEST_ID, IndicatorsBarChartWrapper } from '.';
import { DEFAULT_TIME_RANGE } from '../../../query_bar/hooks/use_filters/utils';
import moment from 'moment';
jest.mock('../../../query_bar/hooks/use_filters');
@ -29,7 +28,7 @@ const mockIndexPattern: DataView = {
],
} as DataView;
const mockTimeRange: TimeRange = DEFAULT_TIME_RANGE;
const mockTimeRange: TimeRange = { from: '', to: '' };
describe('<IndicatorsBarChartWrapper />', () => {
describe('when not loading or refetching', () => {

View file

@ -6,18 +6,14 @@
*/
import { createContext } from 'react';
import { FilterManager, SavedQuery } from '@kbn/data-plugin/public';
import { Filter, Query, TimeRange } from '@kbn/es-query';
import { FilterManager } from '@kbn/data-plugin/public';
export interface IndicatorsFiltersContextValue {
timeRange?: TimeRange;
timeRange: TimeRange;
filters: Filter[];
filterQuery: Query;
handleSavedQuery: (savedQuery: SavedQuery | undefined) => void;
handleSubmitTimeRange: (timeRange?: TimeRange) => void;
handleSubmitQuery: (filterQuery: Query) => void;
filterManager: FilterManager;
savedQuery?: SavedQuery;
}
export const IndicatorsFiltersContext = createContext<IndicatorsFiltersContextValue | undefined>(

View file

@ -5,29 +5,16 @@
* 2.0.
*/
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { useHistory, useLocation } from 'react-router-dom';
import { SavedQuery } from '@kbn/data-plugin/common';
import { Filter, Query, TimeRange } from '@kbn/es-query';
import deepEqual from 'fast-deep-equal';
import { IndicatorsFiltersContext, IndicatorsFiltersContextValue } from './context';
import React, { FC, useMemo } from 'react';
import { useKibana } from '../../../../hooks/use_kibana';
import {
DEFAULT_QUERY,
DEFAULT_TIME_RANGE,
encodeState,
FILTERS_QUERYSTRING_NAMESPACE,
stateFromQueryParams,
} from '../../../query_bar/hooks/use_filters/utils';
import { useSecurityContext } from '../../../../hooks/use_security_context';
import { IndicatorsFiltersContext, IndicatorsFiltersContextValue } from './context';
/**
* Container used to wrap components and share the {@link FilterManager} through React context.
*/
export const IndicatorsFilters: FC = ({ children }) => {
const { pathname: browserPathName, search } = useLocation();
const history = useHistory();
const [savedQuery, setSavedQuery] = useState<SavedQuery | undefined>(undefined);
const securityContext = useSecurityContext();
const {
services: {
@ -37,72 +24,18 @@ export const IndicatorsFilters: FC = ({ children }) => {
},
} = useKibana();
// Filters are picked using the UI widgets
const [filters, setFilters] = useState<Filter[]>([]);
// Time range is self explanatory
const [timeRange, setTimeRange] = useState<TimeRange | undefined>(DEFAULT_TIME_RANGE);
// filterQuery is raw kql query that user can type in to filter results
const [filterQuery, setFilterQuery] = useState<Query>(DEFAULT_QUERY);
// Serialize filters into query string
useEffect(() => {
const filterStateAsString = encodeState({ filters, filterQuery, timeRange });
if (!deepEqual(filterManager.getFilters(), filters)) {
filterManager.setFilters(filters);
}
history.replace({
pathname: browserPathName,
search: `${FILTERS_QUERYSTRING_NAMESPACE}=${filterStateAsString}`,
});
}, [browserPathName, filterManager, filterQuery, filters, history, timeRange]);
// Sync filterManager to local state (after they are changed from the ui)
useEffect(() => {
const subscription = filterManager.getUpdates$().subscribe(() => {
setFilters(filterManager.getFilters());
});
return () => subscription.unsubscribe();
}, [filterManager]);
// Update local state with filter values from the url (on initial mount)
useEffect(() => {
const {
filters: filtersFromQuery,
timeRange: timeRangeFromQuery,
filterQuery: filterQueryFromQuery,
} = stateFromQueryParams(search);
setTimeRange(timeRangeFromQuery);
setFilterQuery(filterQueryFromQuery);
setFilters(filtersFromQuery);
// We only want to have it done on initial render with initial 'search' value;
// that is why 'search' is ommited from the deps array
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filterManager]);
const onSavedQuery = useCallback(
(newSavedQuery: SavedQuery | undefined) => setSavedQuery(newSavedQuery),
[]
);
const globalFilters = securityContext.useFilters();
const globalQuery = securityContext.useQuery();
const globalTimeRange = securityContext.useGlobalTime();
const contextValue: IndicatorsFiltersContextValue = useMemo(
() => ({
timeRange,
filters,
filterQuery,
handleSavedQuery: onSavedQuery,
handleSubmitTimeRange: setTimeRange,
handleSubmitQuery: setFilterQuery,
timeRange: globalTimeRange,
filters: globalFilters,
filterQuery: globalQuery,
filterManager,
savedQuery,
}),
[filterManager, filterQuery, filters, onSavedQuery, savedQuery, timeRange]
[globalFilters, globalQuery, globalTimeRange, filterManager]
);
return (

View file

@ -7,17 +7,17 @@
import { act, renderHook } from '@testing-library/react-hooks';
import { useAggregatedIndicators, UseAggregatedIndicatorsParam } from './use_aggregated_indicators';
import { DEFAULT_TIME_RANGE } from '../../query_bar/hooks/use_filters/utils';
import {
mockedTimefilterService,
TestProvidersComponent,
} from '../../../common/mocks/test_providers';
import { createFetchAggregatedIndicators } from '../services';
import { mockTimeRange } from '../../../common/mocks/mock_indicators_filters_context';
jest.mock('../services/fetch_aggregated_indicators');
const useAggregatedIndicatorsParams: UseAggregatedIndicatorsParam = {
timeRange: DEFAULT_TIME_RANGE,
timeRange: mockTimeRange,
filters: [],
filterQuery: { language: 'kuery', query: '' },
};
@ -28,8 +28,7 @@ const renderUseAggregatedIndicators = () =>
wrapper: TestProvidersComponent,
});
// FLAKY: https://github.com/elastic/kibana/issues/142312
describe.skip('useAggregatedIndicators()', () => {
describe('useAggregatedIndicators()', () => {
beforeEach(jest.clearAllMocks);
type MockedCreateFetchAggregatedIndicators = jest.MockedFunction<
@ -51,7 +50,7 @@ describe.skip('useAggregatedIndicators()', () => {
it('should create and call the aggregatedIndicatorsQuery correctly', async () => {
aggregatedIndicatorsQuery.mockResolvedValue([]);
const { result, rerender } = renderUseAggregatedIndicators();
const { result, rerender, waitFor } = renderUseAggregatedIndicators();
// indicators service and the query should be called just once
expect(
@ -73,6 +72,7 @@ describe.skip('useAggregatedIndicators()', () => {
rerender({
filterQuery: { language: 'kuery', query: "threat.indicator.type: 'file'" },
filters: [],
timeRange: mockTimeRange,
})
);
@ -83,14 +83,17 @@ describe.skip('useAggregatedIndicators()', () => {
}),
expect.any(AbortSignal)
);
await waitFor(() => !result.current.isLoading);
expect(result.current).toMatchInlineSnapshot(`
Object {
"dateRange": Object {
"max": "2022-01-02T00:00:00.000Z",
"min": "2022-01-01T00:00:00.000Z",
},
"isFetching": true,
"isLoading": true,
"isFetching": false,
"isLoading": false,
"onFieldChange": [Function],
"selectedField": "threat.feed.name",
"series": Array [],

View file

@ -12,7 +12,6 @@ import { TimeRangeBounds } from '@kbn/data-plugin/common';
import { useInspector } from '../../../hooks/use_inspector';
import { RawIndicatorFieldId } from '../../../../common/types/indicator';
import { useKibana } from '../../../hooks/use_kibana';
import { DEFAULT_TIME_RANGE } from '../../query_bar/hooks/use_filters/utils';
import { useSourcererDataView } from '.';
import {
ChartSeries,
@ -25,10 +24,7 @@ export interface UseAggregatedIndicatorsParam {
* From and To values passed to the {@link useAggregatedIndicators} hook
* to query indicators for the Indicators barchart.
*/
timeRange?: TimeRange;
/**
* Filters data passed to the {@link useAggregatedIndicators} hook to query indicators.
*/
timeRange: TimeRange;
filters: Filter[];
/**
* Query data passed to the {@link useAggregatedIndicators} hook to query indicators.
@ -66,7 +62,7 @@ export interface UseAggregatedIndicatorsValue {
const DEFAULT_FIELD = RawIndicatorFieldId.Feed;
export const useAggregatedIndicators = ({
timeRange = DEFAULT_TIME_RANGE,
timeRange,
filters,
filterQuery,
}: UseAggregatedIndicatorsParam): UseAggregatedIndicatorsValue => {

View file

@ -9,6 +9,7 @@ import { renderHook, act } from '@testing-library/react-hooks';
import { useIndicators, UseIndicatorsParams, UseIndicatorsValue } from './use_indicators';
import { TestProvidersComponent } from '../../../common/mocks/test_providers';
import { createFetchIndicators } from '../services/fetch_indicators';
import { mockTimeRange } from '../../../common/mocks/mock_indicators_filters_context';
jest.mock('../services/fetch_indicators');
@ -16,6 +17,7 @@ const useIndicatorsParams: UseIndicatorsParams = {
filters: [],
filterQuery: { query: '', language: 'kuery' },
sorting: [],
timeRange: mockTimeRange,
};
const indicatorsQueryResult = { indicators: [], total: 0 };
@ -99,13 +101,15 @@ describe('useIndicators()', () => {
expect.any(AbortSignal)
);
await hookResult.waitFor(() => !hookResult.result.current.isLoading);
expect(hookResult.result.current).toMatchInlineSnapshot(`
Object {
"handleRefresh": [Function],
"indicatorCount": 0,
"indicators": Array [],
"isFetching": true,
"isLoading": true,
"isFetching": false,
"isLoading": false,
"onChangeItemsPerPage": [Function],
"onChangePage": [Function],
"pagination": Object {

View file

@ -22,7 +22,7 @@ export const DEFAULT_PAGE_SIZE = PAGE_SIZES[1];
export interface UseIndicatorsParams {
filterQuery: Query;
filters: Filter[];
timeRange?: TimeRange;
timeRange: TimeRange;
sorting: EuiDataGridSorting['columns'];
}

View file

@ -14,6 +14,7 @@ import { useFilters } from '../query_bar/hooks/use_filters';
import moment from 'moment';
import { TestProvidersComponent } from '../../common/mocks/test_providers';
import { TABLE_TEST_ID } from './components/indicators_table';
import { mockTimeRange } from '../../common/mocks/mock_indicators_filters_context';
jest.mock('../query_bar/hooks/use_filters');
jest.mock('./hooks/use_indicators');
@ -47,9 +48,7 @@ describe('<IndicatorsPage />', () => {
filters: [],
filterQuery: { language: 'kuery', query: '' },
filterManager: {} as any,
handleSavedQuery: stub,
handleSubmitQuery: stub,
handleSubmitTimeRange: stub,
timeRange: mockTimeRange,
});
});
@ -59,9 +58,9 @@ describe('<IndicatorsPage />', () => {
expect(queryByTestId(TABLE_TEST_ID)).toBeInTheDocument();
});
it('should render the query input', () => {
it('should render SIEM Search Bar', () => {
const { queryByTestId } = render(<IndicatorsPage />, { wrapper: TestProvidersComponent });
expect(queryByTestId('iocListPageQueryInput')).toBeInTheDocument();
expect(queryByTestId('SiemSearchBar')).toBeInTheDocument();
});
it('should render stack by selector', () => {

View file

@ -13,13 +13,13 @@ import { useIndicators } from './hooks/use_indicators';
import { DefaultPageLayout } from '../../components/layout';
import { useFilters } from '../query_bar/hooks/use_filters';
import { FiltersGlobal } from '../../containers/filters_global';
import QueryBar from '../query_bar/components/query_bar';
import { useSourcererDataView } from './hooks/use_sourcerer_data_view';
import { FieldTypesProvider } from '../../containers/field_types_provider';
import { InspectorProvider } from '../../containers/inspector';
import { useColumnSettings } from './components/indicators_table/hooks/use_column_settings';
import { useAggregatedIndicators } from './hooks/use_aggregated_indicators';
import { IndicatorsFilters } from './containers/indicators_filters';
import { useSecurityContext } from '../../hooks/use_security_context';
const queryClient = new QueryClient();
@ -38,19 +38,9 @@ const IndicatorsPageContent: VFC = () => {
const columnSettings = useColumnSettings();
const {
timeRange,
filters,
filterManager,
filterQuery,
handleSubmitQuery,
handleSubmitTimeRange,
handleSavedQuery,
savedQuery,
} = useFilters();
const { timeRange, filters, filterQuery } = useFilters();
const {
handleRefresh,
indicatorCount,
indicators,
onChangeItemsPerPage,
@ -78,25 +68,13 @@ const IndicatorsPageContent: VFC = () => {
filterQuery,
});
const { SiemSearchBar } = useSecurityContext();
return (
<FieldTypesProvider>
<DefaultPageLayout pageTitle="Indicators">
<FiltersGlobal>
<QueryBar
dateRangeFrom={timeRange?.from}
dateRangeTo={timeRange?.to}
indexPattern={indexPattern}
filterQuery={filterQuery}
filterManager={filterManager}
filters={filters}
dataTestSubj="iocListPageQueryInput"
displayStyle="detached"
savedQuery={savedQuery}
onRefresh={handleRefresh}
onSubmitQuery={handleSubmitQuery}
onSavedQuery={handleSavedQuery}
onSubmitDateRange={handleSubmitTimeRange}
/>
<SiemSearchBar indexPattern={indexPattern} id="global" />
</FiltersGlobal>
<IndicatorsBarChartWrapper
dateRange={dateRange}

View file

@ -1,11 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export * from './query_bar';
// eslint-disable-next-line import/no-default-export
export { QueryBar as default } from './query_bar';

View file

@ -1,49 +0,0 @@
/*
* 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 { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import { ComponentStory } from '@storybook/react';
import { KibanaContextProvider } from '../../../../hooks/use_kibana';
import { QueryBar } from './query_bar';
export default {
component: QueryBar,
title: 'QueryBar',
argTypes: {
onSubmitQuery: { action: 'onSubmitQuery' },
onChangedQuery: { action: 'onChangedQuery' },
onChangedDateRange: { action: 'onChangedDateRange' },
onSubmitDateRange: { action: 'onSubmitDateRange' },
onSavedQuery: { action: 'onSavedQuery' },
onRefresh: { action: 'onRefresh' },
},
};
const services = {
data: { query: {} },
storage: new Storage(localStorage),
uiSettings: { get: () => {} },
};
const Template: ComponentStory<typeof QueryBar> = (args) => (
<IntlProvider>
<KibanaContextProvider services={services}>
<QueryBar {...args} />{' '}
</KibanaContextProvider>
</IntlProvider>
);
export const Basic = Template.bind({});
Basic.args = {
indexPattern: {} as any,
filterManager: {} as any,
filters: [],
filterQuery: { language: 'kuery', query: '' },
};

View file

@ -1,83 +0,0 @@
/*
* 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 { render, screen, act, waitFor } from '@testing-library/react';
import { QueryBar } from './query_bar';
import userEvent from '@testing-library/user-event';
import { FilterManager } from '@kbn/data-plugin/public';
import { coreMock } from '@kbn/core/public/mocks';
import { TestProvidersComponent } from '../../../../common/mocks/test_providers';
import { getByTestSubj } from '../../../../../common/test/utils';
const mockUiSettingsForFilterManager = coreMock.createStart().uiSettings;
const filterManager = new FilterManager(mockUiSettingsForFilterManager);
describe('QueryBar ', () => {
const onSubmitQuery = jest.fn();
const onSubmitDateRange = jest.fn();
const onSavedQuery = jest.fn();
const onChangedQuery = jest.fn();
beforeEach(async () => {
await act(async () => {
render(
<TestProvidersComponent>
<QueryBar
filterQuery={{ query: '', language: 'kuery' }}
indexPattern={{ fields: [] } as any}
filterManager={filterManager}
filters={[]}
onRefresh={jest.fn()}
onSubmitQuery={onSubmitQuery}
onChangedQuery={onChangedQuery}
onSubmitDateRange={onSubmitDateRange}
onSavedQuery={onSavedQuery}
/>
</TestProvidersComponent>
);
});
// Some parts of this are lazy loaded, we need to wait for quert input to appear before tests can be done
await waitFor(() => screen.queryByRole('input'));
});
it('should call onSubmitDateRange when date range is changed', async () => {
expect(getByTestSubj('superDatePickerToggleQuickMenuButton')).toBeInTheDocument();
await act(async () => {
userEvent.click(getByTestSubj('superDatePickerToggleQuickMenuButton'));
});
await act(async () => {
screen.getByText('Apply').click();
});
expect(onSubmitDateRange).toHaveBeenCalled();
});
it('should call onSubmitQuery when query is changed', async () => {
const queryInput = getByTestSubj('queryInput');
await act(async () => {
userEvent.type(queryInput, 'one_serious_query');
});
expect(onChangedQuery).toHaveBeenCalledWith(
expect.objectContaining({ language: 'kuery', query: expect.any(String) })
);
await act(async () => {
screen.getByText('Refresh').click();
});
expect(onSubmitQuery).toHaveBeenCalled();
});
});

View file

@ -1,179 +0,0 @@
/*
* 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, { memo, useMemo, useCallback } from 'react';
import deepEqual from 'fast-deep-equal';
import { DataView } from '@kbn/data-views-plugin/public';
import type { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query';
import {
FilterManager,
TimeHistory,
SavedQuery,
SavedQueryTimeFilter,
} from '@kbn/data-plugin/public';
import { SearchBar, SearchBarProps } from '@kbn/unified-search-plugin/public';
import { Storage } from '@kbn/kibana-utils-plugin/public';
import { SecuritySolutionDataViewBase } from '../../../../types';
interface QueryPayload {
dateRange: TimeRange;
query?: Query | AggregateQuery;
}
/**
* User defined type guard to verify if we are dealing with Query param
* @param query query param to test
* @returns
*/
const isQuery = (query?: Query | AggregateQuery | null): query is Query => {
return !!query && Object.prototype.hasOwnProperty.call(query, 'query');
};
export interface QueryBarComponentProps {
dataTestSubj?: string;
dateRangeFrom?: string;
dateRangeTo?: string;
hideSavedQuery?: boolean;
indexPattern: SecuritySolutionDataViewBase;
isLoading?: boolean;
isRefreshPaused?: boolean;
filterQuery: Query;
filterManager: FilterManager;
filters: Filter[];
onRefresh: VoidFunction;
onChangedQuery?: (query: Query) => void;
onChangedDateRange?: (dateRange?: TimeRange) => void;
onSubmitQuery: (query: Query, timefilter?: SavedQueryTimeFilter) => void;
onSubmitDateRange: (dateRange?: TimeRange) => void;
refreshInterval?: number;
savedQuery?: SavedQuery;
onSavedQuery: (savedQuery: SavedQuery | undefined) => void;
displayStyle?: SearchBarProps['displayStyle'];
}
export const INDICATOR_FILTER_DROP_AREA = 'indicator-filter-drop-area';
export const QueryBar = memo<QueryBarComponentProps>(
({
dateRangeFrom,
dateRangeTo,
hideSavedQuery = false,
indexPattern,
isLoading = false,
isRefreshPaused,
filterQuery,
filterManager,
filters,
refreshInterval,
savedQuery,
dataTestSubj,
displayStyle,
onChangedQuery,
onSubmitQuery,
onChangedDateRange,
onSubmitDateRange,
onSavedQuery,
onRefresh,
}) => {
const onQuerySubmit = useCallback(
({ query, dateRange }: QueryPayload) => {
if (dateRange != null) {
onSubmitDateRange(dateRange);
}
if (isQuery(query) && !deepEqual(query, filterQuery)) {
onSubmitQuery(query);
} else {
onRefresh();
}
},
[filterQuery, onRefresh, onSubmitDateRange, onSubmitQuery]
);
const onQueryChange = useCallback(
({ query, dateRange }: QueryPayload) => {
if (!onChangedQuery) {
return;
}
if (isQuery(query) && !deepEqual(query, filterQuery)) {
onChangedQuery(query);
}
if (onChangedDateRange && dateRange != null) {
onChangedDateRange(dateRange);
}
},
[filterQuery, onChangedDateRange, onChangedQuery]
);
const onSavedQueryUpdated = useCallback(
(savedQueryUpdated: SavedQuery) => {
const { query: newQuery, filters: newFilters, timefilter } = savedQueryUpdated.attributes;
onSubmitQuery(newQuery, timefilter);
filterManager.setFilters(newFilters || []);
onSavedQuery(savedQueryUpdated);
},
[filterManager, onSubmitQuery, onSavedQuery]
);
const onClearSavedQuery = useCallback(() => {
if (savedQuery != null) {
onSubmitQuery({
query: '',
language: savedQuery.attributes.query.language,
});
filterManager.setFilters([]);
onSavedQuery(undefined);
}
}, [filterManager, onSubmitQuery, onSavedQuery, savedQuery]);
const onFiltersUpdated = useCallback(
(newFilters: Filter[]) => {
return filterManager.setFilters(newFilters);
},
[filterManager]
);
const timeHistory = useMemo(() => new TimeHistory(new Storage(localStorage)), []);
const indexPatterns = useMemo(() => [indexPattern], [indexPattern]);
return (
<SearchBar
showSubmitButton={true}
dateRangeFrom={dateRangeFrom}
dateRangeTo={dateRangeTo}
filters={filters}
indexPatterns={indexPatterns as DataView[]}
isLoading={isLoading}
isRefreshPaused={isRefreshPaused}
query={filterQuery}
onClearSavedQuery={onClearSavedQuery}
onFiltersUpdated={onFiltersUpdated}
onQueryChange={onQueryChange}
onQuerySubmit={onQuerySubmit}
onSaved={onSavedQuery}
onSavedQueryUpdated={onSavedQueryUpdated}
refreshInterval={refreshInterval}
showAutoRefreshOnly={false}
showFilterBar={!hideSavedQuery}
showDatePicker={true}
showQueryBar={true}
showQueryInput={true}
showSaveQuery={true}
timeHistory={timeHistory}
dataTestSubj={dataTestSubj}
savedQuery={savedQuery}
displayStyle={displayStyle}
/>
);
}
);
QueryBar.displayName = 'QueryBar';

View file

@ -1,165 +0,0 @@
/*
* 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 { mockUseKibanaForFilters } from '../../../../common/mocks/mock_use_kibana_for_filters';
import { renderHook, act, RenderHookResult, Renderer } from '@testing-library/react-hooks';
import { useFilters, UseFiltersValue } from './use_filters';
import { useLocation, useHistory } from 'react-router-dom';
import { Filter } from '@kbn/es-query';
import { TestProvidersComponent } from '../../../../common/mocks/test_providers';
import React, { FC } from 'react';
import { IndicatorsFilters } from '../../../indicators/containers/indicators_filters';
jest.mock('react-router-dom', () => ({
MemoryRouter: ({ children }: any) => <>{children}</>,
useLocation: jest.fn().mockReturnValue({ pathname: '', search: '' }),
useHistory: jest.fn().mockReturnValue({ replace: jest.fn() }),
}));
const FiltersWrapper: FC = ({ children }) => (
<TestProvidersComponent>
<IndicatorsFilters>{children}</IndicatorsFilters>{' '}
</TestProvidersComponent>
);
const renderUseFilters = () => renderHook(() => useFilters(), { wrapper: FiltersWrapper });
describe('useFilters()', () => {
let hookResult: RenderHookResult<{}, UseFiltersValue, Renderer<unknown>>;
let mockRef: ReturnType<typeof mockUseKibanaForFilters>;
afterAll(() => jest.unmock('react-router-dom'));
describe('when mounted', () => {
beforeEach(async () => {
mockRef = mockUseKibanaForFilters();
hookResult = renderUseFilters();
});
it('should have valid initial filterQuery value', () => {
expect(hookResult.result.current.filterQuery).toMatchObject({ language: 'kuery', query: '' });
});
describe('when query string is populated', () => {
it('should try to compute the initial state based on query string', async () => {
(useLocation as jest.Mock).mockReturnValue({
search:
'?indicators=(filterQuery:(language:kuery,query:%27threat.indicator.type%20:%20"file"%20%27),filters:!(),timeRange:(from:now/d,to:now/d))',
});
hookResult = renderUseFilters();
expect(hookResult.result.current.filterQuery).toMatchObject({
language: 'kuery',
query: 'threat.indicator.type : "file" ',
});
});
});
});
describe('when filter values change', () => {
const historyReplace = jest.fn();
beforeEach(async () => {
mockRef = mockUseKibanaForFilters();
hookResult = renderUseFilters();
(useHistory as jest.Mock).mockReturnValue({ replace: historyReplace });
historyReplace.mockClear();
});
describe('when filters change', () => {
it('should update history entry', async () => {
const newFilterEntry = { query: { filter: 'new_filter' }, meta: {} };
// Make sure new filter value is returned from filter manager before signalling an update
// to subscribers
mockRef.getFilters.mockReturnValue([newFilterEntry] as Filter[]);
// Emit the filterManager update to see how it propagates to local component state
await act(async () => {
mockRef.$filterUpdates.next(void 0);
});
// Internally, filters should be loaded from filterManager
expect(mockRef.getFilters).toHaveBeenCalled();
// Serialized into browser query string
expect(historyReplace).toHaveBeenCalledWith(
expect.objectContaining({ search: expect.stringMatching(/new_filter/) })
);
// And updated in local hook state
expect(hookResult.result.current.filters).toContain(newFilterEntry);
});
});
describe('when time range changes', () => {
const newTimeRange = { from: 'dawnOfTime', to: 'endOfTime' };
const updateTime = async () => {
// After new time range is selected
await act(async () => {
hookResult.result.current.handleSubmitTimeRange(newTimeRange);
});
};
it('should update its local state', async () => {
expect(hookResult.result.current.timeRange).toBeDefined();
expect(hookResult.result.current.timeRange).not.toEqual(newTimeRange);
// After new time range is selected
await updateTime();
// Local filter state should be updated
expect(hookResult.result.current.timeRange).toEqual(newTimeRange);
});
it('should update history entry', async () => {
expect(historyReplace).not.toHaveBeenCalledWith(
expect.objectContaining({ search: expect.stringMatching(/dawnOfTime/) })
);
// After new time range is selected
await updateTime();
// Query string should be updated
expect(historyReplace).toHaveBeenCalledWith(
expect.objectContaining({ search: expect.stringMatching(/dawnOfTime/) })
);
});
});
describe('when filterQuery changes', () => {
beforeEach(async () => {
// After new time range is selected
await act(async () => {
hookResult.result.current.handleSubmitQuery({
query: 'threat.indicator.type : *',
language: 'kuery',
});
});
});
it('should update history entry', async () => {
expect(historyReplace).toHaveBeenCalledWith(
expect.objectContaining({ search: expect.stringMatching(/threat\.indicator\.type/) })
);
});
it('should update local state', () => {
expect(hookResult.result.current.filterQuery).toMatchObject({
language: 'kuery',
query: 'threat.indicator.type : *',
});
});
});
});
});

View file

@ -1,68 +0,0 @@
/*
* 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 { stateFromQueryParams } from './utils';
describe('encodeState()', () => {});
describe('stateFromQueryParams()', () => {
it('should return valid state object from invalid query', () => {
expect(stateFromQueryParams('')).toMatchObject({
filterQuery: expect.any(Object),
timeRange: expect.any(Object),
filters: expect.any(Array),
});
});
it('should return valid state when indicators fields is invalid', () => {
expect(stateFromQueryParams('?indicators=')).toMatchObject({
filterQuery: expect.any(Object),
timeRange: expect.any(Object),
filters: expect.any(Array),
});
});
it('should deserialize valid query state', () => {
expect(
stateFromQueryParams(
'?indicators=(filterQuery:(language:kuery,query:%27threat.indicator.type%20:%20"file"%20or%20threat.indicator.type%20:%20"url"%20%27),filters:!((%27$state%27:(store:appState),meta:(alias:!n,disabled:!f,index:%27%27,key:_id,negate:!f,type:exists,value:exists),query:(exists:(field:_id)))),timeRange:(from:now-1y/d,to:now))'
)
).toMatchInlineSnapshot(`
Object {
"filterQuery": Object {
"language": "kuery",
"query": "threat.indicator.type : \\"file\\" or threat.indicator.type : \\"url\\" ",
},
"filters": Array [
Object {
"$state": Object {
"store": "appState",
},
"meta": Object {
"alias": null,
"disabled": false,
"index": "",
"key": "_id",
"negate": false,
"type": "exists",
"value": "exists",
},
"query": Object {
"exists": Object {
"field": "_id",
},
},
},
],
"timeRange": Object {
"from": "now-1y/d",
"to": "now",
},
}
`);
});
});

View file

@ -1,73 +0,0 @@
/*
* 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 { Filter, Query, TimeRange } from '@kbn/es-query';
import { parse } from 'query-string';
import { decode, encode } from 'rison-node';
export const FILTERS_QUERYSTRING_NAMESPACE = 'indicators';
export const DEFAULT_TIME_RANGE = { from: 'now/d', to: 'now/d' };
export const DEFAULT_QUERY: Readonly<Query> = { query: '', language: 'kuery' };
const INITIAL_FILTERS_STATE: Readonly<SerializableFilterState> = {
filters: [],
timeRange: DEFAULT_TIME_RANGE,
filterQuery: DEFAULT_QUERY,
};
interface SerializableFilterState {
timeRange?: TimeRange;
filterQuery: Query;
filters: Filter[];
}
/**
* Converts filter state to query string
* @param filterState Serializable filter state to convert into query string
* @returns
*/
export const encodeState = (filterState: SerializableFilterState): string =>
encode(filterState as any);
/**
*
* @param encodedFilterState Serialized filter state to decode
* @returns
*/
const decodeState = (encodedFilterState: string): SerializableFilterState | null =>
decode(encodedFilterState) as unknown as SerializableFilterState;
/**
* Find and convert filter state stored within query string into object literal
* @param searchString Brower query string containing encoded filter information, within single query field
* @returns SerializableFilterState with all the relevant fields ready to use
*/
export const stateFromQueryParams = (searchString: string): SerializableFilterState => {
const { [FILTERS_QUERYSTRING_NAMESPACE]: filtersSerialized } = parse(searchString);
if (!filtersSerialized) {
return INITIAL_FILTERS_STATE;
}
if (Array.isArray(filtersSerialized)) {
throw new Error('serialized filters should not be an array');
}
const deserializedFilters = decodeState(filtersSerialized);
if (!deserializedFilters) {
return INITIAL_FILTERS_STATE;
}
return {
...INITIAL_FILTERS_STATE,
...deserializedFilters,
timeRange: deserializedFilters.timeRange || DEFAULT_TIME_RANGE,
};
};

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { ComponentType, ReactElement, ReactNode } from 'react';
import { ComponentType, ReactElement, ReactNode, VFC } from 'react';
import { CoreStart } from '@kbn/core/public';
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
import {
@ -16,7 +16,7 @@ import {
import { Storage } from '@kbn/kibana-utils-plugin/public';
import { TimelinesUIStart } from '@kbn/timelines-plugin/public';
import type { TriggersAndActionsUIPublicPluginStart as TriggersActionsStart } from '@kbn/triggers-actions-ui-plugin/public';
import { DataViewBase } from '@kbn/es-query';
import { DataViewBase, Filter, Query, TimeRange } from '@kbn/es-query';
import { BrowserField } from '@kbn/rule-registry-plugin/common';
import { Store } from 'redux';
import { DataProvider } from '@kbn/timelines-plugin/common';
@ -102,4 +102,10 @@ export interface SecuritySolutionPluginContext {
from,
to,
}: UseInvestigateInTimelineProps) => () => Promise<void>;
useQuery: () => Query;
useFilters: () => Filter[];
useGlobalTime: () => TimeRange;
SiemSearchBar: VFC<any>;
}