From 2771d69c96330465b56f5b7c18530d04d5e00cb8 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Mon, 20 Jul 2020 21:32:46 +0300 Subject: [PATCH 001/101] [KP] bump es client to rc2 (#72448) * bump es client to rc2 * update code for new typings --- package.json | 2 +- .../security_solution/common/endpoint/index_data.ts | 2 +- x-pack/test/api_integration/apis/fleet/agents/acks.ts | 2 +- .../test/api_integration/apis/fleet/agents/checkin.ts | 2 +- .../test/api_integration/apis/fleet/agents/enroll.ts | 2 +- .../test/api_integration/apis/fleet/unenroll_agent.ts | 2 +- x-pack/test/api_integration/services/resolver.ts | 2 +- yarn.lock | 11 +++++++++++ 8 files changed, 18 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index ceb3ac4cca9377..2f3f95854df040 100644 --- a/package.json +++ b/package.json @@ -125,7 +125,7 @@ "@elastic/apm-rum": "^5.2.0", "@elastic/charts": "19.8.1", "@elastic/datemath": "5.0.3", - "@elastic/elasticsearch": "7.9.0-rc.1", + "@elastic/elasticsearch": "7.9.0-rc.2", "@elastic/ems-client": "7.9.3", "@elastic/eui": "26.3.1", "@elastic/filesaver": "1.1.2", diff --git a/x-pack/plugins/security_solution/common/endpoint/index_data.ts b/x-pack/plugins/security_solution/common/endpoint/index_data.ts index 00b8f0b057afd7..9a61738cd84b44 100644 --- a/x-pack/plugins/security_solution/common/endpoint/index_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/index_data.ts @@ -94,6 +94,6 @@ async function indexAlerts( }, [] ); - await client.bulk({ body, refresh: 'true' }); + await client.bulk({ body, refresh: true }); } } diff --git a/x-pack/test/api_integration/apis/fleet/agents/acks.ts b/x-pack/test/api_integration/apis/fleet/agents/acks.ts index e8381aa9d59ea6..a040ef20081a88 100644 --- a/x-pack/test/api_integration/apis/fleet/agents/acks.ts +++ b/x-pack/test/api_integration/apis/fleet/agents/acks.ts @@ -38,7 +38,7 @@ export default function (providerContext: FtrProviderContext) { await esClient.update({ index: '.kibana', id: 'fleet-agents:agent1', - refresh: 'true', + refresh: true, body: { doc: agentDoc, }, diff --git a/x-pack/test/api_integration/apis/fleet/agents/checkin.ts b/x-pack/test/api_integration/apis/fleet/agents/checkin.ts index 8942deafdd83ce..70147f602e9c75 100644 --- a/x-pack/test/api_integration/apis/fleet/agents/checkin.ts +++ b/x-pack/test/api_integration/apis/fleet/agents/checkin.ts @@ -38,7 +38,7 @@ export default function (providerContext: FtrProviderContext) { await esClient.update({ index: '.kibana', id: 'fleet-agents:agent1', - refresh: 'true', + refresh: true, body: { doc: agentDoc, }, diff --git a/x-pack/test/api_integration/apis/fleet/agents/enroll.ts b/x-pack/test/api_integration/apis/fleet/agents/enroll.ts index 8a21fbcf24c7d3..58440a34457d02 100644 --- a/x-pack/test/api_integration/apis/fleet/agents/enroll.ts +++ b/x-pack/test/api_integration/apis/fleet/agents/enroll.ts @@ -43,7 +43,7 @@ export default function (providerContext: FtrProviderContext) { await esClient.update({ index: '.kibana', id: 'fleet-enrollment-api-keys:ed22ca17-e178-4cfe-8b02-54ea29fbd6d0', - refresh: 'true', + refresh: true, body: { doc: enrollmentApiKeyDoc, }, diff --git a/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts b/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts index bc6c44e590cc4a..bbbce3314e4cc2 100644 --- a/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts +++ b/x-pack/test/api_integration/apis/fleet/unenroll_agent.ts @@ -52,7 +52,7 @@ export default function (providerContext: FtrProviderContext) { await esClient.update({ index: '.kibana', id: 'fleet-agents:agent1', - refresh: 'true', + refresh: true, body: { doc: agentDoc, }, diff --git a/x-pack/test/api_integration/services/resolver.ts b/x-pack/test/api_integration/services/resolver.ts index 750d2f702fb843..7f568a2b003140 100644 --- a/x-pack/test/api_integration/services/resolver.ts +++ b/x-pack/test/api_integration/services/resolver.ts @@ -57,7 +57,7 @@ export function ResolverGeneratorProvider({ getService }: FtrProviderContext) { return array; }, []); // force a refresh here otherwise the documents might not be available when the tests search for them - await client.bulk({ body, refresh: 'true' }); + await client.bulk({ body, refresh: true }); allTrees.push(tree); } return { trees: allTrees, eventsIndex, alertsIndex }; diff --git a/yarn.lock b/yarn.lock index 3924655b5e43ed..4cc802e328ab8b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2183,6 +2183,17 @@ pump "^3.0.0" secure-json-parse "^2.1.0" +"@elastic/elasticsearch@7.9.0-rc.2": + version "7.9.0-rc.2" + resolved "https://registry.yarnpkg.com/@elastic/elasticsearch/-/elasticsearch-7.9.0-rc.2.tgz#cbc935f30940a15484b5ec3758c9b1ef119a5e5c" + integrity sha512-1FKCQJVr7s/LasKq6VbrmbWCI0LjoPcnjgmh2vKPzC+yyEEHVoYlmEfR5wBRchK1meATTXZtDhCVF95+Q9kVbA== + dependencies: + debug "^4.1.1" + decompress-response "^4.2.0" + ms "^2.1.1" + pump "^3.0.0" + secure-json-parse "^2.1.0" + "@elastic/ems-client@7.9.3": version "7.9.3" resolved "https://registry.yarnpkg.com/@elastic/ems-client/-/ems-client-7.9.3.tgz#71b79914f76e347f050ead8474ad65d761e94a8a" From 3ccdd79aa7ff6f03b08de2a21590a92c1a7e759e Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Mon, 20 Jul 2020 12:55:52 -0600 Subject: [PATCH 002/101] [SIEM][Detection Engine] Reduces flakiness within the tests using waitFor() and un-skips the tests (#72479) ## Summary * Utilizes the `waitFor` so that the tests are less flaky and more resilient * Unskips a test that was due to a regression within Elastic Search that should be fixed now * https://github.com/elastic/kibana/issues/71867 * https://github.com/elastic/kibana/issues/71814 * https://github.com/elastic/kibana/issues/71612 ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios --- .../basic/tests/add_prepackaged_rules.ts | 14 ++++++++++++-- .../tests/add_prepackaged_rules.ts | 14 ++++++++++++-- .../security_and_spaces/tests/create_rules_bulk.ts | 3 +-- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/add_prepackaged_rules.ts b/x-pack/test/detection_engine_api_integration/basic/tests/add_prepackaged_rules.ts index 3340ac49b2d2db..a022b7c79c0795 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/add_prepackaged_rules.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/add_prepackaged_rules.ts @@ -13,6 +13,7 @@ import { deleteAllAlerts, deleteAllTimelines, deleteSignalsIndex, + waitFor, } from '../../utils'; // eslint-disable-next-line import/no-default-export @@ -20,8 +21,7 @@ export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const es = getService('es'); - // FLAKY: https://github.com/elastic/kibana/issues/71867 - describe.skip('add_prepackaged_rules', () => { + describe('add_prepackaged_rules', () => { describe('validation errors', () => { it('should give an error that the index must exist first if it does not exist before adding prepackaged rules', async () => { const { body } = await supertest @@ -91,6 +91,16 @@ export default ({ getService }: FtrProviderContext): void => { .send() .expect(200); + // NOTE: I call the GET call until eventually it becomes consistent and that the number of rules to install are zero. + // This is to reduce flakiness where it can for a short period of time try to install the same rule twice. + await waitFor(async () => { + const { body } = await supertest + .get(`${DETECTION_ENGINE_PREPACKAGED_URL}/_status`) + .set('kbn-xsrf', 'true') + .expect(200); + return body.rules_not_installed === 0; + }); + const { body } = await supertest .put(DETECTION_ENGINE_PREPACKAGED_URL) .set('kbn-xsrf', 'true') diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_prepackaged_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_prepackaged_rules.ts index 7671b1bd497443..40456737b87610 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_prepackaged_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_prepackaged_rules.ts @@ -13,6 +13,7 @@ import { deleteAllAlerts, deleteAllTimelines, deleteSignalsIndex, + waitFor, } from '../../utils'; // eslint-disable-next-line import/no-default-export @@ -20,8 +21,7 @@ export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const es = getService('es'); - // FLAKY: https://github.com/elastic/kibana/issues/71814 - describe.skip('add_prepackaged_rules', () => { + describe('add_prepackaged_rules', () => { describe('validation errors', () => { it('should give an error that the index must exist first if it does not exist before adding prepackaged rules', async () => { const { body } = await supertest @@ -91,6 +91,16 @@ export default ({ getService }: FtrProviderContext): void => { .send() .expect(200); + // NOTE: I call the GET call until eventually it becomes consistent and that the number of rules to install are zero. + // This is to reduce flakiness where it can for a short period of time try to install the same rule the same rule twice. + await waitFor(async () => { + const { body } = await supertest + .get(`${DETECTION_ENGINE_PREPACKAGED_URL}/_status`) + .set('kbn-xsrf', 'true') + .expect(200); + return body.rules_not_installed === 0; + }); + const { body } = await supertest .put(DETECTION_ENGINE_PREPACKAGED_URL) .set('kbn-xsrf', 'true') diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules_bulk.ts index b59fd1b744e97a..52865e43be7504 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules_bulk.ts @@ -29,8 +29,7 @@ export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const es = getService('es'); - // Failing ES promotion: https://github.com/elastic/kibana/issues/71612 - describe.skip('create_rules_bulk', () => { + describe('create_rules_bulk', () => { describe('validation errors', () => { it('should give a 200 even if the index does not exist as all bulks return a 200 but have an error of 409 bad request in the body', async () => { const { body } = await supertest From 0f964f66916480f2de1f4b633e5afafc08cf62a0 Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Mon, 20 Jul 2020 12:14:33 -0700 Subject: [PATCH 003/101] [Ingest Manager] Disable asset facet links (#72158) * Disable asset facets * Fix prop name Co-authored-by: Elastic Machine --- .../epm/components/assets_facet_group.tsx | 33 ++++++++++++------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/assets_facet_group.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/assets_facet_group.tsx index 24b4baeaa092bb..b8fab92e40da8c 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/assets_facet_group.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/components/assets_facet_group.tsx @@ -3,7 +3,7 @@ * 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 } from 'react'; import { EuiFacetButton, EuiFacetGroup, @@ -14,8 +14,8 @@ import { EuiTextColor, EuiTitle, } from '@elastic/eui'; -import React, { Fragment } from 'react'; import styled from 'styled-components'; +import { FormattedMessage } from '@kbn/i18n/react'; import { AssetsGroupedByServiceByType, AssetTypeToParts, @@ -43,8 +43,15 @@ const FacetGroup = styled(EuiFacetGroup)` `; const FacetButton = styled(EuiFacetButton)` - padding: '${(props) => props.theme.eui.paddingSizes.xs} 0'; - height: 'unset'; + &&& { + .euiFacetButton__icon, + .euiFacetButton__quantity { + opacity: 1; + } + .euiFacetButton__text { + color: ${(props) => props.theme.eui.euiTextColor}; + } + } `; export function AssetsFacetGroup({ assets }: { assets: AssetsGroupedByServiceByType }) { @@ -70,7 +77,15 @@ export function AssetsFacetGroup({ assets }: { assets: AssetsGroupedByServiceByT -

{ServiceTitleMap[service]} Assets

+

+ +

@@ -83,13 +98,7 @@ export function AssetsFacetGroup({ assets }: { assets: AssetsGroupedByServiceByT const iconType = type in AssetIcons && AssetIcons[type]; const iconNode = iconType ? : ''; return ( - {}} - > + {AssetTitleMap[type]} ); From 4ccf1aed96600382f2ba60bdf631a0d8c7f5e257 Mon Sep 17 00:00:00 2001 From: Davis Plumlee <56367316+dplumlee@users.noreply.github.com> Date: Mon, 20 Jul 2020 15:34:26 -0400 Subject: [PATCH 004/101] [Security Solution][Detections]Exceptions modal bugs (#72471) --- .../exceptions/add_exception_modal/index.tsx | 8 ++++++-- .../exceptions/edit_exception_modal/index.tsx | 10 ++++++++-- .../detections/components/alerts_table/index.tsx | 1 + 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx index 79383676266f5c..53c53f48f076bc 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx @@ -21,6 +21,7 @@ import { EuiCallOut, EuiText, } from '@elastic/eui'; +import { Status } from '../../../../../common/detection_engine/schemas/common/schemas'; import { alertsIndexPattern } from '../../../../../common/endpoint/constants'; import { ExceptionListItemSchema, @@ -67,6 +68,7 @@ interface AddExceptionModalProps { }; onCancel: () => void; onConfirm: (didCloseAlert: boolean) => void; + alertStatus?: Status; } const Modal = styled(EuiModal)` @@ -105,6 +107,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({ alertData, onCancel, onConfirm, + alertStatus, }: AddExceptionModalProps) { const { http } = useKibana().services; const [comment, setComment] = useState(''); @@ -183,7 +186,8 @@ export const AddExceptionModal = memo(function AddExceptionModal({ if (indexPatternLoading === false && isSignalIndexLoading === false) { setShouldDisableBulkClose( entryHasListType(exceptionItemsToAdd) || - entryHasNonEcsType(exceptionItemsToAdd, indexPatterns) + entryHasNonEcsType(exceptionItemsToAdd, indexPatterns) || + exceptionItemsToAdd.length === 0 ); } }, [ @@ -335,7 +339,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({ - {alertData !== undefined && ( + {alertData !== undefined && alertStatus !== 'closed' && ( - {!isSignalIndexLoading && ( + {(addExceptionIsLoading || indexPatternLoading || isSignalIndexLoading) && ( + + )} + + {!isSignalIndexLoading && !addExceptionIsLoading && !indexPatternLoading && ( <> {i18n.EXCEPTION_BUILDER_INFO} diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index 30cfe2d02354fb..1d4c97d85443fe 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -458,6 +458,7 @@ export const AlertsTableComponent: React.FC = ({ alertData={addExceptionModalState.alertData} onCancel={onAddExceptionCancel} onConfirm={onAddExceptionConfirm} + alertStatus={filterGroup} /> )} From b9413cf3c816d65c53d5b7fb5f2d0e1300729f3c Mon Sep 17 00:00:00 2001 From: "Devin W. Hurley" Date: Mon, 20 Jul 2020 15:55:26 -0400 Subject: [PATCH 005/101] [SIEM] [Detections] Fixes faulty circuit breaker (#71999) * removes useSortIds which was leftover from a previous attempt at implementing gap detection mitigation code. This only showed up because I modified the count variable used to determine when we hit maxSignals from utilizing the searchResult hits length to using the count of bulk created items (signals indexed) in this commit 56de45d156be23069815fec17440cf978710451f * removes logs and fixes if statement ordering * adds tests, increases code coverage for search after and bulk create function, updates log statements * update tests after rebase onto master * clean up if statements * fix test data * merge conflicts are hard --- .../signals/__mocks__/es_results.ts | 42 +++- .../signals/build_bulk_body.test.ts | 7 +- .../signals/search_after_bulk_create.test.ts | 207 ++++++++++++++++++ .../signals/search_after_bulk_create.ts | 171 +++++++++------ 4 files changed, 351 insertions(+), 76 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts index 17e05109b9a879..19fcf65ec0c5e1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -56,11 +56,10 @@ export const sampleRuleAlertParams = ( exceptionsList: getListArrayMock(), }); -export const sampleDocNoSortId = (someUuid: string = sampleIdGuid): SignalSourceHit => ({ +export const sampleDocNoSortIdNoVersion = (someUuid: string = sampleIdGuid): SignalSourceHit => ({ _index: 'myFakeSignalIndex', _type: 'doc', _score: 100, - _version: 1, _id: someUuid, _source: { someKey: 'someValue', @@ -68,18 +67,26 @@ export const sampleDocNoSortId = (someUuid: string = sampleIdGuid): SignalSource }, }); -export const sampleDocNoSortIdNoVersion = (someUuid: string = sampleIdGuid): SignalSourceHit => ({ +export const sampleDocWithSortId = ( + someUuid: string = sampleIdGuid, + ip?: string +): SignalSourceHit => ({ _index: 'myFakeSignalIndex', _type: 'doc', _score: 100, + _version: 1, _id: someUuid, _source: { someKey: 'someValue', '@timestamp': '2020-04-20T21:27:45+0000', + source: { + ip: ip ?? '127.0.0.1', + }, }, + sort: ['1234567891111'], }); -export const sampleDocWithSortId = ( +export const sampleDocNoSortId = ( someUuid: string = sampleIdGuid, ip?: string ): SignalSourceHit => ({ @@ -95,7 +102,7 @@ export const sampleDocWithSortId = ( ip: ip ?? '127.0.0.1', }, }, - sort: ['1234567891111'], + sort: [], }); export const sampleEmptyDocSearchResults = (): SignalSearchResponse => ({ @@ -116,6 +123,8 @@ export const sampleEmptyDocSearchResults = (): SignalSearchResponse => ({ export const sampleDocWithAncestors = (): SignalSearchResponse => { const sampleDoc = sampleDocNoSortId(); + delete sampleDoc.sort; + delete sampleDoc._source.source; sampleDoc._source.signal = { parent: { rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', @@ -317,6 +326,29 @@ export const repeatedSearchResultsWithSortId = ( }, }); +export const repeatedSearchResultsWithNoSortId = ( + total: number, + pageSize: number, + guids: string[], + ips?: string[] +) => ({ + took: 10, + timed_out: false, + _shards: { + total: 10, + successful: 10, + failed: 0, + skipped: 0, + }, + hits: { + total, + max_score: 100, + hits: Array.from({ length: pageSize }).map((x, index) => ({ + ...sampleDocNoSortId(guids[index], ips ? ips[index] : '127.0.0.1'), + })), + }, +}); + export const sampleDocSearchResultsWithSortId = ( someUuid: string = sampleIdGuid ): SignalSearchResponse => ({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts index e840ae96cf3c18..fe2e0f2d96fd89 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts @@ -21,8 +21,10 @@ describe('buildBulkBody', () => { test('bulk body builds well-defined body', () => { const sampleParams = sampleRuleAlertParams(); + const doc = sampleDocNoSortId(); + delete doc._source.source; const fakeSignalSourceHit = buildBulkBody({ - doc: sampleDocNoSortId(), + doc, ruleParams: sampleParams, id: sampleRuleGuid, name: 'rule-name', @@ -107,6 +109,7 @@ describe('buildBulkBody', () => { test('bulk body builds original_event if it exists on the event to begin with', () => { const sampleParams = sampleRuleAlertParams(); const doc = sampleDocNoSortId(); + delete doc._source.source; doc._source.event = { action: 'socket_opened', module: 'system', @@ -208,6 +211,7 @@ describe('buildBulkBody', () => { test('bulk body builds original_event if it exists on the event to begin with but no kind information', () => { const sampleParams = sampleRuleAlertParams(); const doc = sampleDocNoSortId(); + delete doc._source.source; doc._source.event = { action: 'socket_opened', module: 'system', @@ -307,6 +311,7 @@ describe('buildBulkBody', () => { test('bulk body builds original_event if it exists on the event to begin with with only kind information', () => { const sampleParams = sampleRuleAlertParams(); const doc = sampleDocNoSortId(); + delete doc._source.source; doc._source.event = { kind: 'event', }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts index 17935f64d5e141..3312191c3b41b3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.test.ts @@ -11,6 +11,7 @@ import { sampleRuleGuid, mockLogger, repeatedSearchResultsWithSortId, + repeatedSearchResultsWithNoSortId, sampleDocSearchResultsNoSortIdNoHits, } from './__mocks__/es_results'; import { searchAfterAndBulkCreate } from './search_after_bulk_create'; @@ -356,6 +357,212 @@ describe('searchAfterAndBulkCreate', () => { expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); }); + test('should return success when all search results are in the allowlist and with sortId present', async () => { + listClient.getListItemByValues = jest + .fn() + .mockResolvedValue([{ value: '1.1.1.1' }, { value: '2.2.2.2' }, { value: '3.3.3.3' }]); + const sampleParams = sampleRuleAlertParams(30); + mockService.callCluster + .mockResolvedValueOnce( + repeatedSearchResultsWithSortId(4, 4, someGuids.slice(0, 3), [ + '1.1.1.1', + '2.2.2.2', + '2.2.2.2', + '2.2.2.2', + ]) + ) + .mockResolvedValueOnce(sampleDocSearchResultsNoSortIdNoHits()); + + const exceptionItem = getExceptionListItemSchemaMock(); + exceptionItem.entries = [ + { + field: 'source.ip', + operator: 'included', + type: 'list', + list: { + id: 'ci-badguys.txt', + type: 'ip', + }, + }, + ]; + const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ + ruleParams: sampleParams, + gap: null, + previousStartedAt: new Date(), + listClient, + exceptionsList: [exceptionItem], + services: mockService, + logger: mockLogger, + id: sampleRuleGuid, + inputIndexPattern, + signalsIndex: DEFAULT_SIGNALS_INDEX, + name: 'rule-name', + actions: [], + createdAt: '2020-01-28T15:58:34.810Z', + updatedAt: '2020-01-28T15:59:14.004Z', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: '5m', + enabled: true, + pageSize: 1, + filter: undefined, + refresh: false, + tags: ['some fake tag 1', 'some fake tag 2'], + throttle: 'no_actions', + buildRuleMessage, + }); + expect(success).toEqual(true); + expect(mockService.callCluster).toHaveBeenCalledTimes(2); + expect(createdSignalsCount).toEqual(0); // should not create any signals because all events were in the allowlist + expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); + }); + + test('should return success when all search results are in the allowlist and no sortId present', async () => { + listClient.getListItemByValues = jest + .fn() + .mockResolvedValue([{ value: '1.1.1.1' }, { value: '2.2.2.2' }, { value: '3.3.3.3' }]); + const sampleParams = sampleRuleAlertParams(30); + mockService.callCluster.mockResolvedValueOnce( + repeatedSearchResultsWithNoSortId(4, 4, someGuids.slice(0, 3), [ + '1.1.1.1', + '2.2.2.2', + '2.2.2.2', + '2.2.2.2', + ]) + ); + + const exceptionItem = getExceptionListItemSchemaMock(); + exceptionItem.entries = [ + { + field: 'source.ip', + operator: 'included', + type: 'list', + list: { + id: 'ci-badguys.txt', + type: 'ip', + }, + }, + ]; + const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ + ruleParams: sampleParams, + gap: null, + previousStartedAt: new Date(), + listClient, + exceptionsList: [exceptionItem], + services: mockService, + logger: mockLogger, + id: sampleRuleGuid, + inputIndexPattern, + signalsIndex: DEFAULT_SIGNALS_INDEX, + name: 'rule-name', + actions: [], + createdAt: '2020-01-28T15:58:34.810Z', + updatedAt: '2020-01-28T15:59:14.004Z', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: '5m', + enabled: true, + pageSize: 1, + filter: undefined, + refresh: false, + tags: ['some fake tag 1', 'some fake tag 2'], + throttle: 'no_actions', + buildRuleMessage, + }); + expect(success).toEqual(true); + expect(mockService.callCluster).toHaveBeenCalledTimes(1); + expect(createdSignalsCount).toEqual(0); // should not create any signals because all events were in the allowlist + expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); + // I don't like testing log statements since logs change but this is the best + // way I can think of to ensure this section is getting hit with this test case. + expect(((mockLogger.debug as unknown) as jest.Mock).mock.calls[7][0]).toContain( + 'sortIds was empty on searchResult' + ); + }); + + test('should return success when no sortId present but search results are in the allowlist', async () => { + const sampleParams = sampleRuleAlertParams(30); + mockService.callCluster + .mockResolvedValueOnce(repeatedSearchResultsWithNoSortId(4, 4, someGuids.slice(0, 3))) + .mockResolvedValueOnce({ + took: 100, + errors: false, + items: [ + { + fakeItemValue: 'fakeItemKey', + }, + { + create: { + status: 201, + }, + }, + { + create: { + status: 201, + }, + }, + { + create: { + status: 201, + }, + }, + { + create: { + status: 201, + }, + }, + ], + }); + + const exceptionItem = getExceptionListItemSchemaMock(); + exceptionItem.entries = [ + { + field: 'source.ip', + operator: 'included', + type: 'list', + list: { + id: 'ci-badguys.txt', + type: 'ip', + }, + }, + ]; + const { success, createdSignalsCount, lastLookBackDate } = await searchAfterAndBulkCreate({ + ruleParams: sampleParams, + gap: null, + previousStartedAt: new Date(), + listClient, + exceptionsList: [exceptionItem], + services: mockService, + logger: mockLogger, + id: sampleRuleGuid, + inputIndexPattern, + signalsIndex: DEFAULT_SIGNALS_INDEX, + name: 'rule-name', + actions: [], + createdAt: '2020-01-28T15:58:34.810Z', + updatedAt: '2020-01-28T15:59:14.004Z', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: '5m', + enabled: true, + pageSize: 1, + filter: undefined, + refresh: false, + tags: ['some fake tag 1', 'some fake tag 2'], + throttle: 'no_actions', + buildRuleMessage, + }); + expect(success).toEqual(true); + expect(mockService.callCluster).toHaveBeenCalledTimes(2); + expect(createdSignalsCount).toEqual(4); // should not create any signals because all events were in the allowlist + expect(lastLookBackDate).toEqual(new Date('2020-04-20T21:27:45+0000')); + // I don't like testing log statements since logs change but this is the best + // way I can think of to ensure this section is getting hit with this test case. + expect(((mockLogger.debug as unknown) as jest.Mock).mock.calls[12][0]).toContain( + 'sortIds was empty on filteredEvents' + ); + }); + test('should return success when no exceptions list provided', async () => { const sampleParams = sampleRuleAlertParams(30); mockService.callCluster diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts index 2a0e39cbbf237f..e90e5996877f81 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/search_after_bulk_create.ts @@ -90,23 +90,12 @@ export const searchAfterAndBulkCreate = async ({ createdSignalsCount: 0, }; - let sortId; // tells us where to start our next search_after query - let searchResultSize = 0; + // sortId tells us where to start our next consecutive search_after query + let sortId; - /* - The purpose of `maxResults` is to ensure we do not perform - extra search_after's. This will be reset on each - iteration, although it really only matters for the first - iteration of the loop. - e.g. if maxSignals = 100 but our search result only yields - 27 documents, there is no point in performing another search - since we know there are no more events that match our rule, - and thus, no more signals we could possibly generate. - However, if maxSignals = 500 and our search yields a total - of 3050 results we don't want to make 3050 signals, - we only want 500. So maxResults will help us control how - many times we perform a search_after - */ + // signalsCreatedCount keeps track of how many signals we have created, + // to ensure we don't exceed maxSignals + let signalsCreatedCount = 0; const totalToFromTuples = getSignalTimeTuples({ logger, @@ -118,7 +107,6 @@ export const searchAfterAndBulkCreate = async ({ interval, buildRuleMessage, }); - const useSortIds = totalToFromTuples.length <= 1; logger.debug(buildRuleMessage(`totalToFromTuples: ${totalToFromTuples.length}`)); while (totalToFromTuples.length > 0) { const tuple = totalToFromTuples.pop(); @@ -127,16 +115,18 @@ export const searchAfterAndBulkCreate = async ({ toReturn.success = false; return toReturn; } - searchResultSize = 0; - while (searchResultSize < tuple.maxSignals) { + signalsCreatedCount = 0; + while (signalsCreatedCount < tuple.maxSignals) { try { logger.debug(buildRuleMessage(`sortIds: ${sortId}`)); + + // perform search_after with optionally undefined sortId const { searchResult, searchDuration, }: { searchResult: SignalSearchResponse; searchDuration: string } = await singleSearchAfter( { - searchAfterSortId: useSortIds ? sortId : undefined, + searchAfterSortId: sortId, index: inputIndexPattern, from: tuple.from.toISOString(), to: tuple.to.toISOString(), @@ -149,6 +139,7 @@ export const searchAfterAndBulkCreate = async ({ ); toReturn.searchAfterTimes.push(searchDuration); + // determine if there are any candidate signals to be processed const totalHits = typeof searchResult.hits.total === 'number' ? searchResult.hits.total @@ -157,7 +148,23 @@ export const searchAfterAndBulkCreate = async ({ logger.debug( buildRuleMessage(`searchResult.hit.hits.length: ${searchResult.hits.hits.length}`) ); - if (totalHits === 0) { + + // search results yielded zero hits so exit + // with search_after, these two values can be different when + // searching with the last sortId of a consecutive search_after + // yields zero hits, but there were hits using the previous + // sortIds. + // e.g. totalHits was 156, index 50 of 100 results, do another search-after + // this time with a new sortId, index 22 of the remainding 56, get another sortId + // search with that sortId, total is still 156 but the hits.hits array is empty. + if (totalHits === 0 || searchResult.hits.hits.length === 0) { + logger.debug( + buildRuleMessage( + `${ + totalHits === 0 ? 'totalHits' : 'searchResult.hits.hits.length' + } was 0, exiting and moving on to next tuple` + ) + ); toReturn.success = true; break; } @@ -167,10 +174,10 @@ export const searchAfterAndBulkCreate = async ({ searchResult.hits.hits[searchResult.hits.hits.length - 1]?._source['@timestamp'] ) : null; - searchResultSize += searchResult.hits.hits.length; // filter out the search results that match with the values found in the list. - // the resulting set are valid signals that are not on the allowlist. + // the resulting set are signals to be indexed, given they are not duplicates + // of signals already present in the signals index. const filteredEvents: SignalSearchResponse = listClient != null ? await filterEventsAgainstList({ @@ -181,55 +188,79 @@ export const searchAfterAndBulkCreate = async ({ buildRuleMessage, }) : searchResult; - if (filteredEvents.hits.total === 0 || filteredEvents.hits.hits.length === 0) { - // everything in the events were allowed, so no need to generate signals - toReturn.success = true; - break; - } - const { - bulkCreateDuration: bulkDuration, - createdItemsCount: createdCount, - } = await singleBulkCreate({ - filteredEvents, - ruleParams, - services, - logger, - id, - signalsIndex, - actions, - name, - createdAt, - createdBy, - updatedAt, - updatedBy, - interval, - enabled, - refresh, - tags, - throttle, - }); - logger.debug(buildRuleMessage(`created ${createdCount} signals`)); - toReturn.createdSignalsCount += createdCount; - if (bulkDuration) { - toReturn.bulkCreateTimes.push(bulkDuration); - } + // only bulk create if there are filteredEvents leftover + // if there isn't anything after going through the value list filter + // skip the call to bulk create and proceed to the next search_after, + // if there is a sort id to continue the search_after with. + if (filteredEvents.hits.hits.length !== 0) { + // make sure we are not going to create more signals than maxSignals allows + if (signalsCreatedCount + filteredEvents.hits.hits.length > tuple.maxSignals) { + filteredEvents.hits.hits = filteredEvents.hits.hits.slice( + 0, + tuple.maxSignals - signalsCreatedCount + ); + } + const { + bulkCreateDuration: bulkDuration, + createdItemsCount: createdCount, + } = await singleBulkCreate({ + filteredEvents, + ruleParams, + services, + logger, + id, + signalsIndex, + actions, + name, + createdAt, + createdBy, + updatedAt, + updatedBy, + interval, + enabled, + refresh, + tags, + throttle, + }); + logger.debug(buildRuleMessage(`created ${createdCount} signals`)); + toReturn.createdSignalsCount += createdCount; + signalsCreatedCount += createdCount; + logger.debug(buildRuleMessage(`signalsCreatedCount: ${signalsCreatedCount}`)); + if (bulkDuration) { + toReturn.bulkCreateTimes.push(bulkDuration); + } - logger.debug( - buildRuleMessage(`filteredEvents.hits.hits: ${filteredEvents.hits.hits.length}`) - ); - if (useSortIds && filteredEvents.hits.hits[0].sort == null) { - logger.debug(buildRuleMessage('sortIds was empty on search')); - toReturn.success = true; - break; - } else if ( - useSortIds && - filteredEvents.hits.hits !== null && - filteredEvents.hits.hits[0].sort !== null - ) { - sortId = filteredEvents.hits.hits[0].sort - ? filteredEvents.hits.hits[0].sort[0] - : undefined; + logger.debug( + buildRuleMessage(`filteredEvents.hits.hits: ${filteredEvents.hits.hits.length}`) + ); + + if ( + filteredEvents.hits.hits[0].sort != null && + filteredEvents.hits.hits[0].sort.length !== 0 + ) { + sortId = filteredEvents.hits.hits[0].sort + ? filteredEvents.hits.hits[0].sort[0] + : undefined; + } else { + logger.debug(buildRuleMessage('sortIds was empty on filteredEvents')); + toReturn.success = true; + break; + } + } else { + // we are guaranteed to have searchResult hits at this point + // because we check before if the totalHits or + // searchResult.hits.hits.length is 0 + if ( + searchResult.hits.hits[0].sort != null && + searchResult.hits.hits[0].sort.length !== 0 + ) { + sortId = searchResult.hits.hits[0].sort ? searchResult.hits.hits[0].sort[0] : undefined; + } else { + logger.debug(buildRuleMessage('sortIds was empty on searchResult')); + toReturn.success = true; + break; + } } } catch (exc) { logger.error(buildRuleMessage(`[-] search_after and bulk threw an error ${exc}`)); From 9947c671ecf87eb04dfdf46eecf9d4cf19559534 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Mon, 20 Jul 2020 15:24:32 -0600 Subject: [PATCH 006/101] [Maps] auto-fit to data bounds (#72129) * [Maps] auto-fit to data bounds * update jest snapshot * add buffer to fit to bounds * sync join layers prior to fitting to bounds * clean-up comment * better names * fix tslint errors * update functional test expect * add functional tests * clean-up * change test run location * fix test expect Co-authored-by: Elastic Machine --- .../public/actions/data_request_actions.ts | 23 ++++++- .../maps/public/actions/map_actions.ts | 37 +++++++---- .../blended_vector_layer.ts | 4 ++ .../layers/vector_layer/vector_layer.d.ts | 2 + .../layers/vector_layer/vector_layer.js | 22 +++++-- .../navigation_panel.test.tsx.snap | 66 +++++++++++++++++++ .../map_settings_panel/map_settings_panel.tsx | 1 + .../map_settings_panel/navigation_panel.tsx | 20 ++++++ .../maps/public/elasticsearch_geo_utils.d.ts | 9 +++ .../maps/public/elasticsearch_geo_utils.js | 11 ++++ .../public/elasticsearch_geo_utils.test.js | 18 +++++ .../public/reducers/default_map_settings.ts | 1 + x-pack/plugins/maps/public/reducers/map.d.ts | 1 + .../apps/maps/auto_fit_to_bounds.js | 35 ++++++++++ .../apps/maps/documents_source/search_hits.js | 2 +- x-pack/test/functional/apps/maps/index.js | 1 + .../test/functional/page_objects/gis_page.js | 18 +++++ 17 files changed, 249 insertions(+), 22 deletions(-) create mode 100644 x-pack/plugins/maps/public/elasticsearch_geo_utils.d.ts create mode 100644 x-pack/test/functional/apps/maps/auto_fit_to_bounds.js diff --git a/x-pack/plugins/maps/public/actions/data_request_actions.ts b/x-pack/plugins/maps/public/actions/data_request_actions.ts index a0c484a82e530b..5919feadfcc2ad 100644 --- a/x-pack/plugins/maps/public/actions/data_request_actions.ts +++ b/x-pack/plugins/maps/public/actions/data_request_actions.ts @@ -37,8 +37,12 @@ import { UPDATE_SOURCE_DATA_REQUEST, } from './map_action_constants'; import { ILayer } from '../classes/layers/layer'; +import { IVectorLayer } from '../classes/layers/vector_layer/vector_layer'; import { DataMeta, MapExtent, MapFilters } from '../../common/descriptor_types'; import { DataRequestAbortError } from '../classes/util/data_request'; +import { scaleBounds } from '../elasticsearch_geo_utils'; + +const FIT_TO_BOUNDS_SCALE_FACTOR = 0.1; export type DataRequestContext = { startLoading(dataId: string, requestToken: symbol, meta: DataMeta): void; @@ -122,13 +126,26 @@ function getDataRequestContext( export function syncDataForAllLayers() { return async (dispatch: Dispatch, getState: () => MapStoreState) => { - const syncPromises = getLayerList(getState()).map(async (layer) => { + const syncPromises = getLayerList(getState()).map((layer) => { return dispatch(syncDataForLayer(layer)); }); await Promise.all(syncPromises); }; } +export function syncDataForAllJoinLayers() { + return async (dispatch: Dispatch, getState: () => MapStoreState) => { + const syncPromises = getLayerList(getState()) + .filter((layer) => { + return 'hasJoins' in layer ? (layer as IVectorLayer).hasJoins() : false; + }) + .map((layer) => { + return dispatch(syncDataForLayer(layer)); + }); + await Promise.all(syncPromises); + }; +} + export function syncDataForLayer(layer: ILayer) { return async (dispatch: Dispatch, getState: () => MapStoreState) => { const dataRequestContext = getDataRequestContext(dispatch, getState, layer.getId()); @@ -284,7 +301,7 @@ export function fitToLayerExtent(layerId: string) { getDataRequestContext(dispatch, getState, layerId) ); if (bounds) { - await dispatch(setGotoWithBounds(bounds)); + await dispatch(setGotoWithBounds(scaleBounds(bounds, FIT_TO_BOUNDS_SCALE_FACTOR))); } } catch (error) { if (!(error instanceof DataRequestAbortError)) { @@ -359,7 +376,7 @@ export function fitToDataBounds() { maxLat: turfUnionBbox[3], }; - dispatch(setGotoWithBounds(dataBounds)); + dispatch(setGotoWithBounds(scaleBounds(dataBounds, FIT_TO_BOUNDS_SCALE_FACTOR))); }; } diff --git a/x-pack/plugins/maps/public/actions/map_actions.ts b/x-pack/plugins/maps/public/actions/map_actions.ts index 75df8689a670ef..ef0cfdf0b4742c 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.ts +++ b/x-pack/plugins/maps/public/actions/map_actions.ts @@ -8,11 +8,13 @@ import { Dispatch } from 'redux'; // @ts-ignore import turf from 'turf'; +import uuid from 'uuid/v4'; import turfBooleanContains from '@turf/boolean-contains'; import { Filter, Query, TimeRange } from 'src/plugins/data/public'; import { MapStoreState } from '../reducers/store'; import { getDataFilters, + getMapSettings, getWaitingForMapReadyLayerListRaw, getQuery, } from '../selectors/map_selectors'; @@ -42,7 +44,11 @@ import { UPDATE_DRAW_STATE, UPDATE_MAP_SETTING, } from './map_action_constants'; -import { syncDataForAllLayers } from './data_request_actions'; +import { + fitToDataBounds, + syncDataForAllJoinLayers, + syncDataForAllLayers, +} from './data_request_actions'; import { addLayer } from './layer_actions'; import { MapSettings } from '../reducers/map'; import { @@ -51,6 +57,7 @@ import { MapExtent, MapRefreshConfig, } from '../../common/descriptor_types'; +import { scaleBounds } from '../elasticsearch_geo_utils'; export function setMapInitError(errorMessage: string) { return { @@ -134,15 +141,7 @@ export function mapExtentChanged(newMapConstants: { zoom: number; extent: MapExt } if (!doesBufferContainExtent || currentZoom !== newZoom) { - const scaleFactor = 0.5; // TODO put scale factor in store and fetch with selector - const width = extent.maxLon - extent.minLon; - const height = extent.maxLat - extent.minLat; - dataFilters.buffer = { - minLon: extent.minLon - width * scaleFactor, - minLat: extent.minLat - height * scaleFactor, - maxLon: extent.maxLon + width * scaleFactor, - maxLat: extent.maxLat + height * scaleFactor, - }; + dataFilters.buffer = scaleBounds(extent, 0.5); } } @@ -197,6 +196,7 @@ function generateQueryTimestamp() { return new Date().toISOString(); } +let lastSetQueryCallId: string = ''; export function setQuery({ query, timeFilters, @@ -226,7 +226,22 @@ export function setQuery({ filters, }); - await dispatch(syncDataForAllLayers()); + if (getMapSettings(getState()).autoFitToDataBounds) { + // Joins are performed on the client. + // As a result, bounds for join layers must also be performed on the client. + // Therefore join layers need to fetch data prior to auto fitting bounds. + const localSetQueryCallId = uuid(); + lastSetQueryCallId = localSetQueryCallId; + await dispatch(syncDataForAllJoinLayers()); + + // setQuery can be triggered before async data fetching completes + // Only continue execution path if setQuery has not been re-triggered. + if (localSetQueryCallId === lastSetQueryCallId) { + dispatch(fitToDataBounds()); + } + } else { + await dispatch(syncDataForAllLayers()); + } }; } diff --git a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts index aefa2beede7d17..c0b9c4553d01e1 100644 --- a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts +++ b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts @@ -237,6 +237,10 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer { return []; } + hasJoins() { + return false; + } + getSource() { return this._isClustered ? this._clusterSource : this._documentSource; } diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.d.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.d.ts index 77daf9c9af5704..e6cb212daddaea 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.d.ts +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.d.ts @@ -35,6 +35,7 @@ export interface IVectorLayer extends ILayer { getStyle(): IVectorStyle; getFeatureById(id: string | number): Feature | null; getPropertiesForTooltip(properties: GeoJsonProperties): Promise; + hasJoins(): boolean; } export class VectorLayer extends AbstractLayer implements IVectorLayer { @@ -81,4 +82,5 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { getStyle(): IVectorStyle; getFeatureById(id: string | number): Feature | null; getPropertiesForTooltip(properties: GeoJsonProperties): Promise; + hasJoins(): boolean; } diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js index 0a4fcfc23060c5..23889bdca2dd7d 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js @@ -85,7 +85,7 @@ export class VectorLayer extends AbstractLayer { }); } - _hasJoins() { + hasJoins() { return this.getValidJoins().length > 0; } @@ -159,7 +159,7 @@ export class VectorLayer extends AbstractLayer { async getBounds({ startLoading, stopLoading, registerCancelCallback, dataFilters }) { const isStaticLayer = !this.getSource().isBoundsAware(); if (isStaticLayer) { - return getFeatureCollectionBounds(this._getSourceFeatureCollection(), this._hasJoins()); + return getFeatureCollectionBounds(this._getSourceFeatureCollection(), this.hasJoins()); } const requestToken = Symbol(`${SOURCE_BOUNDS_DATA_REQUEST_ID}-${this.getId()}`); @@ -193,6 +193,11 @@ export class VectorLayer extends AbstractLayer { return bounds; } + isLoadingBounds() { + const boundsDataRequest = this.getDataRequest(SOURCE_BOUNDS_DATA_REQUEST_ID); + return !!boundsDataRequest && boundsDataRequest.isLoading(); + } + async getLeftJoinFields() { return await this.getSource().getLeftJoinFields(); } @@ -583,7 +588,7 @@ export class VectorLayer extends AbstractLayer { } async syncData(syncContext) { - this._syncData(syncContext, this.getSource(), this.getCurrentStyle()); + await this._syncData(syncContext, this.getSource(), this.getCurrentStyle()); } // TLDR: Do not call getSource or getCurrentStyle in syncData flow. Use 'source' and 'style' arguments instead. @@ -597,13 +602,16 @@ export class VectorLayer extends AbstractLayer { // Given 2 above, which source/style to use can not be pulled from data request state. // Therefore, source and style are provided as arugments and must be used instead of calling getSource or getCurrentStyle. async _syncData(syncContext, source, style) { + if (this.isLoadingBounds()) { + return; + } await this._syncSourceStyleMeta(syncContext, source, style); await this._syncSourceFormatters(syncContext, source, style); const sourceResult = await this._syncSource(syncContext, source, style); if ( !sourceResult.featureCollection || !sourceResult.featureCollection.features.length || - !this._hasJoins() + !this.hasJoins() ) { return; } @@ -711,7 +719,7 @@ export class VectorLayer extends AbstractLayer { mbMap.addLayer(mbLayer); } - const filterExpr = getPointFilterExpression(this._hasJoins()); + const filterExpr = getPointFilterExpression(this.hasJoins()); if (filterExpr !== mbMap.getFilter(pointLayerId)) { mbMap.setFilter(pointLayerId, filterExpr); mbMap.setFilter(textLayerId, filterExpr); @@ -747,7 +755,7 @@ export class VectorLayer extends AbstractLayer { mbMap.addLayer(mbLayer); } - const filterExpr = getPointFilterExpression(this._hasJoins()); + const filterExpr = getPointFilterExpression(this.hasJoins()); if (filterExpr !== mbMap.getFilter(symbolLayerId)) { mbMap.setFilter(symbolLayerId, filterExpr); } @@ -769,7 +777,7 @@ export class VectorLayer extends AbstractLayer { const sourceId = this.getId(); const fillLayerId = this._getMbPolygonLayerId(); const lineLayerId = this._getMbLineLayerId(); - const hasJoins = this._hasJoins(); + const hasJoins = this.hasJoins(); if (!mbMap.getLayer(fillLayerId)) { const mbLayer = { id: fillLayerId, diff --git a/x-pack/plugins/maps/public/connected_components/map_settings_panel/__snapshots__/navigation_panel.test.tsx.snap b/x-pack/plugins/maps/public/connected_components/map_settings_panel/__snapshots__/navigation_panel.test.tsx.snap index 641dd20a1a44aa..18e30d9446e05f 100644 --- a/x-pack/plugins/maps/public/connected_components/map_settings_panel/__snapshots__/navigation_panel.test.tsx.snap +++ b/x-pack/plugins/maps/public/connected_components/map_settings_panel/__snapshots__/navigation_panel.test.tsx.snap @@ -16,6 +16,25 @@ exports[`should render 1`] = ` + + + + + + + + + + + + + + + { + updateMapSetting('autoFitToDataBounds', event.target.checked); + }; + const onZoomChange = (value: Value) => { const minZoom = Math.max(MIN_ZOOM, parseInt(value[0] as string, 10)); const maxZoom = Math.min(MAX_ZOOM, parseInt(value[1] as string, 10)); @@ -207,6 +213,19 @@ export function NavigationPanel({ center, settings, updateMapSetting, zoom }: Pr + + + + + + { expect(bbox).toEqual({ bottom_right: [-170, -89], top_left: [-175, 89] }); }); }); + +describe('scaleBounds', () => { + it('Should scale bounds', () => { + const bounds = { + maxLat: 10, + maxLon: 100, + minLat: 5, + minLon: 95, + }; + expect(scaleBounds(bounds, 0.5)).toEqual({ + maxLat: 12.5, + maxLon: 102.5, + minLat: 2.5, + minLon: 92.5, + }); + }); +}); diff --git a/x-pack/plugins/maps/public/reducers/default_map_settings.ts b/x-pack/plugins/maps/public/reducers/default_map_settings.ts index 9c9b814ae6add2..896ac11e367821 100644 --- a/x-pack/plugins/maps/public/reducers/default_map_settings.ts +++ b/x-pack/plugins/maps/public/reducers/default_map_settings.ts @@ -9,6 +9,7 @@ import { MapSettings } from './map'; export function getDefaultMapSettings(): MapSettings { return { + autoFitToDataBounds: false, initialLocation: INITIAL_LOCATION.LAST_SAVED_LOCATION, fixedLocation: { lat: 0, lon: 0, zoom: 2 }, browserLocation: { zoom: 2 }, diff --git a/x-pack/plugins/maps/public/reducers/map.d.ts b/x-pack/plugins/maps/public/reducers/map.d.ts index 33794fcf8657da..aca75334032d93 100644 --- a/x-pack/plugins/maps/public/reducers/map.d.ts +++ b/x-pack/plugins/maps/public/reducers/map.d.ts @@ -42,6 +42,7 @@ export type MapContext = { }; export type MapSettings = { + autoFitToDataBounds: boolean; initialLocation: INITIAL_LOCATION; fixedLocation: { lat: number; diff --git a/x-pack/test/functional/apps/maps/auto_fit_to_bounds.js b/x-pack/test/functional/apps/maps/auto_fit_to_bounds.js new file mode 100644 index 00000000000000..64c07273c9ccf2 --- /dev/null +++ b/x-pack/test/functional/apps/maps/auto_fit_to_bounds.js @@ -0,0 +1,35 @@ +/* + * 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'; + +export default function ({ getPageObjects }) { + const PageObjects = getPageObjects(['maps']); + + describe('auto fit map to bounds', () => { + describe('without joins', () => { + before(async () => { + await PageObjects.maps.loadSavedMap('document example'); + await PageObjects.maps.enableAutoFitToBounds(); + }); + + it('should automatically fit to bounds when query is applied', async () => { + // Set view to other side of world so no matching results + await PageObjects.maps.setView(-15, -100, 6); + + // Setting query should trigger fit to bounds and move map + const origView = await PageObjects.maps.getView(); + await PageObjects.maps.setAndSubmitQuery('machine.os.raw : "ios"'); + await PageObjects.maps.waitForMapPanAndZoom(origView); + + const { lat, lon, zoom } = await PageObjects.maps.getView(); + expect(Math.round(lat)).to.equal(43); + expect(Math.round(lon)).to.equal(-102); + expect(Math.round(zoom)).to.equal(5); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/maps/documents_source/search_hits.js b/x-pack/test/functional/apps/maps/documents_source/search_hits.js index 5d75679432c979..cc0f3a7df32dee 100644 --- a/x-pack/test/functional/apps/maps/documents_source/search_hits.js +++ b/x-pack/test/functional/apps/maps/documents_source/search_hits.js @@ -103,7 +103,7 @@ export default function ({ getPageObjects, getService }) { await PageObjects.maps.setView(-15, -100, 6); await PageObjects.maps.clickFitToBounds('logstash'); const { lat, lon, zoom } = await PageObjects.maps.getView(); - expect(Math.round(lat)).to.equal(42); + expect(Math.round(lat)).to.equal(43); expect(Math.round(lon)).to.equal(-102); expect(Math.round(zoom)).to.equal(5); }); diff --git a/x-pack/test/functional/apps/maps/index.js b/x-pack/test/functional/apps/maps/index.js index 15928170972d94..d0735aecda78b9 100644 --- a/x-pack/test/functional/apps/maps/index.js +++ b/x-pack/test/functional/apps/maps/index.js @@ -34,6 +34,7 @@ export default function ({ loadTestFile, getService }) { loadTestFile(require.resolve('./vector_styling')); loadTestFile(require.resolve('./saved_object_management')); loadTestFile(require.resolve('./sample_data')); + loadTestFile(require.resolve('./auto_fit_to_bounds')); loadTestFile(require.resolve('./feature_controls/maps_security')); loadTestFile(require.resolve('./feature_controls/maps_spaces')); loadTestFile(require.resolve('./full_screen_mode')); diff --git a/x-pack/test/functional/page_objects/gis_page.js b/x-pack/test/functional/page_objects/gis_page.js index ff50415d3066eb..8a0b4aaefa8880 100644 --- a/x-pack/test/functional/page_objects/gis_page.js +++ b/x-pack/test/functional/page_objects/gis_page.js @@ -656,6 +656,24 @@ export function GisPageProvider({ getService, getPageObjects }) { async getCategorySuggestions() { return await comboBox.getOptionsList(`colorStopInput1`); } + + async enableAutoFitToBounds() { + await testSubjects.click('openSettingsButton'); + const isEnabled = await testSubjects.getAttribute('autoFitToDataBoundsSwitch', 'checked'); + if (!isEnabled) { + await retry.try(async () => { + await testSubjects.click('autoFitToDataBoundsSwitch'); + const ensureEnabled = await testSubjects.getAttribute( + 'autoFitToDataBoundsSwitch', + 'checked' + ); + if (!ensureEnabled) { + throw new Error('autoFitToDataBoundsSwitch is not enabled'); + } + }); + } + await testSubjects.click('mapSettingSubmitButton'); + } } return new GisPage(); } From c3263aa9a2d046ba23edf8cbd9fdc86420d5faeb Mon Sep 17 00:00:00 2001 From: Kevin Qualters <56408403+kqualters-elastic@users.noreply.github.com> Date: Mon, 20 Jul 2020 17:41:25 -0400 Subject: [PATCH 007/101] [Security Solution][Resolver] Update the resolver element ref on scroll events if the position of the element has changed within the page (#72461) --- .../public/resolver/view/use_camera.ts | 54 +++++++++++++++++-- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_camera.ts b/x-pack/plugins/security_solution/public/resolver/view/use_camera.ts index 6a1c35be57000b..661e038d04e32e 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/use_camera.ts +++ b/x-pack/plugins/security_solution/public/resolver/view/use_camera.ts @@ -281,17 +281,61 @@ export function useCamera(): { * handle that. */ export function useAutoUpdatingClientRect(): [DOMRect | null, (node: Element | null) => void] { + // This hooks returns `rect`. const [rect, setRect] = useState(null); - // Using state as ref.current update does not trigger effect hook when reset + + const { ResizeObserver, requestAnimationFrame } = useContext(SideEffectContext); + + // Keep the current DOM node in state so that we can create a ResizeObserver for it via `useEffect`. const [currentNode, setCurrentNode] = useState(null); + // `ref` will be used with a react element. When the element is available, this function will be called. const ref = useCallback((node: Element | null) => { + // track the node in state setCurrentNode(node); - if (node !== null) { - setRect(node.getBoundingClientRect()); - } }, []); - const { ResizeObserver } = useContext(SideEffectContext); + + /** + * Any time the DOM node changes (to something other than `null`) recalculate the DOMRect and set it (which will cause it to be returned from the hook. + * This effect re-runs when the DOM node has changed. + */ + useEffect(() => { + if (currentNode !== null) { + // When the DOM node is received, immedaiately calculate its DOM Rect and return that + setRect(currentNode.getBoundingClientRect()); + } + }, [currentNode]); + + /** + * When scroll events occur, recalculate the DOMRect. DOMRect represents the position of an element relative to the viewport, so that may change during scroll (depending on the layout.) + * This effect re-runs when the DOM node has changed. + */ + useEffect(() => { + // the last scrollX and scrollY values that we handled + let previousX: number = window.scrollX; + let previousY: number = window.scrollY; + + const handleScroll = () => { + requestAnimationFrame(() => { + // synchronously read from the DOM + const currentX = window.scrollX; + const currentY = window.scrollY; + + if (currentNode !== null && (previousX !== currentX || previousY !== currentY)) { + setRect(currentNode.getBoundingClientRect()); + } + + previousX = currentX; + previousY = currentY; + }); + }; + + window.addEventListener('scroll', handleScroll, { passive: true }); + return () => { + window.removeEventListener('scroll', handleScroll); + }; + }, [currentNode, requestAnimationFrame]); + useEffect(() => { if (currentNode !== null) { const resizeObserver = new ResizeObserver((entries) => { From a221e04b3caa161cccef45985032c403f74fb974 Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 20 Jul 2020 15:06:02 -0700 Subject: [PATCH 008/101] [pipeline/commitStatus] update commit status in baseline-capture job (#72366) Co-authored-by: spalger --- .ci/Jenkinsfile_baseline_capture | 36 ++++++++++--------- .../src/test/githubCommitStatus.groovy | 2 ++ vars/githubCommitStatus.groovy | 34 +++++++++++------- 3 files changed, 42 insertions(+), 30 deletions(-) diff --git a/.ci/Jenkinsfile_baseline_capture b/.ci/Jenkinsfile_baseline_capture index 3f90a6bc05af02..b0d3591821642a 100644 --- a/.ci/Jenkinsfile_baseline_capture +++ b/.ci/Jenkinsfile_baseline_capture @@ -4,23 +4,25 @@ library 'kibana-pipeline-library' kibanaLibrary.load() kibanaPipeline(timeoutMinutes: 120) { - ciStats.trackBuild { - catchError { - parallel([ - 'oss-visualRegression': { - workers.ci(name: 'oss-visualRegression', size: 's-highmem', ramDisk: true) { - kibanaPipeline.functionalTestProcess('oss-visualRegression', './test/scripts/jenkins_visual_regression.sh')(1) - } - }, - 'xpack-visualRegression': { - workers.ci(name: 'xpack-visualRegression', size: 's-highmem', ramDisk: true) { - kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh')(1) - } - }, - ]) - } + githubCommitStatus.trackBuild(params.commit, 'kibana-ci-baseline') { + ciStats.trackBuild { + catchError { + parallel([ + 'oss-visualRegression': { + workers.ci(name: 'oss-visualRegression', size: 's-highmem', ramDisk: true) { + kibanaPipeline.functionalTestProcess('oss-visualRegression', './test/scripts/jenkins_visual_regression.sh')(1) + } + }, + 'xpack-visualRegression': { + workers.ci(name: 'xpack-visualRegression', size: 's-highmem', ramDisk: true) { + kibanaPipeline.functionalTestProcess('xpack-visualRegression', './test/scripts/jenkins_xpack_visual_regression.sh')(1) + } + }, + ]) + } - kibanaPipeline.sendMail() - slackNotifications.onFailure() + kibanaPipeline.sendMail() + slackNotifications.onFailure() + } } } diff --git a/.ci/pipeline-library/src/test/githubCommitStatus.groovy b/.ci/pipeline-library/src/test/githubCommitStatus.groovy index 17878624b73cfe..c770d5596f9cb6 100644 --- a/.ci/pipeline-library/src/test/githubCommitStatus.groovy +++ b/.ci/pipeline-library/src/test/githubCommitStatus.groovy @@ -12,6 +12,7 @@ class GithubCommitStatusTest extends KibanaBasePipelineTest { interface BuildState { Object get(String key) + Object has(String key) } interface GithubApi { @@ -25,6 +26,7 @@ class GithubCommitStatusTest extends KibanaBasePipelineTest { buildStateMock = mock(BuildState) githubApiMock = mock(GithubApi) + when(buildStateMock.has('checkoutInfo')).thenReturn(true) when(buildStateMock.get('checkoutInfo')).thenReturn([ commit: 'COMMIT_HASH', ]) when(githubApiMock.post(any(), any())).thenReturn(null) diff --git a/vars/githubCommitStatus.groovy b/vars/githubCommitStatus.groovy index 17d3c234f69283..248d226169a61e 100644 --- a/vars/githubCommitStatus.groovy +++ b/vars/githubCommitStatus.groovy @@ -1,39 +1,47 @@ -def shouldCreateStatuses() { - return !githubPr.isPr() && buildState.get('checkoutInfo') +def defaultCommit() { + if (buildState.has('checkoutInfo')) { + return buildState.get('checkoutInfo').commit + } } -def onStart() { +def onStart(commit = defaultCommit(), context = 'kibana-ci') { catchError { - if (!shouldCreateStatuses()) { + if (githubPr.isPr() || !commit) { return } - def checkoutInfo = buildState.get('checkoutInfo') - create(checkoutInfo.commit, 'pending', 'Build started.') + create(commit, 'pending', 'Build started.', context) } } -def onFinish() { +def onFinish(commit = defaultCommit(), context = 'kibana-ci') { catchError { - if (!shouldCreateStatuses()) { + if (githubPr.isPr() || !commit) { return } - def checkoutInfo = buildState.get('checkoutInfo') def status = buildUtils.getBuildStatus() if (status == 'SUCCESS' || status == 'UNSTABLE') { - create(checkoutInfo.commit, 'success', 'Build completed successfully.') + create(commit, 'success', 'Build completed successfully.', context) } else if(status == 'ABORTED') { - create(checkoutInfo.commit, 'error', 'Build aborted or timed out.') + create(commit, 'error', 'Build aborted or timed out.', context) } else { - create(checkoutInfo.commit, 'error', 'Build failed.') + create(commit, 'error', 'Build failed.', context) } } } +def trackBuild(commit, context, Closure closure) { + onStart(commit, context) + catchError { + closure() + } + onFinish(commit, context) +} + // state: error|failure|pending|success -def create(sha, state, description, context = 'kibana-ci') { +def create(sha, state, description, context) { withGithubCredentials { return githubApi.post("repos/elastic/kibana/statuses/${sha}", [ state: state, From 709e0a0a11fa5c87106c0d1ebc06cebc81b9caea Mon Sep 17 00:00:00 2001 From: Jen Huang Date: Mon, 20 Jul 2020 15:16:46 -0700 Subject: [PATCH 009/101] Fix long combo box items breaking out of flex item width (#72512) --- .../components/package_config_input_stream.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_stream.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_stream.tsx index 11a9df276485b6..04a0f4e6dbb74b 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_stream.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_config/create_package_config_page/components/package_config_input_stream.tsx @@ -5,6 +5,7 @@ */ import React, { useState, Fragment, memo, useMemo } from 'react'; import ReactMarkdown from 'react-markdown'; +import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexGrid, @@ -23,6 +24,10 @@ import { } from '../services'; import { PackageConfigInputVarField } from './package_config_input_var_field'; +const FlexItemWithMaxWidth = styled(EuiFlexItem)` + max-width: calc(50% - ${(props) => props.theme.eui.euiSizeL}); +`; + export const PackageConfigInputStreamConfig: React.FunctionComponent<{ packageInputStream: RegistryStream; packageConfigInputStream: PackageConfigInputStream; @@ -91,7 +96,7 @@ export const PackageConfigInputStreamConfig: React.FunctionComponent<{ - + {requiredVars.map((varDef) => { const { name: varName, type: varType } = varDef; @@ -178,7 +183,7 @@ export const PackageConfigInputStreamConfig: React.FunctionComponent<{ ) : null} - + ); } From 03fe8c3e89b273d0c1b1d82b6951d6a682969242 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Mon, 20 Jul 2020 16:23:38 -0600 Subject: [PATCH 010/101] [SIEM] Uses faster wait from testing-library and removes duplicate older wait idiom (#72509) ## Summary * Removes the older wait pattern that does a block no matter what * Utilizes the improved and better pattern for test-library's waitFor which will test immediately and then poll for results * Changes everything to put their expect statement within the waitFor * Once the waitFor is in TypeScript/JS we can change the import statement to use that If you get a timeout or error this is what it looks like now which improves the developer experience in some ways but does degrade things in others as it suggests that everything is timeout related. However, developers should inspect the values and remove the waitFor() and re-run their tests if they think that they have a real problem during development. Screen Shot 2020-07-20 at 12 40 39 PM See the API for more information: https://testing-library.com/docs/dom-testing-library/api-async#waitfor But in short we should be using: ```ts await waitFor(() => expect(...)); ``` throughout our code at this point and the waitFor will loop quickly and efficiently until it either times out or gets the condition expected. ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios --- .../components/add_comment/index.test.tsx | 13 +- .../cases/components/case_view/index.test.tsx | 125 +++---- .../cases/components/create/index.test.tsx | 7 +- .../components/edit_connector/index.test.tsx | 26 +- .../cases/components/tag_list/index.test.tsx | 13 +- .../user_action_tree/index.test.tsx | 55 ++-- .../events_viewer/events_viewer.test.tsx | 70 ++-- .../components/events_viewer/index.test.tsx | 17 +- .../components/url_state/index.test.tsx | 42 +-- .../public/common/lib/helpers/index.tsx | 6 - .../rules/step_about_rule/index.test.tsx | 105 +++--- .../detection_engine/rules/all/index.test.tsx | 21 +- .../first_last_seen_host/index.test.tsx | 58 ++-- .../alerts_by_category/index.test.tsx | 63 ++-- .../components/overview_host/index.test.tsx | 14 +- .../overview_network/index.test.tsx | 14 +- .../components/open_timeline/index.test.tsx | 304 +++++++++--------- .../open_timeline_modal/index.test.tsx | 18 +- .../open_timeline_modal_button.test.tsx | 25 +- .../components/timeline/body/index.test.tsx | 24 +- .../components/timeline/index.test.tsx | 25 +- .../timeline/epic_local_storage.test.tsx | 24 +- 22 files changed, 578 insertions(+), 491 deletions(-) diff --git a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx index 5da75033d17fa8..88969c3ae5fb3d 100644 --- a/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/add_comment/index.test.tsx @@ -15,7 +15,9 @@ import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router import { useInsertTimeline } from '../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline'; import { usePostComment } from '../../containers/use_post_comment'; import { useForm } from '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'; -import { wait } from '../../../common/lib/helpers'; + +// we don't have the types for waitFor just yet, so using "as waitFor" until when we do +import { wait as waitFor } from '@testing-library/react'; jest.mock( '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' @@ -84,10 +86,11 @@ describe('AddComment ', () => { expect(wrapper.find(`[data-test-subj="loading-spinner"]`).exists()).toBeFalsy(); wrapper.find(`[data-test-subj="submit-comment"]`).first().simulate('click'); - await wait(); - expect(onCommentSaving).toBeCalled(); - expect(postComment).toBeCalledWith(sampleData, onCommentPosted); - expect(formHookMock.reset).toBeCalled(); + await waitFor(() => { + expect(onCommentSaving).toBeCalled(); + expect(postComment).toBeCalledWith(sampleData, onCommentPosted); + expect(formHookMock.reset).toBeCalled(); + }); }); it('should render spinner and disable submit when loading', () => { diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx index 4e29db4022e651..278b972ada9709 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx @@ -15,7 +15,9 @@ import { TestProviders } from '../../../common/mock'; import { useUpdateCase } from '../../containers/use_update_case'; import { useGetCase } from '../../containers/use_get_case'; import { useGetCaseUserActions } from '../../containers/use_get_case_user_actions'; -import { wait } from '../../../common/lib/helpers'; + +// we don't have the types for waitFor just yet, so using "as waitFor" until when we do +import { wait as waitFor } from '@testing-library/react'; import { useConnectors } from '../../containers/configure/use_connectors'; import { connectorsMock } from '../../containers/configure/mock'; @@ -108,30 +110,33 @@ describe('CaseView ', () => { ); - await wait(); - expect(wrapper.find(`[data-test-subj="case-view-title"]`).first().prop('title')).toEqual( - data.title - ); - expect(wrapper.find(`[data-test-subj="case-view-status"]`).first().text()).toEqual(data.status); - expect( - wrapper - .find(`[data-test-subj="case-view-tag-list"] [data-test-subj="case-tag"]`) - .first() - .text() - ).toEqual(data.tags[0]); - expect(wrapper.find(`[data-test-subj="case-view-username"]`).first().text()).toEqual( - data.createdBy.username - ); - expect(wrapper.contains(`[data-test-subj="case-view-closedAt"]`)).toBe(false); - expect(wrapper.find(`[data-test-subj="case-view-createdAt"]`).first().prop('value')).toEqual( - data.createdAt - ); - expect( - wrapper - .find(`[data-test-subj="description-action"] [data-test-subj="user-action-markdown"]`) - .first() - .prop('raw') - ).toEqual(data.description); + await waitFor(() => { + expect(wrapper.find(`[data-test-subj="case-view-title"]`).first().prop('title')).toEqual( + data.title + ); + expect(wrapper.find(`[data-test-subj="case-view-status"]`).first().text()).toEqual( + data.status + ); + expect( + wrapper + .find(`[data-test-subj="case-view-tag-list"] [data-test-subj="case-tag"]`) + .first() + .text() + ).toEqual(data.tags[0]); + expect(wrapper.find(`[data-test-subj="case-view-username"]`).first().text()).toEqual( + data.createdBy.username + ); + expect(wrapper.contains(`[data-test-subj="case-view-closedAt"]`)).toBe(false); + expect(wrapper.find(`[data-test-subj="case-view-createdAt"]`).first().prop('value')).toEqual( + data.createdAt + ); + expect( + wrapper + .find(`[data-test-subj="description-action"] [data-test-subj="user-action-markdown"]`) + .first() + .prop('raw') + ).toEqual(data.description); + }); }); it('should show closed indicators in header when case is closed', async () => { @@ -146,14 +151,15 @@ describe('CaseView ', () => { ); - await wait(); - expect(wrapper.contains(`[data-test-subj="case-view-createdAt"]`)).toBe(false); - expect(wrapper.find(`[data-test-subj="case-view-closedAt"]`).first().prop('value')).toEqual( - basicCaseClosed.closedAt - ); - expect(wrapper.find(`[data-test-subj="case-view-status"]`).first().text()).toEqual( - basicCaseClosed.status - ); + await waitFor(() => { + expect(wrapper.contains(`[data-test-subj="case-view-createdAt"]`)).toBe(false); + expect(wrapper.find(`[data-test-subj="case-view-closedAt"]`).first().prop('value')).toEqual( + basicCaseClosed.closedAt + ); + expect(wrapper.find(`[data-test-subj="case-view-status"]`).first().text()).toEqual( + basicCaseClosed.status + ); + }); }); it('should dispatch update state when button is toggled', async () => { @@ -164,11 +170,12 @@ describe('CaseView ', () => { ); - await wait(); - wrapper - .find('input[data-test-subj="toggle-case-status"]') - .simulate('change', { target: { checked: true } }); - expect(updateCaseProperty).toHaveBeenCalled(); + await waitFor(() => { + wrapper + .find('input[data-test-subj="toggle-case-status"]') + .simulate('change', { target: { checked: true } }); + expect(updateCaseProperty).toHaveBeenCalled(); + }); }); it('should display EditableTitle isLoading', () => { @@ -296,17 +303,17 @@ describe('CaseView ', () => { ); - await wait(); + await waitFor(() => { + expect( + wrapper.find('[data-test-subj="has-data-to-push-button"]').first().exists() + ).toBeTruthy(); - expect( - wrapper.find('[data-test-subj="has-data-to-push-button"]').first().exists() - ).toBeTruthy(); + wrapper.find('[data-test-subj="push-to-external-service"]').first().simulate('click'); - wrapper.find('[data-test-subj="push-to-external-service"]').first().simulate('click'); + wrapper.update(); - wrapper.update(); - - expect(postPushToService).toHaveBeenCalled(); + expect(postPushToService).toHaveBeenCalled(); + }); }); it('should return null if error', () => { @@ -424,17 +431,19 @@ describe('CaseView ', () => { ); - await wait(); - wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); - wrapper.update(); - wrapper.find('button[data-test-subj="dropdown-connector-servicenow-2"]').simulate('click'); - wrapper.update(); - wrapper.find(`[data-test-subj="edit-connectors-submit"]`).last().simulate('click'); - wrapper.update(); - await wait(); - wrapper.update(); - expect( - wrapper.find('[data-test-subj="dropdown-connectors"]').at(0).prop('valueOfSelected') - ).toBe('servicenow-1'); + await waitFor(() => { + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.update(); + wrapper.find('button[data-test-subj="dropdown-connector-servicenow-2"]').simulate('click'); + wrapper.update(); + wrapper.find(`[data-test-subj="edit-connectors-submit"]`).last().simulate('click'); + wrapper.update(); + }); + await waitFor(() => { + wrapper.update(); + expect( + wrapper.find('[data-test-subj="dropdown-connectors"]').at(0).prop('valueOfSelected') + ).toBe('servicenow-1'); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/create/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/create/index.test.tsx index 25c12a53f2f5b4..aefb196e0678db 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/index.test.tsx @@ -19,7 +19,9 @@ import { useGetTags } from '../../containers/use_get_tags'; jest.mock('../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline'); jest.mock('../../containers/use_post_case'); import { useForm } from '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'; -import { wait } from '../../../common/lib/helpers'; + +// we don't have the types for waitFor just yet, so using "as waitFor" until when we do +import { wait as waitFor } from '@testing-library/react'; jest.mock( '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' @@ -97,8 +99,7 @@ describe('Create case', () => { ); wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); - await wait(); - expect(postCase).toBeCalledWith(sampleData); + await waitFor(() => expect(postCase).toBeCalledWith(sampleData)); }); it('should redirect to all cases on cancel click', () => { diff --git a/x-pack/plugins/security_solution/public/cases/components/edit_connector/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/edit_connector/index.test.tsx index 564ce2e19df00a..e531b71e8c90c2 100644 --- a/x-pack/plugins/security_solution/public/cases/components/edit_connector/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/edit_connector/index.test.tsx @@ -11,7 +11,8 @@ import { EditConnector } from './index'; import { getFormMock, useFormMock } from '../__mock__/form'; import { TestProviders } from '../../../common/mock'; import { connectorsMock } from '../../containers/configure/mock'; -import { wait } from '../../../common/lib/helpers'; +// we don't have the types for waitFor just yet, so using "as waitFor" until when we do +import { wait as waitFor } from '@testing-library/react'; import { act } from 'react-dom/test-utils'; jest.mock( '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' @@ -68,8 +69,7 @@ describe('EditConnector ', () => { await act(async () => { wrapper.find(`[data-test-subj="edit-connectors-submit"]`).last().simulate('click'); - await wait(); - expect(onSubmit.mock.calls[0][0]).toBe(sampleConnector); + await waitFor(() => expect(onSubmit.mock.calls[0][0]).toBe(sampleConnector)); }); }); @@ -92,10 +92,11 @@ describe('EditConnector ', () => { await act(async () => { wrapper.find(`[data-test-subj="edit-connectors-submit"]`).last().simulate('click'); - await wait(); - wrapper.update(); + await waitFor(() => { + wrapper.update(); + expect(formHookMock.setFieldValue).toHaveBeenCalledWith('connector', 'none'); + }); }); - expect(formHookMock.setFieldValue).toHaveBeenCalledWith('connector', 'none'); }); it('Resets selector on cancel', async () => { @@ -115,12 +116,13 @@ describe('EditConnector ', () => { await act(async () => { wrapper.find(`[data-test-subj="edit-connectors-cancel"]`).last().simulate('click'); - await wait(); - wrapper.update(); - expect(formHookMock.setFieldValue).toBeCalledWith( - 'connector', - defaultProps.selectedConnector - ); + await waitFor(() => { + wrapper.update(); + expect(formHookMock.setFieldValue).toBeCalledWith( + 'connector', + defaultProps.selectedConnector + ); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/cases/components/tag_list/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/tag_list/index.test.tsx index 950dd6f3779458..939ddfde8b9dc8 100644 --- a/x-pack/plugins/security_solution/public/cases/components/tag_list/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/tag_list/index.test.tsx @@ -11,7 +11,8 @@ import { act } from 'react-dom/test-utils'; import { TagList } from '.'; import { getFormMock } from '../__mock__/form'; import { TestProviders } from '../../../common/mock'; -import { wait } from '../../../common/lib/helpers'; +// we don't have the types for waitFor just yet, so using "as waitFor" until when we do +import { wait as waitFor } from '@testing-library/react'; import { useForm } from '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'; import { useGetTags } from '../../containers/use_get_tags'; @@ -77,8 +78,7 @@ describe('TagList ', () => { wrapper.find(`[data-test-subj="tag-list-edit-button"]`).last().simulate('click'); await act(async () => { wrapper.find(`[data-test-subj="edit-tags-submit"]`).last().simulate('click'); - await wait(); - expect(onSubmit).toBeCalledWith(sampleTags); + await waitFor(() => expect(onSubmit).toBeCalledWith(sampleTags)); }); }); it('Tag options render with new tags added', () => { @@ -107,9 +107,10 @@ describe('TagList ', () => { await act(async () => { expect(wrapper.find(`[data-test-subj="case-tag"]`).last().exists()).toBeFalsy(); wrapper.find(`[data-test-subj="edit-tags-cancel"]`).last().simulate('click'); - await wait(); - wrapper.update(); - expect(wrapper.find(`[data-test-subj="case-tag"]`).last().exists()).toBeTruthy(); + await waitFor(() => { + wrapper.update(); + expect(wrapper.find(`[data-test-subj="case-tag"]`).last().exists()).toBeTruthy(); + }); }); }); it('Renders disabled button', () => { diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.test.tsx index 23f1fb222a841f..9cf13b6f9930a4 100644 --- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.test.tsx @@ -13,7 +13,8 @@ import { useUpdateComment } from '../../containers/use_update_comment'; import { basicCase, basicPush, getUserAction } from '../../containers/mock'; import { UserActionTree } from '.'; import { TestProviders } from '../../../common/mock'; -import { wait } from '../../../common/lib/helpers'; +// we don't have the types for waitFor just yet, so using "as waitFor" until when we do +import { wait as waitFor } from '@testing-library/react'; import { act } from 'react-dom/test-utils'; const fetchUserActions = jest.fn(); @@ -225,22 +226,23 @@ describe('UserActionTree ', () => { .first() .simulate('click'); await act(async () => { - await wait(); - wrapper.update(); - expect( - wrapper - .find( - `[data-test-subj="user-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]` - ) - .exists() - ).toEqual(false); - expect(patchComment).toBeCalledWith({ - commentUpdate: sampleData.content, - caseId: props.data.id, - commentId: props.data.comments[0].id, - fetchUserActions, - updateCase, - version: props.data.comments[0].version, + await waitFor(() => { + wrapper.update(); + expect( + wrapper + .find( + `[data-test-subj="user-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]` + ) + .exists() + ).toEqual(false); + expect(patchComment).toBeCalledWith({ + commentUpdate: sampleData.content, + caseId: props.data.id, + commentId: props.data.comments[0].id, + fetchUserActions, + updateCase, + version: props.data.comments[0].version, + }); }); }); }); @@ -269,15 +271,16 @@ describe('UserActionTree ', () => { .first() .simulate('click'); await act(async () => { - await wait(); - expect( - wrapper - .find( - `[data-test-subj="user-action-${props.data.id}"] [data-test-subj="user-action-markdown-form"]` - ) - .exists() - ).toEqual(false); - expect(onUpdateField).toBeCalledWith({ key: 'description', value: sampleData.content }); + await waitFor(() => { + expect( + wrapper + .find( + `[data-test-subj="user-action-${props.data.id}"] [data-test-subj="user-action-markdown-form"]` + ) + .exists() + ).toEqual(false); + expect(onUpdateField).toBeCalledWith({ key: 'description', value: sampleData.content }); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx index 2a7cbff5ee1493..049953e21febdf 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx @@ -10,7 +10,8 @@ import useResizeObserver from 'use-resize-observer/polyfilled'; import '../../mock/match_media'; import { mockIndexPattern, TestProviders } from '../../mock'; -import { wait } from '../../lib/helpers'; +// we don't have the types for waitFor just yet, so using "as waitFor" until when we do +import { wait as waitFor } from '@testing-library/react'; import { mockEventViewerResponse } from './mock'; import { StatefulEventsViewer } from '.'; @@ -60,12 +61,13 @@ describe('EventsViewer', () => { ); - await wait(); - wrapper.update(); + await waitFor(() => { + wrapper.update(); - expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().text()).toEqual( - 'Showing: 12 events' - ); + expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().text()).toEqual( + 'Showing: 12 events' + ); + }); }); test('it does NOT render fetch index pattern is loading', async () => { @@ -84,10 +86,13 @@ describe('EventsViewer', () => { ); - await wait(); - wrapper.update(); + await waitFor(() => { + wrapper.update(); - expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().exists()).toBe(false); + expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().exists()).toBe( + false + ); + }); }); test('it does NOT render when start is empty', async () => { @@ -106,10 +111,13 @@ describe('EventsViewer', () => { ); - await wait(); - wrapper.update(); + await waitFor(() => { + wrapper.update(); - expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().exists()).toBe(false); + expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().exists()).toBe( + false + ); + }); }); test('it does NOT render when end is empty', async () => { @@ -128,10 +136,13 @@ describe('EventsViewer', () => { ); - await wait(); - wrapper.update(); + await waitFor(() => { + wrapper.update(); - expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().exists()).toBe(false); + expect(wrapper.find(`[data-test-subj="header-section-subtitle"]`).first().exists()).toBe( + false + ); + }); }); test('it renders the Fields Browser as a settings gear', async () => { @@ -148,10 +159,11 @@ describe('EventsViewer', () => { ); - await wait(); - wrapper.update(); + await waitFor(() => { + wrapper.update(); - expect(wrapper.find(`[data-test-subj="show-field-browser"]`).first().exists()).toBe(true); + expect(wrapper.find(`[data-test-subj="show-field-browser"]`).first().exists()).toBe(true); + }); }); test('it renders the footer containing the Load More button', async () => { @@ -168,10 +180,11 @@ describe('EventsViewer', () => { ); - await wait(); - wrapper.update(); + await waitFor(() => { + wrapper.update(); - expect(wrapper.find(`[data-test-subj="TimelineMoreButton"]`).first().exists()).toBe(true); + expect(wrapper.find(`[data-test-subj="TimelineMoreButton"]`).first().exists()).toBe(true); + }); }); defaultHeaders.forEach((header) => { @@ -189,14 +202,15 @@ describe('EventsViewer', () => { ); - await wait(); - wrapper.update(); + await waitFor(() => { + wrapper.update(); - defaultHeaders.forEach((h) => - expect(wrapper.find(`[data-test-subj="header-text-${header.id}"]`).first().exists()).toBe( - true - ) - ); + defaultHeaders.forEach((h) => + expect(wrapper.find(`[data-test-subj="header-text-${header.id}"]`).first().exists()).toBe( + true + ) + ); + }); }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx index 1ab390a85ec50c..4509d01e82e253 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.test.tsx @@ -9,7 +9,8 @@ import { MockedProvider } from 'react-apollo/test-utils'; import useResizeObserver from 'use-resize-observer/polyfilled'; import '../../mock/match_media'; -import { wait } from '../../lib/helpers'; +// we don't have the types for waitFor just yet, so using "as waitFor" until when we do +import { wait as waitFor } from '@testing-library/react'; import { mockIndexPattern, TestProviders } from '../../mock'; import { useMountAppended } from '../../utils/use_mount_appended'; @@ -54,10 +55,11 @@ describe('StatefulEventsViewer', () => { ); - await wait(); - wrapper.update(); + await waitFor(() => { + wrapper.update(); - expect(wrapper.find('[data-test-subj="events-viewer-panel"]').first().exists()).toBe(true); + expect(wrapper.find('[data-test-subj="events-viewer-panel"]').first().exists()).toBe(true); + }); }); // InspectButtonContainer controls displaying InspectButton components @@ -75,9 +77,10 @@ describe('StatefulEventsViewer', () => { ); - await wait(); - wrapper.update(); + await waitFor(() => { + wrapper.update(); - expect(wrapper.find(`InspectButtonContainer`).exists()).toBe(true); + expect(wrapper.find(`InspectButtonContainer`).exists()).toBe(true); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx index 9d0d9e7b250a05..72df9d613abacb 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx @@ -21,7 +21,8 @@ import { } from './test_dependencies'; import { UrlStateContainerPropTypes } from './types'; import { useUrlStateHooks } from './use_url_state'; -import { wait } from '../../lib/helpers'; +// we don't have the types for waitFor just yet, so using "as waitFor" until when we do +import { wait as waitFor } from '@testing-library/react'; let mockProps: UrlStateContainerPropTypes; @@ -194,29 +195,32 @@ describe('UrlStateContainer', () => { }).relativeTimeSearch.undefinedQuery, }); wrapper.update(); - await wait(); if (CONSTANTS.detectionsPage === page) { - expect(mockSetRelativeRangeDatePicker.mock.calls[3][0]).toEqual({ - from: '2020-01-01T00:00:00.000Z', - fromStr: 'now-1d/d', - kind: 'relative', - to: '2020-01-01T00:00:00.000Z', - toStr: 'now-1d/d', - id: 'global', - }); + await waitFor(() => { + expect(mockSetRelativeRangeDatePicker.mock.calls[3][0]).toEqual({ + from: '2020-01-01T00:00:00.000Z', + fromStr: 'now-1d/d', + kind: 'relative', + to: '2020-01-01T00:00:00.000Z', + toStr: 'now-1d/d', + id: 'global', + }); - expect(mockSetRelativeRangeDatePicker.mock.calls[2][0]).toEqual({ - from: 1558732849370, - fromStr: 'now-15m', - kind: 'relative', - to: 1558733749370, - toStr: 'now', - id: 'timeline', + expect(mockSetRelativeRangeDatePicker.mock.calls[2][0]).toEqual({ + from: 1558732849370, + fromStr: 'now-15m', + kind: 'relative', + to: 1558733749370, + toStr: 'now', + id: 'timeline', + }); }); } else { - // There is no change in url state, so that's expected we only have two actions - expect(mockSetRelativeRangeDatePicker.mock.calls.length).toEqual(2); + await waitFor(() => { + // There is no change in url state, so that's expected we only have two actions + expect(mockSetRelativeRangeDatePicker.mock.calls.length).toEqual(2); + }); } } ); diff --git a/x-pack/plugins/security_solution/public/common/lib/helpers/index.tsx b/x-pack/plugins/security_solution/public/common/lib/helpers/index.tsx index 07e706ac2a9afb..96b0343efdf720 100644 --- a/x-pack/plugins/security_solution/public/common/lib/helpers/index.tsx +++ b/x-pack/plugins/security_solution/public/common/lib/helpers/index.tsx @@ -19,12 +19,6 @@ export type WrapArrayIfExitts = (value: Many) => T[] | undefined; export const asArrayIfExists: WrapArrayIfExitts = (value) => !isUndefined(value) ? castArray(value) : undefined; -export const wait = (delay = 0): Promise => { - return new Promise((resolve) => { - return setTimeout(resolve, delay); - }); -}; - /** * Creates a Union Type for all the values of an object */ diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx index 9b2e0069f0ac00..a86c1b7ce1beaa 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/index.test.tsx @@ -13,7 +13,8 @@ import { StepAboutRule } from '.'; import { mockAboutStepRule } from '../../../pages/detection_engine/rules/all/__mocks__/mock'; import { StepRuleDescription } from '../description_step'; import { stepAboutDefaultValue } from './default_value'; -import { wait } from '@testing-library/react'; +// we don't have the types for waitFor just yet, so using "as waitFor" until when we do +import { wait as waitFor } from '@testing-library/react'; import { AboutStepRule } from '../../../pages/detection_engine/rules/types'; const theme = () => ({ eui: euiDarkVars, darkMode: true }); @@ -162,31 +163,32 @@ describe('StepAboutRuleComponent', () => { .simulate('change', { target: { value: 'Test name text' } }); wrapper.find('button[data-test-subj="about-continue"]').first().simulate('click').update(); - await wait(); - const expected: Omit = { - author: [], - isAssociatedToEndpointList: false, - isBuildingBlock: false, - license: '', - ruleNameOverride: '', - timestampOverride: '', - description: 'Test description text', - falsePositives: [''], - name: 'Test name text', - note: '', - references: [''], - riskScore: { value: 50, mapping: [] }, - severity: { value: 'low', mapping: [] }, - tags: [], - threat: [ - { - framework: 'MITRE ATT&CK', - tactic: { id: 'none', name: 'none', reference: 'none' }, - technique: [], - }, - ], - }; - expect(stepDataMock.mock.calls[1][1]).toEqual(expected); + await waitFor(() => { + const expected: Omit = { + author: [], + isAssociatedToEndpointList: false, + isBuildingBlock: false, + license: '', + ruleNameOverride: '', + timestampOverride: '', + description: 'Test description text', + falsePositives: [''], + name: 'Test name text', + note: '', + references: [''], + riskScore: { value: 50, mapping: [] }, + severity: { value: 'low', mapping: [] }, + tags: [], + threat: [ + { + framework: 'MITRE ATT&CK', + tactic: { id: 'none', name: 'none', reference: 'none' }, + technique: [], + }, + ], + }; + expect(stepDataMock.mock.calls[1][1]).toEqual(expected); + }); }); test('it allows user to set the risk score as a number (and not a string)', async () => { @@ -221,30 +223,31 @@ describe('StepAboutRuleComponent', () => { .simulate('change', { target: { value: '80' } }); wrapper.find('[data-test-subj="about-continue"]').first().simulate('click').update(); - await wait(); - const expected: Omit = { - author: [], - isAssociatedToEndpointList: false, - isBuildingBlock: false, - license: '', - ruleNameOverride: '', - timestampOverride: '', - description: 'Test description text', - falsePositives: [''], - name: 'Test name text', - note: '', - references: [''], - riskScore: { value: 80, mapping: [] }, - severity: { value: 'low', mapping: [] }, - tags: [], - threat: [ - { - framework: 'MITRE ATT&CK', - tactic: { id: 'none', name: 'none', reference: 'none' }, - technique: [], - }, - ], - }; - expect(stepDataMock.mock.calls[1][1]).toEqual(expected); + await waitFor(() => { + const expected: Omit = { + author: [], + isAssociatedToEndpointList: false, + isBuildingBlock: false, + license: '', + ruleNameOverride: '', + timestampOverride: '', + description: 'Test description text', + falsePositives: [''], + name: 'Test name text', + note: '', + references: [''], + riskScore: { value: 80, mapping: [] }, + severity: { value: 'low', mapping: [] }, + tags: [], + threat: [ + { + framework: 'MITRE ATT&CK', + tactic: { id: 'none', name: 'none', reference: 'none' }, + technique: [], + }, + ], + }; + expect(stepDataMock.mock.calls[1][1]).toEqual(expected); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx index 58344e9e975346..b07caa754aec9a 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx @@ -11,7 +11,8 @@ import { act } from 'react-dom/test-utils'; import '../../../../../common/mock/match_media'; import { createKibanaContextProviderMock } from '../../../../../common/mock/kibana_react'; import { TestProviders } from '../../../../../common/mock'; -import { wait } from '../../../../../common/lib/helpers'; +// we don't have the types for waitFor just yet, so using "as waitFor" until when we do +import { wait as waitFor } from '@testing-library/react'; import { AllRules } from './index'; jest.mock('react-router-dom', () => { @@ -202,10 +203,10 @@ describe('AllRules', () => { ); await act(async () => { - await wait(); - - expect(wrapper.exists('[data-test-subj="monitoring-table"]')).toBeFalsy(); - expect(wrapper.exists('[data-test-subj="rules-table"]')).toBeTruthy(); + await waitFor(() => { + expect(wrapper.exists('[data-test-subj="monitoring-table"]')).toBeFalsy(); + expect(wrapper.exists('[data-test-subj="rules-table"]')).toBeTruthy(); + }); }); }); @@ -234,11 +235,11 @@ describe('AllRules', () => { monitoringTab.simulate('click'); await act(async () => { - wrapper.update(); - await wait(); - - expect(wrapper.exists('[data-test-subj="monitoring-table"]')).toBeTruthy(); - expect(wrapper.exists('[data-test-subj="rules-table"]')).toBeFalsy(); + await waitFor(() => { + wrapper.update(); + expect(wrapper.exists('[data-test-subj="monitoring-table"]')).toBeTruthy(); + expect(wrapper.exists('[data-test-subj="rules-table"]')).toBeFalsy(); + }); }); }); }); diff --git a/x-pack/plugins/security_solution/public/hosts/components/first_last_seen_host/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/first_last_seen_host/index.test.tsx index 9715c1cb5c8b45..a2f53be7218162 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/first_last_seen_host/index.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/first_last_seen_host/index.test.tsx @@ -7,10 +7,11 @@ import { cloneDeep } from 'lodash/fp'; import React from 'react'; import { MockedProvider } from 'react-apollo/test-utils'; -import { render, act } from '@testing-library/react'; + +// we don't have the types for waitFor just yet, so using "as waitFor" until when we do +import { render, act, wait as waitFor } from '@testing-library/react'; import { mockFirstLastSeenHostQuery } from '../../containers/hosts/first_last_seen/mock'; -import { wait } from '../../../common/lib/helpers'; import { TestProviders } from '../../../common/mock'; import { FirstLastSeenHost, FirstLastSeenHostType } from '.'; @@ -51,10 +52,12 @@ describe('FirstLastSeen Component', () => { ); - await act(() => wait()); - - expect(container.innerHTML).toBe( - `
${firstSeen}
` + await act(() => + waitFor(() => { + expect(container.innerHTML).toBe( + `
${firstSeen}
` + ); + }) ); }); @@ -66,9 +69,12 @@ describe('FirstLastSeen Component', () => { ); - await act(() => wait()); - expect(container.innerHTML).toBe( - `
${lastSeen}
` + await act(() => + waitFor(() => { + expect(container.innerHTML).toBe( + `
${lastSeen}
` + ); + }) ); }); @@ -83,10 +89,12 @@ describe('FirstLastSeen Component', () => { ); - await act(() => wait()); - - expect(container.innerHTML).toBe( - `
${lastSeen}
` + await act(() => + waitFor(() => { + expect(container.innerHTML).toBe( + `
${lastSeen}
` + ); + }) ); }); @@ -101,10 +109,12 @@ describe('FirstLastSeen Component', () => { ); - await act(() => wait()); - - expect(container.innerHTML).toBe( - `
${firstSeen}
` + await act(() => + waitFor(() => { + expect(container.innerHTML).toBe( + `
${firstSeen}
` + ); + }) ); }); @@ -118,8 +128,11 @@ describe('FirstLastSeen Component', () => { ); - await act(() => wait()); - expect(container.textContent).toBe('something-invalid'); + await act(() => + waitFor(() => { + expect(container.textContent).toBe('something-invalid'); + }) + ); }); test('Last Seen With a bad date time string', async () => { @@ -132,7 +145,10 @@ describe('FirstLastSeen Component', () => { ); - await act(() => wait()); - expect(container.textContent).toBe('something-invalid'); + await act(() => + waitFor(() => { + expect(container.textContent).toBe('something-invalid'); + }) + ); }); }); diff --git a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx index 63126da0b9bb5c..f7f1fbc30aeb74 100644 --- a/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/alerts_by_category/index.test.tsx @@ -13,7 +13,8 @@ import { ThemeProvider } from 'styled-components'; import '../../../common/mock/match_media'; import { useQuery } from '../../../common/containers/matrix_histogram'; -import { wait } from '../../../common/lib/helpers'; +// we don't have the types for waitFor just yet, so using "as waitFor" until when we do +import { wait as waitFor } from '@testing-library/react'; import { mockIndexPattern, TestProviders } from '../../../common/mock'; import { AlertsByCategory } from '.'; @@ -57,34 +58,45 @@ describe('Alerts by category', () => { ); - await wait(); - wrapper.update(); + await waitFor(() => { + wrapper.update(); + }); }); - test('it renders the expected title', () => { - expect(wrapper.find('[data-test-subj="header-section-title"]').text()).toEqual( - 'External alert trend' - ); + test('it renders the expected title', async () => { + await waitFor(() => { + expect(wrapper.find('[data-test-subj="header-section-title"]').text()).toEqual( + 'External alert trend' + ); + }); }); - test('it renders the subtitle (to prevent layout thrashing)', () => { - expect(wrapper.find('[data-test-subj="header-panel-subtitle"]').exists()).toBe(true); + test('it renders the subtitle (to prevent layout thrashing)', async () => { + await waitFor(() => { + expect(wrapper.find('[data-test-subj="header-panel-subtitle"]').exists()).toBe(true); + }); }); - test('it renders the expected filter fields', () => { - const expectedOptions = ['event.category', 'event.module']; + test('it renders the expected filter fields', async () => { + await waitFor(() => { + const expectedOptions = ['event.category', 'event.module']; - expectedOptions.forEach((option) => { - expect(wrapper.find(`option[value="${option}"]`).text()).toEqual(option); + expectedOptions.forEach((option) => { + expect(wrapper.find(`option[value="${option}"]`).text()).toEqual(option); + }); }); }); - test('it renders the `View alerts` button', () => { - expect(wrapper.find('[data-test-subj="view-alerts"]').exists()).toBe(true); + test('it renders the `View alerts` button', async () => { + await waitFor(() => { + expect(wrapper.find('[data-test-subj="view-alerts"]').exists()).toBe(true); + }); }); - test('it does NOT render the bar chart when data is not available', () => { - expect(wrapper.find(`.echChart`).exists()).toBe(false); + test('it does NOT render the bar chart when data is not available', async () => { + await waitFor(() => { + expect(wrapper.find(`.echChart`).exists()).toBe(false); + }); }); }); @@ -119,18 +131,21 @@ describe('Alerts by category', () => { ); - await wait(); wrapper.update(); }); - test('it renders the expected subtitle', () => { - expect(wrapper.find('[data-test-subj="header-panel-subtitle"]').text()).toEqual( - 'Showing: 6 external alerts' - ); + test('it renders the expected subtitle', async () => { + await waitFor(() => { + expect(wrapper.find('[data-test-subj="header-panel-subtitle"]').text()).toEqual( + 'Showing: 6 external alerts' + ); + }); }); - test('it renders the bar chart when data is available', () => { - expect(wrapper.find(`.echChart`).exists()).toBe(true); + test('it renders the bar chart when data is available', async () => { + await waitFor(() => { + expect(wrapper.find(`.echChart`).exists()).toBe(true); + }); }); }); }); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx index 30874e88747604..5ff78c9b29cf5a 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_host/index.test.tsx @@ -24,7 +24,8 @@ import { createStore, State } from '../../../common/store'; import { overviewHostQuery } from '../../containers/overview_host/index.gql_query'; import { GetOverviewHostQuery } from '../../../graphql/types'; -import { wait } from '../../../common/lib/helpers'; +// we don't have the types for waitFor just yet, so using "as waitFor" until when we do +import { wait as waitFor } from '@testing-library/react'; jest.mock('../../../common/lib/kibana'); jest.mock('../../../common/components/link_to'); @@ -147,11 +148,12 @@ describe('OverviewHost', () => { ); - await wait(); - wrapper.update(); + await waitFor(() => { + wrapper.update(); - expect(wrapper.find('[data-test-subj="header-panel-subtitle"]').first().text()).toEqual( - 'Showing: 16 events' - ); + expect(wrapper.find('[data-test-subj="header-panel-subtitle"]').first().text()).toEqual( + 'Showing: 16 events' + ); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx index 9ac4f7125f34dc..0bb887b38a4b10 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_network/index.test.tsx @@ -22,7 +22,8 @@ import { OverviewNetwork } from '.'; import { createStore, State } from '../../../common/store'; import { overviewNetworkQuery } from '../../containers/overview_network/index.gql_query'; import { GetOverviewHostQuery } from '../../../graphql/types'; -import { wait } from '../../../common/lib/helpers'; +// we don't have the types for waitFor just yet, so using "as waitFor" until when we do +import { wait as waitFor } from '@testing-library/react'; jest.mock('../../../common/components/link_to'); const mockNavigateToApp = jest.fn(); @@ -155,12 +156,13 @@ describe('OverviewNetwork', () => { ); - await wait(); - wrapper.update(); + await waitFor(() => { + wrapper.update(); - expect(wrapper.find('[data-test-subj="header-panel-subtitle"]').first().text()).toEqual( - 'Showing: 9 events' - ); + expect(wrapper.find('[data-test-subj="header-panel-subtitle"]').first().text()).toEqual( + 'Showing: 9 events' + ); + }); }); it('it renders View Network', () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx index e671244d97b57d..6c1c88f511edb6 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/index.test.tsx @@ -8,7 +8,8 @@ import { mount } from 'enzyme'; import { MockedProvider } from 'react-apollo/test-utils'; import React from 'react'; -import { wait } from '../../../common/lib/helpers'; +// we don't have the types for waitFor just yet, so using "as waitFor" until when we do +import { wait as waitFor } from '@testing-library/react'; import '../../../common/mock/match_media'; import { TestProviders, apolloClient } from '../../../common/mock/test_providers'; import { mockOpenTimelineQueryResults } from '../../../common/mock/timeline_results'; @@ -119,15 +120,15 @@ describe('StatefulOpenTimeline', () => { ); - await wait(); - - wrapper - .find('[data-test-subj="search-bar"] input') - .simulate('keyup', { key: 'Enter', target: { value: ' abcd ' } }); + await waitFor(() => { + wrapper + .find('[data-test-subj="search-bar"] input') + .simulate('keyup', { key: 'Enter', target: { value: ' abcd ' } }); - expect(wrapper.find('[data-test-subj="query-message"]').first().text()).toContain( - 'Showing: 11 timelines with' - ); + expect(wrapper.find('[data-test-subj="query-message"]').first().text()).toContain( + 'Showing: 11 timelines with' + ); + }); }); test('echos (renders) the query when the user enters a query', async () => { @@ -144,15 +145,15 @@ describe('StatefulOpenTimeline', () => { ); - await wait(); - - wrapper - .find('[data-test-subj="search-bar"] input') - .simulate('keyup', { key: 'Enter', target: { value: ' abcd ' } }); + await waitFor(() => { + wrapper + .find('[data-test-subj="search-bar"] input') + .simulate('keyup', { key: 'Enter', target: { value: ' abcd ' } }); - expect(wrapper.find('[data-test-subj="selectable-query-text"]').first().text()).toEqual( - 'with "abcd"' - ); + expect(wrapper.find('[data-test-subj="selectable-query-text"]').first().text()).toEqual( + 'with "abcd"' + ); + }); }); }); @@ -171,12 +172,12 @@ describe('StatefulOpenTimeline', () => { ); - await wait(); - - expect( - wrapper.find(`.${OPEN_TIMELINE_CLASS_NAME} input`).first().getDOMNode().id === - document.activeElement!.id - ).toBe(true); + await waitFor(() => { + expect( + wrapper.find(`.${OPEN_TIMELINE_CLASS_NAME} input`).first().getDOMNode().id === + document.activeElement!.id + ).toBe(true); + }); }); }); @@ -198,26 +199,26 @@ describe('StatefulOpenTimeline', () => { ); - await wait(); - - wrapper - .find('.euiCheckbox__input') - .first() - .simulate('change', { target: { checked: true } }); - - wrapper.find('[data-test-subj="favorite-selected"]').first().simulate('click'); - - expect(addTimelinesToFavorites).toHaveBeenCalledWith([ - 'saved-timeline-11', - 'saved-timeline-10', - 'saved-timeline-9', - 'saved-timeline-8', - 'saved-timeline-6', - 'saved-timeline-5', - 'saved-timeline-4', - 'saved-timeline-3', - 'saved-timeline-2', - ]); + await waitFor(() => { + wrapper + .find('.euiCheckbox__input') + .first() + .simulate('change', { target: { checked: true } }); + + wrapper.find('[data-test-subj="favorite-selected"]').first().simulate('click'); + + expect(addTimelinesToFavorites).toHaveBeenCalledWith([ + 'saved-timeline-11', + 'saved-timeline-10', + 'saved-timeline-9', + 'saved-timeline-8', + 'saved-timeline-6', + 'saved-timeline-5', + 'saved-timeline-4', + 'saved-timeline-3', + 'saved-timeline-2', + ]); + }); }); }); @@ -239,26 +240,26 @@ describe('StatefulOpenTimeline', () => { ); - await wait(); - - wrapper - .find('.euiCheckbox__input') - .first() - .simulate('change', { target: { checked: true } }); - - wrapper.find('[data-test-subj="delete-selected"]').first().simulate('click'); - - expect(deleteTimelines).toHaveBeenCalledWith([ - 'saved-timeline-11', - 'saved-timeline-10', - 'saved-timeline-9', - 'saved-timeline-8', - 'saved-timeline-6', - 'saved-timeline-5', - 'saved-timeline-4', - 'saved-timeline-3', - 'saved-timeline-2', - ]); + await waitFor(() => { + wrapper + .find('.euiCheckbox__input') + .first() + .simulate('change', { target: { checked: true } }); + + wrapper.find('[data-test-subj="delete-selected"]').first().simulate('click'); + + expect(deleteTimelines).toHaveBeenCalledWith([ + 'saved-timeline-11', + 'saved-timeline-10', + 'saved-timeline-9', + 'saved-timeline-8', + 'saved-timeline-6', + 'saved-timeline-5', + 'saved-timeline-4', + 'saved-timeline-3', + 'saved-timeline-2', + ]); + }); }); }); @@ -278,19 +279,19 @@ describe('StatefulOpenTimeline', () => { ); - await wait(); - - wrapper - .find('.euiCheckbox__input') - .first() - .simulate('change', { target: { checked: true } }); + await waitFor(() => { + wrapper + .find('.euiCheckbox__input') + .first() + .simulate('change', { target: { checked: true } }); - const selectedItems: [] = wrapper - .find('[data-test-subj="open-timeline"]') - .last() - .prop('selectedItems'); + const selectedItems: [] = wrapper + .find('[data-test-subj="open-timeline"]') + .last() + .prop('selectedItems'); - expect(selectedItems.length).toEqual(13); // 13 because we did mock 13 timelines in the query + expect(selectedItems.length).toEqual(13); // 13 because we did mock 13 timelines in the query + }); }); }); @@ -366,29 +367,37 @@ describe('StatefulOpenTimeline', () => { ); - await wait(); - wrapper.update(); - - expect( - wrapper.find('[data-test-subj="open-timeline"]').last().prop('itemIdToExpandedNotesRowMap') - ).toEqual({}); - - wrapper.find('[data-test-subj="expand-notes"]').first().simulate('click'); - - expect( - wrapper.find('[data-test-subj="open-timeline"]').last().prop('itemIdToExpandedNotesRowMap') - ).toEqual({ - '10849df0-7b44-11e9-a608-ab3d811609': ( - ({ ...note, savedObjectId: note.noteId }) - ) - : [] - } - /> - ), + await waitFor(() => { + wrapper.update(); + + expect( + wrapper + .find('[data-test-subj="open-timeline"]') + .last() + .prop('itemIdToExpandedNotesRowMap') + ).toEqual({}); + + wrapper.find('[data-test-subj="expand-notes"]').first().simulate('click'); + + expect( + wrapper + .find('[data-test-subj="open-timeline"]') + .last() + .prop('itemIdToExpandedNotesRowMap') + ).toEqual({ + '10849df0-7b44-11e9-a608-ab3d811609': ( + ({ ...note, savedObjectId: note.noteId }) + ) + : [] + } + /> + ), + }); }); }); @@ -407,21 +416,21 @@ describe('StatefulOpenTimeline', () => { ); - await wait(); - - wrapper.update(); + await waitFor(() => { + wrapper.update(); - wrapper.find('[data-test-subj="expand-notes"]').first().simulate('click'); - expect(wrapper.find('[data-test-subj="note-previews-container"]').exists()).toEqual(true); - expect(wrapper.find('[data-test-subj="updated-by"]').exists()).toEqual(true); + wrapper.find('[data-test-subj="expand-notes"]').first().simulate('click'); + expect(wrapper.find('[data-test-subj="note-previews-container"]').exists()).toEqual(true); + expect(wrapper.find('[data-test-subj="updated-by"]').exists()).toEqual(true); - expect( - wrapper - .find('[data-test-subj="note-previews-container"]') - .find('[data-test-subj="updated-by"]') - .first() - .text() - ).toEqual('elastic'); + expect( + wrapper + .find('[data-test-subj="note-previews-container"]') + .find('[data-test-subj="updated-by"]') + .first() + .text() + ).toEqual('elastic'); + }); }); /** @@ -442,11 +451,11 @@ describe('StatefulOpenTimeline', () => { ); - await wait(); - - expect(wrapper.find(`[data-test-subj="timeline-${TimelineTabsStyle.tab}"]`).exists()).toEqual( - true - ); + await waitFor(() => { + expect( + wrapper.find(`[data-test-subj="timeline-${TimelineTabsStyle.tab}"]`).exists() + ).toEqual(true); + }); }); }); @@ -467,13 +476,14 @@ describe('StatefulOpenTimeline', () => { ); const getSelectedItem = (): [] => wrapper.find('[data-test-subj="open-timeline"]').last().prop('selectedItems'); - await wait(); - expect(getSelectedItem().length).toEqual(0); - wrapper - .find('.euiCheckbox__input') - .first() - .simulate('change', { target: { checked: true } }); - expect(getSelectedItem().length).toEqual(13); + await waitFor(() => { + expect(getSelectedItem().length).toEqual(0); + wrapper + .find('.euiCheckbox__input') + .first() + .simulate('change', { target: { checked: true } }); + expect(getSelectedItem().length).toEqual(13); + }); }); }); @@ -492,13 +502,13 @@ describe('StatefulOpenTimeline', () => { ); - await wait(); - - wrapper.update(); + await waitFor(() => { + wrapper.update(); - expect(wrapper.find('[data-test-subj="query-message"]').first().text()).toContain( - 'Showing: 11 timelines ' - ); + expect(wrapper.find('[data-test-subj="query-message"]').first().text()).toContain( + 'Showing: 11 timelines ' + ); + }); }); // TODO - Have been skip because we need to re-implement the test as the component changed @@ -519,21 +529,21 @@ describe('StatefulOpenTimeline', () => { ); - await wait(); - - wrapper - .find( - `[data-test-subj="title-${ - mockOpenTimelineQueryResults[0].result.data!.getAllTimeline.timeline[0].savedObjectId - }"]` - ) - .first() - .simulate('click'); - - expect(onOpenTimeline).toHaveBeenCalledWith({ - duplicate: false, - timelineId: mockOpenTimelineQueryResults[0].result.data!.getAllTimeline.timeline[0] - .savedObjectId, + await waitFor(() => { + wrapper + .find( + `[data-test-subj="title-${ + mockOpenTimelineQueryResults[0].result.data!.getAllTimeline.timeline[0].savedObjectId + }"]` + ) + .first() + .simulate('click'); + + expect(onOpenTimeline).toHaveBeenCalledWith({ + duplicate: false, + timelineId: mockOpenTimelineQueryResults[0].result.data!.getAllTimeline.timeline[0] + .savedObjectId, + }); }); }); @@ -555,10 +565,10 @@ describe('StatefulOpenTimeline', () => { ); - await wait(); + await waitFor(() => { + wrapper.find('[data-test-subj="open-duplicate"]').first().simulate('click'); - wrapper.find('[data-test-subj="open-duplicate"]').first().simulate('click'); - - expect(onOpenTimeline).toBeCalledWith({ duplicate: true, timelineId: 'saved-timeline-11' }); + expect(onOpenTimeline).toBeCalledWith({ duplicate: true, timelineId: 'saved-timeline-11' }); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/index.test.tsx index 8382af6056ca78..3017f553d59d5f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/index.test.tsx @@ -10,7 +10,8 @@ import React from 'react'; import { MockedProvider } from 'react-apollo/test-utils'; import { ThemeProvider } from 'styled-components'; -import { wait } from '../../../../common/lib/helpers'; +// we don't have the types for waitFor just yet, so using "as waitFor" until when we do +import { wait as waitFor } from '@testing-library/react'; import { TestProviderWithoutDragAndDrop } from '../../../../common/mock/test_providers'; import { mockOpenTimelineQueryResults } from '../../../../common/mock/timeline_results'; import { useGetAllTimeline, getAllTimeline } from '../../../containers/all'; @@ -64,10 +65,15 @@ describe('OpenTimelineModal', () => { ); - await wait(); + await waitFor( + () => { + wrapper.update(); - wrapper.update(); - - expect(wrapper.find('div[data-test-subj="open-timeline-modal"].euiModal').length).toEqual(1); - }); + expect(wrapper.find('div[data-test-subj="open-timeline-modal"].euiModal').length).toEqual( + 1 + ); + }, + { timeout: 10000 } + ); + }, 20000); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_button.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_button.test.tsx index 80bca8096f615f..a3f180ce84c58a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_button.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_button.test.tsx @@ -10,7 +10,8 @@ import React from 'react'; import { MockedProvider } from 'react-apollo/test-utils'; import { ThemeProvider } from 'styled-components'; -import { wait } from '../../../../common/lib/helpers'; +// we don't have the types for waitFor just yet, so using "as waitFor" until when we do +import { wait as waitFor } from '@testing-library/react'; import { TestProviderWithoutDragAndDrop } from '../../../../common/mock/test_providers'; import { mockOpenTimelineQueryResults } from '../../../../common/mock/timeline_results'; import * as i18n from '../translations'; @@ -29,13 +30,13 @@ describe('OpenTimelineModalButton', () => { ); - await wait(); - - wrapper.update(); + await waitFor(() => { + wrapper.update(); - expect(wrapper.find('[data-test-subj="open-timeline-button"]').first().text()).toEqual( - i18n.OPEN_TIMELINE - ); + expect(wrapper.find('[data-test-subj="open-timeline-button"]').first().text()).toEqual( + i18n.OPEN_TIMELINE + ); + }); }); describe('onClick prop', () => { @@ -51,13 +52,13 @@ describe('OpenTimelineModalButton', () => { ); - await wait(); - - wrapper.find('[data-test-subj="open-timeline-button"]').first().simulate('click'); + await waitFor(() => { + wrapper.find('[data-test-subj="open-timeline-button"]').first().simulate('click'); - wrapper.update(); + wrapper.update(); - expect(onClick).toBeCalled(); + expect(onClick).toBeCalled(); + }); }); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx index b36f1dcc03261c..5a98263cbd3fd3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/index.test.tsx @@ -16,7 +16,8 @@ import { TestProviders } from '../../../../common/mock/test_providers'; import { Body, BodyProps } from '.'; import { columnRenderers, rowRenderers } from './renderers'; import { Sort } from './sort'; -import { wait } from '../../../../common/lib/helpers'; +// we don't have the types for waitFor just yet, so using "as waitFor" until when we do +import { wait as waitFor } from '@testing-library/react'; import { useMountAppended } from '../../../../common/utils/use_mount_appended'; import { SELECTOR_TIMELINE_BODY_CLASS_NAME, TimelineBody } from '../styles'; import { TimelineType } from '../../../../../common/types/timeline'; @@ -130,16 +131,17 @@ describe('Body', () => { ); wrapper.update(); - await wait(); - wrapper.update(); - headersJustTimestamp.forEach(() => { - expect( - wrapper - .find('[data-test-subj="data-driven-columns"]') - .first() - .find('[data-test-subj="localized-date-tool-tip"]') - .exists() - ).toEqual(true); + await waitFor(() => { + wrapper.update(); + headersJustTimestamp.forEach(() => { + expect( + wrapper + .find('[data-test-subj="data-driven-columns"]') + .first() + .find('[data-test-subj="localized-date-tool-tip"]') + .exists() + ).toEqual(true); + }); }); }, 20000); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx index 8b75f8b398ac1c..51edf7336c4e7d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.test.tsx @@ -16,7 +16,8 @@ import { ReturnSignalIndex, } from '../../../detections/containers/detection_engine/alerts/use_signal_index'; import { mocksSource } from '../../../common/containers/source/mock'; -import { wait } from '../../../common/lib/helpers'; +// we don't have the types for waitFor just yet, so using "as waitFor" until when we do +import { wait as waitFor } from '@testing-library/react'; import { defaultHeaders, mockTimelineData, TestProviders } from '../../../common/mock'; import { Direction } from '../../../graphql/types'; import { timelineQuery } from '../../containers/index.gql_query'; @@ -124,12 +125,13 @@ describe('StatefulTimeline', () => { ); await act(async () => { - await wait(); - wrapper.update(); - const timeline = wrapper.find(Timeline); - expect(timeline.props().indexToAdd).toEqual([ - 'no-alert-index-049FC71A-4C2C-446F-9901-37XMC5024C51', - ]); + await waitFor(() => { + wrapper.update(); + const timeline = wrapper.find(Timeline); + expect(timeline.props().indexToAdd).toEqual([ + 'no-alert-index-049FC71A-4C2C-446F-9901-37XMC5024C51', + ]); + }); }); }); @@ -147,10 +149,11 @@ describe('StatefulTimeline', () => { ); await act(async () => { - await wait(); - wrapper.update(); - const timeline = wrapper.find(Timeline); - expect(timeline.props().indexToAdd).toEqual(['mock-siem-signals-index']); + await waitFor(() => { + wrapper.update(); + const timeline = wrapper.find(Timeline); + expect(timeline.props().indexToAdd).toEqual(['mock-siem-signals-index']); + }); }); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx index 1e0e85d4a48d91..06dd6f44bea94a 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx @@ -7,6 +7,8 @@ import React from 'react'; import { shallow } from 'enzyme'; +// we don't have the types for waitFor just yet, so using "as waitFor" for when we do +import { wait as waitFor } from '@testing-library/react'; import '../../../common/mock/match_media'; import { mockGlobalState, @@ -44,10 +46,6 @@ import { TimelineStatus, TimelineType } from '../../../../common/types/timeline' jest.mock('../../containers/local_storage'); -const wait = (ms: number = 500): Promise => { - return new Promise((resolve) => setTimeout(resolve, ms)); -}; - const addTimelineInStorageMock = addTimelineInStorage as jest.Mock; describe('epicLocalStorage', () => { @@ -128,8 +126,7 @@ describe('epicLocalStorage', () => { ); store.dispatch(upsertColumn({ id: 'test', index: 1, column: defaultHeaders[0] })); - await wait(); - expect(addTimelineInStorageMock).toHaveBeenCalled(); + await waitFor(() => expect(addTimelineInStorageMock).toHaveBeenCalled()); }); it('persist timeline when removing a column ', async () => { @@ -139,8 +136,7 @@ describe('epicLocalStorage', () => { ); store.dispatch(removeColumn({ id: 'test', columnId: '@timestamp' })); - await wait(); - expect(addTimelineInStorageMock).toHaveBeenCalled(); + await waitFor(() => expect(addTimelineInStorageMock).toHaveBeenCalled()); }); it('persists resizing of a column', async () => { @@ -150,8 +146,7 @@ describe('epicLocalStorage', () => { ); store.dispatch(applyDeltaToColumnWidth({ id: 'test', columnId: '@timestamp', delta: 80 })); - await wait(); - expect(addTimelineInStorageMock).toHaveBeenCalled(); + await waitFor(() => expect(addTimelineInStorageMock).toHaveBeenCalled()); }); it('persist the resetting of the fields', async () => { @@ -161,8 +156,7 @@ describe('epicLocalStorage', () => { ); store.dispatch(updateColumns({ id: 'test', columns: defaultHeaders })); - await wait(); - expect(addTimelineInStorageMock).toHaveBeenCalled(); + await waitFor(() => expect(addTimelineInStorageMock).toHaveBeenCalled()); }); it('persist items per page', async () => { @@ -172,8 +166,7 @@ describe('epicLocalStorage', () => { ); store.dispatch(updateItemsPerPage({ id: 'test', itemsPerPage: 50 })); - await wait(); - expect(addTimelineInStorageMock).toHaveBeenCalled(); + await waitFor(() => expect(addTimelineInStorageMock).toHaveBeenCalled()); }); it('persist the sorting of a column', async () => { @@ -191,7 +184,6 @@ describe('epicLocalStorage', () => { }, }) ); - await wait(); - expect(addTimelineInStorageMock).toHaveBeenCalled(); + await waitFor(() => expect(addTimelineInStorageMock).toHaveBeenCalled()); }); }); From 21977a7e6af04bb782f8c5434e24141fca4edc8e Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Mon, 20 Jul 2020 19:18:42 -0400 Subject: [PATCH 011/101] [Security Solution][Exceptions] - Make esTypes and subType available to index patterns (#72336) ## Summary This PR updates the following: - `useFetchIndexPatterns` now returns `indexPatterns` whose fields include `esTypes` and `subType` - Why?? The exceptions builder needs these two fields to determine what fields are of ES type `nested` and parent paths - exceptions add and edit modals now use the `rule.index` field to pass into `useFetchindexPatterns` - Before we were using the signals index and alerts index for endpoint, needs to be rule's index patterns - if no index patterns exist on the rule (if rule created via API, it's not required), then uses `DEFAULT_INDEX_PATTERN` - updates the autocomplete validation to use `IField.esTypes` to check type instead of `IField.type` --- .../autocomplete/field_value_lists.test.tsx | 115 ++++++++++++++---- .../autocomplete/field_value_lists.tsx | 8 +- .../autocomplete/field_value_match.tsx | 8 +- .../autocomplete/field_value_match_any.tsx | 2 +- .../components/autocomplete/helpers.test.ts | 32 +---- .../common/components/autocomplete/helpers.ts | 36 +++--- .../exceptions/add_exception_modal/index.tsx | 50 +++----- .../components/exceptions/builder/index.tsx | 23 ++-- .../exceptions/edit_exception_modal/index.tsx | 40 +++--- .../exceptions/viewer/index.test.tsx | 3 + .../components/exceptions/viewer/index.tsx | 10 +- .../containers/source/index.gql_query.ts | 2 + .../public/common/containers/source/index.tsx | 2 +- .../alerts_table/default_config.tsx | 9 +- .../components/alerts_table/index.tsx | 26 ++-- .../detection_engine/rules/details/index.tsx | 3 +- .../public/graphql/introspection.json | 36 ++++++ .../security_solution/public/graphql/types.ts | 12 ++ .../server/graphql/ecs/resolvers.ts | 34 +++++- .../server/graphql/ecs/schema.gql.ts | 1 + .../server/graphql/source_status/resolvers.ts | 39 ++++++ .../graphql/source_status/schema.gql.ts | 5 + .../security_solution/server/graphql/types.ts | 32 +++++ .../server/lib/index_fields/types.ts | 3 + 24 files changed, 362 insertions(+), 169 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.test.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.test.tsx index 7734344d193b8b..1ff5d770521f3b 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.test.tsx @@ -8,14 +8,27 @@ import { ThemeProvider } from 'styled-components'; import { mount } from 'enzyme'; import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; +// we don't have the types for waitFor just yet, so using "as waitFor" until when we do +import { wait as waitFor } from '@testing-library/react'; import { getField } from '../../../../../../../src/plugins/data/common/index_patterns/fields/fields.mocks.ts'; -import { AutocompleteFieldListsComponent } from './field_value_lists'; +import { ListSchema } from '../../../lists_plugin_deps'; import { getFoundListSchemaMock } from '../../../../../lists/common/schemas/response/found_list_schema.mock'; +import { getListResponseMock } from '../../../../../lists/common/schemas/response/list_schema.mock'; +import { DATE_NOW } from '../../../../../lists/common/constants.mock'; + +import { AutocompleteFieldListsComponent } from './field_value_lists'; -const mockStart = jest.fn(); -const mockResult = getFoundListSchemaMock(); jest.mock('../../../common/lib/kibana'); +const mockStart = jest.fn(); +const mockKeywordList: ListSchema = { + ...getListResponseMock(), + id: 'keyword_list', + type: 'keyword', + name: 'keyword list', +}; +const mockResult = { ...getFoundListSchemaMock() }; +mockResult.data = [...mockResult.data, mockKeywordList]; jest.mock('../../../lists_plugin_deps', () => { const originalModule = jest.requireActual('../../../lists_plugin_deps'); @@ -31,7 +44,7 @@ jest.mock('../../../lists_plugin_deps', () => { }); describe('AutocompleteFieldListsComponent', () => { - test('it renders disabled if "isDisabled" is true', () => { + test('it renders disabled if "isDisabled" is true', async () => { const wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> { ); - expect( - wrapper - .find(`[data-test-subj="valuesAutocompleteComboBox listsComboxBox"] input`) - .prop('disabled') - ).toBeTruthy(); + await waitFor(() => { + expect( + wrapper + .find(`[data-test-subj="valuesAutocompleteComboBox listsComboxBox"] input`) + .prop('disabled') + ).toBeTruthy(); + }); }); - test('it renders loading if "isLoading" is true', () => { + test('it renders loading if "isLoading" is true', async () => { const wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> { /> ); - wrapper - .find(`[data-test-subj="valuesAutocompleteComboBox listsComboxBox"] button`) - .at(0) - .simulate('click'); - expect( + + await waitFor(() => { wrapper - .find( - `EuiComboBoxOptionsList[data-test-subj="valuesAutocompleteComboBox listsComboxBox-optionsList"]` - ) - .prop('isLoading') - ).toBeTruthy(); + .find(`[data-test-subj="valuesAutocompleteComboBox listsComboxBox"] button`) + .at(0) + .simulate('click'); + expect( + wrapper + .find( + `EuiComboBoxOptionsList[data-test-subj="valuesAutocompleteComboBox listsComboxBox-optionsList"]` + ) + .prop('isLoading') + ).toBeTruthy(); + }); }); - test('it allows user to clear values if "isClearable" is true', () => { + test('it allows user to clear values if "isClearable" is true', async () => { const wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> { /> ); - expect( wrapper .find(`[data-test-subj="comboBoxInput"]`) @@ -102,7 +119,55 @@ describe('AutocompleteFieldListsComponent', () => { ).toBeTruthy(); }); - test('it correctly displays selected list', () => { + test('it correctly displays lists that match the selected "keyword" field esType', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + wrapper.find('[data-test-subj="comboBoxToggleListButton"] button').simulate('click'); + + expect( + wrapper + .find('EuiComboBox[data-test-subj="valuesAutocompleteComboBox listsComboxBox"]') + .prop('options') + ).toEqual([{ label: 'keyword list' }]); + }); + + test('it correctly displays lists that match the selected "ip" field esType', () => { + const wrapper = mount( + ({ eui: euiLightVars, darkMode: false })}> + + + ); + + wrapper.find('[data-test-subj="comboBoxToggleListButton"] button').simulate('click'); + + expect( + wrapper + .find('EuiComboBox[data-test-subj="valuesAutocompleteComboBox listsComboxBox"]') + .prop('options') + ).toEqual([{ label: 'some name' }]); + }); + + test('it correctly displays selected list', async () => { const wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> { }).onChange([{ label: 'some name' }]); expect(mockOnChange).toHaveBeenCalledWith({ - created_at: '2020-04-20T15:25:31.830Z', + created_at: DATE_NOW, created_by: 'some user', description: 'some description', id: 'some-list-id', @@ -154,7 +219,7 @@ describe('AutocompleteFieldListsComponent', () => { name: 'some name', tie_breaker_id: '6a76b69d-80df-4ab2-8c3e-85f466b06a0e', type: 'ip', - updated_at: '2020-04-20T15:25:31.830Z', + updated_at: DATE_NOW, updated_by: 'some user', }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.tsx index d8ce27e97874d4..a9d85452651b51 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_lists.tsx @@ -36,8 +36,12 @@ export const AutocompleteFieldListsComponent: React.FC name, []); const optionsMemo = useMemo(() => { - if (selectedField != null) { - return lists.filter(({ type }) => type === selectedField.type); + if ( + selectedField != null && + selectedField.esTypes != null && + selectedField.esTypes.length > 0 + ) { + return lists.filter(({ type }) => selectedField.esTypes?.includes(type)); } else { return []; } diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx index 32a82af114baed..a082811920f888 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/field_value_match.tsx @@ -79,10 +79,10 @@ export const AutocompleteFieldMatchComponent: React.FC validateParams(selectedValue, selectedField ? selectedField.type : ''), - [selectedField, selectedValue] - ); + const isValid = useMemo((): boolean => validateParams(selectedValue, selectedField), [ + selectedField, + selectedValue, + ]); return ( { const areAnyInvalid = selectedComboOptions.filter( - ({ label }) => !validateParams(label, selectedField ? selectedField.type : '') + ({ label }) => !validateParams(label, selectedField) ); return areAnyInvalid.length === 0; }, [selectedComboOptions, selectedField]); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts index cfe23b9391ec0a..cb07d999131076 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.test.ts @@ -55,49 +55,25 @@ describe('helpers', () => { describe('#validateParams', () => { test('returns true if value is undefined', () => { - const isValid = validateParams(undefined, 'date'); + const isValid = validateParams(undefined, getField('@timestamp')); expect(isValid).toBeTruthy(); }); test('returns true if value is empty string', () => { - const isValid = validateParams('', 'date'); + const isValid = validateParams('', getField('@timestamp')); expect(isValid).toBeTruthy(); }); test('returns true if type is "date" and value is valid', () => { - const isValid = validateParams('1994-11-05T08:15:30-05:00', 'date'); + const isValid = validateParams('1994-11-05T08:15:30-05:00', getField('@timestamp')); expect(isValid).toBeTruthy(); }); test('returns false if type is "date" and value is not valid', () => { - const isValid = validateParams('1593478826', 'date'); - - expect(isValid).toBeFalsy(); - }); - - test('returns true if type is "ip" and value is valid', () => { - const isValid = validateParams('126.45.211.34', 'ip'); - - expect(isValid).toBeTruthy(); - }); - - test('returns false if type is "ip" and value is not valid', () => { - const isValid = validateParams('hellooo', 'ip'); - - expect(isValid).toBeFalsy(); - }); - - test('returns true if type is "number" and value is valid', () => { - const isValid = validateParams('123', 'number'); - - expect(isValid).toBeTruthy(); - }); - - test('returns false if type is "number" and value is not valid', () => { - const isValid = validateParams('not a number', 'number'); + const isValid = validateParams('1593478826', getField('@timestamp')); expect(isValid).toBeFalsy(); }); diff --git a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts b/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts index 483ca5d6d332ee..16659593784dba 100644 --- a/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/components/autocomplete/helpers.ts @@ -7,7 +7,7 @@ import dateMath from '@elastic/datemath'; import { EuiComboBoxOptionOption } from '@elastic/eui'; -import { IFieldType, Ipv4Address } from '../../../../../../../src/plugins/data/common'; +import { IFieldType } from '../../../../../../../src/plugins/data/common'; import { EXCEPTION_OPERATORS, @@ -30,29 +30,27 @@ export const getOperators = (field: IFieldType | undefined): OperatorOption[] => } }; -export function validateParams(params: string | undefined, type: string) { +export const validateParams = ( + params: string | undefined, + field: IFieldType | undefined +): boolean => { // Box would show error state if empty otherwise if (params == null || params === '') { return true; } - switch (type) { - case 'date': - const moment = dateMath.parse(params); - return Boolean(moment && moment.isValid()); - case 'ip': - try { - return Boolean(new Ipv4Address(params)); - } catch (e) { - return false; - } - case 'number': - const val = parseFloat(params); - return typeof val === 'number' && !isNaN(val); - default: - return true; - } -} + const types = field != null && field.esTypes != null ? field.esTypes : []; + + return types.reduce((acc, type) => { + switch (type) { + case 'date': + const moment = dateMath.parse(params); + return Boolean(moment && moment.isValid()); + default: + return acc; + } + }, true); +}; export function getGenericComboBoxProps({ options, diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx index 53c53f48f076bc..e630645ef8c4e0 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/add_exception_modal/index.tsx @@ -22,7 +22,6 @@ import { EuiText, } from '@elastic/eui'; import { Status } from '../../../../../common/detection_engine/schemas/common/schemas'; -import { alertsIndexPattern } from '../../../../../common/endpoint/constants'; import { ExceptionListItemSchema, CreateExceptionListItemSchema, @@ -48,24 +47,18 @@ import { } from '../helpers'; import { useFetchIndexPatterns } from '../../../../detections/containers/detection_engine/rules'; -export interface AddExceptionOnClick { +export interface AddExceptionModalBaseProps { ruleName: string; ruleId: string; exceptionListType: ExceptionListType; + ruleIndices: string[]; alertData?: { ecsData: Ecs; nonEcsData: TimelineNonEcsData[]; }; } -interface AddExceptionModalProps { - ruleName: string; - ruleId: string; - exceptionListType: ExceptionListType; - alertData?: { - ecsData: Ecs; - nonEcsData: TimelineNonEcsData[]; - }; +export interface AddExceptionModalProps extends AddExceptionModalBaseProps { onCancel: () => void; onConfirm: (didCloseAlert: boolean) => void; alertStatus?: Status; @@ -78,10 +71,8 @@ const Modal = styled(EuiModal)` `; const ModalHeader = styled(EuiModalHeader)` - ${({ theme }) => css` - flex-direction: column; - align-items: flex-start; - `} + flex-direction: column; + align-items: flex-start; `; const ModalHeaderSubtitle = styled.div` @@ -103,6 +94,7 @@ const ModalBodySection = styled.section` export const AddExceptionModal = memo(function AddExceptionModal({ ruleName, ruleId, + ruleIndices, exceptionListType, alertData, onCancel, @@ -120,10 +112,11 @@ export const AddExceptionModal = memo(function AddExceptionModal({ const [fetchOrCreateListError, setFetchOrCreateListError] = useState(false); const { addError, addSuccess } = useAppToasts(); const { loading: isSignalIndexLoading, signalIndexName } = useSignalIndex(); + const [ + { isLoading: isSignalIndexPatternLoading, indexPatterns: signalIndexPatterns }, + ] = useFetchIndexPatterns(signalIndexName !== null ? [signalIndexName] : []); - const [{ isLoading: indexPatternLoading, indexPatterns }] = useFetchIndexPatterns( - signalIndexName !== null ? [signalIndexName] : [] - ); + const [{ isLoading: isIndexPatternLoading, indexPatterns }] = useFetchIndexPatterns(ruleIndices); const onError = useCallback( (error: Error) => { @@ -183,19 +176,19 @@ export const AddExceptionModal = memo(function AddExceptionModal({ }, [alertData, exceptionListType, ruleExceptionList, ruleName]); useEffect(() => { - if (indexPatternLoading === false && isSignalIndexLoading === false) { + if (isSignalIndexPatternLoading === false && isSignalIndexLoading === false) { setShouldDisableBulkClose( entryHasListType(exceptionItemsToAdd) || - entryHasNonEcsType(exceptionItemsToAdd, indexPatterns) || + entryHasNonEcsType(exceptionItemsToAdd, signalIndexPatterns) || exceptionItemsToAdd.length === 0 ); } }, [ setShouldDisableBulkClose, exceptionItemsToAdd, - indexPatternLoading, + isSignalIndexPatternLoading, isSignalIndexLoading, - indexPatterns, + signalIndexPatterns, ]); useEffect(() => { @@ -274,15 +267,8 @@ export const AddExceptionModal = memo(function AddExceptionModal({ [fetchOrCreateListError, exceptionItemsToAdd] ); - const indexPatternConfig = useCallback(() => { - if (exceptionListType === 'endpoint') { - return [alertsIndexPattern]; - } - return signalIndexName ? [signalIndexName] : []; - }, [exceptionListType, signalIndexName]); - return ( - + {i18n.ADD_EXCEPTION} @@ -301,8 +287,9 @@ export const AddExceptionModal = memo(function AddExceptionModal({ )} {fetchOrCreateListError === false && !isSignalIndexLoading && - !indexPatternLoading && + !isSignalIndexPatternLoading && !isLoadingExceptionList && + !isIndexPatternLoading && ruleExceptionList && ( <> @@ -314,8 +301,7 @@ export const AddExceptionModal = memo(function AddExceptionModal({ listId={ruleExceptionList.list_id} listNamespaceType={ruleExceptionList.namespace_type} ruleName={ruleName} - indexPatternConfig={indexPatternConfig()} - isLoading={false} + indexPatterns={indexPatterns} isOrDisabled={false} isAndDisabled={false} data-test-subj="alert-exception-builder" diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx index 08e5b49073ecf3..f6feca591dc6da 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/builder/index.tsx @@ -3,12 +3,12 @@ * 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, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState, useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import styled from 'styled-components'; import { ExceptionListItemComponent } from './builder_exception_item'; -import { useFetchIndexPatterns } from '../../../../detections/containers/detection_engine/rules/fetch_index_patterns'; +import { IIndexPattern } from '../../../../../../../../src/plugins/data/common'; import { ExceptionListItemSchema, NamespaceType, @@ -22,7 +22,6 @@ import { AndOrBadge } from '../../and_or_badge'; import { BuilderButtonOptions } from './builder_button_options'; import { getNewExceptionItem, filterExceptionItems } from '../helpers'; import { ExceptionsBuilderExceptionItem, CreateExceptionListItemBuilderSchema } from '../types'; -import { Loader } from '../../loader'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import exceptionableFields from '../exceptionable_fields.json'; @@ -51,8 +50,7 @@ interface ExceptionBuilderProps { listId: string; listNamespaceType: NamespaceType; ruleName: string; - indexPatternConfig: string[]; - isLoading: boolean; + indexPatterns: IIndexPattern; isOrDisabled: boolean; isAndDisabled: boolean; onChange: (arg: OnChangeProps) => void; @@ -64,8 +62,7 @@ export const ExceptionBuilder = ({ listId, listNamespaceType, ruleName, - indexPatternConfig, - isLoading, + indexPatterns, isOrDisabled, isAndDisabled, onChange, @@ -75,9 +72,6 @@ export const ExceptionBuilder = ({ exceptionListItems ); const [exceptionsToDelete, setExceptionsToDelete] = useState([]); - const [{ isLoading: indexPatternLoading, indexPatterns }] = useFetchIndexPatterns( - indexPatternConfig ?? [] - ); const handleCheckAndLogic = (items: ExceptionsBuilderExceptionItem[]): void => { setAndLogicIncluded(items.filter(({ entries }) => entries.length > 1).length > 0); @@ -154,7 +148,7 @@ export const ExceptionBuilder = ({ }, [setExceptions, listType, listId, listNamespaceType, ruleName]); // Filters index pattern fields by exceptionable fields if list type is endpoint - const filterIndexPatterns = useCallback(() => { + const filterIndexPatterns = useMemo((): IIndexPattern => { if (listType === 'endpoint') { return { ...indexPatterns, @@ -196,9 +190,6 @@ export const ExceptionBuilder = ({ return ( - {(isLoading || indexPatternLoading) && ( - - )} {exceptions.map((exceptionListItem, index) => ( @@ -224,8 +215,8 @@ export const ExceptionBuilder = ({ key={getExceptionListItemId(exceptionListItem, index)} exceptionItem={exceptionListItem} exceptionId={getExceptionListItemId(exceptionListItem, index)} - indexPattern={filterIndexPatterns()} - isLoading={indexPatternLoading} + indexPattern={filterIndexPatterns} + isLoading={indexPatterns.fields.length === 0} exceptionItemIndex={index} andLogicIncluded={andLogicIncluded} isOnlyItem={exceptions.length === 1} diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx index 51cc684a01de67..d07a8b5f0d2f6d 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/edit_exception_modal/index.tsx @@ -20,7 +20,6 @@ import { EuiFormRow, EuiText, } from '@elastic/eui'; -import { alertsIndexPattern } from '../../../../../common/endpoint/constants'; import { useFetchIndexPatterns } from '../../../../detections/containers/detection_engine/rules'; import { useSignalIndex } from '../../../../detections/containers/detection_engine/alerts/use_signal_index'; import { @@ -45,6 +44,7 @@ import { Loader } from '../../loader'; interface EditExceptionModalProps { ruleName: string; + ruleIndices: string[]; exceptionItem: ExceptionListItemSchema; exceptionListType: ExceptionListType; onCancel: () => void; @@ -58,10 +58,8 @@ const Modal = styled(EuiModal)` `; const ModalHeader = styled(EuiModalHeader)` - ${({ theme }) => css` - flex-direction: column; - align-items: flex-start; - `} + flex-direction: column; + align-items: flex-start; `; const ModalHeaderSubtitle = styled.div` @@ -82,6 +80,7 @@ const ModalBodySection = styled.section` export const EditExceptionModal = memo(function EditExceptionModal({ ruleName, + ruleIndices, exceptionItem, exceptionListType, onCancel, @@ -96,10 +95,11 @@ export const EditExceptionModal = memo(function EditExceptionModal({ >([]); const { addError, addSuccess } = useAppToasts(); const { loading: isSignalIndexLoading, signalIndexName } = useSignalIndex(); + const [ + { isLoading: isSignalIndexPatternLoading, indexPatterns: signalIndexPatterns }, + ] = useFetchIndexPatterns(signalIndexName !== null ? [signalIndexName] : []); - const [{ isLoading: indexPatternLoading, indexPatterns }] = useFetchIndexPatterns( - signalIndexName !== null ? [signalIndexName] : [] - ); + const [{ isLoading: isIndexPatternLoading, indexPatterns }] = useFetchIndexPatterns(ruleIndices); const onError = useCallback( (error) => { @@ -122,19 +122,19 @@ export const EditExceptionModal = memo(function EditExceptionModal({ ); useEffect(() => { - if (indexPatternLoading === false && isSignalIndexLoading === false) { + if (isSignalIndexPatternLoading === false && isSignalIndexLoading === false) { setShouldDisableBulkClose( entryHasListType(exceptionItemsToAdd) || - entryHasNonEcsType(exceptionItemsToAdd, indexPatterns) || + entryHasNonEcsType(exceptionItemsToAdd, signalIndexPatterns) || exceptionItemsToAdd.length === 0 ); } }, [ setShouldDisableBulkClose, exceptionItemsToAdd, - indexPatternLoading, + isSignalIndexPatternLoading, isSignalIndexLoading, - indexPatterns, + signalIndexPatterns, ]); useEffect(() => { @@ -189,15 +189,8 @@ export const EditExceptionModal = memo(function EditExceptionModal({ } }, [addOrUpdateExceptionItems, enrichExceptionItems, shouldBulkCloseAlert, signalIndexName]); - const indexPatternConfig = useCallback(() => { - if (exceptionListType === 'endpoint') { - return [alertsIndexPattern]; - } - return signalIndexName ? [signalIndexName] : []; - }, [exceptionListType, signalIndexName]); - return ( - + {i18n.EDIT_EXCEPTION_TITLE} @@ -206,11 +199,11 @@ export const EditExceptionModal = memo(function EditExceptionModal({ - {(addExceptionIsLoading || indexPatternLoading || isSignalIndexLoading) && ( + {(addExceptionIsLoading || isIndexPatternLoading || isSignalIndexLoading) && ( )} - {!isSignalIndexLoading && !addExceptionIsLoading && !indexPatternLoading && ( + {!isSignalIndexLoading && !addExceptionIsLoading && !isIndexPatternLoading && ( <> {i18n.EXCEPTION_BUILDER_INFO} @@ -221,13 +214,12 @@ export const EditExceptionModal = memo(function EditExceptionModal({ listId={exceptionItem.list_id} listNamespaceType={exceptionItem.namespace_type} ruleName={ruleName} - isLoading={false} isOrDisabled={false} isAndDisabled={false} data-test-subj="edit-exception-modal-builder" id-aria="edit-exception-modal-builder" onChange={handleBuilderOnChange} - indexPatternConfig={indexPatternConfig()} + indexPatterns={indexPatterns} /> diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx index f72008cbdffe14..986f27f6495ec5 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/index.test.tsx @@ -67,6 +67,7 @@ describe('ExceptionsViewer', () => { ({ eui: euiLightVars, darkMode: false })}> { const wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> { const wrapper = mount( ({ eui: euiLightVars, darkMode: false })}> { + }: UseExceptionListSuccess): void => { dispatch({ type: 'setExceptions', lists: newLists, @@ -253,10 +255,11 @@ const ExceptionsViewerComponent = ({ return ( <> {currentModal === 'editModal' && - exceptionToEdit !== null && - exceptionListTypeToEdit !== null && ( + exceptionToEdit != null && + exceptionListTypeToEdit != null && ( 0 ? { fields: fields.map((field) => - pick(['name', 'searchable', 'type', 'aggregatable'], field) + pick(['name', 'searchable', 'type', 'aggregatable', 'esTypes', 'subType'], field) ), title, } diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx index 71cf5c10de7644..a4ce6c0200eb3c 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/default_config.tsx @@ -12,6 +12,7 @@ import { Dispatch } from 'redux'; import { EuiText } from '@elastic/eui'; import { RowRendererId } from '../../../../common/types/timeline'; +import { DEFAULT_INDEX_PATTERN } from '../../../../common/constants'; import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; import { Filter } from '../../../../../../../src/plugins/data/common/es_query'; import { @@ -38,7 +39,7 @@ import { UpdateTimelineLoading, } from './types'; import { Ecs, TimelineNonEcsData } from '../../../graphql/types'; -import { AddExceptionOnClick } from '../../../common/components/exceptions/add_exception_modal'; +import { AddExceptionModalBaseProps } from '../../../common/components/exceptions/add_exception_modal'; import { getMappedNonEcsValue } from '../../../common/components/exceptions/helpers'; export const buildAlertStatusFilter = (status: Status): Filter[] => [ @@ -225,7 +226,7 @@ interface AlertActionArgs { alertData, ruleName, ruleId, - }: AddExceptionOnClick) => void; + }: AddExceptionModalBaseProps) => void; } export const getAlertActions = ({ @@ -346,10 +347,12 @@ export const getAlertActions = ({ onClick: ({ ecsData, data }: TimelineRowActionOnClick) => { const [ruleName] = getMappedNonEcsValue({ data, fieldName: 'signal.rule.name' }); const [ruleId] = getMappedNonEcsValue({ data, fieldName: 'signal.rule.id' }); + const ruleIndices = getMappedNonEcsValue({ data, fieldName: 'signal.rule.index' }); if (ruleId !== undefined) { openAddExceptionModal({ ruleName: ruleName ?? '', ruleId, + ruleIndices: ruleIndices.length > 0 ? ruleIndices : DEFAULT_INDEX_PATTERN, exceptionListType: 'endpoint', alertData: { ecsData, @@ -369,10 +372,12 @@ export const getAlertActions = ({ onClick: ({ ecsData, data }: TimelineRowActionOnClick) => { const [ruleName] = getMappedNonEcsValue({ data, fieldName: 'signal.rule.name' }); const [ruleId] = getMappedNonEcsValue({ data, fieldName: 'signal.rule.id' }); + const ruleIndices = getMappedNonEcsValue({ data, fieldName: 'signal.rule.index' }); if (ruleId !== undefined) { openAddExceptionModal({ ruleName: ruleName ?? '', ruleId, + ruleIndices: ruleIndices.length > 0 ? ruleIndices : DEFAULT_INDEX_PATTERN, exceptionListType: 'detection', alertData: { ecsData, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index 1d4c97d85443fe..1eda358fe5944e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -54,7 +54,7 @@ import { import { getInvestigateInResolverAction } from '../../../timelines/components/timeline/body/helpers'; import { AddExceptionModal, - AddExceptionOnClick, + AddExceptionModalBaseProps, } from '../../../common/components/exceptions/add_exception_modal'; interface OwnProps { @@ -73,9 +73,10 @@ interface OwnProps { type AlertsTableComponentProps = OwnProps & PropsFromRedux; -const addExceptionModalInitialState: AddExceptionOnClick = { +const addExceptionModalInitialState: AddExceptionModalBaseProps = { ruleName: '', ruleId: '', + ruleIndices: [], exceptionListType: 'detection', alertData: undefined, }; @@ -112,7 +113,7 @@ export const AlertsTableComponent: React.FC = ({ const [showClearSelectionAction, setShowClearSelectionAction] = useState(false); const [filterGroup, setFilterGroup] = useState(FILTER_OPEN); const [shouldShowAddExceptionModal, setShouldShowAddExceptionModal] = useState(false); - const [addExceptionModalState, setAddExceptionModalState] = useState( + const [addExceptionModalState, setAddExceptionModalState] = useState( addExceptionModalInitialState ); const [{ browserFields, indexPatterns }] = useFetchIndexPatterns( @@ -216,12 +217,19 @@ export const AlertsTableComponent: React.FC = ({ ); const openAddExceptionModalCallback = useCallback( - ({ ruleName, ruleId, exceptionListType, alertData }: AddExceptionOnClick) => { + ({ + ruleName, + ruleIndices, + ruleId, + exceptionListType, + alertData, + }: AddExceptionModalBaseProps) => { if (alertData !== null && alertData !== undefined) { setShouldShowAddExceptionModal(true); setAddExceptionModalState({ ruleName, ruleId, + ruleIndices, exceptionListType, alertData, }); @@ -421,12 +429,9 @@ export const AlertsTableComponent: React.FC = ({ closeAddExceptionModal(); }, [closeAddExceptionModal]); - const onAddExceptionConfirm = useCallback( - (didCloseAlert: boolean) => { - closeAddExceptionModal(); - }, - [closeAddExceptionModal] - ); + const onAddExceptionConfirm = useCallback(() => closeAddExceptionModal(), [ + closeAddExceptionModal, + ]); if (loading || isEmpty(signalsIndex)) { return ( @@ -454,6 +459,7 @@ export const AlertsTableComponent: React.FC = ({ = ({ ; format?: Maybe; + /** the elastic type as mapped in the index */ + esTypes?: Maybe; + + subType?: Maybe; } export interface AuthenticationsData { @@ -2780,6 +2788,10 @@ export namespace SourceQuery { aggregatable: boolean; format: Maybe; + + esTypes: Maybe; + + subType: Maybe; }; } diff --git a/x-pack/plugins/security_solution/server/graphql/ecs/resolvers.ts b/x-pack/plugins/security_solution/server/graphql/ecs/resolvers.ts index f30b7d192d05d8..414e5b5d95becd 100644 --- a/x-pack/plugins/security_solution/server/graphql/ecs/resolvers.ts +++ b/x-pack/plugins/security_solution/server/graphql/ecs/resolvers.ts @@ -47,9 +47,41 @@ export const toStringArrayScalar = new GraphQLScalarType({ return null; }, }); - +export const toStringArrayNoNullableScalar = new GraphQLScalarType({ + name: 'StringArray', + description: 'Represents value in detail item from the timeline who wants to more than one type', + serialize(value): string[] | undefined { + if (value == null) { + return undefined; + } else if (Array.isArray(value)) { + return convertArrayToString(value) as string[]; + } else if (isBoolean(value) || isNumber(value) || isObject(value)) { + return [convertToString(value)]; + } + return [value]; + }, + parseValue(value) { + return value; + }, + parseLiteral(ast) { + switch (ast.kind) { + case Kind.INT: + return parseInt(ast.value, 10); + case Kind.FLOAT: + return parseFloat(ast.value); + case Kind.STRING: + return ast.value; + case Kind.LIST: + return ast.values; + case Kind.OBJECT: + return ast.fields; + } + return undefined; + }, +}); export const createScalarToStringArrayValueResolvers = () => ({ ToStringArray: toStringArrayScalar, + ToStringArrayNoNullable: toStringArrayNoNullableScalar, }); const convertToString = (value: object | number | boolean | string): string => { diff --git a/x-pack/plugins/security_solution/server/graphql/ecs/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/ecs/schema.gql.ts index 5b093a02b65143..bdc69f85d35429 100644 --- a/x-pack/plugins/security_solution/server/graphql/ecs/schema.gql.ts +++ b/x-pack/plugins/security_solution/server/graphql/ecs/schema.gql.ts @@ -8,6 +8,7 @@ import gql from 'graphql-tag'; export const ecsSchema = gql` scalar ToStringArray + scalar ToStringArrayNoNullable type EventEcsFields { action: ToStringArray diff --git a/x-pack/plugins/security_solution/server/graphql/source_status/resolvers.ts b/x-pack/plugins/security_solution/server/graphql/source_status/resolvers.ts index 24589822f0250e..8d55e645d67911 100644 --- a/x-pack/plugins/security_solution/server/graphql/source_status/resolvers.ts +++ b/x-pack/plugins/security_solution/server/graphql/source_status/resolvers.ts @@ -4,11 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import { GraphQLScalarType, Kind } from 'graphql'; import { SourceStatusResolvers } from '../../graphql/types'; import { AppResolverOf, ChildResolverOf } from '../../lib/framework'; import { IndexFields } from '../../lib/index_fields'; import { SourceStatus } from '../../lib/source_status'; import { QuerySourceResolver } from '../sources/resolvers'; +import { IFieldSubType } from '../../../../../../src/plugins/data/common/index_patterns/types'; export type SourceStatusIndicesExistResolver = ChildResolverOf< AppResolverOf, @@ -50,3 +52,40 @@ export const createSourceStatusResolvers = (libs: { }, }, }); + +export const toIFieldSubTypeNonNullableScalar = new GraphQLScalarType({ + name: 'IFieldSubType', + description: 'Represents value in index pattern field item', + serialize(value): IFieldSubType | undefined { + if (value == null) { + return undefined; + } + + return { + multi: value.multi ?? undefined, + nested: value.nested ?? undefined, + }; + }, + parseValue(value) { + return value; + }, + parseLiteral(ast) { + switch (ast.kind) { + case Kind.INT: + return undefined; + case Kind.FLOAT: + return undefined; + case Kind.STRING: + return undefined; + case Kind.LIST: + return undefined; + case Kind.OBJECT: + return ast; + } + return undefined; + }, +}); + +export const createScalarToIFieldSubTypeNonNullableScalarResolvers = () => ({ + ToIFieldSubTypeNonNullable: toIFieldSubTypeNonNullableScalar, +}); diff --git a/x-pack/plugins/security_solution/server/graphql/source_status/schema.gql.ts b/x-pack/plugins/security_solution/server/graphql/source_status/schema.gql.ts index e484b60f8f3643..3062113f1b6351 100644 --- a/x-pack/plugins/security_solution/server/graphql/source_status/schema.gql.ts +++ b/x-pack/plugins/security_solution/server/graphql/source_status/schema.gql.ts @@ -7,6 +7,8 @@ import gql from 'graphql-tag'; export const sourceStatusSchema = gql` + scalar ToIFieldSubTypeNonNullable + "A descriptor of a field in an index" type IndexField { "Where the field belong" @@ -26,6 +28,9 @@ export const sourceStatusSchema = gql` "Description of the field" description: String format: String + "the elastic type as mapped in the index" + esTypes: ToStringArrayNoNullable + subType: ToIFieldSubTypeNonNullable } extend type SourceStatus { diff --git a/x-pack/plugins/security_solution/server/graphql/types.ts b/x-pack/plugins/security_solution/server/graphql/types.ts index f8a614e86f28e9..1e397a4e6bb6cb 100644 --- a/x-pack/plugins/security_solution/server/graphql/types.ts +++ b/x-pack/plugins/security_solution/server/graphql/types.ts @@ -430,6 +430,10 @@ export enum FlowDirection { biDirectional = 'biDirectional', } +export type ToStringArrayNoNullable = any; + +export type ToIFieldSubTypeNonNullable = any; + export type ToStringArray = string[] | string; export type Date = string; @@ -629,6 +633,10 @@ export interface IndexField { description?: Maybe; format?: Maybe; + /** the elastic type as mapped in the index */ + esTypes?: Maybe; + + subType?: Maybe; } export interface AuthenticationsData { @@ -3579,6 +3587,10 @@ export namespace IndexFieldResolvers { description?: DescriptionResolver, TypeParent, TContext>; format?: FormatResolver, TypeParent, TContext>; + /** the elastic type as mapped in the index */ + esTypes?: EsTypesResolver, TypeParent, TContext>; + + subType?: SubTypeResolver, TypeParent, TContext>; } export type CategoryResolver = Resolver< @@ -3626,6 +3638,16 @@ export namespace IndexFieldResolvers { Parent = IndexField, TContext = SiemContext > = Resolver; + export type EsTypesResolver< + R = Maybe, + Parent = IndexField, + TContext = SiemContext + > = Resolver; + export type SubTypeResolver< + R = Maybe, + Parent = IndexField, + TContext = SiemContext + > = Resolver; } export namespace AuthenticationsDataResolvers { @@ -9317,6 +9339,14 @@ export interface DeprecatedDirectiveArgs { reason?: string; } +export interface ToStringArrayNoNullableScalarConfig + extends GraphQLScalarTypeConfig { + name: 'ToStringArrayNoNullable'; +} +export interface ToIFieldSubTypeNonNullableScalarConfig + extends GraphQLScalarTypeConfig { + name: 'ToIFieldSubTypeNonNullable'; +} export interface ToStringArrayScalarConfig extends GraphQLScalarTypeConfig { name: 'ToStringArray'; } @@ -9490,6 +9520,8 @@ export type IResolvers = { EventsTimelineData?: EventsTimelineDataResolvers.Resolvers; OsFields?: OsFieldsResolvers.Resolvers; HostFields?: HostFieldsResolvers.Resolvers; + ToStringArrayNoNullable?: GraphQLScalarType; + ToIFieldSubTypeNonNullable?: GraphQLScalarType; ToStringArray?: GraphQLScalarType; Date?: GraphQLScalarType; ToNumberArray?: GraphQLScalarType; diff --git a/x-pack/plugins/security_solution/server/lib/index_fields/types.ts b/x-pack/plugins/security_solution/server/lib/index_fields/types.ts index 0c894c6980a31e..67b3c254007e24 100644 --- a/x-pack/plugins/security_solution/server/lib/index_fields/types.ts +++ b/x-pack/plugins/security_solution/server/lib/index_fields/types.ts @@ -6,6 +6,7 @@ import { IndexField } from '../../graphql/types'; import { FrameworkRequest } from '../framework'; +import { IFieldSubType } from '../../../../../../src/plugins/data/common'; export interface FieldsAdapter { getIndexFields(req: FrameworkRequest, indices: string[]): Promise; @@ -16,4 +17,6 @@ export interface IndexFieldDescriptor { type: string; searchable: boolean; aggregatable: boolean; + esTypes?: string[]; + subType?: IFieldSubType; } From 3760dc4f45c466f4379346bf7d6a20c8dfdd2080 Mon Sep 17 00:00:00 2001 From: Quynh Nguyen <43350163+qn895@users.noreply.github.com> Date: Mon, 20 Jul 2020 19:34:53 -0500 Subject: [PATCH 012/101] [ML] Fix annotation detector linking & delayed_data(0) (#72468) --- .../annotations_table/annotations_table.js | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js index 86398a57c3a456..69f7635a660321 100644 --- a/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js +++ b/x-pack/plugins/ml/public/application/components/annotations/annotations_table/annotations_table.js @@ -256,7 +256,7 @@ export class AnnotationsTable extends Component { // if the annotation is at the series level // then pass the partitioning field(s) and detector index to the Single Metric Viewer if (_.has(annotation, 'detector_index')) { - mlTimeSeriesExplorer.detector_index = annotation.detector_index; + mlTimeSeriesExplorer.detectorIndex = annotation.detector_index; } if (_.has(annotation, 'partition_field_value')) { entityCondition[annotation.partition_field_name] = annotation.partition_field_value; @@ -523,10 +523,26 @@ export class AnnotationsTable extends Component { const aggregations = this.props.aggregations ?? this.state.aggregations; if (aggregations) { const buckets = aggregations.event.buckets; - const foundUser = buckets.findIndex((d) => d.key === ANNOTATION_EVENT_USER) > -1; - filterOptions = foundUser - ? buckets - : [{ key: ANNOTATION_EVENT_USER, doc_count: 0 }, ...buckets]; + let foundUser = false; + let foundDelayedData = false; + + buckets.forEach((bucket) => { + if (bucket.key === ANNOTATION_EVENT_USER) { + foundUser = true; + } + if (bucket.key === ANNOTATION_EVENT_DELAYED_DATA) { + foundDelayedData = true; + } + }); + const adjustedBuckets = []; + if (!foundUser) { + adjustedBuckets.push({ key: ANNOTATION_EVENT_USER, doc_count: 0 }); + } + if (!foundDelayedData) { + adjustedBuckets.push({ key: ANNOTATION_EVENT_DELAYED_DATA, doc_count: 0 }); + } + + filterOptions = [...adjustedBuckets, ...buckets]; } const filters = [ { From 990d4c5eec01bc66b434c9d224e7a0965a4c4770 Mon Sep 17 00:00:00 2001 From: Brent Kimmel Date: Mon, 20 Jul 2020 20:59:55 -0400 Subject: [PATCH 013/101] [Security_Solution][Resolver][Bug]: Restore breadcrumb background (#72538) --- .../public/resolver/view/panels/panel_content_utilities.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx index 4dedafe55bb2ce..55b5be21fb4a45 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/panels/panel_content_utilities.tsx @@ -32,7 +32,7 @@ export interface CrumbInfo { } const ThemedBreadcrumbs = styled(EuiBreadcrumbs)<{ background: string; text: string }>` - &.euiBreadcrumbs.euiBreadcrumbs--responsive { + &.euiBreadcrumbs { background-color: ${(props) => props.background}; color: ${(props) => props.text}; padding: 1em; From bfbb8d213816d7d28fbbb121dc58b52d1bb42e0d Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall Date: Mon, 20 Jul 2020 22:18:20 -0400 Subject: [PATCH 014/101] [pre-req] Move .storybook to storybook; standardize files (#72384) --- .eslintignore | 2 +- .eslintrc.js | 6 + src/dev/precommit_hook/casing_check_config.js | 2 +- x-pack/plugins/canvas/.gitignore | 2 +- ...shot => advanced_filter.stories.storyshot} | 0 ...amples.tsx => advanced_filter.stories.tsx} | 0 ...shot => dropdown_filter.stories.storyshot} | 0 ...amples.tsx => dropdown_filter.stories.tsx} | 0 .../time_filter.examples.storyshot | 134 ----- ...ples.storyshot => asset.stories.storyshot} | 0 .../asset_manager.examples.storyshot | 37 -- .../{asset.examples.tsx => asset.stories.tsx} | 0 .../__examples__/color_picker.stories.tsx | 3 +- ...=> custom_element_modal.stories.storyshot} | 0 ...s.tsx => custom_element_modal.stories.tsx} | 0 ...ples.storyshot => debug.stories.storyshot} | 0 .../{debug.examples.tsx => debug.stories.tsx} | 0 .../element_controls.examples.storyshot | 98 ---- ...hot => expression_input.stories.storyshot} | 0 ...mples.tsx => expression_input.stories.tsx} | 0 ....storyshot => edit_menu.stories.storyshot} | 0 ...enu.examples.tsx => edit_menu.stories.tsx} | 0 ...oryshot => element_menu.stories.storyshot} | 0 ....examples.tsx => element_menu.stories.tsx} | 0 ...storyshot => share_menu.stories.storyshot} | 0 ...nu.examples.tsx => share_menu.stories.tsx} | 0 ...ot => extended_template.stories.storyshot} | 0 ...shot => simple_template.stories.storyshot} | 0 ...ples.tsx => extended_template.stories.tsx} | 0 ...amples.tsx => simple_template.stories.tsx} | 0 ...ot => extended_template.stories.storyshot} | 0 ...shot => simple_template.stories.storyshot} | 0 ...ples.tsx => extended_template.stories.tsx} | 3 +- ...amples.tsx => simple_template.stories.tsx} | 0 x-pack/plugins/canvas/scripts/jest.js | 2 +- x-pack/plugins/canvas/scripts/storybook.js | 6 +- ...les.storyshot => canvas.stories.storyshot} | 0 ...mples.storyshot => page.stories.storyshot} | 0 ...hot => rendered_element.stories.storyshot} | 0 ...canvas.examples.tsx => canvas.stories.tsx} | 0 .../{page.examples.tsx => page.stories.tsx} | 0 ...mples.tsx => rendered_element.stories.tsx} | 0 .../footer.components.examples.storyshot | 140 ----- ...les.storyshot => footer.stories.storyshot} | 0 ...ryshot => page_controls.stories.storyshot} | 0 ...s.storyshot => scrubber.stories.storyshot} | 0 ...ples.storyshot => title.stories.storyshot} | 0 ...footer.examples.tsx => footer.stories.tsx} | 0 ...examples.tsx => page_controls.stories.tsx} | 0 ...bber.examples.tsx => scrubber.stories.tsx} | 0 .../{title.examples.tsx => title.stories.tsx} | 0 ...ot => autoplay_settings.stories.storyshot} | 0 .../settings.components.examples.storyshot | 479 ------------------ ...s.storyshot => settings.stories.storyshot} | 0 ...hot => toolbar_settings.stories.storyshot} | 0 ...ples.tsx => autoplay_settings.stories.tsx} | 0 ...ings.examples.tsx => settings.stories.tsx} | 0 ...mples.tsx => toolbar_settings.stories.tsx} | 0 .../canvas/{.storybook => storybook}/.babelrc | 0 .../{.storybook => storybook}/addons.js | 0 .../{.storybook => storybook}/config.js | 25 +- .../{.storybook => storybook}/constants.js | 0 .../canvas/storybook/decorators/index.ts | 19 + .../storybook/decorators/kibana_decorator.tsx | 26 + .../storybook/decorators/router_decorator.tsx | 30 ++ .../{.storybook => storybook}/dll_contexts.js | 6 +- .../{.storybook => storybook}/middleware.js | 9 +- .../preview-head.html | 0 .../storyshots.test.js | 14 +- .../webpack.config.js | 29 +- .../webpack.dll.config.js | 2 +- 71 files changed, 136 insertions(+), 938 deletions(-) rename x-pack/plugins/canvas/canvas_plugin_src/renderers/advanced_filter/component/__examples__/__snapshots__/{advanced_filter.examples.storyshot => advanced_filter.stories.storyshot} (100%) rename x-pack/plugins/canvas/canvas_plugin_src/renderers/advanced_filter/component/__examples__/{advanced_filter.examples.tsx => advanced_filter.stories.tsx} (100%) rename x-pack/plugins/canvas/canvas_plugin_src/renderers/dropdown_filter/component/__examples__/__snapshots__/{dropdown_filter.examples.storyshot => dropdown_filter.stories.storyshot} (100%) rename x-pack/plugins/canvas/canvas_plugin_src/renderers/dropdown_filter/component/__examples__/{dropdown_filter.examples.tsx => dropdown_filter.stories.tsx} (100%) delete mode 100644 x-pack/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/__examples__/__snapshots__/time_filter.examples.storyshot rename x-pack/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/{asset.examples.storyshot => asset.stories.storyshot} (100%) delete mode 100644 x-pack/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset_manager.examples.storyshot rename x-pack/plugins/canvas/public/components/asset_manager/__examples__/{asset.examples.tsx => asset.stories.tsx} (100%) rename x-pack/plugins/canvas/public/components/custom_element_modal/__examples__/__snapshots__/{custom_element_modal.examples.storyshot => custom_element_modal.stories.storyshot} (100%) rename x-pack/plugins/canvas/public/components/custom_element_modal/__examples__/{custom_element_modal.examples.tsx => custom_element_modal.stories.tsx} (100%) rename x-pack/plugins/canvas/public/components/debug/__examples__/__snapshots__/{debug.examples.storyshot => debug.stories.storyshot} (100%) rename x-pack/plugins/canvas/public/components/debug/__examples__/{debug.examples.tsx => debug.stories.tsx} (100%) delete mode 100644 x-pack/plugins/canvas/public/components/element_card/__examples__/__snapshots__/element_controls.examples.storyshot rename x-pack/plugins/canvas/public/components/expression_input/__examples__/__snapshots__/{expression_input.examples.storyshot => expression_input.stories.storyshot} (100%) rename x-pack/plugins/canvas/public/components/expression_input/__examples__/{expression_input.examples.tsx => expression_input.stories.tsx} (100%) rename x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/__snapshots__/{edit_menu.examples.storyshot => edit_menu.stories.storyshot} (100%) rename x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/{edit_menu.examples.tsx => edit_menu.stories.tsx} (100%) rename x-pack/plugins/canvas/public/components/workpad_header/element_menu/__examples__/__snapshots__/{element_menu.examples.storyshot => element_menu.stories.storyshot} (100%) rename x-pack/plugins/canvas/public/components/workpad_header/element_menu/__examples__/{element_menu.examples.tsx => element_menu.stories.tsx} (100%) rename x-pack/plugins/canvas/public/components/workpad_header/share_menu/__examples__/__snapshots__/{share_menu.examples.storyshot => share_menu.stories.storyshot} (100%) rename x-pack/plugins/canvas/public/components/workpad_header/share_menu/__examples__/{share_menu.examples.tsx => share_menu.stories.tsx} (100%) rename x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/__snapshots__/{extended_template.examples.storyshot => extended_template.stories.storyshot} (100%) rename x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/__snapshots__/{simple_template.examples.storyshot => simple_template.stories.storyshot} (100%) rename x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/{extended_template.examples.tsx => extended_template.stories.tsx} (100%) rename x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/{simple_template.examples.tsx => simple_template.stories.tsx} (100%) rename x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/{extended_template.examples.storyshot => extended_template.stories.storyshot} (100%) rename x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/{simple_template.examples.storyshot => simple_template.stories.storyshot} (100%) rename x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/{extended_template.examples.tsx => extended_template.stories.tsx} (95%) rename x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/{simple_template.examples.tsx => simple_template.stories.tsx} (100%) rename x-pack/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/{canvas.examples.storyshot => canvas.stories.storyshot} (100%) rename x-pack/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/{page.examples.storyshot => page.stories.storyshot} (100%) rename x-pack/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/{rendered_element.examples.storyshot => rendered_element.stories.storyshot} (100%) rename x-pack/plugins/canvas/shareable_runtime/components/__examples__/{canvas.examples.tsx => canvas.stories.tsx} (100%) rename x-pack/plugins/canvas/shareable_runtime/components/__examples__/{page.examples.tsx => page.stories.tsx} (100%) rename x-pack/plugins/canvas/shareable_runtime/components/__examples__/{rendered_element.examples.tsx => rendered_element.stories.tsx} (100%) delete mode 100644 x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/footer.components.examples.storyshot rename x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/{footer.examples.storyshot => footer.stories.storyshot} (100%) rename x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/{page_controls.examples.storyshot => page_controls.stories.storyshot} (100%) rename x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/{scrubber.examples.storyshot => scrubber.stories.storyshot} (100%) rename x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/{title.examples.storyshot => title.stories.storyshot} (100%) rename x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/{footer.examples.tsx => footer.stories.tsx} (100%) rename x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/{page_controls.examples.tsx => page_controls.stories.tsx} (100%) rename x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/{scrubber.examples.tsx => scrubber.stories.tsx} (100%) rename x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/{title.examples.tsx => title.stories.tsx} (100%) rename x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/{autoplay_settings.examples.storyshot => autoplay_settings.stories.storyshot} (100%) delete mode 100644 x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/settings.components.examples.storyshot rename x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/{settings.examples.storyshot => settings.stories.storyshot} (100%) rename x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/{toolbar_settings.examples.storyshot => toolbar_settings.stories.storyshot} (100%) rename x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/{autoplay_settings.examples.tsx => autoplay_settings.stories.tsx} (100%) rename x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/{settings.examples.tsx => settings.stories.tsx} (100%) rename x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/{toolbar_settings.examples.tsx => toolbar_settings.stories.tsx} (100%) rename x-pack/plugins/canvas/{.storybook => storybook}/.babelrc (100%) rename x-pack/plugins/canvas/{.storybook => storybook}/addons.js (100%) rename x-pack/plugins/canvas/{.storybook => storybook}/config.js (67%) rename x-pack/plugins/canvas/{.storybook => storybook}/constants.js (100%) create mode 100644 x-pack/plugins/canvas/storybook/decorators/index.ts create mode 100644 x-pack/plugins/canvas/storybook/decorators/kibana_decorator.tsx create mode 100644 x-pack/plugins/canvas/storybook/decorators/router_decorator.tsx rename x-pack/plugins/canvas/{.storybook => storybook}/dll_contexts.js (88%) rename x-pack/plugins/canvas/{.storybook => storybook}/middleware.js (72%) rename x-pack/plugins/canvas/{.storybook => storybook}/preview-head.html (100%) rename x-pack/plugins/canvas/{.storybook => storybook}/storyshots.test.js (95%) rename x-pack/plugins/canvas/{.storybook => storybook}/webpack.config.js (88%) rename x-pack/plugins/canvas/{.storybook => storybook}/webpack.dll.config.js (100%) diff --git a/.eslintignore b/.eslintignore index d983c4bedfaab4..9263b483b8de98 100644 --- a/.eslintignore +++ b/.eslintignore @@ -33,7 +33,7 @@ target /x-pack/plugins/canvas/canvas_plugin /x-pack/plugins/canvas/canvas_plugin_src/lib/flot-charts /x-pack/plugins/canvas/shareable_runtime/build -/x-pack/plugins/canvas/storybook +/x-pack/plugins/canvas/storybook/build /x-pack/plugins/monitoring/public/lib/jquery_flot /x-pack/plugins/reporting/server/export_types/printable_pdf/server/lib/pdf/assets/** /x-pack/legacy/plugins/infra/common/graphql/types.ts diff --git a/.eslintrc.js b/.eslintrc.js index a9ffe2850aa726..e2674e8d7b4073 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1222,6 +1222,12 @@ module.exports = { ], }, }, + { + files: ['x-pack/plugins/canvas/storybook/**'], + rules: { + 'import/no-extraneous-dependencies': 0, + }, + }, { files: ['x-pack/plugins/canvas/canvas_plugin_src/**/*.js'], globals: { canvas: true, $: true }, diff --git a/src/dev/precommit_hook/casing_check_config.js b/src/dev/precommit_hook/casing_check_config.js index 929de8c6701d4a..1e4f048be8ea4e 100644 --- a/src/dev/precommit_hook/casing_check_config.js +++ b/src/dev/precommit_hook/casing_check_config.js @@ -51,7 +51,7 @@ export const IGNORE_FILE_GLOBS = [ '.ci/pipeline-library/**/*', // Files in this directory must match a pre-determined name in some cases. - 'x-pack/plugins/canvas/.storybook/*', + 'x-pack/plugins/canvas/storybook/*', // filename must match language code which requires capital letters '**/translations/*.json', diff --git a/x-pack/plugins/canvas/.gitignore b/x-pack/plugins/canvas/.gitignore index 1c6258670c59cb..d47bd8acf6be20 100644 --- a/x-pack/plugins/canvas/.gitignore +++ b/x-pack/plugins/canvas/.gitignore @@ -57,4 +57,4 @@ canvas_plugin/* webpack_stats.json # Don't commit storybook builds -storybook +storybook/build diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/advanced_filter/component/__examples__/__snapshots__/advanced_filter.examples.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/advanced_filter/component/__examples__/__snapshots__/advanced_filter.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/advanced_filter/component/__examples__/__snapshots__/advanced_filter.examples.storyshot rename to x-pack/plugins/canvas/canvas_plugin_src/renderers/advanced_filter/component/__examples__/__snapshots__/advanced_filter.stories.storyshot diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/advanced_filter/component/__examples__/advanced_filter.examples.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/advanced_filter/component/__examples__/advanced_filter.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/advanced_filter/component/__examples__/advanced_filter.examples.tsx rename to x-pack/plugins/canvas/canvas_plugin_src/renderers/advanced_filter/component/__examples__/advanced_filter.stories.tsx diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/dropdown_filter/component/__examples__/__snapshots__/dropdown_filter.examples.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/dropdown_filter/component/__examples__/__snapshots__/dropdown_filter.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/dropdown_filter/component/__examples__/__snapshots__/dropdown_filter.examples.storyshot rename to x-pack/plugins/canvas/canvas_plugin_src/renderers/dropdown_filter/component/__examples__/__snapshots__/dropdown_filter.stories.storyshot diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/dropdown_filter/component/__examples__/dropdown_filter.examples.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/dropdown_filter/component/__examples__/dropdown_filter.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/renderers/dropdown_filter/component/__examples__/dropdown_filter.examples.tsx rename to x-pack/plugins/canvas/canvas_plugin_src/renderers/dropdown_filter/component/__examples__/dropdown_filter.stories.tsx diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/__examples__/__snapshots__/time_filter.examples.storyshot b/x-pack/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/__examples__/__snapshots__/time_filter.examples.storyshot deleted file mode 100644 index d555fbbe0ce92f..00000000000000 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/time_filter/components/__examples__/__snapshots__/time_filter.examples.storyshot +++ /dev/null @@ -1,134 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Storyshots renderers/TimeFilter default 1`] = ` -
-
-
-
-
-
- -
-
-
-
-
-
- -
-
-
-
- → -
-
-
-
- -
-
-
-
-
-
-
-
-`; diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset.examples.storyshot b/x-pack/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset.examples.storyshot rename to x-pack/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset_manager.examples.storyshot b/x-pack/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset_manager.examples.storyshot deleted file mode 100644 index 9f91668e602a4b..00000000000000 --- a/x-pack/plugins/canvas/public/components/asset_manager/__examples__/__snapshots__/asset_manager.examples.storyshot +++ /dev/null @@ -1,37 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Storyshots components/Assets/AssetManager no assets 1`] = ` - -`; - -exports[`Storyshots components/Assets/AssetManager two assets 1`] = ` - -`; diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__examples__/asset.examples.tsx b/x-pack/plugins/canvas/public/components/asset_manager/__examples__/asset.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/asset_manager/__examples__/asset.examples.tsx rename to x-pack/plugins/canvas/public/components/asset_manager/__examples__/asset.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/color_picker/__examples__/color_picker.stories.tsx b/x-pack/plugins/canvas/public/components/color_picker/__examples__/color_picker.stories.tsx index 0a7ed75ee728ef..c5d42be2d201e2 100644 --- a/x-pack/plugins/canvas/public/components/color_picker/__examples__/color_picker.stories.tsx +++ b/x-pack/plugins/canvas/public/components/color_picker/__examples__/color_picker.stories.tsx @@ -5,7 +5,7 @@ */ import { action } from '@storybook/addon-actions'; -import { boolean, withKnobs } from '@storybook/addon-knobs'; +import { boolean } from '@storybook/addon-knobs'; import { storiesOf } from '@storybook/react'; import React from 'react'; import { ColorPicker } from '../color_picker'; @@ -54,7 +54,6 @@ class Interactive extends React.Component< } storiesOf('components/Color/ColorPicker', module) - .addDecorator(withKnobs) .addParameters({ info: { inline: true, diff --git a/x-pack/plugins/canvas/public/components/custom_element_modal/__examples__/__snapshots__/custom_element_modal.examples.storyshot b/x-pack/plugins/canvas/public/components/custom_element_modal/__examples__/__snapshots__/custom_element_modal.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/custom_element_modal/__examples__/__snapshots__/custom_element_modal.examples.storyshot rename to x-pack/plugins/canvas/public/components/custom_element_modal/__examples__/__snapshots__/custom_element_modal.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/custom_element_modal/__examples__/custom_element_modal.examples.tsx b/x-pack/plugins/canvas/public/components/custom_element_modal/__examples__/custom_element_modal.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/custom_element_modal/__examples__/custom_element_modal.examples.tsx rename to x-pack/plugins/canvas/public/components/custom_element_modal/__examples__/custom_element_modal.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/debug/__examples__/__snapshots__/debug.examples.storyshot b/x-pack/plugins/canvas/public/components/debug/__examples__/__snapshots__/debug.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/debug/__examples__/__snapshots__/debug.examples.storyshot rename to x-pack/plugins/canvas/public/components/debug/__examples__/__snapshots__/debug.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/debug/__examples__/debug.examples.tsx b/x-pack/plugins/canvas/public/components/debug/__examples__/debug.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/debug/__examples__/debug.examples.tsx rename to x-pack/plugins/canvas/public/components/debug/__examples__/debug.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/element_card/__examples__/__snapshots__/element_controls.examples.storyshot b/x-pack/plugins/canvas/public/components/element_card/__examples__/__snapshots__/element_controls.examples.storyshot deleted file mode 100644 index 5e076ba76a9c1b..00000000000000 --- a/x-pack/plugins/canvas/public/components/element_card/__examples__/__snapshots__/element_controls.examples.storyshot +++ /dev/null @@ -1,98 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Storyshots components/ElementTypes/ElementControls has two buttons 1`] = ` -
-
-
- - - -
-
- - - -
-
-
-`; diff --git a/x-pack/plugins/canvas/public/components/expression_input/__examples__/__snapshots__/expression_input.examples.storyshot b/x-pack/plugins/canvas/public/components/expression_input/__examples__/__snapshots__/expression_input.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/expression_input/__examples__/__snapshots__/expression_input.examples.storyshot rename to x-pack/plugins/canvas/public/components/expression_input/__examples__/__snapshots__/expression_input.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/expression_input/__examples__/expression_input.examples.tsx b/x-pack/plugins/canvas/public/components/expression_input/__examples__/expression_input.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/expression_input/__examples__/expression_input.examples.tsx rename to x-pack/plugins/canvas/public/components/expression_input/__examples__/expression_input.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/__snapshots__/edit_menu.examples.storyshot b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/__snapshots__/edit_menu.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/__snapshots__/edit_menu.examples.storyshot rename to x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/__snapshots__/edit_menu.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/edit_menu.examples.tsx b/x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/edit_menu.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/edit_menu.examples.tsx rename to x-pack/plugins/canvas/public/components/workpad_header/edit_menu/__examples__/edit_menu.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__examples__/__snapshots__/element_menu.examples.storyshot b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__examples__/__snapshots__/element_menu.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_header/element_menu/__examples__/__snapshots__/element_menu.examples.storyshot rename to x-pack/plugins/canvas/public/components/workpad_header/element_menu/__examples__/__snapshots__/element_menu.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__examples__/element_menu.examples.tsx b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__examples__/element_menu.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_header/element_menu/__examples__/element_menu.examples.tsx rename to x-pack/plugins/canvas/public/components/workpad_header/element_menu/__examples__/element_menu.stories.tsx diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__examples__/__snapshots__/share_menu.examples.storyshot b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__examples__/__snapshots__/share_menu.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_header/share_menu/__examples__/__snapshots__/share_menu.examples.storyshot rename to x-pack/plugins/canvas/public/components/workpad_header/share_menu/__examples__/__snapshots__/share_menu.stories.storyshot diff --git a/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__examples__/share_menu.examples.tsx b/x-pack/plugins/canvas/public/components/workpad_header/share_menu/__examples__/share_menu.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/components/workpad_header/share_menu/__examples__/share_menu.examples.tsx rename to x-pack/plugins/canvas/public/components/workpad_header/share_menu/__examples__/share_menu.stories.tsx diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/__snapshots__/extended_template.examples.storyshot b/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/__snapshots__/extended_template.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/__snapshots__/extended_template.examples.storyshot rename to x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/__snapshots__/extended_template.stories.storyshot diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/__snapshots__/simple_template.examples.storyshot b/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/__snapshots__/simple_template.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/__snapshots__/simple_template.examples.storyshot rename to x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/__snapshots__/simple_template.stories.storyshot diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/extended_template.examples.tsx b/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/extended_template.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/extended_template.examples.tsx rename to x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/extended_template.stories.tsx diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/simple_template.examples.tsx b/x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/simple_template.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/simple_template.examples.tsx rename to x-pack/plugins/canvas/public/expression_types/arg_types/container_style/__examples__/simple_template.stories.tsx diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/extended_template.examples.storyshot b/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/extended_template.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/extended_template.examples.storyshot rename to x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/extended_template.stories.storyshot diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/simple_template.examples.storyshot b/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/simple_template.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/simple_template.examples.storyshot rename to x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/__snapshots__/simple_template.stories.storyshot diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/extended_template.examples.tsx b/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/extended_template.stories.tsx similarity index 95% rename from x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/extended_template.examples.tsx rename to x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/extended_template.stories.tsx index 4a300b3de8923d..a831f35ad597e0 100644 --- a/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/extended_template.examples.tsx +++ b/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/extended_template.stories.tsx @@ -6,7 +6,7 @@ import { action } from '@storybook/addon-actions'; import { storiesOf } from '@storybook/react'; -import { withKnobs, array, radios, boolean } from '@storybook/addon-knobs'; +import { array, radios, boolean } from '@storybook/addon-knobs'; import React from 'react'; import { ExtendedTemplate } from '../extended_template'; @@ -64,7 +64,6 @@ storiesOf('arguments/SeriesStyle', module) .addDecorator((story) => (
{story()}
)) - .addDecorator(withKnobs) .add('extended', () => ); storiesOf('arguments/SeriesStyle/components', module) diff --git a/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/simple_template.examples.tsx b/x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/simple_template.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/simple_template.examples.tsx rename to x-pack/plugins/canvas/public/expression_types/arg_types/series_style/__examples__/simple_template.stories.tsx diff --git a/x-pack/plugins/canvas/scripts/jest.js b/x-pack/plugins/canvas/scripts/jest.js index b30cb02d2c99ae..a91431a0141c54 100644 --- a/x-pack/plugins/canvas/scripts/jest.js +++ b/x-pack/plugins/canvas/scripts/jest.js @@ -60,7 +60,7 @@ run( if (all) { log.info('Running all available tests. This will take a while...'); } else if (storybook) { - path = 'plugins/canvas/.storybook'; + path = 'plugins/canvas/storybook'; log.info('Running Storybook Snapshot tests...'); } else { log.info('Running tests. This does not include Storybook Snapshots...'); diff --git a/x-pack/plugins/canvas/scripts/storybook.js b/x-pack/plugins/canvas/scripts/storybook.js index c9e6c6c65436c2..beea1814b54d25 100644 --- a/x-pack/plugins/canvas/scripts/storybook.js +++ b/x-pack/plugins/canvas/scripts/storybook.js @@ -10,7 +10,7 @@ const del = require('del'); const { run } = require('@kbn/dev-utils'); const storybook = require('@storybook/react/standalone'); const execa = require('execa'); -const { DLL_OUTPUT } = require('./../.storybook/constants'); +const { DLL_OUTPUT } = require('./../storybook/constants'); const options = { stdio: ['ignore', 'inherit', 'inherit'], @@ -18,7 +18,7 @@ const options = { }; const storybookOptions = { - configDir: path.resolve(__dirname, './../.storybook'), + configDir: path.resolve(__dirname, './../storybook'), mode: 'dev', }; @@ -51,7 +51,7 @@ run( [ 'webpack', '--config', - 'x-pack/plugins/canvas/.storybook/webpack.dll.config.js', + 'x-pack/plugins/canvas/storybook/webpack.dll.config.js', '--progress', '--hide-modules', '--display-entrypoints', diff --git a/x-pack/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/canvas.examples.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/canvas.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/canvas.examples.storyshot rename to x-pack/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/canvas.stories.storyshot diff --git a/x-pack/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/page.examples.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/page.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/page.examples.storyshot rename to x-pack/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/page.stories.storyshot diff --git a/x-pack/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/rendered_element.examples.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/rendered_element.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/rendered_element.examples.storyshot rename to x-pack/plugins/canvas/shareable_runtime/components/__examples__/__snapshots__/rendered_element.stories.storyshot diff --git a/x-pack/plugins/canvas/shareable_runtime/components/__examples__/canvas.examples.tsx b/x-pack/plugins/canvas/shareable_runtime/components/__examples__/canvas.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/__examples__/canvas.examples.tsx rename to x-pack/plugins/canvas/shareable_runtime/components/__examples__/canvas.stories.tsx diff --git a/x-pack/plugins/canvas/shareable_runtime/components/__examples__/page.examples.tsx b/x-pack/plugins/canvas/shareable_runtime/components/__examples__/page.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/__examples__/page.examples.tsx rename to x-pack/plugins/canvas/shareable_runtime/components/__examples__/page.stories.tsx diff --git a/x-pack/plugins/canvas/shareable_runtime/components/__examples__/rendered_element.examples.tsx b/x-pack/plugins/canvas/shareable_runtime/components/__examples__/rendered_element.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/__examples__/rendered_element.examples.tsx rename to x-pack/plugins/canvas/shareable_runtime/components/__examples__/rendered_element.stories.tsx diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/footer.components.examples.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/footer.components.examples.storyshot deleted file mode 100644 index 6d783a26d84242..00000000000000 --- a/x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/footer.components.examples.storyshot +++ /dev/null @@ -1,140 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Storyshots shareables/Footer/components PageControls 1`] = ` -
-
- -
-
- -
-
- -
-
-`; - -exports[`Storyshots shareables/Footer/components Title 1`] = ` -
-
-
- -
-
-
-
-
- This is a test title. -
-
-
-
-
-
-`; diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/footer.examples.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/footer.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/footer.examples.storyshot rename to x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/footer.stories.storyshot diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/page_controls.examples.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/page_controls.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/page_controls.examples.storyshot rename to x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/page_controls.stories.storyshot diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/scrubber.examples.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/scrubber.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/scrubber.examples.storyshot rename to x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/scrubber.stories.storyshot diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/title.examples.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/title.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/title.examples.storyshot rename to x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/__snapshots__/title.stories.storyshot diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/footer.examples.tsx b/x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/footer.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/footer.examples.tsx rename to x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/footer.stories.tsx diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/page_controls.examples.tsx b/x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/page_controls.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/page_controls.examples.tsx rename to x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/page_controls.stories.tsx diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/scrubber.examples.tsx b/x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/scrubber.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/scrubber.examples.tsx rename to x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/scrubber.stories.tsx diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/title.examples.tsx b/x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/title.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/title.examples.tsx rename to x-pack/plugins/canvas/shareable_runtime/components/footer/__examples__/title.stories.tsx diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/autoplay_settings.examples.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/autoplay_settings.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/autoplay_settings.examples.storyshot rename to x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/autoplay_settings.stories.storyshot diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/settings.components.examples.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/settings.components.examples.storyshot deleted file mode 100644 index 1922bd84b174c6..00000000000000 --- a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/settings.components.examples.storyshot +++ /dev/null @@ -1,479 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Storyshots shareables/Settings/components AutoplaySettings, autoplay disabled 1`] = ` -
-
- - - - - - - - - -
-
-
-
-
-
-
- -
-
-
-
- -
-
-
- Use shorthand notation, like 30s, 10m, or 1h -
-
-
-
-
-
-
- -
-
- -
-
-
-
-
-
-`; - -exports[`Storyshots shareables/Settings/components AutoplaySettings, autoplay enabled 1`] = ` -
-
- - - - - - - - - -
-
-
-
-
-
-
- -
-
-
-
- -
-
-
- Use shorthand notation, like 30s, 10m, or 1h -
-
-
-
-
-
-
- -
-
- -
-
-
-
-
-
-`; - -exports[`Storyshots shareables/Settings/components ToolbarSettings, autohide disabled 1`] = ` -
-
-
-
- - - - - - - - - -
-
- Hide the toolbar when the mouse is not within the Canvas? -
-
-
-
-`; - -exports[`Storyshots shareables/Settings/components ToolbarSettings, autohide enabled 1`] = ` -
-
-
-
- - - - - - - - - -
-
- Hide the toolbar when the mouse is not within the Canvas? -
-
-
-
-`; diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/settings.examples.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/settings.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/settings.examples.storyshot rename to x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/settings.stories.storyshot diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/toolbar_settings.examples.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/toolbar_settings.stories.storyshot similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/toolbar_settings.examples.storyshot rename to x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/__snapshots__/toolbar_settings.stories.storyshot diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/autoplay_settings.examples.tsx b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/autoplay_settings.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/autoplay_settings.examples.tsx rename to x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/autoplay_settings.stories.tsx diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/settings.examples.tsx b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/settings.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/settings.examples.tsx rename to x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/settings.stories.tsx diff --git a/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/toolbar_settings.examples.tsx b/x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/toolbar_settings.stories.tsx similarity index 100% rename from x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/toolbar_settings.examples.tsx rename to x-pack/plugins/canvas/shareable_runtime/components/footer/settings/__examples__/toolbar_settings.stories.tsx diff --git a/x-pack/plugins/canvas/.storybook/.babelrc b/x-pack/plugins/canvas/storybook/.babelrc similarity index 100% rename from x-pack/plugins/canvas/.storybook/.babelrc rename to x-pack/plugins/canvas/storybook/.babelrc diff --git a/x-pack/plugins/canvas/.storybook/addons.js b/x-pack/plugins/canvas/storybook/addons.js similarity index 100% rename from x-pack/plugins/canvas/.storybook/addons.js rename to x-pack/plugins/canvas/storybook/addons.js diff --git a/x-pack/plugins/canvas/.storybook/config.js b/x-pack/plugins/canvas/storybook/config.js similarity index 67% rename from x-pack/plugins/canvas/.storybook/config.js rename to x-pack/plugins/canvas/storybook/config.js index 04b4e2a8e7b4b0..f349f9b7ccf98f 100644 --- a/x-pack/plugins/canvas/.storybook/config.js +++ b/x-pack/plugins/canvas/storybook/config.js @@ -4,13 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; import { configure, addDecorator, addParameters } from '@storybook/react'; -import { withKnobs } from '@storybook/addon-knobs/react'; import { withInfo } from '@storybook/addon-info'; import { create } from '@storybook/theming'; -import { KibanaContextProvider } from '../../../../src/plugins/kibana_react/public'; +import { addDecorators } from './decorators'; // If we're running Storyshots, be sure to register the require context hook. // Otherwise, add the other decorators. @@ -31,18 +29,9 @@ if (process.env.NODE_ENV === 'test') { }, }) ); - - // Add optional knobs to customize each story. - addDecorator(withKnobs); } -// Add New Platform Context for any stories that need it -const settings = new Map(); -settings.set('darkMode', true); -const platform = { - uiSettings: settings, -}; -addDecorator(fn => {fn()}); +addDecorators(); function loadStories() { require('./dll_contexts'); @@ -54,14 +43,14 @@ function loadStories() { true, /plugins\/(?=canvas).*light\.css/ ); - css.keys().forEach(filename => css(filename)); + css.keys().forEach((filename) => css(filename)); - // Find all files ending in *.examples.ts - const req = require.context('./..', true, /.(stories|examples).tsx$/); - req.keys().forEach(filename => req(filename)); + // Find all files ending in *.stories.tsx + const req = require.context('./..', true, /.(stories).tsx$/); + req.keys().forEach((filename) => req(filename)); // Import Canvas CSS - require('../public/style/index.scss') + require('../public/style/index.scss'); } // Set up the Storybook environment with custom settings. diff --git a/x-pack/plugins/canvas/.storybook/constants.js b/x-pack/plugins/canvas/storybook/constants.js similarity index 100% rename from x-pack/plugins/canvas/.storybook/constants.js rename to x-pack/plugins/canvas/storybook/constants.js diff --git a/x-pack/plugins/canvas/storybook/decorators/index.ts b/x-pack/plugins/canvas/storybook/decorators/index.ts new file mode 100644 index 00000000000000..aa1e958a410f55 --- /dev/null +++ b/x-pack/plugins/canvas/storybook/decorators/index.ts @@ -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 { addDecorator } from '@storybook/react'; +import { withKnobs } from '@storybook/addon-knobs'; +// @ts-expect-error +import { withInfo } from '@storybook/addon-info'; + +import { routerContextDecorator } from './router_decorator'; +import { kibanaContextDecorator } from './kibana_decorator'; + +export const addDecorators = () => { + addDecorator(withKnobs); + addDecorator(kibanaContextDecorator); + addDecorator(routerContextDecorator); +}; diff --git a/x-pack/plugins/canvas/storybook/decorators/kibana_decorator.tsx b/x-pack/plugins/canvas/storybook/decorators/kibana_decorator.tsx new file mode 100644 index 00000000000000..a55efba5a51837 --- /dev/null +++ b/x-pack/plugins/canvas/storybook/decorators/kibana_decorator.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; + +const settings = new Map(); +settings.set('darkMode', true); +const platform = { + http: { + basePath: { + get: () => '', + prepend: () => '', + remove: () => '', + serverBasePath: '', + }, + }, + uiSettings: settings, +}; + +export const kibanaContextDecorator = (story: Function) => ( + {story()} +); diff --git a/x-pack/plugins/canvas/storybook/decorators/router_decorator.tsx b/x-pack/plugins/canvas/storybook/decorators/router_decorator.tsx new file mode 100644 index 00000000000000..43b0da6473f236 --- /dev/null +++ b/x-pack/plugins/canvas/storybook/decorators/router_decorator.tsx @@ -0,0 +1,30 @@ +/* + * 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 PropTypes from 'prop-types'; + +class RouterContext extends React.Component { + static childContextTypes = { + router: PropTypes.object.isRequired, + }; + + getChildContext() { + return { + router: { + getFullPath: () => 'path', + create: () => '', + }, + }; + } + render() { + return <>{this.props.children}; + } +} + +export function routerContextDecorator(story: Function) { + return {story()}; +} diff --git a/x-pack/plugins/canvas/.storybook/dll_contexts.js b/x-pack/plugins/canvas/storybook/dll_contexts.js similarity index 88% rename from x-pack/plugins/canvas/.storybook/dll_contexts.js rename to x-pack/plugins/canvas/storybook/dll_contexts.js index 529fd8aa5c7914..85f2ab66cab88c 100644 --- a/x-pack/plugins/canvas/.storybook/dll_contexts.js +++ b/x-pack/plugins/canvas/storybook/dll_contexts.js @@ -15,7 +15,7 @@ const css = require.context( true, /\.\/plugins\/(?!canvas).*light\.css/ ); -css.keys().forEach(filename => { +css.keys().forEach((filename) => { css(filename); }); @@ -25,7 +25,7 @@ const uiStyles = require.context( false, /[\/\\](?!mixins|variables|_|\.|bootstrap_(light|dark))[^\/\\]+\.less/ ); -uiStyles.keys().forEach(key => uiStyles(key)); +uiStyles.keys().forEach((key) => uiStyles(key)); const json = require.context('../shareable_runtime/test/workpads', false, /\.json$/); -json.keys().forEach(key => json(key)); +json.keys().forEach((key) => json(key)); diff --git a/x-pack/plugins/canvas/.storybook/middleware.js b/x-pack/plugins/canvas/storybook/middleware.js similarity index 72% rename from x-pack/plugins/canvas/.storybook/middleware.js rename to x-pack/plugins/canvas/storybook/middleware.js index 8bbd2b6c1a22f6..baa524aefa709f 100644 --- a/x-pack/plugins/canvas/.storybook/middleware.js +++ b/x-pack/plugins/canvas/storybook/middleware.js @@ -4,10 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -const serve = require('serve-static'); const path = require('path'); +const serve = require('serve-static'); // Extend the Storybook Middleware to include a route to access Legacy UI assets -module.exports = function(router) { - router.get('/ui', serve(path.resolve(__dirname, '../../../../../src/core/server/core_app/assets'))); +module.exports = function (router) { + router.get( + '/ui', + serve(path.resolve(__dirname, '../../../../../src/core/server/core_app/assets')) + ); }; diff --git a/x-pack/plugins/canvas/.storybook/preview-head.html b/x-pack/plugins/canvas/storybook/preview-head.html similarity index 100% rename from x-pack/plugins/canvas/.storybook/preview-head.html rename to x-pack/plugins/canvas/storybook/preview-head.html diff --git a/x-pack/plugins/canvas/.storybook/storyshots.test.js b/x-pack/plugins/canvas/storybook/storyshots.test.js similarity index 95% rename from x-pack/plugins/canvas/.storybook/storyshots.test.js rename to x-pack/plugins/canvas/storybook/storyshots.test.js index 7195b97712464a..ba4013f7cc8160 100644 --- a/x-pack/plugins/canvas/.storybook/storyshots.test.js +++ b/x-pack/plugins/canvas/storybook/storyshots.test.js @@ -53,7 +53,7 @@ jest.mock('@elastic/eui/packages/react-datepicker', () => { }); // Mock React Portal for components that use modals, tooltips, etc -ReactDOM.createPortal = jest.fn(element => { +ReactDOM.createPortal = jest.fn((element) => { return element; }); @@ -67,7 +67,7 @@ jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => { // https://github.com/elastic/eui/issues/3712 jest.mock('@elastic/eui/lib/components/overlay_mask/overlay_mask', () => { return { - EuiOverlayMask: ({children}) => children, + EuiOverlayMask: ({ children }) => children, }; }); @@ -79,19 +79,19 @@ jest.mock( } ); +import { EuiObserver } from '@elastic/eui/test-env/components/observer/observer'; +jest.mock('@elastic/eui/test-env/components/observer/observer'); +EuiObserver.mockImplementation(() => 'EuiObserver'); + // This element uses a `ref` and cannot be rendered by Jest snapshots. import { RenderedElement } from '../shareable_runtime/components/rendered_element'; jest.mock('../shareable_runtime/components/rendered_element'); RenderedElement.mockImplementation(() => 'RenderedElement'); -import { EuiObserver } from '@elastic/eui/test-env/components/observer/observer'; -jest.mock('@elastic/eui/test-env/components/observer/observer'); -EuiObserver.mockImplementation(() => 'EuiObserver'); - addSerializer(styleSheetSerializer); // Initialize Storyshots and build the Jest Snapshots initStoryshots({ - configPath: path.resolve(__dirname, './../.storybook'), + configPath: path.resolve(__dirname, './../storybook'), test: multiSnapshotWithOptions({}), }); diff --git a/x-pack/plugins/canvas/.storybook/webpack.config.js b/x-pack/plugins/canvas/storybook/webpack.config.js similarity index 88% rename from x-pack/plugins/canvas/.storybook/webpack.config.js rename to x-pack/plugins/canvas/storybook/webpack.config.js index 3148a6742f76a4..1e0e36f7961282 100644 --- a/x-pack/plugins/canvas/.storybook/webpack.config.js +++ b/x-pack/plugins/canvas/storybook/webpack.config.js @@ -14,7 +14,7 @@ const { DLL_OUTPUT, KIBANA_ROOT } = require('./constants'); module.exports = async ({ config }) => { // Find and alter the CSS rule to replace the Kibana public path string with a path // to the route we've added in middleware.js - const cssRule = config.module.rules.find(rule => rule.test.source.includes('.css$')); + const cssRule = config.module.rules.find((rule) => rule.test.source.includes('.css$')); cssRule.use.push({ loader: 'string-replace-loader', options: { @@ -153,7 +153,7 @@ module.exports = async ({ config }) => { config.plugins.push( // replace imports for `uiExports/*` modules with a synthetic module // created by create_ui_exports_module.js - new webpack.NormalModuleReplacementPlugin(/^uiExports\//, resource => { + new webpack.NormalModuleReplacementPlugin(/^uiExports\//, (resource) => { // uiExports used by Canvas const extensions = { hacks: [], @@ -179,10 +179,22 @@ module.exports = async ({ config }) => { }), // Mock out libs used by a few componets to avoid loading in kibana_legacy and platform - new webpack.NormalModuleReplacementPlugin(/(lib)?\/notify/, path.resolve(__dirname, '../tasks/mocks/uiNotify')), - new webpack.NormalModuleReplacementPlugin(/lib\/download_workpad/, path.resolve(__dirname, '../tasks/mocks/downloadWorkpad')), - new webpack.NormalModuleReplacementPlugin(/(lib)?\/custom_element_service/, path.resolve(__dirname, '../tasks/mocks/customElementService')), - new webpack.NormalModuleReplacementPlugin(/(lib)?\/ui_metric/, path.resolve(__dirname, '../tasks/mocks/uiMetric')), + new webpack.NormalModuleReplacementPlugin( + /(lib)?\/notify/, + path.resolve(__dirname, '../tasks/mocks/uiNotify') + ), + new webpack.NormalModuleReplacementPlugin( + /lib\/download_workpad/, + path.resolve(__dirname, '../tasks/mocks/downloadWorkpad') + ), + new webpack.NormalModuleReplacementPlugin( + /(lib)?\/custom_element_service/, + path.resolve(__dirname, '../tasks/mocks/customElementService') + ), + new webpack.NormalModuleReplacementPlugin( + /(lib)?\/ui_metric/, + path.resolve(__dirname, '../tasks/mocks/uiMetric') + ) ); // Tell Webpack about relevant extensions @@ -196,7 +208,10 @@ module.exports = async ({ config }) => { '../tasks/mocks/uiNotifyFormatMsg' ); config.resolve.alias['ui/notify'] = path.resolve(__dirname, '../tasks/mocks/uiNotify'); - config.resolve.alias['ui/url/absolute_to_parsed_url'] = path.resolve(__dirname, '../tasks/mocks/uiAbsoluteToParsedUrl'); + config.resolve.alias['ui/url/absolute_to_parsed_url'] = path.resolve( + __dirname, + '../tasks/mocks/uiAbsoluteToParsedUrl' + ); config.resolve.alias['ui/chrome'] = path.resolve(__dirname, '../tasks/mocks/uiChrome'); config.resolve.alias.ui = path.resolve(KIBANA_ROOT, 'src/legacy/ui/public'); config.resolve.alias.ng_mock$ = path.resolve(KIBANA_ROOT, 'src/test_utils/public/ng_mock'); diff --git a/x-pack/plugins/canvas/.storybook/webpack.dll.config.js b/x-pack/plugins/canvas/storybook/webpack.dll.config.js similarity index 100% rename from x-pack/plugins/canvas/.storybook/webpack.dll.config.js rename to x-pack/plugins/canvas/storybook/webpack.dll.config.js index 5fdc4519f3bd77..0e9371e4cb5e45 100644 --- a/x-pack/plugins/canvas/.storybook/webpack.dll.config.js +++ b/x-pack/plugins/canvas/storybook/webpack.dll.config.js @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -const webpack = require('webpack'); const path = require('path'); +const webpack = require('webpack'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const { DLL_NAME, DLL_OUTPUT, KIBANA_ROOT } = require('./constants'); From a1753ffffdfc248f48561ab5fc2116c4a5e0b8ee Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 20 Jul 2020 20:35:15 -0700 Subject: [PATCH 015/101] [ftr/webdriver] retry on all errors, use Rx so that timers are canceled (#72540) * [ftr/webdriver] retry on all errors, use Rx so that timers are canceled * throw if attemptToCreateCommand() aborts by resolving to undefined Co-authored-by: spalger --- test/functional/services/remote/webdriver.ts | 45 +++++++++++--------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/test/functional/services/remote/webdriver.ts b/test/functional/services/remote/webdriver.ts index 0611c80f59b92f..09fede7fe25464 100644 --- a/test/functional/services/remote/webdriver.ts +++ b/test/functional/services/remote/webdriver.ts @@ -21,10 +21,9 @@ import { resolve } from 'path'; import Fs from 'fs'; import * as Rx from 'rxjs'; -import { mergeMap, map, takeUntil } from 'rxjs/operators'; +import { mergeMap, map, takeUntil, catchError } from 'rxjs/operators'; import { Lifecycle } from '@kbn/test/src/functional_test_runner/lib/lifecycle'; import { ToolingLog } from '@kbn/dev-utils'; -import { delay } from 'bluebird'; import chromeDriver from 'chromedriver'; // @ts-ignore types not available import geckoDriver from 'geckodriver'; @@ -337,25 +336,33 @@ export async function initWebDriver( edgePaths = await installDriver(); } - return await Promise.race([ - (async () => { - await delay(2 * MINUTE); - throw new Error('remote failed to start within 2 minutes'); - })(), - - (async () => { - while (true) { - const command = await Promise.race([ - delay(30 * SECOND), - attemptToCreateCommand(log, browserType, lifecycle, config), - ]); + return await Rx.race( + Rx.timer(2 * MINUTE).pipe( + map(() => { + throw new Error('remote failed to start within 2 minutes'); + }) + ), + Rx.race( + Rx.defer(async () => { + const command = await attemptToCreateCommand(log, browserType, lifecycle, config); if (!command) { - continue; + throw new Error('remote creation aborted'); } - return command; - } - })(), - ]); + }), + Rx.timer(30 * SECOND).pipe( + map(() => { + throw new Error('remote failed to start within 30 seconds'); + }) + ) + ).pipe( + catchError((error, resubscribe) => { + log.warning('Failure while creating webdriver instance'); + log.warning(error); + log.warning('...retrying...'); + return resubscribe; + }) + ) + ).toPromise(); } From 7f6cd5148a042fbdfbc7711037f90a319ca68a07 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Tue, 21 Jul 2020 07:09:25 +0200 Subject: [PATCH 016/101] [Discover] Improve histogram tests (#72235) --- ...er_histogram.js => _discover_histogram.ts} | 54 ++++++------------- .../data.json | 17 ++++++ 2 files changed, 33 insertions(+), 38 deletions(-) rename test/functional/apps/discover/{_discover_histogram.js => _discover_histogram.ts} (61%) create mode 100644 test/functional/fixtures/es_archiver/long_window_logstash_index_pattern/data.json diff --git a/test/functional/apps/discover/_discover_histogram.js b/test/functional/apps/discover/_discover_histogram.ts similarity index 61% rename from test/functional/apps/discover/_discover_histogram.js rename to test/functional/apps/discover/_discover_histogram.ts index e53c953f1514eb..5c78bfccbb9660 100644 --- a/test/functional/apps/discover/_discover_histogram.js +++ b/test/functional/apps/discover/_discover_histogram.ts @@ -18,14 +18,12 @@ */ import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { - const log = getService('log'); +export default function ({ getService, getPageObjects }: FtrProviderContext) { const esArchiver = getService('esArchiver'); - const browser = getService('browser'); const elasticChart = getService('elasticChart'); const kibanaServer = getService('kibanaServer'); - const security = getService('security'); const PageObjects = getPageObjects(['settings', 'common', 'discover', 'header', 'timePicker']); const defaultSettings = { defaultIndex: 'long-window-logstash-*', @@ -33,63 +31,43 @@ export default function ({ getService, getPageObjects }) { }; describe('discover histogram', function describeIndexTests() { - before(async function () { - log.debug('load kibana index with default index pattern'); - await PageObjects.common.navigateToApp('settings'); - await security.testUser.setRoles([ - 'kibana_admin', - 'test_logstash_reader', - 'long_window_logstash', - ]); + before(async () => { await esArchiver.loadIfNeeded('logstash_functional'); await esArchiver.load('long_window_logstash'); - await esArchiver.load('visualize'); - await esArchiver.load('discover'); - - log.debug('create long_window_logstash index pattern'); - // NOTE: long_window_logstash load does NOT create index pattern - await PageObjects.settings.createIndexPattern('long-window-logstash-*'); + await esArchiver.load('long_window_logstash_index_pattern'); await kibanaServer.uiSettings.replace(defaultSettings); - await browser.refresh(); - - log.debug('discover'); await PageObjects.common.navigateToApp('discover'); - await PageObjects.discover.selectIndexPattern('long-window-logstash-*'); - // NOTE: For some reason without setting this relative time, the abs times will not fetch data. - await PageObjects.timePicker.setCommonlyUsedTime('Last_1 year'); }); after(async () => { await esArchiver.unload('long_window_logstash'); - await esArchiver.unload('visualize'); - await esArchiver.unload('discover'); - await security.testUser.restoreDefaults(); + await esArchiver.unload('long_window_logstash_index_pattern'); }); + async function prepareTest(fromTime: string, toTime: string, interval: string) { + await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); + await PageObjects.discover.waitUntilSearchingHasFinished(); + await PageObjects.discover.setChartInterval(interval); + await PageObjects.header.waitUntilLoadingHasFinished(); + } + it('should visualize monthly data with different day intervals', async () => { const fromTime = 'Nov 01, 2017 @ 00:00:00.000'; const toTime = 'Mar 21, 2018 @ 00:00:00.000'; - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); - await PageObjects.discover.setChartInterval('Month'); - await PageObjects.header.waitUntilLoadingHasFinished(); + await prepareTest(fromTime, toTime, 'Month'); const chartCanvasExist = await elasticChart.canvasExists(); expect(chartCanvasExist).to.be(true); }); it('should visualize weekly data with within DST changes', async () => { const fromTime = 'Mar 01, 2018 @ 00:00:00.000'; const toTime = 'May 01, 2018 @ 00:00:00.000'; - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); - await PageObjects.discover.setChartInterval('Week'); - await PageObjects.header.waitUntilLoadingHasFinished(); + await prepareTest(fromTime, toTime, 'Week'); const chartCanvasExist = await elasticChart.canvasExists(); expect(chartCanvasExist).to.be(true); }); - it('should visualize monthly data with different years Scaled to 30 days', async () => { + it('should visualize monthly data with different years scaled to 30 days', async () => { const fromTime = 'Jan 01, 2010 @ 00:00:00.000'; const toTime = 'Mar 21, 2019 @ 00:00:00.000'; - - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); - await PageObjects.discover.setChartInterval('Day'); - await PageObjects.header.waitUntilLoadingHasFinished(); + await prepareTest(fromTime, toTime, 'Day'); const chartCanvasExist = await elasticChart.canvasExists(); expect(chartCanvasExist).to.be(true); const chartIntervalIconTip = await PageObjects.discover.getChartIntervalWarningIcon(); diff --git a/test/functional/fixtures/es_archiver/long_window_logstash_index_pattern/data.json b/test/functional/fixtures/es_archiver/long_window_logstash_index_pattern/data.json new file mode 100644 index 00000000000000..75aa6f06bb11a6 --- /dev/null +++ b/test/functional/fixtures/es_archiver/long_window_logstash_index_pattern/data.json @@ -0,0 +1,17 @@ +{ + "type": "doc", + "value": { + "id": "index-pattern:long-window-logstash-*", + "index": ".kibana", + "source": { + "index-pattern": { + "fields": "[{\"name\":\"referer\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"agent\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"xss.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.lastname\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.dest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"utc_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.char\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"clientip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"machine.ram\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"links\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"id\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"phpmemory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.twitter:card.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"ip\",\"type\":\"ip\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:modified_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:site_name.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:tag\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"agent.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"headings\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"_source\",\"type\":\"_source\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.og:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"request\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"index.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"memory\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_index\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"relatedContent.twitter:site\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.coordinates\",\"type\":\"geo_point\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"meta.related\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@message.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.article:section\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"xss\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"links.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"geo.srcdest\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"extension.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"machine.os.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@tags\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"host.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:type.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"geo.src\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"spaces.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:image:height.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"url\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:site_name\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:title\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"@message\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.twitter:image.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"@timestamp\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"bytes\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"response\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"meta.user.firstname\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":true,\"doc_values\":false},{\"name\":\"relatedContent.og:image:width.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.og:description.raw\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"relatedContent.article:published_time\",\"type\":\"date\",\"count\":0,\"scripted\":false,\"indexed\":true,\"analyzed\":false,\"doc_values\":true},{\"name\":\"_id\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_type\",\"type\":\"string\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false},{\"name\":\"_score\",\"type\":\"number\",\"count\":0,\"scripted\":false,\"indexed\":false,\"analyzed\":false,\"doc_values\":false}]", + "timeFieldName": "@timestamp", + "title": "long-window-logstash-*" + }, + "type": "index-pattern" + } + } +} + + From 511e4543a7828cf0cdb157b88b01352947e0384f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Tue, 21 Jul 2020 08:41:15 +0200 Subject: [PATCH 017/101] [APM] Handle ML errors (#72316) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [APM] Handle ML errors * Add capability check * Improve test * Address Caue’s feedback * Move getSeverity * Fix tsc * Fix copy --- .../plugins/apm/common/anomaly_detection.ts | 50 +++++++++++ .../apm/common/ml_job_constants.test.ts | 41 --------- .../ServiceMap/Popover/AnomalyDetection.tsx | 2 +- .../ServiceMap/Popover/getSeverity.test.ts | 39 +++++++++ .../app/ServiceMap/Popover/getSeverity.ts} | 2 + .../generate_service_map_elements.ts | 2 +- .../app/ServiceMap/cytoscapeOptions.ts | 2 +- .../anomaly_detection/add_environments.tsx | 17 +++- .../Settings/anomaly_detection/create_jobs.ts | 80 +++++++++++------- .../app/Settings/anomaly_detection/index.tsx | 33 ++++++-- .../Settings/anomaly_detection/jobs_list.tsx | 84 +++++++++---------- .../apm/AnomalyDetectionSetupLink.test.tsx | 39 +++++---- .../Links/apm/AnomalyDetectionSetupLink.tsx | 20 ++++- .../anomaly_detection_error.ts | 16 ++++ .../create_anomaly_detection_jobs.ts | 26 +++--- .../get_anomaly_detection_jobs.ts | 16 ++-- .../routes/settings/anomaly_detection.ts | 62 +++++++++++--- 17 files changed, 355 insertions(+), 176 deletions(-) delete mode 100644 x-pack/plugins/apm/common/ml_job_constants.test.ts create mode 100644 x-pack/plugins/apm/public/components/app/ServiceMap/Popover/getSeverity.test.ts rename x-pack/plugins/apm/{common/ml_job_constants.ts => public/components/app/ServiceMap/Popover/getSeverity.ts} (80%) create mode 100644 x-pack/plugins/apm/server/lib/anomaly_detection/anomaly_detection_error.ts diff --git a/x-pack/plugins/apm/common/anomaly_detection.ts b/x-pack/plugins/apm/common/anomaly_detection.ts index 1fd927d82f1866..9e0a3e3d0d889a 100644 --- a/x-pack/plugins/apm/common/anomaly_detection.ts +++ b/x-pack/plugins/apm/common/anomaly_detection.ts @@ -4,9 +4,59 @@ * you may not use this file except in compliance with the Elastic License. */ +import { i18n } from '@kbn/i18n'; + export interface ServiceAnomalyStats { transactionType?: string; anomalyScore?: number; actualValue?: number; jobId?: string; } + +export const MLErrorMessages: Record = { + INSUFFICIENT_LICENSE: i18n.translate( + 'xpack.apm.anomaly_detection.error.insufficient_license', + { + defaultMessage: + 'You must have a platinum license to use Anomaly Detection', + } + ), + MISSING_READ_PRIVILEGES: i18n.translate( + 'xpack.apm.anomaly_detection.error.missing_read_privileges', + { + defaultMessage: + 'You must have "read" privileges to Machine Learning in order to view Anomaly Detection jobs', + } + ), + MISSING_WRITE_PRIVILEGES: i18n.translate( + 'xpack.apm.anomaly_detection.error.missing_write_privileges', + { + defaultMessage: + 'You must have "write" privileges to Machine Learning and APM in order to create Anomaly Detection jobs', + } + ), + ML_NOT_AVAILABLE: i18n.translate( + 'xpack.apm.anomaly_detection.error.not_available', + { + defaultMessage: 'Machine learning is not available', + } + ), + ML_NOT_AVAILABLE_IN_SPACE: i18n.translate( + 'xpack.apm.anomaly_detection.error.not_available_in_space', + { + defaultMessage: 'Machine learning is not available in the selected space', + } + ), + UNEXPECTED: i18n.translate('xpack.apm.anomaly_detection.error.unexpected', { + defaultMessage: 'An unexpected error occurred', + }), +}; + +export enum ErrorCode { + INSUFFICIENT_LICENSE = 'INSUFFICIENT_LICENSE', + MISSING_READ_PRIVILEGES = 'MISSING_READ_PRIVILEGES', + MISSING_WRITE_PRIVILEGES = 'MISSING_WRITE_PRIVILEGES', + ML_NOT_AVAILABLE = 'ML_NOT_AVAILABLE', + ML_NOT_AVAILABLE_IN_SPACE = 'ML_NOT_AVAILABLE_IN_SPACE', + UNEXPECTED = 'UNEXPECTED', +} diff --git a/x-pack/plugins/apm/common/ml_job_constants.test.ts b/x-pack/plugins/apm/common/ml_job_constants.test.ts deleted file mode 100644 index 96e3ba826d2010..00000000000000 --- a/x-pack/plugins/apm/common/ml_job_constants.test.ts +++ /dev/null @@ -1,41 +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 { getSeverity, severity } from './ml_job_constants'; - -describe('ml_job_constants', () => { - describe('getSeverity', () => { - describe('when score is undefined', () => { - it('returns undefined', () => { - expect(getSeverity(undefined)).toEqual(undefined); - }); - }); - - describe('when score < 25', () => { - it('returns warning', () => { - expect(getSeverity(10)).toEqual(severity.warning); - }); - }); - - describe('when score is between 25 and 50', () => { - it('returns minor', () => { - expect(getSeverity(40)).toEqual(severity.minor); - }); - }); - - describe('when score is between 50 and 75', () => { - it('returns major', () => { - expect(getSeverity(60)).toEqual(severity.major); - }); - }); - - describe('when score is 75 or more', () => { - it('returns critical', () => { - expect(getSeverity(100)).toEqual(severity.critical); - }); - }); - }); -}); diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx index 410ba8b5027fbe..b3d19e1aab2cc9 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx @@ -19,9 +19,9 @@ import { fontSize, px } from '../../../../style/variables'; import { asInteger, asDuration } from '../../../../utils/formatters'; import { MLJobLink } from '../../../shared/Links/MachineLearningLinks/MLJobLink'; import { getSeverityColor, popoverWidth } from '../cytoscapeOptions'; -import { getSeverity } from '../../../../../common/ml_job_constants'; import { TRANSACTION_REQUEST } from '../../../../../common/transaction_types'; import { ServiceAnomalyStats } from '../../../../../common/anomaly_detection'; +import { getSeverity } from './getSeverity'; const HealthStatusTitle = styled(EuiTitle)` display: inline; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/getSeverity.test.ts b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/getSeverity.test.ts new file mode 100644 index 00000000000000..52b7d54236db66 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/getSeverity.test.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 { getSeverity, severity } from './getSeverity'; + +describe('getSeverity', () => { + describe('when score is undefined', () => { + it('returns undefined', () => { + expect(getSeverity(undefined)).toEqual(undefined); + }); + }); + + describe('when score < 25', () => { + it('returns warning', () => { + expect(getSeverity(10)).toEqual(severity.warning); + }); + }); + + describe('when score is between 25 and 50', () => { + it('returns minor', () => { + expect(getSeverity(40)).toEqual(severity.minor); + }); + }); + + describe('when score is between 50 and 75', () => { + it('returns major', () => { + expect(getSeverity(60)).toEqual(severity.major); + }); + }); + + describe('when score is 75 or more', () => { + it('returns critical', () => { + expect(getSeverity(100)).toEqual(severity.critical); + }); + }); +}); diff --git a/x-pack/plugins/apm/common/ml_job_constants.ts b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/getSeverity.ts similarity index 80% rename from x-pack/plugins/apm/common/ml_job_constants.ts rename to x-pack/plugins/apm/public/components/app/ServiceMap/Popover/getSeverity.ts index b8c2546bd0c84a..f4eb2033e92311 100644 --- a/x-pack/plugins/apm/common/ml_job_constants.ts +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/getSeverity.ts @@ -11,6 +11,8 @@ export enum severity { warning = 'warning', } +// TODO: Replace with `getSeverity` from: +// https://github.com/elastic/kibana/blob/0f964f66916480f2de1f4b633e5afafc08cf62a0/x-pack/plugins/ml/common/util/anomaly_utils.ts#L129 export function getSeverity(score?: number) { if (typeof score !== 'number') { return undefined; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/__stories__/generate_service_map_elements.ts b/x-pack/plugins/apm/public/components/app/ServiceMap/__stories__/generate_service_map_elements.ts index e7d55cd5707101..012256db3ab987 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/__stories__/generate_service_map_elements.ts +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/__stories__/generate_service_map_elements.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getSeverity } from '../../../../../common/ml_job_constants'; +import { getSeverity } from '../Popover/getSeverity'; export function generateServiceMapElements(size: number): any[] { const services = range(size).map((i) => { diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts index dfcfbee1806a48..4a271019e06db9 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts @@ -10,9 +10,9 @@ import { SPAN_DESTINATION_SERVICE_RESOURCE, } from '../../../../common/elasticsearch_fieldnames'; import { EuiTheme } from '../../../../../observability/public'; -import { severity, getSeverity } from '../../../../common/ml_job_constants'; import { defaultIcon, iconForNode } from './icons'; import { ServiceAnomalyStats } from '../../../../common/anomaly_detection'; +import { severity, getSeverity } from './Popover/getSeverity'; export const popoverWidth = 280; diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx index 4c056d48f4b14a..c9328c4988e5f7 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/add_environments.tsx @@ -17,8 +17,10 @@ import { EuiFlexGroup, EuiFlexItem, EuiFormRow, + EuiEmptyPrompt, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { MLErrorMessages } from '../../../../../common/anomaly_detection'; import { useFetcher, FETCH_STATUS } from '../../../../hooks/useFetcher'; import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; import { createJobs } from './create_jobs'; @@ -34,7 +36,9 @@ export const AddEnvironments = ({ onCreateJobSuccess, onCancel, }: Props) => { - const { toasts } = useApmPluginContext().core.notifications; + const { notifications, application } = useApmPluginContext().core; + const canCreateJob = !!application.capabilities.ml.canCreateJob; + const { toasts } = notifications; const { data = [], status } = useFetcher( (callApmApi) => callApmApi({ @@ -56,6 +60,17 @@ export const AddEnvironments = ({ Array> >([]); + if (!canCreateJob) { + return ( + + {MLErrorMessages.MISSING_WRITE_PRIVILEGES}} + /> + + ); + } + const isLoading = status === FETCH_STATUS.PENDING || status === FETCH_STATUS.LOADING; return ( diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/create_jobs.ts b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/create_jobs.ts index 614632a5a3b092..acea38732b40a2 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/create_jobs.ts +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/create_jobs.ts @@ -6,8 +6,19 @@ import { i18n } from '@kbn/i18n'; import { NotificationsStart } from 'kibana/public'; +import { MLErrorMessages } from '../../../../../common/anomaly_detection'; import { callApmApi } from '../../../../services/rest/createCallApmApi'; +const errorToastTitle = i18n.translate( + 'xpack.apm.anomalyDetection.createJobs.failed.title', + { defaultMessage: 'Anomaly detection jobs could not be created' } +); + +const successToastTitle = i18n.translate( + 'xpack.apm.anomalyDetection.createJobs.succeeded.title', + { defaultMessage: 'Anomaly detection jobs created' } +); + export async function createJobs({ environments, toasts, @@ -16,7 +27,7 @@ export async function createJobs({ toasts: NotificationsStart['toasts']; }) { try { - await callApmApi({ + const res = await callApmApi({ pathname: '/api/apm/settings/anomaly-detection/jobs', method: 'POST', params: { @@ -24,41 +35,50 @@ export async function createJobs({ }, }); + // a known error occurred + if (res?.errorCode) { + toasts.addDanger({ + title: errorToastTitle, + text: MLErrorMessages[res.errorCode], + }); + return false; + } + + // job created successfully toasts.addSuccess({ - title: i18n.translate( - 'xpack.apm.anomalyDetection.createJobs.succeeded.title', - { defaultMessage: 'Anomaly detection jobs created' } - ), - text: i18n.translate( - 'xpack.apm.anomalyDetection.createJobs.succeeded.text', - { - defaultMessage: - 'Anomaly detection jobs successfully created for APM service environments [{environments}]. It will take some time for machine learning to start analyzing traffic for anomalies.', - values: { environments: environments.join(', ') }, - } - ), + title: successToastTitle, + text: getSuccessToastMessage(environments), }); return true; + + // an unknown/unexpected error occurred } catch (error) { toasts.addDanger({ - title: i18n.translate( - 'xpack.apm.anomalyDetection.createJobs.failed.title', - { - defaultMessage: 'Anomaly detection jobs could not be created', - } - ), - text: i18n.translate( - 'xpack.apm.anomalyDetection.createJobs.failed.text', - { - defaultMessage: - 'Something went wrong when creating one ore more anomaly detection jobs for APM service environments [{environments}]. Error: "{errorMessage}"', - values: { - environments: environments.join(', '), - errorMessage: error.message, - }, - } - ), + title: errorToastTitle, + text: getErrorToastMessage(environments, error), }); return false; } } + +function getSuccessToastMessage(environments: string[]) { + return i18n.translate( + 'xpack.apm.anomalyDetection.createJobs.succeeded.text', + { + defaultMessage: + 'Anomaly detection jobs successfully created for APM service environments [{environments}]. It will take some time for machine learning to start analyzing traffic for anomalies.', + values: { environments: environments.join(', ') }, + } + ); +} + +function getErrorToastMessage(environments: string[], error: Error) { + return i18n.translate('xpack.apm.anomalyDetection.createJobs.failed.text', { + defaultMessage: + 'Something went wrong when creating one ore more anomaly detection jobs for APM service environments [{environments}]. Error: "{errorMessage}"', + values: { + environments: environments.join(', '), + errorMessage: error.message, + }, + }); +} diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx index f02350fafbabbe..abbe1e2c83c7b0 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx @@ -7,7 +7,9 @@ import React, { useState } from 'react'; import { EuiTitle, EuiSpacer, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { EuiPanel } from '@elastic/eui'; +import { EuiPanel, EuiEmptyPrompt } from '@elastic/eui'; +import { MLErrorMessages } from '../../../../../common/anomaly_detection'; +import { useApmPluginContext } from '../../../../hooks/useApmPluginContext'; import { JobsList } from './jobs_list'; import { AddEnvironments } from './add_environments'; import { useFetcher } from '../../../../hooks/useFetcher'; @@ -16,24 +18,31 @@ import { useLicense } from '../../../../hooks/useLicense'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; export type AnomalyDetectionApiResponse = APIReturnType< - '/api/apm/settings/anomaly-detection' + '/api/apm/settings/anomaly-detection', + 'GET' >; const DEFAULT_VALUE: AnomalyDetectionApiResponse = { jobs: [], hasLegacyJobs: false, + errorCode: undefined, }; export const AnomalyDetection = () => { + const plugin = useApmPluginContext(); + const canGetJobs = !!plugin.core.application.capabilities.ml.canGetJobs; const license = useLicense(); const hasValidLicense = license?.isActive && license?.hasAtLeast('platinum'); const [viewAddEnvironments, setViewAddEnvironments] = useState(false); const { refetch, data = DEFAULT_VALUE, status } = useFetcher( - (callApmApi) => - callApmApi({ pathname: `/api/apm/settings/anomaly-detection` }), - [], + (callApmApi) => { + if (canGetJobs) { + return callApmApi({ pathname: `/api/apm/settings/anomaly-detection` }); + } + }, + [canGetJobs], { preservePreviousData: false, showToastOnError: false } ); @@ -53,6 +62,17 @@ export const AnomalyDetection = () => { ); } + if (!canGetJobs) { + return ( + + {MLErrorMessages.MISSING_READ_PRIVILEGES}} + /> + + ); + } + return ( <> @@ -83,9 +103,8 @@ export const AnomalyDetection = () => { /> ) : ( { setViewAddEnvironments(true); }} diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx index 5954b82f3b9e7d..67227f99cb5f1e 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx @@ -16,6 +16,10 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { + MLErrorMessages, + ErrorCode, +} from '../../../../../common/anomaly_detection'; import { FETCH_STATUS } from '../../../../hooks/useFetcher'; import { ITableColumn, ManagedTable } from '../../../shared/ManagedTable'; import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; @@ -57,21 +61,12 @@ const columns: Array> = [ ]; interface Props { + data: AnomalyDetectionApiResponse; status: FETCH_STATUS; onAddEnvironments: () => void; - jobs: Jobs; - hasLegacyJobs: boolean; } -export const JobsList = ({ - status, - onAddEnvironments, - jobs, - hasLegacyJobs, -}: Props) => { - const isLoading = - status === FETCH_STATUS.PENDING || status === FETCH_STATUS.LOADING; - - const hasFetchFailure = status === FETCH_STATUS.FAILURE; +export const JobsList = ({ data, status, onAddEnvironments }: Props) => { + const { jobs, hasLegacyJobs, errorCode } = data; return ( @@ -120,15 +115,10 @@ export const JobsList = ({ - ) : hasFetchFailure ? ( - - ) : ( - - ) - } + noItemsMessage={getNoItemsMessage({ + status, + errorCode, + })} columns={columns} items={jobs} /> @@ -139,28 +129,36 @@ export const JobsList = ({ ); }; -function EmptyStatePrompt() { - return ( - <> - {i18n.translate( - 'xpack.apm.settings.anomalyDetection.jobList.emptyListText', - { - defaultMessage: 'No anomaly detection jobs.', - } - )} - - ); -} +function getNoItemsMessage({ + status, + errorCode, +}: { + status: FETCH_STATUS; + errorCode?: ErrorCode; +}) { + // loading state + const isLoading = + status === FETCH_STATUS.PENDING || status === FETCH_STATUS.LOADING; + if (isLoading) { + return ; + } -function FailureStatePrompt() { - return ( - <> - {i18n.translate( - 'xpack.apm.settings.anomalyDetection.jobList.failedFetchText', - { - defaultMessage: 'Unabled to fetch anomaly detection jobs.', - } - )} - + // A known error occured. Show specific error message + if (errorCode) { + return MLErrorMessages[errorCode]; + } + + // An unexpected error occurred. Show default error message + if (status === FETCH_STATUS.FAILURE) { + return i18n.translate( + 'xpack.apm.settings.anomalyDetection.jobList.failedFetchText', + { defaultMessage: 'Unabled to fetch anomaly detection jobs.' } + ); + } + + // no errors occurred + return i18n.translate( + 'xpack.apm.settings.anomalyDetection.jobList.emptyListText', + { defaultMessage: 'No anomaly detection jobs.' } ); } diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.test.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.test.tsx index 268d8bd7ea8239..2149cb676f0d86 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.test.tsx @@ -6,37 +6,48 @@ import { showAlert } from './AnomalyDetectionSetupLink'; +const dataWithJobs = { + hasLegacyJobs: false, + jobs: [ + { job_id: 'job1', environment: 'staging' }, + { job_id: 'job2', environment: 'production' }, + ], +}; +const dataWithoutJobs = ({ jobs: [] } as unknown) as any; + describe('#showAlert', () => { describe('when an environment is selected', () => { it('should return true when there are no jobs', () => { - const result = showAlert([], 'testing'); + const result = showAlert(dataWithoutJobs, 'testing'); expect(result).toBe(true); }); it('should return true when environment is not included in the jobs', () => { - const result = showAlert( - [{ environment: 'staging' }, { environment: 'production' }], - 'testing' - ); + const result = showAlert(dataWithJobs, 'testing'); expect(result).toBe(true); }); it('should return false when environment is included in the jobs', () => { - const result = showAlert( - [{ environment: 'staging' }, { environment: 'production' }], - 'staging' - ); + const result = showAlert(dataWithJobs, 'staging'); expect(result).toBe(false); }); }); + describe('there is no environment selected (All)', () => { it('should return true when there are no jobs', () => { - const result = showAlert([], undefined); + const result = showAlert(dataWithoutJobs, undefined); expect(result).toBe(true); }); it('should return false when there are any number of jobs', () => { - const result = showAlert( - [{ environment: 'staging' }, { environment: 'production' }], - undefined - ); + const result = showAlert(dataWithJobs, undefined); + expect(result).toBe(false); + }); + }); + + describe('when a known error occurred', () => { + it('should return false', () => { + const data = ({ + errorCode: 'MISSING_READ_PRIVILEGES', + } as unknown) as any; + const result = showAlert(data, undefined); expect(result).toBe(false); }); }); diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.tsx index 6f3a5df480d7e7..e989244d431481 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/AnomalyDetectionSetupLink.tsx @@ -6,16 +6,25 @@ import React from 'react'; import { EuiButtonEmpty, EuiToolTip, EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { ErrorCode } from '../../../../../common/anomaly_detection'; +import { APIReturnType } from '../../../../services/rest/createCallApmApi'; import { APMLink } from './APMLink'; import { getEnvironmentLabel } from '../../../../../common/environment_filter_values'; import { useUrlParams } from '../../../../hooks/useUrlParams'; import { useFetcher, FETCH_STATUS } from '../../../../hooks/useFetcher'; +export type AnomalyDetectionApiResponse = APIReturnType< + '/api/apm/settings/anomaly-detection', + 'GET' +>; + +const DEFAULT_DATA = { jobs: [], hasLegacyJobs: false, errorCode: undefined }; + export function AnomalyDetectionSetupLink() { const { uiFilters } = useUrlParams(); const environment = uiFilters.environment; - const { data = { jobs: [], hasLegacyJobs: false }, status } = useFetcher( + const { data = DEFAULT_DATA, status } = useFetcher( (callApmApi) => callApmApi({ pathname: `/api/apm/settings/anomaly-detection` }), [], @@ -28,7 +37,7 @@ export function AnomalyDetectionSetupLink() { {ANOMALY_DETECTION_LINK_LABEL} - {isFetchSuccess && showAlert(data.jobs, environment) && ( + {isFetchSuccess && showAlert(data, environment) && ( @@ -59,9 +68,14 @@ const ANOMALY_DETECTION_LINK_LABEL = i18n.translate( ); export function showAlert( - jobs: Array<{ environment: string }> = [], + { jobs = [], errorCode }: AnomalyDetectionApiResponse, environment: string | undefined ) { + // don't show warning if the user is missing read privileges + if (errorCode === ErrorCode.MISSING_READ_PRIVILEGES) { + return false; + } + return ( // No job exists, or jobs.length === 0 || diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/anomaly_detection_error.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/anomaly_detection_error.ts new file mode 100644 index 00000000000000..993dcf4c5354bf --- /dev/null +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/anomaly_detection_error.ts @@ -0,0 +1,16 @@ +/* + * 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 { ErrorCode, MLErrorMessages } from '../../../common/anomaly_detection'; + +export class AnomalyDetectionError extends Error { + constructor(public code: ErrorCode) { + super(MLErrorMessages[code]); + + this.name = this.constructor.name; + Error.captureStackTrace(this, this.constructor); + } +} diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts index c387c5152b1c51..e5338ac9f57975 100644 --- a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts @@ -7,6 +7,7 @@ import { Logger } from 'kibana/server'; import uuid from 'uuid/v4'; import { snakeCase } from 'lodash'; +import { ErrorCode } from '../../../common/anomaly_detection'; import { PromiseReturnType } from '../../../../observability/typings/common'; import { Setup } from '../helpers/setup_request'; import { @@ -15,6 +16,7 @@ import { } from '../../../common/elasticsearch_fieldnames'; import { APM_ML_JOB_GROUP, ML_MODULE_ID_APM_TRANSACTION } from './constants'; import { getEnvironmentUiFilterES } from '../helpers/convert_ui_filters/get_environment_ui_filter_es'; +import { AnomalyDetectionError } from './anomaly_detection_error'; export type CreateAnomalyDetectionJobsAPIResponse = PromiseReturnType< typeof createAnomalyDetectionJobs @@ -25,21 +27,20 @@ export async function createAnomalyDetectionJobs( logger: Logger ) { const { ml, indices } = setup; + if (!ml) { - logger.warn('Anomaly detection plugin is not available.'); - return []; + throw new AnomalyDetectionError(ErrorCode.ML_NOT_AVAILABLE); } + const mlCapabilities = await ml.mlSystem.mlCapabilities(); if (!mlCapabilities.mlFeatureEnabledInSpace) { - logger.warn('Anomaly detection feature is not enabled for the space.'); - return []; + throw new AnomalyDetectionError(ErrorCode.ML_NOT_AVAILABLE_IN_SPACE); } + if (!mlCapabilities.isPlatinumOrTrialLicense) { - logger.warn( - 'Unable to create anomaly detection jobs due to insufficient license.' - ); - return []; + throw new AnomalyDetectionError(ErrorCode.INSUFFICIENT_LICENSE); } + logger.info( `Creating ML anomaly detection jobs for environments: [${environments}].` ); @@ -59,9 +60,8 @@ export async function createAnomalyDetectionJobs( `Failed to create anomaly detection ML jobs for: [${failedJobIds}]:` ); failedJobs.forEach(({ error }) => logger.error(JSON.stringify(error))); - throw new Error( - `Failed to create anomaly detection ML jobs for: [${failedJobIds}].` - ); + + throw new AnomalyDetectionError(ErrorCode.UNEXPECTED); } return jobResponses; @@ -70,11 +70,11 @@ export async function createAnomalyDetectionJobs( async function createAnomalyDetectionJob({ ml, environment, - indexPatternName = 'apm-*-transaction-*', + indexPatternName, }: { ml: Required['ml']; environment: string; - indexPatternName?: string | undefined; + indexPatternName: string; }) { const randomToken = uuid().substr(-4); diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts index 13b30f159eed1f..62d4243a060285 100644 --- a/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts @@ -5,8 +5,10 @@ */ import { Logger } from 'kibana/server'; +import { ErrorCode } from '../../../common/anomaly_detection'; import { Setup } from '../helpers/setup_request'; import { getMlJobsWithAPMGroup } from './get_ml_jobs_with_apm_group'; +import { AnomalyDetectionError } from './anomaly_detection_error'; export async function getAnomalyDetectionJobs(setup: Setup, logger: Logger) { const { ml } = setup; @@ -15,14 +17,12 @@ export async function getAnomalyDetectionJobs(setup: Setup, logger: Logger) { } const mlCapabilities = await ml.mlSystem.mlCapabilities(); - if ( - !( - mlCapabilities.mlFeatureEnabledInSpace && - mlCapabilities.isPlatinumOrTrialLicense - ) - ) { - logger.warn('Anomaly detection integration is not availble for this user.'); - return []; + if (!mlCapabilities.mlFeatureEnabledInSpace) { + throw new AnomalyDetectionError(ErrorCode.ML_NOT_AVAILABLE_IN_SPACE); + } + + if (!mlCapabilities.isPlatinumOrTrialLicense) { + throw new AnomalyDetectionError(ErrorCode.INSUFFICIENT_LICENSE); } const response = await getMlJobsWithAPMGroup(ml); diff --git a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts index 4d564b773e3971..218d47fcf9bb45 100644 --- a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts +++ b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts @@ -5,12 +5,32 @@ */ import * as t from 'io-ts'; +import { ErrorCode } from '../../../common/anomaly_detection'; +import { PromiseReturnType } from '../../../typings/common'; +import { InsufficientMLCapabilities } from '../../../../ml/server'; import { createRoute } from '../create_route'; import { getAnomalyDetectionJobs } from '../../lib/anomaly_detection/get_anomaly_detection_jobs'; import { createAnomalyDetectionJobs } from '../../lib/anomaly_detection/create_anomaly_detection_jobs'; import { setupRequest } from '../../lib/helpers/setup_request'; import { getAllEnvironments } from '../../lib/environments/get_all_environments'; import { hasLegacyJobs } from '../../lib/anomaly_detection/has_legacy_jobs'; +import { AnomalyDetectionError } from '../../lib/anomaly_detection/anomaly_detection_error'; + +type Jobs = PromiseReturnType; + +function getMlErrorCode(e: Error) { + // Missing privileges + if (e instanceof InsufficientMLCapabilities) { + return ErrorCode.MISSING_READ_PRIVILEGES; + } + + if (e instanceof AnomalyDetectionError) { + return e.code; + } + + // unexpected error + return ErrorCode.UNEXPECTED; +} // get ML anomaly detection jobs for each environment export const anomalyDetectionJobsRoute = createRoute(() => ({ @@ -18,14 +38,25 @@ export const anomalyDetectionJobsRoute = createRoute(() => ({ path: '/api/apm/settings/anomaly-detection', handler: async ({ context, request }) => { const setup = await setupRequest(context, request); - const [jobs, legacyJobs] = await Promise.all([ - getAnomalyDetectionJobs(setup, context.logger), - hasLegacyJobs(setup), - ]); - return { - jobs, - hasLegacyJobs: legacyJobs, - }; + + try { + const [jobs, legacyJobs] = await Promise.all([ + getAnomalyDetectionJobs(setup, context.logger), + hasLegacyJobs(setup), + ]); + return { + jobs, + hasLegacyJobs: legacyJobs, + }; + } catch (e) { + const mlErrorCode = getMlErrorCode(e); + context.logger.warn(`Error while retrieving ML jobs: "${e.message}"`); + return { + jobs: [] as Jobs, + hasLegacyJobs: false, + errorCode: mlErrorCode, + }; + } }, })); @@ -44,11 +75,16 @@ export const createAnomalyDetectionJobsRoute = createRoute(() => ({ handler: async ({ context, request }) => { const { environments } = context.params.body; const setup = await setupRequest(context, request); - return await createAnomalyDetectionJobs( - setup, - environments, - context.logger - ); + + try { + await createAnomalyDetectionJobs(setup, environments, context.logger); + } catch (e) { + const mlErrorCode = getMlErrorCode(e); + context.logger.warn(`Error while creating ML job: "${e.message}"`); + return { + errorCode: mlErrorCode, + }; + } }, })); From 005a1121cc2b50bfd931838592456716e5eb0bff Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Tue, 21 Jul 2020 09:44:25 +0200 Subject: [PATCH 018/101] use KibanaClient interface instead of Client for new client interface (#72388) * use KibanaClient interface instead of Client * add back helpers to ElasticsearchClient interface * use TransportRequestPromise for transport.request * update generated doc --- ...in-core-server.httpservicesetup.registeronpostauth.md | 2 +- ...gin-core-server.httpservicesetup.registeronpreauth.md | 2 +- .../development/core/server/kibana-plugin-core-server.md | 2 +- .../kibana-plugin-core-server.onpreresponsetoolkit.md | 2 +- src/core/server/elasticsearch/client/types.ts | 9 +++++---- src/core/server/server.api.md | 3 ++- 6 files changed, 11 insertions(+), 9 deletions(-) diff --git a/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.registeronpostauth.md b/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.registeronpostauth.md index eff53b7b75fa59..41b82f428948a0 100644 --- a/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.registeronpostauth.md +++ b/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.registeronpostauth.md @@ -14,5 +14,5 @@ registerOnPostAuth: (handler: OnPostAuthHandler) => void; ## Remarks -The auth state is available at stage via http.auth.get(..) Can register any number of registerOnPreRouting, which are called in sequence (from the first registered to the last). See [OnPostAuthHandler](./kibana-plugin-core-server.onpostauthhandler.md). +The auth state is available at stage via http.auth.get(..) Can register any number of registerOnPostAuth, which are called in sequence (from the first registered to the last). See [OnPostAuthHandler](./kibana-plugin-core-server.onpostauthhandler.md). diff --git a/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.registeronpreauth.md b/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.registeronpreauth.md index ce4cacb1c87490..57b1833df5e032 100644 --- a/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.registeronpreauth.md +++ b/docs/development/core/server/kibana-plugin-core-server.httpservicesetup.registeronpreauth.md @@ -14,5 +14,5 @@ registerOnPreAuth: (handler: OnPreAuthHandler) => void; ## Remarks -Can register any number of registerOnPostAuth, which are called in sequence (from the first registered to the last). See [OnPreRoutingHandler](./kibana-plugin-core-server.onpreroutinghandler.md). +Can register any number of registerOnPreAuth, which are called in sequence (from the first registered to the last). See [OnPreAuthHandler](./kibana-plugin-core-server.onpreauthhandler.md). diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index a665327454c1a6..61ffc532f0de5b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -122,7 +122,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [OnPreAuthToolkit](./kibana-plugin-core-server.onpreauthtoolkit.md) | A tool set defining an outcome of OnPreAuth interceptor for incoming request. | | [OnPreResponseExtensions](./kibana-plugin-core-server.onpreresponseextensions.md) | Additional data to extend a response. | | [OnPreResponseInfo](./kibana-plugin-core-server.onpreresponseinfo.md) | Response status code. | -| [OnPreResponseToolkit](./kibana-plugin-core-server.onpreresponsetoolkit.md) | A tool set defining an outcome of OnPreRouting interceptor for incoming request. | +| [OnPreResponseToolkit](./kibana-plugin-core-server.onpreresponsetoolkit.md) | A tool set defining an outcome of OnPreResponse interceptor for incoming request. | | [OnPreRoutingToolkit](./kibana-plugin-core-server.onpreroutingtoolkit.md) | A tool set defining an outcome of OnPreRouting interceptor for incoming request. | | [OpsMetrics](./kibana-plugin-core-server.opsmetrics.md) | Regroups metrics gathered by all the collectors. This contains metrics about the os/runtime, the kibana process and the http server. | | [OpsOsMetrics](./kibana-plugin-core-server.opsosmetrics.md) | OS related metrics | diff --git a/docs/development/core/server/kibana-plugin-core-server.onpreresponsetoolkit.md b/docs/development/core/server/kibana-plugin-core-server.onpreresponsetoolkit.md index 306c375ba4a3c3..44da09d0cc68eb 100644 --- a/docs/development/core/server/kibana-plugin-core-server.onpreresponsetoolkit.md +++ b/docs/development/core/server/kibana-plugin-core-server.onpreresponsetoolkit.md @@ -4,7 +4,7 @@ ## OnPreResponseToolkit interface -A tool set defining an outcome of OnPreRouting interceptor for incoming request. +A tool set defining an outcome of OnPreResponse interceptor for incoming request. Signature: diff --git a/src/core/server/elasticsearch/client/types.ts b/src/core/server/elasticsearch/client/types.ts index 934120c330e920..7ce998aab76692 100644 --- a/src/core/server/elasticsearch/client/types.ts +++ b/src/core/server/elasticsearch/client/types.ts @@ -17,11 +17,12 @@ * under the License. */ -import type { Client } from '@elastic/elasticsearch'; +import type { KibanaClient } from '@elastic/elasticsearch/api/kibana'; import type { ApiResponse, TransportRequestOptions, TransportRequestParams, + TransportRequestPromise, } from '@elastic/elasticsearch/lib/Transport'; /** @@ -30,13 +31,13 @@ import type { * @public */ export type ElasticsearchClient = Omit< - Client, - 'connectionPool' | 'transport' | 'serializer' | 'extend' | 'helpers' | 'child' | 'close' + KibanaClient, + 'connectionPool' | 'transport' | 'serializer' | 'extend' | 'child' | 'close' > & { transport: { request( params: TransportRequestParams, options?: TransportRequestOptions - ): Promise; + ): TransportRequestPromise; }; }; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index a0e16602ba4bfe..4b6bcbc8ad7a0d 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -22,7 +22,6 @@ import { CatTasksParams } from 'elasticsearch'; import { CatThreadPoolParams } from 'elasticsearch'; import { ClearScrollParams } from 'elasticsearch'; import { Client } from 'elasticsearch'; -import { Client as Client_2 } from '@elastic/elasticsearch'; import { ClientOptions } from '@elastic/elasticsearch'; import { ClusterAllocationExplainParams } from 'elasticsearch'; import { ClusterGetSettingsParams } from 'elasticsearch'; @@ -93,6 +92,7 @@ import { IngestDeletePipelineParams } from 'elasticsearch'; import { IngestGetPipelineParams } from 'elasticsearch'; import { IngestPutPipelineParams } from 'elasticsearch'; import { IngestSimulateParams } from 'elasticsearch'; +import { KibanaClient } from '@elastic/elasticsearch/api/kibana'; import { KibanaConfigType } from 'src/core/server/kibana_config'; import { MGetParams } from 'elasticsearch'; import { MGetResponse } from 'elasticsearch'; @@ -143,6 +143,7 @@ import { TasksListParams } from 'elasticsearch'; import { TermvectorsParams } from 'elasticsearch'; import { TransportRequestOptions } from '@elastic/elasticsearch/lib/Transport'; import { TransportRequestParams } from '@elastic/elasticsearch/lib/Transport'; +import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport'; import { Type } from '@kbn/config-schema'; import { TypeOf } from '@kbn/config-schema'; import { UpdateDocumentByQueryParams } from 'elasticsearch'; From efa1795cfddd76f0a7644546072af601481abc20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Tue, 21 Jul 2020 09:09:30 +0100 Subject: [PATCH 019/101] fixing error occurences tooltip (#72425) --- .../components/app/ErrorGroupDetails/Distribution/index.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx index aa95918939dfac..80c749e58c88c7 100644 --- a/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ErrorGroupDetails/Distribution/index.tsx @@ -12,7 +12,6 @@ import d3 from 'd3'; import { scaleUtc } from 'd3-scale'; import { mean } from 'lodash'; import React from 'react'; -import { px } from '../../../../style/variables'; import { asRelativeDateTimeRange } from '../../../../utils/formatters'; import { getTimezoneOffsetInMs } from '../../../shared/charts/CustomPlot/getTimezoneOffsetInMs'; // @ts-ignore @@ -89,7 +88,7 @@ export function ErrorDistribution({ distribution, title }: Props) { {title} bucket.x} From 20c6d9fe524789b450bbda03222f55d0cb927656 Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Tue, 21 Jul 2020 10:35:31 +0200 Subject: [PATCH 020/101] [SIEM][Detections] Updates text for severity and risk_score overrides (#72244) * updates severity mapping description text * updates risk score mapping description * updates default messages with the given suggestions Co-authored-by: Elastic Machine --- .../components/rules/risk_score_mapping/translations.tsx | 2 +- .../components/rules/severity_mapping/translations.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/translations.tsx index 24e82a8f95a6b6..c64a1e6891a444 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/translations.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/risk_score_mapping/translations.tsx @@ -51,7 +51,7 @@ export const RISK_SCORE_DESCRIPTION = i18n.translate( export const RISK_SCORE_MAPPING_DESCRIPTION = i18n.translate( 'xpack.securitySolution.alerts.riskScoreMapping.mappingDescriptionLabel', { - defaultMessage: 'Map a field from the source event (scaled 1-100) to risk score.', + defaultMessage: 'Use a source event value to override the default risk score.', } ); diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/translations.tsx index f0bfc5f4637ab1..12653ec5806bb9 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/translations.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/severity_mapping/translations.tsx @@ -51,7 +51,7 @@ export const SEVERITY_DESCRIPTION = i18n.translate( export const SEVERITY_MAPPING_DESCRIPTION = i18n.translate( 'xpack.securitySolution.alerts.severityMapping.mappingDescriptionLabel', { - defaultMessage: 'Map a value from the source event to a specific severity.', + defaultMessage: 'Use source event values to override the default severity.', } ); From 517c34a7ebf6ad6e02dab08c04155f7e0ab17956 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Tue, 21 Jul 2020 11:34:04 +0200 Subject: [PATCH 021/101] preserve 401 errors from new es client (#71248) * intercept 401 error from new client in routing layer * improvements * lint * fix mocked client construction due to 7.9-rc1 bump * use default WWW-Authenticate value when not provided by ES 401 --- .../elasticsearch/client/errors.test.ts | 82 ++++++++++++ .../server/elasticsearch/client/errors.ts | 32 +++++ .../server/elasticsearch/client/mocks.test.ts | 6 + src/core/server/elasticsearch/client/mocks.ts | 15 ++- .../core_service.test.mocks.ts | 17 ++- .../integration_tests/core_services.test.ts | 117 +++++++++++++++++- src/core/server/http/router/router.ts | 34 ++++- 7 files changed, 288 insertions(+), 15 deletions(-) create mode 100644 src/core/server/elasticsearch/client/errors.test.ts create mode 100644 src/core/server/elasticsearch/client/errors.ts diff --git a/src/core/server/elasticsearch/client/errors.test.ts b/src/core/server/elasticsearch/client/errors.test.ts new file mode 100644 index 00000000000000..35ad4ca71f48c6 --- /dev/null +++ b/src/core/server/elasticsearch/client/errors.test.ts @@ -0,0 +1,82 @@ +/* + * 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 { + ResponseError, + ConnectionError, + ConfigurationError, +} from '@elastic/elasticsearch/lib/errors'; +import { ApiResponse } from '@elastic/elasticsearch'; +import { isResponseError, isUnauthorizedError } from './errors'; + +const createApiResponseError = ({ + statusCode = 200, + headers = {}, + body = {}, +}: { + statusCode?: number; + headers?: Record; + body?: Record; +} = {}): ApiResponse => { + return { + body, + statusCode, + headers, + warnings: [], + meta: {} as any, + }; +}; + +describe('isResponseError', () => { + it('returns `true` when the input is a `ResponseError`', () => { + expect(isResponseError(new ResponseError(createApiResponseError()))).toBe(true); + }); + + it('returns `false` when the input is not a `ResponseError`', () => { + expect(isResponseError(new Error('foo'))).toBe(false); + expect(isResponseError(new ConnectionError('error', createApiResponseError()))).toBe(false); + expect(isResponseError(new ConfigurationError('foo'))).toBe(false); + }); +}); + +describe('isUnauthorizedError', () => { + it('returns true when the input is a `ResponseError` and statusCode === 401', () => { + expect( + isUnauthorizedError(new ResponseError(createApiResponseError({ statusCode: 401 }))) + ).toBe(true); + }); + + it('returns false when the input is a `ResponseError` and statusCode !== 401', () => { + expect( + isUnauthorizedError(new ResponseError(createApiResponseError({ statusCode: 200 }))) + ).toBe(false); + expect( + isUnauthorizedError(new ResponseError(createApiResponseError({ statusCode: 403 }))) + ).toBe(false); + expect( + isUnauthorizedError(new ResponseError(createApiResponseError({ statusCode: 500 }))) + ).toBe(false); + }); + + it('returns `false` when the input is not a `ResponseError`', () => { + expect(isUnauthorizedError(new Error('foo'))).toBe(false); + expect(isUnauthorizedError(new ConnectionError('error', createApiResponseError()))).toBe(false); + expect(isUnauthorizedError(new ConfigurationError('foo'))).toBe(false); + }); +}); diff --git a/src/core/server/elasticsearch/client/errors.ts b/src/core/server/elasticsearch/client/errors.ts new file mode 100644 index 00000000000000..31a27170e11559 --- /dev/null +++ b/src/core/server/elasticsearch/client/errors.ts @@ -0,0 +1,32 @@ +/* + * 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 { ResponseError } from '@elastic/elasticsearch/lib/errors'; + +export type UnauthorizedError = ResponseError & { + statusCode: 401; +}; + +export function isResponseError(error: any): error is ResponseError { + return Boolean(error.body && error.statusCode && error.headers); +} + +export function isUnauthorizedError(error: any): error is UnauthorizedError { + return isResponseError(error) && error.statusCode === 401; +} diff --git a/src/core/server/elasticsearch/client/mocks.test.ts b/src/core/server/elasticsearch/client/mocks.test.ts index b882f8d0c5d79c..a6ce95155331e2 100644 --- a/src/core/server/elasticsearch/client/mocks.test.ts +++ b/src/core/server/elasticsearch/client/mocks.test.ts @@ -49,6 +49,12 @@ describe('Mocked client', () => { expectMocked(client.close); }); + it('used EventEmitter functions should be mocked', () => { + expectMocked(client.on); + expectMocked(client.off); + expectMocked(client.once); + }); + it('`child` should be mocked and return a mocked Client', () => { expectMocked(client.child); diff --git a/src/core/server/elasticsearch/client/mocks.ts b/src/core/server/elasticsearch/client/mocks.ts index 34e83922d4d86c..ec2885dfdf922c 100644 --- a/src/core/server/elasticsearch/client/mocks.ts +++ b/src/core/server/elasticsearch/client/mocks.ts @@ -54,13 +54,20 @@ const createInternalClientMock = (): DeeplyMockedKeys => { mockify(client, omittedProps); - client.transport = { + // client got some read-only (getter) properties + // so we need to extend it to override the getter-only props. + const mock: any = { ...client }; + + mock.transport = { request: jest.fn(), }; - client.close = jest.fn().mockReturnValue(Promise.resolve()); - client.child = jest.fn().mockImplementation(() => createInternalClientMock()); + mock.close = jest.fn().mockReturnValue(Promise.resolve()); + mock.child = jest.fn().mockImplementation(() => createInternalClientMock()); + mock.on = jest.fn(); + mock.off = jest.fn(); + mock.once = jest.fn(); - return (client as unknown) as DeeplyMockedKeys; + return (mock as unknown) as DeeplyMockedKeys; }; export type ElasticSearchClientMock = DeeplyMockedKeys; diff --git a/src/core/server/http/integration_tests/core_service.test.mocks.ts b/src/core/server/http/integration_tests/core_service.test.mocks.ts index c23724b7d332fe..515dad5383c010 100644 --- a/src/core/server/http/integration_tests/core_service.test.mocks.ts +++ b/src/core/server/http/integration_tests/core_service.test.mocks.ts @@ -18,10 +18,12 @@ */ import { elasticsearchServiceMock } from '../../elasticsearch/elasticsearch_service.mock'; -export const clusterClientMock = jest.fn(); -export const clusterClientInstanceMock = elasticsearchServiceMock.createLegacyScopedClusterClient(); +export const MockLegacyScopedClusterClient = jest.fn(); +export const legacyClusterClientInstanceMock = elasticsearchServiceMock.createLegacyScopedClusterClient(); jest.doMock('../../elasticsearch/legacy/scoped_cluster_client', () => ({ - LegacyScopedClusterClient: clusterClientMock.mockImplementation(() => clusterClientInstanceMock), + LegacyScopedClusterClient: MockLegacyScopedClusterClient.mockImplementation( + () => legacyClusterClientInstanceMock + ), })); jest.doMock('elasticsearch', () => { @@ -34,3 +36,12 @@ jest.doMock('elasticsearch', () => { }, }; }); + +export const MockElasticsearchClient = jest.fn(); +jest.doMock('@elastic/elasticsearch', () => { + const real = jest.requireActual('@elastic/elasticsearch'); + return { + ...real, + Client: MockElasticsearchClient, + }; +}); diff --git a/src/core/server/http/integration_tests/core_services.test.ts b/src/core/server/http/integration_tests/core_services.test.ts index 3c5f22500e5e04..6338326626d546 100644 --- a/src/core/server/http/integration_tests/core_services.test.ts +++ b/src/core/server/http/integration_tests/core_services.test.ts @@ -17,14 +17,21 @@ * under the License. */ -import { clusterClientMock, clusterClientInstanceMock } from './core_service.test.mocks'; +import { + MockLegacyScopedClusterClient, + MockElasticsearchClient, + legacyClusterClientInstanceMock, +} from './core_service.test.mocks'; import Boom from 'boom'; import { Request } from 'hapi'; import { errors as esErrors } from 'elasticsearch'; import { LegacyElasticsearchErrorHelpers } from '../../elasticsearch/legacy'; +import { elasticsearchClientMock } from '../../elasticsearch/client/mocks'; +import { ResponseError } from '@elastic/elasticsearch/lib/errors'; import * as kbnTestServer from '../../../../test_utils/kbn_server'; +import { InternalElasticsearchServiceStart } from '../../elasticsearch'; interface User { id: string; @@ -44,6 +51,17 @@ const cookieOptions = { }; describe('http service', () => { + let esClient: ReturnType; + + beforeEach(async () => { + esClient = elasticsearchClientMock.createInternalClient(); + MockElasticsearchClient.mockImplementation(() => esClient); + }, 30000); + + afterEach(async () => { + MockElasticsearchClient.mockClear(); + }); + describe('auth', () => { let root: ReturnType; beforeEach(async () => { @@ -200,7 +218,7 @@ describe('http service', () => { }, 30000); afterEach(async () => { - clusterClientMock.mockClear(); + MockLegacyScopedClusterClient.mockClear(); await root.shutdown(); }); @@ -363,7 +381,7 @@ describe('http service', () => { }, 30000); afterEach(async () => { - clusterClientMock.mockClear(); + MockLegacyScopedClusterClient.mockClear(); await root.shutdown(); }); @@ -386,7 +404,7 @@ describe('http service', () => { await kbnTestServer.request.get(root, '/new-platform/').expect(200); // client contains authHeaders for BWC with legacy platform. - const [client] = clusterClientMock.mock.calls; + const [client] = MockLegacyScopedClusterClient.mock.calls; const [, , clientHeaders] = client; expect(clientHeaders).toEqual(authHeaders); }); @@ -410,7 +428,7 @@ describe('http service', () => { .set('Authorization', authorizationHeader) .expect(200); - const [client] = clusterClientMock.mock.calls; + const [client] = MockLegacyScopedClusterClient.mock.calls; const [, , clientHeaders] = client; expect(clientHeaders).toEqual({ authorization: authorizationHeader }); }); @@ -426,7 +444,7 @@ describe('http service', () => { }) ); - clusterClientInstanceMock.callAsCurrentUser.mockRejectedValue(authenticationError); + legacyClusterClientInstanceMock.callAsCurrentUser.mockRejectedValue(authenticationError); const router = createRouter('/new-platform'); router.get({ path: '/', validate: false }, async (context, req, res) => { @@ -441,4 +459,91 @@ describe('http service', () => { expect(response.header['www-authenticate']).toEqual('authenticate header'); }); }); + + describe('elasticsearch client', () => { + let root: ReturnType; + + beforeEach(async () => { + root = kbnTestServer.createRoot({ plugins: { initialize: false } }); + }, 30000); + + afterEach(async () => { + MockElasticsearchClient.mockClear(); + await root.shutdown(); + }); + + it('forwards unauthorized errors from elasticsearch', async () => { + const { http } = await root.setup(); + const { createRouter } = http; + // eslint-disable-next-line prefer-const + let elasticsearch: InternalElasticsearchServiceStart; + + esClient.ping.mockImplementation(() => + elasticsearchClientMock.createClientError( + new ResponseError({ + statusCode: 401, + body: { + error: { + type: 'Unauthorized', + }, + }, + warnings: [], + headers: { + 'WWW-Authenticate': 'content', + }, + meta: {} as any, + }) + ) + ); + + const router = createRouter('/new-platform'); + router.get({ path: '/', validate: false }, async (context, req, res) => { + await elasticsearch.client.asScoped(req).asInternalUser.ping(); + return res.ok(); + }); + + const coreStart = await root.start(); + elasticsearch = coreStart.elasticsearch; + + const { header } = await kbnTestServer.request.get(root, '/new-platform/').expect(401); + + expect(header['www-authenticate']).toEqual('content'); + }); + + it('uses a default value for `www-authenticate` header when ES 401 does not specify it', async () => { + const { http } = await root.setup(); + const { createRouter } = http; + // eslint-disable-next-line prefer-const + let elasticsearch: InternalElasticsearchServiceStart; + + esClient.ping.mockImplementation(() => + elasticsearchClientMock.createClientError( + new ResponseError({ + statusCode: 401, + body: { + error: { + type: 'Unauthorized', + }, + }, + warnings: [], + headers: {}, + meta: {} as any, + }) + ) + ); + + const router = createRouter('/new-platform'); + router.get({ path: '/', validate: false }, async (context, req, res) => { + await elasticsearch.client.asScoped(req).asInternalUser.ping(); + return res.ok(); + }); + + const coreStart = await root.start(); + elasticsearch = coreStart.elasticsearch; + + const { header } = await kbnTestServer.request.get(root, '/new-platform/').expect(401); + + expect(header['www-authenticate']).toEqual('Basic realm="Authorization Required"'); + }); + }); }); diff --git a/src/core/server/http/router/router.ts b/src/core/server/http/router/router.ts index 35eec746163ceb..cc5279a3961636 100644 --- a/src/core/server/http/router/router.ts +++ b/src/core/server/http/router/router.ts @@ -23,8 +23,17 @@ import Boom from 'boom'; import { isConfigSchema } from '@kbn/config-schema'; import { Logger } from '../../logging'; import { LegacyElasticsearchErrorHelpers } from '../../elasticsearch/legacy/errors'; +import { + isUnauthorizedError as isElasticsearchUnauthorizedError, + UnauthorizedError as EsNotAuthorizedError, +} from '../../elasticsearch/client/errors'; import { KibanaRequest } from './request'; -import { KibanaResponseFactory, kibanaResponseFactory, IKibanaResponse } from './response'; +import { + KibanaResponseFactory, + kibanaResponseFactory, + IKibanaResponse, + ErrorHttpResponseOptions, +} from './response'; import { RouteConfig, RouteConfigOptions, RouteMethod, validBodyOutput } from './route'; import { HapiResponseAdapter } from './response_adapter'; import { RequestHandlerContext } from '../../../server'; @@ -264,7 +273,13 @@ export class Router implements IRouter { return hapiResponseAdapter.handle(kibanaResponse); } catch (e) { this.log.error(e); - // forward 401 (boom) error from ES + // forward 401 errors from ES client + if (isElasticsearchUnauthorizedError(e)) { + return hapiResponseAdapter.handle( + kibanaResponseFactory.unauthorized(convertEsUnauthorized(e)) + ); + } + // forward 401 (boom) errors from legacy ES client if (LegacyElasticsearchErrorHelpers.isNotAuthorizedError(e)) { return e; } @@ -273,6 +288,21 @@ export class Router implements IRouter { } } +const convertEsUnauthorized = (e: EsNotAuthorizedError): ErrorHttpResponseOptions => { + const getAuthenticateHeaderValue = () => { + const header = Object.entries(e.headers).find( + ([key]) => key.toLowerCase() === 'www-authenticate' + ); + return header ? header[1] : 'Basic realm="Authorization Required"'; + }; + return { + body: e.message, + headers: { + 'www-authenticate': getAuthenticateHeaderValue(), + }, + }; +}; + type WithoutHeadArgument = T extends (first: any, ...rest: infer Params) => infer Return ? (...rest: Params) => Return : never; From c5073f4849409437bf73b8ec87616c8c98e2b59e Mon Sep 17 00:00:00 2001 From: cachedout Date: Tue, 21 Jul 2020 12:08:54 +0200 Subject: [PATCH 022/101] Archive e2e test results in ES (#72575) * Archive e2e test results in ES * Disable flaky comment feature and PR notifications * Update .ci/end2end.groovy Co-authored-by: Victor Martinez Co-authored-by: Victor Martinez --- .ci/end2end.groovy | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.ci/end2end.groovy b/.ci/end2end.groovy index 97099c6f87448c..ee117d362d59b1 100644 --- a/.ci/end2end.groovy +++ b/.ci/end2end.groovy @@ -110,6 +110,9 @@ pipeline { archiveArtifacts(allowEmptyArchive: true, artifacts: "${E2E_DIR}/kibana.log") } } + cleanup { + notifyBuildResult(notifyPRComment: false, analyzeFlakey: false, shouldNotify: false) + } } } From 81cbd13db49888d72ba75723d4c41947a0733e3e Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 21 Jul 2020 12:17:01 +0100 Subject: [PATCH 023/101] chore(NA): fix grunt task for test:coverage (#72539) --- tasks/test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tasks/test.js b/tasks/test.js index 96ec4d91db3258..09821b97fe2e85 100644 --- a/tasks/test.js +++ b/tasks/test.js @@ -48,7 +48,7 @@ module.exports = function (grunt) { grunt.task.run(['run:karmaTestServer', ...ciShardTasks]); }); - grunt.registerTask('test:coverage', ['run:testCoverageServer', 'karma:coverage']); + grunt.registerTask('test:coverage', ['run:karmaTestCoverageServer', 'karma:coverage']); grunt.registerTask('test:quick', [ 'checkPlugins', From e1ffcccb9681e24c92d2048b90b3d38da9e3980e Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Tue, 21 Jul 2020 14:45:51 +0300 Subject: [PATCH 024/101] Add inspector for VEGA (#70941) * [WIP] Add inspector for VEGA Closes: #31189 * view -> dataset * cleanup * add spec viewer * cleanup code * use rx to retrieve data from adapters * Make custom inspector adapters registerable from the visType * fix flex-box size * cleanup * remove visTypesWithoutInspector from visualize_embeddable * fix PR comments * add vega folder to sass-lint * fix jest * Update src/plugins/vis_type_vega/public/vega_inspector/components/data_viewer.tsx Co-authored-by: Wylie Conlon * use addSignalListener * cleanup * add onColumnResize handler * EuiCodeEditor -> CodeEditor * fix type_check * fix issue with pagination * fix extra vertical scroll * add area-label for EuiButtonIcon * add area-label for EuiComboBox * Design Commit - Fixing up layout trying to remove any `.eui` classes and uses flex instead of percentage - Fixing text to use `Sentence case` not `Title Case` * Wrapper around signal viewer table * fix Jest snapshot Co-authored-by: Elastic Machine Co-authored-by: Wylie Conlon Co-authored-by: cchaos --- .sass-lint.yml | 1 + src/plugins/data/public/public.api.md | 3 +- .../data/public/search/aggs/buckets/terms.ts | 2 +- .../data/public/search/expressions/esaggs.ts | 2 +- .../utils/courier_inspector_stats.ts | 7 +- src/plugins/data/server/server.api.md | 1 - .../public/application/angular/discover.js | 2 +- .../embeddable/search_embeddable.ts | 2 +- .../expressions/common/execution/execution.ts | 4 +- .../expressions/common/execution/types.ts | 6 +- src/plugins/expressions/public/loader.ts | 5 +- .../public/input_control_vis_type.ts | 1 + .../inspector/common/adapters/index.ts | 9 +- .../common/adapters/request/index.ts | 1 + .../inspector/common/adapters/types.ts | 25 +++ src/plugins/inspector/public/index.scss | 2 +- src/plugins/inspector/public/plugin.tsx | 3 +- src/plugins/inspector/public/types.ts | 12 +- .../inspector_panel.test.tsx.snap | 6 +- .../inspector/public/ui/inspector_panel.scss | 12 ++ .../public/ui/inspector_panel.test.tsx | 3 +- .../inspector/public/ui/inspector_panel.tsx | 8 +- .../inspector/public/view_registry.test.ts | 2 +- src/plugins/inspector/public/view_registry.ts | 3 +- .../views/data/components/data_view.tsx | 3 +- .../inspector/public/views/data/index.tsx | 3 +- .../inspector/public/views/requests/index.ts | 3 +- .../vis_type_markdown/public/markdown_vis.ts | 1 + .../public/timelion_vis_type.tsx | 1 + .../public/metrics_type.ts | 1 + src/plugins/vis_type_vega/kibana.json | 2 +- .../vis_type_vega/public/_vega_vis.scss | 42 ++--- .../public/data_model/search_api.ts | 36 ++++- .../public/data_model/vega_parser.test.js | 1 + .../public/data_model/vega_parser.ts | 6 +- src/plugins/vis_type_vega/public/plugin.ts | 17 +- src/plugins/vis_type_vega/public/vega_fn.ts | 18 ++- .../vega_inspector/components/data_viewer.tsx | 114 ++++++++++++++ .../public/vega_inspector/components/index.ts | 22 +++ .../components/inspector_data_grid.tsx | 144 +++++++++++++++++ .../components/signal_viewer.tsx | 72 +++++++++ .../vega_inspector/components/spec_viewer.tsx | 97 ++++++++++++ .../public/vega_inspector/index.ts | 24 +++ .../public/vega_inspector/vega_adapter.ts | 148 ++++++++++++++++++ .../vega_inspector/vega_data_inspector.scss | 18 +++ .../vega_inspector/vega_data_inspector.tsx | 74 +++++++++ .../public/vega_inspector/vega_inspector.tsx | 57 +++++++ .../public/vega_request_handler.ts | 11 +- src/plugins/vis_type_vega/public/vega_type.ts | 4 +- .../public/vega_view/vega_base_view.js | 5 + .../public/vega_view/vega_map_view.js | 2 +- .../public/vega_view/vega_view.js | 2 +- .../public/embeddable/visualize_embeddable.ts | 14 +- .../public/vis_types/base_vis_type.ts | 4 + test/functional/apps/visualize/_vega_chart.js | 5 - x-pack/plugins/maps/public/kibana_services.js | 2 +- 56 files changed, 992 insertions(+), 83 deletions(-) create mode 100644 src/plugins/inspector/common/adapters/types.ts create mode 100644 src/plugins/inspector/public/ui/inspector_panel.scss create mode 100644 src/plugins/vis_type_vega/public/vega_inspector/components/data_viewer.tsx create mode 100644 src/plugins/vis_type_vega/public/vega_inspector/components/index.ts create mode 100644 src/plugins/vis_type_vega/public/vega_inspector/components/inspector_data_grid.tsx create mode 100644 src/plugins/vis_type_vega/public/vega_inspector/components/signal_viewer.tsx create mode 100644 src/plugins/vis_type_vega/public/vega_inspector/components/spec_viewer.tsx create mode 100644 src/plugins/vis_type_vega/public/vega_inspector/index.ts create mode 100644 src/plugins/vis_type_vega/public/vega_inspector/vega_adapter.ts create mode 100644 src/plugins/vis_type_vega/public/vega_inspector/vega_data_inspector.scss create mode 100644 src/plugins/vis_type_vega/public/vega_inspector/vega_data_inspector.tsx create mode 100644 src/plugins/vis_type_vega/public/vega_inspector/vega_inspector.tsx diff --git a/.sass-lint.yml b/.sass-lint.yml index 50cbe81cc7da25..d6eaaf391de1a2 100644 --- a/.sass-lint.yml +++ b/.sass-lint.yml @@ -3,6 +3,7 @@ files: - 'src/legacy/core_plugins/metrics/**/*.s+(a|c)ss' - 'src/plugins/timelion/**/*.s+(a|c)ss' - 'src/plugins/vis_type_vislib/**/*.s+(a|c)ss' + - 'src/plugins/vis_type_vega/**/*.s+(a|c)ss' - 'src/plugins/vis_type_xy/**/*.s+(a|c)ss' - 'x-pack/plugins/canvas/**/*.s+(a|c)ss' - 'x-pack/plugins/triggers_actions_ui/**/*.s+(a|c)ss' diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 38e0416233e254..a8868c07061c3b 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -52,7 +52,6 @@ import { EuiButtonEmptyProps } from '@elastic/eui'; import { EuiComboBoxProps } from '@elastic/eui'; import { EuiConfirmModalProps } from '@elastic/eui'; import { EuiGlobalToastListToast } from '@elastic/eui'; -import { EventEmitter } from 'events'; import { ExclusiveUnion } from '@elastic/eui'; import { ExistsParams } from 'elasticsearch'; import { ExplainParams } from 'elasticsearch'; @@ -148,7 +147,7 @@ import { ReindexRethrottleParams } from 'elasticsearch'; import { RenderSearchTemplateParams } from 'elasticsearch'; import { Reporter } from '@kbn/analytics'; import { RequestAdapter } from 'src/plugins/inspector/common'; -import { RequestStatistics as RequestStatistics_2 } from 'src/plugins/inspector/common'; +import { RequestStatistics } from 'src/plugins/inspector/common'; import { Required } from '@kbn/utility-types'; import * as Rx from 'rxjs'; import { SavedObject } from 'src/core/server'; diff --git a/src/plugins/data/public/search/aggs/buckets/terms.ts b/src/plugins/data/public/search/aggs/buckets/terms.ts index d3acd33d73d013..5c8483cf213695 100644 --- a/src/plugins/data/public/search/aggs/buckets/terms.ts +++ b/src/plugins/data/public/search/aggs/buckets/terms.ts @@ -129,7 +129,7 @@ export const getTermsBucketAgg = () => const response = await nestedSearchSource.fetch({ abortSignal }); request - .stats(getResponseInspectorStats(nestedSearchSource, response)) + .stats(getResponseInspectorStats(response, nestedSearchSource)) .ok({ json: response }); resp = mergeOtherBucketAggResponse(aggConfigs, resp, response, aggConfig, filterAgg()); } diff --git a/src/plugins/data/public/search/expressions/esaggs.ts b/src/plugins/data/public/search/expressions/esaggs.ts index b01f17762b2bee..690f6b1df11c36 100644 --- a/src/plugins/data/public/search/expressions/esaggs.ts +++ b/src/plugins/data/public/search/expressions/esaggs.ts @@ -160,7 +160,7 @@ const handleCourierRequest = async ({ (searchSource as any).lastQuery = queryHash; - request.stats(getResponseInspectorStats(searchSource, response)).ok({ json: response }); + request.stats(getResponseInspectorStats(response, searchSource)).ok({ json: response }); (searchSource as any).rawResponse = response; } catch (e) { diff --git a/src/plugins/data/public/search/expressions/utils/courier_inspector_stats.ts b/src/plugins/data/public/search/expressions/utils/courier_inspector_stats.ts index 96d0aaa16f6bad..c933e8cd3e9613 100644 --- a/src/plugins/data/public/search/expressions/utils/courier_inspector_stats.ts +++ b/src/plugins/data/public/search/expressions/utils/courier_inspector_stats.ts @@ -61,10 +61,11 @@ export function getRequestInspectorStats(searchSource: ISearchSource) { /** @public */ export function getResponseInspectorStats( - searchSource: ISearchSource, - resp: SearchResponse + resp: SearchResponse, + searchSource?: ISearchSource ) { - const lastRequest = searchSource.history && searchSource.history[searchSource.history.length - 1]; + const lastRequest = + searchSource?.history && searchSource.history[searchSource.history.length - 1]; const stats: RequestStatistics = {}; if (resp && resp.took) { diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index c5d19fef9531ea..99a77ff9aeb107 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -39,7 +39,6 @@ import { DeleteTemplateParams } from 'elasticsearch'; import { DetailedPeerCertificate } from 'tls'; import { Duration } from 'moment'; import { ErrorToastOptions } from 'src/core/public/notifications'; -import { EventEmitter } from 'events'; import { ExistsParams } from 'elasticsearch'; import { ExplainParams } from 'elasticsearch'; import { FieldStatsParams } from 'elasticsearch'; diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index 9b8b32b51cfd8c..c791bdd850151c 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -874,7 +874,7 @@ function discoverController( } function onResults(resp) { - inspectorRequest.stats(getResponseInspectorStats($scope.searchSource, resp)).ok({ json: resp }); + inspectorRequest.stats(getResponseInspectorStats(resp, $scope.searchSource)).ok({ json: resp }); if (getTimeField()) { const tabifiedData = tabifyAggResponse($scope.vis.data.aggs, resp); diff --git a/src/plugins/discover/public/application/embeddable/search_embeddable.ts b/src/plugins/discover/public/application/embeddable/search_embeddable.ts index 9a3dd0d310ff75..b621017677c587 100644 --- a/src/plugins/discover/public/application/embeddable/search_embeddable.ts +++ b/src/plugins/discover/public/application/embeddable/search_embeddable.ts @@ -307,7 +307,7 @@ export class SearchEmbeddable extends Embeddable this.updateOutput({ loading: false, error: undefined }); // Log response to inspector - inspectorRequest.stats(getResponseInspectorStats(searchSource, resp)).ok({ json: resp }); + inspectorRequest.stats(getResponseInspectorStats(resp, searchSource)).ok({ json: resp }); // Apply the changes to the angular scope this.searchScope.$apply(() => { diff --git a/src/plugins/expressions/common/execution/execution.ts b/src/plugins/expressions/common/execution/execution.ts index f42ee189653098..3533500a2fbc59 100644 --- a/src/plugins/expressions/common/execution/execution.ts +++ b/src/plugins/expressions/common/execution/execution.ts @@ -23,7 +23,7 @@ import { createExecutionContainer, ExecutionContainer } from './container'; import { createError } from '../util'; import { Defer, now } from '../../../kibana_utils/common'; import { toPromise } from '../../../data/common/utils/abort_utils'; -import { RequestAdapter, DataAdapter } from '../../../inspector/common'; +import { RequestAdapter, DataAdapter, Adapters } from '../../../inspector/common'; import { isExpressionValueError, ExpressionValueError } from '../expression_types/specs/error'; import { ExpressionAstExpression, @@ -70,7 +70,7 @@ export class Execution< ExtraContext extends Record = Record, Input = unknown, Output = unknown, - InspectorAdapters = ExtraContext['inspectorAdapters'] extends object + InspectorAdapters extends Adapters = ExtraContext['inspectorAdapters'] extends object ? ExtraContext['inspectorAdapters'] : DefaultInspectorAdapters > { diff --git a/src/plugins/expressions/common/execution/types.ts b/src/plugins/expressions/common/execution/types.ts index 51538394cd125c..7c26e586fb790a 100644 --- a/src/plugins/expressions/common/execution/types.ts +++ b/src/plugins/expressions/common/execution/types.ts @@ -18,7 +18,7 @@ */ import { ExpressionType } from '../expression_types'; -import { DataAdapter, RequestAdapter } from '../../../inspector/common'; +import { Adapters, DataAdapter, RequestAdapter } from '../../../inspector/common'; import { TimeRange, Query, Filter } from '../../../data/common'; import { SavedObject, SavedObjectAttributes } from '../../../../core/public'; @@ -26,7 +26,7 @@ import { SavedObject, SavedObjectAttributes } from '../../../../core/public'; * `ExecutionContext` is an object available to all functions during a single execution; * it provides various methods to perform side-effects. */ -export interface ExecutionContext { +export interface ExecutionContext { /** * Get initial input with which execution started. */ @@ -75,7 +75,7 @@ export interface ExecutionContext { /** * Adapters used to open the inspector. */ - adapters: Adapters; + adapters: TAdapters; /** * The title that the inspector is currently using e.g. a visualization name. */ diff --git a/src/plugins/inspector/public/ui/__snapshots__/inspector_panel.test.tsx.snap b/src/plugins/inspector/public/ui/__snapshots__/inspector_panel.test.tsx.snap index 0e9560cbd79622..9ed4e60cac519e 100644 --- a/src/plugins/inspector/public/ui/__snapshots__/inspector_panel.test.tsx.snap +++ b/src/plugins/inspector/public/ui/__snapshots__/inspector_panel.test.tsx.snap @@ -306,9 +306,11 @@ exports[`InspectorPanel should render as expected 1`] = `
- +
div { + display: flex; + flex-direction: column; + + > div { + flex-grow: 1; + } + } +} diff --git a/src/plugins/inspector/public/ui/inspector_panel.test.tsx b/src/plugins/inspector/public/ui/inspector_panel.test.tsx index c482b6fa8033b0..23f698c23793b1 100644 --- a/src/plugins/inspector/public/ui/inspector_panel.test.tsx +++ b/src/plugins/inspector/public/ui/inspector_panel.test.tsx @@ -20,7 +20,8 @@ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { InspectorPanel } from './inspector_panel'; -import { Adapters, InspectorViewDescription } from '../types'; +import { InspectorViewDescription } from '../types'; +import { Adapters } from '../../common'; describe('InspectorPanel', () => { let adapters: Adapters; diff --git a/src/plugins/inspector/public/ui/inspector_panel.tsx b/src/plugins/inspector/public/ui/inspector_panel.tsx index 85705b6b74f55f..37a51257112d63 100644 --- a/src/plugins/inspector/public/ui/inspector_panel.tsx +++ b/src/plugins/inspector/public/ui/inspector_panel.tsx @@ -17,11 +17,13 @@ * under the License. */ +import './inspector_panel.scss'; import { i18n } from '@kbn/i18n'; import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { EuiFlexGroup, EuiFlexItem, EuiFlyoutHeader, EuiTitle, EuiFlyoutBody } from '@elastic/eui'; -import { Adapters, InspectorViewDescription } from '../types'; +import { InspectorViewDescription } from '../types'; +import { Adapters } from '../../common'; import { InspectorViewChooser } from './inspector_view_chooser'; function hasAdaptersChanged(oldAdapters: Adapters, newAdapters: Adapters) { @@ -122,7 +124,9 @@ export class InspectorPanel extends Component - {this.renderSelectedPanel()} + + {this.renderSelectedPanel()} + ); } diff --git a/src/plugins/inspector/public/view_registry.test.ts b/src/plugins/inspector/public/view_registry.test.ts index 542328d4f48dab..13e109f50243c4 100644 --- a/src/plugins/inspector/public/view_registry.test.ts +++ b/src/plugins/inspector/public/view_registry.test.ts @@ -20,7 +20,7 @@ import { InspectorViewRegistry } from './view_registry'; import { InspectorViewDescription } from './types'; -import { Adapters } from './types'; +import { Adapters } from '../common'; function createMockView( params: { diff --git a/src/plugins/inspector/public/view_registry.ts b/src/plugins/inspector/public/view_registry.ts index 800d917af28ca4..be84a62a117128 100644 --- a/src/plugins/inspector/public/view_registry.ts +++ b/src/plugins/inspector/public/view_registry.ts @@ -18,7 +18,8 @@ */ import { EventEmitter } from 'events'; -import { Adapters, InspectorViewDescription } from './types'; +import { InspectorViewDescription } from './types'; +import { Adapters } from '../common'; /** * @callback viewShouldShowFunc diff --git a/src/plugins/inspector/public/views/data/components/data_view.tsx b/src/plugins/inspector/public/views/data/components/data_view.tsx index e03c165d96a276..1a2b6f9922d2d0 100644 --- a/src/plugins/inspector/public/views/data/components/data_view.tsx +++ b/src/plugins/inspector/public/views/data/components/data_view.tsx @@ -30,7 +30,8 @@ import { } from '@elastic/eui'; import { DataTableFormat } from './data_table'; -import { InspectorViewProps, Adapters } from '../../../types'; +import { InspectorViewProps } from '../../../types'; +import { Adapters } from '../../../../common'; import { TabularLoaderOptions, TabularData, diff --git a/src/plugins/inspector/public/views/data/index.tsx b/src/plugins/inspector/public/views/data/index.tsx index 0cd88442bf8f87..b02e02bbe6b6b6 100644 --- a/src/plugins/inspector/public/views/data/index.tsx +++ b/src/plugins/inspector/public/views/data/index.tsx @@ -20,7 +20,8 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { DataViewComponent } from './components/data_view'; -import { Adapters, InspectorViewDescription, InspectorViewProps } from '../../types'; +import { InspectorViewDescription, InspectorViewProps } from '../../types'; +import { Adapters } from '../../../common'; import { IUiSettingsClient } from '../../../../../core/public'; export const getDataViewDescription = ( diff --git a/src/plugins/inspector/public/views/requests/index.ts b/src/plugins/inspector/public/views/requests/index.ts index 741da768727101..00a223e1e30fa4 100644 --- a/src/plugins/inspector/public/views/requests/index.ts +++ b/src/plugins/inspector/public/views/requests/index.ts @@ -19,7 +19,8 @@ import { i18n } from '@kbn/i18n'; import { RequestsViewComponent } from './components/requests_view'; -import { Adapters, InspectorViewDescription } from '../../types'; +import { InspectorViewDescription } from '../../types'; +import { Adapters } from '../../../common'; export const getRequestsViewDescription = (): InspectorViewDescription => ({ title: i18n.translate('inspector.requests.requestsTitle', { diff --git a/src/plugins/vis_type_markdown/public/markdown_vis.ts b/src/plugins/vis_type_markdown/public/markdown_vis.ts index 3309330d7527c9..089e00bb449373 100644 --- a/src/plugins/vis_type_markdown/public/markdown_vis.ts +++ b/src/plugins/vis_type_markdown/public/markdown_vis.ts @@ -66,4 +66,5 @@ export const markdownVisDefinition = { }, requestHandler: 'none', responseHandler: 'none', + inspectorAdapters: {}, }; diff --git a/src/plugins/vis_type_timelion/public/timelion_vis_type.tsx b/src/plugins/vis_type_timelion/public/timelion_vis_type.tsx index 52addb3c2d9d29..b4c90700b160f6 100644 --- a/src/plugins/vis_type_timelion/public/timelion_vis_type.tsx +++ b/src/plugins/vis_type_timelion/public/timelion_vis_type.tsx @@ -62,6 +62,7 @@ export function getTimelionVisDefinition(dependencies: TimelionVisDependencies) }, requestHandler: timelionRequestHandler, responseHandler: 'none', + inspectorAdapters: {}, options: { showIndexSelection: false, showQueryBar: false, diff --git a/src/plugins/vis_type_timeseries/public/metrics_type.ts b/src/plugins/vis_type_timeseries/public/metrics_type.ts index 649ee765cc6428..44b0334a37871f 100644 --- a/src/plugins/vis_type_timeseries/public/metrics_type.ts +++ b/src/plugins/vis_type_timeseries/public/metrics_type.ts @@ -78,5 +78,6 @@ export const metricsVisDefinition = { showIndexSelection: false, }, requestHandler: metricsRequestHandler, + inspectorAdapters: {}, responseHandler: 'none', }; diff --git a/src/plugins/vis_type_vega/kibana.json b/src/plugins/vis_type_vega/kibana.json index d7a92de627a992..7ba5f23f10564b 100644 --- a/src/plugins/vis_type_vega/kibana.json +++ b/src/plugins/vis_type_vega/kibana.json @@ -3,6 +3,6 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": ["data", "visualizations", "mapsLegacy", "expressions"], + "requiredPlugins": ["data", "visualizations", "mapsLegacy", "expressions", "inspector"], "requiredBundles": ["kibanaUtils", "kibanaReact"] } diff --git a/src/plugins/vis_type_vega/public/_vega_vis.scss b/src/plugins/vis_type_vega/public/_vega_vis.scss index 4fc6fbc326ec1c..f9468d677eeed9 100644 --- a/src/plugins/vis_type_vega/public/_vega_vis.scss +++ b/src/plugins/vis_type_vega/public/_vega_vis.scss @@ -17,6 +17,7 @@ // BUG #23514: Make sure Vega doesn't display the controls in two places .vega-bindings { + // sass-lint:disable no-important display: none !important; } } @@ -47,7 +48,7 @@ width: $euiSizeM * 10 - $euiSize; } - input[type="range"] { + input[type='range'] { width: $euiSizeM * 10; display: inline-block; vertical-align: middle; @@ -74,7 +75,7 @@ top: 0; width: 100%; margin: auto; - opacity: 0.8; + opacity: .8; z-index: 1; list-style: none; } @@ -115,25 +116,30 @@ @include euiTextTruncate; padding-top: $euiSizeXS; padding-bottom: $euiSizeXS; - } - td.key { - color: $euiColorMediumShade; - max-width: $euiSize * 10; - text-align: right; - padding-right: $euiSizeXS; - } - td.value { - max-width: $euiSizeL * 10; - text-align: left; - } + &.key { + color: $euiColorMediumShade; + max-width: $euiSize * 10; + text-align: right; + padding-right: $euiSizeXS; + } - @media only screen and (max-width: map-get($euiBreakpoints, 'm')){ - td.key { - max-width: $euiSize * 6; + &.value { + max-width: $euiSizeL * 10; + text-align: left; } - td.value { - max-width: $euiSize * 10; + } + + + @media only screen and (max-width: map-get($euiBreakpoints, 'm')) { + td { + &.key { + max-width: $euiSize * 6; + } + + &.value { + max-width: $euiSize * 10; + } } } } diff --git a/src/plugins/vis_type_vega/public/data_model/search_api.ts b/src/plugins/vis_type_vega/public/data_model/search_api.ts index c2eecf13c2d519..18387a6ab08765 100644 --- a/src/plugins/vis_type_vega/public/data_model/search_api.ts +++ b/src/plugins/vis_type_vega/public/data_model/search_api.ts @@ -18,13 +18,17 @@ */ import { combineLatest } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { map, tap } from 'rxjs/operators'; import { CoreStart, IUiSettingsClient } from 'kibana/public'; import { getSearchParamsFromRequest, SearchRequest, DataPublicPluginStart, + IEsSearchResponse, } from '../../../data/public'; +import { search as dataPluginSearch } from '../../../data/public'; +import { VegaInspectorAdapters } from '../vega_inspector'; +import { RequestResponder } from '../../../inspector/public'; export interface SearchAPIDependencies { uiSettings: IUiSettingsClient; @@ -35,26 +39,52 @@ export interface SearchAPIDependencies { export class SearchAPI { constructor( private readonly dependencies: SearchAPIDependencies, - private readonly abortSignal?: AbortSignal + private readonly abortSignal?: AbortSignal, + public readonly inspectorAdapters?: VegaInspectorAdapters ) {} search(searchRequests: SearchRequest[]) { const { search } = this.dependencies.search; + const requestResponders: any = {}; return combineLatest( searchRequests.map((request, index) => { + const requestId: number = index; const params = getSearchParamsFromRequest(request, { uiSettings: this.dependencies.uiSettings, injectedMetadata: this.dependencies.injectedMetadata, }); + if (this.inspectorAdapters) { + requestResponders[requestId] = this.inspectorAdapters.requests.start( + `#${requestId}`, + request + ); + requestResponders[requestId].json(params.body); + } + return search({ params }, { signal: this.abortSignal }).pipe( + tap((data) => this.inspectSearchResult(data, requestResponders[requestId])), map((data) => ({ - id: index, + id: requestId, rawResponse: data.rawResponse, })) ); }) ); } + + public resetSearchStats() { + if (this.inspectorAdapters) { + this.inspectorAdapters.requests.reset(); + } + } + + private inspectSearchResult(response: IEsSearchResponse, requestResponder: RequestResponder) { + if (requestResponder) { + requestResponder + .stats(dataPluginSearch.getResponseInspectorStats(response.rawResponse)) + .ok({ json: response.rawResponse }); + } + } } diff --git a/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js b/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js index 51aa4313a97b5e..e29e16e3212f43 100644 --- a/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js +++ b/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js @@ -97,6 +97,7 @@ describe('VegaParser._resolveEsQueries', () => { search: jest.fn(() => ({ toPromise: jest.fn(() => Promise.resolve(data)), })), + resetSearchStats: jest.fn(), }; }); diff --git a/src/plugins/vis_type_vega/public/data_model/vega_parser.ts b/src/plugins/vis_type_vega/public/data_model/vega_parser.ts index 17166e15407558..c867523d2b3b34 100644 --- a/src/plugins/vis_type_vega/public/data_model/vega_parser.ts +++ b/src/plugins/vis_type_vega/public/data_model/vega_parser.ts @@ -79,6 +79,7 @@ export class VegaParser { paddingHeight?: number; containerDir?: ControlsLocation | ControlsDirection; controlsDir?: ControlsLocation; + searchAPI: SearchAPI; constructor( spec: VegaSpec | string, @@ -92,10 +93,11 @@ export class VegaParser { this.error = undefined; this.warnings = []; + this.searchAPI = searchAPI; const onWarn = this._onWarning.bind(this); this._urlParsers = { - elasticsearch: new EsQueryParser(timeCache, searchAPI, filters, onWarn), + elasticsearch: new EsQueryParser(timeCache, this.searchAPI, filters, onWarn), emsfile: new EmsFileParser(serviceSettings), url: new UrlParser(onWarn), }; @@ -541,6 +543,8 @@ export class VegaParser { async _resolveDataUrls() { const pending: PendingType = {}; + this.searchAPI.resetSearchStats(); + this._findObjectDataUrls(this.spec!, (obj: Data) => { const url = obj.url; delete obj.url; diff --git a/src/plugins/vis_type_vega/public/plugin.ts b/src/plugins/vis_type_vega/public/plugin.ts index c20a104736291b..00c6b2e3c8d5bf 100644 --- a/src/plugins/vis_type_vega/public/plugin.ts +++ b/src/plugins/vis_type_vega/public/plugin.ts @@ -18,8 +18,10 @@ */ import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../core/public'; import { Plugin as ExpressionsPublicPlugin } from '../../expressions/public'; -import { Plugin as DataPublicPlugin } from '../../data/public'; +import { DataPublicPluginSetup, DataPublicPluginStart } from '../../data/public'; import { VisualizationsSetup } from '../../visualizations/public'; +import { Setup as InspectorSetup } from '../../inspector/public'; + import { setNotifications, setData, @@ -37,11 +39,13 @@ import { IServiceSettings } from '../../maps_legacy/public'; import './index.scss'; import { ConfigSchema } from '../config'; +import { getVegaInspectorView } from './vega_inspector'; + /** @internal */ export interface VegaVisualizationDependencies { core: CoreSetup; plugins: { - data: ReturnType; + data: DataPublicPluginSetup; }; serviceSettings: IServiceSettings; } @@ -50,13 +54,14 @@ export interface VegaVisualizationDependencies { export interface VegaPluginSetupDependencies { expressions: ReturnType; visualizations: VisualizationsSetup; - data: ReturnType; + inspector: InspectorSetup; + data: DataPublicPluginSetup; mapsLegacy: any; } /** @internal */ export interface VegaPluginStartDependencies { - data: ReturnType; + data: DataPublicPluginStart; } /** @internal */ @@ -69,7 +74,7 @@ export class VegaPlugin implements Plugin, void> { public async setup( core: CoreSetup, - { data, expressions, visualizations, mapsLegacy }: VegaPluginSetupDependencies + { inspector, data, expressions, visualizations, mapsLegacy }: VegaPluginSetupDependencies ) { setInjectedVars({ enableExternalUrls: this.initializerContext.config.get().enableExternalUrls, @@ -88,6 +93,8 @@ export class VegaPlugin implements Plugin, void> { serviceSettings: mapsLegacy.serviceSettings, }; + inspector.registerView(getVegaInspectorView({ uiSettings: core.uiSettings })); + expressions.registerFunction(() => createVegaFn(visualizationDependencies)); visualizations.createBaseVisualization(createVegaTypeDefinition(visualizationDependencies)); diff --git a/src/plugins/vis_type_vega/public/vega_fn.ts b/src/plugins/vis_type_vega/public/vega_fn.ts index d077aa7aee0049..c109bb3c6e90ca 100644 --- a/src/plugins/vis_type_vega/public/vega_fn.ts +++ b/src/plugins/vis_type_vega/public/vega_fn.ts @@ -19,9 +19,15 @@ import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { ExpressionFunctionDefinition, KibanaContext, Render } from '../../expressions/public'; +import { + ExecutionContext, + ExpressionFunctionDefinition, + KibanaContext, + Render, +} from '../../expressions/public'; import { VegaVisualizationDependencies } from './plugin'; import { createVegaRequestHandler } from './vega_request_handler'; +import { VegaInspectorAdapters } from './vega_inspector/index'; import { TimeRange, Query } from '../../data/public'; import { VegaParser } from './data_model/vega_parser'; @@ -42,7 +48,13 @@ interface RenderValue { export const createVegaFn = ( dependencies: VegaVisualizationDependencies -): ExpressionFunctionDefinition<'vega', Input, Arguments, Output> => ({ +): ExpressionFunctionDefinition< + 'vega', + Input, + Arguments, + Output, + ExecutionContext +> => ({ name: 'vega', type: 'render', inputTypes: ['kibana_context', 'null'], @@ -57,7 +69,7 @@ export const createVegaFn = ( }, }, async fn(input, args, context) { - const vegaRequestHandler = createVegaRequestHandler(dependencies, context.abortSignal); + const vegaRequestHandler = createVegaRequestHandler(dependencies, context); const response = await vegaRequestHandler({ timeRange: get(input, 'timeRange') as TimeRange, diff --git a/src/plugins/vis_type_vega/public/vega_inspector/components/data_viewer.tsx b/src/plugins/vis_type_vega/public/vega_inspector/components/data_viewer.tsx new file mode 100644 index 00000000000000..9b09a09eb05e0f --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_inspector/components/data_viewer.tsx @@ -0,0 +1,114 @@ +/* + * 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 React, { useState, useCallback, useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiComboBox, + EuiFlexGroup, + EuiComboBoxProps, + EuiFlexItem, + EuiSpacer, + CommonProps, +} from '@elastic/eui'; +import { VegaAdapter, InspectDataSets } from '../vega_adapter'; +import { InspectorDataGrid } from './inspector_data_grid'; + +interface DataViewerProps extends CommonProps { + vegaAdapter: VegaAdapter; +} + +const getDataGridArialabel = (view: InspectDataSets) => + i18n.translate('visTypeVega.inspector.dataViewer.gridAriaLabel', { + defaultMessage: '{name} data grid', + values: { + name: view.id, + }, + }); + +const dataSetAriaLabel = i18n.translate('visTypeVega.inspector.dataViewer.dataSetAriaLabel', { + defaultMessage: 'Data set', +}); + +export const DataViewer = ({ vegaAdapter, ...rest }: DataViewerProps) => { + const [inspectDataSets, setInspectDataSets] = useState([]); + const [selectedView, setSelectedView] = useState(); + const [dataGridAriaLabel, setDataGridAriaLabel] = useState(''); + + const onViewChange: EuiComboBoxProps['onChange'] = useCallback( + (selectedOptions) => { + const newView = inspectDataSets!.find((view) => view.id === selectedOptions[0].label); + + if (newView) { + setDataGridAriaLabel(getDataGridArialabel(newView)); + setSelectedView(newView); + } + }, + [inspectDataSets] + ); + + useEffect(() => { + const subscription = vegaAdapter.getDataSetsSubscription().subscribe((dataSets) => { + setInspectDataSets(dataSets); + }); + + return () => { + subscription.unsubscribe(); + }; + }, [vegaAdapter]); + + useEffect(() => { + if (inspectDataSets) { + if (!selectedView) { + setSelectedView(inspectDataSets[0]); + } else { + setDataGridAriaLabel(getDataGridArialabel(selectedView)); + } + } + }, [selectedView, inspectDataSets]); + + if (!selectedView) { + return null; + } + + return ( + + + + ({ + label: item.id, + }))} + aria-label={dataSetAriaLabel} + onChange={onViewChange} + isClearable={false} + singleSelection={{ asPlainText: true }} + selectedOptions={[{ label: selectedView.id }]} + /> + + + + + + ); +}; diff --git a/src/plugins/vis_type_vega/public/vega_inspector/components/index.ts b/src/plugins/vis_type_vega/public/vega_inspector/components/index.ts new file mode 100644 index 00000000000000..76e631f9ecd942 --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_inspector/components/index.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { DataViewer } from './data_viewer'; +export { SignalViewer } from './signal_viewer'; +export { SpecViewer } from './spec_viewer'; diff --git a/src/plugins/vis_type_vega/public/vega_inspector/components/inspector_data_grid.tsx b/src/plugins/vis_type_vega/public/vega_inspector/components/inspector_data_grid.tsx new file mode 100644 index 00000000000000..00f24e03d81963 --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_inspector/components/inspector_data_grid.tsx @@ -0,0 +1,144 @@ +/* + * 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 React, { useState, useCallback, useMemo, useEffect } from 'react'; +import { EuiDataGrid, EuiDataGridSorting, EuiDataGridProps } from '@elastic/eui'; +import { VegaRuntimeData } from '../vega_adapter'; + +const DEFAULT_PAGE_SIZE = 15; + +interface InspectorDataGridProps extends VegaRuntimeData { + dataGridAriaLabel: string; +} + +export const InspectorDataGrid = ({ columns, data, dataGridAriaLabel }: InspectorDataGridProps) => { + const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: DEFAULT_PAGE_SIZE }); + const onChangeItemsPerPage = useCallback( + (pageSize) => setPagination((p) => ({ ...p, pageSize, pageIndex: 0 })), + [setPagination] + ); + + const onChangePage = useCallback((pageIndex) => setPagination((p) => ({ ...p, pageIndex })), [ + setPagination, + ]); + + // Column visibility + const [visibleColumns, setVisibleColumns] = useState([]); + + useEffect( + () => { + setPagination({ + ...pagination, + pageIndex: 0, + }); + setVisibleColumns(columns.map((column) => column.id)); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [dataGridAriaLabel] + ); + + // Sorting + const [sortingColumns, setSortingColumns] = useState([]); + + const onSort = useCallback( + (newSortingColumns: EuiDataGridSorting['columns']) => { + setSortingColumns(newSortingColumns); + }, + [setSortingColumns] + ); + + let gridData = useMemo(() => { + return [...data].sort((a, b) => { + for (let i = 0; i < sortingColumns.length; i++) { + const column = sortingColumns[i]; + const aValue = a[column.id]; + const bValue = b[column.id]; + + if (aValue < bValue) return column.direction === 'asc' ? -1 : 1; + if (aValue > bValue) return column.direction === 'asc' ? 1 : -1; + } + return 0; + }); + }, [data, sortingColumns]); + + const renderCellValue = useMemo(() => { + return (({ rowIndex, columnId }) => { + let adjustedRowIndex = rowIndex; + + // If we are doing the pagination (instead of leaving that to the grid) + // then the row index must be adjusted as `data` has already been pruned to the page size + adjustedRowIndex = rowIndex - pagination.pageIndex * pagination.pageSize; + + return gridData.hasOwnProperty(adjustedRowIndex) + ? gridData[adjustedRowIndex][columnId] || null + : null; + }) as EuiDataGridProps['renderCellValue']; + }, [gridData, pagination.pageIndex, pagination.pageSize]); + + // Pagination + gridData = useMemo(() => { + const rowStart = pagination.pageIndex * pagination.pageSize; + const rowEnd = Math.min(rowStart + pagination.pageSize, gridData.length); + return gridData.slice(rowStart, rowEnd); + }, [gridData, pagination]); + + // Resize + const [columnsWidth, setColumnsWidth] = useState>({}); + + const onColumnResize: EuiDataGridProps['onColumnResize'] = useCallback( + ({ columnId, width }) => { + setColumnsWidth({ + ...columnsWidth, + [columnId]: width, + }); + }, + [columnsWidth] + ); + + return ( + { + if (columnsWidth[column.id]) { + return { + ...column, + initialWidth: columnsWidth[column.id], + }; + } + return column; + })} + columnVisibility={{ + visibleColumns, + setVisibleColumns, + }} + rowCount={data.length} + renderCellValue={renderCellValue} + sorting={{ columns: sortingColumns, onSort }} + toolbarVisibility={{ + showFullScreenSelector: false, + }} + onColumnResize={onColumnResize} + pagination={{ + ...pagination, + pageSizeOptions: [DEFAULT_PAGE_SIZE, 25, 50], + onChangeItemsPerPage, + onChangePage, + }} + /> + ); +}; diff --git a/src/plugins/vis_type_vega/public/vega_inspector/components/signal_viewer.tsx b/src/plugins/vis_type_vega/public/vega_inspector/components/signal_viewer.tsx new file mode 100644 index 00000000000000..39df004f327a46 --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_inspector/components/signal_viewer.tsx @@ -0,0 +1,72 @@ +/* + * 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 React, { useEffect, useState } from 'react'; + +import { EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { VegaAdapter, InspectSignalsSets } from '../vega_adapter'; +import { InspectorDataGrid } from './inspector_data_grid'; + +interface SignalViewerProps { + vegaAdapter: VegaAdapter; +} + +const initialSignalColumnWidth = 150; + +const signalDataGridAriaLabel = i18n.translate('visTypeVega.inspector.signalViewer.gridAriaLabel', { + defaultMessage: 'Signal values data grid', +}); + +export const SignalViewer = ({ vegaAdapter }: SignalViewerProps) => { + const [inspectSignalsSets, setInspectSignalsSets] = useState(); + + useEffect(() => { + const subscription = vegaAdapter.getSignalsSetsSubscription().subscribe((signalSets) => { + if (signalSets) { + setInspectSignalsSets(signalSets); + } + }); + return () => { + subscription.unsubscribe(); + }; + }, [vegaAdapter]); + + if (!inspectSignalsSets) { + return null; + } + + return ( +
+ + { + if (index === 0) { + return { + ...column, + initialWidth: initialSignalColumnWidth, + }; + } + return column; + })} + data={inspectSignalsSets.data} + dataGridAriaLabel={signalDataGridAriaLabel} + /> +
+ ); +}; diff --git a/src/plugins/vis_type_vega/public/vega_inspector/components/spec_viewer.tsx b/src/plugins/vis_type_vega/public/vega_inspector/components/spec_viewer.tsx new file mode 100644 index 00000000000000..54f7974960aa20 --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_inspector/components/spec_viewer.tsx @@ -0,0 +1,97 @@ +/* + * 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 React, { useEffect, useState } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { + EuiFlexItem, + EuiFlexGroup, + EuiCopy, + EuiButtonEmpty, + EuiSpacer, + CommonProps, +} from '@elastic/eui'; +import { VegaAdapter } from '../vega_adapter'; +import { CodeEditor } from '../../../../kibana_react/public'; + +interface SpecViewerProps extends CommonProps { + vegaAdapter: VegaAdapter; +} + +const copyToClipboardLabel = i18n.translate( + 'visTypeVega.inspector.specViewer.copyToClipboardLabel', + { + defaultMessage: 'Copy to clipboard', + } +); + +export const SpecViewer = ({ vegaAdapter, ...rest }: SpecViewerProps) => { + const [spec, setSpec] = useState(); + + useEffect(() => { + const subscription = vegaAdapter.getSpecSubscription().subscribe((data) => { + if (data) { + setSpec(data); + } + }); + return () => { + subscription.unsubscribe(); + }; + }, [vegaAdapter]); + + if (!spec) { + return null; + } + + return ( + + + +
+ + {(copy) => ( + + {copyToClipboardLabel} + + )} + +
+
+ + {}} + options={{ + readOnly: true, + lineNumbers: 'off', + fontSize: 12, + minimap: { + enabled: false, + }, + scrollBeyondLastLine: false, + wordWrap: 'on', + wrappingIndent: 'indent', + automaticLayout: true, + }} + /> + +
+ ); +}; diff --git a/src/plugins/vis_type_vega/public/vega_inspector/index.ts b/src/plugins/vis_type_vega/public/vega_inspector/index.ts new file mode 100644 index 00000000000000..24da27d2d742d4 --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_inspector/index.ts @@ -0,0 +1,24 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { + createInspectorAdapters, + getVegaInspectorView, + VegaInspectorAdapters, +} from './vega_inspector'; diff --git a/src/plugins/vis_type_vega/public/vega_inspector/vega_adapter.ts b/src/plugins/vis_type_vega/public/vega_inspector/vega_adapter.ts new file mode 100644 index 00000000000000..e4c536af405916 --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_inspector/vega_adapter.ts @@ -0,0 +1,148 @@ +/* + * 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 { Observable, ReplaySubject, fromEventPattern, merge, timer } from 'rxjs'; +import { map, switchMap, filter, debounce } from 'rxjs/operators'; +import { View, Runtime, Spec } from 'vega'; +import { i18n } from '@kbn/i18n'; +import { Assign } from '@kbn/utility-types'; + +interface DebugValues { + view: View; + spec: Spec; +} + +export interface VegaRuntimeData { + columns: Array<{ + id: string; + }>; + data: Array>; +} + +export type InspectDataSets = Assign; +export type InspectSignalsSets = VegaRuntimeData; + +const vegaAdapterSignalLabel = i18n.translate('visTypeVega.inspector.vegaAdapter.signal', { + defaultMessage: 'Signal', +}); + +const vegaAdapterValueLabel = i18n.translate('visTypeVega.inspector.vegaAdapter.value', { + defaultMessage: 'Value', +}); + +/** Get Runtime Scope for Vega View + * @link https://vega.github.io/vega/docs/api/debugging/#scope + **/ +const getVegaRuntimeScope = (debugValues: DebugValues) => + (debugValues.view as any)._runtime as Runtime; + +const serializeColumns = (item: Record, columns: string[]) => { + const nonSerializableFieldLabel = '(..)'; + + return columns.reduce((row: Record, column) => { + try { + const cell = item[column]; + row[column] = typeof cell === 'object' ? JSON.stringify(cell) : `${cell}`; + } catch (e) { + row[column] = nonSerializableFieldLabel; + } + return row; + }, {}); +}; + +export class VegaAdapter { + private debugValuesSubject = new ReplaySubject(); + + bindInspectValues(debugValues: DebugValues) { + this.debugValuesSubject.next(debugValues); + } + + getDataSetsSubscription(): Observable { + return this.debugValuesSubject.pipe( + filter((debugValues) => Boolean(debugValues)), + map((debugValues) => { + const runtimeScope = getVegaRuntimeScope(debugValues); + + return Object.keys(runtimeScope.data || []).reduce((acc: InspectDataSets[], key) => { + const value = runtimeScope.data[key].values.value; + + if (value && value[0]) { + const columns = Object.keys(value[0]); + acc.push({ + id: key, + columns: columns.map((column) => ({ id: column, schema: 'json' })), + data: value.map((item: Record) => serializeColumns(item, columns)), + }); + } + return acc; + }, []); + }) + ); + } + + getSignalsSetsSubscription(): Observable { + const signalsListener = this.debugValuesSubject.pipe( + filter((debugValues) => Boolean(debugValues)), + switchMap((debugValues) => { + const runtimeScope = getVegaRuntimeScope(debugValues); + + return merge( + ...Object.keys(runtimeScope.signals).map((key: string) => + fromEventPattern( + (handler) => debugValues.view.addSignalListener(key, handler), + (handler) => debugValues.view.removeSignalListener(key, handler) + ) + ) + ).pipe( + debounce((val) => timer(350)), + map(() => debugValues) + ); + }) + ); + + return merge(this.debugValuesSubject, signalsListener).pipe( + filter((debugValues) => Boolean(debugValues)), + map((debugValues) => { + const runtimeScope = getVegaRuntimeScope(debugValues); + + return { + columns: [ + { id: vegaAdapterSignalLabel, schema: 'text' }, + { id: vegaAdapterValueLabel, schema: 'json' }, + ], + data: Object.keys(runtimeScope.signals).map((key: string) => + serializeColumns( + { + [vegaAdapterSignalLabel]: key, + [vegaAdapterValueLabel]: runtimeScope.signals[key].value, + }, + [vegaAdapterSignalLabel, vegaAdapterValueLabel] + ) + ), + }; + }) + ); + } + + getSpecSubscription(): Observable { + return this.debugValuesSubject.pipe( + filter((debugValues) => Boolean(debugValues)), + map((debugValues) => JSON.stringify(debugValues.spec, null, 2)) + ); + } +} diff --git a/src/plugins/vis_type_vega/public/vega_inspector/vega_data_inspector.scss b/src/plugins/vis_type_vega/public/vega_inspector/vega_data_inspector.scss new file mode 100644 index 00000000000000..487f505657d3bc --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_inspector/vega_data_inspector.scss @@ -0,0 +1,18 @@ +.vgaVegaDataInspector, +.vgaVegaDataInspector__specViewer { + height: 100%; +} + +.vgaVegaDataInspector { + // TODO: EUI needs to provide props to pass down from EuiTabbedContent to tabs and content + display: flex; + flex-direction: column; + + [role='tablist'] { + flex-shrink: 0; + } + + [role='tabpanel'] { + flex-grow: 1; + } +} diff --git a/src/plugins/vis_type_vega/public/vega_inspector/vega_data_inspector.tsx b/src/plugins/vis_type_vega/public/vega_inspector/vega_data_inspector.tsx new file mode 100644 index 00000000000000..3b9427c96e62aa --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_inspector/vega_data_inspector.tsx @@ -0,0 +1,74 @@ +/* + * 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 './vega_data_inspector.scss'; + +import React from 'react'; +import { EuiTabbedContent } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; +import { VegaInspectorAdapters } from './vega_inspector'; +import { DataViewer, SignalViewer, SpecViewer } from './components'; +import { InspectorViewProps } from '../../../inspector/public'; + +export type VegaDataInspectorProps = InspectorViewProps; + +const dataSetsLabel = i18n.translate('visTypeVega.inspector.dataSetsLabel', { + defaultMessage: 'Data sets', +}); + +const signalValuesLabel = i18n.translate('visTypeVega.inspector.signalValuesLabel', { + defaultMessage: 'Signal values', +}); + +const specLabel = i18n.translate('visTypeVega.inspector.specLabel', { + defaultMessage: 'Spec', +}); + +export const VegaDataInspector = ({ adapters }: VegaDataInspectorProps) => { + const tabs = [ + { + id: 'data-viewer--id', + name: dataSetsLabel, + content: , + }, + { + id: 'signal-viewer--id', + name: signalValuesLabel, + content: , + }, + { + id: 'spec-viewer--id', + name: specLabel, + content: ( + + ), + }, + ]; + + return ( + + ); +}; diff --git a/src/plugins/vis_type_vega/public/vega_inspector/vega_inspector.tsx b/src/plugins/vis_type_vega/public/vega_inspector/vega_inspector.tsx new file mode 100644 index 00000000000000..83d9e467646a6c --- /dev/null +++ b/src/plugins/vis_type_vega/public/vega_inspector/vega_inspector.tsx @@ -0,0 +1,57 @@ +/* + * 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 React from 'react'; + +import { i18n } from '@kbn/i18n'; +import { IUiSettingsClient } from 'kibana/public'; +import { VegaAdapter } from './vega_adapter'; +import { VegaDataInspector, VegaDataInspectorProps } from './vega_data_inspector'; +import { KibanaContextProvider } from '../../../kibana_react/public'; +import { Adapters, RequestAdapter, InspectorViewDescription } from '../../../inspector/public'; + +export interface VegaInspectorAdapters extends Adapters { + requests: RequestAdapter; + vega: VegaAdapter; +} + +const vegaDebugLabel = i18n.translate('visTypeVega.inspector.vegaDebugLabel', { + defaultMessage: 'Vega debug', +}); + +interface VegaInspectorViewDependencies { + uiSettings: IUiSettingsClient; +} + +export const getVegaInspectorView = (dependencies: VegaInspectorViewDependencies) => + ({ + title: vegaDebugLabel, + shouldShow(adapters) { + return Boolean(adapters.vega); + }, + component: (props) => ( + + + + ), + } as InspectorViewDescription); + +export const createInspectorAdapters = (): VegaInspectorAdapters => ({ + requests: new RequestAdapter(), + vega: new VegaAdapter(), +}); diff --git a/src/plugins/vis_type_vega/public/vega_request_handler.ts b/src/plugins/vis_type_vega/public/vega_request_handler.ts index 997b1982d749a8..c09a9466df6028 100644 --- a/src/plugins/vis_type_vega/public/vega_request_handler.ts +++ b/src/plugins/vis_type_vega/public/vega_request_handler.ts @@ -25,6 +25,7 @@ import { TimeCache } from './data_model/time_cache'; import { VegaVisualizationDependencies } from './plugin'; import { VisParams } from './vega_fn'; import { getData, getInjectedMetadata } from './services'; +import { VegaInspectorAdapters } from './vega_inspector'; interface VegaRequestHandlerParams { query: Query; @@ -33,9 +34,14 @@ interface VegaRequestHandlerParams { visParams: VisParams; } +interface VegaRequestHandlerContext { + abortSignal?: AbortSignal; + inspectorAdapters?: VegaInspectorAdapters; +} + export function createVegaRequestHandler( { plugins: { data }, core: { uiSettings }, serviceSettings }: VegaVisualizationDependencies, - abortSignal?: AbortSignal + context: VegaRequestHandlerContext = {} ) { let searchAPI: SearchAPI; const { timefilter } = data.query.timefilter; @@ -54,7 +60,8 @@ export function createVegaRequestHandler( search: getData().search, injectedMetadata: getInjectedMetadata(), }, - abortSignal + context.abortSignal, + context.inspectorAdapters ); } diff --git a/src/plugins/vis_type_vega/public/vega_type.ts b/src/plugins/vis_type_vega/public/vega_type.ts index 5825661f9001c8..d69eb3cfba282d 100644 --- a/src/plugins/vis_type_vega/public/vega_type.ts +++ b/src/plugins/vis_type_vega/public/vega_type.ts @@ -23,9 +23,10 @@ import { VegaVisualizationDependencies } from './plugin'; import { VegaVisEditor } from './components'; import { createVegaRequestHandler } from './vega_request_handler'; -// @ts-ignore +// @ts-expect-error import { createVegaVisualization } from './vega_visualization'; import { getDefaultSpec } from './default_spec'; +import { createInspectorAdapters } from './vega_inspector'; export const createVegaTypeDefinition = (dependencies: VegaVisualizationDependencies) => { const requestHandler = createVegaRequestHandler(dependencies); @@ -54,5 +55,6 @@ export const createVegaTypeDefinition = (dependencies: VegaVisualizationDependen showFilterBar: true, }, stage: 'experimental', + inspectorAdapters: createInspectorAdapters, }; }; diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js index 55c3606bf5e45e..8f88d5c5b20566 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js @@ -364,6 +364,11 @@ export class VegaBaseView { * Set global debug variable to simplify vega debugging in console. Show info message first time */ setDebugValues(view, spec, vlspec) { + this._parser.searchAPI.inspectorAdapters?.vega.bindInspectValues({ + view, + spec: vlspec || spec, + }); + if (window) { if (window.VEGA_DEBUG === undefined && console) { console.log('%cWelcome to Kibana Vega Plugin!', 'font-size: 16px; font-weight: bold;'); diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_map_view.js index 6908fd13a9ca1e..78ae2efdbdda57 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view.js @@ -142,7 +142,7 @@ export class VegaMapView extends VegaBaseView { }); const vegaView = vegaMapLayer.getVegaView(); - this.setDebugValues(vegaView, this._parser.spec, this._parser.vlspec); await this.setView(vegaView); + this.setDebugValues(vegaView, this._parser.spec, this._parser.vlspec); } } diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_view.js index e3455b97b7fe22..98c972ef84ccb5 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_view.js @@ -26,7 +26,6 @@ export class VegaView extends VegaBaseView { if (!this._$container) return; const view = new vega.View(vega.parse(this._parser.spec), this._vegaViewConfig); - this.setDebugValues(view, this._parser.spec, this._parser.vlspec); view.warn = this.onWarn.bind(this); view.error = this.onError.bind(this); @@ -36,5 +35,6 @@ export class VegaView extends VegaBaseView { if (this._parser.useHover) view.hover(); await this.setView(view); + this.setDebugValues(view, this._parser.spec, this._parser.vlspec); } } diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts index 2f9cda32fccdc9..749926e1abd00b 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -34,6 +34,7 @@ import { EmbeddableOutput, Embeddable, IContainer, + Adapters, } from '../../../../plugins/embeddable/public'; import { dispatchRenderComplete } from '../../../../plugins/kibana_utils/public'; import { @@ -78,8 +79,6 @@ export interface VisualizeOutput extends EmbeddableOutput { type ExpressionLoader = InstanceType; -const visTypesWithoutInspector = ['markdown', 'input_control_vis', 'metrics', 'vega', 'timelion']; - export class VisualizeEmbeddable extends Embeddable { private handler?: ExpressionLoader; private timefilter: TimefilterContract; @@ -96,6 +95,7 @@ export class VisualizeEmbeddable extends Embeddable { - if (!this.handler || visTypesWithoutInspector.includes(this.vis.type.name)) { + if (!this.handler || (this.inspectorAdapters && !Object.keys(this.inspectorAdapters).length)) { return undefined; } return this.handler.inspect(); @@ -349,6 +356,7 @@ export class VisualizeEmbeddable extends Embeddable Adapters); } export class BaseVisType { @@ -63,6 +65,7 @@ export class BaseVisType { hierarchicalData: boolean | unknown; setup?: unknown; useCustomNoDataScreen: boolean; + inspectorAdapters?: Adapters | (() => Adapters); constructor(opts: BaseVisTypeOptions) { if (!opts.icon && !opts.image) { @@ -98,6 +101,7 @@ export class BaseVisType { this.requiresSearch = this.requestHandler !== 'none'; this.hierarchicalData = opts.hierarchicalData || false; this.useCustomNoDataScreen = opts.useCustomNoDataScreen || false; + this.inspectorAdapters = opts.inspectorAdapters; } public get schemas() { diff --git a/test/functional/apps/visualize/_vega_chart.js b/test/functional/apps/visualize/_vega_chart.js index 4442e1f969b4b1..c530c6f823133b 100644 --- a/test/functional/apps/visualize/_vega_chart.js +++ b/test/functional/apps/visualize/_vega_chart.js @@ -22,7 +22,6 @@ import expect from '@kbn/expect'; export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['timePicker', 'visualize', 'visChart', 'vegaChart']); const filterBar = getService('filterBar'); - const inspector = getService('inspector'); const log = getService('log'); describe('vega chart in visualize app', () => { @@ -35,10 +34,6 @@ export default function ({ getService, getPageObjects }) { describe('vega chart', () => { describe('initial render', () => { - it('should not have inspector enabled', async function () { - await inspector.expectIsNotEnabled(); - }); - it.skip('should have some initial vega spec text', async function () { const vegaSpec = await PageObjects.vegaChart.getSpec(); expect(vegaSpec).to.contain('{').and.to.contain('data'); diff --git a/x-pack/plugins/maps/public/kibana_services.js b/x-pack/plugins/maps/public/kibana_services.js index 53e128f94dfb6d..89d578f27b1187 100644 --- a/x-pack/plugins/maps/public/kibana_services.js +++ b/x-pack/plugins/maps/public/kibana_services.js @@ -80,7 +80,7 @@ export async function fetchSearchSourceAndRecordWithInspector({ inspectorRequest.json(body); }); resp = await searchSource.fetch({ abortSignal }); - inspectorRequest.stats(getResponseInspectorStats(searchSource, resp)).ok({ json: resp }); + inspectorRequest.stats(getResponseInspectorStats(resp, searchSource)).ok({ json: resp }); } catch (error) { inspectorRequest.error({ error }); throw error; From c74b214fe3815e4ebe7e12cfc804651033fd2fa2 Mon Sep 17 00:00:00 2001 From: Gil Raphaelli Date: Tue, 21 Jul 2020 08:14:12 -0400 Subject: [PATCH 025/101] allow some env settings for ingest manager (#72544) --- .../os_packages/docker_generator/resources/bin/kibana-docker | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker index 745a3d1f0c8307..0913d4ba4e83a6 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker @@ -174,6 +174,8 @@ kibana_vars=( xpack.infra.sources.default.fields.timestamp xpack.infra.sources.default.logAlias xpack.infra.sources.default.metricAlias + xpack.ingestManager.fleet.tlsCheckDisabled + xpack.ingestManager.registryUrl xpack.license_management.enabled xpack.ml.enabled xpack.reporting.capture.browser.autoDownload From 8fdebc9e822af030aa949554d00c69f3143f41ed Mon Sep 17 00:00:00 2001 From: Gidi Meir Morris Date: Tue, 21 Jul 2020 14:08:29 +0100 Subject: [PATCH 026/101] [Task Manager] Batches the update operations in Task Manager (#71470) This PR attempts to batch update tasks in Task Manager in order to avoid overloading the Elasticsearch queue. This is the 1st PR addressing https://github.com/elastic/kibana/issues/65551 Under the hood we now use a Reactive buffer accumulates all calls to the `update` api in the TaskStore and flushes after 50ms or when as many operations as there are workers have been buffered (whichever comes first). --- .../server/buffered_task_store.test.ts | 82 +++++ .../server/buffered_task_store.ts | 39 +++ .../server/lib/bulk_operation_buffer.test.ts | 288 ++++++++++++++++++ .../server/lib/bulk_operation_buffer.ts | 129 ++++++++ .../server/lib/result_type.test.ts | 27 ++ .../task_manager/server/lib/result_type.ts | 19 ++ .../task_manager/server/task_manager.ts | 10 +- .../task_manager/server/task_runner.ts | 2 +- .../task_manager/server/task_store.mock.ts | 31 ++ .../plugins/task_manager/server/task_store.ts | 65 +++- 10 files changed, 685 insertions(+), 7 deletions(-) create mode 100644 x-pack/plugins/task_manager/server/buffered_task_store.test.ts create mode 100644 x-pack/plugins/task_manager/server/buffered_task_store.ts create mode 100644 x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.test.ts create mode 100644 x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.ts create mode 100644 x-pack/plugins/task_manager/server/lib/result_type.test.ts create mode 100644 x-pack/plugins/task_manager/server/task_store.mock.ts diff --git a/x-pack/plugins/task_manager/server/buffered_task_store.test.ts b/x-pack/plugins/task_manager/server/buffered_task_store.test.ts new file mode 100644 index 00000000000000..8e18405c79ed2e --- /dev/null +++ b/x-pack/plugins/task_manager/server/buffered_task_store.test.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 uuid from 'uuid'; +import { taskStoreMock } from './task_store.mock'; +import { BufferedTaskStore } from './buffered_task_store'; +import { asErr, asOk } from './lib/result_type'; +import { TaskStatus } from './task'; + +describe('Buffered Task Store', () => { + test('proxies the TaskStore for `maxAttempts` and `remove`', async () => { + const taskStore = taskStoreMock.create({ maxAttempts: 10 }); + taskStore.bulkUpdate.mockResolvedValue([]); + const bufferedStore = new BufferedTaskStore(taskStore, {}); + + expect(bufferedStore.maxAttempts).toEqual(10); + + bufferedStore.remove('1'); + expect(taskStore.remove).toHaveBeenCalledWith('1'); + }); + + describe('update', () => { + test("proxies the TaskStore's `bulkUpdate`", async () => { + const taskStore = taskStoreMock.create({ maxAttempts: 10 }); + const bufferedStore = new BufferedTaskStore(taskStore, {}); + + const task = mockTask(); + + taskStore.bulkUpdate.mockResolvedValue([asOk(task)]); + + expect(await bufferedStore.update(task)).toMatchObject(task); + expect(taskStore.bulkUpdate).toHaveBeenCalledWith([task]); + }); + + test('handles partially successfull bulkUpdates resolving each call appropriately', async () => { + const taskStore = taskStoreMock.create({ maxAttempts: 10 }); + const bufferedStore = new BufferedTaskStore(taskStore, {}); + + const tasks = [mockTask(), mockTask(), mockTask()]; + + taskStore.bulkUpdate.mockResolvedValueOnce([ + asOk(tasks[0]), + asErr({ entity: tasks[1], error: new Error('Oh no, something went terribly wrong') }), + asOk(tasks[2]), + ]); + + const results = [ + bufferedStore.update(tasks[0]), + bufferedStore.update(tasks[1]), + bufferedStore.update(tasks[2]), + ]; + expect(await results[0]).toMatchObject(tasks[0]); + expect(results[1]).rejects.toMatchInlineSnapshot( + `[Error: Oh no, something went terribly wrong]` + ); + expect(await results[2]).toMatchObject(tasks[2]); + }); + }); +}); + +function mockTask() { + return { + id: `task_${uuid.v4()}`, + attempts: 0, + schedule: undefined, + params: { hello: 'world' }, + retryAt: null, + runAt: new Date(), + scheduledAt: new Date(), + scope: undefined, + startedAt: null, + state: { foo: 'bar' }, + status: TaskStatus.Idle, + taskType: 'report', + user: undefined, + version: '123', + ownerId: '123', + }; +} diff --git a/x-pack/plugins/task_manager/server/buffered_task_store.ts b/x-pack/plugins/task_manager/server/buffered_task_store.ts new file mode 100644 index 00000000000000..e1e5f802204c1c --- /dev/null +++ b/x-pack/plugins/task_manager/server/buffered_task_store.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 { TaskStore } from './task_store'; +import { ConcreteTaskInstance } from './task'; +import { Updatable } from './task_runner'; +import { createBuffer, Operation, BufferOptions } from './lib/bulk_operation_buffer'; +import { unwrapPromise } from './lib/result_type'; + +// by default allow updates to be buffered for up to 50ms +const DEFAULT_BUFFER_MAX_DURATION = 50; + +export class BufferedTaskStore implements Updatable { + private bufferedUpdate: Operation; + constructor(private readonly taskStore: TaskStore, options: BufferOptions) { + this.bufferedUpdate = createBuffer( + (docs) => taskStore.bulkUpdate(docs), + { + bufferMaxDuration: DEFAULT_BUFFER_MAX_DURATION, + ...options, + } + ); + } + + public get maxAttempts(): number { + return this.taskStore.maxAttempts; + } + + public async update(doc: ConcreteTaskInstance): Promise { + return unwrapPromise(this.bufferedUpdate(doc)); + } + + public async remove(id: string): Promise { + return this.taskStore.remove(id); + } +} diff --git a/x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.test.ts b/x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.test.ts new file mode 100644 index 00000000000000..92936562330263 --- /dev/null +++ b/x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.test.ts @@ -0,0 +1,288 @@ +/* + * 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 { createBuffer, Entity, OperationError, BulkOperation } from './bulk_operation_buffer'; +import { mapErr, asOk, asErr, Ok, Err } from './result_type'; + +interface TaskInstance extends Entity { + attempts: number; +} + +const createTask = (function (): () => TaskInstance { + let counter = 0; + return () => ({ + id: `task ${++counter}`, + attempts: 1, + }); +})(); + +function incrementAttempts(task: TaskInstance): Ok { + return asOk({ + ...task, + attempts: task.attempts + 1, + }); +} + +function errorAttempts(task: TaskInstance): Err> { + return asErr({ + entity: incrementAttempts(task).value, + error: { name: '', message: 'Oh no, something went terribly wrong', statusCode: 500 }, + }); +} + +describe('Bulk Operation Buffer', () => { + describe('createBuffer()', () => { + test('batches up multiple Operation calls', async () => { + const bulkUpdate: jest.Mocked> = jest.fn( + ([task1, task2]) => { + return Promise.resolve([incrementAttempts(task1), incrementAttempts(task2)]); + } + ); + + const bufferedUpdate = createBuffer(bulkUpdate); + + const task1 = createTask(); + const task2 = createTask(); + + expect(await Promise.all([bufferedUpdate(task1), bufferedUpdate(task2)])).toMatchObject([ + incrementAttempts(task1), + incrementAttempts(task2), + ]); + expect(bulkUpdate).toHaveBeenCalledWith([task1, task2]); + }); + + test('batch updates are executed at most by the next Event Loop tick by default', async () => { + const bulkUpdate: jest.Mocked> = jest.fn((tasks) => { + return Promise.resolve(tasks.map(incrementAttempts)); + }); + + const bufferedUpdate = createBuffer(bulkUpdate); + + const task1 = createTask(); + const task2 = createTask(); + const task3 = createTask(); + const task4 = createTask(); + const task5 = createTask(); + const task6 = createTask(); + + return new Promise((resolve) => { + Promise.all([bufferedUpdate(task1), bufferedUpdate(task2)]).then((_) => { + expect(bulkUpdate).toHaveBeenCalledTimes(1); + expect(bulkUpdate).toHaveBeenCalledWith([task1, task2]); + expect(bulkUpdate).not.toHaveBeenCalledWith([task3, task4]); + }); + + setTimeout(() => { + // on next tick + setTimeout(() => { + // on next tick + expect(bulkUpdate).toHaveBeenCalledTimes(2); + Promise.all([bufferedUpdate(task5), bufferedUpdate(task6)]).then((_) => { + expect(bulkUpdate).toHaveBeenCalledTimes(3); + expect(bulkUpdate).toHaveBeenCalledWith([task5, task6]); + resolve(); + }); + }, 0); + + expect(bulkUpdate).toHaveBeenCalledTimes(1); + Promise.all([bufferedUpdate(task3), bufferedUpdate(task4)]).then((_) => { + expect(bulkUpdate).toHaveBeenCalledTimes(2); + expect(bulkUpdate).toHaveBeenCalledWith([task3, task4]); + }); + }, 0); + }); + }); + + test('batch updates can be customised to execute after a certain period', async () => { + const bulkUpdate: jest.Mocked> = jest.fn((tasks) => { + return Promise.resolve(tasks.map(incrementAttempts)); + }); + + const bufferMaxDuration = 50; + const bufferedUpdate = createBuffer(bulkUpdate, { bufferMaxDuration }); + + const task1 = createTask(); + const task2 = createTask(); + const task3 = createTask(); + const task4 = createTask(); + const task5 = createTask(); + const task6 = createTask(); + + return new Promise((resolve) => { + Promise.all([bufferedUpdate(task1), bufferedUpdate(task2)]).then((_) => { + expect(bulkUpdate).toHaveBeenCalledTimes(1); + expect(bulkUpdate).toHaveBeenCalledWith([task1, task2]); + expect(bulkUpdate).not.toHaveBeenCalledWith([task3, task4]); + }); + + setTimeout(() => { + // on next tick + setTimeout(() => { + // on next tick + expect(bulkUpdate).toHaveBeenCalledTimes(2); + Promise.all([bufferedUpdate(task5), bufferedUpdate(task6)]).then((_) => { + expect(bulkUpdate).toHaveBeenCalledTimes(3); + expect(bulkUpdate).toHaveBeenCalledWith([task5, task6]); + resolve(); + }); + }, bufferMaxDuration + 1); + + expect(bulkUpdate).toHaveBeenCalledTimes(1); + Promise.all([bufferedUpdate(task3), bufferedUpdate(task4)]).then((_) => { + expect(bulkUpdate).toHaveBeenCalledTimes(2); + expect(bulkUpdate).toHaveBeenCalledWith([task3, task4]); + }); + }, bufferMaxDuration + 1); + }); + }); + + test('batch updates are executed once queue hits a certain bound', async () => { + const bulkUpdate: jest.Mocked> = jest.fn((tasks) => { + return Promise.resolve(tasks.map(incrementAttempts)); + }); + + const bufferedUpdate = createBuffer(bulkUpdate, { + bufferMaxDuration: 100, + bufferMaxOperations: 2, + }); + + const task1 = createTask(); + const task2 = createTask(); + const task3 = createTask(); + const task4 = createTask(); + const task5 = createTask(); + + return new Promise((resolve) => { + bufferedUpdate(task1); + bufferedUpdate(task2); + bufferedUpdate(task3); + bufferedUpdate(task4); + + setTimeout(() => { + expect(bulkUpdate).toHaveBeenCalledTimes(2); + expect(bulkUpdate).toHaveBeenCalledWith([task1, task2]); + expect(bulkUpdate).toHaveBeenCalledWith([task3, task4]); + + setTimeout(() => { + expect(bulkUpdate).toHaveBeenCalledTimes(2); + bufferedUpdate(task5).then((_) => { + expect(bulkUpdate).toHaveBeenCalledTimes(3); + expect(bulkUpdate).toHaveBeenCalledWith([task5]); + resolve(); + }); + }, 50); + }, 50); + }); + }); + + test('queue upper bound is reset after each flush', async () => { + const bulkUpdate: jest.Mocked> = jest.fn((tasks) => { + return Promise.resolve(tasks.map(incrementAttempts)); + }); + + const bufferMaxDuration = 100; + const bufferedUpdate = createBuffer(bulkUpdate, { + bufferMaxDuration, + bufferMaxOperations: 3, + }); + + const task1 = createTask(); + const task2 = createTask(); + const task3 = createTask(); + const task4 = createTask(); + + return new Promise((resolve) => { + bufferedUpdate(task1); + bufferedUpdate(task2); + + setTimeout(() => { + expect(bulkUpdate).toHaveBeenCalledTimes(1); + expect(bulkUpdate).toHaveBeenCalledWith([task1, task2]); + + bufferedUpdate(task3); + bufferedUpdate(task4); + + setTimeout(() => { + expect(bulkUpdate).toHaveBeenCalledTimes(1); + + setTimeout(() => { + expect(bulkUpdate).toHaveBeenCalledTimes(2); + expect(bulkUpdate).toHaveBeenCalledWith([task3, task4]); + resolve(); + }, bufferMaxDuration / 2); + }, bufferMaxDuration / 2); + }, bufferMaxDuration + 1); + }); + }); + test('handles both resolutions and rejections at individual task level', async (done) => { + const bulkUpdate: jest.Mocked> = jest.fn( + ([task1, task2, task3]) => { + return Promise.resolve([ + incrementAttempts(task1), + errorAttempts(task2), + incrementAttempts(task3), + ]); + } + ); + + const bufferedUpdate = createBuffer(bulkUpdate); + + const task1 = createTask(); + const task2 = createTask(); + const task3 = createTask(); + + return Promise.all([ + expect(bufferedUpdate(task1)).resolves.toMatchObject(incrementAttempts(task1)), + expect(bufferedUpdate(task2)).rejects.toMatchObject( + mapErr( + (err: OperationError) => asErr(err.error), + errorAttempts(task2) + ) + ), + expect(bufferedUpdate(task3)).resolves.toMatchObject(incrementAttempts(task3)), + ]).then(() => { + expect(bulkUpdate).toHaveBeenCalledTimes(1); + done(); + }); + }); + + test('handles bulkUpdate failure', async (done) => { + const bulkUpdate: jest.Mocked> = jest.fn(() => { + return Promise.reject(new Error('bulkUpdate is an illusion')); + }); + + const bufferedUpdate = createBuffer(bulkUpdate); + + const task1 = createTask(); + const task2 = createTask(); + const task3 = createTask(); + + return Promise.all([ + expect(bufferedUpdate(task1)).rejects.toMatchInlineSnapshot(` + Object { + "error": [Error: bulkUpdate is an illusion], + "tag": "err", + } + `), + expect(bufferedUpdate(task2)).rejects.toMatchInlineSnapshot(` + Object { + "error": [Error: bulkUpdate is an illusion], + "tag": "err", + } + `), + expect(bufferedUpdate(task3)).rejects.toMatchInlineSnapshot(` + Object { + "error": [Error: bulkUpdate is an illusion], + "tag": "err", + } + `), + ]).then(() => { + expect(bulkUpdate).toHaveBeenCalledTimes(1); + done(); + }); + }); + }); +}); diff --git a/x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.ts b/x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.ts new file mode 100644 index 00000000000000..fca7ce02e0cd74 --- /dev/null +++ b/x-pack/plugins/task_manager/server/lib/bulk_operation_buffer.ts @@ -0,0 +1,129 @@ +/* + * 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 { keyBy, map } from 'lodash'; +import { Subject, race, from } from 'rxjs'; +import { bufferWhen, filter, bufferCount, flatMap, mapTo, first } from 'rxjs/operators'; +import { either, Result, asOk, asErr, Ok, Err } from './result_type'; + +export interface BufferOptions { + bufferMaxDuration?: number; + bufferMaxOperations?: number; +} + +export interface Entity { + id: string; +} + +export interface OperationError { + entity: Input; + error: ErrorOutput; +} + +export type OperationResult = Result< + Output, + OperationError +>; + +export type Operation = ( + entity: Input +) => Promise>; + +export type BulkOperation = ( + entities: Input[] +) => Promise>>; + +const DONT_FLUSH = false; +const FLUSH = true; + +export function createBuffer( + bulkOperation: BulkOperation, + { bufferMaxDuration = 0, bufferMaxOperations = Number.MAX_VALUE }: BufferOptions = {} +): Operation { + const flushBuffer = new Subject(); + + const storeUpdateBuffer = new Subject<{ + entity: Input; + onSuccess: (entity: Ok) => void; + onFailure: (error: Err) => void; + }>(); + + storeUpdateBuffer + .pipe( + bufferWhen(() => flushBuffer), + filter((tasks) => tasks.length > 0) + ) + .subscribe((entities) => { + const entityById = keyBy(entities, ({ entity: { id } }) => id); + bulkOperation(map(entities, 'entity')) + .then((results) => { + results.forEach((result) => + either( + result, + (entity) => { + entityById[entity.id].onSuccess(asOk(entity)); + }, + ({ entity, error }: OperationError) => { + entityById[entity.id].onFailure(asErr(error)); + } + ) + ); + }) + .catch((ex) => { + entities.forEach(({ onFailure }) => onFailure(asErr(ex))); + }); + }); + + let countInBuffer = 0; + const flushAndResetCounter = () => { + countInBuffer = 0; + flushBuffer.next(); + }; + storeUpdateBuffer + .pipe( + // complete once the buffer has either filled to `bufferMaxOperations` or + // a `bufferMaxDuration` has passed. Default to `bufferMaxDuration` being the + // current event loop tick rather than a fixed duration + flatMap(() => { + return ++countInBuffer === 1 + ? race([ + // the race is started in response to the first operation into the buffer + // so we flush once the remaining operations come in (which is `bufferMaxOperations - 1`) + storeUpdateBuffer.pipe(bufferCount(bufferMaxOperations - 1)), + bufferMaxDuration + ? // if theres a max duration, flush buffer based on that + from(resolveIn(bufferMaxDuration)) + : // ensure we flush by the end of the "current" event loop tick + from(resolveImmediate()), + ]).pipe(first(), mapTo(FLUSH)) + : from([DONT_FLUSH]); + }), + filter((shouldFlush) => shouldFlush) + ) + .subscribe({ + next: flushAndResetCounter, + // As this stream is just trying to decide when to flush + // there's no data to lose, so in the case that an error + // is thrown, lets just flush + error: flushAndResetCounter, + }); + + return async function (entity: Input) { + return new Promise((resolve, reject) => { + storeUpdateBuffer.next({ entity, onSuccess: resolve, onFailure: reject }); + }); + }; +} + +function resolveImmediate() { + return new Promise(setImmediate); +} + +function resolveIn(ms: number) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} diff --git a/x-pack/plugins/task_manager/server/lib/result_type.test.ts b/x-pack/plugins/task_manager/server/lib/result_type.test.ts new file mode 100644 index 00000000000000..480a732f1f6170 --- /dev/null +++ b/x-pack/plugins/task_manager/server/lib/result_type.test.ts @@ -0,0 +1,27 @@ +/* + * 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 uuid from 'uuid'; +import { unwrapPromise, asOk, asErr } from './result_type'; + +describe(`Result`, () => { + describe(`unwrapPromise`, () => { + test(`unwraps OKs from the result`, async () => { + const uniqueId = uuid.v4(); + expect(await unwrapPromise(Promise.resolve(asOk(uniqueId)))).toEqual(uniqueId); + }); + + test(`unwraps Errs from the result`, async () => { + const uniqueId = uuid.v4(); + expect(unwrapPromise(Promise.resolve(asErr(uniqueId)))).rejects.toEqual(uniqueId); + }); + + test(`unwraps Errs from the result when promise rejects`, async () => { + const uniqueId = uuid.v4(); + expect(unwrapPromise(Promise.reject(asErr(uniqueId)))).rejects.toEqual(uniqueId); + }); + }); +}); diff --git a/x-pack/plugins/task_manager/server/lib/result_type.ts b/x-pack/plugins/task_manager/server/lib/result_type.ts index edf4d84dd226d8..d21c17d3bb5b3b 100644 --- a/x-pack/plugins/task_manager/server/lib/result_type.ts +++ b/x-pack/plugins/task_manager/server/lib/result_type.ts @@ -47,6 +47,25 @@ export async function promiseResult(future: Promise): Promise(future: Promise>): Promise { + return future + .catch( + // catch rejection as we expect the result of the rejected promise + // to be wrapped in a Result - sadly there's no way to "Type" this + // requirment in Typescript as Promises do not enfore a type on their + // rejection + // The `then` will then unwrap the Result from around `ex` for us + (ex: Err) => ex + ) + .then((result: Result) => + map( + result, + (value: T) => Promise.resolve(value), + (err: E) => Promise.reject(err) + ) + ); +} + export function unwrap(result: Result): T | E { return isOk(result) ? result.value : result.error; } diff --git a/x-pack/plugins/task_manager/server/task_manager.ts b/x-pack/plugins/task_manager/server/task_manager.ts index 23cb33cfac6c23..35ca439bb91305 100644 --- a/x-pack/plugins/task_manager/server/task_manager.ts +++ b/x-pack/plugins/task_manager/server/task_manager.ts @@ -57,6 +57,7 @@ import { } from './task_store'; import { identifyEsError } from './lib/identify_es_error'; import { ensureDeprecatedFieldsAreCorrected } from './lib/correct_deprecated_fields'; +import { BufferedTaskStore } from './buffered_task_store'; const VERSION_CONFLICT_STATUS = 409; @@ -90,7 +91,10 @@ export type TaskLifecycleEvent = TaskMarkRunning | TaskRun | TaskClaim | TaskRun */ export class TaskManager { private definitions: TaskDictionary = {}; + private store: TaskStore; + private bufferedStore: BufferedTaskStore; + private logger: Logger; private pool: TaskPool; // all task related events (task claimed, task marked as running, etc.) are emitted through events$ @@ -139,6 +143,10 @@ export class TaskManager { // pipe store events into the TaskManager's event stream this.store.events.subscribe((event) => this.events$.next(event)); + this.bufferedStore = new BufferedTaskStore(this.store, { + bufferMaxOperations: opts.config.max_workers, + }); + this.pool = new TaskPool({ logger: this.logger, maxWorkers: opts.config.max_workers, @@ -165,7 +173,7 @@ export class TaskManager { return new TaskManagerRunner({ logger: this.logger, instance, - store: this.store, + store: this.bufferedStore, definitions: this.definitions, beforeRun: this.middleware.beforeRun, beforeMarkRunning: this.middleware.beforeMarkRunning, diff --git a/x-pack/plugins/task_manager/server/task_runner.ts b/x-pack/plugins/task_manager/server/task_runner.ts index 4c690a5675f61b..ebf13fac2f311e 100644 --- a/x-pack/plugins/task_manager/server/task_runner.ts +++ b/x-pack/plugins/task_manager/server/task_runner.ts @@ -49,7 +49,7 @@ export interface TaskRunner { toString: () => string; } -interface Updatable { +export interface Updatable { readonly maxAttempts: number; update(doc: ConcreteTaskInstance): Promise; remove(id: string): Promise; diff --git a/x-pack/plugins/task_manager/server/task_store.mock.ts b/x-pack/plugins/task_manager/server/task_store.mock.ts new file mode 100644 index 00000000000000..86db695bc5e2cb --- /dev/null +++ b/x-pack/plugins/task_manager/server/task_store.mock.ts @@ -0,0 +1,31 @@ +/* + * 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 { TaskStore } from './task_store'; + +interface TaskStoreOptions { + maxAttempts?: number; + index?: string; + taskManagerId?: string; +} +export const taskStoreMock = { + create({ maxAttempts = 0, index = '', taskManagerId = '' }: TaskStoreOptions) { + const mocked = ({ + update: jest.fn(), + remove: jest.fn(), + schedule: jest.fn(), + claimAvailableTasks: jest.fn(), + bulkUpdate: jest.fn(), + get: jest.fn(), + getLifecycle: jest.fn(), + fetch: jest.fn(), + maxAttempts, + index, + taskManagerId, + } as unknown) as jest.Mocked; + return mocked; + }, +}; diff --git a/x-pack/plugins/task_manager/server/task_store.ts b/x-pack/plugins/task_manager/server/task_store.ts index 4a691e17011e8d..7ec3db5c99aa78 100644 --- a/x-pack/plugins/task_manager/server/task_store.ts +++ b/x-pack/plugins/task_manager/server/task_store.ts @@ -17,9 +17,10 @@ import { SavedObjectsSerializer, SavedObjectsRawDoc, ISavedObjectsRepository, + SavedObjectsUpdateResponse, } from '../../../../src/core/server'; -import { asOk, asErr } from './lib/result_type'; +import { asOk, asErr, Result } from './lib/result_type'; import { ConcreteTaskInstance, @@ -98,10 +99,10 @@ export interface ClaimOwnershipResult { docs: ConcreteTaskInstance[]; } -export interface BulkUpdateTaskFailureResult { - error: NonNullable; - task: ConcreteTaskInstance; -} +export type BulkUpdateResult = Result< + ConcreteTaskInstance, + { entity: ConcreteTaskInstance; error: Error } +>; export interface UpdateByQueryResult { updated: number; @@ -332,6 +333,54 @@ export class TaskStore { ); } + /** + * Updates the specified docs in the index, returning the docs + * with their versions up to date. + * + * @param {Array} docs + * @returns {Promise>} + */ + public async bulkUpdate(docs: ConcreteTaskInstance[]): Promise { + const attributesByDocId = docs.reduce((attrsById, doc) => { + attrsById.set(doc.id, taskInstanceToAttributes(doc)); + return attrsById; + }, new Map()); + + const updatedSavedObjects: Array = ( + await this.savedObjectsRepository.bulkUpdate( + docs.map((doc) => ({ + type: 'task', + id: doc.id, + options: { version: doc.version }, + attributes: attributesByDocId.get(doc.id)!, + })), + { + refresh: false, + } + ) + ).saved_objects; + + return updatedSavedObjects.map((updatedSavedObject, index) => + isSavedObjectsUpdateResponse(updatedSavedObject) + ? asOk( + savedObjectToConcreteTaskInstance({ + ...updatedSavedObject, + attributes: defaults( + updatedSavedObject.attributes, + attributesByDocId.get(updatedSavedObject.id)! + ), + }) + ) + : asErr({ + // The SavedObjectsRepository maintains the order of the docs + // so we can rely on the index in the `docs` to match an error + // on the same index in the `bulkUpdate` result + entity: docs[index], + error: updatedSavedObject, + }) + ); + } + /** * Removes the specified task from the index. * @@ -468,3 +517,9 @@ function ensureQueryOnlyReturnsTaskObjects(opts: SearchOpts): SearchOpts { query, }; } + +function isSavedObjectsUpdateResponse( + result: SavedObjectsUpdateResponse | Error +): result is SavedObjectsUpdateResponse { + return result && typeof (result as SavedObjectsUpdateResponse).id === 'string'; +} From c63ab91c7bfc574a563a5e0be3bab67156b8ddaa Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Tue, 21 Jul 2020 09:12:50 -0400 Subject: [PATCH 027/101] [Monitoring] Fix the messaging around needing TLS enabled (#72310) * Fix the copy * Fix type issues * PR feedback * Add missing code --- .../public/alerts/lib/security_toasts.tsx | 82 ++----------------- 1 file changed, 5 insertions(+), 77 deletions(-) diff --git a/x-pack/plugins/monitoring/public/alerts/lib/security_toasts.tsx b/x-pack/plugins/monitoring/public/alerts/lib/security_toasts.tsx index 918c0b5c9b6091..2850a5b772c322 100644 --- a/x-pack/plugins/monitoring/public/alerts/lib/security_toasts.tsx +++ b/x-pack/plugins/monitoring/public/alerts/lib/security_toasts.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiSpacer, EuiLink, EuiCode, EuiText } from '@elastic/eui'; +import { EuiSpacer, EuiLink } from '@elastic/eui'; import { Legacy } from '../../legacy_shims'; import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public'; @@ -30,11 +30,10 @@ const showTlsAndEncryptionError = () => {

{i18n.translate('xpack.monitoring.healthCheck.tlsAndEncryptionError', { - defaultMessage: `You must enable Transport Layer Security between Kibana and Elasticsearch - and configure an encryption key in your kibana.yml file to use the Alerting feature.`, + defaultMessage: `Stack monitoring alerts require Transport Layer Security between Kibana and Elasticsearch, and an encryption key in your kibana.yml file.`, })}

- + { }); }; -const showEncryptionError = () => { - const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = Legacy.shims.docLinks; - - Legacy.shims.toastNotifications.addWarning( - { - title: toMountPoint( - - ), - text: toMountPoint( -
- {i18n.translate('xpack.monitoring.healthCheck.encryptionErrorBeforeKey', { - defaultMessage: 'To create an alert, set a value for ', - })} - - {'xpack.encryptedSavedObjects.encryptionKey'} - - {i18n.translate('xpack.monitoring.healthCheck.encryptionErrorAfterKey', { - defaultMessage: ' in your kibana.yml file. ', - })} - - {i18n.translate('xpack.monitoring.healthCheck.encryptionErrorAction', { - defaultMessage: 'Learn how.', - })} - -
- ), - }, - {} - ); -}; - -const showTlsError = () => { - const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = Legacy.shims.docLinks; - - Legacy.shims.toastNotifications.addWarning({ - title: toMountPoint( - - ), - text: toMountPoint( -
- {i18n.translate('xpack.monitoring.healthCheck.tlsError', { - defaultMessage: - 'Alerting relies on API keys, which require TLS between Elasticsearch and Kibana. ', - })} - - {i18n.translate('xpack.monitoring.healthCheck.tlsErrorAction', { - defaultMessage: 'Learn how to enable TLS.', - })} - -
- ), - }); -}; - export const showSecurityToast = (alertingHealth: AlertingFrameworkHealth) => { const { isSufficientlySecure, hasPermanentEncryptionKey } = alertingHealth; + if ( Array.isArray(alertingHealth) || (!alertingHealth.hasOwnProperty('isSufficientlySecure') && @@ -127,11 +59,7 @@ export const showSecurityToast = (alertingHealth: AlertingFrameworkHealth) => { return; } - if (!isSufficientlySecure && !hasPermanentEncryptionKey) { + if (!isSufficientlySecure || !hasPermanentEncryptionKey) { showTlsAndEncryptionError(); - } else if (!isSufficientlySecure) { - showTlsError(); - } else if (!hasPermanentEncryptionKey) { - showEncryptionError(); } }; From fbf41e53795abf257caf5b44636d2ad35c4a364c Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Tue, 21 Jul 2020 14:28:10 +0100 Subject: [PATCH 028/101] [ML] Handling data recognizer saved object errors (#72447) * [ML] Handling data recognizer saved object errors * adding text for unknown errors * fixing typos --- .../plugins/ml/common/types/capabilities.ts | 2 +- x-pack/plugins/ml/common/types/modules.ts | 1 + .../recognize/components/kibana_objects.tsx | 7 ++++- .../models/data_recognizer/data_recognizer.ts | 30 +++++++++++++++++-- 4 files changed, 36 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/ml/common/types/capabilities.ts b/x-pack/plugins/ml/common/types/capabilities.ts index b46dd87eec15fc..f2177b0a3572fb 100644 --- a/x-pack/plugins/ml/common/types/capabilities.ts +++ b/x-pack/plugins/ml/common/types/capabilities.ts @@ -81,7 +81,7 @@ export function getPluginPrivileges() { catalogue: [PLUGIN_ID], savedObject: { all: [], - read: ['index-pattern', 'search'], + read: ['index-pattern', 'dashboard', 'search', 'visualization'], }, }; diff --git a/x-pack/plugins/ml/common/types/modules.ts b/x-pack/plugins/ml/common/types/modules.ts index b476762f6efca5..bfa7e38332c1bb 100644 --- a/x-pack/plugins/ml/common/types/modules.ts +++ b/x-pack/plugins/ml/common/types/modules.ts @@ -30,6 +30,7 @@ export interface KibanaObject { title: string; config: KibanaObjectConfig; exists?: boolean; + error?: any; } export interface KibanaObjects { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/kibana_objects.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/kibana_objects.tsx index 4954b44bf8842a..f8ca7926ad7d66 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/kibana_objects.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/kibana_objects.tsx @@ -46,7 +46,7 @@ export const KibanaObjects: FC = memo(