diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/index/delete_index_api_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/index/delete_index_api_logic.test.ts new file mode 100644 index 0000000000000..6eaa5b3e95cf2 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/index/delete_index_api_logic.test.ts @@ -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 { mockHttpValues } from '../../../__mocks__/kea_logic'; + +import { nextTick } from '@kbn/test-jest-helpers'; + +import { deleteIndex } from './delete_index_api_logic'; + +describe('deleteIndexApiLogic', () => { + const { http } = mockHttpValues; + beforeEach(() => { + jest.clearAllMocks(); + }); + describe('deleteIndex', () => { + it('calls correct api', async () => { + const promise = Promise.resolve(); + http.post.mockReturnValue(promise); + const result = deleteIndex({ indexName: 'deleteIndex' }); + await nextTick(); + expect(http.delete).toHaveBeenCalledWith('/internal/enterprise_search/indices/deleteIndex'); + await expect(result).resolves; + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/index/delete_index_api_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/index/delete_index_api_logic.ts new file mode 100644 index 0000000000000..ff92e4ecef6bc --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/index/delete_index_api_logic.ts @@ -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 { createApiLogic } from '../../../shared/api_logic/create_api_logic'; +import { HttpLogic } from '../../../shared/http'; + +export interface DeleteIndexApiLogicArgs { + indexName: string; +} + +export const deleteIndex = async ({ indexName }: DeleteIndexApiLogicArgs): Promise => { + const route = `/internal/enterprise_search/indices/${indexName}`; + await HttpLogic.values.http.delete(route); + return; +}; + +export const DeleteIndexApiLogic = createApiLogic(['delete_index_api_logic'], deleteIndex); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/index/fetch_indices_api_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/index/fetch_indices_api_logic.test.ts index 13b75e7c534be..42d78b65df449 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/index/fetch_indices_api_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/index/fetch_indices_api_logic.test.ts @@ -28,7 +28,12 @@ describe('FetchIndicesApiLogic', () => { expect(http.get).toHaveBeenCalledWith('/internal/enterprise_search/indices', { query: { page: 1, return_hidden_indices: false, search_query: null, size: 20 }, }); - await expect(result).resolves.toEqual({ isInitialRequest: true, result: 'result' }); + await expect(result).resolves.toEqual({ + isInitialRequest: true, + result: 'result', + returnHiddenIndices: false, + searchQuery: undefined, + }); }); it('sets initialRequest to false if page is not the first page', async () => { const promise = Promise.resolve({ result: 'result' }); @@ -41,7 +46,12 @@ describe('FetchIndicesApiLogic', () => { expect(http.get).toHaveBeenCalledWith('/internal/enterprise_search/indices', { query: { page: 2, return_hidden_indices: false, search_query: null, size: 20 }, }); - await expect(result).resolves.toEqual({ isInitialRequest: false, result: 'result' }); + await expect(result).resolves.toEqual({ + isInitialRequest: false, + result: 'result', + returnHiddenIndices: false, + searchQuery: undefined, + }); }); it('sets initialRequest to false if searchQuery is not empty', async () => { const promise = Promise.resolve({ result: 'result' }); @@ -55,7 +65,12 @@ describe('FetchIndicesApiLogic', () => { expect(http.get).toHaveBeenCalledWith('/internal/enterprise_search/indices', { query: { page: 1, return_hidden_indices: false, search_query: 'a', size: 20 }, }); - await expect(result).resolves.toEqual({ isInitialRequest: false, result: 'result' }); + await expect(result).resolves.toEqual({ + isInitialRequest: false, + result: 'result', + returnHiddenIndices: false, + searchQuery: 'a', + }); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/index/fetch_indices_api_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/index/fetch_indices_api_logic.ts index a1ca3a9c3141f..709c47ed919c0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/index/fetch_indices_api_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/index/fetch_indices_api_logic.ts @@ -38,7 +38,7 @@ export const fetchIndices = async ({ // We need this to determine whether to show the empty state on the indices page const isInitialRequest = meta.page.current === 1 && !searchQuery; - return { ...response, isInitialRequest }; + return { ...response, isInitialRequest, returnHiddenIndices, searchQuery }; }; export const FetchIndicesAPILogic = createApiLogic(['content', 'indices_api_logic'], fetchIndices); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_indices/delete_index_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_indices/delete_index_modal.tsx new file mode 100644 index 0000000000000..99f0e35c352e1 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_indices/delete_index_modal.tsx @@ -0,0 +1,80 @@ +/* + * 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 { useActions, useValues } from 'kea'; + +import { EuiConfirmModal } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { IndicesLogic } from './indices_logic'; + +export const DeleteIndexModal: React.FC = () => { + const { closeDeleteModal, deleteIndex } = useActions(IndicesLogic); + const { deleteModalIndexName: indexName, isDeleteModalVisible } = useValues(IndicesLogic); + return isDeleteModalVisible ? ( + { + closeDeleteModal(); + }} + onConfirm={() => { + deleteIndex({ indexName }); + }} + cancelButtonText={i18n.translate( + 'xpack.enterpriseSearch.content.searchIndices.deleteModal.cancelButton.title', + { + defaultMessage: 'Cancel', + } + )} + confirmButtonText={i18n.translate( + 'xpack.enterpriseSearch.content.searchIndices.deleteModal.confirmButton.title', + { + defaultMessage: 'Delete index', + } + )} + defaultFocusedButton="confirm" + buttonColor="danger" + > +

+ {i18n.translate( + 'xpack.enterpriseSearch.content.searchIndices.deleteModal.delete.description', + { + defaultMessage: + 'You are about to delete the index {indexName}. This will also delete any associated connector documents or crawlers.', + values: { + indexName, + }, + } + )} +

+

+ {i18n.translate( + 'xpack.enterpriseSearch.content.searchIndices.deleteModal.searchEngine.description', + { + defaultMessage: + 'Any associated search engines will no longer be able to access any data stored in this index.', + } + )} +

+

+ {i18n.translate( + 'xpack.enterpriseSearch.content.searchIndices.deleteModal.irrevokable.description', + { + defaultMessage: + "You can't recover a deleted index, connector or crawler configuration. Make sure you have appropriate backups.", + } + )} +

+
+ ) : ( + <> + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_indices/indices_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_indices/indices_logic.test.ts index 36157301d3bae..2574af68ce50c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_indices/indices_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_indices/indices_logic.test.ts @@ -28,11 +28,14 @@ import { IndicesLogic } from './indices_logic'; const DEFAULT_VALUES = { data: undefined, + deleteModalIndexName: '', hasNoIndices: false, indices: [], + isDeleteModalVisible: false, isFirstRequest: true, isLoading: true, meta: DEFAULT_META, + searchParams: { meta: DEFAULT_META, returnHiddenIndices: false }, status: Status.IDLE, }; @@ -64,11 +67,75 @@ describe('IndicesLogic', () => { current: 3, }, }, + searchParams: { + ...DEFAULT_VALUES.searchParams, + meta: { page: { ...DEFAULT_META.page, current: 3 } }, + }, }); }); }); + describe('openDeleteModal', () => { + it('should set deleteIndexName and set isDeleteModalVisible to true', () => { + IndicesLogic.actions.openDeleteModal('delete'); + expect(IndicesLogic.values).toEqual({ + ...DEFAULT_VALUES, + deleteModalIndexName: 'delete', + isDeleteModalVisible: true, + }); + }); + }); + describe('closeDeleteModal', () => { + it('should set deleteIndexName to empty and set isDeleteModalVisible to false', () => { + IndicesLogic.actions.openDeleteModal('delete'); + IndicesLogic.actions.closeDeleteModal(); + expect(IndicesLogic.values).toEqual(DEFAULT_VALUES); + }); + }); }); describe('reducers', () => { + describe('isFirstRequest', () => { + it('should update to true on setIsFirstRequest', () => { + IndicesLogic.actions.setIsFirstRequest(); + expect(IndicesLogic.values).toEqual({ ...DEFAULT_VALUES, isFirstRequest: true }); + }); + it('should update to false on apiError', () => { + IndicesLogic.actions.setIsFirstRequest(); + IndicesLogic.actions.apiError({} as HttpError); + + expect(IndicesLogic.values).toEqual({ + ...DEFAULT_VALUES, + hasNoIndices: false, + indices: [], + isFirstRequest: false, + isLoading: false, + status: Status.ERROR, + }); + }); + it('should update to false on apiSuccess', () => { + IndicesLogic.actions.setIsFirstRequest(); + IndicesLogic.actions.apiSuccess({ + indices: [], + isInitialRequest: false, + meta: DEFAULT_VALUES.meta, + returnHiddenIndices: false, + }); + + expect(IndicesLogic.values).toEqual({ + ...DEFAULT_VALUES, + data: { + indices: [], + isInitialRequest: false, + meta: DEFAULT_VALUES.meta, + returnHiddenIndices: false, + }, + hasNoIndices: false, + indices: [], + isFirstRequest: false, + isLoading: false, + status: Status.SUCCESS, + }); + }); + }); describe('meta', () => { it('updates when apiSuccess listener triggered', () => { const newMeta = { @@ -84,18 +151,28 @@ describe('IndicesLogic', () => { indices, isInitialRequest: true, meta: newMeta, + returnHiddenIndices: true, + searchQuery: 'a', }); expect(IndicesLogic.values).toEqual({ + ...DEFAULT_VALUES, data: { indices, isInitialRequest: true, meta: newMeta, + returnHiddenIndices: true, + searchQuery: 'a', }, hasNoIndices: false, indices: elasticsearchViewIndices, isFirstRequest: false, isLoading: false, meta: newMeta, + searchParams: { + meta: newMeta, + returnHiddenIndices: true, + searchQuery: 'a', + }, status: Status.SUCCESS, }); }); @@ -115,18 +192,25 @@ describe('IndicesLogic', () => { indices: [], isInitialRequest: true, meta, + returnHiddenIndices: false, }); expect(IndicesLogic.values).toEqual({ + ...DEFAULT_VALUES, data: { indices: [], isInitialRequest: true, meta, + returnHiddenIndices: false, }, hasNoIndices: true, indices: [], isFirstRequest: false, isLoading: false, meta, + searchParams: { + ...DEFAULT_VALUES.searchParams, + meta, + }, status: Status.SUCCESS, }); }); @@ -144,18 +228,25 @@ describe('IndicesLogic', () => { indices: [], isInitialRequest: false, meta, + returnHiddenIndices: false, }); expect(IndicesLogic.values).toEqual({ + ...DEFAULT_VALUES, data: { indices: [], isInitialRequest: false, meta, + returnHiddenIndices: false, }, hasNoIndices: false, indices: [], isFirstRequest: false, isLoading: false, meta, + searchParams: { + ...DEFAULT_VALUES.searchParams, + meta, + }, status: Status.SUCCESS, }); }); @@ -172,6 +263,21 @@ describe('IndicesLogic', () => { expect(mockFlashMessageHelpers.flashAPIErrors).toHaveBeenCalledTimes(1); expect(mockFlashMessageHelpers.flashAPIErrors).toHaveBeenCalledWith({}); }); + it('calls flashAPIErrors on deleteError', () => { + IndicesLogic.actions.deleteError({} as HttpError); + expect(mockFlashMessageHelpers.flashAPIErrors).toHaveBeenCalledTimes(1); + expect(mockFlashMessageHelpers.flashAPIErrors).toHaveBeenCalledWith({}); + }); + it('calls flashSuccessToast, closeDeleteModal and fetchIndices on deleteSuccess', () => { + IndicesLogic.actions.fetchIndices = jest.fn(); + IndicesLogic.actions.closeDeleteModal = jest.fn(); + IndicesLogic.actions.deleteSuccess(); + expect(mockFlashMessageHelpers.flashSuccessToast).toHaveBeenCalledTimes(1); + expect(IndicesLogic.actions.fetchIndices).toHaveBeenCalledWith( + IndicesLogic.values.searchParams + ); + expect(IndicesLogic.actions.closeDeleteModal).toHaveBeenCalled(); + }); it('calls makeRequest on fetchIndices', async () => { jest.useFakeTimers(); IndicesLogic.actions.makeRequest = jest.fn(); @@ -223,13 +329,16 @@ describe('IndicesLogic', () => { indices: elasticsearchViewIndices, isInitialRequest: true, meta: DEFAULT_META, + returnHiddenIndices: false, }); expect(IndicesLogic.values).toEqual({ + ...DEFAULT_VALUES, data: { indices: elasticsearchViewIndices, isInitialRequest: true, meta: DEFAULT_META, + returnHiddenIndices: false, }, hasNoIndices: false, indices: elasticsearchViewIndices, @@ -256,9 +365,11 @@ describe('IndicesLogic', () => { ], isInitialRequest: true, meta: DEFAULT_META, + returnHiddenIndices: false, }); expect(IndicesLogic.values).toEqual({ + ...DEFAULT_VALUES, data: { indices: [ { @@ -272,6 +383,7 @@ describe('IndicesLogic', () => { ], isInitialRequest: true, meta: DEFAULT_META, + returnHiddenIndices: false, }, hasNoIndices: false, indices: [ @@ -302,9 +414,11 @@ describe('IndicesLogic', () => { ], isInitialRequest: true, meta: DEFAULT_META, + returnHiddenIndices: false, }); expect(IndicesLogic.values).toEqual({ + ...DEFAULT_VALUES, data: { indices: [ { @@ -314,6 +428,7 @@ describe('IndicesLogic', () => { ], isInitialRequest: true, meta: DEFAULT_META, + returnHiddenIndices: false, }, hasNoIndices: false, indices: [ @@ -343,9 +458,11 @@ describe('IndicesLogic', () => { ], isInitialRequest: true, meta: DEFAULT_META, + returnHiddenIndices: false, }); expect(IndicesLogic.values).toEqual({ + ...DEFAULT_VALUES, data: { indices: [ { @@ -355,6 +472,7 @@ describe('IndicesLogic', () => { ], isInitialRequest: true, meta: DEFAULT_META, + returnHiddenIndices: false, }, hasNoIndices: false, indices: [ @@ -385,9 +503,11 @@ describe('IndicesLogic', () => { ], isInitialRequest: true, meta: DEFAULT_META, + returnHiddenIndices: false, }); expect(IndicesLogic.values).toEqual({ + ...DEFAULT_VALUES, data: { indices: [ { @@ -401,6 +521,7 @@ describe('IndicesLogic', () => { ], isInitialRequest: true, meta: DEFAULT_META, + returnHiddenIndices: false, }, hasNoIndices: false, indices: [ diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_indices/indices_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_indices/indices_logic.ts index 59640a948ddbc..eb09425c1faca 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_indices/indices_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_indices/indices_logic.ts @@ -7,12 +7,23 @@ import { kea, MakeLogicType } from 'kea'; +import { i18n } from '@kbn/i18n'; + import { Meta } from '../../../../../common/types'; import { HttpError, Status } from '../../../../../common/types/api'; import { ElasticsearchIndexWithIngestion } from '../../../../../common/types/indices'; +import { Actions } from '../../../shared/api_logic/create_api_logic'; import { DEFAULT_META } from '../../../shared/constants'; -import { flashAPIErrors, clearFlashMessages } from '../../../shared/flash_messages'; +import { + flashAPIErrors, + clearFlashMessages, + flashSuccessToast, +} from '../../../shared/flash_messages'; import { updateMetaPageIndex } from '../../../shared/table_pagination'; +import { + DeleteIndexApiLogic, + DeleteIndexApiLogicArgs, +} from '../../api/index/delete_index_api_logic'; import { FetchIndicesAPILogic } from '../../api/index/fetch_indices_api_logic'; import { ElasticsearchViewIndex } from '../../types'; import { indexToViewIndex } from '../../utils/indices'; @@ -23,15 +34,25 @@ export interface IndicesActions { indices, isInitialRequest, meta, + returnHiddenIndices, + searchQuery, }: { indices: ElasticsearchIndexWithIngestion[]; isInitialRequest: boolean; meta: Meta; + returnHiddenIndices: boolean; + searchQuery?: string; }): { indices: ElasticsearchIndexWithIngestion[]; isInitialRequest: boolean; meta: Meta; + returnHiddenIndices: boolean; + searchQuery?: string; }; + closeDeleteModal(): void; + deleteError: Actions['apiError']; + deleteIndex: Actions['makeRequest']; + deleteSuccess: Actions['apiSuccess']; fetchIndices({ meta, returnHiddenIndices, @@ -43,34 +64,59 @@ export interface IndicesActions { }): { meta: Meta; returnHiddenIndices: boolean; searchQuery?: string }; makeRequest: typeof FetchIndicesAPILogic.actions.makeRequest; onPaginate(newPageIndex: number): { newPageIndex: number }; - setIsFirstRequest(): boolean; + openDeleteModal(indexName: string): { indexName: string }; + setIsFirstRequest(): void; } export interface IndicesValues { data: typeof FetchIndicesAPILogic.values.data; + deleteModalIndexName: string; hasNoIndices: boolean; indices: ElasticsearchViewIndex[]; + isDeleteModalVisible: boolean; isFirstRequest: boolean; isLoading: boolean; meta: Meta; + searchParams: { meta: Meta; returnHiddenIndices: boolean; searchQuery?: string }; status: typeof FetchIndicesAPILogic.values.status; } export const IndicesLogic = kea>({ actions: { + closeDeleteModal: true, fetchIndices: ({ meta, returnHiddenIndices, searchQuery }) => ({ meta, returnHiddenIndices, searchQuery, }), onPaginate: (newPageIndex) => ({ newPageIndex }), - setIsFirstRequest: () => true, + openDeleteModal: (indexName) => ({ indexName }), + setIsFirstRequest: true, }, connect: { - actions: [FetchIndicesAPILogic, ['makeRequest', 'apiSuccess', 'apiError']], + actions: [ + FetchIndicesAPILogic, + ['makeRequest', 'apiSuccess', 'apiError'], + DeleteIndexApiLogic, + ['apiError as deleteError', 'apiSuccess as deleteSuccess', 'makeRequest as deleteIndex'], + ], values: [FetchIndicesAPILogic, ['data', 'status']], }, - listeners: ({ actions }) => ({ + listeners: ({ actions, values }) => ({ apiError: (e) => flashAPIErrors(e), + deleteError: (e) => flashAPIErrors(e), + deleteSuccess: () => { + flashSuccessToast( + i18n.translate('xpack.enterpriseSearch.content.indices.deleteIndex.successToast.title', { + defaultMessage: + 'Your index {indexName} and any associated connectors or crawlers were successfully deleted', + values: { + indexName: values.deleteModalIndexName, + }, + }) + ); + actions.closeDeleteModal(); + actions.fetchIndices(values.searchParams); + }, fetchIndices: async (input, breakpoint) => { await breakpoint(150); actions.makeRequest(input); @@ -79,6 +125,20 @@ export const IndicesLogic = kea>({ }), path: ['enterprise_search', 'content', 'indices_logic'], reducers: () => ({ + deleteModalIndexName: [ + '', + { + closeDeleteModal: () => '', + openDeleteModal: (_, { indexName }) => indexName, + }, + ], + isDeleteModalVisible: [ + false, + { + closeDeleteModal: () => false, + openDeleteModal: () => true, + }, + ], isFirstRequest: [ true, { @@ -87,11 +147,18 @@ export const IndicesLogic = kea>({ setIsFirstRequest: () => true, }, ], - meta: [ - DEFAULT_META, + searchParams: [ + { meta: DEFAULT_META, returnHiddenIndices: false }, { - apiSuccess: (_, { meta }) => meta, - onPaginate: (state, { newPageIndex }) => updateMetaPageIndex(state, newPageIndex), + apiSuccess: (_, { meta, returnHiddenIndices, searchQuery }) => ({ + meta, + returnHiddenIndices, + searchQuery, + }), + onPaginate: (state, { newPageIndex }) => ({ + ...state, + meta: updateMetaPageIndex(state.meta, newPageIndex), + }), }, ], }), @@ -110,5 +177,6 @@ export const IndicesLogic = kea>({ () => [selectors.status, selectors.isFirstRequest], (status, isFirstRequest) => [Status.LOADING, Status.IDLE].includes(status) && isFirstRequest, ], + meta: [() => [selectors.searchParams], (searchParams) => searchParams.meta], }), }); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_indices/indices_table.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_indices/indices_table.tsx index d68ce14b1b183..0462613b247d8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_indices/indices_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_indices/indices_table.tsx @@ -11,6 +11,7 @@ import { CriteriaWithPagination, EuiBasicTable, EuiBasicTableColumn, + EuiButtonIcon, EuiIcon, EuiText, } from '@elastic/eui'; @@ -37,122 +38,12 @@ const healthColorsMap = { yellow: 'warning', }; -const columns: Array> = [ - { - field: 'name', - name: i18n.translate('xpack.enterpriseSearch.content.searchIndices.name.columnTitle', { - defaultMessage: 'Index name', - }), - render: (name: string) => ( - - {name} - - ), - sortable: true, - truncateText: true, - width: '40%', - }, - { - field: 'health', - name: i18n.translate('xpack.enterpriseSearch.content.searchIndices.health.columnTitle', { - defaultMessage: 'Index health', - }), - render: (health: 'red' | 'green' | 'yellow' | 'unavailable') => ( - - -  {health ?? '-'} - - ), - sortable: true, - truncateText: true, - width: '10%', - }, - { - field: 'count', - name: i18n.translate('xpack.enterpriseSearch.content.searchIndices.docsCount.columnTitle', { - defaultMessage: 'Docs count', - }), - sortable: true, - truncateText: true, - width: '10%', - }, - { - field: 'ingestionMethod', - name: i18n.translate( - 'xpack.enterpriseSearch.content.searchIndices.ingestionMethod.columnTitle', - { - defaultMessage: 'Ingestion method', - } - ), - render: (ingestionMethod: IngestionMethod) => ( - {ingestionMethodToText(ingestionMethod)} - ), - truncateText: true, - width: '10%', - }, - { - name: i18n.translate( - 'xpack.enterpriseSearch.content.searchIndices.ingestionStatus.columnTitle', - { - defaultMessage: 'Ingestion status', - } - ), - render: (index: ElasticsearchViewIndex) => { - const overviewPath = generateEncodedPath(SEARCH_INDEX_PATH, { indexName: index.name }); - if (isCrawlerIndex(index)) { - const label = crawlerStatusToText(index.crawler?.most_recent_crawl_request_status); - - return ( - - ); - } else { - const label = ingestionStatusToText(index.ingestionStatus); - return ( - - ); - } - }, - truncateText: true, - width: '10%', - }, - { - actions: [ - { - render: ({ name }) => ( - - ), - }, - ], - name: i18n.translate('xpack.enterpriseSearch.content.searchIndices.actions.columnTitle', { - defaultMessage: 'Actions', - }), - width: '5%', - }, -]; - interface IndicesTableProps { indices: ElasticsearchViewIndex[]; isLoading?: boolean; meta: Meta; onChange: (criteria: CriteriaWithPagination) => void; + onDelete: (indexName: string) => void; } export const IndicesTable: React.FC = ({ @@ -160,13 +51,141 @@ export const IndicesTable: React.FC = ({ isLoading, meta, onChange, -}) => ( - -); + onDelete, +}) => { + const columns: Array> = [ + { + field: 'name', + name: i18n.translate('xpack.enterpriseSearch.content.searchIndices.name.columnTitle', { + defaultMessage: 'Index name', + }), + render: (name: string) => ( + + {name} + + ), + sortable: true, + truncateText: true, + width: '40%', + }, + { + field: 'health', + name: i18n.translate('xpack.enterpriseSearch.content.searchIndices.health.columnTitle', { + defaultMessage: 'Index health', + }), + render: (health: 'red' | 'green' | 'yellow' | 'unavailable') => ( + + +  {health ?? '-'} + + ), + sortable: true, + truncateText: true, + width: '10%', + }, + { + field: 'count', + name: i18n.translate('xpack.enterpriseSearch.content.searchIndices.docsCount.columnTitle', { + defaultMessage: 'Docs count', + }), + sortable: true, + truncateText: true, + width: '10%', + }, + { + field: 'ingestionMethod', + name: i18n.translate( + 'xpack.enterpriseSearch.content.searchIndices.ingestionMethod.columnTitle', + { + defaultMessage: 'Ingestion method', + } + ), + render: (ingestionMethod: IngestionMethod) => ( + {ingestionMethodToText(ingestionMethod)} + ), + truncateText: true, + width: '10%', + }, + { + name: i18n.translate( + 'xpack.enterpriseSearch.content.searchIndices.ingestionStatus.columnTitle', + { + defaultMessage: 'Ingestion status', + } + ), + render: (index: ElasticsearchViewIndex) => { + const overviewPath = generateEncodedPath(SEARCH_INDEX_PATH, { indexName: index.name }); + if (isCrawlerIndex(index)) { + const label = crawlerStatusToText(index.crawler?.most_recent_crawl_request_status); + + return ( + + ); + } else { + const label = ingestionStatusToText(index.ingestionStatus); + return ( + + ); + } + }, + truncateText: true, + width: '10%', + }, + { + actions: [ + { + render: ({ name }) => ( + + ), + }, + { + render: (index) => + // We don't have a way to delete crawlers yet + isCrawlerIndex(index) ? ( + <> + ) : ( + onDelete(index.name)} + /> + ), + }, + ], + name: i18n.translate('xpack.enterpriseSearch.content.searchIndices.actions.columnTitle', { + defaultMessage: 'Actions', + }), + width: '5%', + }, + ]; + return ( + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_indices/search_indices.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_indices/search_indices.tsx index 96019c8139c97..bc9aa175e1c71 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_indices/search_indices.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_indices/search_indices.tsx @@ -34,6 +34,7 @@ import { useLocalStorage } from '../../../shared/use_local_storage'; import { NEW_INDEX_PATH } from '../../routes'; import { EnterpriseSearchContentPageTemplate } from '../layout/page_template'; +import { DeleteIndexModal } from './delete_index_modal'; import { IndicesLogic } from './indices_logic'; import { IndicesTable } from './indices_table'; @@ -49,7 +50,7 @@ export const baseBreadcrumbs = [ ]; export const SearchIndices: React.FC = () => { - const { fetchIndices, onPaginate, setIsFirstRequest } = useActions(IndicesLogic); + const { fetchIndices, onPaginate, openDeleteModal, setIsFirstRequest } = useActions(IndicesLogic); const { meta, indices, hasNoIndices, isLoading } = useValues(IndicesLogic); const [showHiddenIndices, setShowHiddenIndices] = useState(false); const [searchQuery, setSearchValue] = useState(''); @@ -85,6 +86,7 @@ export const SearchIndices: React.FC = () => { return ( <> + { - + ) : ( diff --git a/x-pack/plugins/enterprise_search/server/lib/analytics/fetch_analytics_collection.test.ts b/x-pack/plugins/enterprise_search/server/lib/analytics/fetch_analytics_collection.test.ts index e157df1df16f6..6fb4e55dd6361 100644 --- a/x-pack/plugins/enterprise_search/server/lib/analytics/fetch_analytics_collection.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/analytics/fetch_analytics_collection.test.ts @@ -9,7 +9,10 @@ import { IScopedClusterClient } from '@kbn/core-elasticsearch-server'; import { ANALYTICS_COLLECTIONS_INDEX } from '../..'; -import { fetchAnalyticsCollectionByName } from './fetch_analytics_collection'; +import { + fetchAnalyticsCollectionByName, + fetchAnalyticsCollections, +} from './fetch_analytics_collection'; import { setupAnalyticsCollectionIndex } from './setup_indices'; jest.mock('./setup_indices', () => ({ @@ -28,6 +31,75 @@ describe('fetch analytics collection lib function', () => { jest.clearAllMocks(); }); + describe('fetch collections', () => { + it('should return a list of analytics collections', async () => { + mockClient.asCurrentUser.search.mockImplementationOnce(() => + Promise.resolve({ + hits: { + hits: [ + { _id: '2', _source: { name: 'example' } }, + { _id: '1', _source: { name: 'example2' } }, + ], + }, + }) + ); + await expect( + fetchAnalyticsCollections(mockClient as unknown as IScopedClusterClient) + ).resolves.toEqual([ + { id: '2', name: 'example' }, + { id: '1', name: 'example2' }, + ]); + }); + + it('should setup the indexes if none exist and return an empty array', async () => { + mockClient.asCurrentUser.search.mockImplementationOnce(() => + Promise.reject({ + meta: { + body: { + error: { + type: 'index_not_found_exception', + }, + }, + }, + }) + ); + + await expect( + fetchAnalyticsCollections(mockClient as unknown as IScopedClusterClient) + ).resolves.toEqual([]); + + expect(setupAnalyticsCollectionIndex as jest.Mock).toHaveBeenCalledWith( + mockClient.asCurrentUser + ); + }); + + it('should not call setup analytics index on other errors and return error', async () => { + const error = { + meta: { + body: { + error: { + type: 'other error', + }, + }, + }, + }; + mockClient.asCurrentUser.search.mockImplementationOnce(() => Promise.reject(error)); + await expect( + fetchAnalyticsCollections(mockClient as unknown as IScopedClusterClient) + ).rejects.toMatchObject(error); + + expect(mockClient.asCurrentUser.search).toHaveBeenCalledWith({ + from: 0, + index: ANALYTICS_COLLECTIONS_INDEX, + query: { + match_all: {}, + }, + size: 1000, + }); + expect(setupAnalyticsCollectionIndex as jest.Mock).not.toHaveBeenCalled(); + }); + }); + describe('fetch collection by name', () => { it('should fetch analytics collection by name', async () => { mockClient.asCurrentUser.search.mockImplementationOnce(() => diff --git a/x-pack/plugins/enterprise_search/server/lib/analytics/fetch_analytics_collection.ts b/x-pack/plugins/enterprise_search/server/lib/analytics/fetch_analytics_collection.ts index 46f718d23976d..ef356ae2bca2c 100644 --- a/x-pack/plugins/enterprise_search/server/lib/analytics/fetch_analytics_collection.ts +++ b/x-pack/plugins/enterprise_search/server/lib/analytics/fetch_analytics_collection.ts @@ -5,12 +5,14 @@ * 2.0. */ +import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { IScopedClusterClient } from '@kbn/core-elasticsearch-server'; import { ANALYTICS_COLLECTIONS_INDEX } from '../..'; import { AnalyticsCollection } from '../../../common/types/analytics'; import { isIndexNotFoundException } from '../../utils/identify_exceptions'; +import { fetchAll } from '../fetch_all'; import { setupAnalyticsCollectionIndex } from './setup_indices'; @@ -36,3 +38,19 @@ export const fetchAnalyticsCollectionByName = async ( return undefined; } }; + +export const fetchAnalyticsCollections = async ( + client: IScopedClusterClient +): Promise => { + const query: QueryDslQueryContainer = { match_all: {} }; + + try { + return await fetchAll(client, ANALYTICS_COLLECTIONS_INDEX, query); + } catch (error) { + if (isIndexNotFoundException(error)) { + await setupAnalyticsCollectionIndex(client.asCurrentUser); + return []; + } + throw error; + } +}; diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/analytics.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/analytics.ts index be53b797239a1..7035329e4d314 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/analytics.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/analytics.ts @@ -11,11 +11,24 @@ import { i18n } from '@kbn/i18n'; import { ErrorCode } from '../../../common/types/error_codes'; import { addAnalyticsCollection } from '../../lib/analytics/add_analytics_collection'; +import { fetchAnalyticsCollections } from '../../lib/analytics/fetch_analytics_collection'; import { RouteDependencies } from '../../plugin'; import { createError } from '../../utils/create_error'; import { elasticsearchErrorHandler } from '../../utils/elasticsearch_error_handler'; export function registerAnalyticsRoutes({ router, log }: RouteDependencies) { + router.get( + { + path: '/internal/enterprise_search/analytics/collections', + validate: {}, + }, + elasticsearchErrorHandler(log, async (context, request, response) => { + const { client } = (await context.core).elasticsearch; + const collections = await fetchAnalyticsCollections(client); + return response.ok({ body: collections }); + }) + ); + router.post( { path: '/internal/enterprise_search/analytics/collections', diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.ts index 5d5f72c297727..6741721053b86 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.ts @@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; import { ErrorCode } from '../../../common/types/error_codes'; +import { deleteConnectorById } from '../../lib/connectors/delete_connector'; import { fetchConnectorByIndexName, fetchConnectors } from '../../lib/connectors/fetch_connectors'; import { fetchCrawlerByIndexName, fetchCrawlers } from '../../lib/crawler/fetch_crawlers'; @@ -124,6 +125,52 @@ export function registerIndexRoutes({ router, log }: RouteDependencies) { }) ); + router.delete( + { + path: '/internal/enterprise_search/indices/{indexName}', + validate: { + params: schema.object({ + indexName: schema.string(), + }), + }, + }, + elasticsearchErrorHandler(log, async (context, request, response) => { + const indexName = decodeURIComponent(request.params.indexName); + const { client } = (await context.core).elasticsearch; + + try { + const connector = await fetchConnectorByIndexName(client, indexName); + const crawler = await fetchCrawlerByIndexName(client, indexName); + + if (connector) { + await deleteConnectorById(client, connector.id); + } + + if (crawler) { + // do nothing for now because we don't have a way to delete a crawler yet + } + + await client.asCurrentUser.indices.delete({ index: indexName }); + + return response.ok({ + body: {}, + headers: { 'content-type': 'application/json' }, + }); + } catch (error) { + if (isIndexNotFoundException(error)) { + return createError({ + errorCode: ErrorCode.INDEX_NOT_FOUND, + message: 'Could not find index', + response, + statusCode: 404, + }); + } + + throw error; + } + }) + ); + router.get( { path: '/internal/enterprise_search/indices/{indexName}/exists', diff --git a/x-pack/plugins/threat_intelligence/public/common/mocks/mock_security_context.tsx b/x-pack/plugins/threat_intelligence/public/common/mocks/mock_security_context.tsx new file mode 100644 index 0000000000000..666fc6318c577 --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/common/mocks/mock_security_context.tsx @@ -0,0 +1,26 @@ +/* + * 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 { SecuritySolutionPluginContext } from '../..'; + +export const getSecuritySolutionContextMock = (): SecuritySolutionPluginContext => ({ + getFiltersGlobalComponent: + () => + ({ children }) => +
{children}
, + licenseService: { + isEnterprise() { + return true; + }, + }, + sourcererDataView: { + browserFields: {}, + selectedPatterns: [], + indexPattern: { fields: [], title: '' }, + }, +}); diff --git a/x-pack/plugins/threat_intelligence/public/common/mocks/story_providers.tsx b/x-pack/plugins/threat_intelligence/public/common/mocks/story_providers.tsx new file mode 100644 index 0000000000000..752e93b756ddb --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/common/mocks/story_providers.tsx @@ -0,0 +1,53 @@ +/* + * 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, { ReactNode, VFC } from 'react'; +import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public'; +import { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import { CoreStart, IUiSettingsClient } from '@kbn/core/public'; +import { SecuritySolutionContext } from '../../containers/security_solution_context'; +import { getSecuritySolutionContextMock } from './mock_security_context'; + +export interface KibanaContextMock { + /** + * For the data plugin (see {@link DataPublicPluginStart}) + */ + data?: DataPublicPluginStart; + /** + * For the core ui-settings package (see {@link IUiSettingsClient}) + */ + uiSettings?: IUiSettingsClient; +} + +export interface StoryProvidersComponentProps { + /** + * Used to generate a new KibanaReactContext (using {@link createKibanaReactContext}) + */ + kibana: KibanaContextMock; + /** + * Component(s) to be displayed inside + */ + children: ReactNode; +} + +/** + * Helper functional component used in Storybook stories. + * Wraps the story with our {@link SecuritySolutionContext} and KibanaReactContext. + */ +export const StoryProvidersComponent: VFC = ({ + children, + kibana, +}) => { + const KibanaReactContext = createKibanaReactContext(kibana as CoreStart); + const securitySolutionContextMock = getSecuritySolutionContextMock(); + + return ( + + {children} + + ); +}; diff --git a/x-pack/plugins/threat_intelligence/public/common/mocks/test_providers.tsx b/x-pack/plugins/threat_intelligence/public/common/mocks/test_providers.tsx index afd526342689b..a2c0318c482aa 100644 --- a/x-pack/plugins/threat_intelligence/public/common/mocks/test_providers.tsx +++ b/x-pack/plugins/threat_intelligence/public/common/mocks/test_providers.tsx @@ -14,6 +14,7 @@ import type { IStorage } from '@kbn/kibana-utils-plugin/public'; import { Storage } from '@kbn/kibana-utils-plugin/public'; import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; import { BehaviorSubject } from 'rxjs'; +import { getSecuritySolutionContextMock } from './mock_security_context'; import { mockUiSetting } from './mock_kibana_ui_setting'; import { KibanaContext } from '../../hooks/use_kibana'; import { SecuritySolutionPluginContext } from '../../types'; @@ -95,22 +96,7 @@ const coreServiceMock = { uiSettings: { get: jest.fn().mockImplementation(mockUiSetting) }, }; -const mockSecurityContext: SecuritySolutionPluginContext = { - getFiltersGlobalComponent: - () => - ({ children }) => -
{children}
, - licenseService: { - isEnterprise() { - return true; - }, - }, - sourcererDataView: { - browserFields: {}, - selectedPatterns: [], - indexPattern: { fields: [], title: '' }, - }, -}; +const mockSecurityContext: SecuritySolutionPluginContext = getSecuritySolutionContextMock(); export const mockedServices = { ...coreServiceMock, diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart_wrapper/indicators_barchart_wrapper.stories.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart_wrapper/indicators_barchart_wrapper.stories.tsx index 1f4cbce1c76a2..478b2ec398b97 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart_wrapper/indicators_barchart_wrapper.stories.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart_wrapper/indicators_barchart_wrapper.stories.tsx @@ -7,12 +7,14 @@ import moment from 'moment'; import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; import { of } from 'rxjs'; import { Story } from '@storybook/react'; import { DataView, DataViewField } from '@kbn/data-views-plugin/common'; -import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public'; -import { CoreStart } from '@kbn/core/public'; import { TimeRange } from '@kbn/es-query'; +import { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import { IUiSettingsClient } from '@kbn/core/public'; +import { StoryProvidersComponent } from '../../../../common/mocks/story_providers'; import { Aggregation, AGGREGATION_NAME } from '../../hooks/use_aggregated_indicators'; import { DEFAULT_TIME_RANGE } from '../../hooks/use_filters/utils'; import { IndicatorsBarChartWrapper } from './indicators_barchart_wrapper'; @@ -74,38 +76,43 @@ const aggregation2: Aggregation = { doc_count: 0, key: '[Filebeat] AbuseCH MalwareBazaar', }; -const KibanaReactContext = createKibanaReactContext({ - data: { - search: { - search: () => - of({ - rawResponse: { - aggregations: { - [AGGREGATION_NAME]: { - buckets: [aggregation1, aggregation2], - }, +const mockData = { + search: { + search: () => + of({ + rawResponse: { + aggregations: { + [AGGREGATION_NAME]: { + buckets: [aggregation1, aggregation2], }, }, - }), - }, - query: { - timefilter: { - timefilter: { - calculateBounds: () => ({ - min: moment(validDate), - max: moment(validDate).add(numberOfDays, 'days'), - }), }, + }), + }, + query: { + timefilter: { + timefilter: { + calculateBounds: () => ({ + min: moment(validDate), + max: moment(validDate).add(numberOfDays, 'days'), + }), }, }, + filterManager: { + getFilters: () => {}, + setFilters: () => {}, + getUpdates$: () => of(), + }, }, - uiSettings: { get: () => {} }, -} as unknown as Partial); +} as unknown as DataPublicPluginStart; + +const mockUiSettings = { get: () => {} } as unknown as IUiSettingsClient; export const Default: Story = () => { return ( - + - + ); }; +Default.decorators = [(story) => {story()}]; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart_wrapper/indicators_barchart_wrapper.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart_wrapper/indicators_barchart_wrapper.test.tsx index b8f06c06aa6f5..9deeb4e5bcb8a 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart_wrapper/indicators_barchart_wrapper.test.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart_wrapper/indicators_barchart_wrapper.test.tsx @@ -12,8 +12,11 @@ import { DataView, DataViewField } from '@kbn/data-views-plugin/common'; import { TestProvidersComponent } from '../../../../common/mocks/test_providers'; import { IndicatorsBarChartWrapper } from './indicators_barchart_wrapper'; import { DEFAULT_TIME_RANGE } from '../../hooks/use_filters/utils'; +import { useFilters } from '../../hooks/use_filters'; -const mockIndexPatterns: DataView = { +jest.mock('../../hooks/use_filters'); + +const mockIndexPattern: DataView = { fields: [ { name: '@timestamp', @@ -25,13 +28,26 @@ const mockIndexPatterns: DataView = { } as DataViewField, ], } as DataView; + const mockTimeRange: TimeRange = DEFAULT_TIME_RANGE; +const stub = () => {}; + describe('', () => { + beforeEach(() => { + (useFilters as jest.MockedFunction).mockReturnValue({ + filters: [], + filterQuery: { language: 'kuery', query: '' }, + filterManager: {} as any, + handleSavedQuery: stub, + handleSubmitQuery: stub, + handleSubmitTimeRange: stub, + }); + }); it('should render barchart and field selector dropdown', () => { const component = render( - + ); diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/indicators_table.stories.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/indicators_table.stories.tsx index b8bc912e6208e..0e94e87372820 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/indicators_table.stories.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/indicators_table.stories.tsx @@ -20,7 +20,7 @@ export default { }; const indicatorsFixture: Indicator[] = Array(10).fill(generateMockIndicator()); -const mockIndexPattern: DataView = {} as DataView; +const mockIndexPattern: DataView = undefined as unknown as DataView; const stub = () => void 0; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_aggregated_indicators.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_aggregated_indicators.test.tsx index 90bffdb646484..7a671f923f796 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_aggregated_indicators.test.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_aggregated_indicators.test.tsx @@ -21,6 +21,9 @@ import { mockedSearchService, mockedTimefilterService, } from '../../../common/mocks/test_providers'; +import { useFilters } from './use_filters'; + +jest.mock('./use_filters/use_filters'); const aggregationResponse = { rawResponse: { aggregations: { [AGGREGATION_NAME]: { buckets: [] } } }, @@ -35,6 +38,8 @@ const useAggregatedIndicatorsParams: UseAggregatedIndicatorsParam = { timeRange: DEFAULT_TIME_RANGE, }; +const stub = () => {}; + describe('useAggregatedIndicators()', () => { beforeEach(jest.clearAllMocks); @@ -45,6 +50,15 @@ describe('useAggregatedIndicators()', () => { describe('when mounted', () => { beforeEach(() => { + (useFilters as jest.MockedFunction).mockReturnValue({ + filters: [], + filterQuery: { language: 'kuery', query: '' }, + filterManager: {} as any, + handleSavedQuery: stub, + handleSubmitQuery: stub, + handleSubmitTimeRange: stub, + }); + renderHook(() => useAggregatedIndicators(useAggregatedIndicatorsParams), { wrapper: TestProvidersComponent, }); diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_aggregated_indicators.ts b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_aggregated_indicators.ts index 0520991e6593d..9322e84d78c4f 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_aggregated_indicators.ts +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_aggregated_indicators.ts @@ -15,6 +15,7 @@ import { isErrorResponse, TimeRangeBounds, } from '@kbn/data-plugin/common'; +import { useFilters } from './use_filters'; import { convertAggregationToChartSeries } from '../../../common/utils/barchart'; import { RawIndicatorFieldId } from '../../../../common/types/indicator'; import { THREAT_QUERY_BASE } from '../../../../common/constants'; @@ -87,6 +88,8 @@ export const useAggregatedIndicators = ({ [queryService, timeRange] ); + const { filters, filterQuery } = useFilters(); + const loadData = useCallback(async () => { const dateFrom: number = (dateRange.min as moment.Moment).toDate().getTime(); const dateTo: number = (dateRange.max as moment.Moment).toDate().getTime(); @@ -101,8 +104,13 @@ export const useAggregatedIndicators = ({ query: THREAT_QUERY_BASE, language: 'kuery', }, + { + query: filterQuery.query as string, + language: 'kuery', + }, ], [ + ...filters, { query: { range: { @@ -175,6 +183,8 @@ export const useAggregatedIndicators = ({ dateRange.max, dateRange.min, field, + filterQuery, + filters, searchService, selectedPatterns, timeRange.from,