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"