diff --git a/docs/user/visualize.asciidoc b/docs/user/visualize.asciidoc index f6be2040e3e8c1..8fe82d6c28b6dc 100644 --- a/docs/user/visualize.asciidoc +++ b/docs/user/visualize.asciidoc @@ -37,14 +37,14 @@ Quickly build several types of basic visualizations by simply dragging and dropp <>:: Computes and combine data from multiple time series data sets. -<>:: +Maps:: * *<>* — Displays geospatial data in {kib}. -* *Coordinate map* — Displays points on a map using a geohash aggregation. +* *<>* — Displays points on a map using a geohash aggregation. -* *Region map* — Merges any structured map data onto a shape. +* *<>* — Merges any structured map data onto a shape. -* *Heat map* — Displays shaded cells within a matrix. +* *<>* — Displays shaded cells within a matrix. <>:: @@ -139,6 +139,7 @@ include::{kib-repo-dir}/visualize/tsvb.asciidoc[] include::{kib-repo-dir}/visualize/timelion.asciidoc[] include::{kib-repo-dir}/visualize/tilemap.asciidoc[] +include::{kib-repo-dir}/visualize/heatmap.asciidoc[] include::{kib-repo-dir}/visualize/for-dashboard.asciidoc[] diff --git a/docs/visualize/heatmap.asciidoc b/docs/visualize/heatmap.asciidoc new file mode 100644 index 00000000000000..bf734979a05570 --- /dev/null +++ b/docs/visualize/heatmap.asciidoc @@ -0,0 +1,40 @@ +[[heat-map]] +== Heat map + +Display graphical representations of data where the individual values are represented by colors. Use heat maps when your data set includes categorical data. For example, use a heat map to see the flights of origin countries compared to destination countries using the sample flight data. + +[role="screenshot"] +image::images/visualize_heat_map_example.png[] + +[float] +[[build-heat-map]] +==== Build a heat map + +To display your data on the heat map, use the supported aggregations. + +Heat maps support the following aggregations: + +* <> +* <> +* <> +* <> + +[float] +[[navigate-heatmap]] +==== Change the color ranges + +When only one color displays on the heat map, you might need to change the color ranges. + +To specify the number of color ranges: + +. Click *Options*. + +. Enter the *Number of colors* to display. + +To specify custom ranges: + +. Click *Options*. + +. Select *Use custom ranges*. + +. Enter the ranges to display. diff --git a/docs/visualize/tilemap.asciidoc b/docs/visualize/tilemap.asciidoc index 349fa681a97776..aa80c96f2cac42 100644 --- a/docs/visualize/tilemap.asciidoc +++ b/docs/visualize/tilemap.asciidoc @@ -9,8 +9,6 @@ Visualize supports the following maps: * *Region* — Display colored boundary vector shapes using a gradient. Darker colors indicate larger values, and lighter colors indicate smaller values. -* *Heat* — Display graphical representations of data where the individual values are represented by colors. - NOTE: The maps in Visualize have been replaced with <>, which offers more functionality. [float] @@ -98,45 +96,3 @@ To navigate the region map, use the navigation options. * To change the zoom level, click *Zoom In* or *Zoom out* image:images/viz-zoom.png[]. * To automatically crop the map boundaries, click *Fit Data Bounds* image:images/viz-fit-bounds.png[]. - -[float] -[[heat-map]] -=== Heat map - -Use heat maps when your data set includes categorical data. For example, use a heat map to see the flights of origin countries compared to destination countries using the sample flight data. - -[role="screenshot"] -image::images/visualize_heat_map_example.png[] - -[float] -[[build-heat-map]] -==== Build a heat map - -To display your data on the heat map, use the supported aggregations. - -Heat maps support the following aggregations: - -* <> -* <> -* <> -* <> - -[float] -[[navigate-heatmap]] -==== Change the color ranges - -When only one color displays on the heat map, you might need to change the color ranges. - -To specify the number of color ranges: - -. Click *Options*. - -. Enter the *Number of colors* to display. - -To specify custom ranges: - -. Click *Options*. - -. Select *Use custom ranges*. - -. Enter the ranges to display. diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/filter_state_manager.test.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/filter_state_manager.test.ts deleted file mode 100644 index 08d5955d3fae9c..00000000000000 --- a/src/legacy/core_plugins/data/public/filter/filter_manager/filter_state_manager.test.ts +++ /dev/null @@ -1,175 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import sinon from 'sinon'; - -import { FilterStateManager } from './filter_state_manager'; - -import { StubState } from './test_helpers/stub_state'; -import { getFilter } from './test_helpers/get_stub_filter'; -import { FilterManager, esFilters } from '../../../../../../plugins/data/public'; - -import { coreMock } from '../../../../../../core/public/mocks'; -const setupMock = coreMock.createSetup(); - -setupMock.uiSettings.get.mockImplementation((key: string) => { - return true; -}); - -describe('filter_state_manager', () => { - let appStateStub: StubState; - let globalStateStub: StubState; - - let filterManager: FilterManager; - - beforeEach(() => { - appStateStub = new StubState(); - globalStateStub = new StubState(); - filterManager = new FilterManager(setupMock.uiSettings); - }); - - describe('app_state_undefined', () => { - beforeEach(() => { - // FilterStateManager is tested indirectly. - // Therefore, we don't need it's instance. - new FilterStateManager( - globalStateStub, - () => { - return undefined; - }, - filterManager - ); - }); - - test('should NOT watch state until both app and global state are defined', done => { - const f1 = getFilter(esFilters.FilterStateStore.GLOBAL_STATE, false, false, 'age', 34); - globalStateStub.filters.push(f1); - - setTimeout(() => { - expect(filterManager.getGlobalFilters()).toHaveLength(0); - done(); - }, 100); - }); - - test('should NOT update app URL when filter manager filters are set', async () => { - appStateStub.save = sinon.stub(); - globalStateStub.save = sinon.stub(); - - const f1 = getFilter(esFilters.FilterStateStore.APP_STATE, false, false, 'age', 34); - const f2 = getFilter(esFilters.FilterStateStore.GLOBAL_STATE, false, false, 'age', 34); - - filterManager.setFilters([f1, f2]); - - sinon.assert.notCalled(appStateStub.save); - sinon.assert.calledOnce(globalStateStub.save); - }); - }); - - describe('app_state_defined', () => { - let filterStateManager: FilterStateManager; - beforeEach(() => { - // FilterStateManager is tested indirectly. - // Therefore, we don't need it's instance. - filterStateManager = new FilterStateManager( - globalStateStub, - () => { - return appStateStub; - }, - filterManager - ); - }); - - afterEach(() => { - filterStateManager.destroy(); - }); - - test('should update filter manager global filters', done => { - const updateSubscription = filterManager.getUpdates$().subscribe(() => { - expect(filterManager.getGlobalFilters()).toHaveLength(1); - if (updateSubscription) { - updateSubscription.unsubscribe(); - } - done(); - }); - - const f1 = getFilter(esFilters.FilterStateStore.GLOBAL_STATE, true, true, 'age', 34); - globalStateStub.filters.push(f1); - }); - - test('should update filter manager app filter', done => { - const updateSubscription = filterManager.getUpdates$().subscribe(() => { - expect(filterManager.getAppFilters()).toHaveLength(1); - if (updateSubscription) { - updateSubscription.unsubscribe(); - } - done(); - }); - - const f1 = getFilter(esFilters.FilterStateStore.APP_STATE, false, false, 'age', 34); - appStateStub.filters.push(f1); - }); - - test('should update URL when filter manager filters are set', () => { - appStateStub.save = sinon.stub(); - globalStateStub.save = sinon.stub(); - - const f1 = getFilter(esFilters.FilterStateStore.APP_STATE, false, false, 'age', 34); - const f2 = getFilter(esFilters.FilterStateStore.GLOBAL_STATE, false, false, 'age', 34); - - filterManager.setFilters([f1, f2]); - - sinon.assert.calledOnce(appStateStub.save); - sinon.assert.calledOnce(globalStateStub.save); - }); - - test('should update URL when filter manager filters are added', () => { - appStateStub.save = sinon.stub(); - globalStateStub.save = sinon.stub(); - - const f1 = getFilter(esFilters.FilterStateStore.APP_STATE, false, false, 'age', 34); - const f2 = getFilter(esFilters.FilterStateStore.GLOBAL_STATE, false, false, 'age', 34); - - filterManager.addFilters([f1, f2]); - - sinon.assert.calledOnce(appStateStub.save); - sinon.assert.calledOnce(globalStateStub.save); - }); - }); - - describe('bug fixes', () => { - /* - ** This test is here to reproduce a bug where a filter manager update - ** would cause filter state manager detects those changes - ** And triggers *another* filter manager update. - */ - test('should NOT re-trigger filter manager', done => { - const f1 = getFilter(esFilters.FilterStateStore.APP_STATE, false, false, 'age', 34); - filterManager.setFilters([f1]); - const setFiltersSpy = sinon.spy(filterManager, 'setFilters'); - - f1.meta.negate = true; - filterManager.setFilters([f1]); - - setTimeout(() => { - expect(setFiltersSpy.callCount).toEqual(1); - done(); - }, 100); - }); - }); -}); diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/filter_state_manager.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/filter_state_manager.ts deleted file mode 100644 index e095493c94c58c..00000000000000 --- a/src/legacy/core_plugins/data/public/filter/filter_manager/filter_state_manager.ts +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import _ from 'lodash'; -import { Subscription } from 'rxjs'; -import { State } from 'ui/state_management/state'; -import { FilterManager, esFilters, Filter } from '../../../../../../plugins/data/public'; - -type GetAppStateFunc = () => { filters?: Filter[]; save?: () => void } | undefined | null; - -/** - * FilterStateManager is responsible for watching for filter changes - * and syncing with FilterManager, as well as syncing FilterManager changes - * back to the URL. - **/ -export class FilterStateManager { - private filterManagerUpdatesSubscription: Subscription; - - filterManager: FilterManager; - globalState: State; - getAppState: GetAppStateFunc; - interval: number | undefined; - - constructor(globalState: State, getAppState: GetAppStateFunc, filterManager: FilterManager) { - this.getAppState = getAppState; - this.globalState = globalState; - this.filterManager = filterManager; - - this.watchFilterState(); - - this.filterManagerUpdatesSubscription = this.filterManager.getUpdates$().subscribe(() => { - this.updateAppState(); - }); - } - - destroy() { - if (this.interval) { - clearInterval(this.interval); - } - this.filterManagerUpdatesSubscription.unsubscribe(); - } - - private watchFilterState() { - // This is a temporary solution to remove rootscope. - // Moving forward, state should provide observable subscriptions. - this.interval = window.setInterval(() => { - const appState = this.getAppState(); - const stateUndefined = !appState || !this.globalState; - if (stateUndefined) return; - - const globalFilters = this.globalState.filters || []; - const appFilters = (appState && appState.filters) || []; - - const globalFilterChanged = !esFilters.compareFilters( - this.filterManager.getGlobalFilters(), - globalFilters, - esFilters.COMPARE_ALL_OPTIONS - ); - const appFilterChanged = !esFilters.compareFilters( - this.filterManager.getAppFilters(), - appFilters, - esFilters.COMPARE_ALL_OPTIONS - ); - const filterStateChanged = globalFilterChanged || appFilterChanged; - - if (!filterStateChanged) return; - - const newGlobalFilters = _.cloneDeep(globalFilters); - const newAppFilters = _.cloneDeep(appFilters); - FilterManager.setFiltersStore(newAppFilters, esFilters.FilterStateStore.APP_STATE); - FilterManager.setFiltersStore(newGlobalFilters, esFilters.FilterStateStore.GLOBAL_STATE); - - this.filterManager.setFilters(newGlobalFilters.concat(newAppFilters)); - }, 10); - } - - private saveState() { - const appState = this.getAppState(); - if (appState && appState.save) appState.save(); - this.globalState.save(); - } - - private updateAppState() { - // Update Angular state before saving State objects (which save it to URL) - const partitionedFilters = this.filterManager.getPartitionedFilters(); - const appState = this.getAppState(); - if (appState) { - appState.filters = partitionedFilters.appFilters; - } - this.globalState.filters = partitionedFilters.globalFilters; - this.saveState(); - } -} diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/index.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/index.ts deleted file mode 100644 index ebb622783c3d1c..00000000000000 --- a/src/legacy/core_plugins/data/public/filter/filter_manager/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { FilterStateManager } from './filter_state_manager'; diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/test_helpers/get_stub_filter.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/test_helpers/get_stub_filter.ts deleted file mode 100644 index 74eaad34fe160e..00000000000000 --- a/src/legacy/core_plugins/data/public/filter/filter_manager/test_helpers/get_stub_filter.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { Filter } from '../../../../../../../plugins/data/public'; - -export function getFilter( - store: any, // I don't want to export only for this, as it should move to data plugin - disabled: boolean, - negated: boolean, - queryKey: string, - queryValue: any -): Filter { - return { - $state: { - store, - }, - meta: { - index: 'logstash-*', - disabled, - negate: negated, - alias: null, - }, - query: { - match: { - [queryKey]: queryValue, - }, - }, - }; -} diff --git a/src/legacy/core_plugins/data/public/filter/filter_manager/test_helpers/stub_state.ts b/src/legacy/core_plugins/data/public/filter/filter_manager/test_helpers/stub_state.ts deleted file mode 100644 index 272c8a4e199134..00000000000000 --- a/src/legacy/core_plugins/data/public/filter/filter_manager/test_helpers/stub_state.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import sinon from 'sinon'; - -import { State } from 'ui/state_management/state'; -import { Filter } from '../../../../../../../plugins/data/public'; - -export class StubState implements State { - filters: Filter[]; - save: sinon.SinonSpy; - - constructor() { - this.save = sinon.stub(); - this.filters = []; - } - - getQueryParamName() { - return '_a'; - } - - translateHashToRison(stateHashOrRison: string | string[]): string | string[] { - return ''; - } -} diff --git a/src/legacy/core_plugins/data/public/index.ts b/src/legacy/core_plugins/data/public/index.ts index dcaaf3ca511cc7..e2066fcfd5e979 100644 --- a/src/legacy/core_plugins/data/public/index.ts +++ b/src/legacy/core_plugins/data/public/index.ts @@ -28,7 +28,6 @@ export function plugin() { /** @public types */ export { DataSetup, DataStart } from './plugin'; -export { SavedQuery, SavedQueryTimeFilter } from '../../../../plugins/data/public'; export { // agg_types AggParam, // only the type is used externally, only in vis editor @@ -46,7 +45,6 @@ export { /** @public static code */ export * from '../common'; -export { FilterStateManager } from './filter/filter_manager'; export { // agg_types TODO need to group these under a namespace or prefix AggConfigs, diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js index 5ccee2b92ce336..c939de9b57078b 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/discover.js @@ -237,28 +237,9 @@ function discoverController( $scope.state = { ...newState }; // detect changes that should trigger fetching of new data - const changes = ['interval', 'sort', 'index', 'query'].filter( + const changes = ['interval', 'sort', 'query'].filter( prop => !_.isEqual(newStatePartial[prop], oldStatePartial[prop]) ); - if (changes.indexOf('index') !== -1) { - try { - $scope.indexPattern = await indexPatterns.get(newStatePartial.index); - $scope.opts.timefield = getTimeField(); - $scope.enableTimeRangeSelector = !!$scope.opts.timefield; - // is needed to rerender the histogram - $scope.vis = undefined; - - // Taking care of sort when switching index pattern: - // Old indexPattern: sort by A - // If A is not available in the new index pattern, sort has to be adapted and propagated to URL - const sort = getSortArray(newStatePartial.sort, $scope.indexPattern); - if (newStatePartial.sort && !_.isEqual(sort, newStatePartial.sort)) { - return await replaceUrlAppState({ sort }); - } - } catch (e) { - toastNotifications.addWarning({ text: getIndexPatternWarning(newStatePartial.index) }); - } - } if (changes.length) { $fetchObservable.next(); @@ -267,8 +248,9 @@ function discoverController( } }); - $scope.setIndexPattern = id => { - setAppState({ index: id }); + $scope.setIndexPattern = async id => { + await replaceUrlAppState({ index: id }); + $route.reload(); }; // update data source when filters update diff --git a/src/legacy/ui/public/filter_manager/index.js b/src/legacy/ui/public/filter_manager/index.js deleted file mode 100644 index 9880b336e76e5c..00000000000000 --- a/src/legacy/ui/public/filter_manager/index.js +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ diff --git a/src/legacy/ui/public/filter_manager/query_filter.d.ts b/src/legacy/ui/public/filter_manager/query_filter.d.ts deleted file mode 100644 index b5d7742f51d460..00000000000000 --- a/src/legacy/ui/public/filter_manager/query_filter.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export type QueryFilter = any; - -export const FilterBarQueryFilterProvider: () => QueryFilter; diff --git a/src/legacy/ui/public/filter_manager/query_filter.js b/src/legacy/ui/public/filter_manager/query_filter.js deleted file mode 100644 index 97b3810b7f1c73..00000000000000 --- a/src/legacy/ui/public/filter_manager/query_filter.js +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { FilterStateManager } from 'plugins/data'; -import { npStart } from 'ui/new_platform'; - -export function FilterBarQueryFilterProvider(getAppState, globalState) { - const { filterManager } = npStart.plugins.data.query; - const filterStateManager = new FilterStateManager(globalState, getAppState, filterManager); - - const queryFilter = {}; - queryFilter.getUpdates$ = filterManager.getUpdates$.bind(filterManager); - queryFilter.getFetches$ = filterManager.getFetches$.bind(filterManager); - queryFilter.getFilters = filterManager.getFilters.bind(filterManager); - queryFilter.getAppFilters = filterManager.getAppFilters.bind(filterManager); - queryFilter.getGlobalFilters = filterManager.getGlobalFilters.bind(filterManager); - queryFilter.removeFilter = filterManager.removeFilter.bind(filterManager); - queryFilter.addFilters = filterManager.addFilters.bind(filterManager); - queryFilter.setFilters = filterManager.setFilters.bind(filterManager); - queryFilter.removeAll = filterManager.removeAll.bind(filterManager); - - queryFilter.destroy = () => { - filterManager.destroy(); - filterStateManager.destroy(); - }; - - return queryFilter; -} diff --git a/tasks/test.js b/tasks/test.js index 504247f5b53551..5618ebba4e6ebe 100644 --- a/tasks/test.js +++ b/tasks/test.js @@ -61,7 +61,7 @@ module.exports = function(grunt) { 'run:apiIntegrationTests', ]); - grunt.registerTask('test:karmaDebug', ['checkPlugins', 'run:karmaDebugServer', 'karma:dev']); + grunt.registerTask('test:karmaDebug', ['checkPlugins', 'run:karmaTestDebugServer', 'karma:dev']); grunt.registerTask('test:mochaCoverage', ['run:mochaCoverage']); grunt.registerTask('test', subTask => { diff --git a/test/accessibility/apps/discover.ts b/test/accessibility/apps/discover.ts index e25d2955159710..cf3d37d29b4915 100644 --- a/test/accessibility/apps/discover.ts +++ b/test/accessibility/apps/discover.ts @@ -25,7 +25,13 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); const inspector = getService('inspector'); + const docTable = getService('docTable'); const filterBar = getService('filterBar'); + const TEST_COLUMN_NAMES = ['@message']; + const TEST_FILTER_COLUMN_NAMES = [ + ['extension', 'jpg'], + ['geo.src', 'IN'], + ]; describe('Discover', () => { before(async () => { @@ -57,7 +63,6 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { await a11y.testAppSnapshot(); }); - // skipping the test for new because we can't fix it right now it.skip('Click on new to clear the search', async () => { await PageObjects.discover.clickNewSearchButton(); await a11y.testAppSnapshot(); @@ -94,7 +99,6 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { await a11y.testAppSnapshot(); }); - // unable to validate on EUI pop-over it('click share button', async () => { await PageObjects.share.clickShareTopNavButton(); await a11y.testAppSnapshot(); @@ -109,5 +113,29 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.discover.closeSidebarFieldFilter(); await a11y.testAppSnapshot(); }); + + it('Add a field from sidebar', async () => { + for (const columnName of TEST_COLUMN_NAMES) { + await PageObjects.discover.clickFieldListItemAdd(columnName); + } + await a11y.testAppSnapshot(); + }); + + it.skip('Add more fields from sidebar', async () => { + for (const [columnName, value] of TEST_FILTER_COLUMN_NAMES) { + await PageObjects.discover.clickFieldListItem(columnName); + await PageObjects.discover.clickFieldListPlusFilter(columnName, value); + } + await a11y.testAppSnapshot(); + }); + + // Context view test + it('should open context view on a doc', async () => { + await docTable.clickRowToggle(); + await (await docTable.getRowActions())[0].click(); + await a11y.testAppSnapshot(); + }); + + // Adding rest of the tests after https://github.com/elastic/kibana/issues/53888 is resolved }); } diff --git a/test/functional/apps/management/_handle_alias.js b/test/functional/apps/management/_handle_alias.js index 3d9368f8d46807..55f6b56d9f0d1d 100644 --- a/test/functional/apps/management/_handle_alias.js +++ b/test/functional/apps/management/_handle_alias.js @@ -25,7 +25,8 @@ export default function({ getService, getPageObjects }) { const retry = getService('retry'); const PageObjects = getPageObjects(['common', 'home', 'settings', 'discover', 'timePicker']); - describe('Index patterns on aliases', function() { + // FLAKY: https://github.com/elastic/kibana/issues/59717 + describe.skip('Index patterns on aliases', function() { before(async function() { await esArchiver.loadIfNeeded('alias'); await esArchiver.load('empty_kibana'); diff --git a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index c8d7cf29a35614..a456372c99c01d 100644 --- a/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/legacy/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -24,8 +24,7 @@ import { Document } from '../../persistence/saved_object_store'; import { getSavedObjectFormat } from './save'; import { WorkspacePanelWrapper } from './workspace_panel_wrapper'; import { generateId } from '../../id_generator'; -import { SavedQuery } from '../../../../../../../src/legacy/core_plugins/data/public'; -import { Filter, Query } from '../../../../../../../src/plugins/data/public'; +import { Filter, Query, SavedQuery } from '../../../../../../../src/plugins/data/public'; export interface EditorFrameProps { doc?: Document; diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx index c669369e6e1d05..3066ac0e113251 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx @@ -206,6 +206,9 @@ const initialState: IndexPatternPrivateState = { }, }, }; + +const dslQuery = { bool: { must: [{ match_all: {} }], filter: [], should: [], must_not: [] } }; + describe('IndexPattern Data Panel', () => { let defaultProps: Parameters[0]; let core: ReturnType; @@ -271,8 +274,8 @@ describe('IndexPattern Data Panel', () => { describe('loading existence data', () => { function testProps() { const setState = jest.fn(); - core.http.get.mockImplementation(async ({ path }) => { - const parts = path.split('/'); + core.http.post.mockImplementation(async path => { + const parts = ((path as unknown) as string).split('/'); const indexPatternTitle = parts[parts.length - 1]; return { indexPatternTitle: `${indexPatternTitle}_testtitle`, @@ -385,24 +388,24 @@ describe('IndexPattern Data Panel', () => { }); expect(setState).toHaveBeenCalledTimes(2); - expect(core.http.get).toHaveBeenCalledTimes(2); + expect(core.http.post).toHaveBeenCalledTimes(2); - expect(core.http.get).toHaveBeenCalledWith({ - path: '/api/lens/existing_fields/a', - query: { + expect(core.http.post).toHaveBeenCalledWith('/api/lens/existing_fields/a', { + body: JSON.stringify({ + dslQuery, fromDate: '2019-01-01', toDate: '2020-01-01', timeFieldName: 'atime', - }, + }), }); - expect(core.http.get).toHaveBeenCalledWith({ - path: '/api/lens/existing_fields/a', - query: { + expect(core.http.post).toHaveBeenCalledWith('/api/lens/existing_fields/a', { + body: JSON.stringify({ + dslQuery, fromDate: '2019-01-01', toDate: '2020-01-02', timeFieldName: 'atime', - }, + }), }); const nextState = setState.mock.calls[1][0]({ @@ -428,22 +431,22 @@ describe('IndexPattern Data Panel', () => { expect(setState).toHaveBeenCalledTimes(2); - expect(core.http.get).toHaveBeenCalledWith({ - path: '/api/lens/existing_fields/a', - query: { + expect(core.http.post).toHaveBeenCalledWith('/api/lens/existing_fields/a', { + body: JSON.stringify({ + dslQuery, fromDate: '2019-01-01', toDate: '2020-01-01', timeFieldName: 'atime', - }, + }), }); - expect(core.http.get).toHaveBeenCalledWith({ - path: '/api/lens/existing_fields/b', - query: { + expect(core.http.post).toHaveBeenCalledWith('/api/lens/existing_fields/b', { + body: JSON.stringify({ + dslQuery, fromDate: '2019-01-01', toDate: '2020-01-01', timeFieldName: 'btime', - }, + }), }); const nextState = setState.mock.calls[1][0]({ @@ -476,13 +479,13 @@ describe('IndexPattern Data Panel', () => { let overlapCount = 0; const props = testProps(); - core.http.get.mockImplementation(({ path }) => { + core.http.post.mockImplementation(path => { if (queryCount) { ++overlapCount; } ++queryCount; - const parts = path.split('/'); + const parts = ((path as unknown) as string).split('/'); const indexPatternTitle = parts[parts.length - 1]; const result = Promise.resolve({ indexPatternTitle, @@ -516,7 +519,7 @@ describe('IndexPattern Data Panel', () => { inst.update(); }); - expect(core.http.get).toHaveBeenCalledTimes(2); + expect(core.http.post).toHaveBeenCalledTimes(2); expect(overlapCount).toEqual(0); }); }); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.tsx b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.tsx index 1b86c07a31c11d..7a3c04b67fbc41 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.tsx +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/datapanel.tsx @@ -40,6 +40,7 @@ import { trackUiEvent } from '../lens_ui_telemetry'; import { syncExistingFields } from './loader'; import { fieldExists } from './pure_helpers'; import { Loader } from '../loader'; +import { esQuery, IIndexPattern } from '../../../../../../src/plugins/data/public'; export type Props = DatasourceDataPanelProps & { changeIndexPattern: ( @@ -113,6 +114,13 @@ export function IndexPatternDataPanel({ timeFieldName: indexPatterns[id].timeFieldName, })); + const dslQuery = esQuery.buildEsQuery( + indexPatterns[currentIndexPatternId] as IIndexPattern, + query, + filters, + esQuery.getEsQueryConfig(core.uiSettings) + ); + return ( <> `${x.title}:${x.timeFieldName}`).join(','), diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.test.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.test.ts index f8961c30d14ee2..ea9c8213ba9098 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.test.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.test.ts @@ -535,9 +535,18 @@ describe('loader', () => { }); describe('syncExistingFields', () => { + const dslQuery = { + bool: { + must: [], + filter: [{ match_all: {} }], + should: [], + must_not: [], + }, + }; + it('should call once for each index pattern', async () => { const setState = jest.fn(); - const fetchJson = jest.fn(({ path }: { path: string }) => { + const fetchJson = jest.fn((path: string) => { const indexPatternTitle = _.last(path.split('/')); return { indexPatternTitle, @@ -553,6 +562,7 @@ describe('loader', () => { fetchJson: fetchJson as any, indexPatterns: [{ id: 'a' }, { id: 'b' }, { id: 'c' }], setState, + dslQuery, }); expect(fetchJson).toHaveBeenCalledTimes(3); diff --git a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.ts b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.ts index ed3d8a91b366d7..f4d5857f4826d4 100644 --- a/x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.ts +++ b/x-pack/legacy/plugins/lens/public/indexpattern_datasource/loader.ts @@ -215,26 +215,28 @@ export async function syncExistingFields({ dateRange, fetchJson, setState, + dslQuery, }: { dateRange: DateRange; indexPatterns: Array<{ id: string; timeFieldName?: string | null }>; - fetchJson: HttpSetup['get']; + fetchJson: HttpSetup['post']; setState: SetState; + dslQuery: object; }) { const emptinessInfo = await Promise.all( indexPatterns.map(pattern => { - const query: Record = { + const body: Record = { + dslQuery, fromDate: dateRange.fromDate, toDate: dateRange.toDate, }; if (pattern.timeFieldName) { - query.timeFieldName = pattern.timeFieldName; + body.timeFieldName = pattern.timeFieldName; } - return fetchJson({ - path: `${BASE_API_URL}/existing_fields/${pattern.id}`, - query, + return fetchJson(`${BASE_API_URL}/existing_fields/${pattern.id}`, { + body: JSON.stringify(body), }) as Promise; }) ); diff --git a/x-pack/legacy/plugins/lens/public/types.ts b/x-pack/legacy/plugins/lens/public/types.ts index b62b920429e7c2..b7983eeb8dbb8b 100644 --- a/x-pack/legacy/plugins/lens/public/types.ts +++ b/x-pack/legacy/plugins/lens/public/types.ts @@ -7,12 +7,11 @@ import { Ast } from '@kbn/interpreter/common'; import { IconType } from '@elastic/eui/src/components/icon/icon'; import { CoreSetup } from 'src/core/public'; -import { SavedQuery } from 'src/legacy/core_plugins/data/public'; import { KibanaDatatable } from '../../../../../src/plugins/expressions/public'; import { DragContextState } from './drag_drop'; import { Document } from './persistence'; import { DateRange } from '../../../../plugins/lens/common'; -import { Query, Filter } from '../../../../../src/plugins/data/public'; +import { Query, Filter, SavedQuery } from '../../../../../src/plugins/data/public'; // eslint-disable-next-line export interface EditorFrameOptions {} diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx index c744c357c9550d..983375ecd4f61b 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/create_analytics_form.tsx @@ -610,7 +610,7 @@ export const CreateAnalyticsForm: FC = ({ actions, sta })} helpText={i18n.translate('xpack.ml.dataframe.analytics.create.excludedFieldsHelpText', { defaultMessage: - 'Optionally select fields to be excluded from analysis. All other supported fields will be included', + 'Select fields to exclude from analysis. All other supported fields are included.', })} error={ excludesOptions.length === 0 && diff --git a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/job_type.tsx b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/job_type.tsx index 988daac528fd77..ffed1ebf522f40 100644 --- a/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/job_type.tsx +++ b/x-pack/legacy/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/create_analytics_form/job_type.tsx @@ -21,7 +21,7 @@ export const JobType: FC = ({ type, setFormState }) => { 'xpack.ml.dataframe.analytics.create.outlierDetectionHelpText', { defaultMessage: - 'Outlier detection jobs require a source index that is mapped as a table-like data structure and will only analyze numeric and boolean fields. Please use the advanced editor to add custom options to the configuration.', + 'Outlier detection jobs require a source index that is mapped as a table-like data structure and analyze only numeric and boolean fields. Use the advanced editor to add custom options to the configuration.', } ); @@ -29,7 +29,7 @@ export const JobType: FC = ({ type, setFormState }) => { 'xpack.ml.dataframe.analytics.create.outlierRegressionHelpText', { defaultMessage: - 'Regression jobs will only analyze numeric fields. Please use the advanced editor to apply custom options such as the prediction field name.', + 'Regression jobs analyze only numeric fields. Use the advanced editor to apply custom options, such as the prediction field name.', } ); @@ -37,7 +37,7 @@ export const JobType: FC = ({ type, setFormState }) => { 'xpack.ml.dataframe.analytics.create.classificationHelpText', { defaultMessage: - 'Classification jobs require a source index that is mapped as a table-like data structure and supports fields that are numeric, boolean, text, keyword or ip. Please use the advanced editor to apply custom options such as the prediction field name.', + 'Classification jobs require a source index that is mapped as a table-like data structure and support fields that are numeric, boolean, text, keyword, or ip. Use the advanced editor to apply custom options, such as the prediction field name.', } ); diff --git a/x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx b/x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx index 2513004af84dd6..4dd1b114ccff39 100644 --- a/x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/search_bar/index.tsx @@ -11,8 +11,14 @@ import { Dispatch } from 'redux'; import { Subscription } from 'rxjs'; import styled from 'styled-components'; import deepEqual from 'fast-deep-equal'; -import { FilterManager, IIndexPattern, TimeRange, Query, Filter } from 'src/plugins/data/public'; -import { SavedQuery } from 'src/legacy/core_plugins/data/public'; +import { + FilterManager, + IIndexPattern, + TimeRange, + Query, + Filter, + SavedQuery, +} from 'src/plugins/data/public'; import { OnTimeChangeProps } from '@elastic/eui'; diff --git a/x-pack/legacy/plugins/siem/public/components/search_bar/selectors.ts b/x-pack/legacy/plugins/siem/public/components/search_bar/selectors.ts index f501466db9ed96..793737a1ad754c 100644 --- a/x-pack/legacy/plugins/siem/public/components/search_bar/selectors.ts +++ b/x-pack/legacy/plugins/siem/public/components/search_bar/selectors.ts @@ -5,9 +5,8 @@ */ import { createSelector } from 'reselect'; -import { SavedQuery } from 'src/legacy/core_plugins/data/public'; import { InputsRange } from '../../store/inputs/model'; -import { Query } from '../../../../../../../src/plugins/data/public'; +import { Query, SavedQuery } from '../../../../../../../src/plugins/data/public'; export { endSelector, diff --git a/x-pack/legacy/plugins/siem/public/containers/case/api.ts b/x-pack/legacy/plugins/siem/public/containers/case/api.ts index 89190afabef9fc..ce98dd3573d300 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/api.ts @@ -119,3 +119,11 @@ export const patchComment = async ( ); return convertToCamelCase(decodeCommentResponse(response)); }; + +export const deleteCases = async (caseIds: string[]): Promise => { + const response = await KibanaServices.get().http.fetch(`${CASES_URL}`, { + method: 'DELETE', + query: { ids: JSON.stringify(caseIds) }, + }); + return response === 'true' ? true : false; +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/constants.ts b/x-pack/legacy/plugins/siem/public/containers/case/constants.ts index ac62ba7b6f9970..a0e57faa7661f4 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/constants.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/constants.ts @@ -7,11 +7,3 @@ export const CASES_URL = `/api/cases`; export const DEFAULT_TABLE_ACTIVE_PAGE = 1; export const DEFAULT_TABLE_LIMIT = 5; -export const FETCH_FAILURE = 'FETCH_FAILURE'; -export const FETCH_INIT = 'FETCH_INIT'; -export const FETCH_SUCCESS = 'FETCH_SUCCESS'; -export const POST_NEW_CASE = 'POST_NEW_CASE'; -export const POST_NEW_COMMENT = 'POST_NEW_COMMENT'; -export const UPDATE_FILTER_OPTIONS = 'UPDATE_FILTER_OPTIONS'; -export const UPDATE_TABLE_SELECTIONS = 'UPDATE_TABLE_SELECTIONS'; -export const UPDATE_QUERY_PARAMS = 'UPDATE_QUERY_PARAMS'; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/types.ts b/x-pack/legacy/plugins/siem/public/containers/case/types.ts index d479abdbd44891..c89993ec671795 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/case/types.ts @@ -16,6 +16,7 @@ export interface Comment { export interface Case { id: string; comments: Comment[]; + commentIds: string[]; createdAt: string; createdBy: ElasticUser; description: string; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.tsx new file mode 100644 index 00000000000000..d5a3b3cf9314cb --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_delete_cases.tsx @@ -0,0 +1,121 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useCallback, useReducer } from 'react'; +import { errorToToaster, useStateToaster } from '../../components/toasters'; +import * as i18n from './translations'; +import { deleteCases } from './api'; + +interface DeleteState { + isDisplayConfirmDeleteModal: boolean; + isDeleted: boolean; + isLoading: boolean; + isError: boolean; +} +type Action = + | { type: 'DISPLAY_MODAL'; payload: boolean } + | { type: 'FETCH_INIT' } + | { type: 'FETCH_SUCCESS'; payload: boolean } + | { type: 'FETCH_FAILURE' } + | { type: 'RESET_IS_DELETED' }; + +const dataFetchReducer = (state: DeleteState, action: Action): DeleteState => { + switch (action.type) { + case 'DISPLAY_MODAL': + return { + ...state, + isDisplayConfirmDeleteModal: action.payload, + }; + case 'FETCH_INIT': + return { + ...state, + isLoading: true, + isError: false, + }; + case 'FETCH_SUCCESS': + return { + ...state, + isLoading: false, + isError: false, + isDeleted: action.payload, + }; + case 'FETCH_FAILURE': + return { + ...state, + isLoading: false, + isError: true, + }; + case 'RESET_IS_DELETED': + return { + ...state, + isDeleted: false, + }; + default: + return state; + } +}; +interface UseDeleteCase extends DeleteState { + dispatchResetIsDeleted: () => void; + handleOnDeleteConfirm: (caseIds: string[]) => void; + handleToggleModal: () => void; +} + +export const useDeleteCases = (): UseDeleteCase => { + const [state, dispatch] = useReducer(dataFetchReducer, { + isDisplayConfirmDeleteModal: false, + isLoading: false, + isError: false, + isDeleted: false, + }); + const [, dispatchToaster] = useStateToaster(); + + const dispatchDeleteCases = useCallback((caseIds: string[]) => { + let cancel = false; + const deleteData = async () => { + try { + dispatch({ type: 'FETCH_INIT' }); + await deleteCases(caseIds); + if (!cancel) { + dispatch({ type: 'FETCH_SUCCESS', payload: true }); + } + } catch (error) { + if (!cancel) { + errorToToaster({ + title: i18n.ERROR_TITLE, + error: error.body && error.body.message ? new Error(error.body.message) : error, + dispatchToaster, + }); + dispatch({ type: 'FETCH_FAILURE' }); + } + } + }; + deleteData(); + return () => { + cancel = true; + }; + }, []); + + const dispatchToggleDeleteModal = useCallback(() => { + dispatch({ type: 'DISPLAY_MODAL', payload: !state.isDisplayConfirmDeleteModal }); + }, [state.isDisplayConfirmDeleteModal]); + + const dispatchResetIsDeleted = useCallback(() => { + dispatch({ type: 'RESET_IS_DELETED' }); + }, [state.isDisplayConfirmDeleteModal]); + + const handleOnDeleteConfirm = useCallback( + caseIds => { + dispatchDeleteCases(caseIds); + dispatchToggleDeleteModal(); + }, + [state.isDisplayConfirmDeleteModal] + ); + const handleToggleModal = useCallback(() => { + dispatchToggleDeleteModal(); + }, [state.isDisplayConfirmDeleteModal]); + + return { ...state, dispatchResetIsDeleted, handleOnDeleteConfirm, handleToggleModal }; +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx index b758f914c991ea..6020969ed63751 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_case.tsx @@ -7,8 +7,6 @@ import { useEffect, useReducer } from 'react'; import { Case } from './types'; -import { FETCH_INIT, FETCH_FAILURE, FETCH_SUCCESS } from './constants'; -import { getTypedPayload } from './utils'; import * as i18n from './translations'; import { errorToToaster, useStateToaster } from '../../components/toasters'; import { getCase } from './api'; @@ -18,40 +16,42 @@ interface CaseState { isLoading: boolean; isError: boolean; } -interface Action { - type: string; - payload?: Case; -} + +type Action = + | { type: 'FETCH_INIT' } + | { type: 'FETCH_SUCCESS'; payload: Case } + | { type: 'FETCH_FAILURE' }; const dataFetchReducer = (state: CaseState, action: Action): CaseState => { switch (action.type) { - case FETCH_INIT: + case 'FETCH_INIT': return { ...state, isLoading: true, isError: false, }; - case FETCH_SUCCESS: + case 'FETCH_SUCCESS': return { ...state, isLoading: false, isError: false, - data: getTypedPayload(action.payload), + data: action.payload, }; - case FETCH_FAILURE: + case 'FETCH_FAILURE': return { ...state, isLoading: false, isError: true, }; default: - throw new Error(); + return state; } }; const initialData: Case = { id: '', createdAt: '', comments: [], + commentIds: [], createdBy: { username: '', }, @@ -63,7 +63,7 @@ const initialData: Case = { version: '', }; -export const useGetCase = (caseId: string): [CaseState] => { +export const useGetCase = (caseId: string): CaseState => { const [state, dispatch] = useReducer(dataFetchReducer, { isLoading: true, isError: false, @@ -74,11 +74,11 @@ export const useGetCase = (caseId: string): [CaseState] => { const callFetch = () => { let didCancel = false; const fetchData = async () => { - dispatch({ type: FETCH_INIT }); + dispatch({ type: 'FETCH_INIT' }); try { const response = await getCase(caseId); if (!didCancel) { - dispatch({ type: FETCH_SUCCESS, payload: response }); + dispatch({ type: 'FETCH_SUCCESS', payload: response }); } } catch (error) { if (!didCancel) { @@ -87,7 +87,7 @@ export const useGetCase = (caseId: string): [CaseState] => { error: error.body && error.body.message ? new Error(error.body.message) : error, dispatchToaster, }); - dispatch({ type: FETCH_FAILURE }); + dispatch({ type: 'FETCH_FAILURE' }); } } }; @@ -100,5 +100,5 @@ export const useGetCase = (caseId: string): [CaseState] => { useEffect(() => { callFetch(); }, [caseId]); - return [state]; + return state; }; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx index 99c7ef0c757c70..1c7c30ae9da187 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_cases.tsx @@ -96,7 +96,7 @@ const dataFetchReducer = (state: UseGetCasesState, action: Action): UseGetCasesS selectedCases: action.payload, }; default: - throw new Error(); + return state; } }; @@ -109,6 +109,7 @@ const initialData: AllCases = { interface UseGetCases extends UseGetCasesState { dispatchUpdateCaseProperty: ({ updateKey, updateValue, caseId, version }: UpdateCase) => void; getCaseCount: (caseState: keyof CaseCount) => void; + refetchCases: (filters: FilterOptions, queryParams: QueryParams) => void; setFilters: (filters: FilterOptions) => void; setQueryParams: (queryParams: QueryParams) => void; setSelectedCases: (mySelectedCases: Case[]) => void; @@ -245,10 +246,17 @@ export const useGetCases = (): UseGetCases => { [state.filterOptions, state.queryParams] ); + const refetchCases = useCallback(() => { + fetchCases(state.filterOptions, state.queryParams); + getCaseCount('open'); + getCaseCount('closed'); + }, [state.filterOptions, state.queryParams]); + return { ...state, dispatchUpdateCaseProperty, getCaseCount, + refetchCases, setFilters, setQueryParams, setSelectedCases, diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.tsx index 5e6df9b92f462e..e3657f5b09da9d 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_get_tags.tsx @@ -8,63 +8,61 @@ import { useEffect, useReducer } from 'react'; import { errorToToaster, useStateToaster } from '../../components/toasters'; import { getTags } from './api'; -import { FETCH_FAILURE, FETCH_INIT, FETCH_SUCCESS } from './constants'; import * as i18n from './translations'; interface TagsState { - data: string[]; + tags: string[]; isLoading: boolean; isError: boolean; } -interface Action { - type: string; - payload?: string[]; -} +type Action = + | { type: 'FETCH_INIT' } + | { type: 'FETCH_SUCCESS'; payload: string[] } + | { type: 'FETCH_FAILURE' }; const dataFetchReducer = (state: TagsState, action: Action): TagsState => { switch (action.type) { - case FETCH_INIT: + case 'FETCH_INIT': return { ...state, isLoading: true, isError: false, }; - case FETCH_SUCCESS: - const getTypedPayload = (a: Action['payload']) => a as string[]; + case 'FETCH_SUCCESS': return { ...state, isLoading: false, isError: false, - data: getTypedPayload(action.payload), + tags: action.payload, }; - case FETCH_FAILURE: + case 'FETCH_FAILURE': return { ...state, isLoading: false, isError: true, }; default: - throw new Error(); + return state; } }; const initialData: string[] = []; -export const useGetTags = (): [TagsState] => { +export const useGetTags = (): TagsState => { const [state, dispatch] = useReducer(dataFetchReducer, { isLoading: false, isError: false, - data: initialData, + tags: initialData, }); const [, dispatchToaster] = useStateToaster(); useEffect(() => { let didCancel = false; const fetchData = async () => { - dispatch({ type: FETCH_INIT }); + dispatch({ type: 'FETCH_INIT' }); try { const response = await getTags(); if (!didCancel) { - dispatch({ type: FETCH_SUCCESS, payload: response }); + dispatch({ type: 'FETCH_SUCCESS', payload: response }); } } catch (error) { if (!didCancel) { @@ -73,7 +71,7 @@ export const useGetTags = (): [TagsState] => { error: error.body && error.body.message ? new Error(error.body.message) : error, dispatchToaster, }); - dispatch({ type: FETCH_FAILURE }); + dispatch({ type: 'FETCH_FAILURE' }); } } }; @@ -82,5 +80,5 @@ export const useGetTags = (): [TagsState] => { didCancel = true; }; }, []); - return [state]; + return state; }; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx index 5cd0911fae81aa..14b9e788469062 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_post_case.tsx @@ -9,7 +9,6 @@ import { useReducer, useCallback } from 'react'; import { CaseRequest } from '../../../../../../plugins/case/common/api'; import { errorToToaster, useStateToaster } from '../../components/toasters'; import { postCase } from './api'; -import { FETCH_FAILURE, FETCH_INIT, FETCH_SUCCESS } from './constants'; import * as i18n from './translations'; import { Case } from './types'; @@ -18,34 +17,34 @@ interface NewCaseState { isLoading: boolean; isError: boolean; } -interface Action { - type: string; - payload?: Case; -} +type Action = + | { type: 'FETCH_INIT' } + | { type: 'FETCH_SUCCESS'; payload: Case } + | { type: 'FETCH_FAILURE' }; const dataFetchReducer = (state: NewCaseState, action: Action): NewCaseState => { switch (action.type) { - case FETCH_INIT: + case 'FETCH_INIT': return { ...state, isLoading: true, isError: false, }; - case FETCH_SUCCESS: + case 'FETCH_SUCCESS': return { ...state, isLoading: false, isError: false, caseData: action.payload ?? null, }; - case FETCH_FAILURE: + case 'FETCH_FAILURE': return { ...state, isLoading: false, isError: true, }; default: - throw new Error(); + return state; } }; @@ -63,11 +62,11 @@ export const usePostCase = (): UsePostCase => { const postMyCase = useCallback(async (data: CaseRequest) => { let cancel = false; try { - dispatch({ type: FETCH_INIT }); + dispatch({ type: 'FETCH_INIT' }); const response = await postCase({ ...data, state: 'open' }); if (!cancel) { dispatch({ - type: FETCH_SUCCESS, + type: 'FETCH_SUCCESS', payload: response, }); } @@ -78,7 +77,7 @@ export const usePostCase = (): UsePostCase => { error: error.body && error.body.message ? new Error(error.body.message) : error, dispatchToaster, }); - dispatch({ type: FETCH_FAILURE }); + dispatch({ type: 'FETCH_FAILURE' }); } } return () => { diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.tsx index 1467c691f547eb..a96cb97d7cc7ba 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_post_comment.tsx @@ -10,7 +10,6 @@ import { CommentRequest } from '../../../../../../plugins/case/common/api'; import { errorToToaster, useStateToaster } from '../../components/toasters'; import { postComment } from './api'; -import { FETCH_FAILURE, FETCH_INIT, FETCH_SUCCESS } from './constants'; import * as i18n from './translations'; import { Comment } from './types'; @@ -20,39 +19,46 @@ interface NewCommentState { isError: boolean; caseId: string; } -interface Action { - type: string; - payload?: Comment; -} +type Action = + | { type: 'RESET_COMMENT_DATA' } + | { type: 'FETCH_INIT' } + | { type: 'FETCH_SUCCESS'; payload: Comment } + | { type: 'FETCH_FAILURE' }; const dataFetchReducer = (state: NewCommentState, action: Action): NewCommentState => { switch (action.type) { - case FETCH_INIT: + case 'RESET_COMMENT_DATA': + return { + ...state, + commentData: null, + }; + case 'FETCH_INIT': return { ...state, isLoading: true, isError: false, }; - case FETCH_SUCCESS: + case 'FETCH_SUCCESS': return { ...state, isLoading: false, isError: false, commentData: action.payload ?? null, }; - case FETCH_FAILURE: + case 'FETCH_FAILURE': return { ...state, isLoading: false, isError: true, }; default: - throw new Error(); + return state; } }; interface UsePostComment extends NewCommentState { postComment: (data: CommentRequest) => void; + resetCommentData: () => void; } export const usePostComment = (caseId: string): UsePostComment => { @@ -67,10 +73,10 @@ export const usePostComment = (caseId: string): UsePostComment => { const postMyComment = useCallback(async (data: CommentRequest) => { let cancel = false; try { - dispatch({ type: FETCH_INIT }); + dispatch({ type: 'FETCH_INIT' }); const response = await postComment(data, state.caseId); if (!cancel) { - dispatch({ type: FETCH_SUCCESS, payload: response }); + dispatch({ type: 'FETCH_SUCCESS', payload: response }); } } catch (error) { if (!cancel) { @@ -79,7 +85,7 @@ export const usePostComment = (caseId: string): UsePostComment => { error: error.body && error.body.message ? new Error(error.body.message) : error, dispatchToaster, }); - dispatch({ type: FETCH_FAILURE }); + dispatch({ type: 'FETCH_FAILURE' }); } } return () => { @@ -87,5 +93,7 @@ export const usePostComment = (caseId: string): UsePostComment => { }; }, []); - return { ...state, postComment: postMyComment }; + const resetCommentData = useCallback(() => dispatch({ type: 'RESET_COMMENT_DATA' }), []); + + return { ...state, postComment: postMyComment, resetCommentData }; }; diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx index 594677aefe245c..2b1081b9b901c8 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_update_case.tsx @@ -10,10 +10,8 @@ import { CaseRequest } from '../../../../../../plugins/case/common/api'; import { errorToToaster, useStateToaster } from '../../components/toasters'; import { patchCase } from './api'; -import { FETCH_FAILURE, FETCH_INIT, FETCH_SUCCESS } from './constants'; import * as i18n from './translations'; import { Case } from './types'; -import { getTypedPayload } from './utils'; type UpdateKey = keyof CaseRequest; @@ -29,30 +27,30 @@ export interface UpdateByKey { updateValue: CaseRequest[UpdateKey]; } -interface Action { - type: string; - payload?: Case | UpdateKey; -} +type Action = + | { type: 'FETCH_INIT'; payload: UpdateKey } + | { type: 'FETCH_SUCCESS'; payload: Case } + | { type: 'FETCH_FAILURE' }; const dataFetchReducer = (state: NewCaseState, action: Action): NewCaseState => { switch (action.type) { - case FETCH_INIT: + case 'FETCH_INIT': return { ...state, isLoading: true, isError: false, - updateKey: getTypedPayload(action.payload), + updateKey: action.payload, }; - case FETCH_SUCCESS: + case 'FETCH_SUCCESS': return { ...state, isLoading: false, isError: false, - caseData: getTypedPayload(action.payload), + caseData: action.payload, updateKey: null, }; - case FETCH_FAILURE: + case 'FETCH_FAILURE': return { ...state, isLoading: false, @@ -60,7 +58,7 @@ const dataFetchReducer = (state: NewCaseState, action: Action): NewCaseState => updateKey: null, }; default: - throw new Error(); + return state; } }; @@ -80,14 +78,14 @@ export const useUpdateCase = (caseId: string, initialData: Case): UseUpdateCase async ({ updateKey, updateValue }: UpdateByKey) => { let cancel = false; try { - dispatch({ type: FETCH_INIT, payload: updateKey }); + dispatch({ type: 'FETCH_INIT', payload: updateKey }); const response = await patchCase( caseId, { [updateKey]: updateValue }, state.caseData.version ); if (!cancel) { - dispatch({ type: FETCH_SUCCESS, payload: response }); + dispatch({ type: 'FETCH_SUCCESS', payload: response }); } } catch (error) { if (!cancel) { @@ -96,7 +94,7 @@ export const useUpdateCase = (caseId: string, initialData: Case): UseUpdateCase error: error.body && error.body.message ? new Error(error.body.message) : error, dispatchToaster, }); - dispatch({ type: FETCH_FAILURE }); + dispatch({ type: 'FETCH_FAILURE' }); } } return () => { diff --git a/x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.tsx b/x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.tsx index 0e39d2303a32a4..a40a1100ca7350 100644 --- a/x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/case/use_update_comment.tsx @@ -4,15 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useReducer, useCallback } from 'react'; +import { useReducer, useCallback, Dispatch } from 'react'; import { errorToToaster, useStateToaster } from '../../components/toasters'; import { patchComment } from './api'; -import { FETCH_FAILURE, FETCH_INIT, FETCH_SUCCESS } from './constants'; import * as i18n from './translations'; import { Comment } from './types'; -import { getTypedPayload } from './utils'; interface CommentUpdateState { comments: Comment[]; @@ -25,22 +23,28 @@ interface CommentUpdate { commentId: string; } -interface Action { - type: string; - payload?: CommentUpdate | string; -} +type Action = + | { type: 'APPEND_COMMENT'; payload: Comment } + | { type: 'FETCH_INIT'; payload: string } + | { type: 'FETCH_SUCCESS'; payload: CommentUpdate } + | { type: 'FETCH_FAILURE'; payload: string }; const dataFetchReducer = (state: CommentUpdateState, action: Action): CommentUpdateState => { switch (action.type) { - case FETCH_INIT: + case 'APPEND_COMMENT': return { ...state, - isLoadingIds: [...state.isLoadingIds, getTypedPayload(action.payload)], + comments: [...state.comments, action.payload], + }; + case 'FETCH_INIT': + return { + ...state, + isLoadingIds: [...state.isLoadingIds, action.payload], isError: false, }; - case FETCH_SUCCESS: - const updatePayload = getTypedPayload(action.payload); + case 'FETCH_SUCCESS': + const updatePayload = action.payload; const foundIndex = state.comments.findIndex( comment => comment.id === updatePayload.commentId ); @@ -55,21 +59,20 @@ const dataFetchReducer = (state: CommentUpdateState, action: Action): CommentUpd isError: false, comments: newComments, }; - case FETCH_FAILURE: + case 'FETCH_FAILURE': return { ...state, - isLoadingIds: state.isLoadingIds.filter( - id => getTypedPayload(action.payload) !== id - ), + isLoadingIds: state.isLoadingIds.filter(id => action.payload !== id), isError: true, }; default: - throw new Error(); + return state; } }; interface UseUpdateComment extends CommentUpdateState { updateComment: (caseId: string, commentId: string, commentUpdate: string) => void; + addPostedComment: Dispatch; } export const useUpdateComment = (comments: Comment[]): UseUpdateComment => { @@ -84,7 +87,7 @@ export const useUpdateComment = (comments: Comment[]): UseUpdateComment => { async (caseId: string, commentId: string, commentUpdate: string) => { let cancel = false; try { - dispatch({ type: FETCH_INIT, payload: commentId }); + dispatch({ type: 'FETCH_INIT', payload: commentId }); const currentComment = state.comments.find(comment => comment.id === commentId) ?? { version: '', }; @@ -95,7 +98,7 @@ export const useUpdateComment = (comments: Comment[]): UseUpdateComment => { currentComment.version ); if (!cancel) { - dispatch({ type: FETCH_SUCCESS, payload: { update: response, commentId } }); + dispatch({ type: 'FETCH_SUCCESS', payload: { update: response, commentId } }); } } catch (error) { if (!cancel) { @@ -104,7 +107,7 @@ export const useUpdateComment = (comments: Comment[]): UseUpdateComment => { error: error.body && error.body.message ? new Error(error.body.message) : error, dispatchToaster, }); - dispatch({ type: FETCH_FAILURE, payload: commentId }); + dispatch({ type: 'FETCH_FAILURE', payload: commentId }); } } return () => { @@ -113,6 +116,10 @@ export const useUpdateComment = (comments: Comment[]): UseUpdateComment => { }, [state] ); + const addPostedComment = useCallback( + (comment: Comment) => dispatch({ type: 'APPEND_COMMENT', payload: comment }), + [] + ); - return { ...state, updateComment: dispatchUpdateComment }; + return { ...state, updateComment: dispatchUpdateComment, addPostedComment }; }; diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx index 6b63961b4194f3..0b3b0daaf4bbcb 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/add_comment/index.tsx @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback } from 'react'; import { EuiButton, EuiLoadingSpinner } from '@elastic/eui'; +import React, { useCallback, useEffect } from 'react'; import styled from 'styled-components'; import { CommentRequest } from '../../../../../../../../plugins/case/common/api'; @@ -16,6 +16,7 @@ import * as i18n from '../../translations'; import { schema } from './schema'; import { InsertTimelinePopover } from '../../../../components/timeline/insert_timeline_popover'; import { useInsertTimeline } from '../../../../components/timeline/insert_timeline_popover/use_insert_timeline'; +import { Comment } from '../../../../containers/case/types'; const MySpinner = styled(EuiLoadingSpinner)` position: absolute; @@ -27,10 +28,13 @@ const initialCommentValue: CommentRequest = { comment: '', }; -export const AddComment = React.memo<{ +interface AddCommentProps { caseId: string; -}>(({ caseId }) => { - const { commentData, isLoading, postComment } = usePostComment(caseId); + onCommentPosted: (commentResponse: Comment) => void; +} + +export const AddComment = React.memo(({ caseId, onCommentPosted }) => { + const { commentData, isLoading, postComment, resetCommentData } = usePostComment(caseId); const { form } = useForm({ defaultValue: initialCommentValue, options: { stripEmptyFields: false }, @@ -40,6 +44,15 @@ export const AddComment = React.memo<{ form, 'comment' ); + + useEffect(() => { + if (commentData !== null) { + onCommentPosted(commentData); + form.reset(); + resetCommentData(); + } + }, [commentData]); + const onSubmit = useCallback(async () => { const { isValid, data } = await form.submit(); if (isValid) { @@ -81,8 +94,6 @@ export const AddComment = React.memo<{ }} /> - {commentData != null && - 'TO DO new comment got added but we didnt update the UI yet. Refresh the page to see your comment ;)'} ); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx index 2e57e5f2f95d9f..bc6dfe4af25ff0 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/__mock__/index.tsx @@ -14,6 +14,7 @@ export const useGetCasesMockState: UseGetCasesState = { id: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15', createdAt: '2020-02-13T19:44:23.627Z', createdBy: { username: 'elastic' }, + commentIds: [], comments: [], description: 'Security banana Issue', state: 'open', @@ -26,6 +27,7 @@ export const useGetCasesMockState: UseGetCasesState = { id: '362a5c10-4e99-11ea-9290-35d05cb55c15', createdAt: '2020-02-13T19:44:13.328Z', createdBy: { username: 'elastic' }, + commentIds: [], comments: [], description: 'Security banana Issue', state: 'open', @@ -38,6 +40,7 @@ export const useGetCasesMockState: UseGetCasesState = { id: '34f8b9e0-4e99-11ea-9290-35d05cb55c15', createdAt: '2020-02-13T19:44:11.328Z', createdBy: { username: 'elastic' }, + commentIds: [], comments: [], description: 'Security banana Issue', state: 'open', @@ -50,6 +53,7 @@ export const useGetCasesMockState: UseGetCasesState = { id: '31890e90-4e99-11ea-9290-35d05cb55c15', createdAt: '2020-02-13T19:44:05.563Z', createdBy: { username: 'elastic' }, + commentIds: [], comments: [], description: 'Security banana Issue', state: 'closed', @@ -62,6 +66,7 @@ export const useGetCasesMockState: UseGetCasesState = { id: '2f5b3210-4e99-11ea-9290-35d05cb55c15', createdAt: '2020-02-13T19:44:01.901Z', createdBy: { username: 'elastic' }, + commentIds: [], comments: [], description: 'Security banana Issue', state: 'open', diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/actions.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/actions.tsx index 0ec09f2b579182..33a1953b9d2f84 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/actions.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/actions.tsx @@ -13,18 +13,19 @@ import { UpdateCase } from '../../../../containers/case/use_get_cases'; interface GetActions { caseStatus: string; dispatchUpdate: Dispatch; + deleteCaseOnClick: (deleteCase: Case) => void; } export const getActions = ({ caseStatus, dispatchUpdate, + deleteCaseOnClick, }: GetActions): Array> => [ { description: i18n.DELETE, icon: 'trash', name: i18n.DELETE, - // eslint-disable-next-line no-console - onClick: ({ id }: Case) => console.log('TO DO Delete case', id), + onClick: deleteCaseOnClick, type: 'icon', 'data-test-subj': 'action-delete', }, diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx index f6ed2694fdc402..db3313d843547e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/columns.tsx @@ -33,9 +33,8 @@ const Spacer = styled.span` margin-left: ${({ theme }) => theme.eui.paddingSizes.s}; `; -const TempNumberComponent = () => {1}; -TempNumberComponent.displayName = 'TempNumberComponent'; - +const renderStringField = (field: string, dataTestSubj: string) => + field != null ? {field} : getEmptyTagValue(); export const getCasesColumns = ( actions: Array> ): CasesColumns[] => [ @@ -59,6 +58,7 @@ export const getCasesColumns = ( } return getEmptyTagValue(); }, + width: '25%', }, { field: 'createdBy', @@ -101,13 +101,15 @@ export const getCasesColumns = ( return getEmptyTagValue(); }, truncateText: true, + width: '20%', }, { align: 'right', - field: 'commentCount', // TO DO once we have commentCount returned in the API: https://github.com/elastic/kibana/issues/58525 + field: 'commentIds', name: i18n.COMMENTS, sortable: true, - render: TempNumberComponent, + render: (comments: Case['commentIds']) => + renderStringField(`${comments.length}`, `case-table-column-commentCount`), }, { field: 'createdAt', diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx index a9dd15086df278..10786940eee7fd 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.test.tsx @@ -15,17 +15,19 @@ import { act } from '@testing-library/react'; import { wait } from '../../../../lib/helpers'; describe('AllCases', () => { + const dispatchUpdateCaseProperty = jest.fn(); + const getCaseCount = jest.fn(); + const refetchCases = jest.fn(); const setFilters = jest.fn(); const setQueryParams = jest.fn(); const setSelectedCases = jest.fn(); - const getCaseCount = jest.fn(); - const dispatchUpdateCaseProperty = jest.fn(); beforeEach(() => { jest.resetAllMocks(); jest.spyOn(apiHook, 'useGetCases').mockReturnValue({ ...useGetCasesMockState, dispatchUpdateCaseProperty, getCaseCount, + refetchCases, setFilters, setQueryParams, setSelectedCases, diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx index 484d9051ee43f9..1d22f6a246960d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { EuiBasicTable, EuiButton, @@ -25,6 +25,7 @@ import { getCasesColumns } from './columns'; import { Case, FilterOptions, SortFieldCase } from '../../../../containers/case/types'; import { useGetCases } from '../../../../containers/case/use_get_cases'; +import { useDeleteCases } from '../../../../containers/case/use_delete_cases'; import { EuiBasicTableOnChange } from '../../../detection_engine/rules/types'; import { Panel } from '../../../../components/panel'; import { CasesTableFilters } from './table_filters'; @@ -41,6 +42,7 @@ import { getBulkItems } from '../bulk_actions'; import { CaseHeaderPage } from '../case_header_page'; import { OpenClosedStats } from '../open_closed_stats'; import { getActions } from './actions'; +import { ConfirmDeleteCaseModal } from '../confirm_delete_case'; const Div = styled.div` margin-top: ${({ theme }) => theme.eui.paddingSizes.m}; @@ -57,11 +59,9 @@ const FlexItemDivider = styled(EuiFlexItem)` const ProgressLoader = styled(EuiProgress)` ${({ theme }) => css` - .euiFlexGroup--gutterMedium > &.euiFlexItem { - top: 2px; - border-radius: ${theme.eui.euiBorderRadius}; - z-index: ${theme.eui.euiZHeader}; - } + top: 2px; + border-radius: ${theme.eui.euiBorderRadius}; + z-index: ${theme.eui.euiZHeader}; `} `; @@ -83,11 +83,95 @@ export const AllCases = React.memo(() => { loading, queryParams, selectedCases, + refetchCases, setFilters, setQueryParams, setSelectedCases, } = useGetCases(); + // Delete case + const { + dispatchResetIsDeleted, + handleOnDeleteConfirm, + handleToggleModal, + isLoading: isDeleting, + isDeleted, + isDisplayConfirmDeleteModal, + } = useDeleteCases(); + + useEffect(() => { + if (isDeleted) { + refetchCases(filterOptions, queryParams); + dispatchResetIsDeleted(); + } + }, [isDeleted, filterOptions, queryParams]); + + const [deleteThisCase, setDeleteThisCase] = useState({ + title: '', + id: '', + }); + const [deleteBulk, setDeleteBulk] = useState([]); + const confirmDeleteModal = useMemo( + () => ( + 0} + onCancel={handleToggleModal} + onConfirm={handleOnDeleteConfirm.bind( + null, + deleteBulk.length > 0 ? deleteBulk : [deleteThisCase.id] + )} + /> + ), + [deleteBulk, deleteThisCase, isDisplayConfirmDeleteModal] + ); + + const toggleDeleteModal = useCallback( + (deleteCase: Case) => { + handleToggleModal(); + setDeleteThisCase(deleteCase); + }, + [isDisplayConfirmDeleteModal] + ); + + const toggleBulkDeleteModal = useCallback( + (deleteCases: string[]) => { + handleToggleModal(); + setDeleteBulk(deleteCases); + }, + [isDisplayConfirmDeleteModal] + ); + + const selectedCaseIds = useMemo( + (): string[] => + selectedCases.reduce((arr: string[], caseObj: Case) => [...arr, caseObj.id], []), + [selectedCases] + ); + + const getBulkItemsPopoverContent = useCallback( + (closePopover: () => void) => ( + + ), + [selectedCaseIds, filterOptions.state] + ); + const actions = useMemo( + () => + getActions({ + caseStatus: filterOptions.state, + deleteCaseOnClick: toggleDeleteModal, + dispatchUpdate: dispatchUpdateCaseProperty, + }), + [filterOptions.state] + ); + const tableOnChangeCallback = useCallback( ({ page, sort }: EuiBasicTableOnChange) => { let newQueryParams = queryParams; @@ -117,12 +201,6 @@ export const AllCases = React.memo(() => { [filterOptions, setFilters] ); - const actions = useMemo( - () => - getActions({ caseStatus: filterOptions.state, dispatchUpdate: dispatchUpdateCaseProperty }), - [filterOptions.state, dispatchUpdateCaseProperty] - ); - const memoizedGetCasesColumns = useMemo(() => getCasesColumns(actions), [filterOptions.state]); const memoizedPagination = useMemo( () => ({ @@ -134,19 +212,6 @@ export const AllCases = React.memo(() => { [data, queryParams] ); - const getBulkItemsPopoverContent = useCallback( - (closePopover: () => void) => ( - - ), - [selectedCases, filterOptions.state] - ); - const sorting: EuiTableSortingType = { sort: { field: queryParams.sortField, direction: queryParams.sortOrder }, }; @@ -162,7 +227,6 @@ export const AllCases = React.memo(() => { [loading] ); const isDataEmpty = useMemo(() => data.total === 0, [data]); - return ( <> @@ -197,7 +261,9 @@ export const AllCases = React.memo(() => { - {isCasesLoading && !isDataEmpty && } + {(isCasesLoading || isDeleting) && !isDataEmpty && ( + + )} { - {i18n.SELECTED_CASES(selectedCases.length)} + {i18n.SHOWING_SELECTED_CASES(selectedCases.length)} { { )} + {confirmDeleteModal} ); }); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx index 5256fb6d7b3ee1..9356577fd18884 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx @@ -38,7 +38,7 @@ const CasesTableFiltersComponent = ({ const [search, setSearch] = useState(initial.search); const [selectedTags, setSelectedTags] = useState(initial.tags); const [showOpenCases, setShowOpenCases] = useState(initial.state === 'open'); - const [{ data }] = useGetTags(); + const { tags } = useGetTags(); const handleSelectedTags = useCallback( newTags => { @@ -106,7 +106,7 @@ const CasesTableFiltersComponent = ({ buttonLabel={i18n.TAGS} onSelectedOptionsChanged={handleSelectedTags} selectedOptions={selectedTags} - options={data} + options={tags} optionsEmptyLabel={i18n.NO_TAGS_AVAILABLE} /> diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts index 19117136ed0464..27532e57166e1d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/all_cases/translations.ts @@ -18,7 +18,7 @@ export const ADD_NEW_CASE = i18n.translate('xpack.siem.case.caseTable.addNewCase defaultMessage: 'Add New Case', }); -export const SELECTED_CASES = (totalRules: number) => +export const SHOWING_SELECTED_CASES = (totalRules: number) => i18n.translate('xpack.siem.case.caseTable.selectedCasesTitle', { values: { totalRules }, defaultMessage: 'Selected {totalRules} {totalRules, plural, =1 {case} other {cases}}', @@ -66,6 +66,3 @@ export const REOPEN_CASE = i18n.translate('xpack.siem.case.caseTable.reopenCase' export const CLOSE_CASE = i18n.translate('xpack.siem.case.caseTable.closeCase', { defaultMessage: 'Close case', }); -export const DUPLICATE_CASE = i18n.translate('xpack.siem.case.caseTable.duplicateCase', { - defaultMessage: 'Duplicate case', -}); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/index.tsx index 2fe25a7d1f5d0d..f171ebf91b7876 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/bulk_actions/index.tsx @@ -4,29 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiContextMenuItem } from '@elastic/eui'; import React from 'react'; +import { EuiContextMenuItem } from '@elastic/eui'; import * as i18n from './translations'; -import { Case } from '../../../../containers/case/types'; interface GetBulkItems { - // cases: Case[]; closePopover: () => void; - // dispatch: Dispatch; - // dispatchToaster: Dispatch; - // reFetchCases: (refreshPrePackagedCase?: boolean) => void; - selectedCases: Case[]; + deleteCasesAction: (cases: string[]) => void; + selectedCaseIds: string[]; caseStatus: string; } export const getBulkItems = ({ - // cases, + deleteCasesAction, closePopover, caseStatus, - // dispatch, - // dispatchToaster, - // reFetchCases, - selectedCases, + selectedCaseIds, }: GetBulkItems) => { return [ caseStatus === 'open' ? ( @@ -36,8 +29,6 @@ export const getBulkItems = ({ disabled={true} // TO DO onClick={async () => { closePopover(); - // await deleteCasesAction(selectedCases, dispatch, dispatchToaster); - // reFetchCases(true); }} > {i18n.BULK_ACTION_CLOSE_SELECTED} @@ -47,10 +38,8 @@ export const getBulkItems = ({ key={i18n.BULK_ACTION_OPEN_SELECTED} icon="magnet" disabled={true} // TO DO - onClick={async () => { + onClick={() => { closePopover(); - // await deleteCasesAction(selectedCases, dispatch, dispatchToaster); - // reFetchCases(true); }} > {i18n.BULK_ACTION_OPEN_SELECTED} @@ -59,11 +48,9 @@ export const getBulkItems = ({ { closePopover(); - // await deleteCasesAction(selectedCases, dispatch, dispatchToaster); - // reFetchCases(true); + deleteCasesAction(selectedCaseIds); }} > {i18n.BULK_ACTION_DELETE_SELECTED} diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx index c2d3cae6774b0f..3875c316e80d10 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/__mock__/index.tsx @@ -11,6 +11,7 @@ export const caseProps: CaseProps = { caseId: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15', initialData: { id: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15', + commentIds: ['a357c6a0-5435-11ea-b427-fb51a1fcb7b8'], comments: [ { comment: 'Solve this fast!', @@ -37,6 +38,7 @@ export const caseProps: CaseProps = { export const data: Case = { id: '3c4ddcc0-4e99-11ea-9290-35d05cb55c15', + commentIds: ['a357c6a0-5435-11ea-b427-fb51a1fcb7b8'], comments: [ { comment: 'Solve this fast!', diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx index c917d27aebea35..080cbdc1435935 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/case_view/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { EuiBadge, EuiButtonToggle, @@ -17,6 +17,7 @@ import { } from '@elastic/eui'; import styled, { css } from 'styled-components'; +import { Redirect } from 'react-router-dom'; import * as i18n from './translations'; import { Case } from '../../../../containers/case/types'; import { FormattedRelativePreferenceDate } from '../../../../components/formatted_date'; @@ -32,6 +33,9 @@ import { useUpdateCase } from '../../../../containers/case/use_update_case'; import { WrapperPage } from '../../../../components/wrapper_page'; import { getTypedPayload } from '../../../../containers/case/utils'; import { WhitePageWrapper } from '../wrappers'; +import { useDeleteCases } from '../../../../containers/case/use_delete_cases'; +import { SiemPageName } from '../../../home/types'; +import { ConfirmDeleteCaseModal } from '../confirm_delete_case'; interface Props { caseId: string; @@ -62,6 +66,7 @@ export interface CaseProps { export const CaseComponent = React.memo(({ caseId, initialData }) => { const { caseData, isLoading, updateKey, updateCaseProperty } = useUpdateCase(caseId, initialData); + // Update Fields const onUpdateField = useCallback( (newUpdateKey: keyof Case, updateValue: Case[keyof Case]) => { switch (newUpdateKey) { @@ -104,13 +109,39 @@ export const CaseComponent = React.memo(({ caseId, initialData }) => }, [updateCaseProperty, caseData.state] ); + const toggleStateCase = useCallback( + e => onUpdateField('state', e.target.checked ? 'open' : 'closed'), + [onUpdateField] + ); + const onSubmitTitle = useCallback(newTitle => onUpdateField('title', newTitle), [onUpdateField]); + const onSubmitTags = useCallback(newTags => onUpdateField('tags', newTags), [onUpdateField]); + + // Delete case + const { + handleToggleModal, + handleOnDeleteConfirm, + isDeleted, + isDisplayConfirmDeleteModal, + } = useDeleteCases(); + const confirmDeleteModal = useMemo( + () => ( + + ), + [isDisplayConfirmDeleteModal] + ); // TO DO refactor each of these const's into their own components const propertyActions = [ { iconType: 'trash', label: 'Delete case', - onClick: () => null, + onClick: handleToggleModal, }, { iconType: 'popout', @@ -124,12 +155,9 @@ export const CaseComponent = React.memo(({ caseId, initialData }) => }, ]; - const onSubmit = useCallback(newTitle => onUpdateField('title', newTitle), [onUpdateField]); - const toggleStateCase = useCallback( - e => onUpdateField('state', e.target.checked ? 'open' : 'closed'), - [onUpdateField] - ); - const onSubmitTags = useCallback(newTags => onUpdateField('tags', newTags), [onUpdateField]); + if (isDeleted) { + return ; + } return ( <> @@ -144,7 +172,7 @@ export const CaseComponent = React.memo(({ caseId, initialData }) => } title={caseData.title} @@ -222,12 +250,13 @@ export const CaseComponent = React.memo(({ caseId, initialData }) => + {confirmDeleteModal} ); }); export const CaseView = React.memo(({ caseId }: Props) => { - const [{ data, isLoading, isError }] = useGetCase(caseId); + const { data, isLoading, isError } = useGetCase(caseId); if (isError) { return null; } diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/confirm_delete_case/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/confirm_delete_case/index.tsx new file mode 100644 index 00000000000000..dff36a6dac5716 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/confirm_delete_case/index.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import * as i18n from './translations'; + +interface ConfirmDeleteCaseModalProps { + caseTitle: string; + isModalVisible: boolean; + isPlural: boolean; + onCancel: () => void; + onConfirm: () => void; +} + +const ConfirmDeleteCaseModalComp: React.FC = ({ + caseTitle, + isModalVisible, + isPlural, + onCancel, + onConfirm, +}) => { + if (!isModalVisible) { + return null; + } + return ( + + + {isPlural ? i18n.CONFIRM_QUESTION_PLURAL : i18n.CONFIRM_QUESTION} + + + ); +}; + +export const ConfirmDeleteCaseModal = React.memo(ConfirmDeleteCaseModalComp); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/confirm_delete_case/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/components/confirm_delete_case/translations.ts new file mode 100644 index 00000000000000..06e940c60d0a13 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/confirm_delete_case/translations.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +export * from '../../translations'; + +export const DELETE_TITLE = (caseTitle: string) => + i18n.translate('xpack.siem.case.confirmDeleteCase.deleteTitle', { + values: { caseTitle }, + defaultMessage: 'Delete "{caseTitle}"', + }); + +export const CONFIRM_QUESTION = i18n.translate( + 'xpack.siem.case.confirmDeleteCase.confirmQuestion', + { + defaultMessage: + 'By deleting this case, all related case data will be permanently removed and you will no longer be able to push data to a third-party case management system. Are you sure you wish to proceed?', + } +); +export const DELETE_SELECTED_CASES = i18n.translate( + 'xpack.siem.case.confirmDeleteCase.selectedCases', + { + defaultMessage: 'Delete selected cases', + } +); + +export const CONFIRM_QUESTION_PLURAL = i18n.translate( + 'xpack.siem.case.confirmDeleteCase.confirmQuestionPlural', + { + defaultMessage: + 'By deleting these cases, all related case data will be permanently removed and you will no longer be able to push data to a third-party case management system. Are you sure you wish to proceed?', + } +); diff --git a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx index d9c4175b2d2bd4..cebc66a0c83631 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/case/components/user_action_tree/index.tsx @@ -24,7 +24,9 @@ const NewId = 'newComent'; export const UserActionTree = React.memo( ({ data: caseData, onUpdateField, isLoadingDescription }: UserActionTreeProps) => { - const { comments, isLoadingIds, updateComment } = useUpdateComment(caseData.comments); + const { comments, isLoadingIds, updateComment, addPostedComment } = useUpdateComment( + caseData.comments + ); const [manageMarkdownEditIds, setManangeMardownEditIds] = useState([]); @@ -63,7 +65,10 @@ export const UserActionTree = React.memo( [caseData.description, handleManageMarkdownEditId, manageMarkdownEditIds, onUpdateField] ); - const MarkdownNewComment = useMemo(() => , [caseData.id]); + const MarkdownNewComment = useMemo( + () => , + [caseData.id] + ); return ( <> diff --git a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts index d9e4c2725cb101..9c0287a56ccbcd 100644 --- a/x-pack/legacy/plugins/siem/public/pages/case/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/case/translations.ts @@ -14,6 +14,14 @@ export const CANCEL = i18n.translate('xpack.siem.case.caseView.cancel', { defaultMessage: 'Cancel', }); +export const DELETE_CASE = i18n.translate('xpack.siem.case.confirmDeleteCase.deleteCase', { + defaultMessage: 'Delete case', +}); + +export const DELETE_CASES = i18n.translate('xpack.siem.case.confirmDeleteCase.deleteCases', { + defaultMessage: 'Delete cases', +}); + export const NAME = i18n.translate('xpack.siem.case.caseView.name', { defaultMessage: 'Name', }); @@ -64,26 +72,10 @@ export const OPTIONAL = i18n.translate('xpack.siem.case.caseView.optional', { defaultMessage: 'Optional', }); -export const LAST_UPDATED = i18n.translate('xpack.siem.case.caseView.updatedAt', { - defaultMessage: 'Last updated', -}); - -export const PAGE_SUBTITLE = i18n.translate('xpack.siem.case.caseView.pageSubtitle', { - defaultMessage: 'Cases within the Elastic SIEM', -}); - export const PAGE_TITLE = i18n.translate('xpack.siem.case.pageTitle', { defaultMessage: 'Cases', }); -export const STATE = i18n.translate('xpack.siem.case.caseView.state', { - defaultMessage: 'State', -}); - -export const SUBMIT = i18n.translate('xpack.siem.case.caseView.submit', { - defaultMessage: 'Submit', -}); - export const CREATE_CASE = i18n.translate('xpack.siem.case.caseView.createCase', { defaultMessage: 'Create case', }); diff --git a/x-pack/legacy/plugins/siem/public/store/inputs/actions.ts b/x-pack/legacy/plugins/siem/public/store/inputs/actions.ts index f9da0e558c6555..5b26957843f082 100644 --- a/x-pack/legacy/plugins/siem/public/store/inputs/actions.ts +++ b/x-pack/legacy/plugins/siem/public/store/inputs/actions.ts @@ -6,10 +6,9 @@ import actionCreatorFactory from 'typescript-fsa'; -import { SavedQuery } from 'src/legacy/core_plugins/data/public'; import { InspectQuery, Refetch, RefetchKql } from './model'; import { InputsModelId } from './constants'; -import { Filter } from '../../../../../../../src/plugins/data/public'; +import { Filter, SavedQuery } from '../../../../../../../src/plugins/data/public'; const actionCreator = actionCreatorFactory('x-pack/siem/local/inputs'); diff --git a/x-pack/legacy/plugins/siem/public/store/inputs/model.ts b/x-pack/legacy/plugins/siem/public/store/inputs/model.ts index dab6ef3113df00..04facf3b98c3bb 100644 --- a/x-pack/legacy/plugins/siem/public/store/inputs/model.ts +++ b/x-pack/legacy/plugins/siem/public/store/inputs/model.ts @@ -5,11 +5,10 @@ */ import { Dispatch } from 'redux'; -import { SavedQuery } from 'src/legacy/core_plugins/data/public'; import { Omit } from '../../../common/utility_types'; import { InputsModelId } from './constants'; import { CONSTANTS } from '../../components/url_state/constants'; -import { Query, Filter } from '../../../../../../../src/plugins/data/public'; +import { Query, Filter, SavedQuery } from '../../../../../../../src/plugins/data/public'; export interface AbsoluteTimeRange { kind: 'absolute'; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts index 399f09bd3e776c..d318171f3bb48b 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts @@ -37,9 +37,14 @@ const FIRED_ACTIONS = { }; const getCurrentValueFromAggregations = (aggregations: Aggregation) => { - const { buckets } = aggregations.aggregatedIntervals; - const { value } = buckets[buckets.length - 1].aggregatedValue; - return value; + try { + const { buckets } = aggregations.aggregatedIntervals; + if (!buckets.length) return null; // No Data state + const { value } = buckets[buckets.length - 1].aggregatedValue; + return value; + } catch (e) { + return undefined; // Error state + } }; const getParsedFilterQuery: ( @@ -138,34 +143,37 @@ const getMetric: ( aggs, }; - if (groupBy) { - const bucketSelector = ( - response: InfraDatabaseSearchResponse<{}, CompositeAggregationsResponse> - ) => response.aggregations?.groupings?.buckets || []; - const afterKeyHandler = createAfterKeyHandler( - 'aggs.groupings.composite.after', - response => response.aggregations?.groupings?.after_key - ); - const compositeBuckets = (await getAllCompositeData( - body => callCluster('search', { body, index: indexPattern }), - searchBody, - bucketSelector, - afterKeyHandler - )) as Array; - return compositeBuckets.reduce( - (result, bucket) => ({ - ...result, - [bucket.key.groupBy]: getCurrentValueFromAggregations(bucket), - }), - {} - ); + try { + if (groupBy) { + const bucketSelector = ( + response: InfraDatabaseSearchResponse<{}, CompositeAggregationsResponse> + ) => response.aggregations?.groupings?.buckets || []; + const afterKeyHandler = createAfterKeyHandler( + 'aggs.groupings.composite.after', + response => response.aggregations?.groupings?.after_key + ); + const compositeBuckets = (await getAllCompositeData( + body => callCluster('search', { body, index: indexPattern }), + searchBody, + bucketSelector, + afterKeyHandler + )) as Array; + return compositeBuckets.reduce( + (result, bucket) => ({ + ...result, + [bucket.key.groupBy]: getCurrentValueFromAggregations(bucket), + }), + {} + ); + } + const result = await callCluster('search', { + body: searchBody, + index: indexPattern, + }); + return { '*': getCurrentValueFromAggregations(result.aggregations) }; + } catch (e) { + return { '*': undefined }; // Trigger an Error state } - - const result = await callCluster('search', { - body: searchBody, - index: indexPattern, - }); - return { '*': getCurrentValueFromAggregations(result.aggregations) }; }; const comparatorMap = { @@ -220,14 +228,15 @@ export async function registerMetricThresholdAlertType(alertingPlugin: PluginSet criteria.map(criterion => (async () => { const currentValues = await getMetric(services, criterion, groupBy, filterQuery); - if (typeof currentValues === 'undefined') - throw new Error('Could not get current value of metric'); const { threshold, comparator } = criterion; const comparisonFunction = comparatorMap[comparator]; return mapValues(currentValues, value => ({ - shouldFire: comparisonFunction(value, threshold), + shouldFire: + value !== undefined && value !== null && comparisonFunction(value, threshold), currentValue: value, + isNoData: value === null, + isError: value === undefined, })); })() ) @@ -237,8 +246,12 @@ export async function registerMetricThresholdAlertType(alertingPlugin: PluginSet for (const group of groups) { const alertInstance = services.alertInstanceFactory(`${alertUUID}-${group}`); + // AND logic; all criteria must be across the threshold const shouldAlertFire = alertResults.every(result => result[group].shouldFire); - + // AND logic; because we need to evaluate all criteria, if one of them reports no data then the + // whole alert is in a No Data/Error state + const isNoData = alertResults.some(result => result[group].isNoData); + const isError = alertResults.some(result => result[group].isError); if (shouldAlertFire) { alertInstance.scheduleActions(FIRED_ACTIONS.id, { group, @@ -248,7 +261,13 @@ export async function registerMetricThresholdAlertType(alertingPlugin: PluginSet // Future use: ability to fetch display current alert state alertInstance.replaceState({ - alertState: shouldAlertFire ? AlertStates.ALERT : AlertStates.OK, + alertState: isError + ? AlertStates.ERROR + : isNoData + ? AlertStates.NO_DATA + : shouldAlertFire + ? AlertStates.ALERT + : AlertStates.OK, }); } }, diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts index 1c3d0cea3dc841..e247eb8a3f8891 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/types.ts @@ -19,6 +19,8 @@ export enum Comparator { export enum AlertStates { OK, ALERT, + NO_DATA, + ERROR, } export type TimeUnit = 's' | 'm' | 'h' | 'd'; diff --git a/x-pack/plugins/lens/server/routes/existing_fields.ts b/x-pack/plugins/lens/server/routes/existing_fields.ts index 40b2766a647cf7..57c16804135370 100644 --- a/x-pack/plugins/lens/server/routes/existing_fields.ts +++ b/x-pack/plugins/lens/server/routes/existing_fields.ts @@ -46,14 +46,16 @@ const metaFields = ['_source', '_id', '_type', '_index', '_score']; export async function existingFieldsRoute(setup: CoreSetup) { const router = setup.http.createRouter(); - router.get( + + router.post( { path: `${BASE_API_URL}/existing_fields/{indexPatternId}`, validate: { params: schema.object({ indexPatternId: schema.string(), }), - query: schema.object({ + body: schema.object({ + dslQuery: schema.object({}, { allowUnknowns: true }), fromDate: schema.maybe(schema.string()), toDate: schema.maybe(schema.string()), timeFieldName: schema.maybe(schema.string()), @@ -64,8 +66,8 @@ export async function existingFieldsRoute(setup: CoreSetup) { try { return res.ok({ body: await fetchFieldExistence({ - ...req.query, ...req.params, + ...req.body, context, }), }); @@ -91,12 +93,14 @@ export async function existingFieldsRoute(setup: CoreSetup) { async function fetchFieldExistence({ context, indexPatternId, + dslQuery = { match_all: {} }, fromDate, toDate, timeFieldName, }: { indexPatternId: string; context: RequestHandlerContext; + dslQuery: object; fromDate?: string; toDate?: string; timeFieldName?: string; @@ -109,10 +113,10 @@ async function fetchFieldExistence({ } = await fetchIndexPatternDefinition(indexPatternId, context); const fields = buildFieldList(indexPattern, mappings, fieldDescriptors); - const docs = await fetchIndexPatternStats({ fromDate, toDate, + dslQuery, client: context.core.elasticsearch.dataClient, index: indexPatternTitle, timeFieldName: timeFieldName || indexPattern.attributes.timeFieldName, @@ -197,6 +201,7 @@ export function buildFieldList( async function fetchIndexPatternStats({ client, index, + dslQuery, timeFieldName, fromDate, toDate, @@ -204,17 +209,15 @@ async function fetchIndexPatternStats({ }: { client: IScopedClusterClient; index: string; + dslQuery: object; timeFieldName?: string; fromDate?: string; toDate?: string; fields: Field[]; }) { - let query; - - if (timeFieldName && fromDate && toDate) { - query = { - bool: { - filter: [ + const filter = + timeFieldName && fromDate && toDate + ? [ { range: { [timeFieldName]: { @@ -223,16 +226,17 @@ async function fetchIndexPatternStats({ }, }, }, - ], - }, - }; - } else { - query = { - match_all: {}, - }; - } - const scriptedFields = fields.filter(f => f.isScript); + dslQuery, + ] + : [dslQuery]; + + const query = { + bool: { + filter, + }, + }; + const scriptedFields = fields.filter(f => f.isScript); const result = await client.callAsCurrentUser('search', { index, body: { @@ -251,7 +255,6 @@ async function fetchIndexPatternStats({ }, {} as Record), }, }); - return result.hits.hits; } diff --git a/x-pack/test/api_integration/apis/lens/existing_fields.ts b/x-pack/test/api_integration/apis/lens/existing_fields.ts index cf65e114d38965..b3810cf468b55d 100644 --- a/x-pack/test/api_integration/apis/lens/existing_fields.ts +++ b/x-pack/test/api_integration/apis/lens/existing_fields.ts @@ -8,8 +8,8 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; -const TEST_START_TIME = encodeURIComponent('2015-09-19T06:31:44.000'); -const TEST_END_TIME = encodeURIComponent('2015-09-23T18:31:44.000'); +const TEST_START_TIME = '2015-09-19T06:31:44.000'; +const TEST_END_TIME = '2015-09-23T18:31:44.000'; const COMMON_HEADERS = { 'kbn-xsrf': 'some-xsrf-token', }; @@ -147,12 +147,17 @@ export default ({ getService }: FtrProviderContext) => { describe('existence', () => { it('should find which fields exist in the sample documents', async () => { const { body } = await supertest - .get( - `/api/lens/existing_fields/${encodeURIComponent( - 'logstash-*' - )}?fromDate=${TEST_START_TIME}&toDate=${TEST_END_TIME}` - ) + .post(`/api/lens/existing_fields/${encodeURIComponent('logstash-*')}`) .set(COMMON_HEADERS) + .send({ + dslQuery: { + bool: { + filter: [{ match_all: {} }], + }, + }, + fromDate: TEST_START_TIME, + toDate: TEST_END_TIME, + }) .expect(200); expect(body.indexPatternTitle).to.eql('logstash-*'); @@ -161,25 +166,67 @@ export default ({ getService }: FtrProviderContext) => { it('should succeed for thousands of fields', async () => { const { body } = await supertest - .get( - `/api/lens/existing_fields/${encodeURIComponent( - 'metricbeat-*' - )}?fromDate=${TEST_START_TIME}&toDate=${TEST_END_TIME}` - ) + .post(`/api/lens/existing_fields/${encodeURIComponent('metricbeat-*')}`) .set(COMMON_HEADERS) + .send({ + dslQuery: { match_all: {} }, + fromDate: TEST_START_TIME, + toDate: TEST_END_TIME, + }) .expect(200); expect(body.indexPatternTitle).to.eql('metricbeat-*'); expect(body.existingFieldNames.sort()).to.eql(metricBeatData.sort()); }); - it('should throw a 404 for a non-existent index', async () => { - await supertest - .get( - `/api/lens/existing_fields/nadachance?fromDate=${TEST_START_TIME}&toDate=${TEST_END_TIME}` - ) + it('should return fields filtered by query and filters', async () => { + const expectedFieldNames = [ + '@message', + '@message.raw', + '@tags', + '@tags.raw', + '@timestamp', + 'agent', + 'agent.raw', + 'bytes', + 'clientip', + 'extension', + 'extension.raw', + 'headings', + 'headings.raw', + 'host', + 'host.raw', + 'index', + 'index.raw', + 'referer', + 'request', + 'request.raw', + 'response', + 'response.raw', + 'spaces', + 'spaces.raw', + 'type', + 'url', + 'url.raw', + 'utc_time', + 'xss', + 'xss.raw', + ]; + + const { body } = await supertest + .post(`/api/lens/existing_fields/${encodeURIComponent('logstash-*')}`) .set(COMMON_HEADERS) - .expect(404); + .send({ + dslQuery: { + bool: { + filter: [{ match: { referer: 'https://www.taylorswift.com/' } }], + }, + }, + fromDate: TEST_START_TIME, + toDate: TEST_END_TIME, + }) + .expect(200); + expect(body.existingFieldNames.sort()).to.eql(expectedFieldNames.sort()); }); }); }); diff --git a/x-pack/test/api_integration/apis/lens/field_stats.ts b/x-pack/test/api_integration/apis/lens/field_stats.ts index 51d81668d275d9..2d394e35725c29 100644 --- a/x-pack/test/api_integration/apis/lens/field_stats.ts +++ b/x-pack/test/api_integration/apis/lens/field_stats.ts @@ -62,7 +62,7 @@ export default ({ getService }: FtrProviderContext) => { }) .expect(200); - expect(body).to.have.property('totalDocuments', 4633); + expect(body).to.have.property('totalDocuments', 4634); }); it('should return an auto histogram for numbers and top values', async () => { @@ -82,9 +82,9 @@ export default ({ getService }: FtrProviderContext) => { .expect(200); expect(body).to.eql({ - totalDocuments: 4633, - sampledDocuments: 4633, - sampledValues: 4633, + totalDocuments: 4634, + sampledDocuments: 4634, + sampledValues: 4634, histogram: { buckets: [ { @@ -96,7 +96,7 @@ export default ({ getService }: FtrProviderContext) => { key: 1999, }, { - count: 885, + count: 886, key: 3998, }, { @@ -139,6 +139,10 @@ export default ({ getService }: FtrProviderContext) => { count: 5, key: 3954, }, + { + count: 5, + key: 5846, + }, { count: 5, key: 6497, @@ -159,10 +163,6 @@ export default ({ getService }: FtrProviderContext) => { count: 4, key: 4669, }, - { - count: 4, - key: 5846, - }, { count: 4, key: 5863, @@ -193,11 +193,11 @@ export default ({ getService }: FtrProviderContext) => { .expect(200); expect(body).to.eql({ - totalDocuments: 4633, + totalDocuments: 4634, histogram: { buckets: [ { - count: 1161, + count: 1162, key: 1442875680000, }, { @@ -230,8 +230,8 @@ export default ({ getService }: FtrProviderContext) => { .expect(200); expect(body).to.eql({ - totalDocuments: 4633, - sampledDocuments: 4633, + totalDocuments: 4634, + sampledDocuments: 4634, sampledValues: 4633, topValues: { buckets: [ diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/date_nanos_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/date_nanos_job.ts index 880a268bba794d..2a9824f46778da 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/date_nanos_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/date_nanos_job.ts @@ -165,8 +165,7 @@ export default function({ getService }: FtrProviderContext) { }, ]; - // FLAKY: https://github.com/elastic/kibana/issues/59419 - describe.skip('job on data set with date_nanos time field', function() { + describe('job on data set with date_nanos time field', function() { this.tags(['smoke', 'mlqa']); before(async () => { await esArchiver.load('ml/event_rate_nanos'); diff --git a/x-pack/test/functional/es_archives/logstash_functional/data.json.gz b/x-pack/test/functional/es_archives/logstash_functional/data.json.gz index 2cef0717385267..02195f888b93ce 100644 Binary files a/x-pack/test/functional/es_archives/logstash_functional/data.json.gz and b/x-pack/test/functional/es_archives/logstash_functional/data.json.gz differ diff --git a/x-pack/test/functional/services/machine_learning/job_wizard_common.ts b/x-pack/test/functional/services/machine_learning/job_wizard_common.ts index 38e6694669c1a7..70d576a3d847b7 100644 --- a/x-pack/test/functional/services/machine_learning/job_wizard_common.ts +++ b/x-pack/test/functional/services/machine_learning/job_wizard_common.ts @@ -280,11 +280,13 @@ export function MachineLearningJobWizardCommonProvider( withAdvancedSection: sectionOptions.withAdvancedSection, })) === false ) { - await testSubjects.clickWhenNotDisabled(subj); + await retry.tryForTime(5 * 1000, async () => { + await testSubjects.clickWhenNotDisabled(subj); + await this.assertDedicatedIndexSwitchCheckedState(true, { + withAdvancedSection: sectionOptions.withAdvancedSection, + }); + }); } - await this.assertDedicatedIndexSwitchCheckedState(true, { - withAdvancedSection: sectionOptions.withAdvancedSection, - }); }, async assertModelMemoryLimitInputExists(