diff --git a/.ci/Jenkinsfile_baseline_capture b/.ci/Jenkinsfile_baseline_capture index 33ecfcd84fd3e..791cacf7abb4c 100644 --- a/.ci/Jenkinsfile_baseline_capture +++ b/.ci/Jenkinsfile_baseline_capture @@ -12,12 +12,12 @@ kibanaPipeline(timeoutMinutes: 120) { ]) { parallel([ 'oss-baseline': { - workers.ci(name: 'oss-baseline', size: 's-highmem', ramDisk: true, runErrorReporter: false) { + workers.ci(name: 'oss-baseline', size: 'l', ramDisk: true, runErrorReporter: false) { kibanaPipeline.functionalTestProcess('oss-baseline', './test/scripts/jenkins_baseline.sh')() } }, 'xpack-baseline': { - workers.ci(name: 'xpack-baseline', size: 's-highmem', ramDisk: true, runErrorReporter: false) { + workers.ci(name: 'xpack-baseline', size: 'l', ramDisk: true, runErrorReporter: false) { kibanaPipeline.functionalTestProcess('xpack-baseline', './test/scripts/jenkins_xpack_baseline.sh')() } }, diff --git a/docs/apm/service-maps.asciidoc b/docs/apm/service-maps.asciidoc index db2f85c54c762..d629a95073a74 100644 --- a/docs/apm/service-maps.asciidoc +++ b/docs/apm/service-maps.asciidoc @@ -2,11 +2,6 @@ [[service-maps]] === Service maps -beta::[] - -WARNING: Service map support for Internet Explorer 11 is extremely limited. -Please use Chrome or Firefox if available. - A service map is a real-time visual representation of the instrumented services in your application's architecture. It shows you how these services are connected, along with high-level metrics like average transaction duration, requests per minute, and errors per minute. diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.getfieldsforwildcard.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.getfieldsforwildcard.md index 6bd3bbf2433cd..52382372d6d96 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.getfieldsforwildcard.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpatternsfetcher.getfieldsforwildcard.md @@ -12,6 +12,9 @@ Get a list of field objects for an index pattern that may contain wildcards getFieldsForWildcard(options: { pattern: string | string[]; metaFields?: string[]; + fieldCapsOptions?: { + allowNoIndices: boolean; + }; }): Promise; ``` @@ -19,7 +22,7 @@ getFieldsForWildcard(options: { | Parameter | Type | Description | | --- | --- | --- | -| options | {
pattern: string | string[];
metaFields?: string[];
} | | +| options | {
pattern: string | string[];
metaFields?: string[];
fieldCapsOptions?: {
allowNoIndices: boolean;
};
} | | Returns: diff --git a/package.json b/package.json index 1f2749ea44a90..57f5ac16059c9 100644 --- a/package.json +++ b/package.json @@ -350,7 +350,7 @@ "babel-eslint": "^10.0.3", "babel-jest": "^25.5.1", "babel-plugin-istanbul": "^6.0.0", - "backport": "5.5.1", + "backport": "5.6.0", "brace": "0.11.1", "chai": "3.5.0", "chance": "1.0.18", diff --git a/packages/kbn-es-archiver/src/cli.ts b/packages/kbn-es-archiver/src/cli.ts index 41abe83c148cd..87df07fe865bd 100644 --- a/packages/kbn-es-archiver/src/cli.ts +++ b/packages/kbn-es-archiver/src/cli.ts @@ -144,7 +144,7 @@ export function runCli() { const query = flags.query; let parsedQuery; - if (typeof query === 'string') { + if (typeof query === 'string' && query.length > 0) { try { parsedQuery = JSON.parse(query); } catch (err) { diff --git a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx index fc88b31711b23..abef8afcc3985 100644 --- a/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx +++ b/src/plugins/console/public/application/containers/editor/legacy/console_editor/editor.tsx @@ -182,6 +182,9 @@ function EditorUI({ initialTextValue }: EditorProps) { unsubscribeResizer(); clearSubscriptions(); window.removeEventListener('hashchange', onHashChange); + if (editorInstanceRef.current) { + editorInstanceRef.current.getCoreEditor().destroy(); + } }; }, [saveCurrentTextObject, initialTextValue, history, setInputEditor, settingsService]); diff --git a/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts b/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts index 469ef6d79fae5..393b7eee346f5 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts +++ b/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts @@ -408,4 +408,8 @@ export class LegacyCoreEditor implements CoreEditor { }, ]); } + + destroy() { + this.editor.destroy(); + } } diff --git a/src/plugins/console/public/types/core_editor.ts b/src/plugins/console/public/types/core_editor.ts index b71f4fff44ca5..d88d8f86b874c 100644 --- a/src/plugins/console/public/types/core_editor.ts +++ b/src/plugins/console/public/types/core_editor.ts @@ -268,4 +268,9 @@ export interface CoreEditor { * detects a change */ registerAutocompleter(autocompleter: AutoCompleterFunction): void; + + /** + * Release any resources in use by the editor. + */ + destroy(): void; } diff --git a/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts b/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts index ff9d67152e268..57c636a9e3c69 100644 --- a/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts +++ b/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts @@ -55,9 +55,10 @@ export class IndexPatternsFetcher { async getFieldsForWildcard(options: { pattern: string | string[]; metaFields?: string[]; + fieldCapsOptions?: { allowNoIndices: boolean }; }): Promise { - const { pattern, metaFields } = options; - return await getFieldCapabilities(this._callDataCluster, pattern, metaFields); + const { pattern, metaFields, fieldCapsOptions } = options; + return await getFieldCapabilities(this._callDataCluster, pattern, metaFields, fieldCapsOptions); } /** diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/es_api.ts b/src/plugins/data/server/index_patterns/fetcher/lib/es_api.ts index 0738a16034d46..27ce14f9a3597 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/es_api.ts +++ b/src/plugins/data/server/index_patterns/fetcher/lib/es_api.ts @@ -69,15 +69,20 @@ export async function callIndexAliasApi( * * @param {Function} callCluster bound function for accessing an es client * @param {Array|String} indices + * @param {Object} fieldCapsOptions * @return {Promise} */ -export async function callFieldCapsApi(callCluster: LegacyAPICaller, indices: string[] | string) { +export async function callFieldCapsApi( + callCluster: LegacyAPICaller, + indices: string[] | string, + fieldCapsOptions: { allowNoIndices: boolean } = { allowNoIndices: false } +) { try { return (await callCluster('fieldCaps', { index: indices, fields: '*', ignoreUnavailable: true, - allowNoIndices: false, + ...fieldCapsOptions, })) as FieldCapsResponse; } catch (error) { throw convertEsError(indices, error); diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.test.js b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.test.js index a0af7582ac6f3..0e5757b7b782b 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.test.js +++ b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.test.js @@ -61,7 +61,7 @@ describe('index_patterns/field_capabilities/field_capabilities', () => { await getFieldCapabilities(footballs[0], footballs[1]); sinon.assert.calledOnce(callFieldCapsApi); - calledWithExactly(callFieldCapsApi, [footballs[0], footballs[1]]); + calledWithExactly(callFieldCapsApi, [footballs[0], footballs[1], undefined]); }); }); diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.ts b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.ts index 6b26c82dc95e7..62e77e0adad66 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.ts +++ b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.ts @@ -32,14 +32,20 @@ import { FieldDescriptor } from '../../index_patterns_fetcher'; * @param {Function} callCluster bound function for accessing an es client * @param {Array} [indices=[]] the list of indexes to check * @param {Array} [metaFields=[]] the list of internal fields to include + * @param {Object} fieldCapsOptions * @return {Promise>} */ export async function getFieldCapabilities( callCluster: LegacyAPICaller, indices: string | string[] = [], - metaFields: string[] = [] + metaFields: string[] = [], + fieldCapsOptions?: { allowNoIndices: boolean } ) { - const esFieldCaps: FieldCapsResponse = await callFieldCapsApi(callCluster, indices); + const esFieldCaps: FieldCapsResponse = await callFieldCapsApi( + callCluster, + indices, + fieldCapsOptions + ); const fieldsFromFieldCapsByName = keyBy(readFieldCapsResponse(esFieldCaps), 'name'); const allFieldsUnsorted = Object.keys(fieldsFromFieldCapsByName) diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index c48aa8397dc83..2024e9e7f2974 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -687,6 +687,9 @@ export class IndexPatternsFetcher { getFieldsForWildcard(options: { pattern: string | string[]; metaFields?: string[]; + fieldCapsOptions?: { + allowNoIndices: boolean; + }; }): Promise; } diff --git a/src/plugins/embeddable/public/bootstrap.ts b/src/plugins/embeddable/public/bootstrap.ts index 33cf210763b10..5c95214ef591b 100644 --- a/src/plugins/embeddable/public/bootstrap.ts +++ b/src/plugins/embeddable/public/bootstrap.ts @@ -19,7 +19,6 @@ import { UiActionsSetup } from '../../ui_actions/public'; import { contextMenuTrigger, - createFilterAction, panelBadgeTrigger, EmbeddableContext, CONTEXT_MENU_TRIGGER, @@ -29,8 +28,6 @@ import { ACTION_INSPECT_PANEL, REMOVE_PANEL_ACTION, ACTION_EDIT_PANEL, - FilterActionContext, - ACTION_APPLY_FILTER, panelNotificationTrigger, PANEL_NOTIFICATION_TRIGGER, } from './lib'; @@ -48,7 +45,6 @@ declare module '../../ui_actions/public' { [ACTION_INSPECT_PANEL]: EmbeddableContext; [REMOVE_PANEL_ACTION]: EmbeddableContext; [ACTION_EDIT_PANEL]: EmbeddableContext; - [ACTION_APPLY_FILTER]: FilterActionContext; } } @@ -60,8 +56,4 @@ export const bootstrap = (uiActions: UiActionsSetup) => { uiActions.registerTrigger(contextMenuTrigger); uiActions.registerTrigger(panelBadgeTrigger); uiActions.registerTrigger(panelNotificationTrigger); - - const actionApplyFilter = createFilterAction(); - - uiActions.registerAction(actionApplyFilter); }; diff --git a/src/plugins/embeddable/public/index.ts b/src/plugins/embeddable/public/index.ts index c5d8853ada5e8..7609f07d660bc 100644 --- a/src/plugins/embeddable/public/index.ts +++ b/src/plugins/embeddable/public/index.ts @@ -24,7 +24,6 @@ import { EmbeddablePublicPlugin } from './plugin'; export { ACTION_ADD_PANEL, - ACTION_APPLY_FILTER, ACTION_EDIT_PANEL, Adapters, AddPanelAction, diff --git a/src/plugins/embeddable/public/lib/actions/apply_filter_action.test.ts b/src/plugins/embeddable/public/lib/actions/apply_filter_action.test.ts deleted file mode 100644 index 88c1a5917e609..0000000000000 --- a/src/plugins/embeddable/public/lib/actions/apply_filter_action.test.ts +++ /dev/null @@ -1,134 +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 { createFilterAction } from './apply_filter_action'; -import { expectErrorAsync } from '../../tests/helpers'; -import { defaultTrigger } from '../../../../ui_actions/public/triggers'; - -test('has ACTION_APPLY_FILTER type and id', () => { - const action = createFilterAction(); - expect(action.id).toBe('ACTION_APPLY_FILTER'); - expect(action.type).toBe('ACTION_APPLY_FILTER'); -}); - -test('has expected display name', () => { - const action = createFilterAction(); - expect(action.getDisplayName({} as any)).toMatchInlineSnapshot(`"Apply filter to current view"`); -}); - -describe('getIconType()', () => { - test('returns "filter" icon', async () => { - const action = createFilterAction(); - const result = action.getIconType({} as any); - expect(result).toBe('filter'); - }); -}); - -describe('isCompatible()', () => { - test('when embeddable filters and filters exist, returns true', async () => { - const action = createFilterAction(); - const result = await action.isCompatible({ - embeddable: { - getRoot: () => ({ - getInput: () => ({ - filters: [], - }), - }), - } as any, - filters: [], - trigger: defaultTrigger, - }); - expect(result).toBe(true); - }); - - test('when embeddable filters not set, returns false', async () => { - const action = createFilterAction(); - const result = await action.isCompatible({ - embeddable: { - getRoot: () => ({ - getInput: () => ({ - // filters: [], - }), - }), - } as any, - filters: [], - trigger: defaultTrigger, - }); - expect(result).toBe(false); - }); - - test('when triggerContext or filters are not set, returns false', async () => { - const action = createFilterAction(); - - const result1 = await action.isCompatible({ - embeddable: { - getRoot: () => ({ - getInput: () => ({ - filters: [], - }), - }), - } as any, - } as any); - expect(result1).toBe(false); - }); -}); - -const getEmbeddable = () => { - const root = { - getInput: jest.fn(() => ({ - filters: [], - })), - updateInput: jest.fn(), - }; - const embeddable = { - getRoot: () => root, - } as any; - return [embeddable, root]; -}; - -describe('execute()', () => { - describe('when no filters are given', () => { - test('throws an error', async () => { - const action = createFilterAction(); - const error = await expectErrorAsync(() => - action.execute({ - embeddable: getEmbeddable(), - } as any) - ); - expect(error).toBeInstanceOf(Error); - expect(error.message).toBe('Applying a filter requires a filter and embeddable as context'); - }); - - test('updates filter input on success', async () => { - const action = createFilterAction(); - const [embeddable, root] = getEmbeddable(); - - await action.execute({ - embeddable, - filters: ['FILTER' as any], - trigger: defaultTrigger, - }); - - expect(root.updateInput).toHaveBeenCalledTimes(1); - expect(root.updateInput.mock.calls[0][0]).toMatchObject({ - filters: ['FILTER'], - }); - }); - }); -}); diff --git a/src/plugins/embeddable/public/lib/actions/apply_filter_action.ts b/src/plugins/embeddable/public/lib/actions/apply_filter_action.ts deleted file mode 100644 index 3460203aac29c..0000000000000 --- a/src/plugins/embeddable/public/lib/actions/apply_filter_action.ts +++ /dev/null @@ -1,69 +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 { i18n } from '@kbn/i18n'; -import { ActionByType, createAction, IncompatibleActionError } from '../ui_actions'; -import { IEmbeddable, EmbeddableInput } from '../embeddables'; -import { Filter } from '../../../../../plugins/data/public'; - -export const ACTION_APPLY_FILTER = 'ACTION_APPLY_FILTER'; - -type RootEmbeddable = IEmbeddable; -export interface FilterActionContext { - embeddable: IEmbeddable; - filters: Filter[]; -} - -async function isCompatible(context: FilterActionContext) { - if (context.embeddable === undefined) { - return false; - } - const root = context.embeddable.getRoot() as RootEmbeddable; - return Boolean(root.getInput().filters !== undefined && context.filters !== undefined); -} - -export function createFilterAction(): ActionByType { - return createAction({ - type: ACTION_APPLY_FILTER, - id: ACTION_APPLY_FILTER, - order: 100, - getIconType: () => 'filter', - getDisplayName: () => { - return i18n.translate('embeddableApi.actions.applyFilterActionTitle', { - defaultMessage: 'Apply filter to current view', - }); - }, - isCompatible, - execute: async ({ embeddable, filters }) => { - if (!filters || !embeddable) { - throw new Error('Applying a filter requires a filter and embeddable as context'); - } - - if (!(await isCompatible({ embeddable, filters }))) { - throw new IncompatibleActionError(); - } - - const root = embeddable.getRoot() as RootEmbeddable; - - root.updateInput({ - filters, - }); - }, - }); -} diff --git a/src/plugins/embeddable/public/lib/actions/index.ts b/src/plugins/embeddable/public/lib/actions/index.ts index ea32c6aa2d455..8be2c3f5df450 100644 --- a/src/plugins/embeddable/public/lib/actions/index.ts +++ b/src/plugins/embeddable/public/lib/actions/index.ts @@ -17,5 +17,4 @@ * under the License. */ -export * from './apply_filter_action'; export * from './edit_panel_action'; diff --git a/src/plugins/embeddable/public/tests/apply_filter_action.test.ts b/src/plugins/embeddable/public/tests/apply_filter_action.test.ts deleted file mode 100644 index f8c4a4a7e4b72..0000000000000 --- a/src/plugins/embeddable/public/tests/apply_filter_action.test.ts +++ /dev/null @@ -1,153 +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 { testPlugin } from './test_plugin'; -import { EmbeddableOutput, isErrorEmbeddable, createFilterAction } from '../lib'; -import { - FilterableContainer, - FilterableContainerInput, - FILTERABLE_CONTAINER, - FilterableEmbeddableFactory, - HelloWorldContainer, - FILTERABLE_EMBEDDABLE, - FilterableEmbeddable, - FilterableContainerFactory, - FilterableEmbeddableInput, -} from '../lib/test_samples'; -import { esFilters } from '../../../data/public'; -import { applyFilterTrigger } from '../../../ui_actions/public'; - -test('ApplyFilterAction applies the filter to the root of the container tree', async () => { - const { doStart, setup } = testPlugin(); - - const factory2 = new FilterableEmbeddableFactory(); - const factory1 = new FilterableContainerFactory(async () => await api.getEmbeddableFactory); - setup.registerEmbeddableFactory(factory2.type, factory2); - setup.registerEmbeddableFactory(factory1.type, factory1); - - const api = doStart(); - - const applyFilterAction = createFilterAction(); - - const root = new FilterableContainer( - { id: 'root', panels: {}, filters: [] }, - api.getEmbeddableFactory - ); - - const node1 = await root.addNewEmbeddable< - FilterableContainerInput, - EmbeddableOutput, - FilterableContainer - >(FILTERABLE_CONTAINER, { panels: {}, id: 'node1' }); - - const node2 = await root.addNewEmbeddable< - FilterableContainerInput, - EmbeddableOutput, - FilterableContainer - >(FILTERABLE_CONTAINER, { panels: {}, id: 'Node2' }); - - if (isErrorEmbeddable(node1) || isErrorEmbeddable(node2)) { - throw new Error(); - } - - const embeddable = await node2.addNewEmbeddable< - FilterableEmbeddableInput, - EmbeddableOutput, - FilterableEmbeddable - >(FILTERABLE_EMBEDDABLE, { id: 'leaf' }); - - if (isErrorEmbeddable(embeddable)) { - throw new Error(); - } - - const filter: any = { - $state: { store: esFilters.FilterStateStore.APP_STATE }, - meta: { - disabled: false, - negate: false, - alias: '', - }, - query: { match: { extension: { query: 'foo' } } }, - }; - - await applyFilterAction.execute({ embeddable, filters: [filter], trigger: applyFilterTrigger }); - expect(root.getInput().filters.length).toBe(1); - expect(node1.getInput().filters.length).toBe(1); - expect(embeddable.getInput().filters.length).toBe(1); - expect(node2.getInput().filters.length).toBe(1); -}); - -test('ApplyFilterAction is incompatible if the root container does not accept a filter as input', async () => { - const { doStart, setup } = testPlugin(); - - const factory = new FilterableEmbeddableFactory(); - setup.registerEmbeddableFactory(factory.type, factory); - const api = doStart(); - const applyFilterAction = createFilterAction(); - - const parent = new HelloWorldContainer({ id: 'root', panels: {} }, { - getEmbeddableFactory: api.getEmbeddableFactory, - } as any); - const embeddable = await parent.addNewEmbeddable< - FilterableContainerInput, - EmbeddableOutput, - FilterableContainer - >(FILTERABLE_EMBEDDABLE, { id: 'leaf' }); - - if (isErrorEmbeddable(embeddable)) { - throw new Error(); - } - - // @ts-ignore - expect(await applyFilterAction.isCompatible({ embeddable })).toBe(false); -}); - -test('trying to execute on incompatible context throws an error ', async () => { - const { doStart, setup } = testPlugin(); - - const factory = new FilterableEmbeddableFactory(); - setup.registerEmbeddableFactory(factory.type, factory); - - const api = doStart(); - const applyFilterAction = createFilterAction(); - - const parent = new HelloWorldContainer({ id: 'root', panels: {} }, { - getEmbeddableFactory: api.getEmbeddableFactory, - } as any); - - const embeddable = await parent.addNewEmbeddable< - FilterableContainerInput, - EmbeddableOutput, - FilterableContainer - >(FILTERABLE_EMBEDDABLE, { id: 'leaf' }); - - if (isErrorEmbeddable(embeddable)) { - throw new Error(); - } - - async function check() { - await applyFilterAction.execute({ embeddable } as any); - } - await expect(check()).rejects.toThrow(Error); -}); - -test('gets title', async () => { - const applyFilterAction = createFilterAction(); - expect(applyFilterAction.getDisplayName({} as any)).toBeDefined(); -}); diff --git a/src/plugins/vis_type_timeseries/server/lib/get_fields.ts b/src/plugins/vis_type_timeseries/server/lib/get_fields.ts index 777de89672bbe..26a1792e3ec70 100644 --- a/src/plugins/vis_type_timeseries/server/lib/get_fields.ts +++ b/src/plugins/vis_type_timeseries/server/lib/get_fields.ts @@ -16,15 +16,16 @@ * specific language governing permissions and limitations * under the License. */ -import { uniqBy } from 'lodash'; +import { uniqBy, get } from 'lodash'; import { first, map } from 'rxjs/operators'; import { KibanaRequest, RequestHandlerContext } from 'kibana/server'; -// @ts-ignore -import { getIndexPatternObject } from './vis_data/helpers/get_index_pattern'; -import { indexPatterns } from '../../../data/server'; import { Framework } from '../plugin'; -import { IndexPatternFieldDescriptor, IndexPatternsFetcher } from '../../../data/server'; +import { + indexPatterns, + IndexPatternFieldDescriptor, + IndexPatternsFetcher, +} from '../../../data/server'; import { ReqFacade } from './search_strategies/strategies/abstract_search_strategy'; export async function getFields( @@ -58,7 +59,15 @@ export async function getFields( .toPromise(); }, }; - const { indexPatternString } = await getIndexPatternObject(reqFacade, indexPattern); + let indexPatternString = indexPattern; + + if (!indexPatternString) { + const [, { data }] = await framework.core.getStartServices(); + const indexPatternsService = await data.indexPatterns.indexPatternsServiceFactory(request); + const defaultIndexPattern = await indexPatternsService.getDefault(); + indexPatternString = get(defaultIndexPattern, 'title', ''); + } + const { searchStrategy, capabilities, diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js index 6773ee482b098..4dcc67dc46976 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.js @@ -49,6 +49,7 @@ describe('AbstractSearchStrategy', () => { expect(fields).toBe(mockedFields); expect(req.pre.indexPatternsService.getFieldsForWildcard).toHaveBeenCalledWith({ pattern: indexPattern, + fieldCapsOptions: { allowNoIndices: true }, }); }); diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts index 92b7e6976962e..2eb92b2b777e8 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.ts @@ -84,6 +84,7 @@ export class AbstractSearchStrategy { return await indexPatternsService!.getFieldsForWildcard({ pattern: indexPattern, + fieldCapsOptions: { allowNoIndices: true }, }); } diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.js index e4bda194299df..82a2ef66cb1c0 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/get_interval_and_timefield.js @@ -17,12 +17,10 @@ * under the License. */ -import { get } from 'lodash'; - const DEFAULT_TIME_FIELD = '@timestamp'; export function getIntervalAndTimefield(panel, series = {}, indexPatternObject) { - const getDefaultTimeField = () => get(indexPatternObject, 'timeFieldName', DEFAULT_TIME_FIELD); + const getDefaultTimeField = () => indexPatternObject?.timeFieldName ?? DEFAULT_TIME_FIELD; const timeField = (series.override_index_pattern && series.series_time_field) || diff --git a/test/accessibility/apps/dashboard_panel.ts b/test/accessibility/apps/dashboard_panel.ts index 1a817ce6b7a1c..03fa76387da1f 100644 --- a/test/accessibility/apps/dashboard_panel.ts +++ b/test/accessibility/apps/dashboard_panel.ts @@ -18,6 +18,7 @@ */ import { FtrProviderContext } from '../ftr_provider_context'; + export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'dashboard', 'header', 'home', 'settings']); const a11y = getService('a11y'); @@ -30,7 +31,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { useActualUrl: true, }); - await PageObjects.home.addSampleDataSet('flights'); await PageObjects.common.navigateToApp('dashboard'); await testSubjects.click('dashboardListingTitleLink-[Flights]-Global-Flight-Dashboard'); diff --git a/x-pack/plugins/actions/common/types.ts b/x-pack/plugins/actions/common/types.ts index 49e8f3e80b14a..41ec4d2a88e9f 100644 --- a/x-pack/plugins/actions/common/types.ts +++ b/x-pack/plugins/actions/common/types.ts @@ -24,3 +24,13 @@ export interface ActionResult { config: Record; isPreconfigured: boolean; } + +// the result returned from an action type executor function +export interface ActionTypeExecutorResult { + actionId: string; + status: 'ok' | 'error'; + message?: string; + serviceMessage?: string; + data?: Data; + retry?: null | boolean | Date; +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts index 7a0e24521a1c6..3d92d5ebf33fc 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/es_index.test.ts @@ -284,4 +284,47 @@ describe('execute()', () => { ] `); }); + + test('resolves with an error when an error occurs in the indexing operation', async () => { + const secrets = {}; + // minimal params + const config = { index: 'index-value', refresh: false, executionTimeField: null }; + const params = { + documents: [{ '': 'bob' }], + }; + + const actionId = 'some-id'; + + services.callCluster.mockResolvedValue({ + took: 0, + errors: true, + items: [ + { + index: { + _index: 'indexme', + _id: '7buTjHQB0SuNSiS9Hayt', + status: 400, + error: { + type: 'mapper_parsing_exception', + reason: 'failed to parse', + caused_by: { + type: 'illegal_argument_exception', + reason: 'field name cannot be an empty string', + }, + }, + }, + }, + ], + }); + + expect(await actionType.executor({ actionId, config, secrets, params, services })) + .toMatchInlineSnapshot(` + Object { + "actionId": "some-id", + "message": "error indexing documents", + "serviceMessage": "failed to parse (field name cannot be an empty string)", + "status": "error", + } + `); + }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/es_index.ts b/x-pack/plugins/actions/server/builtin_action_types/es_index.ts index 53bf75651b1e5..868c07b775c78 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/es_index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/es_index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { curry } from 'lodash'; +import { curry, find } from 'lodash'; import { i18n } from '@kbn/i18n'; import { schema, TypeOf } from '@kbn/config-schema'; @@ -85,21 +85,39 @@ async function executor( refresh: config.refresh, }; - let result; try { - result = await services.callCluster('bulk', bulkParams); + const result = await services.callCluster('bulk', bulkParams); + + const err = find(result.items, 'index.error.reason'); + if (err) { + return wrapErr( + `${err.index.error!.reason}${ + err.index.error?.caused_by ? ` (${err.index.error?.caused_by?.reason})` : '' + }`, + actionId, + logger + ); + } + + return { status: 'ok', data: result, actionId }; } catch (err) { - const message = i18n.translate('xpack.actions.builtin.esIndex.errorIndexingErrorMessage', { - defaultMessage: 'error indexing documents', - }); - logger.error(`error indexing documents: ${err.message}`); - return { - status: 'error', - actionId, - message, - serviceMessage: err.message, - }; + return wrapErr(err.message, actionId, logger); } +} - return { status: 'ok', data: result, actionId }; +function wrapErr( + errMessage: string, + actionId: string, + logger: Logger +): ActionTypeExecutorResult { + const message = i18n.translate('xpack.actions.builtin.esIndex.errorIndexingErrorMessage', { + defaultMessage: 'error indexing documents', + }); + logger.error(`error indexing documents: ${errMessage}`); + return { + status: 'error', + actionId, + message, + serviceMessage: errMessage, + }; } diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts index 3e92ca331bb93..a23a2b0893261 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -15,6 +15,8 @@ import { SavedObjectsClientContract, SavedObjectAttributes, } from '../../../../src/core/server'; +import { ActionTypeExecutorResult } from '../common'; +export { ActionTypeExecutorResult } from '../common'; export type WithoutQueryAndParams = Pick>; export type GetServicesFunction = (request: KibanaRequest) => Services; @@ -80,16 +82,6 @@ export interface FindActionResult extends ActionResult { referencedByCount: number; } -// the result returned from an action type executor function -export interface ActionTypeExecutorResult { - actionId: string; - status: 'ok' | 'error'; - message?: string; - serviceMessage?: string; - data?: Data; - retry?: null | boolean | Date; -} - // signature of the action type executor function export type ExecutorType = ( options: ActionTypeExecutorOptions diff --git a/x-pack/plugins/alerts/server/task_runner/task_runner.ts b/x-pack/plugins/alerts/server/task_runner/task_runner.ts index 5be684eca4651..7ea3f83d747c0 100644 --- a/x-pack/plugins/alerts/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerts/server/task_runner/task_runner.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { pickBy, mapValues, omit, without } from 'lodash'; +import { pickBy, mapValues, without } from 'lodash'; import { Logger, KibanaRequest } from '../../../../../src/core/server'; import { TaskRunnerContext } from './task_runner_factory'; import { ConcreteTaskInstance } from '../../../task_manager/server'; @@ -228,12 +228,13 @@ export class TaskRunner { }); if (!muteAll) { - const enabledAlertInstances = omit(instancesWithScheduledActions, ...mutedInstanceIds); + const mutedInstanceIdsSet = new Set(mutedInstanceIds); await Promise.all( - Object.entries(enabledAlertInstances) + Object.entries(instancesWithScheduledActions) .filter( - ([, alertInstance]: [string, AlertInstance]) => !alertInstance.isThrottled(throttle) + ([alertInstanceName, alertInstance]: [string, AlertInstance]) => + !alertInstance.isThrottled(throttle) && !mutedInstanceIdsSet.has(alertInstanceName) ) .map(([id, alertInstance]: [string, AlertInstance]) => this.executeAlertInstance(id, alertInstance, executionHandler) diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageViewsChart.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageViewsChart.tsx index c76be19edfe47..904144dec6de9 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageViewsChart.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/PageViewsChart.tsx @@ -33,7 +33,10 @@ import { ChartWrapper } from '../ChartWrapper'; import { I18LABELS } from '../translations'; interface Props { - data?: Array>; + data?: { + topItems: string[]; + items: Array>; + }; loading: boolean; } @@ -68,15 +71,9 @@ export function PageViewsChart({ data, loading }: Props) { }); }; - let breakdownAccessors: Set = new Set(); - if (data && data.length > 0) { - data.forEach((item) => { - breakdownAccessors = new Set([ - ...Array.from(breakdownAccessors), - ...Object.keys(item).filter((key) => key !== 'x'), - ]); - }); - } + const breakdownAccessors = data?.topItems?.length ? data?.topItems : ['y']; + + const [darkMode] = useUiSetting$('theme:darkMode'); const customSeriesNaming: SeriesNameFn = ({ yAccessor }) => { if (yAccessor === 'y') { @@ -86,8 +83,6 @@ export function PageViewsChart({ data, loading }: Props) { return yAccessor; }; - const [darkMode] = useUiSetting$('theme:darkMode'); - return ( {(!loading || data) && ( @@ -115,7 +110,8 @@ export function PageViewsChart({ data, loading }: Props) { id="page_views" title={I18LABELS.pageViews} position={Position.Left} - tickFormat={(d) => numeral(d).format('0a')} + tickFormat={(d) => numeral(d).format('0')} + labelFormat={(d) => numeral(d).format('0a')} /> diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/BetaBadge.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/BetaBadge.tsx deleted file mode 100644 index b468470e3a17d..0000000000000 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/BetaBadge.tsx +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiBetaBadge } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import styled from 'styled-components'; - -const BetaBadgeContainer = styled.div` - right: ${({ theme }) => theme.eui.gutterTypes.gutterMedium}; - position: absolute; - top: ${({ theme }) => theme.eui.gutterTypes.gutterSmall}; - z-index: 1; /* The element containing the cytoscape canvas has z-index = 0. */ -`; - -export function BetaBadge() { - return ( - - - - ); -} diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx index cb5a57e9ab9fb..bb450131bdfb8 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx @@ -4,15 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { useTheme } from '../../../hooks/useTheme'; +import React from 'react'; +import { useTrackPageview } from '../../../../../observability/public'; import { invalidLicenseMessage, isActivePlatinumLicense, } from '../../../../common/service_map'; import { useFetcher } from '../../../hooks/useFetcher'; import { useLicense } from '../../../hooks/useLicense'; +import { useTheme } from '../../../hooks/useTheme'; import { useUrlParams } from '../../../hooks/useUrlParams'; import { callApmApi } from '../../../services/rest/createCallApmApi'; import { LicensePrompt } from '../../shared/LicensePrompt'; @@ -22,8 +23,6 @@ import { getCytoscapeDivStyle } from './cytoscapeOptions'; import { EmptyBanner } from './EmptyBanner'; import { Popover } from './Popover'; import { useRefDimensions } from './useRefDimensions'; -import { BetaBadge } from './BetaBadge'; -import { useTrackPageview } from '../../../../../observability/public'; interface ServiceMapProps { serviceName?: string; @@ -80,7 +79,6 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { style={getCytoscapeDivStyle(theme)} > - {serviceName && } @@ -96,7 +94,7 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { grow={false} style={{ width: 600, textAlign: 'center' as const }} > - + ); diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts b/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts index f25062c67f87a..543aa911b0b1f 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts @@ -51,6 +51,16 @@ export async function getPageViewTrends({ } : undefined, }, + ...(breakdownItem + ? { + topBreakdowns: { + terms: { + field: breakdownItem.fieldName, + size: 9, + }, + }, + } + : {}), }, }, }); @@ -59,25 +69,44 @@ export async function getPageViewTrends({ const response = await apmEventClient.search(params); + const { topBreakdowns } = response.aggregations ?? {}; + + // we are only displaying top 9 + const topItems: string[] = (topBreakdowns?.buckets ?? []).map( + ({ key }) => key as string + ); + const result = response.aggregations?.pageViews.buckets ?? []; - return result.map((bucket) => { - const { key: xVal, doc_count: bCount } = bucket; - const res: Record = { - x: xVal, - y: bCount, - }; - if ('breakdown' in bucket) { - const categoryBuckets = bucket.breakdown.buckets; - categoryBuckets.forEach(({ key, doc_count: docCount }) => { - if (key === 'Other') { - res[key + `(${breakdownItem?.name})`] = docCount; - } else { - res[key] = docCount; + return { + topItems, + items: result.map((bucket) => { + const { key: xVal, doc_count: bCount } = bucket; + const res: Record = { + x: xVal, + y: bCount, + }; + if ('breakdown' in bucket) { + let top9Count = 0; + const categoryBuckets = bucket.breakdown.buckets; + categoryBuckets.forEach(({ key, doc_count: docCount }) => { + if (topItems.includes(key as string)) { + if (res[key]) { + // if term is already in object, just add it to it + res[key] += docCount; + } else { + res[key] = docCount; + } + top9Count += docCount; + } + }); + // Top 9 plus others, get a diff from parent bucket total + if (bCount > top9Count) { + res.Other = bCount - top9Count; } - }); - } + } - return res; - }); + return res; + }), + }; } diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts index 5c183fd9150dd..7c2137ce65d83 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.ts @@ -96,10 +96,12 @@ async function getErrorStats({ setup, serviceName, environment, + searchAggregatedTransactions, }: { setup: Options['setup']; serviceName: string; environment?: string; + searchAggregatedTransactions: boolean; }) { const setupWithBlankUiFilters = { ...setup, @@ -108,6 +110,7 @@ async function getErrorStats({ const { noHits, average } = await getErrorRate({ setup: setupWithBlankUiFilters, serviceName, + searchAggregatedTransactions, }); return { avgErrorRate: noHits ? null : average }; } diff --git a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap index a65536df37bc8..431f11066aaff 100644 --- a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap @@ -277,6 +277,13 @@ Array [ "services": Object { "aggs": Object { "outcomes": Object { + "aggs": Object { + "count": Object { + "value_count": Object { + "field": "transaction.duration.us", + }, + }, + }, "terms": Object { "field": "event.outcome", }, @@ -284,6 +291,13 @@ Array [ "timeseries": Object { "aggs": Object { "outcomes": Object { + "aggs": Object { + "count": Object { + "value_count": Object { + "field": "transaction.duration.us", + }, + }, + }, "terms": Object { "field": "event.outcome", }, diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts index 17799203fe73b..65bc3f7e47171 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts @@ -14,7 +14,6 @@ import { EVENT_OUTCOME, } from '../../../../common/elasticsearch_fieldnames'; import { mergeProjection } from '../../../projections/util/merge_projection'; -import { ProcessorEvent } from '../../../../common/processor_event'; import { ServicesItemsSetup, ServicesItemsProjection, @@ -258,6 +257,7 @@ export const getTransactionRates = async ({ export const getTransactionErrorRates = async ({ setup, projection, + searchAggregatedTransactions, }: AggregationParams) => { const { apmEventClient, start, end } = setup; @@ -265,12 +265,25 @@ export const getTransactionErrorRates = async ({ terms: { field: EVENT_OUTCOME, }, + aggs: { + count: { + value_count: { + field: getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ), + }, + }, + }, }; const response = await apmEventClient.search( mergeProjection(projection, { apm: { - events: [ProcessorEvent.transaction], + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ], }, body: { size: 0, @@ -319,11 +332,11 @@ export const getTransactionErrorRates = async ({ const successfulTransactions = outcomeResponse.buckets.find( (bucket) => bucket.key === EventOutcome.success - )?.doc_count ?? 0; + )?.count.value ?? 0; const failedTransactions = outcomeResponse.buckets.find( (bucket) => bucket.key === EventOutcome.failure - )?.doc_count ?? 0; + )?.count.value ?? 0; return failedTransactions / (successfulTransactions + failedTransactions); } diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts index 82595317342f1..3dc126c45d328 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts @@ -11,7 +11,6 @@ import { SERVICE_NAME, EVENT_OUTCOME, } from '../../../common/elasticsearch_fieldnames'; -import { ProcessorEvent } from '../../../common/processor_event'; import { rangeFilter } from '../../../common/utils/range_filter'; import { Setup, @@ -19,17 +18,23 @@ import { SetupUIFilters, } from '../helpers/setup_request'; import { getBucketSize } from '../helpers/get_bucket_size'; +import { + getProcessorEventForAggregatedTransactions, + getTransactionDurationFieldForAggregatedTransactions, +} from '../helpers/aggregated_transactions'; export async function getErrorRate({ serviceName, transactionType, transactionName, setup, + searchAggregatedTransactions, }: { serviceName: string; transactionType?: string; transactionName?: string; setup: Setup & SetupTimeRange & SetupUIFilters; + searchAggregatedTransactions: boolean; }) { const { start, end, uiFiltersES, apmEventClient } = setup; @@ -53,7 +58,11 @@ export async function getErrorRate({ const params = { apm: { - events: [ProcessorEvent.transaction], + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ], }, body: { size: 0, @@ -67,8 +76,19 @@ export async function getErrorRate({ extended_bounds: { min: start, max: end }, }, aggs: { - erroneous_transactions: { - filter: { term: { [EVENT_OUTCOME]: EventOutcome.failure } }, + [EVENT_OUTCOME]: { + terms: { + field: EVENT_OUTCOME, + }, + aggs: { + count: { + value_count: { + field: getTransactionDurationFieldForAggregatedTransactions( + searchAggregatedTransactions + ), + }, + }, + }, }, }, }, @@ -81,18 +101,24 @@ export async function getErrorRate({ const noHits = resp.hits.total.value === 0; const erroneousTransactionsRate = - resp.aggregations?.total_transactions.buckets.map( - ({ - key, - doc_count: totalTransactions, - erroneous_transactions: erroneousTransactions, - }) => { - return { - x: key, - y: erroneousTransactions.doc_count / totalTransactions, - }; - } - ) || []; + resp.aggregations?.total_transactions.buckets.map((bucket) => { + const successful = + bucket[EVENT_OUTCOME].buckets.find( + (eventOutcomeBucket) => + eventOutcomeBucket.key === EventOutcome.success + )?.count.value ?? 0; + + const failed = + bucket[EVENT_OUTCOME].buckets.find( + (eventOutcomeBucket) => + eventOutcomeBucket.key === EventOutcome.failure + )?.count.value ?? 0; + + return { + x: bucket.key, + y: failed / (successful + failed), + }; + }) || []; const average = mean( erroneousTransactionsRate diff --git a/x-pack/plugins/apm/server/routes/transaction_groups.ts b/x-pack/plugins/apm/server/routes/transaction_groups.ts index 888c4363f77b9..10e917f385e71 100644 --- a/x-pack/plugins/apm/server/routes/transaction_groups.ts +++ b/x-pack/plugins/apm/server/routes/transaction_groups.ts @@ -210,11 +210,17 @@ export const transactionGroupsErrorRateRoute = createRoute(() => ({ const { params } = context; const { serviceName } = params.path; const { transactionType, transactionName } = params.query; + + const searchAggregatedTransactions = await getSearchAggregatedTransactions( + setup + ); + return getErrorRate({ serviceName, transactionType, transactionName, setup, + searchAggregatedTransactions, }); }, })); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts index 0f7bfe09edf7e..9410b9ef7cb03 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts @@ -56,6 +56,15 @@ describe('AppLogic', () => { }), }); }); + + it('gracefully handles missing initial data', () => { + AppLogic.actions.initializeAppData({}); + + expect(AppLogic.values).toEqual({ + ...DEFAULT_VALUES, + hasInitialized: true, + }); + }); }); describe('setOnboardingComplete()', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts index 8e5a8d75f407f..932e84af45c2b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts @@ -39,7 +39,7 @@ export const AppLogic = kea>({ account: [ {}, { - initializeAppData: (_, { appSearch: account }) => account, + initializeAppData: (_, { appSearch: account }) => account || {}, setOnboardingComplete: (account) => ({ ...account, onboardingComplete: true, @@ -49,7 +49,7 @@ export const AppLogic = kea>({ configuredLimits: [ {}, { - initializeAppData: (_, { configuredLimits }) => configuredLimits.appSearch, + initializeAppData: (_, { configuredLimits }) => configuredLimits?.appSearch || {}, }, ], ilmEnabled: [ diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.test.tsx new file mode 100644 index 0000000000000..8d48875a8e1f5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.test.tsx @@ -0,0 +1,19 @@ +/* + * 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 { shallow } from 'enzyme'; + +import { ErrorStatePrompt } from '../../../shared/error_state'; +import { ErrorConnecting } from './'; + +describe('ErrorConnecting', () => { + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(ErrorStatePrompt)).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.tsx new file mode 100644 index 0000000000000..567c77792583d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiPage, EuiPageContent } from '@elastic/eui'; + +import { ErrorStatePrompt } from '../../../shared/error_state'; + +export const ErrorConnecting: React.FC = () => ( + + + + + +); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/index.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/index.ts new file mode 100644 index 0000000000000..c8b71e1a6e791 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/index.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ + +export { ErrorConnecting } from './error_connecting'; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx index cd2a22a45bbb4..b2918dac086f6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx @@ -6,13 +6,20 @@ import React from 'react'; import { shallow } from 'enzyme'; - import { EuiPage } from '@elastic/eui'; +import '../__mocks__/kea.mock'; +import { useValues } from 'kea'; + import { EnterpriseSearch } from './'; +import { ErrorConnecting } from './components/error_connecting'; import { ProductCard } from './components/product_card'; describe('EnterpriseSearch', () => { + beforeEach(() => { + (useValues as jest.Mock).mockReturnValue({ errorConnecting: false }); + }); + it('renders the overview page and product cards', () => { const wrapper = shallow( @@ -22,6 +29,14 @@ describe('EnterpriseSearch', () => { expect(wrapper.find(ProductCard)).toHaveLength(2); }); + it('renders the error connecting prompt', () => { + (useValues as jest.Mock).mockReturnValueOnce({ errorConnecting: true }); + const wrapper = shallow(); + + expect(wrapper.find(ErrorConnecting)).toHaveLength(1); + expect(wrapper.find(EuiPage)).toHaveLength(0); + }); + describe('access checks', () => { it('does not render the App Search card if the user does not have access to AS', () => { const wrapper = shallow( diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx index 373f595a6a9ea..3a3ba02e07058 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx @@ -5,6 +5,7 @@ */ import React from 'react'; +import { useValues } from 'kea'; import { EuiPage, EuiPageBody, @@ -21,9 +22,11 @@ import { i18n } from '@kbn/i18n'; import { IInitialAppData } from '../../../common/types'; import { APP_SEARCH_PLUGIN, WORKPLACE_SEARCH_PLUGIN } from '../../../common/constants'; +import { HttpLogic } from '../shared/http'; import { SetEnterpriseSearchChrome as SetPageChrome } from '../shared/kibana_chrome'; import { SendEnterpriseSearchTelemetry as SendTelemetry } from '../shared/telemetry'; +import { ErrorConnecting } from './components/error_connecting'; import { ProductCard } from './components/product_card'; import AppSearchImage from './assets/app_search.png'; @@ -31,9 +34,12 @@ import WorkplaceSearchImage from './assets/workplace_search.png'; import './index.scss'; export const EnterpriseSearch: React.FC = ({ access = {} }) => { + const { errorConnecting } = useValues(HttpLogic); const { hasAppSearchAccess, hasWorkplaceSearchAccess } = access; - return ( + return errorConnecting ? ( + + ) : ( diff --git a/x-pack/plugins/enterprise_search/public/applications/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/index.test.tsx index e0cf2814b46b4..053c450ab925e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.test.tsx @@ -10,7 +10,7 @@ import { AppMountParameters } from 'src/core/public'; import { coreMock } from 'src/core/public/mocks'; import { licensingMock } from '../../../licensing/public/mocks'; -import { renderApp } from './'; +import { renderApp, renderHeaderActions } from './'; import { AppSearch } from './app_search'; import { WorkplaceSearch } from './workplace_search'; @@ -33,6 +33,7 @@ describe('renderApp', () => { const unmount = renderApp(MockApp, params, core, plugins, config, data); expect(params.element.querySelector('.hello-world')).not.toBeNull(); + unmount(); expect(params.element.innerHTML).toEqual(''); }); @@ -47,3 +48,16 @@ describe('renderApp', () => { expect(params.element.querySelector('.setupGuide')).not.toBeNull(); }); }); + +describe('renderHeaderActions', () => { + it('mounts and unmounts any HeaderActions component', () => { + const mockHeaderEl = document.createElement('header'); + const MockHeaderActions = () => ; + + const unmount = renderHeaderActions(MockHeaderActions, mockHeaderEl, {} as any); + expect(mockHeaderEl.querySelector('.hello-world')).not.toBeNull(); + + unmount(); + expect(mockHeaderEl.innerHTML).toEqual(''); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/index.tsx b/x-pack/plugins/enterprise_search/public/applications/index.tsx index 82f884644be4a..43056f2f65538 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.tsx @@ -88,3 +88,22 @@ export const renderApp = ( ReactDOM.unmountComponentAtNode(params.element); }; }; + +/** + * Render function for Kibana's header action menu chrome - + * reusable by any Enterprise Search plugin simply by passing in + * a custom HeaderActions component (e.g., WorkplaceSearchHeaderActions) + * @see https://github.com/elastic/kibana/blob/master/docs/development/core/public/kibana-plugin-core-public.appmountparameters.setheaderactionmenu.md + */ +interface IHeaderActionsProps { + externalUrl: IExternalUrl; +} + +export const renderHeaderActions = ( + HeaderActions: React.FC, + kibanaHeaderEl: HTMLElement, + externalUrl: IExternalUrl +) => { + ReactDOM.render(, kibanaHeaderEl); + return () => ReactDOM.unmountComponentAtNode(kibanaHeaderEl); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx index 8f7cf090e2d57..1d64b453b2c2c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx @@ -33,7 +33,7 @@ describe('Shared Telemetry Helpers', () => { metric: 'setup_guide', }); - expect(httpMock.put).toHaveBeenCalledWith('/api/enterprise_search/telemetry', { + expect(httpMock.put).toHaveBeenCalledWith('/api/enterprise_search/stats', { headers, body: '{"product":"enterprise_search","action":"viewed","metric":"setup_guide"}', }); @@ -54,7 +54,7 @@ describe('Shared Telemetry Helpers', () => { http: httpMock, }); - expect(httpMock.put).toHaveBeenCalledWith('/api/enterprise_search/telemetry', { + expect(httpMock.put).toHaveBeenCalledWith('/api/enterprise_search/stats', { headers, body: '{"product":"enterprise_search","action":"viewed","metric":"page"}', }); @@ -65,7 +65,7 @@ describe('Shared Telemetry Helpers', () => { http: httpMock, }); - expect(httpMock.put).toHaveBeenCalledWith('/api/enterprise_search/telemetry', { + expect(httpMock.put).toHaveBeenCalledWith('/api/enterprise_search/stats', { headers, body: '{"product":"app_search","action":"clicked","metric":"button"}', }); @@ -76,7 +76,7 @@ describe('Shared Telemetry Helpers', () => { http: httpMock, }); - expect(httpMock.put).toHaveBeenCalledWith('/api/enterprise_search/telemetry', { + expect(httpMock.put).toHaveBeenCalledWith('/api/enterprise_search/stats', { headers, body: '{"product":"workplace_search","action":"error","metric":"not_found"}', }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx index 4df1428221de6..e3c9ba9b8a218 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx @@ -27,7 +27,7 @@ interface ISendTelemetry extends ISendTelemetryProps { export const sendTelemetry = async ({ http, product, action, metric }: ISendTelemetry) => { try { const body = JSON.stringify({ product, action, metric }); - await http.put('/api/enterprise_search/telemetry', { headers, body }); + await http.put('/api/enterprise_search/stats', { headers, body }); } catch (error) { throw new Error('Unable to send telemetry'); } diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.test.ts index c52eceb2d2fdd..974e07069ddba 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.test.ts @@ -50,5 +50,15 @@ describe('AppLogic', () => { expect(AppLogic.values).toEqual(expectedLogicValues); }); + + it('gracefully handles missing initial data', () => { + AppLogic.actions.initializeAppData({}); + + expect(AppLogic.values).toEqual({ + ...DEFAULT_VALUES, + hasInitialized: true, + isFederatedAuth: false, + }); + }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts index 94bd1d529b65f..629d1969a8f59 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts @@ -21,6 +21,9 @@ export interface IAppActions { initializeAppData(props: IInitialAppData): IInitialAppData; } +const emptyOrg = {} as IOrganization; +const emptyAccount = {} as IAccount; + export const AppLogic = kea>({ path: ['enterprise_search', 'workplace_search', 'app_logic'], actions: { @@ -43,15 +46,15 @@ export const AppLogic = kea>({ }, ], organization: [ - {} as IOrganization, + emptyOrg, { - initializeAppData: (_, { workplaceSearch }) => workplaceSearch!.organization, + initializeAppData: (_, { workplaceSearch }) => workplaceSearch?.organization || emptyOrg, }, ], account: [ - {} as IAccount, + emptyAccount, { - initializeAppData: (_, { workplaceSearch }) => workplaceSearch!.account, + initializeAppData: (_, { workplaceSearch }) => workplaceSearch?.account || emptyAccount, }, ], }, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/index.ts index 41861a8ee2dc5..915638246c00e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/index.ts @@ -5,3 +5,4 @@ */ export { WorkplaceSearchNav } from './nav'; +export { WorkplaceSearchHeaderActions } from './kibana_header_actions'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.test.tsx new file mode 100644 index 0000000000000..a006c5e3775d5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.test.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { EuiButtonEmpty } from '@elastic/eui'; +import { ExternalUrl } from '../../../shared/enterprise_search_url'; + +import { WorkplaceSearchHeaderActions } from './'; + +describe('WorkplaceSearchHeaderActions', () => { + const externalUrl = new ExternalUrl('http://localhost:3002'); + + it('renders a link to the search application', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiButtonEmpty).prop('href')).toEqual('http://localhost:3002/ws/search'); + }); + + it('does not render without an Enterprise Search host URL set', () => { + const wrapper = shallow(); + + expect(wrapper.isEmptyRender()).toBe(true); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.tsx new file mode 100644 index 0000000000000..fa32d598f848d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.tsx @@ -0,0 +1,28 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { EuiButtonEmpty } from '@elastic/eui'; + +import { IExternalUrl } from '../../../shared/enterprise_search_url'; + +interface IProps { + externalUrl: IExternalUrl; +} + +export const WorkplaceSearchHeaderActions: React.FC = ({ externalUrl }) => { + const { enterpriseSearchUrl, getWorkplaceSearchUrl } = externalUrl; + if (!enterpriseSearchUrl) return null; + + return ( + + {i18n.translate('xpack.enterpriseSearch.workplaceSearch.headerActions.searchApplication', { + defaultMessage: 'Go to search application', + })} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/plugin.ts b/x-pack/plugins/enterprise_search/public/plugin.ts index 0ef58a7c03f10..c23bb23be3979 100644 --- a/x-pack/plugins/enterprise_search/public/plugin.ts +++ b/x-pack/plugins/enterprise_search/public/plugin.ts @@ -103,9 +103,16 @@ export class EnterpriseSearchPlugin implements Plugin { await this.getInitialData(coreStart.http); - const { renderApp } = await import('./applications'); + const { renderApp, renderHeaderActions } = await import('./applications'); const { WorkplaceSearch } = await import('./applications/workplace_search'); + const { WorkplaceSearchHeaderActions } = await import( + './applications/workplace_search/components/layout' + ); + params.setHeaderActionMenu((element) => + renderHeaderActions(WorkplaceSearchHeaderActions, element, this.data.externalUrl) + ); + return renderApp(WorkplaceSearch, params, coreStart, plugins, this.config, this.data); }, }); diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts index acddd3539965a..bd6f4b9da91fd 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts @@ -35,7 +35,7 @@ describe('Enterprise Search Telemetry API', () => { }); }); - describe('PUT /api/enterprise_search/telemetry', () => { + describe('PUT /api/enterprise_search/stats', () => { it('increments the saved objects counter for App Search', async () => { (incrementUICounter as jest.Mock).mockImplementation(jest.fn(() => successResponse)); diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.ts index bfc07c8b64ef5..8f6638ddc099e 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.ts @@ -25,7 +25,7 @@ export function registerTelemetryRoute({ }: IRouteDependencies) { router.put( { - path: '/api/enterprise_search/telemetry', + path: '/api/enterprise_search/stats', validate: { body: schema.object({ product: schema.oneOf([ diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/point_datatype.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/point_datatype.test.tsx new file mode 100644 index 0000000000000..0ee70d63ba667 --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/point_datatype.test.tsx @@ -0,0 +1,158 @@ +/* + * 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 { act } from 'react-dom/test-utils'; + +import { componentHelpers, MappingsEditorTestBed } from '../helpers'; + +const { setup, getMappingsEditorDataFactory } = componentHelpers.mappingsEditor; + +// Parameters automatically added to the point datatype when saved (with the default values) +export const defaultPointParameters = { + type: 'point', + ignore_malformed: false, + ignore_z_value: true, +}; + +describe('Mappings editor: point datatype', () => { + /** + * Variable to store the mappings data forwarded to the consumer component + */ + let data: any; + let onChangeHandler: jest.Mock = jest.fn(); + let getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler); + let testBed: MappingsEditorTestBed; + + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + beforeEach(() => { + onChangeHandler = jest.fn(); + getMappingsEditorData = getMappingsEditorDataFactory(onChangeHandler); + }); + + test('initial view and default parameters values', async () => { + const defaultMappings = { + properties: { + myField: { + type: 'point', + }, + }, + }; + + const updatedMappings = { ...defaultMappings }; + + await act(async () => { + testBed = setup({ value: defaultMappings, onChange: onChangeHandler }); + }); + testBed.component.update(); + + const { + component, + actions: { startEditField, updateFieldAndCloseFlyout }, + } = testBed; + + // Open the flyout to edit the field + await startEditField('myField'); + + // Save the field and close the flyout + await updateFieldAndCloseFlyout(); + + // It should have the default parameters values added + updatedMappings.properties.myField = defaultPointParameters; + + ({ data } = await getMappingsEditorData(component)); + expect(data).toEqual(updatedMappings); + }); + + describe('meta parameter', () => { + const defaultMappings = { + properties: { + myField: { + type: 'point', + }, + }, + }; + + const updatedMappings = { ...defaultMappings }; + + const metaParameter = { + meta: { + my_metadata: 'foobar', + }, + }; + + beforeEach(async () => { + await act(async () => { + testBed = setup({ value: defaultMappings, onChange: onChangeHandler }); + }); + testBed.component.update(); + }); + + test('valid meta object', async () => { + const { + component, + actions: { + startEditField, + updateFieldAndCloseFlyout, + showAdvancedSettings, + toggleFormRow, + updateJsonEditor, + }, + } = testBed; + + // Open the flyout to edit the field + await startEditField('myField'); + await showAdvancedSettings(); + + // Enable the meta parameter and add value + toggleFormRow('metaParameter'); + await act(async () => { + updateJsonEditor('metaParameterEditor', metaParameter.meta); + }); + component.update(); + + // Save the field and close the flyout + await updateFieldAndCloseFlyout(); + + // It should have the default parameters values added, plus metadata + updatedMappings.properties.myField = { + ...defaultPointParameters, + ...metaParameter, + }; + + ({ data } = await getMappingsEditorData(component)); + expect(data).toEqual(updatedMappings); + }); + + test('strip empty string', async () => { + const { + component, + actions: { startEditField, updateFieldAndCloseFlyout, showAdvancedSettings, toggleFormRow }, + } = testBed; + + // Open the flyout to edit the field + await startEditField('myField'); + await showAdvancedSettings(); + + // Enable the meta parameter + toggleFormRow('metaParameter'); + + // Save the field and close the flyout without adding any values to meta parameter + await updateFieldAndCloseFlyout(); + + // It should have the default parameters values added + updatedMappings.properties.myField = defaultPointParameters; + + ({ data } = await getMappingsEditorData(component)); + expect(data).toEqual(updatedMappings); + }); + }); +}); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx index 2a4af89c46559..e123dea6ff2ff 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/helpers/mappings_editor.helpers.tsx @@ -239,6 +239,10 @@ const createActions = (testBed: TestBed) => { const getCheckboxValue = (testSubject: TestSubjects): boolean => find(testSubject).props().checked; + const toggleFormRow = (formRowName: string) => { + form.toggleEuiSwitch(`${formRowName}.formRowToggle`); + }; + return { selectTab, getFieldAt, @@ -252,6 +256,7 @@ const createActions = (testBed: TestBed) => { getComboBoxValue, getToggleValue, getCheckboxValue, + toggleFormRow, }; }; @@ -365,4 +370,6 @@ export type TestSubjects = | 'searchQuoteAnalyzer-custom' | 'searchQuoteAnalyzer-toggleCustomButton' | 'searchQuoteAnalyzer-custom.input' - | 'useSameAnalyzerForSearchCheckBox.input'; + | 'useSameAnalyzerForSearchCheckBox.input' + | 'metaParameterEditor' + | string; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/ignore_z_value_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/ignore_z_value_parameter.tsx index bd118ac08964f..ce58a264db968 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/ignore_z_value_parameter.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/ignore_z_value_parameter.tsx @@ -10,15 +10,18 @@ import { i18n } from '@kbn/i18n'; import { EditFieldFormRow } from '../fields/edit_field'; -export const IgnoreZValueParameter = () => ( +export const IgnoreZValueParameter = ({ description }: { description?: string }) => ( ); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/meta_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/meta_parameter.tsx index c8af296318b61..a950ba82d0eac 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/meta_parameter.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/meta_parameter.tsx @@ -32,6 +32,7 @@ export const MetaParameter: FunctionComponent = ({ defaultToggleValue }) }), href: documentationService.getMetaLink(), }} + data-test-subj="metaParameter" > = ({ defaultToggleValue }) component={JsonEditorField} componentProps={{ euiCodeEditorProps: { + ['data-test-subj']: 'metaParameterEditor', height: '300px', 'aria-label': i18n.translate('xpack.idxMgmt.mappingsEditor.metaParameterAriaLabel', { defaultMessage: 'metadata field data editor', diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/index.ts index 8fcd02e4a362e..6b092c5561b3b 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/index.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/index.ts @@ -32,6 +32,7 @@ import { HistogramType } from './histogram_type'; import { ConstantKeywordType } from './constant_keyword_type'; import { RankFeatureType } from './rank_feature_type'; import { WildcardType } from './wildcard_type'; +import { PointType } from './point_type'; const typeToParametersFormMap: { [key in DataType]?: ComponentType } = { alias: AliasType, @@ -60,6 +61,7 @@ const typeToParametersFormMap: { [key in DataType]?: ComponentType } = { constant_keyword: ConstantKeywordType, rank_feature: RankFeatureType, wildcard: WildcardType, + point: PointType, }; export const getParametersFormForType = ( diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/point_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/point_type.tsx new file mode 100644 index 0000000000000..9108c56e4496b --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/point_type.tsx @@ -0,0 +1,76 @@ +/* + * 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, { FunctionComponent } from 'react'; + +import { i18n } from '@kbn/i18n'; + +import { NormalizedField, Field as FieldType, ParameterName } from '../../../../types'; +import { UseField, TextAreaField } from '../../../../shared_imports'; +import { getFieldConfig } from '../../../../lib'; +import { + IgnoreMalformedParameter, + IgnoreZValueParameter, + NullValueParameter, + MetaParameter, +} from '../../field_parameters'; +import { AdvancedParametersSection, BasicParametersSection } from '../edit_field'; + +interface Props { + field: NormalizedField; +} + +const getDefaultToggleValue = (param: ParameterName, field: FieldType) => { + return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue; +}; + +export const PointType: FunctionComponent = ({ field }) => { + return ( + <> + + + + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx index a4d3bf3832d5c..293ae56d57ace 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx @@ -821,6 +821,26 @@ export const TYPE_DEFINITION: { [key in DataType]: DataTypeDefinition } = {

), }, + point: { + label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.pointDescription', { + defaultMessage: 'Point', + }), + value: 'point', + documentation: { + main: '/point.html', + }, + description: () => ( +

+ {'x,y'}, + }} + /> +

+ ), + }, wildcard: { label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.wildcardDescription', { defaultMessage: 'Wildcard', @@ -882,6 +902,7 @@ export const MAIN_TYPES: MainType[] = [ 'token_count', 'histogram', 'wildcard', + 'point', 'other', ]; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/parameters_definition.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/parameters_definition.tsx index fd17dc1b8fd1e..4ffedc8ca114d 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/parameters_definition.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/parameters_definition.tsx @@ -382,6 +382,50 @@ export const PARAMETERS_DEFINITION: { [key in ParameterName]: ParameterDefinitio }, schema: t.any, }, + null_value_point: { + fieldConfig: { + defaultValue: '', + label: nullValueLabel, + helpText: () => ( + + {i18n.translate( + 'xpack.idxMgmt.mappingsEditor.parameters.pointWellKnownTextDocumentationLink', + { + defaultMessage: 'Well-Known Text', + } + )} + + ), + }} + /> + ), + validations: [ + { + validator: nullValueValidateEmptyField, + }, + ], + deserializer: (value: any) => { + if (value === '') { + return value; + } + return JSON.stringify(value); + }, + serializer: (value: string) => { + try { + return JSON.parse(value); + } catch (error) { + // swallow error and return non-parsed value; + return value; + } + }, + }, + schema: t.any, + }, copy_to: { fieldConfig: { defaultValue: '', @@ -476,12 +520,22 @@ export const PARAMETERS_DEFINITION: { [key in ParameterName]: ParameterDefinitio return JSON.stringify(value, null, 2); }, serializer: (value: string) => { - const parsed = JSON.parse(value); - // If an empty object was passed, strip out this value entirely. - if (!Object.keys(parsed).length) { + // Strip out empty strings + if (value.trim() === '') { return undefined; } - return parsed; + + try { + const parsed = JSON.parse(value); + // If an empty object was passed, strip out this value entirely. + if (!Object.keys(parsed).length) { + return undefined; + } + return parsed; + } catch (error) { + // swallow error and return non-parsed value; + return value; + } }, }, schema: t.any, diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts index 97dca49fc93ed..ca38a8d1e6c33 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts @@ -59,6 +59,7 @@ export type MainType = | 'geo_point' | 'geo_shape' | 'token_count' + | 'point' | 'histogram' | 'constant_keyword' | 'wildcard' @@ -109,6 +110,7 @@ export type ParameterName = | 'null_value_boolean' | 'null_value_geo_point' | 'null_value_ip' + | 'null_value_point' | 'copy_to' | 'dynamic' | 'dynamic_toggle' diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/components/layout.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/components/layout.tsx index 3bcf0aab9a5c8..6edce74d162bb 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/components/layout.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/components/layout.tsx @@ -111,7 +111,7 @@ export const CreatePackagePolicyPageLayout: React.FunctionComponent<{ ) : ( ); }, [from]); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/index.tsx index a02214a6fe7fa..39f35fed56ef5 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/index.tsx @@ -242,7 +242,7 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { notifications.toasts.addSuccess({ title: i18n.translate('xpack.ingestManager.createPackagePolicy.addedNotificationTitle', { - defaultMessage: `Successfully added '{packagePolicyName}'`, + defaultMessage: `'{packagePolicyName}' integration added.`, values: { packagePolicyName: packagePolicy.name, }, @@ -250,7 +250,7 @@ export const CreatePackagePolicyPage: React.FunctionComponent = () => { text: agentCount && agentPolicy ? i18n.translate('xpack.ingestManager.createPackagePolicy.addedNotificationMessage', { - defaultMessage: `Fleet will deploy updates to all agents that use the '{agentPolicyName}' policy`, + defaultMessage: `Fleet will deploy updates to all agents that use the '{agentPolicyName}' policy.`, values: { agentPolicyName: agentPolicy.name, }, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/components/confirm_delete_modal.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/components/confirm_delete_modal.tsx index d2092f070a22a..a115e03a369a2 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/components/confirm_delete_modal.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/components/confirm_delete_modal.tsx @@ -21,7 +21,7 @@ export const ConfirmEnrollmentTokenDelete = (props: Props) => { { confirmButtonText={i18n.translate( 'xpack.ingestManager.enrollmentTokenDeleteModal.deleteButton', { - defaultMessage: 'Delete', + defaultMessage: 'Revoke enrollment token', } )} defaultFocusedButton="confirm" @@ -42,7 +42,8 @@ export const ConfirmEnrollmentTokenDelete = (props: Props) => { > = ({

diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx index d85a6e8b5b833..f447469a02df2 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx @@ -268,7 +268,7 @@ export const EnrollmentTokenListPage: React.FunctionComponent<{}> = () => { @@ -290,7 +290,7 @@ export const EnrollmentTokenListPage: React.FunctionComponent<{}> = () => { setFlyoutOpen(true)}> diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/setup_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/setup_page/index.tsx index eeade9036df00..fbd74f8b03e72 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/setup_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/setup_page/index.tsx @@ -95,8 +95,7 @@ export const SetupPage: React.FunctionComponent<{ diff --git a/x-pack/plugins/ingest_manager/server/services/agents/acks.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/acks.test.ts index 866aa587b8a56..c7b4098803827 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/acks.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/acks.test.ts @@ -57,7 +57,7 @@ describe('test agent acks services', () => { ); }); - it('should update config field on the agent if a policy change is acknowledged', async () => { + it('should update config field on the agent if a policy change is acknowledged with an agent without policy', async () => { const mockSavedObjectsClient = savedObjectsClientMock.create(); const actionAttributes = { @@ -116,6 +116,114 @@ describe('test agent acks services', () => { `); }); + it('should update config field on the agent if a policy change is acknowledged with a higher revision than the agent one', async () => { + const mockSavedObjectsClient = savedObjectsClientMock.create(); + + const actionAttributes = { + type: 'CONFIG_CHANGE', + policy_id: 'policy1', + policy_revision: 4, + sent_at: '2020-03-14T19:45:02.620Z', + timestamp: '2019-01-04T14:32:03.36764-05:00', + created_at: '2020-03-14T19:45:02.620Z', + ack_data: JSON.stringify({ packages: ['system'] }), + }; + + mockSavedObjectsClient.bulkGet.mockReturnValue( + Promise.resolve({ + saved_objects: [ + { + id: 'action2', + references: [], + type: AGENT_ACTION_SAVED_OBJECT_TYPE, + attributes: actionAttributes, + }, + ], + } as SavedObjectsBulkResponse) + ); + + await acknowledgeAgentActions( + mockSavedObjectsClient, + ({ + id: 'id', + type: AGENT_TYPE_PERMANENT, + policy_id: 'policy1', + policy_revision: 3, + } as unknown) as Agent, + [ + { + type: 'ACTION_RESULT', + subtype: 'CONFIG', + timestamp: '2019-01-04T14:32:03.36764-05:00', + action_id: 'action2', + agent_id: 'id', + } as AgentEvent, + ] + ); + expect(mockSavedObjectsClient.bulkUpdate).toBeCalled(); + expect(mockSavedObjectsClient.bulkUpdate.mock.calls[0][0]).toHaveLength(1); + expect(mockSavedObjectsClient.bulkUpdate.mock.calls[0][0][0]).toMatchInlineSnapshot(` + Object { + "attributes": Object { + "packages": Array [ + "system", + ], + "policy_revision": 4, + }, + "id": "id", + "type": "fleet-agents", + } + `); + }); + + it('should not update config field on the agent if a policy change is acknowledged with a lower revision than the agent one', async () => { + const mockSavedObjectsClient = savedObjectsClientMock.create(); + + const actionAttributes = { + type: 'CONFIG_CHANGE', + policy_id: 'policy1', + policy_revision: 4, + sent_at: '2020-03-14T19:45:02.620Z', + timestamp: '2019-01-04T14:32:03.36764-05:00', + created_at: '2020-03-14T19:45:02.620Z', + ack_data: JSON.stringify({ packages: ['system'] }), + }; + + mockSavedObjectsClient.bulkGet.mockReturnValue( + Promise.resolve({ + saved_objects: [ + { + id: 'action2', + references: [], + type: AGENT_ACTION_SAVED_OBJECT_TYPE, + attributes: actionAttributes, + }, + ], + } as SavedObjectsBulkResponse) + ); + + await acknowledgeAgentActions( + mockSavedObjectsClient, + ({ + id: 'id', + type: AGENT_TYPE_PERMANENT, + policy_id: 'policy1', + policy_revision: 5, + } as unknown) as Agent, + [ + { + type: 'ACTION_RESULT', + subtype: 'CONFIG', + timestamp: '2019-01-04T14:32:03.36764-05:00', + action_id: 'action2', + agent_id: 'id', + } as AgentEvent, + ] + ); + expect(mockSavedObjectsClient.bulkUpdate).toBeCalled(); + expect(mockSavedObjectsClient.bulkUpdate.mock.calls[0][0]).toHaveLength(0); + }); + it('should not update config field on the agent if a policy change for an old revision is acknowledged', async () => { const mockSavedObjectsClient = savedObjectsClientMock.create(); diff --git a/x-pack/plugins/ingest_manager/server/services/agents/acks.ts b/x-pack/plugins/ingest_manager/server/services/agents/acks.ts index d29dfcec7ef30..1392710eb0eff 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/acks.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/acks.ts @@ -139,16 +139,12 @@ function getLatestConfigChangePolicyActionIfUpdated( !isAgentPolicyAction(action) || action.type !== 'CONFIG_CHANGE' || action.policy_id !== agent.policy_id || - (acc?.policy_revision ?? 0) < (agent.policy_revision || 0) + (action?.policy_revision ?? 0) < (agent.policy_revision || 0) ) { return acc; } - if (action.policy_revision > (acc?.policy_revision ?? 0)) { - return action; - } - - return acc; + return action; }, null); } diff --git a/x-pack/plugins/searchprofiler/public/application/editor/editor.tsx b/x-pack/plugins/searchprofiler/public/application/editor/editor.tsx index 3141f5bedc8f9..7e7d74155b2d9 100644 --- a/x-pack/plugins/searchprofiler/public/application/editor/editor.tsx +++ b/x-pack/plugins/searchprofiler/public/application/editor/editor.tsx @@ -56,6 +56,12 @@ export const Editor = memo(({ licenseEnabled, initialValue, onEditorReady }: Pro setTextArea(licenseEnabled ? containerRef.current!.querySelector('textarea') : null); onEditorReady(createEditorShim(editorInstanceRef.current)); + + return () => { + if (editorInstanceRef.current) { + editorInstanceRef.current.destroy(); + } + }; }, [initialValue, onEditorReady, licenseEnabled]); return ( diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts index 63a57c20a8593..a39638e48892d 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/index.ts @@ -9,6 +9,7 @@ export * from './authentications'; export * from './common'; export * from './details'; export * from './first_last_seen'; +export * from './kpi'; export * from './overview'; export * from './uncommon_processes'; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/kpi/authentications/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/kpi/authentications/index.ts new file mode 100644 index 0000000000000..cbf1f32c3b5fa --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/kpi/authentications/index.ts @@ -0,0 +1,24 @@ +/* + * 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 { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; +import { Inspect, Maybe } from '../../../../common'; +import { RequestBasicOptions } from '../../..'; +import { HostsKpiHistogramData } from '../common'; + +export interface HostsKpiAuthenticationsHistogramCount { + doc_count: number; +} + +export type HostsKpiAuthenticationsRequestOptions = RequestBasicOptions; + +export interface HostsKpiAuthenticationsStrategyResponse extends IEsSearchResponse { + authenticationsSuccess: Maybe; + authenticationsSuccessHistogram: Maybe; + authenticationsFailure: Maybe; + authenticationsFailureHistogram: Maybe; + inspect?: Maybe; +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/kpi/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/kpi/common/index.ts new file mode 100644 index 0000000000000..52e65bb995796 --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/kpi/common/index.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* + * 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 { Maybe } from '../../../../common'; + +export interface HostsKpiHistogramData { + x?: Maybe; + y?: Maybe; +} + +export interface HostsKpiHistogram { + key_as_string: string; + key: number; + doc_count: number; + count: T; +} + +export interface HostsKpiGeneralHistogramCount { + value: number; +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/kpi/hosts/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/kpi/hosts/index.ts new file mode 100644 index 0000000000000..8e8bd97c9b60b --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/kpi/hosts/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; +import { Inspect, Maybe } from '../../../../common'; +import { RequestBasicOptions } from '../../..'; +import { HostsKpiHistogramData } from '../common'; + +export type HostsKpiHostsRequestOptions = RequestBasicOptions; + +export interface HostsKpiHostsStrategyResponse extends IEsSearchResponse { + hosts: Maybe; + hostsHistogram: Maybe; + inspect?: Maybe; +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/kpi/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/kpi/index.ts new file mode 100644 index 0000000000000..dc34f619e0362 --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/kpi/index.ts @@ -0,0 +1,25 @@ +/* + * 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. + */ + +export * from './authentications'; +export * from './common'; +export * from './hosts'; +export * from './unique_ips'; + +import { HostsKpiAuthenticationsStrategyResponse } from './authentications'; +import { HostsKpiHostsStrategyResponse } from './hosts'; +import { HostsKpiUniqueIpsStrategyResponse } from './unique_ips'; + +export enum HostsKpiQueries { + kpiAuthentications = 'hostsKpiAuthentications', + kpiHosts = 'hostsKpiHosts', + kpiUniqueIps = 'hostsKpiUniqueIps', +} + +export type HostsKpiStrategyResponse = + | Omit + | Omit + | Omit; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/kpi/unique_ips/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/kpi/unique_ips/index.ts new file mode 100644 index 0000000000000..18a603725f401 --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/kpi/unique_ips/index.ts @@ -0,0 +1,20 @@ +/* + * 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 { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; +import { Inspect, Maybe } from '../../../../common'; +import { RequestBasicOptions } from '../../..'; +import { HostsKpiHistogramData } from '../common'; + +export type HostsKpiUniqueIpsRequestOptions = RequestBasicOptions; + +export interface HostsKpiUniqueIpsStrategyResponse extends IEsSearchResponse { + uniqueSourceIps: Maybe; + uniqueSourceIpsHistogram: Maybe; + uniqueDestinationIps: Maybe; + uniqueDestinationIpsHistogram: Maybe; + inspect?: Maybe; +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts index 95f3cd4fd7da7..cfcf613b662bc 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts @@ -20,6 +20,13 @@ import { HostsStrategyResponse, HostUncommonProcessesStrategyResponse, HostUncommonProcessesRequestOptions, + HostsKpiQueries, + HostsKpiAuthenticationsStrategyResponse, + HostsKpiAuthenticationsRequestOptions, + HostsKpiHostsStrategyResponse, + HostsKpiHostsRequestOptions, + HostsKpiUniqueIpsStrategyResponse, + HostsKpiUniqueIpsRequestOptions, } from './hosts'; import { NetworkQueries, @@ -70,6 +77,7 @@ export * from './network'; export type FactoryQueryTypes = | HostsQueries + | HostsKpiQueries | NetworkQueries | NetworkKpiQueries | typeof MatrixHistogramQuery; @@ -106,6 +114,12 @@ export type StrategyResponseType = T extends HostsQ ? HostFirstLastSeenStrategyResponse : T extends HostsQueries.uncommonProcesses ? HostUncommonProcessesStrategyResponse + : T extends HostsKpiQueries.kpiAuthentications + ? HostsKpiAuthenticationsStrategyResponse + : T extends HostsKpiQueries.kpiHosts + ? HostsKpiHostsStrategyResponse + : T extends HostsKpiQueries.kpiUniqueIps + ? HostsKpiUniqueIpsStrategyResponse : T extends NetworkQueries.details ? NetworkDetailsStrategyResponse : T extends NetworkQueries.dns @@ -148,6 +162,12 @@ export type StrategyRequestType = T extends HostsQu ? HostFirstLastSeenRequestOptions : T extends HostsQueries.uncommonProcesses ? HostUncommonProcessesRequestOptions + : T extends HostsKpiQueries.kpiAuthentications + ? HostsKpiAuthenticationsRequestOptions + : T extends HostsKpiQueries.kpiHosts + ? HostsKpiHostsRequestOptions + : T extends HostsKpiQueries.kpiUniqueIps + ? HostsKpiUniqueIpsRequestOptions : T extends NetworkQueries.details ? NetworkDetailsRequestOptions : T extends NetworkQueries.dns diff --git a/x-pack/plugins/security_solution/public/common/components/stat_items/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/stat_items/index.test.tsx index 664d8b2ff5598..310d4c52ec5bc 100644 --- a/x-pack/plugins/security_solution/public/common/components/stat_items/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/stat_items/index.test.tsx @@ -39,8 +39,10 @@ import { } from '../../mock'; import { State, createStore } from '../../store'; import { Provider as ReduxStoreProvider } from 'react-redux'; -import { KpiHostsData } from '../../../graphql/types'; -import { NetworkKpiStrategyResponse } from '../../../../common/search_strategy'; +import { + HostsKpiStrategyResponse, + NetworkKpiStrategyResponse, +} from '../../../../common/search_strategy'; const from = '2019-06-15T06:00:00.000Z'; const to = '2019-06-18T06:00:00.000Z'; @@ -242,7 +244,7 @@ describe('useKpiMatrixStatus', () => { data, }: { fieldsMapping: Readonly; - data: NetworkKpiStrategyResponse | KpiHostsData; + data: NetworkKpiStrategyResponse | HostsKpiStrategyResponse; }) => { const statItemsProps: StatItemsProps[] = useKpiMatrixStatus( fieldsMapping, diff --git a/x-pack/plugins/security_solution/public/common/components/stat_items/index.tsx b/x-pack/plugins/security_solution/public/common/components/stat_items/index.tsx index 13a93a784a2c9..34fb344eed3c4 100644 --- a/x-pack/plugins/security_solution/public/common/components/stat_items/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/stat_items/index.tsx @@ -18,8 +18,10 @@ import { get, getOr } from 'lodash/fp'; import React, { useState, useEffect } from 'react'; import styled from 'styled-components'; -import { NetworkKpiStrategyResponse } from '../../../../common/search_strategy'; -import { KpiHostsData } from '../../../graphql/types'; +import { + HostsKpiStrategyResponse, + NetworkKpiStrategyResponse, +} from '../../../../common/search_strategy'; import { AreaChart } from '../charts/areachart'; import { BarChart } from '../charts/barchart'; import { ChartSeriesData, ChartData, ChartSeriesConfigs, UpdateDateRange } from '../charts/common'; @@ -113,12 +115,12 @@ export const barchartConfigs = (config?: { onElementClick?: ElementClickListener export const addValueToFields = ( fields: StatItem[], - data: KpiHostsData | NetworkKpiStrategyResponse + data: HostsKpiStrategyResponse | NetworkKpiStrategyResponse ): StatItem[] => fields.map((field) => ({ ...field, value: get(field.key, data) })); export const addValueToAreaChart = ( fields: StatItem[], - data: KpiHostsData | NetworkKpiStrategyResponse + data: HostsKpiStrategyResponse | NetworkKpiStrategyResponse ): ChartSeriesData[] => fields .filter((field) => get(`${field.key}Histogram`, data) != null) @@ -130,7 +132,7 @@ export const addValueToAreaChart = ( export const addValueToBarChart = ( fields: StatItem[], - data: KpiHostsData | NetworkKpiStrategyResponse + data: HostsKpiStrategyResponse | NetworkKpiStrategyResponse ): ChartSeriesData[] => { if (fields.length === 0) return []; return fields.reduce((acc: ChartSeriesData[], field: StatItem, idx: number) => { @@ -159,7 +161,7 @@ export const addValueToBarChart = ( export const useKpiMatrixStatus = ( mappings: Readonly, - data: KpiHostsData | NetworkKpiStrategyResponse, + data: HostsKpiStrategyResponse | NetworkKpiStrategyResponse, id: string, from: string, to: string, diff --git a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/translations.ts b/x-pack/plugins/security_solution/public/hosts/components/authentications_table/translations.ts index 0e918275e2b18..d437a6b73f71a 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/translations.ts +++ b/x-pack/plugins/security_solution/public/hosts/components/authentications_table/translations.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; export const AUTHENTICATIONS = i18n.translate( - 'xpack.securitySolution.authenticationsTable.authenticationFailures', + 'xpack.securitySolution.authenticationsTable.authentications', { defaultMessage: 'Authentications', } diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/__snapshots__/index.test.tsx.snap deleted file mode 100644 index 2b2a35945bdf1..0000000000000 --- a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/__snapshots__/index.test.tsx.snap +++ /dev/null @@ -1,155 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`kpiHostsComponent render it should render KpiHostDetailsData 1`] = ` - - - - -`; - -exports[`kpiHostsComponent render it should render KpiHostsData 1`] = ` - - - - - -`; - -exports[`kpiHostsComponent render it should render spinner if it is loading 1`] = ` - - - - - -`; diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/authentications/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/authentications/index.tsx new file mode 100644 index 0000000000000..0949616827470 --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/authentications/index.tsx @@ -0,0 +1,73 @@ +/* + * 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 { StatItems } from '../../../../common/components/stat_items'; +import { useHostsKpiAuthentications } from '../../../containers/kpi_hosts/authentications'; +import { HostsKpiBaseComponentManage } from '../common'; +import { HostsKpiProps, HostsKpiChartColors } from '../types'; +import * as i18n from './translations'; + +export const fieldsMapping: Readonly = [ + { + key: 'authentication', + fields: [ + { + key: 'authenticationsSuccess', + name: i18n.SUCCESS_CHART_LABEL, + description: i18n.SUCCESS_UNIT_LABEL, + value: null, + color: HostsKpiChartColors.authenticationsSuccess, + icon: 'check', + }, + { + key: 'authenticationsFailure', + name: i18n.FAIL_CHART_LABEL, + description: i18n.FAIL_UNIT_LABEL, + value: null, + color: HostsKpiChartColors.authenticationsFailure, + icon: 'cross', + }, + ], + enableAreaChart: true, + enableBarChart: true, + description: i18n.USER_AUTHENTICATIONS, + }, +]; + +const HostsKpiAuthenticationsComponent: React.FC = ({ + filterQuery, + from, + to, + narrowDateRange, + setQuery, + skip, +}) => { + const [loading, { refetch, id, inspect, ...data }] = useHostsKpiAuthentications({ + filterQuery, + endDate: to, + startDate: from, + skip, + }); + + return ( + + ); +}; + +export const HostsKpiAuthentications = React.memo(HostsKpiAuthenticationsComponent); diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/translations.ts b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/authentications/translations.ts similarity index 55% rename from x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/translations.ts rename to x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/authentications/translations.ts index 82543e6f106fa..5175781159c91 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/translations.ts +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/authentications/translations.ts @@ -3,11 +3,8 @@ * 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 const HOSTS = i18n.translate('xpack.securitySolution.kpiHosts.hosts.title', { - defaultMessage: 'Hosts', -}); +import { i18n } from '@kbn/i18n'; export const USER_AUTHENTICATIONS = i18n.translate( 'xpack.securitySolution.kpiHosts.userAuthentications.title', @@ -43,35 +40,3 @@ export const FAIL_CHART_LABEL = i18n.translate( defaultMessage: 'Fail', } ); - -export const UNIQUE_IPS = i18n.translate('xpack.securitySolution.kpiHosts.uniqueIps.title', { - defaultMessage: 'Unique IPs', -}); - -export const SOURCE_UNIT_LABEL = i18n.translate( - 'xpack.securitySolution.kpiHosts.uniqueIps.sourceUnitLabel', - { - defaultMessage: 'source', - } -); - -export const DESTINATION_UNIT_LABEL = i18n.translate( - 'xpack.securitySolution.kpiHosts.uniqueIps.destinationUnitLabel', - { - defaultMessage: 'destination', - } -); - -export const SOURCE_CHART_LABEL = i18n.translate( - 'xpack.securitySolution.kpiHosts.uniqueIps.sourceChartLabel', - { - defaultMessage: 'Src.', - } -); - -export const DESTINATION_CHART_LABEL = i18n.translate( - 'xpack.securitySolution.kpiHosts.uniqueIps.destinationChartLabel', - { - defaultMessage: 'Dest.', - } -); diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/common/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/common/index.tsx new file mode 100644 index 0000000000000..7c51a503092af --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/common/index.tsx @@ -0,0 +1,69 @@ +/* + * 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 { EuiFlexItem, EuiLoadingSpinner, EuiFlexGroup } from '@elastic/eui'; +import styled from 'styled-components'; + +import { manageQuery } from '../../../../common/components/page/manage_query'; +import { HostsKpiStrategyResponse } from '../../../../../common/search_strategy'; +import { + StatItemsComponent, + StatItemsProps, + useKpiMatrixStatus, + StatItems, +} from '../../../../common/components/stat_items'; +import { UpdateDateRange } from '../../../../common/components/charts/common'; + +const kpiWidgetHeight = 247; + +export const FlexGroup = styled(EuiFlexGroup)` + min-height: ${kpiWidgetHeight}px; +`; + +FlexGroup.displayName = 'FlexGroup'; + +export const HostsKpiBaseComponent = React.memo<{ + fieldsMapping: Readonly; + data: HostsKpiStrategyResponse; + loading?: boolean; + id: string; + from: string; + to: string; + narrowDateRange: UpdateDateRange; +}>(({ fieldsMapping, data, id, loading = false, from, to, narrowDateRange }) => { + const statItemsProps: StatItemsProps[] = useKpiMatrixStatus( + fieldsMapping, + data, + id, + from, + to, + narrowDateRange + ); + + if (loading) { + return ( + + + + + + ); + } + + return ( + + {statItemsProps.map((mappedStatItemProps) => ( + + ))} + + ); +}); + +HostsKpiBaseComponent.displayName = 'HostsKpiBaseComponent'; + +export const HostsKpiBaseComponentManage = manageQuery(HostsKpiBaseComponent); diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/hosts/index.tsx new file mode 100644 index 0000000000000..b1c4d6331e450 --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/hosts/index.tsx @@ -0,0 +1,62 @@ +/* + * 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 { StatItems } from '../../../../common/components/stat_items'; +import { useHostsKpiHosts } from '../../../containers/kpi_hosts/hosts'; +import { HostsKpiBaseComponentManage } from '../common'; +import { HostsKpiProps, HostsKpiChartColors } from '../types'; +import * as i18n from './translations'; + +export const fieldsMapping: Readonly = [ + { + key: 'hosts', + fields: [ + { + key: 'hosts', + value: null, + color: HostsKpiChartColors.hosts, + icon: 'storage', + }, + ], + enableAreaChart: true, + description: i18n.HOSTS, + }, +]; + +const HostsKpiHostsComponent: React.FC = ({ + filterQuery, + from, + to, + narrowDateRange, + setQuery, + skip, +}) => { + const [loading, { refetch, id, inspect, ...data }] = useHostsKpiHosts({ + filterQuery, + endDate: to, + startDate: from, + skip, + }); + + return ( + + ); +}; + +export const HostsKpiHosts = React.memo(HostsKpiHostsComponent); diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/hosts/translations.ts b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/hosts/translations.ts new file mode 100644 index 0000000000000..7754591ab415b --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/hosts/translations.ts @@ -0,0 +1,11 @@ +/* + * 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 const HOSTS = i18n.translate('xpack.securitySolution.kpiHosts.hosts.title', { + defaultMessage: 'Hosts', +}); diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/index.test.tsx deleted file mode 100644 index 7731881df6d2c..0000000000000 --- a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/index.test.tsx +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { mockKpiHostsData, mockKpiHostDetailsData } from './mock'; -import React from 'react'; -import { shallow, ShallowWrapper } from 'enzyme'; -import '../../../common/mock/match_media'; -import { KpiHostsComponentBase } from '.'; -import * as statItems from '../../../common/components/stat_items'; -import { kpiHostsMapping } from './kpi_hosts_mapping'; -import { kpiHostDetailsMapping } from './kpi_host_details_mapping'; - -describe('kpiHostsComponent', () => { - const ID = 'kpiHost'; - const from = '2019-06-15T06:00:00.000Z'; - const to = '2019-06-18T06:00:00.000Z'; - const narrowDateRange = () => {}; - describe('render', () => { - test('it should render spinner if it is loading', () => { - const wrapper: ShallowWrapper = shallow( - - ); - expect(wrapper).toMatchSnapshot(); - }); - - test('it should render KpiHostsData', () => { - const wrapper: ShallowWrapper = shallow( - - ); - expect(wrapper).toMatchSnapshot(); - }); - - test('it should render KpiHostDetailsData', () => { - const wrapper: ShallowWrapper = shallow( - - ); - expect(wrapper).toMatchSnapshot(); - }); - }); - - const table = [ - [mockKpiHostsData, kpiHostsMapping] as [typeof mockKpiHostsData, typeof kpiHostsMapping], - [mockKpiHostDetailsData, kpiHostDetailsMapping] as [ - typeof mockKpiHostDetailsData, - typeof kpiHostDetailsMapping - ], - ]; - - describe.each(table)( - 'it should handle KpiHostsProps and KpiHostDetailsProps', - (data, mapping) => { - let mockUseKpiMatrixStatus: jest.SpyInstance; - beforeAll(() => { - mockUseKpiMatrixStatus = jest.spyOn(statItems, 'useKpiMatrixStatus'); - }); - - beforeEach(() => { - shallow( - - ); - }); - - afterEach(() => { - mockUseKpiMatrixStatus.mockClear(); - }); - - afterAll(() => { - mockUseKpiMatrixStatus.mockRestore(); - }); - - test(`it should apply correct mapping by given data type`, () => { - expect(mockUseKpiMatrixStatus).toBeCalledWith(mapping, data, ID, from, to, narrowDateRange); - }); - } - ); -}); diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/index.tsx index c39e86591013f..fff4c64900a8b 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/index.tsx @@ -4,81 +4,78 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; import React from 'react'; -import styled from 'styled-components'; +import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; -import { KpiHostsData, KpiHostDetailsData } from '../../../graphql/types'; -import { - StatItemsComponent, - StatItemsProps, - useKpiMatrixStatus, -} from '../../../common/components/stat_items'; -import { kpiHostsMapping } from './kpi_hosts_mapping'; -import { kpiHostDetailsMapping } from './kpi_host_details_mapping'; -import { UpdateDateRange } from '../../../common/components/charts/common'; +import { HostsKpiAuthentications } from './authentications'; +import { HostsKpiHosts } from './hosts'; +import { HostsKpiUniqueIps } from './unique_ips'; +import { HostsKpiProps } from './types'; -const kpiWidgetHeight = 247; - -interface GenericKpiHostProps { - from: string; - id: string; - loading: boolean; - to: string; - narrowDateRange: UpdateDateRange; -} - -interface KpiHostsProps extends GenericKpiHostProps { - data: KpiHostsData; -} - -interface KpiHostDetailsProps extends GenericKpiHostProps { - data: KpiHostDetailsData; -} - -const FlexGroupSpinner = styled(EuiFlexGroup)` - { - min-height: ${kpiWidgetHeight}px; - } -`; - -FlexGroupSpinner.displayName = 'FlexGroupSpinner'; - -export const KpiHostsComponentBase = ({ - data, - from, - loading, - id, - to, - narrowDateRange, -}: KpiHostsProps | KpiHostDetailsProps) => { - const mappings = - (data as KpiHostsData).hosts !== undefined ? kpiHostsMapping : kpiHostDetailsMapping; - const statItemsProps: StatItemsProps[] = useKpiMatrixStatus( - mappings, - data, - id, - from, - to, - narrowDateRange - ); - return loading ? ( - - - +export const HostsKpiComponent = React.memo( + ({ filterQuery, from, to, setQuery, skip, narrowDateRange }) => ( + + + + + + + + + - - ) : ( - - {statItemsProps.map((mappedStatItemProps, idx) => { - return ; - })} - ); -}; + ) +); -KpiHostsComponentBase.displayName = 'KpiHostsComponentBase'; +HostsKpiComponent.displayName = 'HostsKpiComponent'; -export const KpiHostsComponent = React.memo(KpiHostsComponentBase); +export const HostsDetailsKpiComponent = React.memo( + ({ filterQuery, from, to, setQuery, skip, narrowDateRange }) => ( + + + + + + + + + ) +); -KpiHostsComponent.displayName = 'KpiHostsComponent'; +HostsDetailsKpiComponent.displayName = 'HostsDetailsKpiComponent'; diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/kpi_host_details_mapping.ts b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/kpi_host_details_mapping.ts deleted file mode 100644 index b3e98b70c4cb0..0000000000000 --- a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/kpi_host_details_mapping.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import * as i18n from './translations'; -import { StatItems } from '../../../common/components/stat_items'; -import { KpiHostsChartColors } from './types'; - -export const kpiHostDetailsMapping: Readonly = [ - { - key: 'authentication', - index: 0, - fields: [ - { - key: 'authSuccess', - name: i18n.SUCCESS_CHART_LABEL, - description: i18n.SUCCESS_UNIT_LABEL, - value: null, - color: KpiHostsChartColors.authSuccess, - icon: 'check', - }, - { - key: 'authFailure', - name: i18n.FAIL_CHART_LABEL, - description: i18n.FAIL_UNIT_LABEL, - value: null, - color: KpiHostsChartColors.authFailure, - icon: 'cross', - }, - ], - enableAreaChart: true, - enableBarChart: true, - grow: 1, - description: i18n.USER_AUTHENTICATIONS, - }, - { - key: 'uniqueIps', - index: 1, - fields: [ - { - key: 'uniqueSourceIps', - name: i18n.SOURCE_CHART_LABEL, - description: i18n.SOURCE_UNIT_LABEL, - value: null, - color: KpiHostsChartColors.uniqueSourceIps, - icon: 'visMapCoordinate', - }, - { - key: 'uniqueDestinationIps', - name: i18n.DESTINATION_CHART_LABEL, - description: i18n.DESTINATION_UNIT_LABEL, - value: null, - color: KpiHostsChartColors.uniqueDestinationIps, - icon: 'visMapCoordinate', - }, - ], - enableAreaChart: true, - enableBarChart: true, - grow: 1, - description: i18n.UNIQUE_IPS, - }, -]; diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/kpi_hosts_mapping.ts b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/kpi_hosts_mapping.ts deleted file mode 100644 index 78a9fd5b84d1f..0000000000000 --- a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/kpi_hosts_mapping.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import * as i18n from './translations'; -import { KpiHostsChartColors } from './types'; -import { StatItems } from '../../../common/components/stat_items'; - -export const kpiHostsMapping: Readonly = [ - { - key: 'hosts', - index: 0, - fields: [ - { - key: 'hosts', - value: null, - color: KpiHostsChartColors.hosts, - icon: 'storage', - }, - ], - enableAreaChart: true, - grow: 2, - description: i18n.HOSTS, - }, - { - key: 'authentication', - index: 1, - fields: [ - { - key: 'authSuccess', - name: i18n.SUCCESS_CHART_LABEL, - description: i18n.SUCCESS_UNIT_LABEL, - value: null, - color: KpiHostsChartColors.authSuccess, - icon: 'check', - }, - { - key: 'authFailure', - name: i18n.FAIL_CHART_LABEL, - description: i18n.FAIL_UNIT_LABEL, - value: null, - color: KpiHostsChartColors.authFailure, - icon: 'cross', - }, - ], - enableAreaChart: true, - enableBarChart: true, - grow: 4, - description: i18n.USER_AUTHENTICATIONS, - }, - { - key: 'uniqueIps', - index: 2, - fields: [ - { - key: 'uniqueSourceIps', - name: i18n.SOURCE_CHART_LABEL, - description: i18n.SOURCE_UNIT_LABEL, - value: null, - color: KpiHostsChartColors.uniqueSourceIps, - icon: 'visMapCoordinate', - }, - { - key: 'uniqueDestinationIps', - name: i18n.DESTINATION_CHART_LABEL, - description: i18n.DESTINATION_UNIT_LABEL, - value: null, - color: KpiHostsChartColors.uniqueDestinationIps, - icon: 'visMapCoordinate', - }, - ], - enableAreaChart: true, - enableBarChart: true, - grow: 4, - description: i18n.UNIQUE_IPS, - }, -]; diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/mock.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/mock.tsx deleted file mode 100644 index a1d081af20435..0000000000000 --- a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/mock.tsx +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const mockKpiHostsData = { - hosts: 986, - hostsHistogram: [ - { - x: new Date('2019-05-03T13:00:00.000Z').valueOf(), - y: 919, - }, - { - x: new Date('2019-05-04T01:00:00.000Z').valueOf(), - y: 82, - }, - { - x: new Date('2019-05-04T13:00:00.000Z').valueOf(), - y: 4, - }, - ], - authSuccess: 61, - authSuccessHistogram: [ - { - x: new Date('2019-05-03T13:00:00.000Z').valueOf(), - y: 8, - }, - { - x: new Date('2019-05-04T01:00:00.000Z').valueOf(), - y: 52, - }, - { - x: new Date('2019-05-04T13:00:00.000Z').valueOf(), - y: 1, - }, - ], - authFailure: 15722, - authFailureHistogram: [ - { - x: new Date('2019-05-03T13:00:00.000Z').valueOf(), - y: 11731, - }, - { - x: new Date('2019-05-04T01:00:00.000Z').valueOf(), - y: 3979, - }, - { - x: new Date('2019-05-04T13:00:00.000Z').valueOf(), - y: 12, - }, - ], - uniqueSourceIps: 1407, - uniqueSourceIpsHistogram: [ - { - x: new Date('2019-05-03T13:00:00.000Z').valueOf(), - y: 1182, - }, - { - x: new Date('2019-05-04T01:00:00.000Z').valueOf(), - y: 364, - }, - { - x: new Date('2019-05-04T13:00:00.000Z').valueOf(), - y: 63, - }, - ], - uniqueDestinationIps: 1954, - uniqueDestinationIpsHistogram: [ - { - x: new Date('2019-05-03T13:00:00.000Z').valueOf(), - y: 1809, - }, - { - x: new Date('2019-05-04T01:00:00.000Z').valueOf(), - y: 407, - }, - { - x: new Date('2019-05-04T13:00:00.000Z').valueOf(), - y: 64, - }, - ], -}; -export const mockKpiHostDetailsData = { - authSuccess: 61, - authSuccessHistogram: [ - { - x: new Date('2019-05-03T13:00:00.000Z').valueOf(), - y: 8, - }, - { - x: new Date('2019-05-04T01:00:00.000Z').valueOf(), - y: 52, - }, - { - x: new Date('2019-05-04T13:00:00.000Z').valueOf(), - y: 1, - }, - ], - authFailure: 15722, - authFailureHistogram: [ - { - x: new Date('2019-05-03T13:00:00.000Z').valueOf(), - y: 11731, - }, - { - x: new Date('2019-05-04T01:00:00.000Z').valueOf(), - y: 3979, - }, - { - x: new Date('2019-05-04T13:00:00.000Z').valueOf(), - y: 12, - }, - ], - uniqueSourceIps: 1407, - uniqueSourceIpsHistogram: [ - { - x: new Date('2019-05-03T13:00:00.000Z').valueOf(), - y: 1182, - }, - { - x: new Date('2019-05-04T01:00:00.000Z').valueOf(), - y: 364, - }, - { - x: new Date('2019-05-04T13:00:00.000Z').valueOf(), - y: 63, - }, - ], - uniqueDestinationIps: 1954, - uniqueDestinationIpsHistogram: [ - { - x: new Date('2019-05-03T13:00:00.000Z').valueOf(), - y: 1809, - }, - { - x: new Date('2019-05-04T01:00:00.000Z').valueOf(), - y: 407, - }, - { - x: new Date('2019-05-04T13:00:00.000Z').valueOf(), - y: 64, - }, - ], -}; diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/types.ts b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/types.ts index fd48368124795..7cdbb16ee348c 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/types.ts +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/types.ts @@ -4,9 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -export enum KpiHostsChartColors { - authSuccess = '#54B399', - authFailure = '#E7664C', +import { UpdateDateRange } from '../../../common/components/charts/common'; +import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; + +export interface HostsKpiProps { + filterQuery: string; + from: string; + to: string; + narrowDateRange: UpdateDateRange; + setQuery: GlobalTimeArgs['setQuery']; + skip: boolean; +} + +export enum HostsKpiChartColors { + authenticationsSuccess = '#54B399', + authenticationsFailure = '#E7664C', uniqueSourceIps = '#D36086', uniqueDestinationIps = '#9170B8', hosts = '#6092C0', diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/unique_ips/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/unique_ips/index.tsx new file mode 100644 index 0000000000000..c6f430faacccb --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/unique_ips/index.tsx @@ -0,0 +1,73 @@ +/* + * 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 { StatItems } from '../../../../common/components/stat_items'; +import { useHostsKpiUniqueIps } from '../../../containers/kpi_hosts/unique_ips'; +import { HostsKpiBaseComponentManage } from '../common'; +import { HostsKpiProps, HostsKpiChartColors } from '../types'; +import * as i18n from './translations'; + +export const fieldsMapping: Readonly = [ + { + key: 'uniqueIps', + fields: [ + { + key: 'uniqueSourceIps', + name: i18n.SOURCE_CHART_LABEL, + description: i18n.SOURCE_UNIT_LABEL, + value: null, + color: HostsKpiChartColors.uniqueSourceIps, + icon: 'visMapCoordinate', + }, + { + key: 'uniqueDestinationIps', + name: i18n.DESTINATION_CHART_LABEL, + description: i18n.DESTINATION_UNIT_LABEL, + value: null, + color: HostsKpiChartColors.uniqueDestinationIps, + icon: 'visMapCoordinate', + }, + ], + enableAreaChart: true, + enableBarChart: true, + description: i18n.UNIQUE_IPS, + }, +]; + +const HostsKpiUniqueIpsComponent: React.FC = ({ + filterQuery, + from, + to, + narrowDateRange, + setQuery, + skip, +}) => { + const [loading, { refetch, id, inspect, ...data }] = useHostsKpiUniqueIps({ + filterQuery, + endDate: to, + startDate: from, + skip, + }); + + return ( + + ); +}; + +export const HostsKpiUniqueIps = React.memo(HostsKpiUniqueIpsComponent); diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/unique_ips/translations.ts b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/unique_ips/translations.ts new file mode 100644 index 0000000000000..6cc651880be7b --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/unique_ips/translations.ts @@ -0,0 +1,39 @@ +/* + * 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 const UNIQUE_IPS = i18n.translate('xpack.securitySolution.kpiHosts.uniqueIps.title', { + defaultMessage: 'Unique IPs', +}); + +export const SOURCE_UNIT_LABEL = i18n.translate( + 'xpack.securitySolution.kpiHosts.uniqueIps.sourceUnitLabel', + { + defaultMessage: 'source', + } +); + +export const DESTINATION_UNIT_LABEL = i18n.translate( + 'xpack.securitySolution.kpiHosts.uniqueIps.destinationUnitLabel', + { + defaultMessage: 'destination', + } +); + +export const SOURCE_CHART_LABEL = i18n.translate( + 'xpack.securitySolution.kpiHosts.uniqueIps.sourceChartLabel', + { + defaultMessage: 'Src.', + } +); + +export const DESTINATION_CHART_LABEL = i18n.translate( + 'xpack.securitySolution.kpiHosts.uniqueIps.destinationChartLabel', + { + defaultMessage: 'Dest.', + } +); diff --git a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/authentications/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/authentications/index.tsx new file mode 100644 index 0000000000000..0d90b73e0a584 --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/authentications/index.tsx @@ -0,0 +1,170 @@ +/* + * 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 deepEqual from 'fast-deep-equal'; +import { noop } from 'lodash/fp'; +import { useCallback, useEffect, useRef, useState } from 'react'; + +import { DEFAULT_INDEX_KEY } from '../../../../../common/constants'; +import { inputsModel } from '../../../../common/store'; +import { createFilter } from '../../../../common/containers/helpers'; +import { useKibana } from '../../../../common/lib/kibana'; +import { + HostsKpiQueries, + HostsKpiAuthenticationsRequestOptions, + HostsKpiAuthenticationsStrategyResponse, +} from '../../../../../common/search_strategy'; +import { ESTermQuery } from '../../../../../common/typed_json'; + +import * as i18n from './translations'; +import { AbortError } from '../../../../../../../../src/plugins/data/common'; +import { getInspectResponse } from '../../../../helpers'; +import { InspectResponse } from '../../../../types'; + +const ID = 'hostsKpiAuthenticationsQuery'; + +export interface HostsKpiAuthenticationsArgs + extends Omit { + id: string; + inspect: InspectResponse; + isInspected: boolean; + refetch: inputsModel.Refetch; +} + +interface UseHostsKpiAuthentications { + filterQuery?: ESTermQuery | string; + endDate: string; + skip?: boolean; + startDate: string; +} + +export const useHostsKpiAuthentications = ({ + filterQuery, + endDate, + skip = false, + startDate, +}: UseHostsKpiAuthentications): [boolean, HostsKpiAuthenticationsArgs] => { + const { data, notifications, uiSettings } = useKibana().services; + const refetch = useRef(noop); + const abortCtrl = useRef(new AbortController()); + const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY); + const [loading, setLoading] = useState(false); + const [hostsKpiAuthenticationsRequest, setHostsKpiAuthenticationsRequest] = useState< + HostsKpiAuthenticationsRequestOptions + >({ + defaultIndex, + factoryQueryType: HostsKpiQueries.kpiAuthentications, + filterQuery: createFilter(filterQuery), + id: ID, + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + }); + + const [hostsKpiAuthenticationsResponse, setHostsKpiAuthenticationsResponse] = useState< + HostsKpiAuthenticationsArgs + >({ + authenticationsSuccess: 0, + authenticationsSuccessHistogram: [], + authenticationsFailure: 0, + authenticationsFailureHistogram: [], + id: ID, + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + refetch: refetch.current, + }); + + const hostsKpiAuthenticationsSearch = useCallback( + (request: HostsKpiAuthenticationsRequestOptions) => { + let didCancel = false; + const asyncSearch = async () => { + abortCtrl.current = new AbortController(); + setLoading(true); + + const searchSubscription$ = data.search + .search( + request, + { + strategy: 'securitySolutionSearchStrategy', + abortSignal: abortCtrl.current.signal, + } + ) + .subscribe({ + next: (response) => { + if (!response.isPartial && !response.isRunning) { + if (!didCancel) { + setLoading(false); + setHostsKpiAuthenticationsResponse((prevResponse) => ({ + ...prevResponse, + authenticationsSuccess: response.authenticationsSuccess, + authenticationsSuccessHistogram: response.authenticationsSuccessHistogram, + authenticationsFailure: response.authenticationsFailure, + authenticationsFailureHistogram: response.authenticationsFailureHistogram, + inspect: getInspectResponse(response, prevResponse.inspect), + refetch: refetch.current, + })); + } + searchSubscription$.unsubscribe(); + } else if (response.isPartial && !response.isRunning) { + if (!didCancel) { + setLoading(false); + } + // TODO: Make response error status clearer + notifications.toasts.addWarning(i18n.ERROR_HOSTS_KPI_AUTHENTICATIONS); + searchSubscription$.unsubscribe(); + } + }, + error: (msg) => { + if (!(msg instanceof AbortError)) { + notifications.toasts.addDanger({ + title: i18n.FAIL_HOSTS_KPI_AUTHENTICATIONS, + text: msg.message, + }); + } + }, + }); + }; + abortCtrl.current.abort(); + asyncSearch(); + refetch.current = asyncSearch; + return () => { + didCancel = true; + abortCtrl.current.abort(); + }; + }, + [data.search, notifications.toasts] + ); + + useEffect(() => { + setHostsKpiAuthenticationsRequest((prevRequest) => { + const myRequest = { + ...prevRequest, + defaultIndex, + filterQuery: createFilter(filterQuery), + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + }; + if (!skip && !deepEqual(prevRequest, myRequest)) { + return myRequest; + } + return prevRequest; + }); + }, [defaultIndex, endDate, filterQuery, skip, startDate]); + + useEffect(() => { + hostsKpiAuthenticationsSearch(hostsKpiAuthenticationsRequest); + }, [hostsKpiAuthenticationsRequest, hostsKpiAuthenticationsSearch]); + + return [loading, hostsKpiAuthenticationsResponse]; +}; diff --git a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/authentications/translations.ts b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/authentications/translations.ts new file mode 100644 index 0000000000000..fb5af83d0acef --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/authentications/translations.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const ERROR_HOSTS_KPI_AUTHENTICATIONS = i18n.translate( + 'xpack.securitySolution.hostsKpiAuthentications.errorSearchDescription', + { + defaultMessage: `An error has occurred on hosts kpi authentications search`, + } +); + +export const FAIL_HOSTS_KPI_AUTHENTICATIONS = i18n.translate( + 'xpack.securitySolution.hostsKpiAuthentications.failSearchDescription', + { + defaultMessage: `Failed to run search on hosts kpi authentications`, + } +); diff --git a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/hosts/index.tsx new file mode 100644 index 0000000000000..190ce1aa7eae1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/hosts/index.tsx @@ -0,0 +1,158 @@ +/* + * 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 deepEqual from 'fast-deep-equal'; +import { noop } from 'lodash/fp'; +import { useCallback, useEffect, useRef, useState } from 'react'; + +import { DEFAULT_INDEX_KEY } from '../../../../../common/constants'; +import { inputsModel } from '../../../../common/store'; +import { createFilter } from '../../../../common/containers/helpers'; +import { useKibana } from '../../../../common/lib/kibana'; +import { + HostsKpiQueries, + HostsKpiHostsRequestOptions, + HostsKpiHostsStrategyResponse, +} from '../../../../../common/search_strategy'; +import { ESTermQuery } from '../../../../../common/typed_json'; + +import * as i18n from './translations'; +import { AbortError } from '../../../../../../../../src/plugins/data/common'; +import { getInspectResponse } from '../../../../helpers'; +import { InspectResponse } from '../../../../types'; + +const ID = 'hostsKpiHostsQuery'; + +export interface HostsKpiHostsArgs extends Omit { + id: string; + inspect: InspectResponse; + isInspected: boolean; + refetch: inputsModel.Refetch; +} + +interface UseHostsKpiHosts { + filterQuery?: ESTermQuery | string; + endDate: string; + skip?: boolean; + startDate: string; +} + +export const useHostsKpiHosts = ({ + filterQuery, + endDate, + skip = false, + startDate, +}: UseHostsKpiHosts): [boolean, HostsKpiHostsArgs] => { + const { data, notifications, uiSettings } = useKibana().services; + const refetch = useRef(noop); + const abortCtrl = useRef(new AbortController()); + const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY); + const [loading, setLoading] = useState(false); + const [hostsKpiHostsRequest, setHostsKpiHostsRequest] = useState({ + defaultIndex, + factoryQueryType: HostsKpiQueries.kpiHosts, + filterQuery: createFilter(filterQuery), + id: ID, + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + }); + + const [hostsKpiHostsResponse, setHostsKpiHostsResponse] = useState({ + hosts: 0, + hostsHistogram: [], + id: ID, + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + refetch: refetch.current, + }); + + const hostsKpiHostsSearch = useCallback( + (request: HostsKpiHostsRequestOptions) => { + let didCancel = false; + const asyncSearch = async () => { + abortCtrl.current = new AbortController(); + setLoading(true); + + const searchSubscription$ = data.search + .search(request, { + strategy: 'securitySolutionSearchStrategy', + abortSignal: abortCtrl.current.signal, + }) + .subscribe({ + next: (response) => { + if (!response.isPartial && !response.isRunning) { + if (!didCancel) { + setLoading(false); + setHostsKpiHostsResponse((prevResponse) => ({ + ...prevResponse, + hosts: response.hosts, + hostsHistogram: response.hostsHistogram, + inspect: getInspectResponse(response, prevResponse.inspect), + refetch: refetch.current, + })); + } + searchSubscription$.unsubscribe(); + } else if (response.isPartial && !response.isRunning) { + if (!didCancel) { + setLoading(false); + } + // TODO: Make response error status clearer + notifications.toasts.addWarning(i18n.ERROR_HOSTS_KPI_HOSTS); + searchSubscription$.unsubscribe(); + } + }, + error: (msg) => { + if (!(msg instanceof AbortError)) { + notifications.toasts.addDanger({ + title: i18n.FAIL_HOSTS_KPI_HOSTS, + text: msg.message, + }); + } + }, + }); + }; + abortCtrl.current.abort(); + asyncSearch(); + refetch.current = asyncSearch; + return () => { + didCancel = true; + abortCtrl.current.abort(); + }; + }, + [data.search, notifications.toasts] + ); + + useEffect(() => { + setHostsKpiHostsRequest((prevRequest) => { + const myRequest = { + ...prevRequest, + defaultIndex, + filterQuery: createFilter(filterQuery), + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + }; + if (!skip && !deepEqual(prevRequest, myRequest)) { + return myRequest; + } + return prevRequest; + }); + }, [defaultIndex, endDate, filterQuery, skip, startDate]); + + useEffect(() => { + hostsKpiHostsSearch(hostsKpiHostsRequest); + }, [hostsKpiHostsRequest, hostsKpiHostsSearch]); + + return [loading, hostsKpiHostsResponse]; +}; diff --git a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/hosts/translations.ts b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/hosts/translations.ts new file mode 100644 index 0000000000000..2a15563a4b1cd --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/hosts/translations.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const ERROR_HOSTS_KPI_HOSTS = i18n.translate( + 'xpack.securitySolution.hostsKpiHosts.errorSearchDescription', + { + defaultMessage: `An error has occurred on hosts kpi hosts search`, + } +); + +export const FAIL_HOSTS_KPI_HOSTS = i18n.translate( + 'xpack.securitySolution.hostsKpiHosts.failSearchDescription', + { + defaultMessage: `Failed to run search on hosts kpi hosts`, + } +); diff --git a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/index.tsx index 1a6df58f04597..c0ae767219aae 100644 --- a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/index.tsx @@ -4,82 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect, ConnectedProps } from 'react-redux'; - -import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; -import { GetKpiHostsQuery, KpiHostsData } from '../../../graphql/types'; -import { inputsModel, inputsSelectors, State } from '../../../common/store'; -import { useUiSetting } from '../../../common/lib/kibana'; -import { createFilter, getDefaultFetchPolicy } from '../../../common/containers/helpers'; -import { QueryTemplateProps } from '../../../common/containers/query_template'; - -import { kpiHostsQuery } from './index.gql_query'; - -const ID = 'kpiHostsQuery'; - -export interface KpiHostsArgs { - id: string; - inspect: inputsModel.InspectQuery; - kpiHosts: KpiHostsData; - loading: boolean; - refetch: inputsModel.Refetch; -} - -export interface KpiHostsProps extends QueryTemplateProps { - children: (args: KpiHostsArgs) => React.ReactNode; -} - -const KpiHostsComponentQuery = React.memo( - ({ id = ID, children, endDate, filterQuery, isInspected, skip, sourceId, startDate }) => ( - - query={kpiHostsQuery} - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - skip={skip} - variables={{ - sourceId, - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - filterQuery: createFilter(filterQuery), - defaultIndex: useUiSetting(DEFAULT_INDEX_KEY), - inspect: isInspected, - }} - > - {({ data, loading, refetch }) => { - const kpiHosts = getOr({}, `source.KpiHosts`, data); - return children({ - id, - inspect: getOr(null, 'source.KpiHosts.inspect', data), - kpiHosts, - loading, - refetch, - }); - }} - - ) -); - -KpiHostsComponentQuery.displayName = 'KpiHostsComponentQuery'; - -const makeMapStateToProps = () => { - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { id = ID }: KpiHostsProps) => { - const { isInspected } = getQuery(state, id); - return { - isInspected, - }; - }; - return mapStateToProps; -}; - -const connector = connect(makeMapStateToProps); - -type PropsFromRedux = ConnectedProps; - -export const KpiHostsQuery = connector(KpiHostsComponentQuery); +export * from './authentications'; +export * from './hosts'; +export * from './unique_ips'; diff --git a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/unique_ips/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/unique_ips/index.tsx new file mode 100644 index 0000000000000..ac5cc12807f00 --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/unique_ips/index.tsx @@ -0,0 +1,167 @@ +/* + * 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 deepEqual from 'fast-deep-equal'; +import { noop } from 'lodash/fp'; +import { useCallback, useEffect, useRef, useState } from 'react'; + +import { DEFAULT_INDEX_KEY } from '../../../../../common/constants'; +import { inputsModel } from '../../../../common/store'; +import { createFilter } from '../../../../common/containers/helpers'; +import { useKibana } from '../../../../common/lib/kibana'; +import { + HostsKpiQueries, + HostsKpiUniqueIpsRequestOptions, + HostsKpiUniqueIpsStrategyResponse, +} from '../../../../../common/search_strategy'; +import { ESTermQuery } from '../../../../../common/typed_json'; + +import * as i18n from './translations'; +import { AbortError } from '../../../../../../../../src/plugins/data/common'; +import { getInspectResponse } from '../../../../helpers'; +import { InspectResponse } from '../../../../types'; + +const ID = 'hostsKpiUniqueIpsQuery'; + +export interface HostsKpiUniqueIpsArgs + extends Omit { + id: string; + inspect: InspectResponse; + isInspected: boolean; + refetch: inputsModel.Refetch; +} + +interface UseHostsKpiUniqueIps { + filterQuery?: ESTermQuery | string; + endDate: string; + skip?: boolean; + startDate: string; +} + +export const useHostsKpiUniqueIps = ({ + filterQuery, + endDate, + skip = false, + startDate, +}: UseHostsKpiUniqueIps): [boolean, HostsKpiUniqueIpsArgs] => { + const { data, notifications, uiSettings } = useKibana().services; + const refetch = useRef(noop); + const abortCtrl = useRef(new AbortController()); + const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY); + const [loading, setLoading] = useState(false); + const [hostsKpiUniqueIpsRequest, setHostsKpiUniqueIpsRequest] = useState< + HostsKpiUniqueIpsRequestOptions + >({ + defaultIndex, + factoryQueryType: HostsKpiQueries.kpiUniqueIps, + filterQuery: createFilter(filterQuery), + id: ID, + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + }); + + const [hostsKpiUniqueIpsResponse, setHostsKpiUniqueIpsResponse] = useState( + { + uniqueSourceIps: 0, + uniqueSourceIpsHistogram: [], + uniqueDestinationIps: 0, + uniqueDestinationIpsHistogram: [], + id: ID, + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + refetch: refetch.current, + } + ); + + const hostsKpiUniqueIpsSearch = useCallback( + (request: HostsKpiUniqueIpsRequestOptions) => { + let didCancel = false; + const asyncSearch = async () => { + abortCtrl.current = new AbortController(); + setLoading(true); + + const searchSubscription$ = data.search + .search(request, { + strategy: 'securitySolutionSearchStrategy', + abortSignal: abortCtrl.current.signal, + }) + .subscribe({ + next: (response) => { + if (!response.isPartial && !response.isRunning) { + if (!didCancel) { + setLoading(false); + setHostsKpiUniqueIpsResponse((prevResponse) => ({ + ...prevResponse, + uniqueSourceIps: response.uniqueSourceIps, + uniqueSourceIpsHistogram: response.uniqueSourceIpsHistogram, + uniqueDestinationIps: response.uniqueDestinationIps, + uniqueDestinationIpsHistogram: response.uniqueDestinationIpsHistogram, + inspect: getInspectResponse(response, prevResponse.inspect), + refetch: refetch.current, + })); + } + searchSubscription$.unsubscribe(); + } else if (response.isPartial && !response.isRunning) { + if (!didCancel) { + setLoading(false); + } + // TODO: Make response error status clearer + notifications.toasts.addWarning(i18n.ERROR_HOSTS_KPI_UNIQUE_IPS); + searchSubscription$.unsubscribe(); + } + }, + error: (msg) => { + if (!(msg instanceof AbortError)) { + notifications.toasts.addDanger({ + title: i18n.FAIL_HOSTS_KPI_UNIQUE_IPS, + text: msg.message, + }); + } + }, + }); + }; + abortCtrl.current.abort(); + asyncSearch(); + refetch.current = asyncSearch; + return () => { + didCancel = true; + abortCtrl.current.abort(); + }; + }, + [data.search, notifications.toasts] + ); + + useEffect(() => { + setHostsKpiUniqueIpsRequest((prevRequest) => { + const myRequest = { + ...prevRequest, + defaultIndex, + filterQuery: createFilter(filterQuery), + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + }; + if (!skip && !deepEqual(prevRequest, myRequest)) { + return myRequest; + } + return prevRequest; + }); + }, [defaultIndex, endDate, filterQuery, skip, startDate]); + + useEffect(() => { + hostsKpiUniqueIpsSearch(hostsKpiUniqueIpsRequest); + }, [hostsKpiUniqueIpsRequest, hostsKpiUniqueIpsSearch]); + + return [loading, hostsKpiUniqueIpsResponse]; +}; diff --git a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/unique_ips/translations.ts b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/unique_ips/translations.ts new file mode 100644 index 0000000000000..2d1574b080ac1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/unique_ips/translations.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const ERROR_HOSTS_KPI_UNIQUE_IPS = i18n.translate( + 'xpack.securitySolution.hostsKpiUniqueIps.errorSearchDescription', + { + defaultMessage: `An error has occurred on hosts kpi unique ips search`, + } +); + +export const FAIL_HOSTS_KPI_UNIQUE_IPS = i18n.translate( + 'xpack.securitySolution.hostsKpiUniqueIps.failSearchDescription', + { + defaultMessage: `Failed to run search on hosts kpi unique ips`, + } +); diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx index d8cd59f119d52..57e1b128ce64d 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx @@ -21,13 +21,12 @@ import { hasMlUserPermissions } from '../../../../common/machine_learning/has_ml import { useMlCapabilities } from '../../../common/components/ml/hooks/use_ml_capabilities'; import { scoreIntervalToDateTime } from '../../../common/components/ml/score/score_interval_to_datetime'; import { SiemNavigation } from '../../../common/components/navigation'; -import { KpiHostsComponent } from '../../components/kpi_hosts'; +import { HostsDetailsKpiComponent } from '../../components/kpi_hosts'; import { HostOverview } from '../../../overview/components/host_overview'; import { manageQuery } from '../../../common/components/page/manage_query'; import { SiemSearchBar } from '../../../common/components/search_bar'; import { WrapperPage } from '../../../common/components/wrapper_page'; import { HostOverviewByNameQuery } from '../../containers/hosts/details'; -import { KpiHostDetailsQuery } from '../../containers/kpi_host_details'; import { useGlobalTime } from '../../../common/containers/use_global_time'; import { useWithSource } from '../../../common/containers/source'; import { LastEventIndexKey } from '../../../graphql/types'; @@ -54,7 +53,6 @@ import { TimelineId } from '../../../../common/types/timeline'; import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; const HostOverviewManage = manageQuery(HostOverview); -const KpiHostDetailsManage = manageQuery(KpiHostsComponent); const HostDetailsComponent = React.memo( ({ @@ -160,27 +158,14 @@ const HostDetailsComponent = React.memo( - - {({ kpiHostDetails, id, inspect, loading, refetch }) => ( - - )} - + /> diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx index ef88c255b1735..4b8e3cc6987ac 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts.tsx @@ -17,11 +17,9 @@ import { HeaderPage } from '../../common/components/header_page'; import { LastEventTime } from '../../common/components/last_event_time'; import { hasMlUserPermissions } from '../../../common/machine_learning/has_ml_user_permissions'; import { SiemNavigation } from '../../common/components/navigation'; -import { KpiHostsComponent } from '../components/kpi_hosts'; -import { manageQuery } from '../../common/components/page/manage_query'; +import { HostsKpiComponent } from '../components/kpi_hosts'; import { SiemSearchBar } from '../../common/components/search_bar'; import { WrapperPage } from '../../common/components/wrapper_page'; -import { KpiHostsQuery } from '../containers/kpi_hosts'; import { useFullScreen } from '../../common/containers/use_full_screen'; import { useGlobalTime } from '../../common/containers/use_global_time'; import { useWithSource } from '../../common/containers/source'; @@ -49,8 +47,6 @@ import { timelineSelectors } from '../../timelines/store/timeline'; import { timelineDefaults } from '../../timelines/store/timeline/defaults'; import { TimelineModel } from '../../timelines/store/timeline/model'; -const KpiHostsComponentManage = manageQuery(KpiHostsComponent); - export const HostsComponent = React.memo( ({ filters, graphEventId, query, setAbsoluteRangeDatePicker, hostsPagePath }) => { const { to, from, deleteQuery, setQuery, isInitializing } = useGlobalTime(); @@ -109,27 +105,14 @@ export const HostsComponent = React.memo( title={i18n.PAGE_TITLE} /> - - {({ kpiHosts, loading, id, inspect, refetch }) => ( - - )} - + narrowDateRange={narrowDateRange} + /> diff --git a/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx b/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx index 65ddb9305f607..d3fc68874ce91 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/navigation/authentications_query_tab_body.tsx @@ -16,7 +16,7 @@ import { MatrixHistogramConfigs, } from '../../../common/components/matrix_histogram/types'; import { MatrixHistogram } from '../../../common/components/matrix_histogram'; -import { KpiHostsChartColors } from '../../components/kpi_hosts/types'; +import { HostsKpiChartColors } from '../../components/kpi_hosts/types'; import * as i18n from '../translations'; import { MatrixHistogramType } from '../../../../common/search_strategy/security_solution'; @@ -24,7 +24,7 @@ const AuthenticationTableManage = manageQuery(AuthenticationTable); const ID = 'authenticationsHistogramQuery'; -const authStackByOptions: MatrixHistogramOption[] = [ +const authenticationsStackByOptions: MatrixHistogramOption[] = [ { text: 'event.outcome', value: 'event.outcome', @@ -32,31 +32,32 @@ const authStackByOptions: MatrixHistogramOption[] = [ ]; const DEFAULT_STACK_BY = 'event.outcome'; -enum AuthMatrixDataGroup { - authSuccess = 'success', - authFailure = 'failure', +enum AuthenticationsMatrixDataGroup { + authenticationsSuccess = 'success', + authenticationsFailure = 'failure', } -export const authMatrixDataMappingFields: MatrixHistogramMappingTypes = { - [AuthMatrixDataGroup.authSuccess]: { - key: AuthMatrixDataGroup.authSuccess, +export const authenticationsMatrixDataMappingFields: MatrixHistogramMappingTypes = { + [AuthenticationsMatrixDataGroup.authenticationsSuccess]: { + key: AuthenticationsMatrixDataGroup.authenticationsSuccess, value: null, - color: KpiHostsChartColors.authSuccess, + color: HostsKpiChartColors.authenticationsSuccess, }, - [AuthMatrixDataGroup.authFailure]: { - key: AuthMatrixDataGroup.authFailure, + [AuthenticationsMatrixDataGroup.authenticationsFailure]: { + key: AuthenticationsMatrixDataGroup.authenticationsFailure, value: null, - color: KpiHostsChartColors.authFailure, + color: HostsKpiChartColors.authenticationsFailure, }, }; const histogramConfigs: MatrixHistogramConfigs = { defaultStackByOption: - authStackByOptions.find((o) => o.text === DEFAULT_STACK_BY) ?? authStackByOptions[0], + authenticationsStackByOptions.find((o) => o.text === DEFAULT_STACK_BY) ?? + authenticationsStackByOptions[0], errorMessage: i18n.ERROR_FETCHING_AUTHENTICATIONS_DATA, histogramType: MatrixHistogramType.authentications, - mapping: authMatrixDataMappingFields, - stackByOptions: authStackByOptions, + mapping: authenticationsMatrixDataMappingFields, + stackByOptions: authenticationsStackByOptions, title: i18n.NAVIGATION_AUTHENTICATIONS_TITLE, }; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts index 4faef85afbdc8..61bcd222b1b1e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts @@ -58,6 +58,7 @@ describe('EndpointList store concerns', () => { patternsError: undefined, isAutoRefreshEnabled: true, autoRefreshInterval: DEFAULT_POLL_INTERVAL, + queryStrategyVersion: undefined, }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts index 7673702f54370..7872c8824a8ee 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts @@ -17,6 +17,7 @@ import { nonExistingPolicies, patterns, searchBarQuery, + isTransformEnabled, } from './selectors'; import { EndpointState, PolicyIds } from '../types'; import { @@ -70,24 +71,6 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory HostResultList = (options = {}) => { const { total = 1, request_page_size: requestPageSize = 10, request_page_index: requestPageIndex = 0, + query_strategy_version: queryStrategyVersion = MetadataQueryStrategyVersions.VERSION_2, } = options; // Skip any that are before the page we're on @@ -50,7 +52,7 @@ export const mockEndpointResultList: (options?: { hosts.push({ metadata: generator.generateHostMetadata(), host_status: HostStatus.ERROR, - query_strategy_version: MetadataQueryStrategyVersions.VERSION_2, + query_strategy_version: queryStrategyVersion, }); } const mock: HostResultList = { @@ -58,7 +60,7 @@ export const mockEndpointResultList: (options?: { total, request_page_size: requestPageSize, request_page_index: requestPageIndex, - query_strategy_version: MetadataQueryStrategyVersions.VERSION_2, + query_strategy_version: queryStrategyVersion, }; return mock; }; @@ -84,6 +86,7 @@ const endpointListApiPathHandlerMocks = ({ endpointPackagePolicies = [], policyResponse = generator.generatePolicyResponse(), agentPolicy = generator.generateAgentPolicy(), + queryStrategyVersion = MetadataQueryStrategyVersions.VERSION_2, }: { /** route handlers will be setup for each individual host in this array */ endpointsResults?: HostResultList['hosts']; @@ -91,6 +94,7 @@ const endpointListApiPathHandlerMocks = ({ endpointPackagePolicies?: GetPolicyListResponse['items']; policyResponse?: HostPolicyResponse; agentPolicy?: GetAgentPoliciesResponseItem; + queryStrategyVersion?: MetadataQueryStrategyVersions; } = {}) => { const apiHandlers = { // endpoint package info @@ -107,7 +111,7 @@ const endpointListApiPathHandlerMocks = ({ request_page_size: 10, request_page_index: 0, total: endpointsResults?.length || 0, - query_strategy_version: MetadataQueryStrategyVersions.VERSION_2, + query_strategy_version: queryStrategyVersion, }; }, @@ -164,11 +168,16 @@ export const setEndpointListApiMockImplementation: ( apiResponses?: Parameters[0] ) => void = ( mockedHttpService, - { endpointsResults = mockEndpointResultList({ total: 3 }).hosts, ...pathHandlersOptions } = {} + { + endpointsResults = mockEndpointResultList({ total: 3 }).hosts, + queryStrategyVersion = MetadataQueryStrategyVersions.VERSION_2, + ...pathHandlersOptions + } = {} ) => { const apiHandlers = endpointListApiPathHandlerMocks({ ...pathHandlersOptions, endpointsResults, + queryStrategyVersion, }); mockedHttpService.post diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts index 99a1df7eb4002..0f948f74a48e4 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts @@ -36,6 +36,7 @@ export const initialEndpointListState: Immutable = { patternsError: undefined, isAutoRefreshEnabled: true, autoRefreshInterval: DEFAULT_POLL_INTERVAL, + queryStrategyVersion: undefined, }; /* eslint-disable-next-line complexity */ @@ -49,6 +50,7 @@ export const endpointListReducer: ImmutableReducer = ( total, request_page_size: pageSize, request_page_index: pageIndex, + query_strategy_version: queryStrategyVersion, } = action.payload; return { ...state, @@ -56,6 +58,7 @@ export const endpointListReducer: ImmutableReducer = ( total, pageSize, pageIndex, + queryStrategyVersion, loading: false, error: undefined, }; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts index a3eee8063d6f7..fe47d60afc339 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts @@ -14,6 +14,7 @@ import { HostPolicyResponseAppliedAction, HostPolicyResponseConfiguration, HostPolicyResponseActionStatus, + MetadataQueryStrategyVersions, } from '../../../../../common/endpoint/types'; import { EndpointState, EndpointIndexUIQueryParams } from '../types'; import { extractListPaginationParams } from '../../../common/routing'; @@ -54,11 +55,18 @@ export const isAutoRefreshEnabled = (state: Immutable) => state.i export const autoRefreshInterval = (state: Immutable) => state.autoRefreshInterval; +const queryStrategyVersion = (state: Immutable) => state.queryStrategyVersion; + export const endpointPackageVersion = createSelector( endpointPackageInfo, (info) => info?.version ?? undefined ); +export const isTransformEnabled = createSelector( + queryStrategyVersion, + (version) => version !== MetadataQueryStrategyVersions.VERSION_1 +); + /** * Returns the index patterns for the SearchBar to use for autosuggest */ diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts index 77f21243ea120..bdd0d5e942cef 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts @@ -11,6 +11,7 @@ import { HostPolicyResponse, AppLocation, PolicyData, + MetadataQueryStrategyVersions, } from '../../../../common/endpoint/types'; import { ServerApiError } from '../../../common/types'; import { GetPackagesResponse } from '../../../../../ingest_manager/common'; @@ -65,6 +66,8 @@ export interface EndpointState { isAutoRefreshEnabled: boolean; /** The current auto refresh interval for data in ms */ autoRefreshInterval: number; + /** The query strategy version that informs whether the transform for KQL is enabled or not */ + queryStrategyVersion?: MetadataQueryStrategyVersions; } /** diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index bd8344f41fe3a..51a6be18471aa 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -108,6 +108,31 @@ describe('when on the list page', () => { }); }); + describe('when loading data with the query_strategy_version is `v1`', () => { + beforeEach(() => { + reactTestingLibrary.act(() => { + const mockedEndpointListData = mockEndpointResultList({ + total: 4, + query_strategy_version: MetadataQueryStrategyVersions.VERSION_1, + }); + setEndpointListApiMockImplementation(coreStart.http, { + endpointsResults: mockedEndpointListData.hosts, + queryStrategyVersion: mockedEndpointListData.query_strategy_version, + }); + }); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + it('should not display the KQL bar', async () => { + const renderResult = render(); + await reactTestingLibrary.act(async () => { + await middlewareSpy.waitForAction('serverReturnedEndpointList'); + }); + expect(renderResult.queryByTestId('adminSearchBar')).toBeNull(); + }); + }); + describe('when there is no selected host in the url', () => { it('should not show the flyout', () => { const renderResult = render(); @@ -123,7 +148,9 @@ describe('when on the list page', () => { let firstPolicyID: string; beforeEach(() => { reactTestingLibrary.act(() => { - const hostListData = mockEndpointResultList({ total: 4 }).hosts; + const mockedEndpointData = mockEndpointResultList({ total: 4 }); + const hostListData = mockedEndpointData.hosts; + const queryStrategyVersion = mockedEndpointData.query_strategy_version; firstPolicyID = hostListData[0].metadata.Endpoint.policy.applied.id; @@ -132,7 +159,7 @@ describe('when on the list page', () => { hostListData[index] = { metadata: hostListData[index].metadata, host_status: status, - query_strategy_version: MetadataQueryStrategyVersions.VERSION_2, + query_strategy_version: queryStrategyVersion, }; } ); @@ -682,11 +709,11 @@ describe('when on the list page', () => { let renderAndWaitForData: () => Promise>; const mockEndpointListApi = () => { - const { hosts } = mockEndpointResultList(); + const { hosts, query_strategy_version: queryStrategyVersion } = mockEndpointResultList(); hostInfo = { host_status: hosts[0].host_status, metadata: hosts[0].metadata, - query_strategy_version: MetadataQueryStrategyVersions.VERSION_2, + query_strategy_version: queryStrategyVersion, }; const packagePolicy = docGenerator.generatePolicyPackagePolicy(); packagePolicy.id = hosts[0].metadata.Endpoint.policy.applied.id; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx index ecee2bee0c58b..3e1f08eee7b94 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx @@ -135,6 +135,7 @@ export const EndpointList = () => { autoRefreshInterval, isAutoRefreshEnabled, patternsError, + isTransformEnabled, } = useEndpointSelector(selector); const { formatUrl, search } = useFormatUrl(SecurityPageName.administration); @@ -532,8 +533,8 @@ export const EndpointList = () => { const hasListData = listData && listData.length > 0; const refreshStyle = useMemo(() => { - return { display: endpointsExist ? 'flex' : 'none', maxWidth: 200 }; - }, [endpointsExist]); + return { display: endpointsExist && isTransformEnabled ? 'flex' : 'none', maxWidth: 200 }; + }, [endpointsExist, isTransformEnabled]); const refreshIsPaused = useMemo(() => { return !endpointsExist ? false : hasSelectedEndpoint ? true : !isAutoRefreshEnabled; @@ -543,6 +544,10 @@ export const EndpointList = () => { return !endpointsExist ? DEFAULT_POLL_INTERVAL : autoRefreshInterval; }, [endpointsExist, autoRefreshInterval]); + const shouldShowKQLBar = useMemo(() => { + return endpointsExist && !patternsError && isTransformEnabled; + }, [endpointsExist, patternsError, isTransformEnabled]); + return ( { {hasSelectedEndpoint && } <> - {endpointsExist && !patternsError && ( + {shouldShowKQLBar && ( diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/network/components/kpi_network/__snapshots__/index.test.tsx.snap index 49562162e94a8..a03d7c2317517 100644 --- a/x-pack/plugins/security_solution/public/network/components/kpi_network/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/__snapshots__/index.test.tsx.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`KpiNetwork Component rendering it renders the default widget 1`] = ` -; data: NetworkKpiStrategyResponse; loading?: boolean; @@ -64,6 +64,6 @@ export const KpiNetworkBaseComponent = React.memo<{ ); }); -KpiNetworkBaseComponent.displayName = 'KpiNetworkBaseComponent'; +NetworkKpiBaseComponent.displayName = 'NetworkKpiBaseComponent'; -export const KpiNetworkBaseComponentManage = manageQuery(KpiNetworkBaseComponent); +export const NetworkKpiBaseComponentManage = manageQuery(NetworkKpiBaseComponent); diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/dns/index.tsx b/x-pack/plugins/security_solution/public/network/components/kpi_network/dns/index.tsx index 889f3dacc2d98..0f13b0e8f874e 100644 --- a/x-pack/plugins/security_solution/public/network/components/kpi_network/dns/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/dns/index.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { StatItems } from '../../../../common/components/stat_items'; import { useNetworkKpiDns } from '../../../containers/kpi_network/dns'; -import { KpiNetworkBaseComponentManage } from '../common'; +import { NetworkKpiBaseComponentManage } from '../common'; import { NetworkKpiProps } from '../types'; import * as i18n from './translations'; @@ -41,7 +41,7 @@ const NetworkKpiDnsComponent: React.FC = ({ }); return ( - { +describe('NetworkKpiComponent', () => { const state: State = mockGlobalState; const props = { from: '2019-06-15T06:00:00.000Z', @@ -53,11 +53,11 @@ describe('KpiNetwork Component', () => { test('it renders the default widget', () => { const wrapper = shallow( - + ); - expect(wrapper.find('KpiNetworkComponent')).toMatchSnapshot(); + expect(wrapper.find('NetworkKpiComponent')).toMatchSnapshot(); }); }); }); diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/index.tsx b/x-pack/plugins/security_solution/public/network/components/kpi_network/index.tsx index 674e592940fa6..95534e1a61988 100644 --- a/x-pack/plugins/security_solution/public/network/components/kpi_network/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/index.tsx @@ -14,7 +14,7 @@ import { NetworkKpiUniqueFlows } from './unique_flows'; import { NetworkKpiUniquePrivateIps } from './unique_private_ips'; import { NetworkKpiProps } from './types'; -export const KpiNetworkComponent = React.memo( +export const NetworkKpiComponent = React.memo( ({ filterQuery, from, to, setQuery, skip, narrowDateRange }) => ( @@ -78,4 +78,4 @@ export const KpiNetworkComponent = React.memo( ) ); -KpiNetworkComponent.displayName = 'KpiNetworkComponent'; +NetworkKpiComponent.displayName = 'NetworkKpiComponent'; diff --git a/x-pack/plugins/security_solution/public/network/components/kpi_network/network_events/index.tsx b/x-pack/plugins/security_solution/public/network/components/kpi_network/network_events/index.tsx index 3ee2acf1a115c..18217e41f2a27 100644 --- a/x-pack/plugins/security_solution/public/network/components/kpi_network/network_events/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/kpi_network/network_events/index.tsx @@ -9,7 +9,7 @@ import { euiPaletteColorBlind } from '@elastic/eui'; import { StatItems } from '../../../../common/components/stat_items'; import { useNetworkKpiNetworkEvents } from '../../../containers/kpi_network/network_events'; -import { KpiNetworkBaseComponentManage } from '../common'; +import { NetworkKpiBaseComponentManage } from '../common'; import { NetworkKpiProps } from '../types'; import * as i18n from './translations'; @@ -46,7 +46,7 @@ const NetworkKpiNetworkEventsComponent: React.FC = ({ }); return ( - = ({ }); return ( - = ({ }); return ( - = ({ }); return ( - ( - > = { @@ -32,7 +32,7 @@ export const buildQuery = ({ defaultIndex, docValueFields, }: HostAuthenticationsRequestOptions) => { - const esFields = reduceFields(authenticationFields, { ...hostFieldsMap, ...sourceFieldsMap }); + const esFields = reduceFields(authenticationsFields, { ...hostFieldsMap, ...sourceFieldsMap }); const filter = [ ...createQueryFilterClauses(filterQuery), diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.ts index d61914fda7d06..ce8900a578102 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/helpers.ts @@ -15,7 +15,7 @@ import { StrategyResponseType, } from '../../../../../../common/search_strategy/security_solution'; -export const authenticationFields = [ +export const authenticationsFields = [ '_id', 'failures', 'successes', @@ -31,7 +31,7 @@ export const authenticationFields = [ ]; export const formatAuthenticationData = ( - fields: readonly string[] = authenticationFields, + fields: readonly string[] = authenticationsFields, hit: AuthenticationHit, fieldMap: Readonly> ): AuthenticationsEdges => diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/index.tsx b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/index.tsx index a43f53880587a..e09d8de7ba945 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/index.tsx +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/authentications/index.tsx @@ -20,7 +20,7 @@ import { import { inspectStringifyObject } from '../../../../../utils/build_query'; import { SecuritySolutionFactory } from '../../types'; import { auditdFieldsMap, buildQuery as buildAuthenticationQuery } from './dsl/query.dsl'; -import { authenticationFields, formatAuthenticationData, getHits } from './helpers'; +import { authenticationsFields, formatAuthenticationData, getHits } from './helpers'; export const authentications: SecuritySolutionFactory = { buildDsl: (options: HostAuthenticationsRequestOptions) => { @@ -40,7 +40,7 @@ export const authentications: SecuritySolutionFactory - formatAuthenticationData(authenticationFields, hit, auditdFieldsMap) + formatAuthenticationData(authenticationsFields, hit, auditdFieldsMap) ); const edges = authenticationEdges.splice(cursorStart, querySize - cursorStart); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.test.ts index edcba88a0cd89..44c55ab6e7c9d 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.test.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.test.ts @@ -5,13 +5,16 @@ */ import { hostsFactory } from '.'; -import { HostsQueries } from '../../../../../common/search_strategy'; +import { HostsQueries, HostsKpiQueries } from '../../../../../common/search_strategy'; import { allHosts } from './all'; import { hostDetails } from './details'; import { hostOverview } from './overview'; import { firstLastSeenHost } from './last_first_seen'; import { uncommonProcesses } from './uncommon_processes'; import { authentications } from './authentications'; +import { hostsKpiAuthentications } from './kpi/authentications'; +import { hostsKpiHosts } from './kpi/hosts'; +import { hostsKpiUniqueIps } from './kpi/unique_ips'; jest.mock('./all'); jest.mock('./details'); @@ -19,6 +22,9 @@ jest.mock('./overview'); jest.mock('./last_first_seen'); jest.mock('./uncommon_processes'); jest.mock('./authentications'); +jest.mock('./kpi/authentications'); +jest.mock('./kpi/hosts'); +jest.mock('./kpi/unique_ips'); describe('hostsFactory', () => { test('should include correct apis', () => { @@ -29,6 +35,9 @@ describe('hostsFactory', () => { [HostsQueries.firstLastSeen]: firstLastSeenHost, [HostsQueries.uncommonProcesses]: uncommonProcesses, [HostsQueries.authentications]: authentications, + [HostsKpiQueries.kpiAuthentications]: hostsKpiAuthentications, + [HostsKpiQueries.kpiHosts]: hostsKpiHosts, + [HostsKpiQueries.kpiUniqueIps]: hostsKpiUniqueIps, }; expect(hostsFactory).toEqual(expectedHostsFactory); }); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.ts index 85619cfec62ce..ad6a6182d331b 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.ts @@ -7,6 +7,7 @@ import { FactoryQueryTypes, HostsQueries, + HostsKpiQueries, } from '../../../../../common/search_strategy/security_solution'; import { SecuritySolutionFactory } from '../types'; @@ -16,12 +17,21 @@ import { hostOverview } from './overview'; import { firstLastSeenHost } from './last_first_seen'; import { uncommonProcesses } from './uncommon_processes'; import { authentications } from './authentications'; +import { hostsKpiAuthentications } from './kpi/authentications'; +import { hostsKpiHosts } from './kpi/hosts'; +import { hostsKpiUniqueIps } from './kpi/unique_ips'; -export const hostsFactory: Record> = { +export const hostsFactory: Record< + HostsQueries | HostsKpiQueries, + SecuritySolutionFactory +> = { [HostsQueries.details]: hostDetails, [HostsQueries.hosts]: allHosts, [HostsQueries.overview]: hostOverview, [HostsQueries.firstLastSeen]: firstLastSeenHost, [HostsQueries.uncommonProcesses]: uncommonProcesses, [HostsQueries.authentications]: authentications, + [HostsKpiQueries.kpiAuthentications]: hostsKpiAuthentications, + [HostsKpiQueries.kpiHosts]: hostsKpiHosts, + [HostsKpiQueries.kpiUniqueIps]: hostsKpiUniqueIps, }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/authentications/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/authentications/helpers.ts new file mode 100644 index 0000000000000..513e361b5be05 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/authentications/helpers.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + HostsKpiHistogram, + HostsKpiAuthenticationsHistogramCount, + HostsKpiHistogramData, +} from '../../../../../../../common/search_strategy'; + +export const formatAuthenticationsHistogramData = ( + data: Array> +): HostsKpiHistogramData[] | null => + data && data.length > 0 + ? data.map(({ key, count }) => ({ + x: key, + y: count.doc_count, + })) + : null; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/authentications/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/authentications/index.ts new file mode 100644 index 0000000000000..bafc9a3accc6e --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/authentications/index.ts @@ -0,0 +1,63 @@ +/* + * 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 { getOr } from 'lodash/fp'; + +import { IEsSearchResponse } from '../../../../../../../../../../src/plugins/data/common'; +import { + HostsKpiQueries, + HostsKpiAuthenticationsStrategyResponse, + HostsKpiAuthenticationsRequestOptions, +} from '../../../../../../../common/search_strategy/security_solution/hosts'; +import { inspectStringifyObject } from '../../../../../../utils/build_query'; +import { SecuritySolutionFactory } from '../../../types'; +import { buildHostsKpiAuthenticationsQuery } from './query.hosts_kpi_authentications.dsl'; +import { formatAuthenticationsHistogramData } from './helpers'; + +export const hostsKpiAuthentications: SecuritySolutionFactory = { + buildDsl: (options: HostsKpiAuthenticationsRequestOptions) => + buildHostsKpiAuthenticationsQuery(options), + parse: async ( + options: HostsKpiAuthenticationsRequestOptions, + response: IEsSearchResponse + ): Promise => { + const inspect = { + dsl: [inspectStringifyObject(buildHostsKpiAuthenticationsQuery(options))], + }; + + const authenticationsSuccessHistogram = getOr( + null, + 'aggregations.authentication_success_histogram.buckets', + response.rawResponse + ); + const authenticationsFailureHistogram = getOr( + null, + 'aggregations.authentication_failure_histogram.buckets', + response.rawResponse + ); + + return { + ...response, + inspect, + authenticationsSuccess: getOr( + null, + 'aggregations.authentication_success.doc_count', + response.rawResponse + ), + authenticationsSuccessHistogram: formatAuthenticationsHistogramData( + authenticationsSuccessHistogram + ), + authenticationsFailure: getOr( + null, + 'aggregations.authentication_failure.doc_count', + response.rawResponse + ), + authenticationsFailureHistogram: formatAuthenticationsHistogramData( + authenticationsFailureHistogram + ), + }; + }, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/authentications/query.hosts_kpi_authentications.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/authentications/query.hosts_kpi_authentications.dsl.ts new file mode 100644 index 0000000000000..8da5f7f95c5d1 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/authentications/query.hosts_kpi_authentications.dsl.ts @@ -0,0 +1,101 @@ +/* + * 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 { HostsKpiAuthenticationsRequestOptions } from '../../../../../../../common/search_strategy/security_solution/hosts'; +import { createQueryFilterClauses } from '../../../../../../utils/build_query'; + +export const buildHostsKpiAuthenticationsQuery = ({ + filterQuery, + timerange: { from, to }, + defaultIndex, +}: HostsKpiAuthenticationsRequestOptions) => { + const filter = [ + ...createQueryFilterClauses(filterQuery), + { + bool: { + filter: [ + { + term: { + 'event.category': 'authentication', + }, + }, + ], + }, + }, + { + range: { + '@timestamp': { + gte: from, + lte: to, + format: 'strict_date_optional_time', + }, + }, + }, + ]; + + const dslQuery = { + index: defaultIndex, + allowNoIndices: true, + ignoreUnavailable: true, + body: { + aggs: { + authentication_success: { + filter: { + term: { + 'event.outcome': 'success', + }, + }, + }, + authentication_success_histogram: { + auto_date_histogram: { + field: '@timestamp', + buckets: '6', + }, + aggs: { + count: { + filter: { + term: { + 'event.outcome': 'success', + }, + }, + }, + }, + }, + authentication_failure: { + filter: { + term: { + 'event.outcome': 'failure', + }, + }, + }, + authentication_failure_histogram: { + auto_date_histogram: { + field: '@timestamp', + buckets: '6', + }, + aggs: { + count: { + filter: { + term: { + 'event.outcome': 'failure', + }, + }, + }, + }, + }, + }, + query: { + bool: { + filter, + }, + }, + size: 0, + track_total_hits: false, + }, + }; + + return dslQuery; +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/common/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/common/index.ts new file mode 100644 index 0000000000000..080ef05c99136 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/common/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + HostsKpiHistogram, + HostsKpiGeneralHistogramCount, + HostsKpiHistogramData, +} from '../../../../../../../common/search_strategy'; + +export const formatGeneralHistogramData = ( + data: Array> +): HostsKpiHistogramData[] | null => + data && data.length > 0 + ? data.map(({ key, count }) => ({ + x: key, + y: count.value, + })) + : null; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/hosts/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/hosts/index.ts new file mode 100644 index 0000000000000..6d91ebf09895e --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/hosts/index.ts @@ -0,0 +1,42 @@ +/* + * 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 { getOr } from 'lodash/fp'; + +import { IEsSearchResponse } from '../../../../../../../../../../src/plugins/data/common'; +import { + HostsKpiQueries, + HostsKpiHostsStrategyResponse, + HostsKpiHostsRequestOptions, +} from '../../../../../../../common/search_strategy/security_solution/hosts'; +import { inspectStringifyObject } from '../../../../../../utils/build_query'; +import { SecuritySolutionFactory } from '../../../types'; +import { buildHostsKpiHostsQuery } from './query.hosts_kpi_hosts.dsl'; +import { formatGeneralHistogramData } from '../common'; + +export const hostsKpiHosts: SecuritySolutionFactory = { + buildDsl: (options: HostsKpiHostsRequestOptions) => buildHostsKpiHostsQuery(options), + parse: async ( + options: HostsKpiHostsRequestOptions, + response: IEsSearchResponse + ): Promise => { + const inspect = { + dsl: [inspectStringifyObject(buildHostsKpiHostsQuery(options))], + }; + + const hostsHistogram = getOr( + null, + 'aggregations.hosts_histogram.buckets', + response.rawResponse + ); + return { + ...response, + inspect, + hosts: getOr(null, 'aggregations.hosts.value', response.rawResponse), + hostsHistogram: formatGeneralHistogramData(hostsHistogram), + }; + }, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/hosts/query.hosts_kpi_hosts.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/hosts/query.hosts_kpi_hosts.dsl.ts new file mode 100644 index 0000000000000..704743cc434ed --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/hosts/query.hosts_kpi_hosts.dsl.ts @@ -0,0 +1,64 @@ +/* + * 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 { HostsKpiHostsRequestOptions } from '../../../../../../../common/search_strategy/security_solution/hosts'; +import { createQueryFilterClauses } from '../../../../../../utils/build_query'; + +export const buildHostsKpiHostsQuery = ({ + filterQuery, + timerange: { from, to }, + defaultIndex, +}: HostsKpiHostsRequestOptions) => { + const filter = [ + ...createQueryFilterClauses(filterQuery), + { + range: { + '@timestamp': { + gte: from, + lte: to, + format: 'strict_date_optional_time', + }, + }, + }, + ]; + + const dslQuery = { + index: defaultIndex, + allowNoIndices: true, + ignoreUnavailable: true, + body: { + aggregations: { + hosts: { + cardinality: { + field: 'host.name', + }, + }, + hosts_histogram: { + auto_date_histogram: { + field: '@timestamp', + buckets: '6', + }, + aggs: { + count: { + cardinality: { + field: 'host.name', + }, + }, + }, + }, + }, + query: { + bool: { + filter, + }, + }, + size: 0, + track_total_hits: false, + }, + }; + + return dslQuery; +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/index.ts new file mode 100644 index 0000000000000..f4793ecd53f8f --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/index.ts @@ -0,0 +1,10 @@ +/* + * 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. + */ + +export * from './authentications'; +export * from './common'; +export * from './hosts'; +export * from './unique_ips'; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/unique_ips/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/unique_ips/index.ts new file mode 100644 index 0000000000000..2f890e6fdacca --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/unique_ips/index.ts @@ -0,0 +1,55 @@ +/* + * 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 { getOr } from 'lodash/fp'; + +import { IEsSearchResponse } from '../../../../../../../../../../src/plugins/data/common'; +import { + HostsKpiQueries, + HostsKpiUniqueIpsStrategyResponse, + HostsKpiUniqueIpsRequestOptions, +} from '../../../../../../../common/search_strategy/security_solution/hosts'; +import { inspectStringifyObject } from '../../../../../../utils/build_query'; +import { SecuritySolutionFactory } from '../../../types'; +import { buildHostsKpiUniqueIpsQuery } from './query.hosts_kpi_unique_ips.dsl'; +import { formatGeneralHistogramData } from '../common'; + +export const hostsKpiUniqueIps: SecuritySolutionFactory = { + buildDsl: (options: HostsKpiUniqueIpsRequestOptions) => buildHostsKpiUniqueIpsQuery(options), + parse: async ( + options: HostsKpiUniqueIpsRequestOptions, + response: IEsSearchResponse + ): Promise => { + const inspect = { + dsl: [inspectStringifyObject(buildHostsKpiUniqueIpsQuery(options))], + }; + + const uniqueSourceIpsHistogram = getOr( + null, + 'aggregations.unique_source_ips_histogram.buckets', + response.rawResponse + ); + + const uniqueDestinationIpsHistogram = getOr( + null, + 'aggregations.unique_destination_ips_histogram.buckets', + response.rawResponse + ); + + return { + ...response, + inspect, + uniqueSourceIps: getOr(null, 'aggregations.unique_source_ips.value', response.rawResponse), + uniqueSourceIpsHistogram: formatGeneralHistogramData(uniqueSourceIpsHistogram), + uniqueDestinationIps: getOr( + null, + 'aggregations.unique_destination_ips.value', + response.rawResponse + ), + uniqueDestinationIpsHistogram: formatGeneralHistogramData(uniqueDestinationIpsHistogram), + }; + }, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/unique_ips/query.hosts_kpi_unique_ips.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/unique_ips/query.hosts_kpi_unique_ips.dsl.ts new file mode 100644 index 0000000000000..618c6cb51f666 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/unique_ips/query.hosts_kpi_unique_ips.dsl.ts @@ -0,0 +1,82 @@ +/* + * 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 { HostsKpiUniqueIpsRequestOptions } from '../../../../../../../common/search_strategy/security_solution/hosts'; +import { createQueryFilterClauses } from '../../../../../../utils/build_query'; + +export const buildHostsKpiUniqueIpsQuery = ({ + filterQuery, + timerange: { from, to }, + defaultIndex, +}: HostsKpiUniqueIpsRequestOptions) => { + const filter = [ + ...createQueryFilterClauses(filterQuery), + { + range: { + '@timestamp': { + gte: from, + lte: to, + format: 'strict_date_optional_time', + }, + }, + }, + ]; + + const dslQuery = { + index: defaultIndex, + allowNoIndices: true, + ignoreUnavailable: true, + body: { + aggregations: { + unique_source_ips: { + cardinality: { + field: 'source.ip', + }, + }, + unique_source_ips_histogram: { + auto_date_histogram: { + field: '@timestamp', + buckets: '6', + }, + aggs: { + count: { + cardinality: { + field: 'source.ip', + }, + }, + }, + }, + unique_destination_ips: { + cardinality: { + field: 'destination.ip', + }, + }, + unique_destination_ips_histogram: { + auto_date_histogram: { + field: '@timestamp', + buckets: '6', + }, + aggs: { + count: { + cardinality: { + field: 'destination.ip', + }, + }, + }, + }, + }, + query: { + bool: { + filter, + }, + }, + size: 0, + track_total_hits: false, + }, + }; + + return dslQuery; +}; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 781f351d7d241..f626835da8e11 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1465,7 +1465,6 @@ "discover.uninitializedText": "クエリを作成、フィルターを追加、または[更新]をクリックして、現在のクエリの結果を取得します。", "discover.uninitializedTitle": "検索開始", "discover.valueIsNotConfiguredIndexPatternIDWarningTitle": "{stateVal}は設定されたインデックスパターンIDではありません", - "embeddableApi.actions.applyFilterActionTitle": "現在のビューにフィルターを適用", "embeddableApi.addPanel.createNewDefaultOption": "新規作成...", "embeddableApi.addPanel.displayName": "パネルの追加", "embeddableApi.addPanel.noMatchingObjectsMessage": "一致するオブジェクトが見つかりませんでした。", @@ -4807,8 +4806,6 @@ "xpack.apm.serviceMap.avgMemoryUsagePopoverStat": "メモリー使用状況(平均)", "xpack.apm.serviceMap.avgReqPerMinutePopoverMetric": "1分あたりのリクエスト(平均)", "xpack.apm.serviceMap.avgTransDurationPopoverStat": "トランザクションの長さ(平均)", - "xpack.apm.serviceMap.betaBadge": "ベータ", - "xpack.apm.serviceMap.betaTooltipMessage": "現在、この機能はベータです。不具合を見つけた場合やご意見がある場合、サポートに問い合わせるか、またはディスカッションフォーラムにご報告ください。", "xpack.apm.serviceMap.center": "中央", "xpack.apm.serviceMap.download": "ダウンロード", "xpack.apm.serviceMap.emptyBanner.docsLink": "詳細はドキュメントをご覧ください", @@ -14891,7 +14888,7 @@ "xpack.securitySolution.auditd.violatedSeLinuxPolicyDescription": "selinuxポリシーに違反しました", "xpack.securitySolution.auditd.wasAuthorizedToUseDescription": "が以下の使用を承認されました。", "xpack.securitySolution.auditd.withResultDescription": "結果付き", - "xpack.securitySolution.authenticationsTable.authenticationFailures": "認証", + "xpack.securitySolution.authenticationsTable.authentications": "認証", "xpack.securitySolution.authenticationsTable.failures": "失敗", "xpack.securitySolution.authenticationsTable.lastFailedDestination": "前回失敗したデスティネーション", "xpack.securitySolution.authenticationsTable.lastFailedSource": "前回失敗したソース", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index fd749ba4709ec..d6baa87ca9e2f 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1466,7 +1466,6 @@ "discover.uninitializedText": "编写查询,添加一些筛选,或只需单击“刷新”来检索当前查询的结果。", "discover.uninitializedTitle": "开始搜索", "discover.valueIsNotConfiguredIndexPatternIDWarningTitle": "{stateVal} 不是配置的索引模式 ID", - "embeddableApi.actions.applyFilterActionTitle": "将筛选应用于当前视图", "embeddableApi.addPanel.createNewDefaultOption": "新建", "embeddableApi.addPanel.displayName": "添加面板", "embeddableApi.addPanel.noMatchingObjectsMessage": "未找到任何匹配对象。", @@ -4810,8 +4809,6 @@ "xpack.apm.serviceMap.avgMemoryUsagePopoverStat": "内存使用率(平均值)", "xpack.apm.serviceMap.avgReqPerMinutePopoverMetric": "每分钟请求数(平均)", "xpack.apm.serviceMap.avgTransDurationPopoverStat": "事务持续时间(平均值)", - "xpack.apm.serviceMap.betaBadge": "公测版", - "xpack.apm.serviceMap.betaTooltipMessage": "此功能当前为公测版。如果遇到任何错误或有任何反馈,请报告问题或访问我们的论坛。", "xpack.apm.serviceMap.center": "中", "xpack.apm.serviceMap.download": "下载", "xpack.apm.serviceMap.emptyBanner.docsLink": "在文档中了解详情", @@ -14900,7 +14897,7 @@ "xpack.securitySolution.auditd.violatedSeLinuxPolicyDescription": "已违反 selinux 策略", "xpack.securitySolution.auditd.wasAuthorizedToUseDescription": "有权使用", "xpack.securitySolution.auditd.withResultDescription": ",结果为", - "xpack.securitySolution.authenticationsTable.authenticationFailures": "身份验证", + "xpack.securitySolution.authenticationsTable.authentications": "身份验证", "xpack.securitySolution.authenticationsTable.failures": "错误", "xpack.securitySolution.authenticationsTable.lastFailedDestination": "上一失败目标", "xpack.securitySolution.authenticationsTable.lastFailedSource": "上一失败源", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx index 0742ed8a778ef..2bcd87830901b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/add_message_variables.tsx @@ -61,6 +61,7 @@ export const AddMessageVariables: React.FunctionComponent = ({ setIsVariablesPopoverOpen(true)} iconType="indexOpen" diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.tsx index 495707db4975c..0a04db1b5ddfa 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.tsx @@ -32,48 +32,47 @@ export const IndexParamsFields = ({ }; return ( - <> - 0 ? ((documents[0] as unknown) as string) : undefined + 0 ? ((documents[0] as unknown) as string) : undefined + } + label={i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.documentsFieldLabel', + { + defaultMessage: 'Document to index', } - label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.documentsFieldLabel', - { - defaultMessage: 'Document to index', - } - )} - aria-label={i18n.translate( - 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.jsonDocAriaLabel', - { - defaultMessage: 'Code editor', - } - )} - errors={errors.documents as string[]} - onDocumentsChange={onDocumentsChange} - helpText={ - - - + )} + aria-label={i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.indexAction.jsonDocAriaLabel', + { + defaultMessage: 'Code editor', } - onBlur={() => { - if ( - !(documents && documents.length > 0 ? ((documents[0] as unknown) as string) : undefined) - ) { - // set document as empty to turn on the validation for non empty valid JSON object - onDocumentsChange('{}'); - } - }} - /> - + )} + errors={errors.documents as string[]} + onDocumentsChange={onDocumentsChange} + helpText={ + + + + } + onBlur={() => { + if ( + !(documents && documents.length > 0 ? ((documents[0] as unknown) as string) : undefined) + ) { + // set document as empty to turn on the validation for non empty valid JSON object + onDocumentsChange('{}'); + } + }} + /> ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.test.ts index 43b22361aea36..ad3a5b40bd00d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.test.ts @@ -12,6 +12,7 @@ import { loadActionTypes, loadAllActions, updateActionConnector, + executeAction, } from './action_connector_api'; const http = httpServiceMock.createStartContract(); @@ -128,3 +129,32 @@ describe('deleteActions', () => { `); }); }); + +describe('executeAction', () => { + test('should call execute API', async () => { + const id = '123'; + const params = { + stringParams: 'someString', + numericParams: 123, + }; + + http.post.mockResolvedValueOnce({ + actionId: id, + status: 'ok', + }); + + const result = await executeAction({ id, http, params }); + expect(result).toEqual({ + actionId: id, + status: 'ok', + }); + expect(http.post.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "/api/actions/action/123/_execute", + Object { + "body": "{\\"params\\":{\\"stringParams\\":\\"someString\\",\\"numericParams\\":123}}", + }, + ] + `); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.ts index 46a676ac06539..c2c7139d13bf0 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/action_connector_api.ts @@ -7,6 +7,7 @@ import { HttpSetup } from 'kibana/public'; import { BASE_ACTION_API_PATH } from '../constants'; import { ActionConnector, ActionConnectorWithoutId, ActionType } from '../../types'; +import { ActionTypeExecutorResult } from '../../../../../plugins/actions/common'; export async function loadActionTypes({ http }: { http: HttpSetup }): Promise { return await http.get(`${BASE_ACTION_API_PATH}/list_action_types`); @@ -65,3 +66,17 @@ export async function deleteActions({ ); return { successes, errors }; } + +export async function executeAction({ + id, + params, + http, +}: { + id: string; + http: HttpSetup; + params: Record; +}): Promise> { + return await http.post(`${BASE_ACTION_API_PATH}/action/${id}/_execute`, { + body: JSON.stringify({ params }), + }); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.scss b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.scss new file mode 100644 index 0000000000000..873a3ceb762cd --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.scss @@ -0,0 +1,3 @@ +.connectorEditFlyoutTabs { + margin-bottom: '-25px'; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx index dd9eeae266987..0c2f4df0ca52b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.test.tsx @@ -152,6 +152,6 @@ describe('connector_edit_flyout', () => { const preconfiguredBadge = wrapper.find('[data-test-subj="preconfiguredBadge"]'); expect(preconfiguredBadge.exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="saveEditedActionButton"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="saveAndCloseEditedActionButton"]').exists()).toBeFalsy(); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx index ca75e730062ab..fc902a4fabcd8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx @@ -19,15 +19,21 @@ import { EuiBetaBadge, EuiText, EuiLink, + EuiTabs, + EuiTab, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { Option, none, some } from 'fp-ts/lib/Option'; import { ActionConnectorForm, validateBaseProperties } from './action_connector_form'; +import { TestConnectorForm } from './test_connector_form'; import { ActionConnectorTableItem, ActionConnector, IErrorObject } from '../../../types'; import { connectorReducer } from './connector_reducer'; -import { updateActionConnector } from '../../lib/action_connector_api'; +import { updateActionConnector, executeAction } from '../../lib/action_connector_api'; import { hasSaveActionsCapability } from '../../lib/capabilities'; import { useActionsConnectorsContext } from '../../context/actions_connectors_context'; import { PLUGIN } from '../../constants/plugin'; +import { ActionTypeExecutorResult } from '../../../../../actions/common'; +import './connector_edit_flyout.scss'; export interface ConnectorEditProps { initialConnector: ActionConnectorTableItem; @@ -40,7 +46,6 @@ export const ConnectorEditFlyout = ({ editFlyoutVisible, setEditFlyoutVisibility, }: ConnectorEditProps) => { - let hasErrors = false; const { http, toastNotifications, @@ -56,13 +61,26 @@ export const ConnectorEditFlyout = ({ connector: { ...initialConnector, secrets: {} }, }); const [isSaving, setIsSaving] = useState(false); + const [selectedTab, setTab] = useState<'config' | 'test'>('config'); + + const [hasChanges, setHasChanges] = useState(false); const setConnector = (key: string, value: any) => { dispatch({ command: { type: 'setConnector' }, payload: { key, value } }); }; + const [testExecutionActionParams, setTestExecutionActionParams] = useState< + Record + >({}); + const [testExecutionResult, setTestExecutionResult] = useState< + Option> + >(none); + const [isExecutingAction, setIsExecutinAction] = useState(false); + const closeFlyout = useCallback(() => { setEditFlyoutVisibility(false); setConnector('connector', { ...initialConnector, secrets: {} }); + setHasChanges(false); + setTestExecutionResult(none); // eslint-disable-next-line react-hooks/exhaustive-deps }, [setEditFlyoutVisibility]); @@ -71,11 +89,13 @@ export const ConnectorEditFlyout = ({ } const actionTypeModel = actionTypeRegistry.get(connector.actionTypeId); - const errors = { + const errorsInConnectorConfig = { ...actionTypeModel?.validateConnector(connector).errors, ...validateBaseProperties(connector).errors, } as IErrorObject; - hasErrors = !!Object.keys(errors).find((errorKey) => errors[errorKey].length >= 1); + const hasErrorsInConnectorConfig = !!Object.keys(errorsInConnectorConfig).find( + (errorKey) => errorsInConnectorConfig[errorKey].length >= 1 + ); const onActionConnectorSave = async (): Promise => await updateActionConnector({ http, connector, id: connector.id }) @@ -173,6 +193,32 @@ export const ConnectorEditFlyout = ({ ); + const onExecutAction = () => { + setIsExecutinAction(true); + return executeAction({ id: connector.id, params: testExecutionActionParams, http }).then( + (result) => { + setIsExecutinAction(false); + setTestExecutionResult(some(result)); + return result; + } + ); + }; + + const onSaveClicked = async (closeAfterSave: boolean = true) => { + setIsSaving(true); + const savedAction = await onActionConnectorSave(); + setIsSaving(false); + if (savedAction) { + setHasChanges(false); + if (closeAfterSave) { + closeFlyout(); + } + if (reloadConnectors) { + reloadConnectors(); + } + } + }; + return ( @@ -184,40 +230,78 @@ export const ConnectorEditFlyout = ({ ) : null} {flyoutTitle} + + setTab('config')} + data-test-subj="configureConnectorTab" + isSelected={'config' === selectedTab} + > + {i18n.translate('xpack.triggersActionsUI.sections.editConnectorForm.tabText', { + defaultMessage: 'Configuration', + })} + + setTab('test')} + data-test-subj="testConnectorTab" + isSelected={'test' === selectedTab} + > + {i18n.translate('xpack.triggersActionsUI.sections.testConnectorForm.tabText', { + defaultMessage: 'Test', + })} + + - {!connector.isPreconfigured ? ( - { + setHasChanges(true); + // if the user changes the connector, "forget" the last execution + // so the user comes back to a clean form ready to run a fresh test + setTestExecutionResult(none); + dispatch(changes); + }} + actionTypeRegistry={actionTypeRegistry} + http={http} + docLinks={docLinks} + capabilities={capabilities} + consumer={consumer} + /> + ) : ( + + + {i18n.translate( + 'xpack.triggersActionsUI.sections.editConnectorForm.descriptionText', + { + defaultMessage: 'This connector is readonly.', + } + )} + + + + + + ) + ) : ( + - ) : ( - - - {i18n.translate( - 'xpack.triggersActionsUI.sections.editConnectorForm.descriptionText', - { - defaultMessage: 'This connector is readonly.', - } - )} - - - - - )} @@ -232,35 +316,48 @@ export const ConnectorEditFlyout = ({ )} - {canSave && actionTypeModel && !connector.isPreconfigured ? ( - - { - setIsSaving(true); - const savedAction = await onActionConnectorSave(); - setIsSaving(false); - if (savedAction) { - closeFlyout(); - if (reloadConnectors) { - reloadConnectors(); - } - } - }} - > - - - - ) : null} + + + {canSave && actionTypeModel && !connector.isPreconfigured ? ( + + + { + await onSaveClicked(false); + }} + > + + + + + { + await onSaveClicked(); + }} + > + + + + + ) : null} + + diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/test_connector_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/test_connector_form.test.tsx new file mode 100644 index 0000000000000..482bccb5517f1 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/test_connector_form.test.tsx @@ -0,0 +1,212 @@ +/* + * 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, { lazy } from 'react'; +import { I18nProvider } from '@kbn/i18n/react'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; +import TestConnectorForm from './test_connector_form'; +import { none, some } from 'fp-ts/lib/Option'; +import { ActionConnector, ValidationResult } from '../../../types'; +import { actionTypeRegistryMock } from '../../action_type_registry.mock'; +import { ActionsConnectorsContextProvider } from '../../context/actions_connectors_context'; +import { EuiFormRow, EuiFieldText, EuiText, EuiLink, EuiForm, EuiSelect } from '@elastic/eui'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; + +const mockedActionParamsFields = lazy(async () => ({ + default() { + return ( + + + + + + + Link to some help + + } + > + + + + ); + }, +})); + +const actionType = { + id: 'my-action-type', + iconClass: 'test', + selectMessage: 'test', + validateConnector: (): ValidationResult => { + return { errors: {} }; + }, + validateParams: (): ValidationResult => { + const validationResult = { errors: {} }; + return validationResult; + }, + actionConnectorFields: null, + actionParamsFields: mockedActionParamsFields, +}; + +describe('test_connector_form', () => { + let deps: any; + let actionTypeRegistry; + beforeAll(async () => { + actionTypeRegistry = actionTypeRegistryMock.create(); + + const mocks = coreMock.createSetup(); + const [ + { + application: { capabilities }, + }, + ] = await mocks.getStartServices(); + deps = { + http: mocks.http, + toastNotifications: mocks.notifications.toasts, + docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' }, + actionTypeRegistry, + capabilities, + }; + actionTypeRegistry.get.mockReturnValue(actionType); + }); + + it('renders initially as the action form and execute button and no result', async () => { + const connector = { + actionTypeId: actionType.id, + config: {}, + secrets: {}, + } as ActionConnector; + const wrapper = mountWithIntl( + + { + return new Promise(() => {}); + }, + docLinks: deps!.docLinks, + }} + > + {}} + isExecutingAction={false} + onExecutAction={async () => ({ + actionId: '', + status: 'ok', + })} + executionResult={none} + /> + + + ); + const executeActionButton = wrapper?.find('[data-test-subj="executeActionButton"]'); + expect(executeActionButton?.exists()).toBeTruthy(); + expect(executeActionButton?.first().prop('isDisabled')).toBe(false); + + const result = wrapper?.find('[data-test-subj="executionAwaiting"]'); + expect(result?.exists()).toBeTruthy(); + }); + + it('renders successful results', async () => { + const connector = { + actionTypeId: actionType.id, + config: {}, + secrets: {}, + } as ActionConnector; + const wrapper = mountWithIntl( + + { + return new Promise(() => {}); + }, + docLinks: deps!.docLinks, + }} + > + {}} + isExecutingAction={false} + onExecutAction={async () => ({ + actionId: '', + status: 'ok', + })} + executionResult={some({ + actionId: '', + status: 'ok', + })} + /> + + + ); + const result = wrapper?.find('[data-test-subj="executionSuccessfulResult"]'); + expect(result?.exists()).toBeTruthy(); + }); + + it('renders failure results', async () => { + const connector = { + actionTypeId: actionType.id, + config: {}, + secrets: {}, + } as ActionConnector; + const wrapper = mountWithIntl( + + { + return new Promise(() => {}); + }, + docLinks: deps!.docLinks, + }} + > + {}} + isExecutingAction={false} + onExecutAction={async () => ({ + actionId: '', + status: 'error', + message: 'Error Message', + })} + executionResult={some({ + actionId: '', + status: 'error', + message: 'Error Message', + })} + /> + + + ); + const result = wrapper?.find('[data-test-subj="executionFailureResult"]'); + expect(result?.exists()).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/test_connector_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/test_connector_form.tsx new file mode 100644 index 0000000000000..a73fd4e22e637 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/test_connector_form.tsx @@ -0,0 +1,224 @@ +/* + * 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, { Fragment, Suspense } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiButton, + EuiSteps, + EuiLoadingSpinner, + EuiDescriptionList, + EuiCallOut, + EuiSpacer, +} from '@elastic/eui'; +import { Option, map, getOrElse } from 'fp-ts/lib/Option'; +import { pipe } from 'fp-ts/lib/pipeable'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; +import { ActionConnector } from '../../../types'; +import { useActionsConnectorsContext } from '../../context/actions_connectors_context'; +import { ActionTypeExecutorResult } from '../../../../../actions/common'; + +export interface ConnectorAddFlyoutProps { + connector: ActionConnector; + executeEnabled: boolean; + isExecutingAction: boolean; + setActionParams: (params: Record) => void; + actionParams: Record; + onExecutAction: () => Promise>; + executionResult: Option>; +} + +export const TestConnectorForm = ({ + connector, + executeEnabled, + executionResult, + actionParams, + setActionParams, + onExecutAction, + isExecutingAction, +}: ConnectorAddFlyoutProps) => { + const { actionTypeRegistry, docLinks } = useActionsConnectorsContext(); + const actionTypeModel = actionTypeRegistry.get(connector.actionTypeId); + const ParamsFieldsComponent = actionTypeModel.actionParamsFields; + + const actionErrors = actionTypeModel?.validateParams(actionParams); + const hasErrors = !!Object.values(actionErrors.errors).find((errors) => errors.length > 0); + + const steps = [ + { + title: 'Create an action', + children: ParamsFieldsComponent ? ( + + + + + + } + > + + setActionParams({ + ...actionParams, + [field]: value, + }) + } + messageVariables={[]} + docLinks={docLinks} + actionConnector={connector} + /> + + ) : ( + +

This Connector does not require any Action Parameter.

+
+ ), + }, + { + title: 'Run the action', + children: ( + + {executeEnabled ? null : ( + + +

+ +

+
+ +
+ )} + + + + + +
+ ), + }, + { + title: 'Results', + children: pipe( + executionResult, + map((result) => + result.status === 'ok' ? ( + + ) : ( + + ) + ), + getOrElse(() => ) + ), + }, + ]; + + return ; +}; + +const AwaitingExecution = () => ( + +

+ +

+
+); + +const SuccessfulExecution = () => ( + +

+ +

+
+); + +const FailedExecussion = ({ + executionResult: { message, serviceMessage }, +}: { + executionResult: ActionTypeExecutorResult; +}) => { + const items = [ + { + title: i18n.translate( + 'xpack.triggersActionsUI.sections.testConnectorForm.executionFailureDescription', + { + defaultMessage: 'The following error was found:', + } + ), + description: + message ?? + i18n.translate( + 'xpack.triggersActionsUI.sections.testConnectorForm.executionFailureUnknownReason', + { + defaultMessage: 'Unknown reason', + } + ), + }, + ]; + if (serviceMessage) { + items.push({ + title: i18n.translate( + 'xpack.triggersActionsUI.sections.testConnectorForm.executionFailureAdditionalDetails', + { + defaultMessage: 'Details:', + } + ), + description: serviceMessage, + }); + } + return ( + + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { TestConnectorForm as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx index 837529bfc938d..352c9a67bbee2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx @@ -194,55 +194,15 @@ export const ActionsConnectorsList: React.FunctionComponent = () => { truncateText: true, }, { - field: 'isPreconfigured', name: '', - render: (value: number, item: ActionConnectorTableItem) => { - if (item.isPreconfigured) { - return ( - - - - - - ); - } + render: (item: ActionConnectorTableItem) => { return ( - - - setConnectorsToDelete([item.id])} - iconType={'trash'} - /> - - + setConnectorsToDelete([item.id])} + /> ); }, @@ -344,28 +304,6 @@ export const ActionsConnectorsList: React.FunctionComponent = () => { /> ); - const noPermissionPrompt = ( - - - - } - body={ -

- -

- } - /> - ); - return (
{ {data.length === 0 && canSave && !isLoadingActions && !isLoadingActionTypes && ( setAddFlyoutVisibility(true)} /> )} - {data.length === 0 && !canSave && noPermissionPrompt} + {data.length === 0 && !canSave && } { function getActionsCountByActionType(actions: ActionConnector[], actionTypeId: string) { return actions.filter((action) => action.actionTypeId === actionTypeId).length; } + +const DeleteOperation: React.FunctionComponent<{ + item: ActionConnectorTableItem; + canDelete: boolean; + onDelete: () => void; +}> = ({ item, canDelete, onDelete }) => { + if (item.isPreconfigured) { + return ( + + + + ); + } + return ( + + + + + + ); +}; + +const NoPermissionPrompt: React.FunctionComponent<{}> = () => ( + + + + } + body={ +

+ +

+ } + /> +); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs.tsx index 146cebabbb382..110eff36e3df9 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs.tsx @@ -239,7 +239,7 @@ export class UpgradeAssistantTabs extends React.Component { this.setState({ telemetryState: TelemetryState.Running }); - await this.props.http.fetch('/api/upgrade_assistant/telemetry/ui_open', { + await this.props.http.fetch('/api/upgrade_assistant/stats/ui_open', { method: 'PUT', body: JSON.stringify(set({}, tabName, true)), }); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/button.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/button.tsx index a20f4117f693d..747430f455f22 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/button.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/tabs/checkup/deprecations/reindex/button.tsx @@ -239,7 +239,7 @@ export class ReindexButton extends React.Component { }); afterEach(() => jest.clearAllMocks()); - describe('PUT /api/upgrade_assistant/telemetry/ui_open', () => { + describe('PUT /api/upgrade_assistant/stats/ui_open', () => { it('returns correct payload with single option', async () => { const returnPayload = { overview: true, @@ -51,7 +51,7 @@ describe('Upgrade Assistant Telemetry API', () => { const resp = await routeDependencies.router.getHandler({ method: 'put', - pathPattern: '/api/upgrade_assistant/telemetry/ui_open', + pathPattern: '/api/upgrade_assistant/stats/ui_open', })( routeHandlerContextMock, createRequestMock({ body: returnPayload }), @@ -72,7 +72,7 @@ describe('Upgrade Assistant Telemetry API', () => { const resp = await routeDependencies.router.getHandler({ method: 'put', - pathPattern: '/api/upgrade_assistant/telemetry/ui_open', + pathPattern: '/api/upgrade_assistant/stats/ui_open', })( routeHandlerContextMock, createRequestMock({ @@ -93,7 +93,7 @@ describe('Upgrade Assistant Telemetry API', () => { const resp = await routeDependencies.router.getHandler({ method: 'put', - pathPattern: '/api/upgrade_assistant/telemetry/ui_open', + pathPattern: '/api/upgrade_assistant/stats/ui_open', })( routeHandlerContextMock, createRequestMock({ @@ -108,7 +108,7 @@ describe('Upgrade Assistant Telemetry API', () => { }); }); - describe('PUT /api/upgrade_assistant/telemetry/ui_reindex', () => { + describe('PUT /api/upgrade_assistant/stats/ui_reindex', () => { it('returns correct payload with single option', async () => { const returnPayload = { close: false, @@ -121,7 +121,7 @@ describe('Upgrade Assistant Telemetry API', () => { const resp = await routeDependencies.router.getHandler({ method: 'put', - pathPattern: '/api/upgrade_assistant/telemetry/ui_reindex', + pathPattern: '/api/upgrade_assistant/stats/ui_reindex', })( routeHandlerContextMock, createRequestMock({ @@ -147,7 +147,7 @@ describe('Upgrade Assistant Telemetry API', () => { const resp = await routeDependencies.router.getHandler({ method: 'put', - pathPattern: '/api/upgrade_assistant/telemetry/ui_reindex', + pathPattern: '/api/upgrade_assistant/stats/ui_reindex', })( routeHandlerContextMock, createRequestMock({ @@ -169,7 +169,7 @@ describe('Upgrade Assistant Telemetry API', () => { const resp = await routeDependencies.router.getHandler({ method: 'put', - pathPattern: '/api/upgrade_assistant/telemetry/ui_reindex', + pathPattern: '/api/upgrade_assistant/stats/ui_reindex', })( routeHandlerContextMock, createRequestMock({ diff --git a/x-pack/plugins/upgrade_assistant/server/routes/telemetry.ts b/x-pack/plugins/upgrade_assistant/server/routes/telemetry.ts index 900a5e64c55c3..71f5de01f6a44 100644 --- a/x-pack/plugins/upgrade_assistant/server/routes/telemetry.ts +++ b/x-pack/plugins/upgrade_assistant/server/routes/telemetry.ts @@ -12,7 +12,7 @@ import { RouteDependencies } from '../types'; export function registerTelemetryRoutes({ router, getSavedObjectsService }: RouteDependencies) { router.put( { - path: '/api/upgrade_assistant/telemetry/ui_open', + path: '/api/upgrade_assistant/stats/ui_open', validate: { body: schema.object({ overview: schema.boolean({ defaultValue: false }), @@ -40,7 +40,7 @@ export function registerTelemetryRoutes({ router, getSavedObjectsService }: Rout router.put( { - path: '/api/upgrade_assistant/telemetry/ui_reindex', + path: '/api/upgrade_assistant/stats/ui_reindex', validate: { body: schema.object({ close: schema.boolean({ defaultValue: false }), diff --git a/x-pack/plugins/uptime/public/components/common/charts/duration_line_series_list.tsx b/x-pack/plugins/uptime/public/components/common/charts/duration_line_series_list.tsx index 4223e918393b6..edb7e13ed064f 100644 --- a/x-pack/plugins/uptime/public/components/common/charts/duration_line_series_list.tsx +++ b/x-pack/plugins/uptime/public/components/common/charts/duration_line_series_list.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { LineSeries, CurveType } from '@elastic/charts'; +import { LineSeries, CurveType, Fit } from '@elastic/charts'; import { LocationDurationLine } from '../../../../common/types'; import { convertMicrosecondsToMilliseconds as microsToMillis } from '../../../lib/helper'; @@ -28,6 +28,7 @@ export const DurationLineSeriesList = ({ lines }: Props) => ( yAccessors={[1]} yScaleToDataExtent={false} yScaleType="linear" + fit={Fit.Linear} /> ))} diff --git a/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx b/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx index 39b8a38f60982..3f252c2a436ae 100644 --- a/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx +++ b/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx @@ -17,6 +17,7 @@ import { EuiTitle, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useContext } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; +import numeral from '@elastic/numeral'; import moment from 'moment'; import { getChartDateLabel } from '../../../lib/helper'; import { ChartWrapper } from './chart_wrapper'; @@ -144,6 +145,8 @@ export const PingHistogramComponent: React.FC = ({ defaultMessage: 'Ping Y Axis', })} position="left" + tickFormat={(d) => numeral(d).format('0')} + labelFormat={(d) => numeral(d).format('0a')} title={i18n.translate('xpack.uptime.snapshotHistogram.yAxis.title', { defaultMessage: 'Pings', description: diff --git a/x-pack/test/accessibility/apps/dashboard_edit_panel.ts b/x-pack/test/accessibility/apps/dashboard_edit_panel.ts deleted file mode 100644 index 9c0827e4128cf..0000000000000 --- a/x-pack/test/accessibility/apps/dashboard_edit_panel.ts +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { FtrProviderContext } from '../ftr_provider_context'; - -export default function ({ getService, getPageObjects }: FtrProviderContext) { - const { dashboard } = getPageObjects(['dashboard']); - const a11y = getService('a11y'); - const dashboardPanelActions = getService('dashboardPanelActions'); - const testSubjects = getService('testSubjects'); - const esArchiver = getService('esArchiver'); - const drilldowns = getService('dashboardDrilldownsManage'); - const kibanaServer = getService('kibanaServer'); - const PageObjects = getPageObjects(['security', 'common']); - const toasts = getService('toasts'); - - describe('Dashboard Edit Panel', () => { - before(async () => { - await esArchiver.load('dashboard/drilldowns'); - await esArchiver.loadIfNeeded('logstash_functional'); - await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' }); - await PageObjects.common.navigateToApp('dashboard'); - await dashboard.loadSavedDashboard(drilldowns.DASHBOARD_WITH_PIE_CHART_NAME); - await testSubjects.click('dashboardEditMode'); - }); - - after(async () => { - await esArchiver.unload('dashboard/drilldowns'); - }); - - // embeddable edit panel - it(' A11y test on dashboard edit panel menu options', async () => { - await testSubjects.click('embeddablePanelToggleMenuIcon'); - await a11y.testAppSnapshot(); - }); - - // clone panel - it(' A11y test on dashboard embeddable clone panel', async () => { - await testSubjects.click('embeddablePanelAction-clonePanel'); - await a11y.testAppSnapshot(); - await toasts.dismissAllToasts(); - await dashboardPanelActions.removePanelByTitle('Visualization PieChart (copy)'); - }); - - // edit dashboard title - it(' A11y test on dashboard embeddable edit dashboard title', async () => { - await testSubjects.click('embeddablePanelToggleMenuIcon'); - await testSubjects.click('embeddablePanelAction-ACTION_CUSTOMIZE_PANEL'); - await a11y.testAppSnapshot(); - await testSubjects.click('customizePanelHideTitle'); - await a11y.testAppSnapshot(); - await testSubjects.click('saveNewTitleButton'); - }); - - // https://github.com/elastic/kibana/issues/77931 - it.skip('A11y test for edit visualization and save', async () => { - await testSubjects.click('embeddablePanelToggleMenuIcon'); - await testSubjects.click('embeddablePanelAction-editPanel'); - await testSubjects.click('visualizesaveAndReturnButton'); - await a11y.testAppSnapshot(); - }); - - // https://github.com/elastic/kibana/issues/77422 - it.skip('A11y test on dashboard embeddable custom time range', async () => { - await testSubjects.click('embeddablePanelToggleMenuIcon'); - await testSubjects.click('embeddablePanelAction-CUSTOM_TIME_RANGE'); - await a11y.testAppSnapshot(); - }); - - // inspector panel - it('A11y test on dashboard embeddable open inspector', async () => { - await testSubjects.click('embeddablePanelToggleMenuIcon'); - await testSubjects.click('embeddablePanelAction-openInspector'); - await a11y.testAppSnapshot(); - await testSubjects.click('euiFlyoutCloseButton'); - }); - - // create drilldown - it('A11y test on dashboard embeddable open flyout and drilldown', async () => { - await testSubjects.click('embeddablePanelToggleMenuIcon'); - await testSubjects.click('embeddablePanelAction-OPEN_FLYOUT_ADD_DRILLDOWN'); - await a11y.testAppSnapshot(); - await testSubjects.click('flyoutCloseButton'); - }); - - // fullscreen - it('A11y test on dashboard embeddable fullscreen', async () => { - await testSubjects.click('embeddablePanelToggleMenuIcon'); - await testSubjects.click('embeddablePanelAction-togglePanel'); - await a11y.testAppSnapshot(); - }); - - // minimize fullscreen panel - it('A11y test on dashboard embeddable fullscreen minimize ', async () => { - await testSubjects.click('embeddablePanelToggleMenuIcon'); - await testSubjects.click('embeddablePanelAction-togglePanel'); - await a11y.testAppSnapshot(); - }); - - // replace panel - it('A11y test on dashboard embeddable replace panel', async () => { - await testSubjects.click('embeddablePanelToggleMenuIcon'); - await testSubjects.click('embeddablePanelAction-replacePanel'); - await a11y.testAppSnapshot(); - await testSubjects.click('euiFlyoutCloseButton'); - }); - - // delete from dashboard - it('A11y test on dashboard embeddable delete panel', async () => { - await testSubjects.click('embeddablePanelToggleMenuIcon'); - await testSubjects.click('embeddablePanelAction-deletePanel'); - await a11y.testAppSnapshot(); - }); - }); -} diff --git a/x-pack/test/accessibility/config.ts b/x-pack/test/accessibility/config.ts index 9382a7186db54..bae7b688fd28c 100644 --- a/x-pack/test/accessibility/config.ts +++ b/x-pack/test/accessibility/config.ts @@ -21,7 +21,6 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('./apps/search_profiler'), require.resolve('./apps/uptime'), require.resolve('./apps/spaces'), - require.resolve('./apps/dashboard_edit_panel'), ], pageObjects, services, diff --git a/x-pack/test/apm_api_integration/trial/tests/csm/__snapshots__/page_views.snap b/x-pack/test/apm_api_integration/trial/tests/csm/__snapshots__/page_views.snap new file mode 100644 index 0000000000000..38b009fc73d34 --- /dev/null +++ b/x-pack/test/apm_api_integration/trial/tests/csm/__snapshots__/page_views.snap @@ -0,0 +1,280 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CSM page views when there is data returns page views 1`] = ` +Object { + "items": Array [ + Object { + "x": 1600149947000, + "y": 1, + }, + Object { + "x": 1600149957000, + "y": 0, + }, + Object { + "x": 1600149967000, + "y": 0, + }, + Object { + "x": 1600149977000, + "y": 0, + }, + Object { + "x": 1600149987000, + "y": 0, + }, + Object { + "x": 1600149997000, + "y": 0, + }, + Object { + "x": 1600150007000, + "y": 0, + }, + Object { + "x": 1600150017000, + "y": 0, + }, + Object { + "x": 1600150027000, + "y": 1, + }, + Object { + "x": 1600150037000, + "y": 0, + }, + Object { + "x": 1600150047000, + "y": 0, + }, + Object { + "x": 1600150057000, + "y": 0, + }, + Object { + "x": 1600150067000, + "y": 0, + }, + Object { + "x": 1600150077000, + "y": 1, + }, + Object { + "x": 1600150087000, + "y": 0, + }, + Object { + "x": 1600150097000, + "y": 0, + }, + Object { + "x": 1600150107000, + "y": 0, + }, + Object { + "x": 1600150117000, + "y": 0, + }, + Object { + "x": 1600150127000, + "y": 0, + }, + Object { + "x": 1600150137000, + "y": 0, + }, + Object { + "x": 1600150147000, + "y": 0, + }, + Object { + "x": 1600150157000, + "y": 0, + }, + Object { + "x": 1600150167000, + "y": 0, + }, + Object { + "x": 1600150177000, + "y": 1, + }, + Object { + "x": 1600150187000, + "y": 0, + }, + Object { + "x": 1600150197000, + "y": 0, + }, + Object { + "x": 1600150207000, + "y": 1, + }, + Object { + "x": 1600150217000, + "y": 0, + }, + Object { + "x": 1600150227000, + "y": 0, + }, + Object { + "x": 1600150237000, + "y": 1, + }, + ], + "topItems": Array [], +} +`; + +exports[`CSM page views when there is data returns page views with breakdown 1`] = ` +Object { + "items": Array [ + Object { + "Chrome": 1, + "x": 1600149947000, + "y": 1, + }, + Object { + "x": 1600149957000, + "y": 0, + }, + Object { + "x": 1600149967000, + "y": 0, + }, + Object { + "x": 1600149977000, + "y": 0, + }, + Object { + "x": 1600149987000, + "y": 0, + }, + Object { + "x": 1600149997000, + "y": 0, + }, + Object { + "x": 1600150007000, + "y": 0, + }, + Object { + "x": 1600150017000, + "y": 0, + }, + Object { + "Chrome": 1, + "x": 1600150027000, + "y": 1, + }, + Object { + "x": 1600150037000, + "y": 0, + }, + Object { + "x": 1600150047000, + "y": 0, + }, + Object { + "x": 1600150057000, + "y": 0, + }, + Object { + "x": 1600150067000, + "y": 0, + }, + Object { + "Chrome": 1, + "x": 1600150077000, + "y": 1, + }, + Object { + "x": 1600150087000, + "y": 0, + }, + Object { + "x": 1600150097000, + "y": 0, + }, + Object { + "x": 1600150107000, + "y": 0, + }, + Object { + "x": 1600150117000, + "y": 0, + }, + Object { + "x": 1600150127000, + "y": 0, + }, + Object { + "x": 1600150137000, + "y": 0, + }, + Object { + "x": 1600150147000, + "y": 0, + }, + Object { + "x": 1600150157000, + "y": 0, + }, + Object { + "x": 1600150167000, + "y": 0, + }, + Object { + "Chrome": 1, + "x": 1600150177000, + "y": 1, + }, + Object { + "x": 1600150187000, + "y": 0, + }, + Object { + "x": 1600150197000, + "y": 0, + }, + Object { + "Chrome Mobile": 1, + "x": 1600150207000, + "y": 1, + }, + Object { + "x": 1600150217000, + "y": 0, + }, + Object { + "x": 1600150227000, + "y": 0, + }, + Object { + "Chrome Mobile": 1, + "x": 1600150237000, + "y": 1, + }, + ], + "topItems": Array [ + "Chrome", + "Chrome Mobile", + ], +} +`; + +exports[`CSM page views when there is no data returns empty list 1`] = ` +Object { + "items": Array [], + "topItems": Array [], +} +`; + +exports[`CSM page views when there is no data returns empty list with breakdowns 1`] = ` +Object { + "items": Array [], + "topItems": Array [], +} +`; diff --git a/x-pack/test/apm_api_integration/trial/tests/csm/page_views.ts b/x-pack/test/apm_api_integration/trial/tests/csm/page_views.ts new file mode 100644 index 0000000000000..ca5670d41d8ee --- /dev/null +++ b/x-pack/test/apm_api_integration/trial/tests/csm/page_views.ts @@ -0,0 +1,65 @@ +/* + * 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 expect from '@kbn/expect'; +import { expectSnapshot } from '../../../common/match_snapshot'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +export default function rumServicesApiTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('CSM page views', () => { + describe('when there is no data', () => { + it('returns empty list', async () => { + const response = await supertest.get( + '/api/apm/rum-client/page-view-trends?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-14T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22elastic-co-rum-test%22%5D%7D' + ); + + expect(response.status).to.be(200); + expectSnapshot(response.body).toMatch(); + }); + it('returns empty list with breakdowns', async () => { + const response = await supertest.get( + '/api/apm/rum-client/page-view-trends?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-14T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22elastic-co-rum-test%22%5D%7D&breakdowns=%7B%22name%22%3A%22Browser%22%2C%22fieldName%22%3A%22user_agent.name%22%2C%22type%22%3A%22category%22%7D' + ); + + expect(response.status).to.be(200); + expectSnapshot(response.body).toMatch(); + }); + }); + + describe('when there is data', () => { + before(async () => { + await esArchiver.load('8.0.0'); + await esArchiver.load('rum_8.0.0'); + }); + after(async () => { + await esArchiver.unload('8.0.0'); + await esArchiver.unload('rum_8.0.0'); + }); + + it('returns page views', async () => { + const response = await supertest.get( + '/api/apm/rum-client/page-view-trends?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-16T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22kibana-frontend-8_0_0%22%5D%7D' + ); + + expect(response.status).to.be(200); + + expectSnapshot(response.body).toMatch(); + }); + it('returns page views with breakdown', async () => { + const response = await supertest.get( + '/api/apm/rum-client/page-view-trends?start=2020-09-07T20%3A35%3A54.654Z&end=2020-09-16T20%3A35%3A54.654Z&uiFilters=%7B%22serviceName%22%3A%5B%22kibana-frontend-8_0_0%22%5D%7D&breakdowns=%7B%22name%22%3A%22Browser%22%2C%22fieldName%22%3A%22user_agent.name%22%2C%22type%22%3A%22category%22%7D' + ); + + expect(response.status).to.be(200); + + expectSnapshot(response.body).toMatch(); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/trial/tests/index.ts b/x-pack/test/apm_api_integration/trial/tests/index.ts index ae62253c62d81..a026f91a02cd7 100644 --- a/x-pack/test/apm_api_integration/trial/tests/index.ts +++ b/x-pack/test/apm_api_integration/trial/tests/index.ts @@ -35,6 +35,7 @@ export default function observabilityApiIntegrationTests({ loadTestFile }: FtrPr loadTestFile(require.resolve('./csm/csm_services.ts')); loadTestFile(require.resolve('./csm/web_core_vitals.ts')); loadTestFile(require.resolve('./csm/long_task_metrics.ts')); + loadTestFile(require.resolve('./csm/page_views.ts')); }); }); } diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts index 86e355988da0b..151c837640228 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/connectors.ts @@ -17,6 +17,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const testSubjects = getService('testSubjects'); const pageObjects = getPageObjects(['common', 'triggersActionsUI', 'header']); const find = getService('find'); + const retry = getService('retry'); + const comboBox = getService('comboBox'); describe('Connectors', function () { before(async () => { @@ -76,7 +78,9 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.setValue('slackWebhookUrlInput', 'https://test'); - await find.clickByCssSelector('[data-test-subj="saveEditedActionButton"]:not(disabled)'); + await find.clickByCssSelector( + '[data-test-subj="saveAndCloseEditedActionButton"]:not(disabled)' + ); const toastTitle = await pageObjects.common.closeToast(); expect(toastTitle).to.eql(`Updated '${updatedConnectorName}'`); @@ -92,6 +96,64 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { ]); }); + it('should test a connector and display a successful result', async () => { + const connectorName = generateUniqueKey(); + const indexName = generateUniqueKey(); + await createIndexConnector(connectorName, indexName); + + await pageObjects.triggersActionsUI.searchConnectors(connectorName); + + const searchResultsBeforeEdit = await pageObjects.triggersActionsUI.getConnectorsList(); + expect(searchResultsBeforeEdit.length).to.eql(1); + + await find.clickByCssSelector('[data-test-subj="connectorsTableCell-name"] button'); + + await find.clickByCssSelector('[data-test-subj="testConnectorTab"]'); + + // test success + await testSubjects.setValue('documentsJsonEditor', '{ "key": "value" }'); + + await find.clickByCssSelector('[data-test-subj="executeActionButton"]:not(disabled)'); + + await retry.try(async () => { + await testSubjects.find('executionSuccessfulResult'); + }); + + await find.clickByCssSelector( + '[data-test-subj="cancelSaveEditedConnectorButton"]:not(disabled)' + ); + }); + + it('should test a connector and display a failure result', async () => { + const connectorName = generateUniqueKey(); + const indexName = generateUniqueKey(); + await createIndexConnector(connectorName, indexName); + + await pageObjects.triggersActionsUI.searchConnectors(connectorName); + + const searchResultsBeforeEdit = await pageObjects.triggersActionsUI.getConnectorsList(); + expect(searchResultsBeforeEdit.length).to.eql(1); + + await find.clickByCssSelector('[data-test-subj="connectorsTableCell-name"] button'); + + await find.clickByCssSelector('[data-test-subj="testConnectorTab"]'); + + await testSubjects.setValue('documentsJsonEditor', '{ "": "value" }'); + + await find.clickByCssSelector('[data-test-subj="executeActionButton"]:not(disabled)'); + + await retry.try(async () => { + const executionFailureResultCallout = await testSubjects.find('executionFailureResult'); + expect(await executionFailureResultCallout.getVisibleText()).to.match( + /error indexing documents/ + ); + }); + + await find.clickByCssSelector( + '[data-test-subj="cancelSaveEditedConnectorButton"]:not(disabled)' + ); + }); + it('should reset connector when canceling an edit', async () => { const connectorName = generateUniqueKey(); await createConnector(connectorName); @@ -193,7 +255,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await find.clickByCssSelector('[data-test-subj="connectorsTableCell-name"] button'); expect(await testSubjects.exists('preconfiguredBadge')).to.be(true); - expect(await testSubjects.exists('saveEditedActionButton')).to.be(false); + expect(await testSubjects.exists('saveAndCloseEditedActionButton')).to.be(false); }); }); @@ -209,4 +271,17 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await find.clickByCssSelector('[data-test-subj="saveNewActionButton"]:not(disabled)'); await pageObjects.common.closeToast(); } + + async function createIndexConnector(connectorName: string, indexName: string) { + await pageObjects.triggersActionsUI.clickCreateConnectorButton(); + + await testSubjects.click('.index-card'); + + await testSubjects.setValue('nameInput', connectorName); + + await comboBox.set('connectorIndexesComboBox', indexName); + + await find.clickByCssSelector('[data-test-subj="saveNewActionButton"]:not(disabled)'); + await pageObjects.common.closeToast(); + } }; diff --git a/yarn.lock b/yarn.lock index cec2697f6c15c..9e96158771cde 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2458,6 +2458,25 @@ jsonwebtoken "^8.3.0" lru-cache "^5.1.1" +"@octokit/auth-token@^2.4.0": + version "2.4.2" + resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.4.2.tgz#10d0ae979b100fa6b72fa0e8e63e27e6d0dbff8a" + integrity sha512-jE/lE/IKIz2v1+/P0u4fJqv0kYwXOTujKemJMFr6FeopsxlIK3+wKDCJGnysg81XID5TgZQbIfuJ5J0lnTiuyQ== + dependencies: + "@octokit/types" "^5.0.0" + +"@octokit/core@^3.0.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@octokit/core/-/core-3.1.2.tgz#c937d5f9621b764573068fcd2e5defcc872fd9cc" + integrity sha512-AInOFULmwOa7+NFi9F8DlDkm5qtZVmDQayi7TUgChE3yeIGPq0Y+6cAEXPexQ3Ea+uZy66hKEazR7DJyU+4wfw== + dependencies: + "@octokit/auth-token" "^2.4.0" + "@octokit/graphql" "^4.3.1" + "@octokit/request" "^5.4.0" + "@octokit/types" "^5.0.0" + before-after-hook "^2.1.0" + universal-user-agent "^6.0.0" + "@octokit/endpoint@^3.2.0": version "3.2.3" resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-3.2.3.tgz#bd9aea60cd94ce336656b57a5c9cb7f10be8f4f3" @@ -2468,6 +2487,44 @@ universal-user-agent "^2.0.1" url-template "^2.0.8" +"@octokit/endpoint@^6.0.1": + version "6.0.6" + resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-6.0.6.tgz#4f09f2b468976b444742a1d5069f6fa45826d999" + integrity sha512-7Cc8olaCoL/mtquB7j/HTbPM+sY6Ebr4k2X2y4JoXpVKQ7r5xB4iGQE0IoO58wIPsUk4AzoT65AMEpymSbWTgQ== + dependencies: + "@octokit/types" "^5.0.0" + is-plain-object "^5.0.0" + universal-user-agent "^6.0.0" + +"@octokit/graphql@^4.3.1": + version "4.5.6" + resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-4.5.6.tgz#708143ba15cf7c1879ed6188266e7f270be805d4" + integrity sha512-Rry+unqKTa3svswT2ZAuqenpLrzJd+JTv89LTeVa5UM/5OX8o4KTkPL7/1ABq4f/ZkELb0XEK/2IEoYwykcLXg== + dependencies: + "@octokit/request" "^5.3.0" + "@octokit/types" "^5.0.0" + universal-user-agent "^6.0.0" + +"@octokit/plugin-paginate-rest@^2.2.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.4.0.tgz#92f951ddc8a1cd505353fa07650752ca25ed7e93" + integrity sha512-YT6Klz3LLH6/nNgi0pheJnUmTFW4kVnxGft+v8Itc41IIcjl7y1C8TatmKQBbCSuTSNFXO5pCENnqg6sjwpJhg== + dependencies: + "@octokit/types" "^5.5.0" + +"@octokit/plugin-request-log@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-1.0.0.tgz#eef87a431300f6148c39a7f75f8cfeb218b2547e" + integrity sha512-ywoxP68aOT3zHCLgWZgwUJatiENeHE7xJzYjfz8WI0goynp96wETBF+d95b8g/uL4QmS6owPVlaxiz3wyMAzcw== + +"@octokit/plugin-rest-endpoint-methods@4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-4.2.0.tgz#c5a0691b3aba5d8b4ef5dffd6af3649608f167ba" + integrity sha512-1/qn1q1C1hGz6W/iEDm9DoyNoG/xdFDt78E3eZ5hHeUfJTLJgyAMdj9chL/cNBHjcjd+FH5aO1x0VCqR2RE0mw== + dependencies: + "@octokit/types" "^5.5.0" + deprecation "^2.3.1" + "@octokit/plugin-retry@^2.2.0": version "2.2.0" resolved "https://registry.yarnpkg.com/@octokit/plugin-retry/-/plugin-retry-2.2.0.tgz#11f3957a46ccdb7b7f33caabf8c17e57b25b80b2" @@ -2475,6 +2532,15 @@ dependencies: bottleneck "^2.15.3" +"@octokit/request-error@^2.0.0": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-2.0.2.tgz#0e76b83f5d8fdda1db99027ea5f617c2e6ba9ed0" + integrity sha512-2BrmnvVSV1MXQvEkrb9zwzP0wXFNbPJij922kYBTLIlIafukrGOb+ABBT2+c6wZiuyWDH1K1zmjGQ0toN/wMWw== + dependencies: + "@octokit/types" "^5.0.1" + deprecation "^2.0.0" + once "^1.4.0" + "@octokit/request@2.4.2", "@octokit/request@^2.1.2", "@octokit/request@^2.4.2": version "2.4.2" resolved "https://registry.yarnpkg.com/@octokit/request/-/request-2.4.2.tgz#87c36e820dd1e43b1629f4f35c95b00cd456320b" @@ -2487,6 +2553,20 @@ once "^1.4.0" universal-user-agent "^2.0.1" +"@octokit/request@^5.3.0", "@octokit/request@^5.4.0": + version "5.4.9" + resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.4.9.tgz#0a46f11b82351b3416d3157261ad9b1558c43365" + integrity sha512-CzwVvRyimIM1h2n9pLVYfTDmX9m+KHSgCpqPsY8F1NdEK8IaWqXhSBXsdjOBFZSpEcxNEeg4p0UO9cQ8EnOCLA== + dependencies: + "@octokit/endpoint" "^6.0.1" + "@octokit/request-error" "^2.0.0" + "@octokit/types" "^5.0.0" + deprecation "^2.0.0" + is-plain-object "^5.0.0" + node-fetch "^2.6.1" + once "^1.4.0" + universal-user-agent "^6.0.0" + "@octokit/rest@^16.23.2": version "16.23.2" resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-16.23.2.tgz#975e84610427c4ab6c41bec77c24aed9b7563db4" @@ -2505,6 +2585,23 @@ universal-user-agent "^2.0.0" url-template "^2.0.8" +"@octokit/rest@^18.0.6": + version "18.0.6" + resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-18.0.6.tgz#76c274f1a68f40741a131768ef483f041e7b98b6" + integrity sha512-ES4lZBKPJMX/yUoQjAZiyFjei9pJ4lTTfb9k7OtYoUzKPDLl/M8jiHqt6qeSauyU4eZGLw0sgP1WiQl9FYeM5w== + dependencies: + "@octokit/core" "^3.0.0" + "@octokit/plugin-paginate-rest" "^2.2.0" + "@octokit/plugin-request-log" "^1.0.0" + "@octokit/plugin-rest-endpoint-methods" "4.2.0" + +"@octokit/types@^5.0.0", "@octokit/types@^5.0.1", "@octokit/types@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-5.5.0.tgz#e5f06e8db21246ca102aa28444cdb13ae17a139b" + integrity sha512-UZ1pErDue6bZNjYOotCNveTXArOMZQFG6hKJfOnGnulVCMcVVi7YIIuuR4WfBhjo7zgpmzn/BkPDnUXtNx+PcQ== + dependencies: + "@types/node" ">= 8" + "@percy/agent@^0.26.0": version "0.26.0" resolved "https://registry.yarnpkg.com/@percy/agent/-/agent-0.26.0.tgz#9f06849d752df7368198835d0b5edc16c2d69a0c" @@ -4112,16 +4209,30 @@ "@types/node" "*" "@types/webpack" "*" -"@types/lodash@^4.14.159": - version "4.14.159" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.159.tgz#61089719dc6fdd9c5cb46efc827f2571d1517065" - integrity sha512-gF7A72f7WQN33DpqOWw9geApQPh4M3PxluMtaHxWHXEGSN12/WbcEk/eNSqWNQcQhF66VSZ06vCF94CrHwXJDg== +"@types/lodash.difference@^4.5.6": + version "4.5.6" + resolved "https://registry.yarnpkg.com/@types/lodash.difference/-/lodash.difference-4.5.6.tgz#41ec5c4e684eeacf543848a9a1b2a4856ccf9853" + integrity sha512-wXH53r+uoUCrKhmh7S5Gf6zo3vpsx/zH2R4pvkmDlmopmMTCROAUXDpPMXATGCWkCjE6ik3VZzZUxBgMjZho9Q== + dependencies: + "@types/lodash" "*" -"@types/lodash@^4.14.160": +"@types/lodash.intersection@^4.4.6": + version "4.4.6" + resolved "https://registry.yarnpkg.com/@types/lodash.intersection/-/lodash.intersection-4.4.6.tgz#0fb241badf6edbb2a7d194a70c50e950e2486e68" + integrity sha512-6ewsKax7+HgT+7mEhzXT6tIyIHc/mjCwZJnarvLbCrtW21qmDQHWbaJj4Ht4DQDBmMdnvZe8APuVlsMpZ5E5mQ== + dependencies: + "@types/lodash" "*" + +"@types/lodash@*", "@types/lodash@^4.14.160": version "4.14.161" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.161.tgz#a21ca0777dabc6e4f44f3d07f37b765f54188b18" integrity sha512-EP6O3Jkr7bXvZZSZYlsgt5DIjiGr0dXP1/jVEwVLTFgg0d+3lWVQkRavYVQszV7dYUwvg0B8R0MBDpcmXg7XIA== +"@types/lodash@^4.14.159": + version "4.14.159" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.159.tgz#61089719dc6fdd9c5cb46efc827f2571d1517065" + integrity sha512-gF7A72f7WQN33DpqOWw9geApQPh4M3PxluMtaHxWHXEGSN12/WbcEk/eNSqWNQcQhF66VSZ06vCF94CrHwXJDg== + "@types/log-symbols@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@types/log-symbols/-/log-symbols-2.0.0.tgz#7919e2ec3c8d13879bfdcab310dd7a3f7fc9466d" @@ -4269,7 +4380,7 @@ dependencies: "@types/node" "*" -"@types/node@*", "@types/node@8.10.54", "@types/node@>=10.17.17 <10.20.0", "@types/node@>=8.9.0", "@types/node@^12.0.2": +"@types/node@*", "@types/node@8.10.54", "@types/node@>= 8", "@types/node@>=10.17.17 <10.20.0", "@types/node@>=8.9.0", "@types/node@^12.0.2": version "10.17.26" resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.26.tgz#a8a119960bff16b823be4c617da028570779bcfd" integrity sha512-myMwkO2Cr82kirHY8uknNRHEVtn0wV3DTQfkrjx17jmkstDRZ24gNUdl8AHXVyVclTYI/bNjgTPTAWvWLqXqkw== @@ -7213,26 +7324,31 @@ bach@^1.0.0: async-settle "^1.0.0" now-and-later "^2.0.0" -backport@5.5.1: - version "5.5.1" - resolved "https://registry.yarnpkg.com/backport/-/backport-5.5.1.tgz#2eeddbdc4cfc0530119bdb2b0c3c30bc7ef574dd" - integrity sha512-vQuGrxxMx9H64ywqsIYUHL8+/xvPeP0nnBa0YQt5S+XqW7etaqOoa5dFW0c77ADdqjfLlGUIvtc2i6UrmqeFUQ== +backport@5.6.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/backport/-/backport-5.6.0.tgz#6dcc0485e5eecf66bb6f950983fd0b018217ec20" + integrity sha512-wz7Ve3uslhGUMtHuctqIEtZFItTGKRRMiNANYso0iw1ar81ILsczDGgxeOlzmmnIQFi1ZvEs6lX3cgypGfef9A== dependencies: - axios "^0.19.2" + "@octokit/rest" "^18.0.6" + "@types/lodash.difference" "^4.5.6" + "@types/lodash.intersection" "^4.4.6" + axios "^0.19.0" dedent "^0.7.0" del "^5.1.0" - find-up "^4.1.0" - inquirer "^7.3.1" + find-up "^5.0.0" + inquirer "^7.3.3" + lodash.difference "^4.5.0" lodash.flatmap "^4.5.0" + lodash.intersection "^4.4.0" lodash.isempty "^4.4.0" lodash.isstring "^4.0.1" lodash.uniq "^4.5.0" make-dir "^3.1.0" - ora "^4.0.4" + ora "^5.1.0" safe-json-stringify "^1.2.0" - strip-json-comments "^3.1.0" + strip-json-comments "^3.1.1" winston "^3.3.3" - yargs "^15.4.0" + yargs "^16.0.3" bail@^1.0.0: version "1.0.2" @@ -7306,6 +7422,11 @@ before-after-hook@^1.4.0: resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-1.4.0.tgz#2b6bf23dca4f32e628fd2747c10a37c74a4b484d" integrity sha512-l5r9ir56nda3qu14nAXIlyq1MmUSs0meCIaFAh8HwkFwP1F8eToOuS3ah2VAHHcY04jaYD7FpJC5JTXHYRbkzg== +before-after-hook@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.1.0.tgz#b6c03487f44e24200dd30ca5e6a1979c5d2fb635" + integrity sha512-IWIbu7pMqyw3EAJHzzHbWa85b6oud/yfKYg5rqB5hNE8CeMi3nX+2C2sj0HswfblST86hpVEOAb9x34NZd6P7A== + big-integer@^1.6.16: version "1.6.48" resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.48.tgz#8fd88bd1632cba4a1c8c3e3d7159f08bb95b4b9e" @@ -8622,10 +8743,10 @@ cli-spinners@^2.0.0: resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.1.0.tgz#22c34b4d51f573240885b201efda4e4ec9fff3c7" integrity sha512-8B00fJOEh1HPrx4fo5eW16XmE1PcL1tGpGrxy63CXGP9nHdPBN63X75hA1zhvQuhVztJWLqV58Roj2qlNM7cAA== -cli-spinners@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.2.0.tgz#e8b988d9206c692302d8ee834e7a85c0144d8f77" - integrity sha512-tgU3fKwzYjiLEQgPMD9Jt+JjHVL9kW93FiIMX/l7rivvOD4/LL0Mf7gda3+4U2KJBloybwgj5KEoQgGRioMiKQ== +cli-spinners@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.4.0.tgz#c6256db216b878cfba4720e719cec7cf72685d7f" + integrity sha512-sJAofoarcm76ZGpuooaO0eDy8saEy+YoZBLjC4h8srt4jeBnkYeOgqxgsJQTpyt2LjI5PTfLJHSL+41Yu4fEJA== cli-table3@0.5.1: version "0.5.1" @@ -8744,6 +8865,15 @@ cliui@^6.0.0: strip-ansi "^6.0.0" wrap-ansi "^6.2.0" +cliui@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.1.tgz#a4cb67aad45cd83d8d05128fc9f4d8fbb887e6b3" + integrity sha512-rcvHOWyGyid6I1WjT/3NatKj2kDt9OdSHSXpyLXaMWFbKpGACNW8pRhhdPUq9MWUOdwn8Rz9AVETjF4105rZZQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + clone-buffer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58" @@ -10639,6 +10769,11 @@ deprecation@^1.0.1: resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-1.0.1.tgz#2df79b79005752180816b7b6e079cbd80490d711" integrity sha512-ccVHpE72+tcIKaGMql33x5MAjKQIZrk+3x2GbJ7TeraUCZWHoT+KSZpoC+JQFsUBlSTXUrBaGiF0j6zVTepPLg== +deprecation@^2.0.0, deprecation@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" + integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== + des.js@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc" @@ -11755,6 +11890,11 @@ es6-weak-map@^2.0.1, es6-weak-map@^2.0.2: es6-iterator "^2.0.1" es6-symbol "^3.1.1" +escalade@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.0.tgz#e8e2d7c7a8b76f6ee64c2181d6b8151441602d4e" + integrity sha512-mAk+hPSO8fLDkhV7V0dXazH5pDc6MrjBTPyD3VeKzxnVFjH1MIxbCdqGZB9O8+EwWakZs3ZCbDS4IpRt79V1ig== + escape-goat@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675" @@ -13059,6 +13199,14 @@ find-up@^4.0.0, find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + find-versions@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/find-versions/-/find-versions-2.0.0.tgz#2ad90d490f6828c1aa40292cf709ac3318210c3c" @@ -13694,7 +13842,7 @@ get-caller-file@^1.0.1: resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== -get-caller-file@^2.0.1: +get-caller-file@^2.0.1, get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== @@ -16006,7 +16154,7 @@ inquirer@^6.0.0: strip-ansi "^5.1.0" through "^2.3.6" -inquirer@^7.0.0, inquirer@^7.3.1, inquirer@^7.3.3: +inquirer@^7.0.0, inquirer@^7.3.3: version "7.3.3" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003" integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA== @@ -16677,6 +16825,11 @@ is-plain-object@3.0.0, is-plain-object@^3.0.0: dependencies: isobject "^4.0.0" +is-plain-object@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" + integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== + is-promise@^2.1, is-promise@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" @@ -18711,6 +18864,13 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + locutus@^2.0.5: version "2.0.10" resolved "https://registry.yarnpkg.com/locutus/-/locutus-2.0.10.tgz#f903619466a98a4ab76e8b87a5854b55a743b917" @@ -19011,7 +19171,7 @@ log-symbols@2.2.0, log-symbols@^2.0.0, log-symbols@^2.1.0, log-symbols@^2.2.0: dependencies: chalk "^2.0.1" -log-symbols@3.0.0, log-symbols@^3.0.0: +log-symbols@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-3.0.0.tgz#f3a08516a5dea893336a7dee14d18a1cfdab77c4" integrity sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ== @@ -21460,16 +21620,16 @@ ora@^3.0.0: strip-ansi "^5.2.0" wcwidth "^1.0.1" -ora@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/ora/-/ora-4.0.4.tgz#e8da697cc5b6a47266655bf68e0fb588d29a545d" - integrity sha512-77iGeVU1cIdRhgFzCK8aw1fbtT1B/iZAvWjS+l/o1x0RShMgxHUZaD2yDpWsNCPwXg9z1ZA78Kbdvr8kBmG/Ww== +ora@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/ora/-/ora-5.1.0.tgz#b188cf8cd2d4d9b13fd25383bc3e5cba352c94f8" + integrity sha512-9tXIMPvjZ7hPTbk8DFq1f7Kow/HU/pQYB60JbNq+QnGwcyhWVZaQ4hM9zQDEsPxw/muLpgiHSaumUZxCAmod/w== dependencies: - chalk "^3.0.0" + chalk "^4.1.0" cli-cursor "^3.1.0" - cli-spinners "^2.2.0" + cli-spinners "^2.4.0" is-interactive "^1.0.0" - log-symbols "^3.0.0" + log-symbols "^4.0.0" mute-stream "0.0.8" strip-ansi "^6.0.0" wcwidth "^1.0.1" @@ -21640,6 +21800,13 @@ p-limit@^2.0.0, p-limit@^2.2.0, p-limit@^2.3.0: dependencies: p-try "^2.0.0" +p-limit@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.0.2.tgz#1664e010af3cadc681baafd3e2a437be7b0fb5fe" + integrity sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg== + dependencies: + p-try "^2.0.0" + p-locate@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" @@ -21661,6 +21828,13 @@ p-locate@^4.1.0: dependencies: p-limit "^2.2.0" +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + p-map@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b" @@ -26947,10 +27121,10 @@ strip-json-comments@^3.0.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.0.1.tgz#85713975a91fb87bf1b305cca77395e40d2a64a7" integrity sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw== -strip-json-comments@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.0.tgz#7638d31422129ecf4457440009fba03f9f9ac180" - integrity sha512-e6/d0eBu7gHtdCqFt0xJr642LdToM5/cN4Qb9DbHjVx1CP5RyeM+zH7pbecEmDv/lBqb0QH+6Uqq75rxFPkM0w== +strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== strip-json-comments@~1.0.1: version "1.0.4" @@ -28688,6 +28862,11 @@ universal-user-agent@^2.0.0, universal-user-agent@^2.0.1: dependencies: os-name "^3.0.0" +universal-user-agent@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee" + integrity sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w== + universalify@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" @@ -30272,6 +30451,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -30521,6 +30709,11 @@ y18n@^4.0.0: resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== +y18n@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.1.tgz#1ad2a7eddfa8bce7caa2e1f6b5da96c39d99d571" + integrity sha512-/jJ831jEs4vGDbYPQp4yGKDYPSCCEQ45uZWJHE1AoYBzqdZi8+LDWas0z4HrmJXmKdpFsTiowSHXdxyFhpmdMg== + yallist@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" @@ -30582,6 +30775,11 @@ yargs-parser@^18.1.1, yargs-parser@^18.1.2, yargs-parser@^18.1.3: camelcase "^5.0.0" decamelize "^1.2.0" +yargs-parser@^20.0.0: + version "20.2.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.0.tgz#944791ca2be2e08ddadd3d87e9de4c6484338605" + integrity sha512-2agPoRFPoIcFzOIp6656gcvsg2ohtscpw2OINr/q46+Sq41xz2OYLqx5HRHabmFU1OARIPAYH5uteICE7mn/5A== + yargs-unparser@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-1.6.0.tgz#ef25c2c769ff6bd09e4b0f9d7c605fb27846ea9f" @@ -30624,7 +30822,7 @@ yargs@13.3.2, yargs@^13.2.2, yargs@^13.3.0, yargs@^13.3.2: y18n "^4.0.0" yargs-parser "^13.1.2" -yargs@^15.0.2, yargs@^15.1.0, yargs@^15.3.1, yargs@^15.4.0, yargs@^15.4.1: +yargs@^15.0.2, yargs@^15.1.0, yargs@^15.3.1, yargs@^15.4.1: version "15.4.1" resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== @@ -30641,6 +30839,19 @@ yargs@^15.0.2, yargs@^15.1.0, yargs@^15.3.1, yargs@^15.4.0, yargs@^15.4.1: y18n "^4.0.0" yargs-parser "^18.1.2" +yargs@^16.0.3: + version "16.0.3" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.0.3.tgz#7a919b9e43c90f80d4a142a89795e85399a7e54c" + integrity sha512-6+nLw8xa9uK1BOEOykaiYAJVh6/CjxWXK/q9b5FpRgNslt8s22F2xMBqVIKgCRjNgGvGPBy8Vog7WN7yh4amtA== + dependencies: + cliui "^7.0.0" + escalade "^3.0.2" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.1" + yargs-parser "^20.0.0" + yargs@^3.15.0: version "3.32.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.32.0.tgz#03088e9ebf9e756b69751611d2a5ef591482c995"