From 345a2cbfc11738af64013bf11e8a34990bc2a98b Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 10 Nov 2021 11:55:20 -0500 Subject: [PATCH 01/43] [Security Solution][Investigations] Fix overlapping tooltips (#117874) (#118187) * feat: allow to pass `tooltipPosition` to `DefaultDraggable` * fix: prevent suricata field name and google url tooltips from overlapping * test: add test for passing the tooltipPosition * chore: distinguish between type imports and regular imports * test: update suricata signature snapshots * test: make sure that suricata signature tooltips do not overlap Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Jan Monschke --- .../cypress/helpers/rules.ts | 15 +++++++++++++ .../timelines/row_renderers.spec.ts | 22 +++++++++++++++++++ .../cypress/screens/timeline.ts | 6 +++++ .../components/draggables/index.test.tsx | 17 ++++++++++++++ .../common/components/draggables/index.tsx | 19 ++++++++++++---- .../suricata_signature.test.tsx.snap | 1 + .../renderers/suricata/suricata_signature.tsx | 1 + 7 files changed, 77 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/helpers/rules.ts b/x-pack/plugins/security_solution/cypress/helpers/rules.ts index ebe357c382770..63542f9a78f84 100644 --- a/x-pack/plugins/security_solution/cypress/helpers/rules.ts +++ b/x-pack/plugins/security_solution/cypress/helpers/rules.ts @@ -20,3 +20,18 @@ export const formatMitreAttackDescription = (mitre: Mitre[]) => { ) .join(''); }; + +export const elementsOverlap = ($element1: JQuery, $element2: JQuery) => { + const rectA = $element1[0].getBoundingClientRect(); + const rectB = $element2[0].getBoundingClientRect(); + + // If they don't overlap horizontally, they don't overlap + if (rectA.right < rectB.left || rectB.right < rectA.left) { + return false; + } else if (rectA.bottom < rectB.top || rectB.bottom < rectA.top) { + // If they don't overlap vertically, they don't overlap + return false; + } else { + return true; + } +}; diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/row_renderers.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/row_renderers.spec.ts index 0755142fbdc58..2219339d0577d 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/row_renderers.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/row_renderers.spec.ts @@ -5,12 +5,16 @@ * 2.0. */ +import { elementsOverlap } from '../../helpers/rules'; import { TIMELINE_ROW_RENDERERS_DISABLE_ALL_BTN, TIMELINE_ROW_RENDERERS_MODAL_CLOSE_BUTTON, TIMELINE_ROW_RENDERERS_MODAL_ITEMS_CHECKBOX, TIMELINE_ROW_RENDERERS_SEARCHBOX, TIMELINE_SHOW_ROW_RENDERERS_GEAR, + TIMELINE_ROW_RENDERERS_SURICATA_SIGNATURE, + TIMELINE_ROW_RENDERERS_SURICATA_SIGNATURE_TOOLTIP, + TIMELINE_ROW_RENDERERS_SURICATA_LINK_TOOLTIP, } from '../../screens/timeline'; import { cleanKibana } from '../../tasks/common'; @@ -81,4 +85,22 @@ describe('Row renderers', () => { cy.wait('@updateTimeline').its('response.statusCode').should('eq', 200); }); + + describe('Suricata', () => { + it('Signature tooltips do not overlap', () => { + // Hover the signature to show the tooltips + cy.get(TIMELINE_ROW_RENDERERS_SURICATA_SIGNATURE) + .parents('.euiPopover__anchor') + .trigger('mouseover'); + + cy.get(TIMELINE_ROW_RENDERERS_SURICATA_LINK_TOOLTIP).then(($googleLinkTooltip) => { + cy.get(TIMELINE_ROW_RENDERERS_SURICATA_SIGNATURE_TOOLTIP).then(($signatureTooltip) => { + expect( + elementsOverlap($googleLinkTooltip, $signatureTooltip), + 'tooltips do not overlap' + ).to.equal(false); + }); + }); + }); + }); }); diff --git a/x-pack/plugins/security_solution/cypress/screens/timeline.ts b/x-pack/plugins/security_solution/cypress/screens/timeline.ts index f7495f3730dc4..619e7d01f10e2 100644 --- a/x-pack/plugins/security_solution/cypress/screens/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/screens/timeline.ts @@ -245,6 +245,12 @@ export const TIMELINE_ROW_RENDERERS_MODAL_ITEMS_CHECKBOX = `${TIMELINE_ROW_RENDE export const TIMELINE_ROW_RENDERERS_SEARCHBOX = `${TIMELINE_ROW_RENDERERS_MODAL} input[type="search"]`; +export const TIMELINE_ROW_RENDERERS_SURICATA_SIGNATURE = `${TIMELINE_ROW_RENDERERS_MODAL} [data-test-subj="render-content-suricata.eve.alert.signature"]`; + +export const TIMELINE_ROW_RENDERERS_SURICATA_LINK_TOOLTIP = `[data-test-subj="externalLinkTooltip"]`; + +export const TIMELINE_ROW_RENDERERS_SURICATA_SIGNATURE_TOOLTIP = `[data-test-subj="suricata.eve.alert.signature-tooltip"]`; + export const TIMELINE_SHOW_ROW_RENDERERS_GEAR = '[data-test-subj="show-row-renderers-gear"]'; export const TIMELINE_TABS = '[data-test-subj="timeline"] .euiTabs'; diff --git a/x-pack/plugins/security_solution/public/common/components/draggables/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/draggables/index.test.tsx index f77bf0f347f79..e1f052dbf83b0 100644 --- a/x-pack/plugins/security_solution/public/common/components/draggables/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/draggables/index.test.tsx @@ -7,6 +7,7 @@ import { shallow } from 'enzyme'; import React from 'react'; +import { EuiToolTip } from '@elastic/eui'; import { DRAGGABLE_KEYBOARD_INSTRUCTIONS_NOT_DRAGGING_SCREEN_READER_ONLY } from '../drag_and_drop/translations'; import { TestProviders } from '../../mock'; @@ -326,5 +327,21 @@ describe('draggables', () => { expect(wrapper.find('[data-test-subj="some-field-tooltip"]').first().exists()).toBe(false); }); + + test('it uses the specified tooltipPosition', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(EuiToolTip).first().props().position).toEqual('top'); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/draggables/index.tsx b/x-pack/plugins/security_solution/public/common/components/draggables/index.tsx index e33a8e42e6a39..26eaec4f7a76e 100644 --- a/x-pack/plugins/security_solution/public/common/components/draggables/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/draggables/index.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import { EuiBadge, EuiToolTip, IconType } from '@elastic/eui'; +import { EuiBadge, EuiToolTip } from '@elastic/eui'; +import type { IconType, ToolTipPositions } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; @@ -29,6 +30,7 @@ export interface DefaultDraggableType { children?: React.ReactNode; timelineId?: string; tooltipContent?: React.ReactNode; + tooltipPosition?: ToolTipPositions; } /** @@ -60,11 +62,13 @@ export const Content = React.memo<{ children?: React.ReactNode; field: string; tooltipContent?: React.ReactNode; + tooltipPosition?: ToolTipPositions; value?: string | null; -}>(({ children, field, tooltipContent, value }) => +}>(({ children, field, tooltipContent, tooltipPosition, value }) => !tooltipContentIsExplicitlyNull(tooltipContent) ? ( <>{children ? children : value} @@ -88,6 +92,7 @@ Content.displayName = 'Content'; * @param children - defaults to displaying `value`, this allows an arbitrary visualization to be displayed in lieu of the default behavior * @param tooltipContent - defaults to displaying `field`, pass `null` to * prevent a tooltip from being displayed, or pass arbitrary content + * @param tooltipPosition - defaults to eui's default tooltip position * @param queryValue - defaults to `value`, this query overrides the `queryMatch.value` used by the `DataProvider` that represents the data * @param hideTopN - defaults to `false`, when true, the option to aggregate this field will be hidden */ @@ -102,6 +107,7 @@ export const DefaultDraggable = React.memo( children, timelineId, tooltipContent, + tooltipPosition, queryValue, }) => { const dataProviderProp: DataProvider = useMemo( @@ -128,11 +134,16 @@ export const DefaultDraggable = React.memo( ) : ( - + {children} ), - [children, field, tooltipContent, value] + [children, field, tooltipContent, tooltipPosition, value] ); if (value == null) return null; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/__snapshots__/suricata_signature.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/__snapshots__/suricata_signature.test.tsx.snap index e55465cfd8895..c9f04ca2313a3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/__snapshots__/suricata_signature.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/__snapshots__/suricata_signature.test.tsx.snap @@ -26,6 +26,7 @@ exports[`SuricataSignature rendering it renders the default SuricataSignature 1` data-test-subj="draggable-signature-link" field="suricata.eve.alert.signature" id="suricata-signature-default-draggable-test-doc-id-123-suricata.eve.alert.signature" + tooltipPosition="bottom" value="ET SCAN ATTACK Hello" >
diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_signature.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_signature.tsx index 18e2d0844779b..ea721200730e9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_signature.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/suricata/suricata_signature.tsx @@ -130,6 +130,7 @@ export const SuricataSignature = React.memo<{ id={`suricata-signature-default-draggable-${contextId}-${id}-${SURICATA_SIGNATURE_FIELD_NAME}`} isDraggable={isDraggable} value={signature} + tooltipPosition="bottom" >
From 7c01822781fdeb8a466be4f697755b1e1780fca3 Mon Sep 17 00:00:00 2001 From: Michael Dokolin Date: Wed, 10 Nov 2021 19:22:11 +0100 Subject: [PATCH 02/43] [Reporting] Optimize visualizations awaiter performance (#118012) (#118193) --- .../chromium/driver/chromium_driver.ts | 40 +++++-------------- .../screenshots/wait_for_visualizations.ts | 29 ++++++++++---- 2 files changed, 31 insertions(+), 38 deletions(-) diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts index e7c2b68ba2712..0947d24f827c2 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts +++ b/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts @@ -210,36 +210,16 @@ export class HeadlessChromiumDriver { return resp; } - public async waitFor( - { - fn, - args, - toEqual, - timeout, - }: { - fn: EvaluateFn; - args: SerializableOrJSHandle[]; - toEqual: number; - timeout: number; - }, - context: EvaluateMetaOpts, - logger: LevelLogger - ): Promise { - const startTime = Date.now(); - - while (true) { - const result = await this.evaluate({ fn, args }, context, logger); - if (result === toEqual) { - return; - } - - if (Date.now() - startTime > timeout) { - throw new Error( - `Timed out waiting for the items selected to equal ${toEqual}. Found: ${result}. Context: ${context.context}` - ); - } - await new Promise((r) => setTimeout(r, WAIT_FOR_DELAY_MS)); - } + public async waitFor({ + fn, + args, + timeout, + }: { + fn: EvaluateFn; + args: SerializableOrJSHandle[]; + timeout: number; + }): Promise { + await this.page.waitForFunction(fn, { timeout, polling: WAIT_FOR_DELAY_MS }, ...args); } public async setViewport( diff --git a/x-pack/plugins/reporting/server/lib/screenshots/wait_for_visualizations.ts b/x-pack/plugins/reporting/server/lib/screenshots/wait_for_visualizations.ts index d4bf1db2a0c5a..10a53b238d892 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/wait_for_visualizations.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/wait_for_visualizations.ts @@ -11,10 +11,23 @@ import { HeadlessChromiumDriver } from '../../browsers'; import { LayoutInstance } from '../layouts'; import { CONTEXT_WAITFORELEMENTSTOBEINDOM } from './constants'; -type SelectorArgs = Record; +interface CompletedItemsCountParameters { + context: string; + count: number; + renderCompleteSelector: string; +} -const getCompletedItemsCount = ({ renderCompleteSelector }: SelectorArgs) => { - return document.querySelectorAll(renderCompleteSelector).length; +const getCompletedItemsCount = ({ + context, + count, + renderCompleteSelector, +}: CompletedItemsCountParameters) => { + const { length } = document.querySelectorAll(renderCompleteSelector); + + // eslint-disable-next-line no-console + console.debug(`evaluate ${context}: waitng for ${count} elements, got ${length}.`); + + return length >= count; }; /* @@ -40,11 +53,11 @@ export const waitForVisualizations = async ( ); try { - await browser.waitFor( - { fn: getCompletedItemsCount, args: [{ renderCompleteSelector }], toEqual, timeout }, - { context: CONTEXT_WAITFORELEMENTSTOBEINDOM }, - logger - ); + await browser.waitFor({ + fn: getCompletedItemsCount, + args: [{ renderCompleteSelector, context: CONTEXT_WAITFORELEMENTSTOBEINDOM, count: toEqual }], + timeout, + }); logger.debug(`found ${toEqual} rendered elements in the DOM`); } catch (err) { From eb600d460196c3084d7d4d543debf4bece83d5f7 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 10 Nov 2021 13:26:12 -0500 Subject: [PATCH 03/43] [CI] Rebalance/split cigroups and speed up overall CI time (#118191) (#118202) Co-authored-by: Brian Seeders --- .buildkite/pipelines/es_snapshots/verify.yml | 8 +++---- .buildkite/pipelines/flaky_tests/pipeline.js | 2 +- .buildkite/pipelines/flaky_tests/runner.js | 6 ++--- .buildkite/pipelines/hourly.yml | 14 +++++------ .buildkite/pipelines/pull_request/base.yml | 16 ++++++------- .../type_check_plugin_public_api_docs.sh | 24 ++++++++++++++++--- .ci/ci_groups.yml | 14 +++++++++++ test/functional/apps/management/index.ts | 2 +- test/functional/apps/visualize/index.ts | 4 ++-- .../security_and_spaces/tests/index.ts | 2 +- x-pack/test/api_integration/apis/index.ts | 4 ++-- .../api_integration/apis/security/index.ts | 2 +- .../test/api_integration/apis/spaces/index.ts | 2 +- .../security_and_spaces/tests/basic/index.ts | 2 +- .../security_and_spaces/tests/trial/index.ts | 3 +-- .../exception_operators_data_types/index.ts | 4 ++-- .../test/functional/apps/dashboard/index.ts | 2 +- x-pack/test/functional/apps/discover/index.ts | 2 +- x-pack/test/functional/apps/lens/index.ts | 13 ++++++---- x-pack/test/functional/apps/maps/index.js | 8 +++++-- x-pack/test/functional/apps/ml/index.ts | 21 +++++++++------- x-pack/test/functional/apps/security/index.ts | 2 +- .../test/functional/apps/transform/index.ts | 2 +- x-pack/test/functional_basic/apps/ml/index.ts | 2 +- .../security_and_spaces/apis/index.ts | 2 +- .../functional/tests/index.ts | 2 +- .../tests/kerberos/index.ts | 2 +- .../tests/saml/index.ts | 2 +- .../tests/session_idle/index.ts | 2 +- 29 files changed, 108 insertions(+), 63 deletions(-) diff --git a/.buildkite/pipelines/es_snapshots/verify.yml b/.buildkite/pipelines/es_snapshots/verify.yml index 9cddade0b7482..7d700b1e0f489 100755 --- a/.buildkite/pipelines/es_snapshots/verify.yml +++ b/.buildkite/pipelines/es_snapshots/verify.yml @@ -27,9 +27,9 @@ steps: - command: .buildkite/scripts/steps/functional/xpack_cigroup.sh label: 'Default CI Group' - parallelism: 13 + parallelism: 27 agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 150 key: default-cigroup @@ -41,7 +41,7 @@ steps: - command: CI_GROUP=Docker .buildkite/scripts/steps/functional/xpack_cigroup.sh label: 'Docker CI Group' agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 120 key: default-cigroup-docker @@ -77,7 +77,7 @@ steps: - command: .buildkite/scripts/steps/test/api_integration.sh label: 'API Integration Tests' agents: - queue: jest + queue: n2-2 timeout_in_minutes: 120 key: api-integration diff --git a/.buildkite/pipelines/flaky_tests/pipeline.js b/.buildkite/pipelines/flaky_tests/pipeline.js index 37e8a97406eb5..bf4abb9ff4c89 100644 --- a/.buildkite/pipelines/flaky_tests/pipeline.js +++ b/.buildkite/pipelines/flaky_tests/pipeline.js @@ -8,7 +8,7 @@ const stepInput = (key, nameOfSuite) => { }; const OSS_CI_GROUPS = 12; -const XPACK_CI_GROUPS = 13; +const XPACK_CI_GROUPS = 27; const inputs = [ { diff --git a/.buildkite/pipelines/flaky_tests/runner.js b/.buildkite/pipelines/flaky_tests/runner.js index b5ccab137fd01..0c2db5c724f7b 100644 --- a/.buildkite/pipelines/flaky_tests/runner.js +++ b/.buildkite/pipelines/flaky_tests/runner.js @@ -78,7 +78,7 @@ for (const testSuite of testSuites) { steps.push({ command: `CI_GROUP=${CI_GROUP} .buildkite/scripts/steps/functional/xpack_cigroup.sh`, label: `Default CI Group ${CI_GROUP}`, - agents: { queue: 'ci-group-6' }, + agents: { queue: 'n2-4' }, depends_on: 'build', parallelism: RUN_COUNT, concurrency: concurrency, @@ -103,7 +103,7 @@ for (const testSuite of testSuites) { steps.push({ command: `.buildkite/scripts/steps/functional/${IS_XPACK ? 'xpack' : 'oss'}_firefox.sh`, label: `${IS_XPACK ? 'Default' : 'OSS'} Firefox`, - agents: { queue: IS_XPACK ? 'ci-group-6' : 'ci-group-4d' }, + agents: { queue: IS_XPACK ? 'n2-4' : 'ci-group-4d' }, depends_on: 'build', parallelism: RUN_COUNT, concurrency: concurrency, @@ -118,7 +118,7 @@ for (const testSuite of testSuites) { IS_XPACK ? 'xpack' : 'oss' }_accessibility.sh`, label: `${IS_XPACK ? 'Default' : 'OSS'} Accessibility`, - agents: { queue: IS_XPACK ? 'ci-group-6' : 'ci-group-4d' }, + agents: { queue: IS_XPACK ? 'n2-4' : 'ci-group-4d' }, depends_on: 'build', parallelism: RUN_COUNT, concurrency: concurrency, diff --git a/.buildkite/pipelines/hourly.yml b/.buildkite/pipelines/hourly.yml index 9e9990816ad1d..bc9644820784d 100644 --- a/.buildkite/pipelines/hourly.yml +++ b/.buildkite/pipelines/hourly.yml @@ -17,9 +17,9 @@ steps: - command: .buildkite/scripts/steps/functional/xpack_cigroup.sh label: 'Default CI Group' - parallelism: 13 + parallelism: 27 agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 250 key: default-cigroup @@ -31,7 +31,7 @@ steps: - command: CI_GROUP=Docker .buildkite/scripts/steps/functional/xpack_cigroup.sh label: 'Docker CI Group' agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 120 key: default-cigroup-docker @@ -67,7 +67,7 @@ steps: - command: .buildkite/scripts/steps/functional/xpack_accessibility.sh label: 'Default Accessibility Tests' agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 120 retry: @@ -89,7 +89,7 @@ steps: - command: .buildkite/scripts/steps/functional/xpack_firefox.sh label: 'Default Firefox Tests' agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 120 retry: @@ -100,7 +100,7 @@ steps: - command: .buildkite/scripts/steps/functional/oss_misc.sh label: 'OSS Misc Functional Tests' agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 120 retry: @@ -111,7 +111,7 @@ steps: - command: .buildkite/scripts/steps/functional/xpack_saved_object_field_metrics.sh label: 'Saved Object Field Metrics' agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 120 retry: diff --git a/.buildkite/pipelines/pull_request/base.yml b/.buildkite/pipelines/pull_request/base.yml index 34db52772e619..b99473c23d746 100644 --- a/.buildkite/pipelines/pull_request/base.yml +++ b/.buildkite/pipelines/pull_request/base.yml @@ -15,9 +15,9 @@ steps: - command: .buildkite/scripts/steps/functional/xpack_cigroup.sh label: 'Default CI Group' - parallelism: 13 + parallelism: 27 agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 150 key: default-cigroup @@ -29,7 +29,7 @@ steps: - command: CI_GROUP=Docker .buildkite/scripts/steps/functional/xpack_cigroup.sh label: 'Docker CI Group' agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 120 key: default-cigroup-docker @@ -65,7 +65,7 @@ steps: - command: .buildkite/scripts/steps/functional/xpack_accessibility.sh label: 'Default Accessibility Tests' agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 120 retry: @@ -87,7 +87,7 @@ steps: - command: .buildkite/scripts/steps/functional/xpack_firefox.sh label: 'Default Firefox Tests' agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 120 retry: @@ -98,7 +98,7 @@ steps: - command: .buildkite/scripts/steps/functional/oss_misc.sh label: 'OSS Misc Functional Tests' agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 120 retry: @@ -109,7 +109,7 @@ steps: - command: .buildkite/scripts/steps/functional/xpack_saved_object_field_metrics.sh label: 'Saved Object Field Metrics' agents: - queue: ci-group-6 + queue: n2-4 depends_on: build timeout_in_minutes: 120 retry: @@ -156,7 +156,7 @@ steps: - command: .buildkite/scripts/steps/checks.sh label: 'Checks' agents: - queue: c2-4 + queue: c2-8 key: checks timeout_in_minutes: 120 diff --git a/.buildkite/scripts/steps/checks/type_check_plugin_public_api_docs.sh b/.buildkite/scripts/steps/checks/type_check_plugin_public_api_docs.sh index 1d73d1748ddf7..5827fd5eb2284 100755 --- a/.buildkite/scripts/steps/checks/type_check_plugin_public_api_docs.sh +++ b/.buildkite/scripts/steps/checks/type_check_plugin_public_api_docs.sh @@ -11,9 +11,27 @@ checks-reporter-with-killswitch "Build TS Refs" \ --no-cache \ --force -echo --- Check Types checks-reporter-with-killswitch "Check Types" \ - node scripts/type_check + node scripts/type_check &> target/check_types.log & +check_types_pid=$! + +node --max-old-space-size=12000 scripts/build_api_docs &> target/build_api_docs.log & +api_docs_pid=$! + +wait $check_types_pid +check_types_exit=$? + +wait $api_docs_pid +api_docs_exit=$? + +echo --- Check Types +cat target/check_types.log +if [[ "$check_types_exit" != "0" ]]; then echo "^^^ +++"; fi echo --- Building api docs -node --max-old-space-size=12000 scripts/build_api_docs +cat target/build_api_docs.log +if [[ "$api_docs_exit" != "0" ]]; then echo "^^^ +++"; fi + +if [[ "${api_docs_exit}${check_types_exit}" != "00" ]]; then + exit 1 +fi diff --git a/.ci/ci_groups.yml b/.ci/ci_groups.yml index 9c3a039f51166..1be6e8c196a2d 100644 --- a/.ci/ci_groups.yml +++ b/.ci/ci_groups.yml @@ -25,4 +25,18 @@ xpack: - ciGroup11 - ciGroup12 - ciGroup13 + - ciGroup14 + - ciGroup15 + - ciGroup16 + - ciGroup17 + - ciGroup18 + - ciGroup19 + - ciGroup20 + - ciGroup21 + - ciGroup22 + - ciGroup23 + - ciGroup24 + - ciGroup25 + - ciGroup26 + - ciGroup27 - ciGroupDocker diff --git a/test/functional/apps/management/index.ts b/test/functional/apps/management/index.ts index 4787d7b9ee532..c906697021ecf 100644 --- a/test/functional/apps/management/index.ts +++ b/test/functional/apps/management/index.ts @@ -22,7 +22,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { }); describe('', function () { - this.tags('ciGroup7'); + this.tags('ciGroup9'); loadTestFile(require.resolve('./_create_index_pattern_wizard')); loadTestFile(require.resolve('./_index_pattern_create_delete')); diff --git a/test/functional/apps/visualize/index.ts b/test/functional/apps/visualize/index.ts index 3bc4da0163909..68b95f3521a24 100644 --- a/test/functional/apps/visualize/index.ts +++ b/test/functional/apps/visualize/index.ts @@ -74,8 +74,8 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_metric_chart')); }); - describe('visualize ciGroup4', function () { - this.tags('ciGroup4'); + describe('visualize ciGroup1', function () { + this.tags('ciGroup1'); loadTestFile(require.resolve('./_pie_chart')); loadTestFile(require.resolve('./_shared_item')); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/index.ts index a1f15c0db75fd..211fe9ec26863 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/index.ts @@ -58,7 +58,7 @@ export async function tearDown(getService: FtrProviderContext['getService']) { // eslint-disable-next-line import/no-default-export export default function alertingApiIntegrationTests({ loadTestFile }: FtrProviderContext) { describe('alerting api integration security and spaces enabled', function () { - this.tags('ciGroup5'); + this.tags('ciGroup17'); loadTestFile(require.resolve('./actions')); loadTestFile(require.resolve('./alerting')); diff --git a/x-pack/test/api_integration/apis/index.ts b/x-pack/test/api_integration/apis/index.ts index c3d08ba306692..56b2042dc4854 100644 --- a/x-pack/test/api_integration/apis/index.ts +++ b/x-pack/test/api_integration/apis/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('apis', function () { - this.tags('ciGroup6'); + this.tags('ciGroup18'); loadTestFile(require.resolve('./search')); loadTestFile(require.resolve('./es')); @@ -27,12 +27,12 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./maps')); loadTestFile(require.resolve('./security_solution')); loadTestFile(require.resolve('./lens')); - loadTestFile(require.resolve('./ml')); loadTestFile(require.resolve('./transform')); loadTestFile(require.resolve('./lists')); loadTestFile(require.resolve('./upgrade_assistant')); loadTestFile(require.resolve('./searchprofiler')); loadTestFile(require.resolve('./painless_lab')); loadTestFile(require.resolve('./file_upload')); + loadTestFile(require.resolve('./ml')); }); } diff --git a/x-pack/test/api_integration/apis/security/index.ts b/x-pack/test/api_integration/apis/security/index.ts index e190e02d9bdea..eb81d8245dbff 100644 --- a/x-pack/test/api_integration/apis/security/index.ts +++ b/x-pack/test/api_integration/apis/security/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security', function () { - this.tags('ciGroup6'); + this.tags('ciGroup18'); // Updates here should be mirrored in `./security_basic.ts` if tests // should also run under a basic license. diff --git a/x-pack/test/api_integration/apis/spaces/index.ts b/x-pack/test/api_integration/apis/spaces/index.ts index 7267329249b22..3ca0040e39ec9 100644 --- a/x-pack/test/api_integration/apis/spaces/index.ts +++ b/x-pack/test/api_integration/apis/spaces/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('spaces', function () { - this.tags('ciGroup6'); + this.tags('ciGroup18'); loadTestFile(require.resolve('./get_active_space')); loadTestFile(require.resolve('./saved_objects')); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/basic/index.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/basic/index.ts index 3fb7d7a29af39..ce2f59a115e69 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/basic/index.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/basic/index.ts @@ -12,7 +12,7 @@ import { createSpacesAndUsers, deleteSpacesAndUsers } from '../../../common/lib/ export default ({ loadTestFile, getService }: FtrProviderContext): void => { describe('cases security and spaces enabled: basic', function () { // Fastest ciGroup for the moment. - this.tags('ciGroup13'); + this.tags('ciGroup27'); before(async () => { await createSpacesAndUsers(getService); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/index.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/index.ts index cd4b062c065a0..1605003bf7015 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/index.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/index.ts @@ -11,8 +11,7 @@ import { createSpacesAndUsers, deleteSpacesAndUsers } from '../../../common/lib/ // eslint-disable-next-line import/no-default-export export default ({ loadTestFile, getService }: FtrProviderContext): void => { describe('cases security and spaces enabled: trial', function () { - // Fastest ciGroup for the moment. - this.tags('ciGroup13'); + this.tags('ciGroup25'); before(async () => { await createSpacesAndUsers(getService); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/index.ts index cebd20b698c26..85cc484146032 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/index.ts @@ -11,7 +11,7 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext): void => { describe('Detection exceptions data types and operators', function () { describe('', function () { - this.tags('ciGroup11'); + this.tags('ciGroup23'); loadTestFile(require.resolve('./date')); loadTestFile(require.resolve('./double')); @@ -20,7 +20,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { }); describe('', function () { - this.tags('ciGroup12'); + this.tags('ciGroup24'); loadTestFile(require.resolve('./ip')); loadTestFile(require.resolve('./ip_array')); diff --git a/x-pack/test/functional/apps/dashboard/index.ts b/x-pack/test/functional/apps/dashboard/index.ts index 73c9b83de917f..59211ecf37f2d 100644 --- a/x-pack/test/functional/apps/dashboard/index.ts +++ b/x-pack/test/functional/apps/dashboard/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('dashboard', function () { - this.tags('ciGroup7'); + this.tags('ciGroup19'); loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./preserve_url')); diff --git a/x-pack/test/functional/apps/discover/index.ts b/x-pack/test/functional/apps/discover/index.ts index af117a2182034..9eda11bc6e6fb 100644 --- a/x-pack/test/functional/apps/discover/index.ts +++ b/x-pack/test/functional/apps/discover/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('discover', function () { - this.tags('ciGroup1'); + this.tags('ciGroup25'); loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./preserve_url')); diff --git a/x-pack/test/functional/apps/lens/index.ts b/x-pack/test/functional/apps/lens/index.ts index f9e4835f044af..79f9b8f645c1a 100644 --- a/x-pack/test/functional/apps/lens/index.ts +++ b/x-pack/test/functional/apps/lens/index.ts @@ -50,10 +50,6 @@ export default function ({ getService, loadTestFile, getPageObjects }: FtrProvid describe('', function () { this.tags(['ciGroup4', 'skipFirefox']); - loadTestFile(require.resolve('./add_to_dashboard')); - loadTestFile(require.resolve('./table')); - loadTestFile(require.resolve('./runtime_fields')); - loadTestFile(require.resolve('./dashboard')); loadTestFile(require.resolve('./colors')); loadTestFile(require.resolve('./chart_data')); loadTestFile(require.resolve('./time_shift')); @@ -69,5 +65,14 @@ export default function ({ getService, loadTestFile, getPageObjects }: FtrProvid // has to be last one in the suite because it overrides saved objects loadTestFile(require.resolve('./rollup')); }); + + describe('', function () { + this.tags(['ciGroup16', 'skipFirefox']); + + loadTestFile(require.resolve('./add_to_dashboard')); + loadTestFile(require.resolve('./table')); + loadTestFile(require.resolve('./runtime_fields')); + loadTestFile(require.resolve('./dashboard')); + }); }); } diff --git a/x-pack/test/functional/apps/maps/index.js b/x-pack/test/functional/apps/maps/index.js index 6a2a843682f26..b85859bf2d5d3 100644 --- a/x-pack/test/functional/apps/maps/index.js +++ b/x-pack/test/functional/apps/maps/index.js @@ -73,8 +73,13 @@ export default function ({ loadTestFile, getService }) { }); describe('', function () { - this.tags('ciGroup10'); + this.tags('ciGroup22'); loadTestFile(require.resolve('./es_geo_grid_source')); + loadTestFile(require.resolve('./embeddable')); + }); + + describe('', function () { + this.tags('ciGroup10'); loadTestFile(require.resolve('./es_pew_pew_source')); loadTestFile(require.resolve('./joins')); loadTestFile(require.resolve('./mapbox_styles')); @@ -83,7 +88,6 @@ export default function ({ loadTestFile, getService }) { loadTestFile(require.resolve('./add_layer_panel')); loadTestFile(require.resolve('./import_geojson')); loadTestFile(require.resolve('./layer_errors')); - loadTestFile(require.resolve('./embeddable')); loadTestFile(require.resolve('./visualize_create_menu')); loadTestFile(require.resolve('./discover')); }); diff --git a/x-pack/test/functional/apps/ml/index.ts b/x-pack/test/functional/apps/ml/index.ts index 2e3a29d50dd11..493813daa4f72 100644 --- a/x-pack/test/functional/apps/ml/index.ts +++ b/x-pack/test/functional/apps/ml/index.ts @@ -13,8 +13,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { describe('machine learning', function () { describe('', function () { - this.tags('ciGroup3'); - before(async () => { await ml.securityCommon.createMlRoles(); await ml.securityCommon.createMlUsers(); @@ -47,12 +45,19 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { await ml.testResources.resetKibanaTimeZone(); }); - loadTestFile(require.resolve('./permissions')); - loadTestFile(require.resolve('./pages')); - loadTestFile(require.resolve('./anomaly_detection')); - loadTestFile(require.resolve('./data_visualizer')); - loadTestFile(require.resolve('./data_frame_analytics')); - loadTestFile(require.resolve('./model_management')); + describe('', function () { + this.tags('ciGroup15'); + loadTestFile(require.resolve('./permissions')); + loadTestFile(require.resolve('./pages')); + loadTestFile(require.resolve('./data_visualizer')); + loadTestFile(require.resolve('./data_frame_analytics')); + loadTestFile(require.resolve('./model_management')); + }); + + describe('', function () { + this.tags('ciGroup26'); + loadTestFile(require.resolve('./anomaly_detection')); + }); }); describe('', function () { diff --git a/x-pack/test/functional/apps/security/index.ts b/x-pack/test/functional/apps/security/index.ts index 3b4c6989d38fa..fc9caafbabb29 100644 --- a/x-pack/test/functional/apps/security/index.ts +++ b/x-pack/test/functional/apps/security/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security app', function () { - this.tags('ciGroup4'); + this.tags('ciGroup7'); loadTestFile(require.resolve('./security')); loadTestFile(require.resolve('./doc_level_security_roles')); diff --git a/x-pack/test/functional/apps/transform/index.ts b/x-pack/test/functional/apps/transform/index.ts index 90bb95fd6b3e8..4a9aafb072852 100644 --- a/x-pack/test/functional/apps/transform/index.ts +++ b/x-pack/test/functional/apps/transform/index.ts @@ -16,7 +16,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { const transform = getService('transform'); describe('transform', function () { - this.tags(['ciGroup9', 'transform']); + this.tags(['ciGroup21', 'transform']); before(async () => { await transform.securityCommon.createTransformRoles(); diff --git a/x-pack/test/functional_basic/apps/ml/index.ts b/x-pack/test/functional_basic/apps/ml/index.ts index ed1ab4f417584..af2fdc8c45f29 100644 --- a/x-pack/test/functional_basic/apps/ml/index.ts +++ b/x-pack/test/functional_basic/apps/ml/index.ts @@ -12,7 +12,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { const ml = getService('ml'); describe('machine learning basic license', function () { - this.tags(['ciGroup2', 'skipFirefox', 'mlqa']); + this.tags(['ciGroup14', 'skipFirefox', 'mlqa']); before(async () => { await ml.securityCommon.createMlRoles(); diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts index 5412f9d9bdfed..740b9d91927bf 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts @@ -13,7 +13,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { const supertest = getService('supertest'); describe('saved objects security and spaces enabled', function () { - this.tags('ciGroup8'); + this.tags('ciGroup20'); before(async () => { await createUsersAndRoles(es, supertest); diff --git a/x-pack/test/saved_object_tagging/functional/tests/index.ts b/x-pack/test/saved_object_tagging/functional/tests/index.ts index 7a82574f34b6e..fbf0954382dd1 100644 --- a/x-pack/test/saved_object_tagging/functional/tests/index.ts +++ b/x-pack/test/saved_object_tagging/functional/tests/index.ts @@ -11,7 +11,7 @@ import { createUsersAndRoles } from '../../common/lib'; // eslint-disable-next-line import/no-default-export export default function ({ loadTestFile, getService }: FtrProviderContext) { describe('saved objects tagging - functional tests', function () { - this.tags('ciGroup2'); + this.tags('ciGroup14'); before(async () => { await createUsersAndRoles(getService); diff --git a/x-pack/test/security_api_integration/tests/kerberos/index.ts b/x-pack/test/security_api_integration/tests/kerberos/index.ts index 3faec0badd89e..39aac8cc4ca2f 100644 --- a/x-pack/test/security_api_integration/tests/kerberos/index.ts +++ b/x-pack/test/security_api_integration/tests/kerberos/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Kerberos', function () { - this.tags('ciGroup6'); + this.tags('ciGroup16'); loadTestFile(require.resolve('./kerberos_login')); }); diff --git a/x-pack/test/security_api_integration/tests/saml/index.ts b/x-pack/test/security_api_integration/tests/saml/index.ts index 375864c71432d..dbabb835ee980 100644 --- a/x-pack/test/security_api_integration/tests/saml/index.ts +++ b/x-pack/test/security_api_integration/tests/saml/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - SAML', function () { - this.tags('ciGroup6'); + this.tags('ciGroup18'); loadTestFile(require.resolve('./saml_login')); }); diff --git a/x-pack/test/security_api_integration/tests/session_idle/index.ts b/x-pack/test/security_api_integration/tests/session_idle/index.ts index bbf811de70db4..76457ee7ad0c7 100644 --- a/x-pack/test/security_api_integration/tests/session_idle/index.ts +++ b/x-pack/test/security_api_integration/tests/session_idle/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('security APIs - Session Idle', function () { - this.tags('ciGroup6'); + this.tags('ciGroup18'); loadTestFile(require.resolve('./cleanup')); loadTestFile(require.resolve('./extension')); From d99287a4a808c59558b5742e434808003c77969a Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 10 Nov 2021 13:29:33 -0500 Subject: [PATCH 04/43] [8.0] [APM] Search term with certain characters will cause APM UI to crash (#118063) (#118179) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [APM] Search term with certain characters will cause APM UI to crash (#118063) * skiping broken test Co-authored-by: Cauê Marcondes <55978943+cauemarcondes@users.noreply.github.com> Co-authored-by: cauemarcondes --- .../header_filters/generate_data.ts | 47 ++++++++++++++++ .../header_filters/header_filters.spec.ts | 54 +++++++++++++++++++ .../service_inventory.spec.ts | 2 +- .../url_params_context/resolve_url_params.ts | 2 +- 4 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/header_filters/generate_data.ts create mode 100644 x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/header_filters/header_filters.spec.ts diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/header_filters/generate_data.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/header_filters/generate_data.ts new file mode 100644 index 0000000000000..9ebaa1747d909 --- /dev/null +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/header_filters/generate_data.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { service, timerange } from '@elastic/apm-synthtrace'; + +export function generateData({ + from, + to, + specialServiceName, +}: { + from: number; + to: number; + specialServiceName: string; +}) { + const range = timerange(from, to); + + const service1 = service(specialServiceName, 'production', 'java') + .instance('service-1-prod-1') + .podId('service-1-prod-1-pod'); + + const opbeansNode = service('opbeans-node', 'production', 'nodejs').instance( + 'opbeans-node-prod-1' + ); + + return [ + ...range + .interval('2m') + .rate(1) + .flatMap((timestamp, index) => [ + ...service1 + .transaction('GET /apple 🍎 ') + .timestamp(timestamp) + .duration(1000) + .success() + .serialize(), + ...opbeansNode + .transaction('GET /banana 🍌') + .timestamp(timestamp) + .duration(500) + .success() + .serialize(), + ]), + ]; +} diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/header_filters/header_filters.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/header_filters/header_filters.spec.ts new file mode 100644 index 0000000000000..2fa8b1588a630 --- /dev/null +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/header_filters/header_filters.spec.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import url from 'url'; +import { synthtrace } from '../../../../../synthtrace'; +import { generateData } from './generate_data'; + +const start = '2021-10-10T00:00:00.000Z'; +const end = '2021-10-10T00:15:00.000Z'; + +const serviceOverviewHref = url.format({ + pathname: '/app/apm/services', + query: { rangeFrom: start, rangeTo: end }, +}); + +const specialServiceName = + 'service 1 / ? # [ ] @ ! $ & ( ) * + , ; = < > % {} | ^ ` <>'; + +describe('Service inventory - header filters', () => { + before(async () => { + await synthtrace.index( + generateData({ + from: new Date(start).getTime(), + to: new Date(end).getTime(), + specialServiceName, + }) + ); + }); + + after(async () => { + await synthtrace.clean(); + }); + + beforeEach(() => { + cy.loginAsReadOnlyUser(); + }); + + describe('Filtering by kuerybar', () => { + it('filters by service.name with special characters', () => { + cy.visit(serviceOverviewHref); + cy.contains('Services'); + cy.contains('opbeans-node'); + cy.contains('service 1'); + cy.get('[data-test-subj="headerFilterKuerybar"]') + .type(`service.name: "${specialServiceName}"`) + .type('{enter}'); + cy.contains('service 1'); + cy.url().should('include', encodeURIComponent(specialServiceName)); + }); + }); +}); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/service_inventory.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/service_inventory.spec.ts index f82510c86116b..72e43451bcfe4 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/service_inventory.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/service_inventory.spec.ts @@ -93,7 +93,7 @@ describe('When navigating to the service inventory', () => { cy.wait(aliasNames); }); - it('when selecting a different time range and clicking the refresh button', () => { + it.skip('when selecting a different time range and clicking the refresh button', () => { cy.wait(aliasNames); cy.changeTimeRange('Last 30 days'); diff --git a/x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts b/x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts index 845fdb175bb65..c37d83983a00b 100644 --- a/x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts +++ b/x-pack/plugins/apm/public/context/url_params_context/resolve_url_params.ts @@ -81,7 +81,7 @@ export function resolveUrlParams(location: Location, state: TimeUrlParams) { detailTab: toString(detailTab), flyoutDetailTab: toString(flyoutDetailTab), spanId: toNumber(spanId), - kuery: kuery && decodeURIComponent(kuery), + kuery, transactionName, transactionType, searchTerm: toString(searchTerm), From 49102b42e174a01da7556d4acaed49c1b776dcf4 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 10 Nov 2021 13:58:43 -0500 Subject: [PATCH 05/43] Update web crawler link to lead directly to the crawler page (#118111) (#118199) Co-authored-by: Vadim Yakhin --- .../sections/epm/screens/home/available_packages.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx index ca932554290bb..ff65304ca475c 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx @@ -266,7 +266,7 @@ export const AvailablePackages: React.FC = memo(() => { } - href={addBasePath('/app/enterprise_search/app_search')} + href={addBasePath('/app/enterprise_search/app_search/engines/new?method=crawler')} title={i18n.translate('xpack.fleet.featuredSearchTitle', { defaultMessage: 'Web site crawler', })} From e8b538b27ee41e83c4de6516487ea091f6bf419a Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 10 Nov 2021 14:18:35 -0500 Subject: [PATCH 06/43] Removing focusable element in tags badge (#118062) (#118204) Co-authored-by: ymao1 --- .../application/sections/alerts_list/components/alerts_list.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx index b48d5a6a3629f..162f41605e91e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx @@ -438,6 +438,7 @@ export const AlertsList: React.FunctionComponent = () => { color="hollow" iconType="tag" iconSide="left" + tabIndex={-1} onClick={() => setTagPopoverOpenIndex(item.index)} onClickAriaLabel="Tags" iconOnClick={() => setTagPopoverOpenIndex(item.index)} From a4dd96a7b927d38e6b582fd9ec9682a67c29e9d3 Mon Sep 17 00:00:00 2001 From: Catherine Liu Date: Wed, 10 Nov 2021 13:06:43 -0700 Subject: [PATCH 07/43] Revert "Revert "[Canvas] By-Value Embeddables (#113827)" (#116527)" (#117613) (#117751) * Revert "Revert "[Canvas] By-Value Embeddables (#113827)" (#116527)" This reverts commit 9e6e84571f92b4602681ea0f9171f5b8cc0643d8. * Fix ts error Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../application/top_nav/editor_menu.tsx | 2 +- .../server/collectors/management/schema.ts | 4 + .../server/collectors/management/types.ts | 1 + src/plugins/presentation_util/common/labs.ts | 17 +- .../solution_toolbar/items/quick_group.scss | 28 ++- src/plugins/telemetry/schema/oss_plugins.json | 6 + .../expression_types/embeddable.ts | 2 +- .../functions/browser/index.ts | 12 +- .../functions/external/embeddable.test.ts | 60 +++++++ .../functions/external/embeddable.ts | 145 +++++++++++++++ .../functions/external/index.ts | 19 +- .../functions/external/saved_lens.ts | 16 +- .../functions/external/saved_map.ts | 5 +- .../functions/external/saved_visualization.ts | 3 +- .../canvas/canvas_plugin_src/plugin.ts | 10 ++ .../renderers/embeddable/embeddable.tsx | 56 +++--- .../embeddable_input_to_expression.ts | 8 +- .../embeddable.test.ts | 128 +++++++++++++ .../input_type_to_expression/embeddable.ts | 13 ++ .../input_type_to_expression/lens.test.ts | 5 +- .../input_type_to_expression/lens.ts | 2 +- .../input_type_to_expression/map.test.ts | 12 +- .../input_type_to_expression/map.ts | 7 +- .../visualization.test.ts | 5 +- .../input_type_to_expression/visualization.ts | 4 +- .../canvas/common/lib/embeddable_dataurl.ts | 13 ++ .../canvas/i18n/functions/dict/embeddable.ts | 25 +++ .../canvas/i18n/functions/function_help.ts | 2 + x-pack/plugins/canvas/kibana.json | 1 + .../embeddable_flyout/flyout.component.tsx | 44 +++-- .../components/embeddable_flyout/flyout.tsx | 21 ++- .../public/components/hooks/workpad/index.tsx | 2 + .../hooks/workpad/use_incoming_embeddable.ts | 86 +++++++++ .../public/components/workpad/workpad.tsx | 4 + .../components/workpad_app/workpad_app.scss | 2 +- .../editor_menu.stories.storyshot | 81 +++++++++ .../__stories__/editor_menu.stories.tsx | 107 +++++++++++ .../editor_menu/editor_menu.component.tsx | 170 ++++++++++++++++++ .../editor_menu/editor_menu.tsx | 147 +++++++++++++++ .../workpad_header/editor_menu/index.ts | 9 + .../element_menu/element_menu.component.tsx | 6 +- .../workpad_header/element_menu/index.ts | 3 +- .../workpad_header.component.tsx | 28 +-- x-pack/plugins/canvas/public/plugin.tsx | 9 +- .../routes/workpad/hooks/use_workpad.ts | 4 +- .../canvas/public/services/embeddables.ts | 6 +- .../plugins/canvas/public/services/index.ts | 4 + .../public/services/kibana/embeddables.ts | 1 + .../canvas/public/services/kibana/index.ts | 3 + .../public/services/kibana/visualizations.ts | 21 +++ .../public/services/stubs/embeddables.ts | 1 + .../canvas/public/services/stubs/index.ts | 3 + .../public/services/stubs/visualizations.ts | 19 ++ .../canvas/public/services/visualizations.ts | 14 ++ .../public/state/reducers/embeddable.ts | 2 +- x-pack/plugins/canvas/server/plugin.ts | 9 +- .../canvas/server/setup_interpreter.ts | 12 +- x-pack/plugins/canvas/types/embeddables.ts | 16 ++ x-pack/plugins/canvas/types/functions.ts | 10 +- x-pack/plugins/canvas/types/index.ts | 1 + 60 files changed, 1329 insertions(+), 127 deletions(-) create mode 100644 x-pack/plugins/canvas/canvas_plugin_src/functions/external/embeddable.test.ts create mode 100644 x-pack/plugins/canvas/canvas_plugin_src/functions/external/embeddable.ts create mode 100644 x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/embeddable.test.ts create mode 100644 x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/embeddable.ts create mode 100644 x-pack/plugins/canvas/common/lib/embeddable_dataurl.ts create mode 100644 x-pack/plugins/canvas/i18n/functions/dict/embeddable.ts create mode 100644 x-pack/plugins/canvas/public/components/hooks/workpad/use_incoming_embeddable.ts create mode 100644 x-pack/plugins/canvas/public/components/workpad_header/editor_menu/__stories__/__snapshots__/editor_menu.stories.storyshot create mode 100644 x-pack/plugins/canvas/public/components/workpad_header/editor_menu/__stories__/editor_menu.stories.tsx create mode 100644 x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.component.tsx create mode 100644 x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.tsx create mode 100644 x-pack/plugins/canvas/public/components/workpad_header/editor_menu/index.ts create mode 100644 x-pack/plugins/canvas/public/services/kibana/visualizations.ts create mode 100644 x-pack/plugins/canvas/public/services/stubs/visualizations.ts create mode 100644 x-pack/plugins/canvas/public/services/visualizations.ts create mode 100644 x-pack/plugins/canvas/types/embeddables.ts diff --git a/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx b/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx index 8a46a16c1bf0c..effbf8ce980d7 100644 --- a/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx +++ b/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx @@ -231,7 +231,7 @@ export const EditorMenu = ({ dashboardContainer, createNewVisType }: Props) => { = { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, + 'labs:canvas:byValueEmbeddable': { + type: 'boolean', + _meta: { description: 'Non-default value of setting.' }, + }, 'labs:canvas:useDataService': { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts index 793ec4302d600..69287d37dfa28 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -122,6 +122,7 @@ export interface UsageStats { 'banners:textColor': string; 'banners:backgroundColor': string; 'labs:canvas:enable_ui': boolean; + 'labs:canvas:byValueEmbeddable': boolean; 'labs:canvas:useDataService': boolean; 'labs:presentation:timeToPresent': boolean; 'labs:dashboard:enable_ui': boolean; diff --git a/src/plugins/presentation_util/common/labs.ts b/src/plugins/presentation_util/common/labs.ts index cb976e73b5edf..8eefbd6981280 100644 --- a/src/plugins/presentation_util/common/labs.ts +++ b/src/plugins/presentation_util/common/labs.ts @@ -11,7 +11,9 @@ import { i18n } from '@kbn/i18n'; export const LABS_PROJECT_PREFIX = 'labs:'; export const DEFER_BELOW_FOLD = `${LABS_PROJECT_PREFIX}dashboard:deferBelowFold` as const; export const DASHBOARD_CONTROLS = `${LABS_PROJECT_PREFIX}dashboard:dashboardControls` as const; -export const projectIDs = [DEFER_BELOW_FOLD, DASHBOARD_CONTROLS] as const; +export const BY_VALUE_EMBEDDABLE = `${LABS_PROJECT_PREFIX}canvas:byValueEmbeddable` as const; + +export const projectIDs = [DEFER_BELOW_FOLD, DASHBOARD_CONTROLS, BY_VALUE_EMBEDDABLE] as const; export const environmentNames = ['kibana', 'browser', 'session'] as const; export const solutionNames = ['canvas', 'dashboard', 'presentation'] as const; @@ -48,6 +50,19 @@ export const projects: { [ID in ProjectID]: ProjectConfig & { id: ID } } = { }), solutions: ['dashboard'], }, + [BY_VALUE_EMBEDDABLE]: { + id: BY_VALUE_EMBEDDABLE, + isActive: true, + isDisplayed: true, + environments: ['kibana', 'browser', 'session'], + name: i18n.translate('presentationUtil.labs.enableByValueEmbeddableName', { + defaultMessage: 'By-Value Embeddables', + }), + description: i18n.translate('presentationUtil.labs.enableByValueEmbeddableDescription', { + defaultMessage: 'Enables support for by-value embeddables in Canvas', + }), + solutions: ['canvas'], + }, }; export type ProjectID = typeof projectIDs[number]; diff --git a/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.scss b/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.scss index 434f06b69a684..9f70ae353405b 100644 --- a/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.scss +++ b/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.scss @@ -1,9 +1,25 @@ .quickButtonGroup { - .quickButtonGroup__button { - background-color: $euiColorEmptyShade; - // sass-lint:disable-block no-important - border-width: $euiBorderWidthThin !important; - border-style: solid !important; - border-color: $euiBorderColor !important; + .euiButtonGroup__buttons { + border-radius: $euiBorderRadius; + + .quickButtonGroup__button { + background-color: $euiColorEmptyShade; + // sass-lint:disable-block no-important + border-width: $euiBorderWidthThin !important; + border-style: solid !important; + border-color: $euiBorderColor !important; + } + + .quickButtonGroup__button:first-of-type { + // sass-lint:disable-block no-important + border-top-left-radius: $euiBorderRadius !important; + border-bottom-left-radius: $euiBorderRadius !important; + } + + .quickButtonGroup__button:last-of-type { + // sass-lint:disable-block no-important + border-top-right-radius: $euiBorderRadius !important; + border-bottom-right-radius: $euiBorderRadius !important; + } } } diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 668bf23403124..21273482dd2b8 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -7683,6 +7683,12 @@ "description": "Non-default value of setting." } }, + "labs:canvas:byValueEmbeddable": { + "type": "boolean", + "_meta": { + "description": "Non-default value of setting." + } + }, "labs:canvas:useDataService": { "type": "boolean", "_meta": { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/expression_types/embeddable.ts b/x-pack/plugins/canvas/canvas_plugin_src/expression_types/embeddable.ts index 66cb95a4a210a..1f447c7ed834e 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/expression_types/embeddable.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/expression_types/embeddable.ts @@ -6,7 +6,7 @@ */ import { ExpressionTypeDefinition } from '../../../../../src/plugins/expressions'; -import { EmbeddableInput } from '../../../../../src/plugins/embeddable/common/'; +import { EmbeddableInput } from '../../types'; import { EmbeddableTypes } from './embeddable_types'; export const EmbeddableExpressionType = 'embeddable'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/browser/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/browser/index.ts index 2cfdebafb70df..d6d7a0f867849 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/browser/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/browser/index.ts @@ -6,7 +6,6 @@ */ import { functions as commonFunctions } from '../common'; -import { functions as externalFunctions } from '../external'; import { location } from './location'; import { markdown } from './markdown'; import { urlparam } from './urlparam'; @@ -14,13 +13,4 @@ import { escount } from './escount'; import { esdocs } from './esdocs'; import { essql } from './essql'; -export const functions = [ - location, - markdown, - urlparam, - escount, - esdocs, - essql, - ...commonFunctions, - ...externalFunctions, -]; +export const functions = [location, markdown, urlparam, escount, esdocs, essql, ...commonFunctions]; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/embeddable.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/embeddable.test.ts new file mode 100644 index 0000000000000..001fb0e3f62e3 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/embeddable.test.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { embeddableFunctionFactory } from './embeddable'; +import { getQueryFilters } from '../../../common/lib/build_embeddable_filters'; +import { ExpressionValueFilter } from '../../../types'; +import { encode } from '../../../common/lib/embeddable_dataurl'; +import { InitializeArguments } from '.'; + +const filterContext: ExpressionValueFilter = { + type: 'filter', + and: [ + { + type: 'filter', + and: [], + value: 'filter-value', + column: 'filter-column', + filterType: 'exactly', + }, + { + type: 'filter', + and: [], + column: 'time-column', + filterType: 'time', + from: '2019-06-04T04:00:00.000Z', + to: '2019-06-05T04:00:00.000Z', + }, + ], +}; + +describe('embeddable', () => { + const fn = embeddableFunctionFactory({} as InitializeArguments)().fn; + const config = { + id: 'some-id', + timerange: { from: '15m', to: 'now' }, + title: 'test embeddable', + }; + + const args = { + config: encode(config), + type: 'visualization', + }; + + it('accepts null context', () => { + const expression = fn(null, args, {} as any); + + expect(expression.input.filters).toEqual([]); + }); + + it('accepts filter context', () => { + const expression = fn(filterContext, args, {} as any); + const embeddableFilters = getQueryFilters(filterContext.and); + + expect(expression.input.filters).toEqual(embeddableFilters); + }); +}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/embeddable.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/embeddable.ts new file mode 100644 index 0000000000000..7ef8f0a09eb90 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/embeddable.ts @@ -0,0 +1,145 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; +import { ExpressionValueFilter, EmbeddableInput } from '../../../types'; +import { EmbeddableExpressionType, EmbeddableExpression } from '../../expression_types'; +import { getFunctionHelp } from '../../../i18n'; +import { SavedObjectReference } from '../../../../../../src/core/types'; +import { getQueryFilters } from '../../../common/lib/build_embeddable_filters'; +import { decode, encode } from '../../../common/lib/embeddable_dataurl'; +import { InitializeArguments } from '.'; + +export interface Arguments { + config: string; + type: string; +} + +const defaultTimeRange = { + from: 'now-15m', + to: 'now', +}; + +const baseEmbeddableInput = { + timeRange: defaultTimeRange, + disableTriggers: true, + renderMode: 'noInteractivity', +}; + +type Return = EmbeddableExpression; + +type EmbeddableFunction = ExpressionFunctionDefinition< + 'embeddable', + ExpressionValueFilter | null, + Arguments, + Return +>; + +export function embeddableFunctionFactory({ + embeddablePersistableStateService, +}: InitializeArguments): () => EmbeddableFunction { + return function embeddable(): EmbeddableFunction { + const { help, args: argHelp } = getFunctionHelp().embeddable; + + return { + name: 'embeddable', + help, + args: { + config: { + aliases: ['_'], + types: ['string'], + required: true, + help: argHelp.config, + }, + type: { + types: ['string'], + required: true, + help: argHelp.type, + }, + }, + context: { + types: ['filter'], + }, + type: EmbeddableExpressionType, + fn: (input, args) => { + const filters = input ? input.and : []; + + const embeddableInput = decode(args.config) as EmbeddableInput; + + return { + type: EmbeddableExpressionType, + input: { + ...baseEmbeddableInput, + ...embeddableInput, + filters: getQueryFilters(filters), + }, + generatedAt: Date.now(), + embeddableType: args.type, + }; + }, + + extract(state) { + const input = decode(state.config[0] as string); + + // extracts references for by-reference embeddables + if (input.savedObjectId) { + const refName = 'embeddable.savedObjectId'; + + const references: SavedObjectReference[] = [ + { + name: refName, + type: state.type[0] as string, + id: input.savedObjectId as string, + }, + ]; + + return { + state, + references, + }; + } + + // extracts references for by-value embeddables + const { state: extractedState, references: extractedReferences } = + embeddablePersistableStateService.extract({ + ...input, + type: state.type[0], + }); + + const { type, ...extractedInput } = extractedState; + + return { + state: { ...state, config: [encode(extractedInput)], type: [type] }, + references: extractedReferences, + }; + }, + + inject(state, references) { + const input = decode(state.config[0] as string); + const savedObjectReference = references.find( + (ref) => ref.name === 'embeddable.savedObjectId' + ); + + // injects saved object id for by-references embeddable + if (savedObjectReference) { + input.savedObjectId = savedObjectReference.id; + state.config[0] = encode(input); + state.type[0] = savedObjectReference.type; + } else { + // injects references for by-value embeddables + const { type, ...injectedInput } = embeddablePersistableStateService.inject( + { ...input, type: state.type[0] }, + references + ); + state.config[0] = encode(injectedInput); + state.type[0] = type; + } + return state; + }, + }; + }; +} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/index.ts index 407a0e2ebfe05..1d69e181b5fd9 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/index.ts @@ -5,9 +5,26 @@ * 2.0. */ +import { EmbeddableStart } from 'src/plugins/embeddable/public'; +import { embeddableFunctionFactory } from './embeddable'; import { savedLens } from './saved_lens'; import { savedMap } from './saved_map'; import { savedSearch } from './saved_search'; import { savedVisualization } from './saved_visualization'; -export const functions = [savedLens, savedMap, savedVisualization, savedSearch]; +export interface InitializeArguments { + embeddablePersistableStateService: { + extract: EmbeddableStart['extract']; + inject: EmbeddableStart['inject']; + }; +} + +export function initFunctions(initialize: InitializeArguments) { + return [ + embeddableFunctionFactory(initialize), + savedLens, + savedMap, + savedSearch, + savedVisualization, + ]; +} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.ts index 082a69a874cae..67947691f7757 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.ts @@ -9,9 +9,8 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { PaletteOutput } from 'src/plugins/charts/common'; import { Filter as DataFilter } from '@kbn/es-query'; import { TimeRange } from 'src/plugins/data/common'; -import { EmbeddableInput } from 'src/plugins/embeddable/common'; import { getQueryFilters } from '../../../common/lib/build_embeddable_filters'; -import { ExpressionValueFilter, TimeRange as TimeRangeArg } from '../../../types'; +import { ExpressionValueFilter, EmbeddableInput, TimeRange as TimeRangeArg } from '../../../types'; import { EmbeddableTypes, EmbeddableExpressionType, @@ -27,7 +26,7 @@ interface Arguments { } export type SavedLensInput = EmbeddableInput & { - id: string; + savedObjectId: string; timeRange?: TimeRange; filters: DataFilter[]; palette?: PaletteOutput; @@ -73,18 +72,19 @@ export function savedLens(): ExpressionFunctionDefinition< }, }, type: EmbeddableExpressionType, - fn: (input, args) => { + fn: (input, { id, timerange, title, palette }) => { const filters = input ? input.and : []; return { type: EmbeddableExpressionType, input: { - id: args.id, + id, + savedObjectId: id, filters: getQueryFilters(filters), - timeRange: args.timerange || defaultTimeRange, - title: args.title === null ? undefined : args.title, + timeRange: timerange || defaultTimeRange, + title: title === null ? undefined : title, disableTriggers: true, - palette: args.palette, + palette, }, embeddableType: EmbeddableTypes.lens, generatedAt: Date.now(), diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.ts index 538ed3f919823..a7471c755155c 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.ts @@ -30,7 +30,7 @@ const defaultTimeRange = { to: 'now', }; -type Output = EmbeddableExpression; +type Output = EmbeddableExpression; export function savedMap(): ExpressionFunctionDefinition< 'savedMap', @@ -85,8 +85,9 @@ export function savedMap(): ExpressionFunctionDefinition< return { type: EmbeddableExpressionType, input: { - attributes: { title: '' }, id: args.id, + attributes: { title: '' }, + savedObjectId: args.id, filters: getQueryFilters(filters), timeRange: args.timerange || defaultTimeRange, refreshConfig: { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.ts index 5c0442b43250c..31e3fb2a8c564 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.ts @@ -25,7 +25,7 @@ interface Arguments { title: string | null; } -type Output = EmbeddableExpression; +type Output = EmbeddableExpression; const defaultTimeRange = { from: 'now-15m', @@ -94,6 +94,7 @@ export function savedVisualization(): ExpressionFunctionDefinition< type: EmbeddableExpressionType, input: { id, + savedObjectId: id, disableTriggers: true, timeRange: timerange || defaultTimeRange, filters: getQueryFilters(filters), diff --git a/x-pack/plugins/canvas/canvas_plugin_src/plugin.ts b/x-pack/plugins/canvas/canvas_plugin_src/plugin.ts index 91c573fc4148b..591795637aebe 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/plugin.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/plugin.ts @@ -7,12 +7,14 @@ import { CoreSetup, CoreStart, Plugin } from 'src/core/public'; import { ChartsPluginStart } from 'src/plugins/charts/public'; +import { PresentationUtilPluginStart } from 'src/plugins/presentation_util/public'; import { CanvasSetup } from '../public'; import { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; import { UiActionsStart } from '../../../../src/plugins/ui_actions/public'; import { Start as InspectorStart } from '../../../../src/plugins/inspector/public'; import { functions } from './functions/browser'; +import { initFunctions } from './functions/external'; import { typeFunctions } from './expression_types'; import { renderFunctions, renderFunctionFactories } from './renderers'; @@ -25,6 +27,7 @@ export interface StartDeps { uiActions: UiActionsStart; inspector: InspectorStart; charts: ChartsPluginStart; + presentationUtil: PresentationUtilPluginStart; } export type SetupInitializer = (core: CoreSetup, plugins: SetupDeps) => T; @@ -39,6 +42,13 @@ export class CanvasSrcPlugin implements Plugin plugins.canvas.addRenderers(renderFunctions); core.getStartServices().then(([coreStart, depsStart]) => { + const externalFunctions = initFunctions({ + embeddablePersistableStateService: { + extract: depsStart.embeddable.extract, + inject: depsStart.embeddable.inject, + }, + }); + plugins.canvas.addFunctions(externalFunctions); plugins.canvas.addRenderers( renderFunctionFactories.map((factory: any) => factory(coreStart, depsStart)) ); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx index 73e839433c25e..953746c280840 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx @@ -13,16 +13,17 @@ import { IEmbeddable, EmbeddableFactory, EmbeddableFactoryNotFoundError, + isErrorEmbeddable, } from '../../../../../../src/plugins/embeddable/public'; import { EmbeddableExpression } from '../../expression_types/embeddable'; import { RendererStrings } from '../../../i18n'; import { embeddableInputToExpression } from './embeddable_input_to_expression'; -import { EmbeddableInput } from '../../expression_types'; -import { RendererFactory } from '../../../types'; +import { RendererFactory, EmbeddableInput } from '../../../types'; import { CANVAS_EMBEDDABLE_CLASSNAME } from '../../../common/lib'; const { embeddable: strings } = RendererStrings; +// registry of references to embeddables on the workpad const embeddablesRegistry: { [key: string]: IEmbeddable | Promise; } = {}; @@ -30,11 +31,11 @@ const embeddablesRegistry: { const renderEmbeddableFactory = (core: CoreStart, plugins: StartDeps) => { const I18nContext = core.i18n.Context; - return (embeddableObject: IEmbeddable, domNode: HTMLElement) => { + return (embeddableObject: IEmbeddable) => { return (
@@ -56,6 +57,9 @@ export const embeddableRendererFactory = ( reuseDomNode: true, render: async (domNode, { input, embeddableType }, handlers) => { const uniqueId = handlers.getElementId(); + const isByValueEnabled = plugins.presentationUtil.labsService.isProjectEnabled( + 'labs:canvas:byValueEmbeddable' + ); if (!embeddablesRegistry[uniqueId]) { const factory = Array.from(plugins.embeddable.getEmbeddableFactories()).find( @@ -67,15 +71,27 @@ export const embeddableRendererFactory = ( throw new EmbeddableFactoryNotFoundError(embeddableType); } - const embeddablePromise = factory - .createFromSavedObject(input.id, input) - .then((embeddable) => { - embeddablesRegistry[uniqueId] = embeddable; - return embeddable; - }); - embeddablesRegistry[uniqueId] = embeddablePromise; - - const embeddableObject = await (async () => embeddablePromise)(); + const embeddableInput = { ...input, id: uniqueId }; + + const embeddablePromise = input.savedObjectId + ? factory + .createFromSavedObject(input.savedObjectId, embeddableInput) + .then((embeddable) => { + // stores embeddable in registrey + embeddablesRegistry[uniqueId] = embeddable; + return embeddable; + }) + : factory.create(embeddableInput).then((embeddable) => { + if (!embeddable || isErrorEmbeddable(embeddable)) { + return; + } + // stores embeddable in registry + embeddablesRegistry[uniqueId] = embeddable as IEmbeddable; + return embeddable; + }); + embeddablesRegistry[uniqueId] = embeddablePromise as Promise; + + const embeddableObject = (await (async () => embeddablePromise)()) as IEmbeddable; const palettes = await plugins.charts.palettes.getPalettes(); @@ -86,7 +102,8 @@ export const embeddableRendererFactory = ( const updatedExpression = embeddableInputToExpression( updatedInput, embeddableType, - palettes + palettes, + isByValueEnabled ); if (updatedExpression) { @@ -94,15 +111,7 @@ export const embeddableRendererFactory = ( } }); - ReactDOM.render(renderEmbeddable(embeddableObject, domNode), domNode, () => - handlers.done() - ); - - handlers.onResize(() => { - ReactDOM.render(renderEmbeddable(embeddableObject, domNode), domNode, () => - handlers.done() - ); - }); + ReactDOM.render(renderEmbeddable(embeddableObject), domNode, () => handlers.done()); handlers.onDestroy(() => { subscription.unsubscribe(); @@ -115,6 +124,7 @@ export const embeddableRendererFactory = ( } else { const embeddable = embeddablesRegistry[uniqueId]; + // updating embeddable input with changes made to expression or filters if ('updateInput' in embeddable) { embeddable.updateInput(input); embeddable.reload(); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression.ts index 41cefad6a470f..80830eac24021 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression.ts @@ -10,6 +10,7 @@ import { EmbeddableTypes, EmbeddableInput } from '../../expression_types'; import { toExpression as mapToExpression } from './input_type_to_expression/map'; import { toExpression as visualizationToExpression } from './input_type_to_expression/visualization'; import { toExpression as lensToExpression } from './input_type_to_expression/lens'; +import { toExpression as genericToExpression } from './input_type_to_expression/embeddable'; export const inputToExpressionTypeMap = { [EmbeddableTypes.map]: mapToExpression, @@ -23,8 +24,13 @@ export const inputToExpressionTypeMap = { export function embeddableInputToExpression( input: EmbeddableInput, embeddableType: string, - palettes: PaletteRegistry + palettes: PaletteRegistry, + useGenericEmbeddable?: boolean ): string | undefined { + if (useGenericEmbeddable) { + return genericToExpression(input, embeddableType); + } + if (inputToExpressionTypeMap[embeddableType]) { return inputToExpressionTypeMap[embeddableType](input as any, palettes); } diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/embeddable.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/embeddable.test.ts new file mode 100644 index 0000000000000..4b78acec8750a --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/embeddable.test.ts @@ -0,0 +1,128 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { toExpression } from './embeddable'; +import { EmbeddableInput } from '../../../../types'; +import { decode } from '../../../../common/lib/embeddable_dataurl'; +import { fromExpression } from '@kbn/interpreter/common'; + +describe('toExpression', () => { + describe('by-reference embeddable input', () => { + const baseEmbeddableInput = { + id: 'elementId', + savedObjectId: 'embeddableId', + filters: [], + }; + + it('converts to an embeddable expression', () => { + const input: EmbeddableInput = baseEmbeddableInput; + + const expression = toExpression(input, 'visualization'); + const ast = fromExpression(expression); + + expect(ast.type).toBe('expression'); + expect(ast.chain[0].function).toBe('embeddable'); + expect(ast.chain[0].arguments.type[0]).toBe('visualization'); + + const config = decode(ast.chain[0].arguments.config[0] as string); + + expect(config.savedObjectId).toStrictEqual(input.savedObjectId); + }); + + it('includes optional input values', () => { + const input: EmbeddableInput = { + ...baseEmbeddableInput, + title: 'title', + timeRange: { + from: 'now-1h', + to: 'now', + }, + }; + + const expression = toExpression(input, 'visualization'); + const ast = fromExpression(expression); + + const config = decode(ast.chain[0].arguments.config[0] as string); + + expect(config).toHaveProperty('title', input.title); + expect(config).toHaveProperty('timeRange'); + expect(config.timeRange).toHaveProperty('from', input.timeRange?.from); + expect(config.timeRange).toHaveProperty('to', input.timeRange?.to); + }); + + it('includes empty panel title', () => { + const input: EmbeddableInput = { + ...baseEmbeddableInput, + title: '', + }; + + const expression = toExpression(input, 'visualization'); + const ast = fromExpression(expression); + + const config = decode(ast.chain[0].arguments.config[0] as string); + + expect(config).toHaveProperty('title', input.title); + }); + }); + + describe('by-value embeddable input', () => { + const baseEmbeddableInput = { + id: 'elementId', + disableTriggers: true, + filters: [], + }; + it('converts to an embeddable expression', () => { + const input: EmbeddableInput = baseEmbeddableInput; + + const expression = toExpression(input, 'visualization'); + const ast = fromExpression(expression); + + expect(ast.type).toBe('expression'); + expect(ast.chain[0].function).toBe('embeddable'); + expect(ast.chain[0].arguments.type[0]).toBe('visualization'); + + const config = decode(ast.chain[0].arguments.config[0] as string); + expect(config.filters).toStrictEqual(input.filters); + expect(config.disableTriggers).toStrictEqual(input.disableTriggers); + }); + + it('includes optional input values', () => { + const input: EmbeddableInput = { + ...baseEmbeddableInput, + title: 'title', + timeRange: { + from: 'now-1h', + to: 'now', + }, + }; + + const expression = toExpression(input, 'visualization'); + const ast = fromExpression(expression); + + const config = decode(ast.chain[0].arguments.config[0] as string); + + expect(config).toHaveProperty('title', input.title); + expect(config).toHaveProperty('timeRange'); + expect(config.timeRange).toHaveProperty('from', input.timeRange?.from); + expect(config.timeRange).toHaveProperty('to', input.timeRange?.to); + }); + + it('includes empty panel title', () => { + const input: EmbeddableInput = { + ...baseEmbeddableInput, + title: '', + }; + + const expression = toExpression(input, 'visualization'); + const ast = fromExpression(expression); + + const config = decode(ast.chain[0].arguments.config[0] as string); + + expect(config).toHaveProperty('title', input.title); + }); + }); +}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/embeddable.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/embeddable.ts new file mode 100644 index 0000000000000..94d86f6640be1 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/embeddable.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { encode } from '../../../../common/lib/embeddable_dataurl'; +import { EmbeddableInput } from '../../../expression_types'; + +export function toExpression(input: EmbeddableInput, embeddableType: string): string { + return `embeddable config="${encode(input)}" type="${embeddableType}"`; +} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.test.ts index 24da7238bcee9..224cdfba389d7 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.test.ts @@ -11,7 +11,8 @@ import { fromExpression, Ast } from '@kbn/interpreter/common'; import { chartPluginMock } from 'src/plugins/charts/public/mocks'; const baseEmbeddableInput = { - id: 'embeddableId', + id: 'elementId', + savedObjectId: 'embeddableId', filters: [], }; @@ -27,7 +28,7 @@ describe('toExpression', () => { expect(ast.type).toBe('expression'); expect(ast.chain[0].function).toBe('savedLens'); - expect(ast.chain[0].arguments.id).toStrictEqual([input.id]); + expect(ast.chain[0].arguments.id).toStrictEqual([input.savedObjectId]); expect(ast.chain[0].arguments).not.toHaveProperty('title'); expect(ast.chain[0].arguments).not.toHaveProperty('timerange'); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.ts index 35e106f234fa4..5a13b73b3fe74 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.ts @@ -14,7 +14,7 @@ export function toExpression(input: SavedLensInput, palettes: PaletteRegistry): expressionParts.push('savedLens'); - expressionParts.push(`id="${input.id}"`); + expressionParts.push(`id="${input.savedObjectId}"`); if (input.title !== undefined) { expressionParts.push(`title="${input.title}"`); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.test.ts index 804d0d849cc7f..af7b40a9b283d 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.test.ts @@ -6,12 +6,12 @@ */ import { toExpression } from './map'; -import { MapEmbeddableInput } from '../../../../../../plugins/maps/public/embeddable'; import { fromExpression, Ast } from '@kbn/interpreter/common'; const baseSavedMapInput = { + id: 'elementId', attributes: { title: '' }, - id: 'embeddableId', + savedObjectId: 'embeddableId', filters: [], isLayerTOCOpen: false, refreshConfig: { @@ -23,7 +23,7 @@ const baseSavedMapInput = { describe('toExpression', () => { it('converts to a savedMap expression', () => { - const input: MapEmbeddableInput = { + const input = { ...baseSavedMapInput, }; @@ -33,7 +33,7 @@ describe('toExpression', () => { expect(ast.type).toBe('expression'); expect(ast.chain[0].function).toBe('savedMap'); - expect(ast.chain[0].arguments.id).toStrictEqual([input.id]); + expect(ast.chain[0].arguments.id).toStrictEqual([input.savedObjectId]); expect(ast.chain[0].arguments).not.toHaveProperty('title'); expect(ast.chain[0].arguments).not.toHaveProperty('center'); @@ -41,7 +41,7 @@ describe('toExpression', () => { }); it('includes optional input values', () => { - const input: MapEmbeddableInput = { + const input = { ...baseSavedMapInput, mapCenter: { lat: 1, @@ -73,7 +73,7 @@ describe('toExpression', () => { }); it('includes empty panel title', () => { - const input: MapEmbeddableInput = { + const input = { ...baseSavedMapInput, title: '', }; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.ts index 3fd6a68a327c6..03746f38b4696 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.ts @@ -5,13 +5,14 @@ * 2.0. */ -import { MapEmbeddableInput } from '../../../../../../plugins/maps/public/embeddable'; +import { MapEmbeddableInput } from '../../../../../../plugins/maps/public'; -export function toExpression(input: MapEmbeddableInput): string { +export function toExpression(input: MapEmbeddableInput & { savedObjectId: string }): string { const expressionParts = [] as string[]; expressionParts.push('savedMap'); - expressionParts.push(`id="${input.id}"`); + + expressionParts.push(`id="${input.savedObjectId}"`); if (input.title !== undefined) { expressionParts.push(`title="${input.title}"`); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.test.ts index c5106b9a102b4..4c61a130f3c95 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.test.ts @@ -9,7 +9,8 @@ import { toExpression } from './visualization'; import { fromExpression, Ast } from '@kbn/interpreter/common'; const baseInput = { - id: 'embeddableId', + id: 'elementId', + savedObjectId: 'embeddableId', }; describe('toExpression', () => { @@ -24,7 +25,7 @@ describe('toExpression', () => { expect(ast.type).toBe('expression'); expect(ast.chain[0].function).toBe('savedVisualization'); - expect(ast.chain[0].arguments.id).toStrictEqual([input.id]); + expect(ast.chain[0].arguments.id).toStrictEqual([input.savedObjectId]); }); it('includes timerange if given', () => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.ts index bcb73b2081fee..364d7cd0755db 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.ts @@ -7,11 +7,11 @@ import { VisualizeInput } from 'src/plugins/visualizations/public'; -export function toExpression(input: VisualizeInput): string { +export function toExpression(input: VisualizeInput & { savedObjectId: string }): string { const expressionParts = [] as string[]; expressionParts.push('savedVisualization'); - expressionParts.push(`id="${input.id}"`); + expressionParts.push(`id="${input.savedObjectId}"`); if (input.title !== undefined) { expressionParts.push(`title="${input.title}"`); diff --git a/x-pack/plugins/canvas/common/lib/embeddable_dataurl.ts b/x-pack/plugins/canvas/common/lib/embeddable_dataurl.ts new file mode 100644 index 0000000000000..e76dedfe63b14 --- /dev/null +++ b/x-pack/plugins/canvas/common/lib/embeddable_dataurl.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EmbeddableInput } from '../../types'; + +export const encode = (input: Partial) => + Buffer.from(JSON.stringify(input)).toString('base64'); +export const decode = (serializedInput: string) => + JSON.parse(Buffer.from(serializedInput, 'base64').toString()); diff --git a/x-pack/plugins/canvas/i18n/functions/dict/embeddable.ts b/x-pack/plugins/canvas/i18n/functions/dict/embeddable.ts new file mode 100644 index 0000000000000..279f58799e8c0 --- /dev/null +++ b/x-pack/plugins/canvas/i18n/functions/dict/embeddable.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { embeddableFunctionFactory } from '../../../canvas_plugin_src/functions/external/embeddable'; +import { FunctionHelp } from '../function_help'; +import { FunctionFactory } from '../../../types'; + +export const help: FunctionHelp>> = { + help: i18n.translate('xpack.canvas.functions.embeddableHelpText', { + defaultMessage: `Returns an embeddable with the provided configuration`, + }), + args: { + config: i18n.translate('xpack.canvas.functions.embeddable.args.idHelpText', { + defaultMessage: `The base64 encoded embeddable input object`, + }), + type: i18n.translate('xpack.canvas.functions.embeddable.args.typeHelpText', { + defaultMessage: `The embeddable type`, + }), + }, +}; diff --git a/x-pack/plugins/canvas/i18n/functions/function_help.ts b/x-pack/plugins/canvas/i18n/functions/function_help.ts index 5eae785fefa2e..520d32af1c272 100644 --- a/x-pack/plugins/canvas/i18n/functions/function_help.ts +++ b/x-pack/plugins/canvas/i18n/functions/function_help.ts @@ -27,6 +27,7 @@ import { help as demodata } from './dict/demodata'; import { help as doFn } from './dict/do'; import { help as dropdownControl } from './dict/dropdown_control'; import { help as eq } from './dict/eq'; +import { help as embeddable } from './dict/embeddable'; import { help as escount } from './dict/escount'; import { help as esdocs } from './dict/esdocs'; import { help as essql } from './dict/essql'; @@ -182,6 +183,7 @@ export const getFunctionHelp = (): FunctionHelpDict => ({ do: doFn, dropdownControl, eq, + embeddable, escount, esdocs, essql, diff --git a/x-pack/plugins/canvas/kibana.json b/x-pack/plugins/canvas/kibana.json index 9c4d1b2179d82..2fd312502a3c7 100644 --- a/x-pack/plugins/canvas/kibana.json +++ b/x-pack/plugins/canvas/kibana.json @@ -25,6 +25,7 @@ "features", "inspector", "presentationUtil", + "visualizations", "uiActions", "share" ], diff --git a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx index bf731876bf8c8..57f52fcf21f0f 100644 --- a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx +++ b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { FC, useCallback } from 'react'; import { EuiFlyout, EuiFlyoutHeader, EuiFlyoutBody, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -27,38 +27,44 @@ const strings = { }; export interface Props { onClose: () => void; - onSelect: (id: string, embeddableType: string) => void; + onSelect: (id: string, embeddableType: string, isByValueEnabled?: boolean) => void; availableEmbeddables: string[]; + isByValueEnabled?: boolean; } -export const AddEmbeddableFlyout: FC = ({ onSelect, availableEmbeddables, onClose }) => { +export const AddEmbeddableFlyout: FC = ({ + onSelect, + availableEmbeddables, + onClose, + isByValueEnabled, +}) => { const embeddablesService = useEmbeddablesService(); const platformService = usePlatformService(); const { getEmbeddableFactories } = embeddablesService; const { getSavedObjects, getUISettings } = platformService; - const onAddPanel = (id: string, savedObjectType: string, name: string) => { - const embeddableFactories = getEmbeddableFactories(); + const onAddPanel = useCallback( + (id: string, savedObjectType: string) => { + const embeddableFactories = getEmbeddableFactories(); + // Find the embeddable type from the saved object type + const found = Array.from(embeddableFactories).find((embeddableFactory) => { + return Boolean( + embeddableFactory.savedObjectMetaData && + embeddableFactory.savedObjectMetaData.type === savedObjectType + ); + }); - // Find the embeddable type from the saved object type - const found = Array.from(embeddableFactories).find((embeddableFactory) => { - return Boolean( - embeddableFactory.savedObjectMetaData && - embeddableFactory.savedObjectMetaData.type === savedObjectType - ); - }); - - const foundEmbeddableType = found ? found.type : 'unknown'; + const foundEmbeddableType = found ? found.type : 'unknown'; - onSelect(id, foundEmbeddableType); - }; + onSelect(id, foundEmbeddableType, isByValueEnabled); + }, + [isByValueEnabled, getEmbeddableFactories, onSelect] + ); const embeddableFactories = getEmbeddableFactories(); const availableSavedObjects = Array.from(embeddableFactories) - .filter((factory) => { - return availableEmbeddables.includes(factory.type); - }) + .filter((factory) => isByValueEnabled || availableEmbeddables.includes(factory.type)) .map((factory) => factory.savedObjectMetaData) .filter>(function ( maybeSavedObjectMetaData diff --git a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.tsx b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.tsx index 770a4cac606b0..4dc8d963932d8 100644 --- a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.tsx +++ b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.tsx @@ -8,12 +8,14 @@ import React, { useMemo, useEffect, useCallback } from 'react'; import { createPortal } from 'react-dom'; import { useSelector, useDispatch } from 'react-redux'; +import { encode } from '../../../common/lib/embeddable_dataurl'; import { AddEmbeddableFlyout as Component, Props as ComponentProps } from './flyout.component'; // @ts-expect-error untyped local import { addElement } from '../../state/actions/elements'; import { getSelectedPage } from '../../state/selectors/workpad'; import { EmbeddableTypes } from '../../../canvas_plugin_src/expression_types/embeddable'; import { State } from '../../../types'; +import { useLabsService } from '../../services'; const allowedEmbeddables = { [EmbeddableTypes.map]: (id: string) => { @@ -65,6 +67,9 @@ export const AddEmbeddablePanel: React.FunctionComponent = ({ availableEmbeddables, ...restProps }) => { + const labsService = useLabsService(); + const isByValueEnabled = labsService.isProjectEnabled('labs:canvas:byValueEmbeddable'); + const dispatch = useDispatch(); const pageId = useSelector((state) => getSelectedPage(state)); @@ -74,18 +79,27 @@ export const AddEmbeddablePanel: React.FunctionComponent = ({ ); const onSelect = useCallback( - (id: string, type: string) => { + (id: string, type: string): void => { const partialElement = { expression: `markdown "Could not find embeddable for type ${type}" | render`, }; - if (allowedEmbeddables[type]) { + + // If by-value is enabled, we'll handle both by-reference and by-value embeddables + // with the new generic `embeddable` function. + // Otherwise we fallback to the embeddable type specific expressions. + if (isByValueEnabled) { + const config = encode({ savedObjectId: id }); + partialElement.expression = `embeddable config="${config}" + type="${type}" +| render`; + } else if (allowedEmbeddables[type]) { partialElement.expression = allowedEmbeddables[type](id); } addEmbeddable(pageId, partialElement); restProps.onClose(); }, - [addEmbeddable, pageId, restProps] + [addEmbeddable, pageId, restProps, isByValueEnabled] ); return ( @@ -93,6 +107,7 @@ export const AddEmbeddablePanel: React.FunctionComponent = ({ {...restProps} availableEmbeddables={availableEmbeddables || []} onSelect={onSelect} + isByValueEnabled={isByValueEnabled} /> ); }; diff --git a/x-pack/plugins/canvas/public/components/hooks/workpad/index.tsx b/x-pack/plugins/canvas/public/components/hooks/workpad/index.tsx index 50d527036560a..ffd5b095b12e5 100644 --- a/x-pack/plugins/canvas/public/components/hooks/workpad/index.tsx +++ b/x-pack/plugins/canvas/public/components/hooks/workpad/index.tsx @@ -6,3 +6,5 @@ */ export { useDownloadWorkpad, useDownloadRenderedWorkpad } from './use_download_workpad'; + +export { useIncomingEmbeddable } from './use_incoming_embeddable'; diff --git a/x-pack/plugins/canvas/public/components/hooks/workpad/use_incoming_embeddable.ts b/x-pack/plugins/canvas/public/components/hooks/workpad/use_incoming_embeddable.ts new file mode 100644 index 0000000000000..2f8e2503ea57e --- /dev/null +++ b/x-pack/plugins/canvas/public/components/hooks/workpad/use_incoming_embeddable.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect } from 'react'; +import { useDispatch } from 'react-redux'; +import { fromExpression } from '@kbn/interpreter/common'; +import { CANVAS_APP } from '../../../../common/lib'; +import { decode, encode } from '../../../../common/lib/embeddable_dataurl'; +import { CanvasElement, CanvasPage } from '../../../../types'; +import { useEmbeddablesService, useLabsService } from '../../../services'; +// @ts-expect-error unconverted file +import { addElement } from '../../../state/actions/elements'; +// @ts-expect-error unconverted file +import { selectToplevelNodes } from '../../../state/actions/transient'; + +import { + updateEmbeddableExpression, + fetchEmbeddableRenderable, +} from '../../../state/actions/embeddable'; +import { clearValue } from '../../../state/actions/resolved_args'; + +export const useIncomingEmbeddable = (selectedPage: CanvasPage) => { + const embeddablesService = useEmbeddablesService(); + const labsService = useLabsService(); + const dispatch = useDispatch(); + const isByValueEnabled = labsService.isProjectEnabled('labs:canvas:byValueEmbeddable'); + const stateTransferService = embeddablesService.getStateTransfer(); + + // fetch incoming embeddable from state transfer service. + const incomingEmbeddable = stateTransferService.getIncomingEmbeddablePackage(CANVAS_APP, true); + + useEffect(() => { + if (isByValueEnabled && incomingEmbeddable) { + const { embeddableId, input: incomingInput, type } = incomingEmbeddable; + + // retrieve existing element + const originalElement = selectedPage.elements.find( + ({ id }: CanvasElement) => id === embeddableId + ); + + if (originalElement) { + const originalAst = fromExpression(originalElement!.expression); + + const functionIndex = originalAst.chain.findIndex( + ({ function: fn }) => fn === 'embeddable' + ); + + const originalInput = decode( + originalAst.chain[functionIndex].arguments.config[0] as string + ); + + // clear out resolved arg for old embeddable + const argumentPath = [embeddableId, 'expressionRenderable']; + dispatch(clearValue({ path: argumentPath })); + + const updatedInput = { ...originalInput, ...incomingInput }; + + const expression = `embeddable config="${encode(updatedInput)}" + type="${type}" +| render`; + + dispatch( + updateEmbeddableExpression({ + elementId: originalElement.id, + embeddableExpression: expression, + }) + ); + + // update resolved args + dispatch(fetchEmbeddableRenderable(originalElement.id)); + + // select new embeddable element + dispatch(selectToplevelNodes([embeddableId])); + } else { + const expression = `embeddable config="${encode(incomingInput)}" + type="${type}" +| render`; + dispatch(addElement(selectedPage.id, { expression })); + } + } + }, [dispatch, selectedPage, incomingEmbeddable, isByValueEnabled]); +}; diff --git a/x-pack/plugins/canvas/public/components/workpad/workpad.tsx b/x-pack/plugins/canvas/public/components/workpad/workpad.tsx index 622c885b6ef28..7cc077203c737 100644 --- a/x-pack/plugins/canvas/public/components/workpad/workpad.tsx +++ b/x-pack/plugins/canvas/public/components/workpad/workpad.tsx @@ -27,6 +27,7 @@ import { WorkpadRoutingContext } from '../../routes/workpad'; import { usePlatformService } from '../../services'; import { Workpad as WorkpadComponent, Props } from './workpad.component'; import { State } from '../../../types'; +import { useIncomingEmbeddable } from '../hooks'; type ContainerProps = Pick; @@ -58,6 +59,9 @@ export const Workpad: FC = (props) => { }; }); + const selectedPage = propsFromState.pages[propsFromState.selectedPageNumber - 1]; + useIncomingEmbeddable(selectedPage); + const fetchAllRenderables = useCallback(() => { dispatch(fetchAllRenderablesAction()); }, [dispatch]); diff --git a/x-pack/plugins/canvas/public/components/workpad_app/workpad_app.scss b/x-pack/plugins/canvas/public/components/workpad_app/workpad_app.scss index 4acdca10d61cc..0ddd44ed8f9a8 100644 --- a/x-pack/plugins/canvas/public/components/workpad_app/workpad_app.scss +++ b/x-pack/plugins/canvas/public/components/workpad_app/workpad_app.scss @@ -31,7 +31,7 @@ $canvasLayoutFontSize: $euiFontSizeS; .canvasLayout__stageHeader { flex-grow: 0; flex-basis: auto; - padding: $euiSizeS; + padding: $euiSizeS $euiSize; font-size: $canvasLayoutFontSize; border-bottom: $euiBorderThin; background: $euiColorLightestShade; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/__stories__/__snapshots__/editor_menu.stories.storyshot b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/__stories__/__snapshots__/editor_menu.stories.storyshot new file mode 100644 index 0000000000000..f4aab0e59e7ee --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/__stories__/__snapshots__/editor_menu.stories.storyshot @@ -0,0 +1,81 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots components/WorkpadHeader/EditorMenu dark mode 1`] = ` +
+
+ +
+
+`; + +exports[`Storyshots components/WorkpadHeader/EditorMenu default 1`] = ` +
+
+ +
+
+`; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/__stories__/editor_menu.stories.tsx b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/__stories__/editor_menu.stories.tsx new file mode 100644 index 0000000000000..01048bc0af301 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/__stories__/editor_menu.stories.tsx @@ -0,0 +1,107 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { storiesOf } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import React from 'react'; +import { EmbeddableFactoryDefinition, IEmbeddable } from 'src/plugins/embeddable/public'; +import { BaseVisType, VisTypeAlias } from 'src/plugins/visualizations/public'; +import { EditorMenu } from '../editor_menu.component'; + +const testFactories: EmbeddableFactoryDefinition[] = [ + { + type: 'ml_anomaly_swimlane', + getDisplayName: () => 'Anomaly swimlane', + getIconType: () => '', + getDescription: () => 'Description for anomaly swimlane', + isEditable: () => Promise.resolve(true), + create: () => Promise.resolve({ id: 'swimlane_embeddable' } as IEmbeddable), + grouping: [ + { + id: 'ml', + getDisplayName: () => 'machine learning', + getIconType: () => 'machineLearningApp', + }, + ], + }, + { + type: 'ml_anomaly_chart', + getDisplayName: () => 'Anomaly chart', + getIconType: () => '', + getDescription: () => 'Description for anomaly chart', + isEditable: () => Promise.resolve(true), + create: () => Promise.resolve({ id: 'anomaly_chart_embeddable' } as IEmbeddable), + grouping: [ + { + id: 'ml', + getDisplayName: () => 'machine learning', + getIconType: () => 'machineLearningApp', + }, + ], + }, + { + type: 'log_stream', + getDisplayName: () => 'Log stream', + getIconType: () => '', + getDescription: () => 'Description for log stream', + isEditable: () => Promise.resolve(true), + create: () => Promise.resolve({ id: 'anomaly_chart_embeddable' } as IEmbeddable), + }, +]; + +const testVisTypes: BaseVisType[] = [ + { title: 'TSVB', icon: '', description: 'Description of TSVB', name: 'tsvb' } as BaseVisType, + { + titleInWizard: 'Custom visualization', + title: 'Vega', + icon: '', + description: 'Description of Vega', + name: 'vega', + } as BaseVisType, +]; + +const testVisTypeAliases: VisTypeAlias[] = [ + { + title: 'Lens', + aliasApp: 'lens', + aliasPath: 'path/to/lens', + icon: 'lensApp', + name: 'lens', + description: 'Description of Lens app', + stage: 'production', + }, + { + title: 'Maps', + aliasApp: 'maps', + aliasPath: 'path/to/maps', + icon: 'gisApp', + name: 'maps', + description: 'Description of Maps app', + stage: 'production', + }, +]; + +storiesOf('components/WorkpadHeader/EditorMenu', module) + .add('default', () => ( + action('createNewVisType')} + createNewEmbeddable={() => action('createNewEmbeddable')} + /> + )) + .add('dark mode', () => ( + action('createNewVisType')} + createNewEmbeddable={() => action('createNewEmbeddable')} + /> + )); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.component.tsx new file mode 100644 index 0000000000000..e8f762f9731a1 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.component.tsx @@ -0,0 +1,170 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC } from 'react'; +import { + EuiContextMenu, + EuiContextMenuPanelItemDescriptor, + EuiContextMenuItemIcon, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { EmbeddableFactoryDefinition } from '../../../../../../../src/plugins/embeddable/public'; +import { BaseVisType, VisTypeAlias } from '../../../../../../../src/plugins/visualizations/public'; +import { SolutionToolbarPopover } from '../../../../../../../src/plugins/presentation_util/public'; + +const strings = { + getEditorMenuButtonLabel: () => + i18n.translate('xpack.canvas.solutionToolbar.editorMenuButtonLabel', { + defaultMessage: 'Select type', + }), +}; + +interface FactoryGroup { + id: string; + appName: string; + icon: EuiContextMenuItemIcon; + panelId: number; + factories: EmbeddableFactoryDefinition[]; +} + +interface Props { + factories: EmbeddableFactoryDefinition[]; + isDarkThemeEnabled?: boolean; + promotedVisTypes: BaseVisType[]; + visTypeAliases: VisTypeAlias[]; + createNewVisType: (visType?: BaseVisType | VisTypeAlias) => () => void; + createNewEmbeddable: (factory: EmbeddableFactoryDefinition) => () => void; +} + +export const EditorMenu: FC = ({ + factories, + isDarkThemeEnabled, + promotedVisTypes, + visTypeAliases, + createNewVisType, + createNewEmbeddable, +}: Props) => { + const factoryGroupMap: Record = {}; + const ungroupedFactories: EmbeddableFactoryDefinition[] = []; + + let panelCount = 1; + + // Maps factories with a group to create nested context menus for each group type + // and pushes ungrouped factories into a separate array + factories.forEach((factory: EmbeddableFactoryDefinition, index) => { + const { grouping } = factory; + + if (grouping) { + grouping.forEach((group) => { + if (factoryGroupMap[group.id]) { + factoryGroupMap[group.id].factories.push(factory); + } else { + factoryGroupMap[group.id] = { + id: group.id, + appName: group.getDisplayName ? group.getDisplayName({}) : group.id, + icon: (group.getIconType ? group.getIconType({}) : 'empty') as EuiContextMenuItemIcon, + factories: [factory], + panelId: panelCount, + }; + + panelCount++; + } + }); + } else { + ungroupedFactories.push(factory); + } + }); + + const getVisTypeMenuItem = (visType: BaseVisType): EuiContextMenuPanelItemDescriptor => { + const { name, title, titleInWizard, description, icon = 'empty' } = visType; + return { + name: titleInWizard || title, + icon: icon as string, + onClick: createNewVisType(visType), + 'data-test-subj': `visType-${name}`, + toolTipContent: description, + }; + }; + + const getVisTypeAliasMenuItem = ( + visTypeAlias: VisTypeAlias + ): EuiContextMenuPanelItemDescriptor => { + const { name, title, description, icon = 'empty' } = visTypeAlias; + + return { + name: title, + icon, + onClick: createNewVisType(visTypeAlias), + 'data-test-subj': `visType-${name}`, + toolTipContent: description, + }; + }; + + const getEmbeddableFactoryMenuItem = ( + factory: EmbeddableFactoryDefinition + ): EuiContextMenuPanelItemDescriptor => { + const icon = factory?.getIconType ? factory.getIconType() : 'empty'; + + const toolTipContent = factory?.getDescription ? factory.getDescription() : undefined; + + return { + name: factory.getDisplayName(), + icon, + toolTipContent, + onClick: createNewEmbeddable(factory), + 'data-test-subj': `createNew-${factory.type}`, + }; + }; + + const editorMenuPanels = [ + { + id: 0, + items: [ + ...visTypeAliases.map(getVisTypeAliasMenuItem), + ...Object.values(factoryGroupMap).map(({ id, appName, icon, panelId }) => ({ + name: appName, + icon, + panel: panelId, + 'data-test-subj': `canvasEditorMenu-${id}Group`, + })), + ...ungroupedFactories.map(getEmbeddableFactoryMenuItem), + ...promotedVisTypes.map(getVisTypeMenuItem), + ], + }, + ...Object.values(factoryGroupMap).map( + ({ appName, panelId, factories: groupFactories }: FactoryGroup) => ({ + id: panelId, + title: appName, + items: groupFactories.map(getEmbeddableFactoryMenuItem), + }) + ), + ]; + + return ( + + {() => ( + + )} + + ); +}; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.tsx b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.tsx new file mode 100644 index 0000000000000..dad34e6983c5d --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.tsx @@ -0,0 +1,147 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC, useCallback } from 'react'; +import { useLocation } from 'react-router-dom'; +import { trackCanvasUiMetric, METRIC_TYPE } from '../../../../public/lib/ui_metric'; +import { + useEmbeddablesService, + usePlatformService, + useVisualizationsService, +} from '../../../services'; +import { + BaseVisType, + VisGroups, + VisTypeAlias, +} from '../../../../../../../src/plugins/visualizations/public'; +import { + EmbeddableFactoryDefinition, + EmbeddableInput, +} from '../../../../../../../src/plugins/embeddable/public'; +import { CANVAS_APP } from '../../../../common/lib'; +import { encode } from '../../../../common/lib/embeddable_dataurl'; +import { ElementSpec } from '../../../../types'; +import { EditorMenu as Component } from './editor_menu.component'; + +interface Props { + /** + * Handler for adding a selected element to the workpad + */ + addElement: (element: Partial) => void; +} + +export const EditorMenu: FC = ({ addElement }) => { + const embeddablesService = useEmbeddablesService(); + const { pathname, search } = useLocation(); + const platformService = usePlatformService(); + const stateTransferService = embeddablesService.getStateTransfer(); + const visualizationsService = useVisualizationsService(); + const IS_DARK_THEME = platformService.getUISetting('theme:darkMode'); + + const createNewVisType = useCallback( + (visType?: BaseVisType | VisTypeAlias) => () => { + let path = ''; + let appId = ''; + + if (visType) { + if (trackCanvasUiMetric) { + trackCanvasUiMetric(METRIC_TYPE.CLICK, `${visType.name}:create`); + } + + if ('aliasPath' in visType) { + appId = visType.aliasApp; + path = visType.aliasPath; + } else { + appId = 'visualize'; + path = `#/create?type=${encodeURIComponent(visType.name)}`; + } + } else { + appId = 'visualize'; + path = '#/create?'; + } + + stateTransferService.navigateToEditor(appId, { + path, + state: { + originatingApp: CANVAS_APP, + originatingPath: `#/${pathname}${search}`, + }, + }); + }, + [stateTransferService, pathname, search] + ); + + const createNewEmbeddable = useCallback( + (factory: EmbeddableFactoryDefinition) => async () => { + if (trackCanvasUiMetric) { + trackCanvasUiMetric(METRIC_TYPE.CLICK, factory.type); + } + let embeddableInput; + if (factory.getExplicitInput) { + embeddableInput = await factory.getExplicitInput(); + } else { + const newEmbeddable = await factory.create({} as EmbeddableInput); + embeddableInput = newEmbeddable?.getInput(); + } + + if (embeddableInput) { + const config = encode(embeddableInput); + const expression = `embeddable config="${config}" + type="${factory.type}" +| render`; + + addElement({ expression }); + } + }, + [addElement] + ); + + const getVisTypesByGroup = (group: VisGroups): BaseVisType[] => + visualizationsService + .getByGroup(group) + .sort(({ name: a }: BaseVisType | VisTypeAlias, { name: b }: BaseVisType | VisTypeAlias) => { + if (a < b) { + return -1; + } + if (a > b) { + return 1; + } + return 0; + }) + .filter(({ hidden }: BaseVisType) => !hidden); + + const visTypeAliases = visualizationsService + .getAliases() + .sort(({ promotion: a = false }: VisTypeAlias, { promotion: b = false }: VisTypeAlias) => + a === b ? 0 : a ? -1 : 1 + ); + + const factories = embeddablesService + ? Array.from(embeddablesService.getEmbeddableFactories()).filter( + ({ type, isEditable, canCreateNew, isContainerType }) => + isEditable() && + !isContainerType && + canCreateNew() && + !['visualization', 'ml'].some((factoryType) => { + return type.includes(factoryType); + }) + ) + : []; + + const promotedVisTypes = getVisTypesByGroup(VisGroups.PROMOTED); + + return ( + + ); +}; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/index.ts new file mode 100644 index 0000000000000..0f903b1bbbe2e --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { EditorMenu } from './editor_menu'; +export { EditorMenu as EditorMenuComponent } from './editor_menu.component'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx index 8ac581b0866a4..1cfab236d9a9c 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx @@ -12,11 +12,11 @@ import { EuiContextMenu, EuiIcon, EuiContextMenuPanelItemDescriptor } from '@ela import { i18n } from '@kbn/i18n'; import { PrimaryActionPopover } from '../../../../../../../src/plugins/presentation_util/public'; import { getId } from '../../../lib/get_id'; -import { ClosePopoverFn } from '../../popover'; import { CONTEXT_MENU_TOP_BORDER_CLASSNAME } from '../../../../common/lib'; import { ElementSpec } from '../../../../types'; import { flattenPanelTree } from '../../../lib/flatten_panel_tree'; import { AssetManager } from '../../asset_manager'; +import { ClosePopoverFn } from '../../popover'; import { SavedElementsModal } from '../../saved_elements_modal'; interface CategorizedElementLists { @@ -112,7 +112,7 @@ const categorizeElementsByType = (elements: ElementSpec[]): { [key: string]: Ele return categories; }; -interface Props { +export interface Props { /** * Dictionary of elements from elements registry */ @@ -120,7 +120,7 @@ interface Props { /** * Handler for adding a selected element to the workpad */ - addElement: (element: ElementSpec) => void; + addElement: (element: Partial) => void; } export const ElementMenu: FunctionComponent = ({ elements, addElement }) => { diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.ts index 52c8daece7690..037bb84b0cdba 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.ts +++ b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.ts @@ -5,5 +5,4 @@ * 2.0. */ -export { ElementMenu } from './element_menu'; -export { ElementMenu as ElementMenuComponent } from './element_menu.component'; +export { ElementMenu } from './element_menu.component'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx index f031d7c263199..b84e4faf2925e 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx @@ -27,6 +27,7 @@ import { ElementMenu } from './element_menu'; import { ShareMenu } from './share_menu'; import { ViewMenu } from './view_menu'; import { LabsControl } from './labs_control'; +import { EditorMenu } from './editor_menu'; const strings = { getFullScreenButtonAriaLabel: () => @@ -160,24 +161,22 @@ export const WorkpadHeader: FC = ({ + {isWriteable && ( + + + {{ + primaryActionButton: , + quickButtonGroup: , + addFromLibraryButton: , + extraButtons: [], + }} + + + )} - {isWriteable && ( - - - {{ - primaryActionButton: ( - - ), - quickButtonGroup: , - addFromLibraryButton: , - }} - - - )} @@ -192,6 +191,7 @@ export const WorkpadHeader: FC = ({ + diff --git a/x-pack/plugins/canvas/public/plugin.tsx b/x-pack/plugins/canvas/public/plugin.tsx index 5d1f05fdbe8bf..d2375064603c3 100644 --- a/x-pack/plugins/canvas/public/plugin.tsx +++ b/x-pack/plugins/canvas/public/plugin.tsx @@ -8,6 +8,7 @@ import { BehaviorSubject } from 'rxjs'; import type { SharePluginSetup } from 'src/plugins/share/public'; import { ChartsPluginSetup, ChartsPluginStart } from 'src/plugins/charts/public'; +import { VisualizationsStart } from 'src/plugins/visualizations/public'; import { ReportingStart } from '../../reporting/public'; import { CoreSetup, @@ -63,6 +64,7 @@ export interface CanvasStartDeps { charts: ChartsPluginStart; data: DataPublicPluginStart; presentationUtil: PresentationUtilPluginStart; + visualizations: VisualizationsStart; spaces?: SpacesPluginStart; } @@ -122,7 +124,12 @@ export class CanvasPlugin const { pluginServices } = await import('./services'); pluginServices.setRegistry( - pluginServiceRegistry.start({ coreStart, startPlugins, initContext: this.initContext }) + pluginServiceRegistry.start({ + coreStart, + startPlugins, + appUpdater: this.appUpdater, + initContext: this.initContext, + }) ); // Load application bundle diff --git a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad.ts b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad.ts index 963a69a8f11f0..f117998bbd3eb 100644 --- a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad.ts +++ b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad.ts @@ -50,7 +50,7 @@ export const useWorkpad = ( setResolveInfo({ aliasId, outcome, id: workpadId }); // If it's an alias match, we know we are going to redirect so don't even dispatch that we got the workpad - if (outcome !== 'aliasMatch') { + if (storedWorkpad.id !== workpadId && outcome !== 'aliasMatch') { workpad.aliasId = aliasId; dispatch(setAssets(assets)); @@ -61,7 +61,7 @@ export const useWorkpad = ( setError(e as Error | string); } })(); - }, [workpadId, dispatch, setError, loadPages, workpadResolve]); + }, [workpadId, dispatch, setError, loadPages, workpadResolve, storedWorkpad.id]); useEffect(() => { // If the resolved info is not for the current workpad id, bail out diff --git a/x-pack/plugins/canvas/public/services/embeddables.ts b/x-pack/plugins/canvas/public/services/embeddables.ts index 24d7a57e086f2..26b150b7a5349 100644 --- a/x-pack/plugins/canvas/public/services/embeddables.ts +++ b/x-pack/plugins/canvas/public/services/embeddables.ts @@ -5,8 +5,12 @@ * 2.0. */ -import { EmbeddableFactory } from '../../../../../src/plugins/embeddable/public'; +import { + EmbeddableFactory, + EmbeddableStateTransfer, +} from '../../../../../src/plugins/embeddable/public'; export interface CanvasEmbeddablesService { getEmbeddableFactories: () => IterableIterator; + getStateTransfer: () => EmbeddableStateTransfer; } diff --git a/x-pack/plugins/canvas/public/services/index.ts b/x-pack/plugins/canvas/public/services/index.ts index f4292810b8089..ed55f919e4c76 100644 --- a/x-pack/plugins/canvas/public/services/index.ts +++ b/x-pack/plugins/canvas/public/services/index.ts @@ -17,6 +17,7 @@ import { CanvasNavLinkService } from './nav_link'; import { CanvasNotifyService } from './notify'; import { CanvasPlatformService } from './platform'; import { CanvasReportingService } from './reporting'; +import { CanvasVisualizationsService } from './visualizations'; import { CanvasWorkpadService } from './workpad'; export interface CanvasPluginServices { @@ -28,6 +29,7 @@ export interface CanvasPluginServices { notify: CanvasNotifyService; platform: CanvasPlatformService; reporting: CanvasReportingService; + visualizations: CanvasVisualizationsService; workpad: CanvasWorkpadService; } @@ -44,4 +46,6 @@ export const useNavLinkService = () => (() => pluginServices.getHooks().navLink. export const useNotifyService = () => (() => pluginServices.getHooks().notify.useService())(); export const usePlatformService = () => (() => pluginServices.getHooks().platform.useService())(); export const useReportingService = () => (() => pluginServices.getHooks().reporting.useService())(); +export const useVisualizationsService = () => + (() => pluginServices.getHooks().visualizations.useService())(); export const useWorkpadService = () => (() => pluginServices.getHooks().workpad.useService())(); diff --git a/x-pack/plugins/canvas/public/services/kibana/embeddables.ts b/x-pack/plugins/canvas/public/services/kibana/embeddables.ts index 054b9da7409fb..8d1a86edab3d8 100644 --- a/x-pack/plugins/canvas/public/services/kibana/embeddables.ts +++ b/x-pack/plugins/canvas/public/services/kibana/embeddables.ts @@ -16,4 +16,5 @@ export type EmbeddablesServiceFactory = KibanaPluginServiceFactory< export const embeddablesServiceFactory: EmbeddablesServiceFactory = ({ startPlugins }) => ({ getEmbeddableFactories: startPlugins.embeddable.getEmbeddableFactories, + getStateTransfer: startPlugins.embeddable.getStateTransfer, }); diff --git a/x-pack/plugins/canvas/public/services/kibana/index.ts b/x-pack/plugins/canvas/public/services/kibana/index.ts index 1eb010e8d6f9d..91767947bc0a6 100644 --- a/x-pack/plugins/canvas/public/services/kibana/index.ts +++ b/x-pack/plugins/canvas/public/services/kibana/index.ts @@ -22,6 +22,7 @@ import { navLinkServiceFactory } from './nav_link'; import { notifyServiceFactory } from './notify'; import { platformServiceFactory } from './platform'; import { reportingServiceFactory } from './reporting'; +import { visualizationsServiceFactory } from './visualizations'; import { workpadServiceFactory } from './workpad'; export { customElementServiceFactory } from './custom_element'; @@ -31,6 +32,7 @@ export { labsServiceFactory } from './labs'; export { notifyServiceFactory } from './notify'; export { platformServiceFactory } from './platform'; export { reportingServiceFactory } from './reporting'; +export { visualizationsServiceFactory } from './visualizations'; export { workpadServiceFactory } from './workpad'; export const pluginServiceProviders: PluginServiceProviders< @@ -45,6 +47,7 @@ export const pluginServiceProviders: PluginServiceProviders< notify: new PluginServiceProvider(notifyServiceFactory), platform: new PluginServiceProvider(platformServiceFactory), reporting: new PluginServiceProvider(reportingServiceFactory), + visualizations: new PluginServiceProvider(visualizationsServiceFactory), workpad: new PluginServiceProvider(workpadServiceFactory), }; diff --git a/x-pack/plugins/canvas/public/services/kibana/visualizations.ts b/x-pack/plugins/canvas/public/services/kibana/visualizations.ts new file mode 100644 index 0000000000000..e319ec1c1f427 --- /dev/null +++ b/x-pack/plugins/canvas/public/services/kibana/visualizations.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { KibanaPluginServiceFactory } from '../../../../../../src/plugins/presentation_util/public'; +import { CanvasStartDeps } from '../../plugin'; +import { CanvasVisualizationsService } from '../visualizations'; + +export type VisualizationsServiceFactory = KibanaPluginServiceFactory< + CanvasVisualizationsService, + CanvasStartDeps +>; + +export const visualizationsServiceFactory: VisualizationsServiceFactory = ({ startPlugins }) => ({ + showNewVisModal: startPlugins.visualizations.showNewVisModal, + getByGroup: startPlugins.visualizations.getByGroup, + getAliases: startPlugins.visualizations.getAliases, +}); diff --git a/x-pack/plugins/canvas/public/services/stubs/embeddables.ts b/x-pack/plugins/canvas/public/services/stubs/embeddables.ts index 173d27563e2b2..9c2cf4d0650ab 100644 --- a/x-pack/plugins/canvas/public/services/stubs/embeddables.ts +++ b/x-pack/plugins/canvas/public/services/stubs/embeddables.ts @@ -14,4 +14,5 @@ const noop = (..._args: any[]): any => {}; export const embeddablesServiceFactory: EmbeddablesServiceFactory = () => ({ getEmbeddableFactories: noop, + getStateTransfer: noop, }); diff --git a/x-pack/plugins/canvas/public/services/stubs/index.ts b/x-pack/plugins/canvas/public/services/stubs/index.ts index 06a5ff49e9c04..2216013a29c12 100644 --- a/x-pack/plugins/canvas/public/services/stubs/index.ts +++ b/x-pack/plugins/canvas/public/services/stubs/index.ts @@ -22,6 +22,7 @@ import { navLinkServiceFactory } from './nav_link'; import { notifyServiceFactory } from './notify'; import { platformServiceFactory } from './platform'; import { reportingServiceFactory } from './reporting'; +import { visualizationsServiceFactory } from './visualizations'; import { workpadServiceFactory } from './workpad'; export { customElementServiceFactory } from './custom_element'; @@ -31,6 +32,7 @@ export { navLinkServiceFactory } from './nav_link'; export { notifyServiceFactory } from './notify'; export { platformServiceFactory } from './platform'; export { reportingServiceFactory } from './reporting'; +export { visualizationsServiceFactory } from './visualizations'; export { workpadServiceFactory } from './workpad'; export const pluginServiceProviders: PluginServiceProviders = { @@ -42,6 +44,7 @@ export const pluginServiceProviders: PluginServiceProviders; + +const noop = (..._args: any[]): any => {}; + +export const visualizationsServiceFactory: VisualizationsServiceFactory = () => ({ + showNewVisModal: noop, + getByGroup: noop, + getAliases: noop, +}); diff --git a/x-pack/plugins/canvas/public/services/visualizations.ts b/x-pack/plugins/canvas/public/services/visualizations.ts new file mode 100644 index 0000000000000..c602b1dd39f3d --- /dev/null +++ b/x-pack/plugins/canvas/public/services/visualizations.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { VisualizationsStart } from '../../../../../src/plugins/visualizations/public'; + +export interface CanvasVisualizationsService { + showNewVisModal: VisualizationsStart['showNewVisModal']; + getByGroup: VisualizationsStart['getByGroup']; + getAliases: VisualizationsStart['getAliases']; +} diff --git a/x-pack/plugins/canvas/public/state/reducers/embeddable.ts b/x-pack/plugins/canvas/public/state/reducers/embeddable.ts index 4cfdc7f21945f..092d4300d86b7 100644 --- a/x-pack/plugins/canvas/public/state/reducers/embeddable.ts +++ b/x-pack/plugins/canvas/public/state/reducers/embeddable.ts @@ -40,7 +40,7 @@ export const embeddableReducer = handleActions< const element = pageWithElement.elements.find((elem) => elem.id === elementId); - if (!element) { + if (!element || element.expression === embeddableExpression) { return workpadState; } diff --git a/x-pack/plugins/canvas/server/plugin.ts b/x-pack/plugins/canvas/server/plugin.ts index 4071b891e4c3d..ebe43ba76a46a 100644 --- a/x-pack/plugins/canvas/server/plugin.ts +++ b/x-pack/plugins/canvas/server/plugin.ts @@ -14,6 +14,7 @@ import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; import { BfetchServerSetup } from 'src/plugins/bfetch/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { HomeServerPluginSetup } from 'src/plugins/home/server'; +import { EmbeddableSetup } from 'src/plugins/embeddable/server'; import { ESSQL_SEARCH_STRATEGY } from '../common/lib/constants'; import { ReportingSetup } from '../../reporting/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; @@ -30,6 +31,7 @@ import { CanvasRouteHandlerContext, createWorkpadRouteContext } from './workpad_ interface PluginsSetup { expressions: ExpressionsServerSetup; + embeddable: EmbeddableSetup; features: FeaturesPluginSetup; home: HomeServerPluginSetup; bfetch: BfetchServerSetup; @@ -82,7 +84,12 @@ export class CanvasPlugin implements Plugin { const kibanaIndex = coreSetup.savedObjects.getKibanaIndex(); registerCanvasUsageCollector(plugins.usageCollection, kibanaIndex); - setupInterpreter(expressionsFork); + setupInterpreter(expressionsFork, { + embeddablePersistableStateService: { + extract: plugins.embeddable.extract, + inject: plugins.embeddable.inject, + }, + }); coreSetup.getStartServices().then(([_, depsStart]) => { const strategy = essqlSearchStrategyProvider(); diff --git a/x-pack/plugins/canvas/server/setup_interpreter.ts b/x-pack/plugins/canvas/server/setup_interpreter.ts index 2fe23eb86c086..849ad79717056 100644 --- a/x-pack/plugins/canvas/server/setup_interpreter.ts +++ b/x-pack/plugins/canvas/server/setup_interpreter.ts @@ -7,9 +7,15 @@ import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; import { functions } from '../canvas_plugin_src/functions/server'; -import { functions as externalFunctions } from '../canvas_plugin_src/functions/external'; +import { + initFunctions as initExternalFunctions, + InitializeArguments, +} from '../canvas_plugin_src/functions/external'; -export function setupInterpreter(expressions: ExpressionsServerSetup) { +export function setupInterpreter( + expressions: ExpressionsServerSetup, + dependencies: InitializeArguments +) { functions.forEach((f) => expressions.registerFunction(f)); - externalFunctions.forEach((f) => expressions.registerFunction(f)); + initExternalFunctions(dependencies).forEach((f) => expressions.registerFunction(f)); } diff --git a/x-pack/plugins/canvas/types/embeddables.ts b/x-pack/plugins/canvas/types/embeddables.ts new file mode 100644 index 0000000000000..b78efece59d8f --- /dev/null +++ b/x-pack/plugins/canvas/types/embeddables.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { TimeRange } from 'src/plugins/data/public'; +import { Filter } from '@kbn/es-query'; +import { EmbeddableInput as Input } from '../../../../src/plugins/embeddable/common/'; + +export type EmbeddableInput = Input & { + timeRange?: TimeRange; + filters?: Filter[]; + savedObjectId?: string; +}; diff --git a/x-pack/plugins/canvas/types/functions.ts b/x-pack/plugins/canvas/types/functions.ts index 2569e0b10685b..c80102915ed95 100644 --- a/x-pack/plugins/canvas/types/functions.ts +++ b/x-pack/plugins/canvas/types/functions.ts @@ -10,8 +10,8 @@ import { UnwrapPromiseOrReturn } from '@kbn/utility-types'; import { functions as commonFunctions } from '../canvas_plugin_src/functions/common'; import { functions as browserFunctions } from '../canvas_plugin_src/functions/browser'; import { functions as serverFunctions } from '../canvas_plugin_src/functions/server'; -import { functions as externalFunctions } from '../canvas_plugin_src/functions/external'; -import { initFunctions } from '../public/functions'; +import { initFunctions as initExternalFunctions } from '../canvas_plugin_src/functions/external'; +import { initFunctions as initClientFunctions } from '../public/functions'; /** * A `ExpressionFunctionFactory` is a powerful type used for any function that produces @@ -90,9 +90,11 @@ export type FunctionFactory = type CommonFunction = FunctionFactory; type BrowserFunction = FunctionFactory; type ServerFunction = FunctionFactory; -type ExternalFunction = FunctionFactory; +type ExternalFunction = FunctionFactory< + ReturnType extends Array ? U : never +>; type ClientFunctions = FunctionFactory< - ReturnType extends Array ? U : never + ReturnType extends Array ? U : never >; /** diff --git a/x-pack/plugins/canvas/types/index.ts b/x-pack/plugins/canvas/types/index.ts index 09ae1510be6da..930f337292088 100644 --- a/x-pack/plugins/canvas/types/index.ts +++ b/x-pack/plugins/canvas/types/index.ts @@ -9,6 +9,7 @@ export * from '../../../../src/plugins/expressions/common'; export * from './assets'; export * from './canvas'; export * from './elements'; +export * from './embeddables'; export * from './filters'; export * from './functions'; export * from './renderers'; From 9a7a5962fcb903d788c2a17fb7f1742a63ec57f2 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 10 Nov 2021 15:07:17 -0500 Subject: [PATCH 08/43] Add missing await (#118171) (#118195) Co-authored-by: Dmitry Shevchenko --- .../rule_execution_log/event_log_adapter/event_log_adapter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/event_log_adapter.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/event_log_adapter.ts index e5660da8d4cf4..8b55339aa9f02 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/event_log_adapter.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/event_log_adapter.ts @@ -104,7 +104,7 @@ export class EventLogAdapter implements IRuleExecutionLogClient { await this.savedObjectsAdapter.logStatusChange(args); if (args.metrics) { - this.logExecutionMetrics({ + await this.logExecutionMetrics({ ruleId: args.ruleId, ruleName: args.ruleName, ruleType: args.ruleType, From d0d58e2b79fbe794cf3e6f88bdddbbc8baf154c4 Mon Sep 17 00:00:00 2001 From: gchaps <33642766+gchaps@users.noreply.github.com> Date: Wed, 10 Nov 2021 12:41:15 -0800 Subject: [PATCH 09/43] [DOCS] Renames index pattern in management and monitoring (#117939) (#118225) * [DOCS] Renames index pattern in management, monitoring, and graph * [DOCS] Renames index pattern on landing page * Updates URL in doc link service * Update docs/management/advanced-options.asciidoc Co-authored-by: Lisa Cawley * Update docs/user/monitoring/kibana-alerts.asciidoc Co-authored-by: Lisa Cawley Co-authored-by: lcawl Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: lcawl Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/index-extra-title-page.html | 2 +- docs/management/advanced-options.asciidoc | 18 ++--- .../management-rollup-index-pattern.png | Bin 59955 -> 0 bytes ...ns.asciidoc => manage-data-views.asciidoc} | 70 ++++++++++-------- .../managing-saved-objects.asciidoc | 8 +- docs/management/numeral.asciidoc | 2 +- .../create_and_manage_rollups.asciidoc | 28 +++---- docs/redirects.asciidoc | 5 ++ docs/user/graph/configuring-graph.asciidoc | 2 +- docs/user/management.asciidoc | 10 +-- docs/user/monitoring/kibana-alerts.asciidoc | 28 +++---- .../public/doc_links/doc_links_service.ts | 2 +- 12 files changed, 90 insertions(+), 85 deletions(-) delete mode 100644 docs/management/images/management-rollup-index-pattern.png rename docs/management/{manage-index-patterns.asciidoc => manage-data-views.asciidoc} (76%) diff --git a/docs/index-extra-title-page.html b/docs/index-extra-title-page.html index 2621848ebea8a..ff1c879c0f409 100644 --- a/docs/index-extra-title-page.html +++ b/docs/index-extra-title-page.html @@ -64,7 +64,7 @@
  • Create an index patternCreate a data view
  • diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 56b7eb09252ed..7e7ff1137794c 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -2,7 +2,7 @@ == Advanced Settings *Advanced Settings* control the behavior of {kib}. For example, you can change the format used to display dates, -specify the default index pattern, and set the precision for displayed decimal values. +specify the default data view, and set the precision for displayed decimal values. . Open the main menu, then click *Stack Management > Advanced Settings*. . Scroll or search for the setting. @@ -134,10 +134,6 @@ value by the maximum number of aggregations in each visualization. [[history-limit]]`history:limit`:: In fields that have history, such as query inputs, show this many recent values. -[[indexpattern-placeholder]]`indexPattern:placeholder`:: -The default placeholder value to use in -*Management > Index Patterns > Create Index Pattern*. - [[metafields]]`metaFields`:: Fields that exist outside of `_source`. Kibana merges these fields into the document when displaying it. @@ -283,7 +279,7 @@ value is 5. [[context-tiebreakerfields]]`context:tieBreakerFields`:: A comma-separated list of fields to use for breaking a tie between documents that have the same timestamp value. The first field that is present and sortable -in the current index pattern is used. +in the current data view is used. [[defaultcolumns]]`defaultColumns`:: The columns that appear by default on the *Discover* page. The default is @@ -296,7 +292,7 @@ The number of rows to show in the *Discover* table. Specifies the maximum number of fields to show in the document column of the *Discover* table. [[discover-modify-columns-on-switch]]`discover:modifyColumnsOnSwitch`:: -When enabled, removes the columns that are not in the new index pattern. +When enabled, removes the columns that are not in the new data view. [[discover-sample-size]]`discover:sampleSize`:: Specifies the number of rows to display in the *Discover* table. @@ -314,7 +310,7 @@ does not have an effect when loading a saved search. When enabled, displays multi-fields in the expanded document view. [[discover-sort-defaultorder]]`discover:sort:defaultOrder`:: -The default sort direction for time-based index patterns. +The default sort direction for time-based data views. [[doctable-hidetimecolumn]]`doc_table:hideTimeColumn`:: Hides the "Time" column in *Discover* and in all saved searches on dashboards. @@ -391,8 +387,8 @@ A custom image to use in the footer of the PDF. ==== Rollup [horizontal] -[[rollups-enableindexpatterns]]`rollups:enableIndexPatterns`:: -Enables the creation of index patterns that capture rollup indices, which in +[[rollups-enabledataviews]]`rollups:enableDataViews`:: +Enables the creation of data views that capture rollup indices, which in turn enables visualizations based on rollup data. Refresh the page to apply the changes. @@ -408,7 +404,7 @@ to use when `courier:setRequestPreference` is set to "custom". [[courier-ignorefilteriffieldnotinindex]]`courier:ignoreFilterIfFieldNotInIndex`:: Skips filters that apply to fields that don't exist in the index for a visualization. Useful when dashboards consist of visualizations from multiple -index patterns. +data views. [[courier-maxconcurrentshardrequests]]`courier:maxConcurrentShardRequests`:: Controls the {ref}/search-multi-search.html[max_concurrent_shard_requests] diff --git a/docs/management/images/management-rollup-index-pattern.png b/docs/management/images/management-rollup-index-pattern.png deleted file mode 100644 index de7976e63f0503cfb861604f2b05b9335b9924a7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 59955 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 Jp#*> and <>. +To add runtime fields to your data views, open the data view you want to change, +then define the field values by emitting a single value using +the {ref}/modules-scripting-painless.html[Painless scripting language]. +You can also add runtime fields in <> and <>. -. Open the main menu, then click *Stack Management > Index Patterns*. +. Open the main menu, then click *Stack Management > Data Views*. -. Select the index pattern you want to add the runtime field to, then click *Add field*. +. Select the data view that you want to add the runtime field to, then click *Add field*. . Enter the field *Name*, then select the *Type*. -. Select *Set custom label*, then enter the label you want to display where the index pattern is used, such as *Discover*. +. Select *Set custom label*, then enter the label you want to display where the data view is used, +such as *Discover*. -. Select *Set value*, then define the script. The script must match the *Type*, or the index pattern fails anywhere it is used. +. Select *Set value*, then define the script. The script must match the *Type*, or the data view fails anywhere it is used. . To help you define the script, use the *Preview*: @@ -46,7 +53,8 @@ To add runtime fields to your index patterns, open the index pattern you want to * To filter the fields list, enter the keyword in *Filter fields*. -* To pin frequently used fields to the top of the list, hover over the field, then click image:images/stackManagement-indexPatterns-pinRuntimeField-7.15.png[Icon to pin field to the top of the list]. +* To pin frequently used fields to the top of the list, hover over the field, +then click image:images/stackManagement-indexPatterns-pinRuntimeField-7.15.png[Icon to pin field to the top of the list]. . Click *Create field*. @@ -54,7 +62,7 @@ To add runtime fields to your index patterns, open the index pattern you want to [[runtime-field-examples]] ==== Runtime field examples -Try the runtime field examples on your own using the <> data index pattern. +Try the runtime field examples on your own using the <> data. [float] [[simple-hello-world-example]] @@ -110,7 +118,7 @@ if (source != null) { emit(source); return; } -else { +else { emit("None"); } ---- @@ -123,7 +131,7 @@ def source = doc['machine.os.keyword'].value; if (source != "") { emit(source); } -else { +else { emit("None"); } ---- @@ -132,15 +140,15 @@ else { [[manage-runtime-fields]] ==== Manage runtime fields -Edit the settings for runtime fields, or remove runtime fields from index patterns. +Edit the settings for runtime fields, or remove runtime fields from data views. -. Open the main menu, then click *Stack Management > Index Patterns*. +. Open the main menu, then click *Stack Management > Data Views*. -. Select the index pattern that contains the runtime field you want to manage, then open the runtime field edit options or delete the runtime field. +. Select the data view that contains the runtime field you want to manage, then open the runtime field edit options or delete the runtime field. [float] [[scripted-fields]] -=== Add scripted fields to index patterns +=== Add scripted fields to data views deprecated::[7.13,Use {ref}/runtime.html[runtime fields] instead of scripted fields. Runtime fields support Painless scripts and provide greater flexibility.] @@ -168,11 +176,11 @@ https://www.elastic.co/blog/using-painless-kibana-scripted-fields[Using Painless [[create-scripted-field]] ==== Create scripted fields -Create and add scripted fields to your index patterns. +Create and add scripted fields to your data views. -. Open the main menu, then click *Stack Management > Index Patterns*. +. Open the main menu, then click *Stack Management > Data Views*. -. Select the index pattern you want to add a scripted field to. +. Select the data view you want to add a scripted field to. . Select the *Scripted fields* tab, then click *Add scripted field*. @@ -186,9 +194,9 @@ For more information about scripted fields in {es}, refer to {ref}/modules-scrip [[update-scripted-field]] ==== Manage scripted fields -. Open the main menu, then click *Stack Management > Index Patterns*. +. Open the main menu, then click *Stack Management > Data Views*. -. Select the index pattern that contains the scripted field you want to manage. +. Select the data view that contains the scripted field you want to manage. . Select the *Scripted fields* tab, then open the scripted field edit options or delete the scripted field. @@ -202,9 +210,9 @@ exceptions when you view the dynamically generated data. {kib} uses the same field types as {es}, however, some {es} field types are unsupported in {kib}. To customize how {kib} displays data fields, use the formatting options. -. Open the main menu, then click *Stack Management > Index Patterns*. +. Open the main menu, then click *Stack Management > Data Views*. -. Click the index pattern that contains the field you want to change. +. Click the data view that contains the field you want to change. . Find the field, then open the edit options (image:management/index-patterns/images/edit_icon.png[Data field edit icon]). @@ -261,4 +269,4 @@ include::field-formatters/string-formatter.asciidoc[] include::field-formatters/duration-formatter.asciidoc[] -include::field-formatters/color-formatter.asciidoc[] \ No newline at end of file +include::field-formatters/color-formatter.asciidoc[] diff --git a/docs/management/managing-saved-objects.asciidoc b/docs/management/managing-saved-objects.asciidoc index 5b39c6ad1c4cd..b9859575051af 100644 --- a/docs/management/managing-saved-objects.asciidoc +++ b/docs/management/managing-saved-objects.asciidoc @@ -2,10 +2,10 @@ == Saved Objects The *Saved Objects* UI helps you keep track of and manage your saved objects. These objects -store data for later use, including dashboards, visualizations, maps, index patterns, +store data for later use, including dashboards, visualizations, maps, data views, Canvas workpads, and more. -To get started, open the main menu, then click *Stack Management > Saved Objects*. +To get started, open the main menu, then click *Stack Management > Saved Objects*. [role="screenshot"] image::images/management-saved-objects.png[Saved Objects] @@ -85,7 +85,7 @@ You have two options for exporting saved objects. * Click *Export x objects*, and export objects by type. This action creates an NDJSON with all your saved objects. By default, the NDJSON includes child objects that are related to the saved -objects. Exported dashboards include their associated index patterns. +objects. Exported dashboards include their associated data views. NOTE: The <> configuration setting limits the number of saved objects which may be exported. @@ -120,7 +120,7 @@ If you access an object whose index has been deleted, you can: * Recreate the index so you can continue using the object. * Delete the object and recreate it using a different index. * Change the index name in the object's `reference` array to point to an existing -index pattern. This is useful if the index you were working with has been renamed. +data view. This is useful if the index you were working with has been renamed. WARNING: Validation is not performed for object properties. Submitting an invalid change will render the object unusable. A more failsafe approach is to use diff --git a/docs/management/numeral.asciidoc b/docs/management/numeral.asciidoc index 893873eb1075a..d6c8fbc9011fc 100644 --- a/docs/management/numeral.asciidoc +++ b/docs/management/numeral.asciidoc @@ -9,7 +9,7 @@ they are now maintained by {kib}. Numeral formatting patterns are used in multiple places in {kib}, including: * <> -* <> +* <> * <> * <> diff --git a/docs/management/rollups/create_and_manage_rollups.asciidoc b/docs/management/rollups/create_and_manage_rollups.asciidoc index 51821a935d3f5..bdfd3f65b3c87 100644 --- a/docs/management/rollups/create_and_manage_rollups.asciidoc +++ b/docs/management/rollups/create_and_manage_rollups.asciidoc @@ -5,7 +5,7 @@ experimental::[] A rollup job is a periodic task that aggregates data from indices specified -by an index pattern, and then rolls it into a new index. Rollup indices are a good way to +by a data view, and then rolls it into a new index. Rollup indices are a good way to compactly store months or years of historical data for use in visualizations and reports. @@ -33,9 +33,9 @@ the process. You fill in the name, data flow, and how often you want to roll up the data. Then you define a date histogram aggregation for the rollup job and optionally define terms, histogram, and metrics aggregations. -When defining the index pattern, you must enter a name that is different than +When defining the data view, you must enter a name that is different than the output rollup index. Otherwise, the job -will attempt to capture the data in the rollup index. For example, if your index pattern is `metricbeat-*`, +will attempt to capture the data in the rollup index. For example, if your data view is `metricbeat-*`, you can name your rollup index `rollup-metricbeat`, but not `metricbeat-rollup`. [role="screenshot"] @@ -66,7 +66,7 @@ You can read more at {ref}/rollup-job-config.html[rollup job configuration]. This example creates a rollup job to capture log data from sample web logs. Before you start, <>. -In this example, you want data that is older than 7 days in the target index pattern `kibana_sample_data_logs` +In this example, you want data that is older than 7 days in the target data view `kibana_sample_data_logs` to roll up into the `rollup_logstash` index. You’ll bucket the rolled up data on an hourly basis, using 60m for the time bucket configuration. This allows for more granular queries, such as 2h and 12h. @@ -85,7 +85,7 @@ As you walk through the *Create rollup job* UI, enter the data: |Name |`logs_job` -|Index pattern +|Data view |`kibana_sample_data_logs` |Rollup index name @@ -139,27 +139,23 @@ rollup index, or you can remove or archive it using < Index Patterns*. +. Open the main menu, then click *Stack Management > Data Views*. -. Click *Create index pattern*, and select *Rollup index pattern* from the dropdown. -+ -[role="screenshot"] -image::images/management-rollup-index-pattern.png[][Create rollup index pattern] +. Click *Create data view*, and select *Rollup data view* from the dropdown. -. Enter *rollup_logstash,kibana_sample_logs* as your *Index Pattern* and `@timestamp` +. Enter *rollup_logstash,kibana_sample_logs* as your *Data View* and `@timestamp` as the *Time Filter field name*. + -The notation for a combination index pattern with both raw and rolled up data -is `rollup_logstash,kibana_sample_data_logs`. In this index pattern, `rollup_logstash` -matches the rolled up index pattern and `kibana_sample_data_logs` matches the index -pattern for raw data. +The notation for a combination data view with both raw and rolled up data +is `rollup_logstash,kibana_sample_data_logs`. In this data view, `rollup_logstash` +matches the rolled up data view and `kibana_sample_data_logs` matches the data view for raw data. . Open the main menu, click *Dashboard*, then *Create dashboard*. . Set the <> to *Last 90 days*. . On the dashboard, click *Create visualization*. - + . Choose `rollup_logstash,kibana_sample_data_logs` as your source to see both the raw and rolled up data. + diff --git a/docs/redirects.asciidoc b/docs/redirects.asciidoc index 4010083d601b5..2b00ccd67dc96 100644 --- a/docs/redirects.asciidoc +++ b/docs/redirects.asciidoc @@ -363,3 +363,8 @@ This content has moved. Refer to <>. == Index patterns has been renamed to data views. This content has moved. Refer to <>. + +[role="exclude",id="managing-index-patterns"] +== Index patterns has been renamed to data views. + +This content has moved. Refer to <>. diff --git a/docs/user/graph/configuring-graph.asciidoc b/docs/user/graph/configuring-graph.asciidoc index 968e08db33d49..aa9e6e6db3ee6 100644 --- a/docs/user/graph/configuring-graph.asciidoc +++ b/docs/user/graph/configuring-graph.asciidoc @@ -8,7 +8,7 @@ By default, both the configuration and data are saved for the workspace: [horizontal] *configuration*:: -The selected index pattern, fields, colors, icons, +The selected data view, fields, colors, icons, and settings. *data*:: The visualized content (the vertices and connections displayed in diff --git a/docs/user/management.asciidoc b/docs/user/management.asciidoc index 1f38d50e2d0bd..9d6392c39ba84 100644 --- a/docs/user/management.asciidoc +++ b/docs/user/management.asciidoc @@ -4,7 +4,7 @@ [partintro] -- *Stack Management* is home to UIs for managing all things Elastic Stack— -indices, clusters, licenses, UI settings, index patterns, spaces, and more. +indices, clusters, licenses, UI settings, data views, spaces, and more. Access to individual features is governed by {es} and {kib} privileges. @@ -128,12 +128,12 @@ Kerberos, PKI, OIDC, and SAML. [cols="50, 50"] |=== -a| <> -|Manage the data fields in the index patterns that retrieve your data from {es}. +a| <> +|Manage the fields in the data views that retrieve your data from {es}. | <> | Copy, edit, delete, import, and export your saved objects. -These include dashboards, visualizations, maps, index patterns, Canvas workpads, and more. +These include dashboards, visualizations, maps, data views, Canvas workpads, and more. | <> |Create, manage, and assign tags to your saved objects. @@ -183,7 +183,7 @@ include::{kib-repo-dir}/management/action-types.asciidoc[] include::{kib-repo-dir}/management/managing-licenses.asciidoc[] -include::{kib-repo-dir}/management/manage-index-patterns.asciidoc[] +include::{kib-repo-dir}/management/manage-data-views.asciidoc[] include::{kib-repo-dir}/management/numeral.asciidoc[] diff --git a/docs/user/monitoring/kibana-alerts.asciidoc b/docs/user/monitoring/kibana-alerts.asciidoc index 64ba8bf044e4f..f6deaed7fa3b9 100644 --- a/docs/user/monitoring/kibana-alerts.asciidoc +++ b/docs/user/monitoring/kibana-alerts.asciidoc @@ -5,21 +5,21 @@ The {stack} {monitor-features} provide <> out-of-the box to notify you of potential issues in the {stack}. These rules are preconfigured based on the -best practices recommended by Elastic. However, you can tailor them to meet your +best practices recommended by Elastic. However, you can tailor them to meet your specific needs. [role="screenshot"] image::user/monitoring/images/monitoring-kibana-alerting-notification.png["{kib} alerting notifications in {stack-monitor-app}"] -When you open *{stack-monitor-app}* for the first time, you will be asked to acknowledge the creation of these default rules. They are initially configured to detect and notify on various +When you open *{stack-monitor-app}* for the first time, you will be asked to acknowledge the creation of these default rules. They are initially configured to detect and notify on various conditions across your monitored clusters. You can view notifications for: *Cluster health*, *Resource utilization*, and *Errors and exceptions* for {es} in real time. -NOTE: The default {watcher} based "cluster alerts" for {stack-monitor-app} have -been recreated as rules in {kib} {alert-features}. For this reason, the existing -{watcher} email action +NOTE: The default {watcher} based "cluster alerts" for {stack-monitor-app} have +been recreated as rules in {kib} {alert-features}. For this reason, the existing +{watcher} email action `monitoring.cluster_alerts.email_notifications.email_address` no longer works. -The default action for all {stack-monitor-app} rules is to write to {kib} logs +The default action for all {stack-monitor-app} rules is to write to {kib} logs and display a notification in the UI. To review and modify existing *{stack-monitor-app}* rules, click *Enter setup mode* on the *Cluster overview* page. @@ -47,21 +47,21 @@ checks on a schedule time of 1 minute with a re-notify interval of 1 day. This rule checks for {es} nodes that use a high amount of JVM memory. By default, the condition is set at 85% or more averaged over the last 5 minutes. -The default rule checks on a schedule time of 1 minute with a re-notify interval of 1 day. +The default rule checks on a schedule time of 1 minute with a re-notify interval of 1 day. [discrete] [[kibana-alerts-missing-monitoring-data]] == Missing monitoring data -This rule checks for {es} nodes that stop sending monitoring data. By default, +This rule checks for {es} nodes that stop sending monitoring data. By default, the condition is set to missing for 15 minutes looking back 1 day. The default rule checks on a schedule -time of 1 minute with a re-notify interval of 6 hours. +time of 1 minute with a re-notify interval of 6 hours. [discrete] [[kibana-alerts-thread-pool-rejections]] == Thread pool rejections (search/write) -This rule checks for {es} nodes that experience thread pool rejections. By +This rule checks for {es} nodes that experience thread pool rejections. By default, the condition is set at 300 or more over the last 5 minutes. The default rule checks on a schedule time of 1 minute with a re-notify interval of 1 day. Thresholds can be set independently for `search` and `write` type rejections. @@ -72,14 +72,14 @@ independently for `search` and `write` type rejections. This rule checks for read exceptions on any of the replicated {es} clusters. The condition is met if 1 or more read exceptions are detected in the last hour. The -default rule checks on a schedule time of 1 minute with a re-notify interval of 6 hours. +default rule checks on a schedule time of 1 minute with a re-notify interval of 6 hours. [discrete] [[kibana-alerts-large-shard-size]] == Large shard size This rule checks for a large average shard size (across associated primaries) on -any of the specified index patterns in an {es} cluster. The condition is met if +any of the specified data views in an {es} cluster. The condition is met if an index's average shard size is 55gb or higher in the last 5 minutes. The default rule matches the pattern of `-.*` by running checks on a schedule time of 1 minute with a re-notify interval of 12 hours. @@ -124,8 +124,8 @@ valid for 30 days. == Alerts and rules [discrete] === Create default rules -This option can be used to create default rules in this kibana space. This is -useful for scenarios when you didn't choose to create these default rules initially +This option can be used to create default rules in this Kibana space. This is +useful for scenarios when you didn't choose to create these default rules initially or anytime later if the rules were accidentally deleted. NOTE: Some action types are subscription features, while others are free. diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index d4d8f7c972d7e..f6e2a71449460 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -152,7 +152,7 @@ export class DocLinksService { introduction: `${KIBANA_DOCS}index-patterns.html`, fieldFormattersNumber: `${KIBANA_DOCS}numeral.html`, fieldFormattersString: `${KIBANA_DOCS}field-formatters-string.html`, - runtimeFields: `${KIBANA_DOCS}managing-index-patterns.html#runtime-fields`, + runtimeFields: `${KIBANA_DOCS}managing-data-views.html#runtime-fields`, }, addData: `${KIBANA_DOCS}connect-to-elasticsearch.html`, kibana: `${KIBANA_DOCS}index.html`, From f0626831ab41733273a6ae169f1973e235221a35 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 10 Nov 2021 15:47:15 -0500 Subject: [PATCH 10/43] fix addd to case relative (#117487) (#118226) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Shahzad --- .../components/action_menu/action_menu.tsx | 8 ++++-- .../header/add_to_case_action.test.tsx | 28 +++++++++++++++++++ .../header/add_to_case_action.tsx | 8 ++++-- 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.tsx index 08b4a3b948c57..512a6389bbf72 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.tsx @@ -36,9 +36,11 @@ export function ExpViewActionMenuContent({ responsive={false} style={{ paddingRight: 20 }} > - - - + {timeRange && ( + + + + )} + ); + expect(await findByText('Add to case')).toBeInTheDocument(); + + expect(useAddToCaseHook).toHaveBeenCalledWith( + expect.objectContaining({ + lensAttributes: { + title: 'Performance distribution', + }, + timeRange: { + from: '2021-11-10T10:52:06.091Z', + to: '2021-11-10T10:52:06.091Z', + }, + }) + ); + }); + it('should be able to click add to case button', async function () { const initSeries = { data: [ diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/add_to_case_action.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/add_to_case_action.tsx index bc813a4980e78..1d230c765edae 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/add_to_case_action.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/add_to_case_action.tsx @@ -15,9 +15,10 @@ import { TypedLensByValueInput } from '../../../../../../lens/public'; import { useAddToCase } from '../hooks/use_add_to_case'; import { Case, SubCase } from '../../../../../../cases/common'; import { observabilityFeatureId } from '../../../../../common'; +import { parseRelativeDate } from '../components/date_range_picker'; export interface AddToCaseProps { - timeRange?: { from: string; to: string }; + timeRange: { from: string; to: string }; lensAttributes: TypedLensByValueInput['attributes'] | null; } @@ -31,11 +32,14 @@ export function AddToCaseAction({ lensAttributes, timeRange }: AddToCaseProps) { [http.basePath] ); + const absoluteFromDate = parseRelativeDate(timeRange.from); + const absoluteToDate = parseRelativeDate(timeRange.to, { roundUp: true }); + const { createCaseUrl, goToCreateCase, onCaseClicked, isCasesOpen, setIsCasesOpen, isSaving } = useAddToCase({ lensAttributes, getToastText, - timeRange, + timeRange: { from: absoluteFromDate.toISOString(), to: absoluteToDate.toISOString() }, }); const getAllCasesSelectorModalProps: AllCasesSelectorModalProps = { From f59fc870e96d28bbeaafbb1b7f12810472fb7350 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 10 Nov 2021 15:53:04 -0500 Subject: [PATCH 11/43] [docs] Alerting - index patterns => data views (#115613) (#118241) * [user docs - index patterns] index pattern => data view (#110421) * index patterns => data views * maps docs changes * add alerting docs * Apply suggestions from code review Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> * cleanup Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> Co-authored-by: Matthew Kime Co-authored-by: gchaps <33642766+gchaps@users.noreply.github.com> --- docs/user/alerting/rule-types/es-query.asciidoc | 2 +- docs/user/alerting/rule-types/geo-rule-types.asciidoc | 10 +++++----- docs/user/alerting/rule-types/index-threshold.asciidoc | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/user/alerting/rule-types/es-query.asciidoc b/docs/user/alerting/rule-types/es-query.asciidoc index 86367a6a2e2c0..e3ce35687260f 100644 --- a/docs/user/alerting/rule-types/es-query.asciidoc +++ b/docs/user/alerting/rule-types/es-query.asciidoc @@ -17,7 +17,7 @@ Define properties to detect the condition. [role="screenshot"] image::user/alerting/images/rule-types-es-query-conditions.png[Five clauses define the condition to detect] -Index:: This clause requires an *index or index pattern* and a *time field* that will be used for the *time window*. +Index:: This clause requires an *index or data view* and a *time field* that will be used for the *time window*. Size:: This clause specifies the number of documents to pass to the configured actions when the the threshold condition is met. {es} query:: This clause specifies the ES DSL query to execute. The number of documents that match this query will be evaluated against the threshold condition. Aggregations are not supported at this time. diff --git a/docs/user/alerting/rule-types/geo-rule-types.asciidoc b/docs/user/alerting/rule-types/geo-rule-types.asciidoc index 244cf90c855a7..454c51ad69860 100644 --- a/docs/user/alerting/rule-types/geo-rule-types.asciidoc +++ b/docs/user/alerting/rule-types/geo-rule-types.asciidoc @@ -10,17 +10,17 @@ In the event that an entity is contained within a boundary, an alert may be gene ==== Requirements To create a Tracking containment rule, the following requirements must be present: -- *Tracks index or index pattern*: An index containing a `geo_point` field, `date` field, +- *Tracks index or data view*: An index containing a `geo_point` field, `date` field, and some form of entity identifier. An entity identifier is a `keyword` or `number` field that consistently identifies the entity to be tracked. The data in this index should be dynamically updating so that there are entity movements to alert upon. -- *Boundaries index or index pattern*: An index containing `geo_shape` data, such as boundary data and bounding box data. +- *Boundaries index or data view*: An index containing `geo_shape` data, such as boundary data and bounding box data. This data is presumed to be static (not updating). Shape data matching the query is harvested once when the rule is created and anytime after when the rule is re-enabled after disablement. By design, current interval entity locations (_current_ is determined by `date` in -the *Tracked index or index pattern*) are queried to determine if they are contained +the *Tracked index or data view*) are queried to determine if they are contained within any monitored boundaries. Entity data should be somewhat "real time", meaning the dates of new documents aren’t older than the current time minus the amount of the interval. If data older than @@ -39,13 +39,13 @@ as well as 2 Kuery bars used to provide additional filtering context for each of [role="screenshot"] image::user/alerting/images/alert-types-tracking-containment-conditions.png[Five clauses define the condition to detect] -Index (entity):: This clause requires an *index or index pattern*, a *time field* that will be used for the *time window*, and a *`geo_point` field* for tracking. +Index (entity):: This clause requires an *index or data view*, a *time field* that will be used for the *time window*, and a *`geo_point` field* for tracking. When entity:: This clause specifies which crossing option to track. The values *Entered*, *Exited*, and *Crossed* can be selected to indicate which crossing conditions should trigger a rule. *Entered* alerts on entry into a boundary, *Exited* alerts on exit from a boundary, and *Crossed* alerts on all boundary crossings whether they be entrances or exits. -Index (Boundary):: This clause requires an *index or index pattern*, a *`geo_shape` field* +Index (Boundary):: This clause requires an *index or data view*, a *`geo_shape` field* identifying boundaries, and an optional *Human-readable boundary name* for better alerting messages. diff --git a/docs/user/alerting/rule-types/index-threshold.asciidoc b/docs/user/alerting/rule-types/index-threshold.asciidoc index 8c45c158414f4..c65b0f66b1b63 100644 --- a/docs/user/alerting/rule-types/index-threshold.asciidoc +++ b/docs/user/alerting/rule-types/index-threshold.asciidoc @@ -17,7 +17,7 @@ Define properties to detect the condition. [role="screenshot"] image::user/alerting/images/rule-types-index-threshold-conditions.png[Five clauses define the condition to detect] -Index:: This clause requires an *index or index pattern* and a *time field* that will be used for the *time window*. +Index:: This clause requires an *index or data view* and a *time field* that will be used for the *time window*. When:: This clause specifies how the value to be compared to the threshold is calculated. The value is calculated by aggregating a numeric field a the *time window*. The aggregation options are: `count`, `average`, `sum`, `min`, and `max`. When using `count` the document count is used, and an aggregation field is not necessary. Over/Grouped Over:: This clause lets you configure whether the aggregation is applied over all documents, or should be split into groups using a grouping field. If grouping is used, an <> will be created for each group when it exceeds the threshold. To limit the number of alerts on high cardinality fields, you must specify the number of groups to check against the threshold. Only the *top* groups are checked. Threshold:: This clause defines a threshold value and a comparison operator (one of `is above`, `is above or equals`, `is below`, `is below or equals`, or `is between`). The result of the aggregation is compared to this threshold. From ad9f62fad22f391e3a1d52f7050cd172d8865adc Mon Sep 17 00:00:00 2001 From: Rachel Shen Date: Wed, 10 Nov 2021 14:10:32 -0700 Subject: [PATCH 12/43] [Lens] fix focus on legend action popovers (#115066) (#118231) --- .../pie/public/utils/get_legend_actions.tsx | 9 +++++++-- .../xy/public/utils/get_legend_actions.tsx | 14 ++++++++++++-- .../shared_components/legend_action_popover.tsx | 8 +++++++- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/plugins/vis_types/pie/public/utils/get_legend_actions.tsx b/src/plugins/vis_types/pie/public/utils/get_legend_actions.tsx index cd1d1d71aaa76..2a27063a0e7d5 100644 --- a/src/plugins/vis_types/pie/public/utils/get_legend_actions.tsx +++ b/src/plugins/vis_types/pie/public/utils/get_legend_actions.tsx @@ -10,7 +10,7 @@ import React, { useState, useEffect, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiContextMenuPanelDescriptor, EuiIcon, EuiPopover, EuiContextMenu } from '@elastic/eui'; -import { LegendAction, SeriesIdentifier } from '@elastic/charts'; +import { LegendAction, SeriesIdentifier, useLegendAction } from '@elastic/charts'; import { DataPublicPluginStart } from '../../../../data/public'; import { PieVisParams } from '../types'; import { ClickTriggerEvent } from '../../../../charts/public'; @@ -30,6 +30,7 @@ export const getLegendActions = ( const [popoverOpen, setPopoverOpen] = useState(false); const [isfilterable, setIsfilterable] = useState(true); const filterData = useMemo(() => getFilterEventData(pieSeries), [pieSeries]); + const [ref, onClose] = useLegendAction(); useEffect(() => { (async () => setIsfilterable(await canFilter(filterData, actions)))(); @@ -82,6 +83,7 @@ export const getLegendActions = ( const Button = (
    setPopoverOpen(false)} + closePopover={() => { + setPopoverOpen(false); + onClose(); + }} panelPaddingSize="none" anchorPosition="upLeft" title={i18n.translate('visTypePie.legend.filterOptionsLegend', { diff --git a/src/plugins/vis_types/xy/public/utils/get_legend_actions.tsx b/src/plugins/vis_types/xy/public/utils/get_legend_actions.tsx index 98ace7dd57a39..d52e3a457f8e9 100644 --- a/src/plugins/vis_types/xy/public/utils/get_legend_actions.tsx +++ b/src/plugins/vis_types/xy/public/utils/get_legend_actions.tsx @@ -10,7 +10,12 @@ import React, { useState, useEffect, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiContextMenuPanelDescriptor, EuiIcon, EuiPopover, EuiContextMenu } from '@elastic/eui'; -import { LegendAction, XYChartSeriesIdentifier, SeriesName } from '@elastic/charts'; +import { + LegendAction, + XYChartSeriesIdentifier, + SeriesName, + useLegendAction, +} from '@elastic/charts'; import { ClickTriggerEvent } from '../../../../charts/public'; @@ -25,6 +30,7 @@ export const getLegendActions = ( const [isfilterable, setIsfilterable] = useState(false); const series = xySeries as XYChartSeriesIdentifier; const filterData = useMemo(() => getFilterEventData(series), [series]); + const [ref, onClose] = useLegendAction(); useEffect(() => { (async () => setIsfilterable(await canFilter(filterData)))(); @@ -69,6 +75,7 @@ export const getLegendActions = ( const Button = (
    setPopoverOpen(false)} + closePopover={() => { + setPopoverOpen(false); + onClose(); + }} panelPaddingSize="none" anchorPosition="upLeft" title={i18n.translate('visTypeXy.legend.filterOptionsLegend', { diff --git a/x-pack/plugins/lens/public/shared_components/legend_action_popover.tsx b/x-pack/plugins/lens/public/shared_components/legend_action_popover.tsx index b13603a15e52d..76992b35dac68 100644 --- a/x-pack/plugins/lens/public/shared_components/legend_action_popover.tsx +++ b/x-pack/plugins/lens/public/shared_components/legend_action_popover.tsx @@ -8,6 +8,7 @@ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiContextMenuPanelDescriptor, EuiIcon, EuiPopover, EuiContextMenu } from '@elastic/eui'; +import { useLegendAction } from '@elastic/charts'; import type { LensFilterEvent } from '../types'; export interface LegendActionPopoverProps { @@ -31,6 +32,7 @@ export const LegendActionPopover: React.FunctionComponent { const [popoverOpen, setPopoverOpen] = useState(false); + const [ref, onClose] = useLegendAction(); const panels: EuiContextMenuPanelDescriptor[] = [ { id: 'main', @@ -65,6 +67,7 @@ export const LegendActionPopover: React.FunctionComponent setPopoverOpen(false)} + closePopover={() => { + setPopoverOpen(false); + onClose(); + }} panelPaddingSize="none" anchorPosition="upLeft" title={i18n.translate('xpack.lens.shared.legend.filterOptionsLegend', { From cb9ddbc02e26d06f44c3a553414bac6ae86c11e5 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 10 Nov 2021 16:18:20 -0500 Subject: [PATCH 13/43] [Fleet] Remove force flag from agent instruction (#118099) (#118233) Co-authored-by: Nicolas Chaulet --- .../components/install_command_utils.test.ts | 57 +++++++++---------- .../components/install_command_utils.ts | 6 +- .../enrollment_instructions/manual/index.tsx | 4 +- 3 files changed, 33 insertions(+), 34 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/install_command_utils.test.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/install_command_utils.test.ts index b4e7982c52f7b..62580a1445f06 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/install_command_utils.test.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/install_command_utils.test.ts @@ -17,9 +17,9 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - "sudo ./elastic-agent install -f \\\\ - --fleet-server-es=http://elasticsearch:9200 \\\\ - --fleet-server-service-token=service-token-1" + "sudo ./elastic-agent install \\\\ + --fleet-server-es=http://elasticsearch:9200 \\\\ + --fleet-server-service-token=service-token-1" `); }); @@ -31,9 +31,9 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - ".\\\\elastic-agent.exe install -f \` - --fleet-server-es=http://elasticsearch:9200 \` - --fleet-server-service-token=service-token-1" + ".\\\\elastic-agent.exe install \` + --fleet-server-es=http://elasticsearch:9200 \` + --fleet-server-service-token=service-token-1" `); }); @@ -45,9 +45,9 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - "sudo elastic-agent enroll -f \\\\ - --fleet-server-es=http://elasticsearch:9200 \\\\ - --fleet-server-service-token=service-token-1" + "sudo elastic-agent enroll \\\\ + --fleet-server-es=http://elasticsearch:9200 \\\\ + --fleet-server-service-token=service-token-1" `); }); }); @@ -62,9 +62,9 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - "sudo ./elastic-agent install -f \\\\ - --fleet-server-es=http://elasticsearch:9200 \\\\ - --fleet-server-service-token=service-token-1 \\\\ + "sudo ./elastic-agent install \\\\ + --fleet-server-es=http://elasticsearch:9200 \\\\ + --fleet-server-service-token=service-token-1 \\\\ --fleet-server-policy=policy-1" `); }); @@ -78,9 +78,9 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - ".\\\\elastic-agent.exe install -f \` - --fleet-server-es=http://elasticsearch:9200 \` - --fleet-server-service-token=service-token-1 \` + ".\\\\elastic-agent.exe install \` + --fleet-server-es=http://elasticsearch:9200 \` + --fleet-server-service-token=service-token-1 \` --fleet-server-policy=policy-1" `); }); @@ -94,9 +94,9 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - "sudo elastic-agent enroll -f \\\\ - --fleet-server-es=http://elasticsearch:9200 \\\\ - --fleet-server-service-token=service-token-1 \\\\ + "sudo elastic-agent enroll \\\\ + --fleet-server-es=http://elasticsearch:9200 \\\\ + --fleet-server-service-token=service-token-1 \\\\ --fleet-server-policy=policy-1" `); }); @@ -115,9 +115,8 @@ describe('getInstallCommandForPlatform', () => { expect(res).toMatchInlineSnapshot(` "sudo ./elastic-agent install --url=http://fleetserver:8220 \\\\ - -f \\\\ - --fleet-server-es=http://elasticsearch:9200 \\\\ - --fleet-server-service-token=service-token-1 \\\\ + --fleet-server-es=http://elasticsearch:9200 \\\\ + --fleet-server-service-token=service-token-1 \\\\ --fleet-server-policy=policy-1 \\\\ --certificate-authorities= \\\\ --fleet-server-es-ca= \\\\ @@ -138,9 +137,8 @@ describe('getInstallCommandForPlatform', () => { expect(res).toMatchInlineSnapshot(` ".\\\\elastic-agent.exe install --url=http://fleetserver:8220 \` - -f \` - --fleet-server-es=http://elasticsearch:9200 \` - --fleet-server-service-token=service-token-1 \` + --fleet-server-es=http://elasticsearch:9200 \` + --fleet-server-service-token=service-token-1 \` --fleet-server-policy=policy-1 \` --certificate-authorities= \` --fleet-server-es-ca= \` @@ -161,9 +159,8 @@ describe('getInstallCommandForPlatform', () => { expect(res).toMatchInlineSnapshot(` "sudo elastic-agent enroll --url=http://fleetserver:8220 \\\\ - -f \\\\ - --fleet-server-es=http://elasticsearch:9200 \\\\ - --fleet-server-service-token=service-token-1 \\\\ + --fleet-server-es=http://elasticsearch:9200 \\\\ + --fleet-server-service-token=service-token-1 \\\\ --fleet-server-policy=policy-1 \\\\ --certificate-authorities= \\\\ --fleet-server-es-ca= \\\\ @@ -181,9 +178,9 @@ describe('getInstallCommandForPlatform', () => { ); expect(res).toMatchInlineSnapshot(` - "sudo elastic-agent enroll -f \\\\ - --fleet-server-es=http://elasticsearch:9200 \\\\ - --fleet-server-service-token=service-token-1" + "sudo elastic-agent enroll \\\\ + --fleet-server-es=http://elasticsearch:9200 \\\\ + --fleet-server-service-token=service-token-1" `); }); }); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/install_command_utils.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/install_command_utils.ts index e129d7a4d5b4e..f5c40e8071691 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/install_command_utils.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/install_command_utils.ts @@ -20,10 +20,12 @@ export function getInstallCommandForPlatform( if (isProductionDeployment && fleetServerHost) { commandArguments += `--url=${fleetServerHost} ${newLineSeparator}\n`; + } else { + commandArguments += ` ${newLineSeparator}\n`; } - commandArguments += ` -f ${newLineSeparator}\n --fleet-server-es=${esHost}`; - commandArguments += ` ${newLineSeparator}\n --fleet-server-service-token=${serviceToken}`; + commandArguments += ` --fleet-server-es=${esHost}`; + commandArguments += ` ${newLineSeparator}\n --fleet-server-service-token=${serviceToken}`; if (policyId) { commandArguments += ` ${newLineSeparator}\n --fleet-server-policy=${policyId}`; } diff --git a/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/index.tsx b/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/index.tsx index 6d4d6a7172534..425fe6c6bd67e 100644 --- a/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/index.tsx +++ b/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/index.tsx @@ -38,9 +38,9 @@ export const ManualInstructions: React.FunctionComponent = ({ const enrollArgs = getfleetServerHostsEnrollArgs(apiKey, fleetServerHosts); - const linuxMacCommand = `sudo ./elastic-agent install -f ${enrollArgs}`; + const linuxMacCommand = `sudo ./elastic-agent install ${enrollArgs}`; - const windowsCommand = `.\\elastic-agent.exe install -f ${enrollArgs}`; + const windowsCommand = `.\\elastic-agent.exe install ${enrollArgs}`; return ( <> From 2ed71ef050b389fae5e7f3df972559dede11549d Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 10 Nov 2021 16:19:15 -0500 Subject: [PATCH 14/43] chore: rename test subjects to camel case (#118046) (#118235) * chore: rename test subjects to camel case * chore: rename test subjects to camel case /2 * Updates expectations * Restores files not in observability * Fixes test subjects * Fixes selector * Fixes selector Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Claudio Procida --- .../app/cases/callout/callout.test.tsx | 22 ++++++++--------- .../components/app/cases/callout/callout.tsx | 6 ++--- .../app/cases/callout/index.test.tsx | 24 +++++++++---------- .../app/cases/create/flyout.test.tsx | 2 +- .../components/app/cases/create/flyout.tsx | 2 +- .../components/app/news_feed/index.test.tsx | 2 +- .../public/components/app/news_feed/index.tsx | 2 +- .../public/components/app/resources/index.tsx | 2 +- .../public/pages/alerts/alerts_disclaimer.tsx | 4 ++-- .../pages/alerts/alerts_table_t_grid.tsx | 2 +- .../public/pages/alerts/filter_for_value.tsx | 4 ++-- .../alerts/workflow_status_filter.test.tsx | 2 +- .../pages/alerts/workflow_status_filter.tsx | 6 ++--- .../public/pages/cases/empty_page.tsx | 8 +++---- .../pages/cases/feature_no_permissions.tsx | 2 +- .../page_objects/observability_page.ts | 4 ++-- .../services/observability/alerts/common.ts | 10 ++++---- .../observability/alerts/alert_disclaimer.ts | 4 ++-- 18 files changed, 53 insertions(+), 55 deletions(-) diff --git a/x-pack/plugins/observability/public/components/app/cases/callout/callout.test.tsx b/x-pack/plugins/observability/public/components/app/cases/callout/callout.test.tsx index b0b6fc0e3b793..25d32d0cae884 100644 --- a/x-pack/plugins/observability/public/components/app/cases/callout/callout.test.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/callout/callout.test.tsx @@ -32,25 +32,25 @@ describe('Callout', () => { it('renders the callout', () => { const wrapper = mount(); - expect(wrapper.find(`[data-test-subj="case-callout-md5-hex"]`).exists()).toBeTruthy(); - expect(wrapper.find(`[data-test-subj="callout-messages-md5-hex"]`).exists()).toBeTruthy(); - expect(wrapper.find(`[data-test-subj="callout-dismiss-md5-hex"]`).exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="caseCallout-md5-hex"]`).exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="calloutMessages-md5-hex"]`).exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="calloutDismiss-md5-hex"]`).exists()).toBeTruthy(); }); it('hides the callout', () => { const wrapper = mount(); - expect(wrapper.find(`[data-test-subj="case-callout-md5-hex"]`).exists()).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="caseCallout-md5-hex"]`).exists()).toBeFalsy(); }); it('does not show any messages when the list is empty', () => { const wrapper = mount(); - expect(wrapper.find(`[data-test-subj="callout-messages-md5-hex"]`).exists()).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="calloutMessages-md5-hex"]`).exists()).toBeFalsy(); }); it('transform the button color correctly - primary', () => { const wrapper = mount(); const className = - wrapper.find(`button[data-test-subj="callout-dismiss-md5-hex"]`).first().prop('className') ?? + wrapper.find(`button[data-test-subj="calloutDismiss-md5-hex"]`).first().prop('className') ?? ''; expect(className.includes('euiButton--primary')).toBeTruthy(); }); @@ -58,7 +58,7 @@ describe('Callout', () => { it('transform the button color correctly - success', () => { const wrapper = mount(); const className = - wrapper.find(`button[data-test-subj="callout-dismiss-md5-hex"]`).first().prop('className') ?? + wrapper.find(`button[data-test-subj="calloutDismiss-md5-hex"]`).first().prop('className') ?? ''; expect(className.includes('euiButton--secondary')).toBeTruthy(); }); @@ -66,7 +66,7 @@ describe('Callout', () => { it('transform the button color correctly - warning', () => { const wrapper = mount(); const className = - wrapper.find(`button[data-test-subj="callout-dismiss-md5-hex"]`).first().prop('className') ?? + wrapper.find(`button[data-test-subj="calloutDismiss-md5-hex"]`).first().prop('className') ?? ''; expect(className.includes('euiButton--warning')).toBeTruthy(); }); @@ -74,15 +74,15 @@ describe('Callout', () => { it('transform the button color correctly - danger', () => { const wrapper = mount(); const className = - wrapper.find(`button[data-test-subj="callout-dismiss-md5-hex"]`).first().prop('className') ?? + wrapper.find(`button[data-test-subj="calloutDismiss-md5-hex"]`).first().prop('className') ?? ''; expect(className.includes('euiButton--danger')).toBeTruthy(); }); it('dismiss the callout correctly', () => { const wrapper = mount(); - expect(wrapper.find(`[data-test-subj="callout-dismiss-md5-hex"]`).exists()).toBeTruthy(); - wrapper.find(`button[data-test-subj="callout-dismiss-md5-hex"]`).simulate('click'); + expect(wrapper.find(`[data-test-subj="calloutDismiss-md5-hex"]`).exists()).toBeTruthy(); + wrapper.find(`button[data-test-subj="calloutDismiss-md5-hex"]`).simulate('click'); wrapper.update(); expect(defaultProps.handleDismissCallout).toHaveBeenCalledWith('md5-hex', 'primary'); diff --git a/x-pack/plugins/observability/public/components/app/cases/callout/callout.tsx b/x-pack/plugins/observability/public/components/app/cases/callout/callout.tsx index 5aa637c8f806d..15bd250c6ceb6 100644 --- a/x-pack/plugins/observability/public/components/app/cases/callout/callout.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/callout/callout.tsx @@ -35,10 +35,10 @@ function CallOutComponent({ ); return showCallOut && !isEmpty(messages) ? ( - - + + diff --git a/x-pack/plugins/observability/public/components/app/cases/callout/index.test.tsx b/x-pack/plugins/observability/public/components/app/cases/callout/index.test.tsx index 18d4dee45b9d5..bb0284398f4b3 100644 --- a/x-pack/plugins/observability/public/components/app/cases/callout/index.test.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/callout/index.test.tsx @@ -42,7 +42,7 @@ describe('CaseCallOut ', () => { ); const id = createCalloutId(['message-one', 'message-two']); - expect(wrapper.find(`[data-test-subj="callout-messages-${id}"]`).last().exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="calloutMessages-${id}"]`).last().exists()).toBeTruthy(); }); it('groups the messages correctly', () => { @@ -69,11 +69,9 @@ describe('CaseCallOut ', () => { const idPrimary = createCalloutId(['message-two']); expect( - wrapper.find(`[data-test-subj="case-callout-${idPrimary}"]`).last().exists() - ).toBeTruthy(); - expect( - wrapper.find(`[data-test-subj="case-callout-${idDanger}"]`).last().exists() + wrapper.find(`[data-test-subj="caseCallout-${idPrimary}"]`).last().exists() ).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="caseCallout-${idDanger}"]`).last().exists()).toBeTruthy(); }); it('dismisses the callout correctly', () => { @@ -91,9 +89,9 @@ describe('CaseCallOut ', () => { const id = createCalloutId(['message-one']); - expect(wrapper.find(`[data-test-subj="case-callout-${id}"]`).last().exists()).toBeTruthy(); - wrapper.find(`[data-test-subj="callout-dismiss-${id}"]`).last().simulate('click'); - expect(wrapper.find(`[data-test-subj="case-callout-${id}"]`).exists()).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="caseCallout-${id}"]`).last().exists()).toBeTruthy(); + wrapper.find(`[data-test-subj="calloutDismiss-${id}"]`).last().simulate('click'); + expect(wrapper.find(`[data-test-subj="caseCallout-${id}"]`).exists()).toBeFalsy(); }); it('persist the callout of type primary when dismissed', () => { @@ -112,7 +110,7 @@ describe('CaseCallOut ', () => { const id = createCalloutId(['message-one']); expect(securityLocalStorageMock.getMessages).toHaveBeenCalledWith(observabilityAppId); - wrapper.find(`[data-test-subj="callout-dismiss-${id}"]`).last().simulate('click'); + wrapper.find(`[data-test-subj="calloutDismiss-${id}"]`).last().simulate('click'); expect(securityLocalStorageMock.addMessage).toHaveBeenCalledWith(observabilityAppId, id); }); @@ -137,7 +135,7 @@ describe('CaseCallOut ', () => { ); - expect(wrapper.find(`[data-test-subj="case-callout-${id}"]`).last().exists()).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="caseCallout-${id}"]`).last().exists()).toBeFalsy(); }); it('do not persist a callout of type danger', () => { @@ -160,7 +158,7 @@ describe('CaseCallOut ', () => { ); const id = createCalloutId(['message-one']); - wrapper.find(`button[data-test-subj="callout-dismiss-${id}"]`).simulate('click'); + wrapper.find(`button[data-test-subj="calloutDismiss-${id}"]`).simulate('click'); wrapper.update(); expect(securityLocalStorageMock.addMessage).not.toHaveBeenCalled(); }); @@ -185,7 +183,7 @@ describe('CaseCallOut ', () => { ); const id = createCalloutId(['message-one']); - wrapper.find(`button[data-test-subj="callout-dismiss-${id}"]`).simulate('click'); + wrapper.find(`button[data-test-subj="calloutDismiss-${id}"]`).simulate('click'); wrapper.update(); expect(securityLocalStorageMock.addMessage).not.toHaveBeenCalled(); }); @@ -210,7 +208,7 @@ describe('CaseCallOut ', () => { ); const id = createCalloutId(['message-one']); - wrapper.find(`button[data-test-subj="callout-dismiss-${id}"]`).simulate('click'); + wrapper.find(`button[data-test-subj="calloutDismiss-${id}"]`).simulate('click'); wrapper.update(); expect(securityLocalStorageMock.addMessage).not.toHaveBeenCalled(); }); diff --git a/x-pack/plugins/observability/public/components/app/cases/create/flyout.test.tsx b/x-pack/plugins/observability/public/components/app/cases/create/flyout.test.tsx index dc3db695a3fbf..977263a9721ea 100644 --- a/x-pack/plugins/observability/public/components/app/cases/create/flyout.test.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/create/flyout.test.tsx @@ -45,7 +45,7 @@ describe('CreateCaseFlyout', () => { ); - expect(wrapper.find(`[data-test-subj='create-case-flyout']`).exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj='createCaseFlyout']`).exists()).toBeTruthy(); }); it('Closing modal calls onCloseCaseModal', () => { diff --git a/x-pack/plugins/observability/public/components/app/cases/create/flyout.tsx b/x-pack/plugins/observability/public/components/app/cases/create/flyout.tsx index 896bc27a97674..e8147ef7098ad 100644 --- a/x-pack/plugins/observability/public/components/app/cases/create/flyout.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/create/flyout.tsx @@ -54,7 +54,7 @@ function CreateCaseFlyoutComponent({ }: CreateCaseModalProps) { const { cases } = useKibana().services; return ( - +

    {i18n.CREATE_TITLE}

    diff --git a/x-pack/plugins/observability/public/components/app/news_feed/index.test.tsx b/x-pack/plugins/observability/public/components/app/news_feed/index.test.tsx index 97b5dbc679839..f6e641082e557 100644 --- a/x-pack/plugins/observability/public/components/app/news_feed/index.test.tsx +++ b/x-pack/plugins/observability/public/components/app/news_feed/index.test.tsx @@ -59,6 +59,6 @@ describe('News', () => { ); expect(getByText("What's new")).toBeInTheDocument(); expect(getAllByText('Read full story').length).toEqual(3); - expect(queryAllByTestId('news_image').length).toEqual(1); + expect(queryAllByTestId('newsImage').length).toEqual(1); }); }); diff --git a/x-pack/plugins/observability/public/components/app/news_feed/index.tsx b/x-pack/plugins/observability/public/components/app/news_feed/index.tsx index 68039f80b0b94..6f1a5f33b9ba7 100644 --- a/x-pack/plugins/observability/public/components/app/news_feed/index.tsx +++ b/x-pack/plugins/observability/public/components/app/news_feed/index.tsx @@ -85,7 +85,7 @@ function NewsItem({ item }: { item: INewsItem }) { {item.image_url?.en && (
    - + ); } diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_disclaimer.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_disclaimer.tsx index 4bf71574ea7f9..1d1aaf12cf785 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_disclaimer.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts_disclaimer.tsx @@ -33,7 +33,7 @@ export function AlertsDisclaimer() { <> {!experimentalMsgAck && ( toggleActionsPopover(eventId)} - data-test-subj="alerts-table-row-action-more" + data-test-subj="alertsTableRowActionMore" /> } diff --git a/x-pack/plugins/observability/public/pages/alerts/filter_for_value.tsx b/x-pack/plugins/observability/public/pages/alerts/filter_for_value.tsx index f75ae488c9b28..7017f573415da 100644 --- a/x-pack/plugins/observability/public/pages/alerts/filter_for_value.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/filter_for_value.tsx @@ -35,7 +35,7 @@ const FilterForValueButton: React.FC = React.memo( Component ? ( = React.memo( { const props = { onChange, status }; const { getByTestId } = render(); - const button = getByTestId(`workflow-status-filter-${status}-button`); + const button = getByTestId(`workflowStatusFilterButton-${status}`); const input = button.querySelector('input') as Element; Simulate.change(input); diff --git a/x-pack/plugins/observability/public/pages/alerts/workflow_status_filter.tsx b/x-pack/plugins/observability/public/pages/alerts/workflow_status_filter.tsx index 20073e9937b4f..d857b9d6bd650 100644 --- a/x-pack/plugins/observability/public/pages/alerts/workflow_status_filter.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/workflow_status_filter.tsx @@ -21,7 +21,7 @@ const options: Array = label: i18n.translate('xpack.observability.alerts.workflowStatusFilter.openButtonLabel', { defaultMessage: 'Open', }), - 'data-test-subj': 'workflow-status-filter-open-button', + 'data-test-subj': 'workflowStatusFilterButton-open', }, { id: 'acknowledged', @@ -31,14 +31,14 @@ const options: Array = defaultMessage: 'Acknowledged', } ), - 'data-test-subj': 'workflow-status-filter-acknowledged-button', + 'data-test-subj': 'workflowStatusFilterButton-acknowledged', }, { id: 'closed', label: i18n.translate('xpack.observability.alerts.workflowStatusFilter.closedButtonLabel', { defaultMessage: 'Closed', }), - 'data-test-subj': 'workflow-status-filter-closed-button', + 'data-test-subj': 'workflowStatusFilterButton-closed', }, ]; diff --git a/x-pack/plugins/observability/public/pages/cases/empty_page.tsx b/x-pack/plugins/observability/public/pages/cases/empty_page.tsx index c6fc4b59ef77c..faeafa6b4730f 100644 --- a/x-pack/plugins/observability/public/pages/cases/empty_page.tsx +++ b/x-pack/plugins/observability/public/pages/cases/empty_page.tsx @@ -59,7 +59,7 @@ const EmptyPageComponent = React.memo(({ actions, message, title (({ actions, message, title iconType={icon} target={target} fill={fill} - data-test-subj={`empty-page-${titles[idx]}-action`} + data-test-subj={`emptyPageAction-${titles[idx]}`} > {label} @@ -83,7 +83,7 @@ const EmptyPageComponent = React.memo(({ actions, message, title {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} (({ actions, message, title onClick={onClick} iconType={icon} target={target} - data-test-subj={`empty-page-${titles[idx]}-action`} + data-test-subj={`emptyPageAction-${titles[idx]}`} > {label} diff --git a/x-pack/plugins/observability/public/pages/cases/feature_no_permissions.tsx b/x-pack/plugins/observability/public/pages/cases/feature_no_permissions.tsx index 5075570c15b3e..2d8631a94e04c 100644 --- a/x-pack/plugins/observability/public/pages/cases/feature_no_permissions.tsx +++ b/x-pack/plugins/observability/public/pages/cases/feature_no_permissions.tsx @@ -29,7 +29,7 @@ export const CaseFeatureNoPermissions = React.memo(() => { ); diff --git a/x-pack/test/functional/page_objects/observability_page.ts b/x-pack/test/functional/page_objects/observability_page.ts index f89dafe4f3a73..07cceca4be122 100644 --- a/x-pack/test/functional/page_objects/observability_page.ts +++ b/x-pack/test/functional/page_objects/observability_page.ts @@ -28,7 +28,7 @@ export function ObservabilityPageProvider({ getService, getPageObjects }: FtrPro }, async expectNoReadOnlyCallout() { - await testSubjects.missingOrFail('case-callout-e41900b01c9ef0fa81dd6ff326083fb3'); + await testSubjects.missingOrFail('caseCallout-e41900b01c9ef0fa81dd6ff326083fb3'); }, async expectNoDataPage() { @@ -50,7 +50,7 @@ export function ObservabilityPageProvider({ getService, getPageObjects }: FtrPro }, async expectForbidden() { - const h2 = await testSubjects.find('no_feature_permissions', 20000); + const h2 = await testSubjects.find('noFeaturePermissions', 20000); const text = await h2.getVisibleText(); expect(text).to.contain('Kibana feature privileges required'); }, diff --git a/x-pack/test/functional/services/observability/alerts/common.ts b/x-pack/test/functional/services/observability/alerts/common.ts index 8a32b41e9b8e9..373f7558f8739 100644 --- a/x-pack/test/functional/services/observability/alerts/common.ts +++ b/x-pack/test/functional/services/observability/alerts/common.ts @@ -16,7 +16,7 @@ const DATE_WITH_DATA = { }; const ALERTS_FLYOUT_SELECTOR = 'alertsFlyout'; -const FILTER_FOR_VALUE_BUTTON_SELECTOR = 'filter-for-value'; +const FILTER_FOR_VALUE_BUTTON_SELECTOR = 'filterForValue'; const ALERTS_TABLE_CONTAINER_SELECTOR = 'events-viewer-panel'; const ACTION_COLUMN_INDEX = 1; @@ -71,7 +71,7 @@ export function ObservabilityAlertsCommonProvider({ }; const getExperimentalDisclaimer = async () => { - return testSubjects.existOrFail('o11y-experimental-disclaimer'); + return testSubjects.existOrFail('o11yExperimentalDisclaimer'); }; const getTableCellsInRows = async () => { @@ -173,7 +173,7 @@ export function ObservabilityAlertsCommonProvider({ const openActionsMenuForRow = async (rowIndex: number) => { const rows = await getTableCellsInRows(); const actionsOverflowButton = await testSubjects.findDescendant( - 'alerts-table-row-action-more', + 'alertsTableRowActionMore', rows[rowIndex][ACTION_COLUMN_INDEX] ); await actionsOverflowButton.click(); @@ -196,7 +196,7 @@ export function ObservabilityAlertsCommonProvider({ const setWorkflowStatusFilter = async (workflowStatus: WorkflowStatus) => { const buttonGroupButton = await testSubjects.find( - `workflow-status-filter-${workflowStatus}-button` + `workflowStatusFilterButton-${workflowStatus}` ); await buttonGroupButton.click(); }; @@ -223,7 +223,7 @@ export function ObservabilityAlertsCommonProvider({ const getActionsButtonByIndex = async (index: number) => { const actionsOverflowButtons = await find.allByCssSelector( - '[data-test-subj="alerts-table-row-action-more"]' + '[data-test-subj="alertsTableRowActionMore"]' ); return actionsOverflowButtons[index] || null; }; diff --git a/x-pack/test/observability_functional/apps/observability/alerts/alert_disclaimer.ts b/x-pack/test/observability_functional/apps/observability/alerts/alert_disclaimer.ts index c687210286304..d63739da47d5b 100644 --- a/x-pack/test/observability_functional/apps/observability/alerts/alert_disclaimer.ts +++ b/x-pack/test/observability_functional/apps/observability/alerts/alert_disclaimer.ts @@ -34,8 +34,8 @@ export default ({ getService, getPageObject }: FtrProviderContext) => { }); it('Dismiss experimental disclaimer', async () => { - await testSubjects.click('o11y-experimental-disclaimer-dismiss-btn'); - const o11yExperimentalDisclaimer = await testSubjects.exists('o11y-experimental-disclaimer'); + await testSubjects.click('o11yExperimentalDisclaimerDismissBtn'); + const o11yExperimentalDisclaimer = await testSubjects.exists('o11yExperimentalDisclaimer'); expect(o11yExperimentalDisclaimer).not.to.be(null); }); }); From 7595121751147ed18d5d14bb9272c262e8d52160 Mon Sep 17 00:00:00 2001 From: Spencer Date: Wed, 10 Nov 2021 14:26:34 -0700 Subject: [PATCH 15/43] [8.0] Remove direct usage of EUI theme vars (#116232) (#118228) * Remove direct usage of EUI theme vars (#116232) Co-authored-by: spalger Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> # Conflicts: # src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.tsx # x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/service_inventory.spec.ts # x-pack/plugins/apm/public/application/uxApp.tsx * commit using @elastic.co --- .../elastic-eslint-config-kibana/.eslintrc.js | 10 ++++++ packages/kbn-ui-shared-deps-npm/BUILD.bazel | 1 - .../eui_theme_vars/package.json | 4 --- packages/kbn-ui-shared-deps-src/src/index.js | 3 +- packages/kbn-ui-shared-deps-src/src/theme.ts | 2 ++ .../public/static/components/current_time.tsx | 6 ++-- .../public/static/components/endzones.tsx | 6 ++-- .../discover_grid_document_selection.tsx | 6 ++-- .../discover_grid_expand_button.tsx | 6 ++-- .../discover_grid/get_render_cell_value.tsx | 6 ++-- .../public/react_expression_renderer.tsx | 2 +- .../components/form_fields/type_field.tsx | 5 ++- .../public/code_editor/code_editor_field.tsx | 6 ++-- .../public/code_editor/editor_theme.ts | 6 ++-- .../vislib/components/tooltip/tooltip.js | 2 +- x-pack/plugins/apm/common/viz_colors.ts | 2 +- .../service_inventory.spec.ts | 1 + .../plugins/apm/public/application/uxApp.tsx | 3 +- .../PercentileAnnotations.tsx | 2 +- .../URLFilter/URLSearch/render_option.tsx | 2 +- .../app/service_map/Controls.test.tsx | 2 +- .../public/components/routing/app_root.tsx | 3 +- .../apm/public/utils/httpStatusCodeToColor.ts | 2 +- .../java/gc/fetch_and_transform_gc_metrics.ts | 2 +- .../by_agent/java/gc/get_gc_rate_chart.ts | 2 +- .../by_agent/java/gc/get_gc_time_chart.ts | 2 +- .../by_agent/java/heap_memory/index.ts | 2 +- .../by_agent/java/non_heap_memory/index.ts | 2 +- .../by_agent/java/thread_count/index.ts | 2 +- .../lib/metrics/by_agent/shared/cpu/index.ts | 2 +- .../lib/metrics/transform_metrics_chart.ts | 2 +- .../public/common/mock/test_providers.tsx | 2 +- .../components/header_page/index.test.tsx | 2 +- .../utility_bar/utility_bar.test.tsx | 2 +- .../stats_table/hooks/use_color_range.ts | 6 ++-- .../agents/components/agent_health.tsx | 4 +-- .../sections/agents/services/agent_status.tsx | 6 ++-- .../ml/common/util/group_color_utils.ts | 2 +- .../color_range_legend/use_color_range.ts | 6 ++-- .../components/job_messages/job_messages.tsx | 2 +- .../scatterplot_matrix.test.tsx | 2 +- .../scatterplot_matrix_vega_lite_spec.test.ts | 2 +- .../scatterplot_matrix_vega_lite_spec.ts | 2 +- .../get_roc_curve_chart_vega_lite_spec.tsx | 2 +- .../decision_path_chart.tsx | 2 +- .../feature_importance_summary.tsx | 2 +- .../components/charts/common/settings.ts | 6 ++-- .../core_web_vitals/palette_legends.tsx | 3 +- x-pack/plugins/osquery/public/application.tsx | 3 +- .../__examples__/index.stories.tsx | 2 +- .../conditions_table/index.stories.tsx | 2 +- .../viewer/exception_item/index.stories.tsx | 2 +- .../exceptions_viewer_header.stories.tsx | 2 +- .../components/header_page/index.test.tsx | 2 +- .../components/header_section/index.test.tsx | 2 +- .../item_details_card/index.stories.tsx | 2 +- .../text_field_value/index.stories.tsx | 2 +- .../threat_match/logic_buttons.stories.tsx | 2 +- .../utility_bar/utility_bar.test.tsx | 2 +- .../public/common/lib/theme/use_eui_theme.tsx | 6 ++-- .../public/common/mock/test_providers.tsx | 2 +- .../components/rules/severity_badge/index.tsx | 2 +- .../components/rules/step_about_rule/data.tsx | 2 +- .../components/config_form/index.stories.tsx | 2 +- .../trusted_apps_grid/index.stories.tsx | 2 +- .../view_type_toggle/index.stories.tsx | 2 +- .../network/components/details/index.tsx | 6 ++-- .../map_tool_tip/tooltip_footer.tsx | 2 +- .../components/host_overview/index.tsx | 6 ++-- .../public/resolver/view/use_colors.ts | 36 +++++++++---------- .../public/resolver/view/use_cube_assets.ts | 20 +++++------ .../timelines/public/mock/test_providers.tsx | 2 +- .../expanded_row_messages_pane.tsx | 2 +- .../components/execution_duration_chart.tsx | 2 +- .../public/contexts/uptime_theme_context.tsx | 3 +- 75 files changed, 148 insertions(+), 129 deletions(-) delete mode 100644 packages/kbn-ui-shared-deps-npm/eui_theme_vars/package.json diff --git a/packages/elastic-eslint-config-kibana/.eslintrc.js b/packages/elastic-eslint-config-kibana/.eslintrc.js index 6bfefc8e118d4..99377540d38f7 100644 --- a/packages/elastic-eslint-config-kibana/.eslintrc.js +++ b/packages/elastic-eslint-config-kibana/.eslintrc.js @@ -88,6 +88,16 @@ module.exports = { exclude: USES_STYLED_COMPONENTS, disallowedMessage: `Prefer using @emotion/react instead. To use styled-components, ensure you plugin is enabled in @kbn/dev-utils/src/babel.ts.` }, + ...[ + '@elastic/eui/dist/eui_theme_light.json', + '@elastic/eui/dist/eui_theme_dark.json', + '@elastic/eui/dist/eui_theme_amsterdam_light.json', + '@elastic/eui/dist/eui_theme_amsterdam_dark.json', + ].map(from => ({ + from, + to: false, + disallowedMessage: `Use "@kbn/ui-shared-deps-src/theme" to access theme vars.` + })), ], ], diff --git a/packages/kbn-ui-shared-deps-npm/BUILD.bazel b/packages/kbn-ui-shared-deps-npm/BUILD.bazel index bbad873429b2b..416a4d4799b7b 100644 --- a/packages/kbn-ui-shared-deps-npm/BUILD.bazel +++ b/packages/kbn-ui-shared-deps-npm/BUILD.bazel @@ -23,7 +23,6 @@ filegroup( ) NPM_MODULE_EXTRA_FILES = [ - "eui_theme_vars/package.json", "package.json", "README.md" ] diff --git a/packages/kbn-ui-shared-deps-npm/eui_theme_vars/package.json b/packages/kbn-ui-shared-deps-npm/eui_theme_vars/package.json deleted file mode 100644 index a2448adf4d096..0000000000000 --- a/packages/kbn-ui-shared-deps-npm/eui_theme_vars/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "main": "../target_node/eui_theme_vars.js", - "types": "../target_types/eui_theme_vars.d.ts" -} \ No newline at end of file diff --git a/packages/kbn-ui-shared-deps-src/src/index.js b/packages/kbn-ui-shared-deps-src/src/index.js index 3e3643d3e2988..630cf75c447fd 100644 --- a/packages/kbn-ui-shared-deps-src/src/index.js +++ b/packages/kbn-ui-shared-deps-src/src/index.js @@ -59,8 +59,7 @@ exports.externals = { '@elastic/eui/lib/services': '__kbnSharedDeps__.ElasticEuiLibServices', '@elastic/eui/lib/services/format': '__kbnSharedDeps__.ElasticEuiLibServicesFormat', '@elastic/eui/dist/eui_charts_theme': '__kbnSharedDeps__.ElasticEuiChartsTheme', - '@elastic/eui/dist/eui_theme_light.json': '__kbnSharedDeps__.Theme.euiLightVars', - '@elastic/eui/dist/eui_theme_dark.json': '__kbnSharedDeps__.Theme.euiDarkVars', + // transient dep of eui 'react-beautiful-dnd': '__kbnSharedDeps__.ReactBeautifulDnD', lodash: '__kbnSharedDeps__.Lodash', diff --git a/packages/kbn-ui-shared-deps-src/src/theme.ts b/packages/kbn-ui-shared-deps-src/src/theme.ts index f058913cdeeab..33b8a594bfa5d 100644 --- a/packages/kbn-ui-shared-deps-src/src/theme.ts +++ b/packages/kbn-ui-shared-deps-src/src/theme.ts @@ -6,7 +6,9 @@ * Side Public License, v 1. */ +/* eslint-disable-next-line @kbn/eslint/module_migration */ import { default as v8Light } from '@elastic/eui/dist/eui_theme_amsterdam_light.json'; +/* eslint-disable-next-line @kbn/eslint/module_migration */ import { default as v8Dark } from '@elastic/eui/dist/eui_theme_amsterdam_dark.json'; const globals: any = typeof window === 'undefined' ? {} : window; diff --git a/src/plugins/charts/public/static/components/current_time.tsx b/src/plugins/charts/public/static/components/current_time.tsx index 9cc261bf3ed86..ad05f451b607f 100644 --- a/src/plugins/charts/public/static/components/current_time.tsx +++ b/src/plugins/charts/public/static/components/current_time.tsx @@ -10,8 +10,10 @@ import moment, { Moment } from 'moment'; import React, { FC } from 'react'; import { LineAnnotation, AnnotationDomainType, LineAnnotationStyle } from '@elastic/charts'; -import lightEuiTheme from '@elastic/eui/dist/eui_theme_light.json'; -import darkEuiTheme from '@elastic/eui/dist/eui_theme_dark.json'; +import { + euiLightVars as lightEuiTheme, + euiDarkVars as darkEuiTheme, +} from '@kbn/ui-shared-deps-src/theme'; interface CurrentTimeProps { isDarkMode: boolean; diff --git a/src/plugins/charts/public/static/components/endzones.tsx b/src/plugins/charts/public/static/components/endzones.tsx index 85a020e54eb37..695b51c9702d2 100644 --- a/src/plugins/charts/public/static/components/endzones.tsx +++ b/src/plugins/charts/public/static/components/endzones.tsx @@ -17,8 +17,10 @@ import { } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSpacer } from '@elastic/eui'; -import lightEuiTheme from '@elastic/eui/dist/eui_theme_light.json'; -import darkEuiTheme from '@elastic/eui/dist/eui_theme_dark.json'; +import { + euiLightVars as lightEuiTheme, + euiDarkVars as darkEuiTheme, +} from '@kbn/ui-shared-deps-src/theme'; interface EndzonesProps { isDarkMode: boolean; diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.tsx index c87d425d601c5..a1967b48d6db6 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.tsx @@ -17,8 +17,10 @@ import { EuiDataGridCellValueElementProps, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import themeDark from '@elastic/eui/dist/eui_theme_dark.json'; -import themeLight from '@elastic/eui/dist/eui_theme_light.json'; +import { + euiLightVars as themeLight, + euiDarkVars as themeDark, +} from '@kbn/ui-shared-deps-src/theme'; import { ElasticSearchHit } from '../../doc_views/doc_views_types'; import { DiscoverGridContext } from './discover_grid_context'; diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_expand_button.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_expand_button.tsx index f259d5c5c3658..e6adc821976db 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_expand_button.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_expand_button.tsx @@ -8,8 +8,10 @@ import React, { useContext, useEffect } from 'react'; import { EuiButtonIcon, EuiDataGridCellValueElementProps, EuiToolTip } from '@elastic/eui'; -import themeDark from '@elastic/eui/dist/eui_theme_dark.json'; -import themeLight from '@elastic/eui/dist/eui_theme_light.json'; +import { + euiLightVars as themeLight, + euiDarkVars as themeDark, +} from '@kbn/ui-shared-deps-src/theme'; import { i18n } from '@kbn/i18n'; import { DiscoverGridContext } from './discover_grid_context'; import { EsHitRecord } from '../../types'; diff --git a/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx b/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx index 4066c13f6391e..8d4c4a6bbdad9 100644 --- a/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx @@ -7,8 +7,10 @@ */ import React, { Fragment, useContext, useEffect } from 'react'; -import themeLight from '@elastic/eui/dist/eui_theme_light.json'; -import themeDark from '@elastic/eui/dist/eui_theme_dark.json'; +import { + euiLightVars as themeLight, + euiDarkVars as themeDark, +} from '@kbn/ui-shared-deps-src/theme'; import type { IndexPattern } from 'src/plugins/data/common'; import { diff --git a/src/plugins/expressions/public/react_expression_renderer.tsx b/src/plugins/expressions/public/react_expression_renderer.tsx index 77b4402b22c06..b42ea3f3fd149 100644 --- a/src/plugins/expressions/public/react_expression_renderer.tsx +++ b/src/plugins/expressions/public/react_expression_renderer.tsx @@ -12,7 +12,7 @@ import { Observable, Subscription } from 'rxjs'; import { filter } from 'rxjs/operators'; import useShallowCompareEffect from 'react-use/lib/useShallowCompareEffect'; import { EuiLoadingChart, EuiProgress } from '@elastic/eui'; -import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars as theme } from '@kbn/ui-shared-deps-src/theme'; import { IExpressionLoaderParams, ExpressionRenderError, ExpressionRendererEvent } from './types'; import { ExpressionAstExpression, IInterpreterRenderHandlers } from '../common'; import { ExpressionLoader } from './loader'; diff --git a/src/plugins/index_pattern_editor/public/components/form_fields/type_field.tsx b/src/plugins/index_pattern_editor/public/components/form_fields/type_field.tsx index e8a48c5679879..0f4a040d1317b 100644 --- a/src/plugins/index_pattern_editor/public/components/form_fields/type_field.tsx +++ b/src/plugins/index_pattern_editor/public/components/form_fields/type_field.tsx @@ -8,8 +8,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -// @ts-ignore -import { euiColorAccent } from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; import { FormattedMessage } from '@kbn/i18n/react'; import { @@ -54,7 +53,7 @@ const rollupSelectItem = ( defaultMessage="Rollup data view" />   - + diff --git a/src/plugins/kibana_react/public/code_editor/code_editor_field.tsx b/src/plugins/kibana_react/public/code_editor/code_editor_field.tsx index 0e6ab21159f15..85263b7006c16 100644 --- a/src/plugins/kibana_react/public/code_editor/code_editor_field.tsx +++ b/src/plugins/kibana_react/public/code_editor/code_editor_field.tsx @@ -7,8 +7,10 @@ */ import React from 'react'; -import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; -import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; +import { + euiLightVars as lightTheme, + euiDarkVars as darkTheme, +} from '@kbn/ui-shared-deps-src/theme'; import { EuiFormControlLayout } from '@elastic/eui'; import { CodeEditor, Props } from './code_editor'; diff --git a/src/plugins/kibana_react/public/code_editor/editor_theme.ts b/src/plugins/kibana_react/public/code_editor/editor_theme.ts index 0f362a28ea622..6c2727b123de8 100644 --- a/src/plugins/kibana_react/public/code_editor/editor_theme.ts +++ b/src/plugins/kibana_react/public/code_editor/editor_theme.ts @@ -8,8 +8,10 @@ import { monaco } from '@kbn/monaco'; -import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; -import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; +import { + euiLightVars as lightTheme, + euiDarkVars as darkTheme, +} from '@kbn/ui-shared-deps-src/theme'; // NOTE: For talk around where this theme information will ultimately live, // please see this discuss issue: https://github.com/elastic/kibana/issues/43814 diff --git a/src/plugins/vis_types/vislib/public/vislib/components/tooltip/tooltip.js b/src/plugins/vis_types/vislib/public/vislib/components/tooltip/tooltip.js index e2decb86c9032..1faebdf0ce89c 100644 --- a/src/plugins/vis_types/vislib/public/vislib/components/tooltip/tooltip.js +++ b/src/plugins/vis_types/vislib/public/vislib/components/tooltip/tooltip.js @@ -12,7 +12,7 @@ import $ from 'jquery'; import { Binder } from '../../lib/binder'; import { positionTooltip } from './position_tooltip'; -import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars as theme } from '@kbn/ui-shared-deps-src/theme'; let allContents = []; diff --git a/x-pack/plugins/apm/common/viz_colors.ts b/x-pack/plugins/apm/common/viz_colors.ts index 20287f6e097bc..5b4946f346841 100644 --- a/x-pack/plugins/apm/common/viz_colors.ts +++ b/x-pack/plugins/apm/common/viz_colors.ts @@ -5,7 +5,7 @@ * 2.0. */ -import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars as lightTheme } from '@kbn/ui-shared-deps-src/theme'; function getVizColorsForTheme(theme = lightTheme) { return [ diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/service_inventory.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/service_inventory.spec.ts index 72e43451bcfe4..1122e3c88a315 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/service_inventory.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_inventory/service_inventory.spec.ts @@ -93,6 +93,7 @@ describe('When navigating to the service inventory', () => { cy.wait(aliasNames); }); + // FAILING, @caue.marcondes will be fixing soon it.skip('when selecting a different time range and clicking the refresh button', () => { cy.wait(aliasNames); diff --git a/x-pack/plugins/apm/public/application/uxApp.tsx b/x-pack/plugins/apm/public/application/uxApp.tsx index 2e4ba786811f8..4c4d68839a2f4 100644 --- a/x-pack/plugins/apm/public/application/uxApp.tsx +++ b/x-pack/plugins/apm/public/application/uxApp.tsx @@ -5,8 +5,7 @@ * 2.0. */ -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars, euiDarkVars } from '@kbn/ui-shared-deps-src/theme'; import { AppMountParameters, CoreStart } from 'kibana/public'; import React from 'react'; import ReactDOM from 'react-dom'; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/PercentileAnnotations.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/PercentileAnnotations.tsx index ac713ad8dd8a8..35c6fb3c634cc 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/PercentileAnnotations.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/PercentileAnnotations.tsx @@ -13,7 +13,7 @@ import { LineAnnotationStyle, Position, } from '@elastic/charts'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; import { EuiToolTip } from '@elastic/eui'; interface Props { diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/render_option.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/render_option.tsx index f5f5a04353c50..4a4d8e9d3e191 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/render_option.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/URLFilter/URLSearch/render_option.tsx @@ -8,7 +8,7 @@ import React, { ReactNode } from 'react'; import { EuiHighlight, EuiSelectableOption } from '@elastic/eui'; import styled from 'styled-components'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; const StyledSpan = styled.span` color: ${euiLightVars.euiColorSecondaryText}; diff --git a/x-pack/plugins/apm/public/components/app/service_map/Controls.test.tsx b/x-pack/plugins/apm/public/components/app/service_map/Controls.test.tsx index 2ebd63badc41e..f2dd9cce8f27e 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/Controls.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/Controls.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars as lightTheme } from '@kbn/ui-shared-deps-src/theme'; import { render } from '@testing-library/react'; import cytoscape from 'cytoscape'; import React, { ReactNode } from 'react'; diff --git a/x-pack/plugins/apm/public/components/routing/app_root.tsx b/x-pack/plugins/apm/public/components/routing/app_root.tsx index bc4119a3e835a..0e8e6732dc943 100644 --- a/x-pack/plugins/apm/public/components/routing/app_root.tsx +++ b/x-pack/plugins/apm/public/components/routing/app_root.tsx @@ -5,8 +5,7 @@ * 2.0. */ -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars, euiDarkVars } from '@kbn/ui-shared-deps-src/theme'; import { RouteRenderer, RouterProvider } from '@kbn/typed-react-router-config'; import React from 'react'; import { Route } from 'react-router-dom'; diff --git a/x-pack/plugins/apm/public/utils/httpStatusCodeToColor.ts b/x-pack/plugins/apm/public/utils/httpStatusCodeToColor.ts index 345eb7aa3f635..1b44a90fe7bfc 100644 --- a/x-pack/plugins/apm/public/utils/httpStatusCodeToColor.ts +++ b/x-pack/plugins/apm/public/utils/httpStatusCodeToColor.ts @@ -5,7 +5,7 @@ * 2.0. */ -import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars as theme } from '@kbn/ui-shared-deps-src/theme'; const { euiColorDarkShade, euiColorWarning } = theme; export const errorColor = '#c23c2b'; diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts index fb66cb9649085..117b372d445d2 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts @@ -6,7 +6,7 @@ */ import { sum, round } from 'lodash'; -import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars as theme } from '@kbn/ui-shared-deps-src/theme'; import { isFiniteNumber } from '../../../../../../common/utils/is_finite_number'; import { Setup } from '../../../../helpers/setup_request'; import { getMetricsDateHistogramParams } from '../../../../helpers/metrics'; diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_rate_chart.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_rate_chart.ts index 07f02bb6f8fdc..22dcb3e0f08ff 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_rate_chart.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_rate_chart.ts @@ -5,7 +5,7 @@ * 2.0. */ -import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars as theme } from '@kbn/ui-shared-deps-src/theme'; import { i18n } from '@kbn/i18n'; import { METRIC_JAVA_GC_COUNT } from '../../../../../../common/elasticsearch_fieldnames'; import { Setup } from '../../../../helpers/setup_request'; diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_time_chart.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_time_chart.ts index 9f2fc2ba582f3..4b85ad94f6494 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_time_chart.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/get_gc_time_chart.ts @@ -5,7 +5,7 @@ * 2.0. */ -import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars as theme } from '@kbn/ui-shared-deps-src/theme'; import { i18n } from '@kbn/i18n'; import { METRIC_JAVA_GC_TIME } from '../../../../../../common/elasticsearch_fieldnames'; import { Setup } from '../../../../helpers/setup_request'; diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/heap_memory/index.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/heap_memory/index.ts index 71f3973f51998..a872a3af76d7e 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/heap_memory/index.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/heap_memory/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars as theme } from '@kbn/ui-shared-deps-src/theme'; import { i18n } from '@kbn/i18n'; import { METRIC_JAVA_HEAP_MEMORY_MAX, diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/non_heap_memory/index.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/non_heap_memory/index.ts index 2ed70bf846dfa..9fa758cb4dbd8 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/non_heap_memory/index.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/non_heap_memory/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars as theme } from '@kbn/ui-shared-deps-src/theme'; import { i18n } from '@kbn/i18n'; import { METRIC_JAVA_NON_HEAP_MEMORY_MAX, diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/thread_count/index.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/thread_count/index.ts index e5e98fc418e5d..306666d27cd1c 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/thread_count/index.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/thread_count/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars as theme } from '@kbn/ui-shared-deps-src/theme'; import { i18n } from '@kbn/i18n'; import { METRIC_JAVA_THREAD_COUNT, diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/cpu/index.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/cpu/index.ts index e5042c8c80c70..0911081b20324 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/cpu/index.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/shared/cpu/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars as theme } from '@kbn/ui-shared-deps-src/theme'; import { i18n } from '@kbn/i18n'; import { METRIC_SYSTEM_CPU_PERCENT, diff --git a/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.ts b/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.ts index f4829f2d5faa0..fea853af93b84 100644 --- a/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.ts +++ b/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.ts @@ -5,7 +5,7 @@ * 2.0. */ -import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars as theme } from '@kbn/ui-shared-deps-src/theme'; import { ESSearchResponse } from '../../../../../../src/core/types/elasticsearch'; import { getVizColorForIndex } from '../../../common/viz_colors'; import { GenericMetricsRequest } from './fetch_and_transform_metrics'; diff --git a/x-pack/plugins/cases/public/common/mock/test_providers.tsx b/x-pack/plugins/cases/public/common/mock/test_providers.tsx index b31e9e4e1b19d..8a4b66d38cc0f 100644 --- a/x-pack/plugins/cases/public/common/mock/test_providers.tsx +++ b/x-pack/plugins/cases/public/common/mock/test_providers.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; +import { euiDarkVars } from '@kbn/ui-shared-deps-src/theme'; import { I18nProvider } from '@kbn/i18n/react'; import React from 'react'; import { BehaviorSubject } from 'rxjs'; diff --git a/x-pack/plugins/cases/public/components/header_page/index.test.tsx b/x-pack/plugins/cases/public/components/header_page/index.test.tsx index d84a6d9272def..55e5d0907c869 100644 --- a/x-pack/plugins/cases/public/components/header_page/index.test.tsx +++ b/x-pack/plugins/cases/public/components/header_page/index.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; +import { euiDarkVars } from '@kbn/ui-shared-deps-src/theme'; import { shallow } from 'enzyme'; import React from 'react'; diff --git a/x-pack/plugins/cases/public/components/utility_bar/utility_bar.test.tsx b/x-pack/plugins/cases/public/components/utility_bar/utility_bar.test.tsx index 98af25a9af466..43ebd9bee3ca9 100644 --- a/x-pack/plugins/cases/public/components/utility_bar/utility_bar.test.tsx +++ b/x-pack/plugins/cases/public/components/utility_bar/utility_bar.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; +import { euiDarkVars } from '@kbn/ui-shared-deps-src/theme'; import { mount, shallow } from 'enzyme'; import React from 'react'; import { TestProviders } from '../../common/mock'; diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/hooks/use_color_range.ts b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/hooks/use_color_range.ts index b1d26a5437b44..92a88f4d60670 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/hooks/use_color_range.ts +++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/hooks/use_color_range.ts @@ -7,8 +7,10 @@ import d3 from 'd3'; import { useMemo } from 'react'; -import euiThemeLight from '@elastic/eui/dist/eui_theme_light.json'; -import euiThemeDark from '@elastic/eui/dist/eui_theme_dark.json'; +import { + euiLightVars as euiThemeLight, + euiDarkVars as euiThemeDark, +} from '@kbn/ui-shared-deps-src/theme'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_health.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_health.tsx index fbac6ad74906d..701d68c0e29e3 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_health.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_health.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { FormattedMessage, FormattedRelative } from '@kbn/i18n/react'; import { EuiBadge, EuiToolTip } from '@elastic/eui'; -import * as euiVars from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars as euiVars } from '@kbn/ui-shared-deps-src/theme'; import type { Agent } from '../../../types'; @@ -29,7 +29,7 @@ const Status = { ), Inactive: ( - + ), diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/services/agent_status.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/services/agent_status.tsx index 74e9879936d42..8eafcef0dc6de 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/services/agent_status.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/services/agent_status.tsx @@ -7,20 +7,20 @@ import { euiPaletteColorBlindBehindText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import * as euiVars from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; import type { SimplifiedAgentStatus } from '../../../types'; const visColors = euiPaletteColorBlindBehindText(); const colorToHexMap = { // using variables as mentioned here https://elastic.github.io/eui/#/guidelines/getting-started - default: euiVars.default.euiColorLightShade, + default: euiLightVars.euiColorLightShade, primary: visColors[1], secondary: visColors[0], accent: visColors[2], warning: visColors[5], danger: visColors[9], - inactive: euiVars.default.euiColorDarkShade, + inactive: euiLightVars.euiColorDarkShade, }; export const AGENT_STATUSES: SimplifiedAgentStatus[] = [ diff --git a/x-pack/plugins/ml/common/util/group_color_utils.ts b/x-pack/plugins/ml/common/util/group_color_utils.ts index bb3b347e25334..63f0e13676d58 100644 --- a/x-pack/plugins/ml/common/util/group_color_utils.ts +++ b/x-pack/plugins/ml/common/util/group_color_utils.ts @@ -5,7 +5,7 @@ * 2.0. */ -import euiVars from '@elastic/eui/dist/eui_theme_dark.json'; +import { euiDarkVars as euiVars } from '@kbn/ui-shared-deps-src/theme'; import { stringHash } from './string_utils'; diff --git a/x-pack/plugins/ml/public/application/components/color_range_legend/use_color_range.ts b/x-pack/plugins/ml/public/application/components/color_range_legend/use_color_range.ts index 2809a4321e7bb..2ccc687d145d0 100644 --- a/x-pack/plugins/ml/public/application/components/color_range_legend/use_color_range.ts +++ b/x-pack/plugins/ml/public/application/components/color_range_legend/use_color_range.ts @@ -7,8 +7,10 @@ import d3 from 'd3'; import { useMemo } from 'react'; -import euiThemeLight from '@elastic/eui/dist/eui_theme_light.json'; -import euiThemeDark from '@elastic/eui/dist/eui_theme_dark.json'; +import { + euiLightVars as euiThemeLight, + euiDarkVars as euiThemeDark, +} from '@kbn/ui-shared-deps-src/theme'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/plugins/ml/public/application/components/job_messages/job_messages.tsx b/x-pack/plugins/ml/public/application/components/job_messages/job_messages.tsx index 2311807b6bbe6..facef2c02d578 100644 --- a/x-pack/plugins/ml/public/application/components/job_messages/job_messages.tsx +++ b/x-pack/plugins/ml/public/application/components/job_messages/job_messages.tsx @@ -17,7 +17,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars as theme } from '@kbn/ui-shared-deps-src/theme'; import { JobMessage } from '../../../../common/types/audit_message'; import { JobIcon } from '../job_message_icon'; diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.test.tsx b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.test.tsx index d0e70c38c23b4..846a8da83acb0 100644 --- a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.test.tsx +++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.test.tsx @@ -10,7 +10,7 @@ import { render, waitFor, screen } from '@testing-library/react'; import { __IntlProvider as IntlProvider } from '@kbn/i18n/react'; -import euiThemeLight from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars as euiThemeLight } from '@kbn/ui-shared-deps-src/theme'; import { ScatterplotMatrix } from './scatterplot_matrix'; diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.test.ts b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.test.ts index e401d70abe759..ed8a49cd36f02 100644 --- a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.test.ts +++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.test.ts @@ -10,7 +10,7 @@ import 'jest-canvas-mock'; // @ts-ignore import { compile } from 'vega-lite/build/vega-lite'; -import euiThemeLight from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars as euiThemeLight } from '@kbn/ui-shared-deps-src/theme'; import { LEGEND_TYPES } from '../vega_chart/common'; diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.ts b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.ts index 861b3727cea1b..83525a4837dc9 100644 --- a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.ts +++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix_vega_lite_spec.ts @@ -9,7 +9,7 @@ // @ts-ignore import type { TopLevelSpec } from 'vega-lite/build/vega-lite'; -import euiThemeLight from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars as euiThemeLight } from '@kbn/ui-shared-deps-src/theme'; import { euiPaletteColorBlind, euiPaletteNegative, euiPalettePositive } from '@elastic/eui'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/get_roc_curve_chart_vega_lite_spec.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/get_roc_curve_chart_vega_lite_spec.tsx index ef5bcb83e871f..2d116e0dd851e 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/get_roc_curve_chart_vega_lite_spec.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/get_roc_curve_chart_vega_lite_spec.tsx @@ -10,7 +10,7 @@ import type { TopLevelSpec } from 'vega-lite/build/vega-lite'; import { euiPaletteColorBlind, euiPaletteGray } from '@elastic/eui'; -import euiThemeLight from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars as euiThemeLight } from '@kbn/ui-shared-deps-src/theme'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_chart.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_chart.tsx index dfaf58eba03d8..d91b742b8cfe1 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_chart.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/feature_importance/decision_path_chart.tsx @@ -24,7 +24,7 @@ import { EuiIcon } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; -import euiVars from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars as euiVars } from '@kbn/ui-shared-deps-src/theme'; import type { DecisionPathPlotData } from './use_classification_path_data'; import { formatSingleValue } from '../../../../../formatters/format_value'; import { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/total_feature_importance_summary/feature_importance_summary.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/total_feature_importance_summary/feature_importance_summary.tsx index 6fe32a59c7614..9157e1fe4b678 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/total_feature_importance_summary/feature_importance_summary.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/total_feature_importance_summary/feature_importance_summary.tsx @@ -21,7 +21,7 @@ import { BarSeriesSpec, } from '@elastic/charts'; import { i18n } from '@kbn/i18n'; -import euiVars from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars as euiVars } from '@kbn/ui-shared-deps-src/theme'; import { TotalFeatureImportance, isClassificationTotalFeatureImportance, diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/common/settings.ts b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/common/settings.ts index 861b72a5a58b7..3d386073849f4 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/common/settings.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/charts/common/settings.ts @@ -5,8 +5,10 @@ * 2.0. */ -import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; -import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; +import { + euiLightVars as lightTheme, + euiDarkVars as darkTheme, +} from '@kbn/ui-shared-deps-src/theme'; import { JobCreatorType, isMultiMetricJobCreator, diff --git a/x-pack/plugins/observability/public/components/shared/core_web_vitals/palette_legends.tsx b/x-pack/plugins/observability/public/components/shared/core_web_vitals/palette_legends.tsx index 840702c744379..70ae61b5e0d74 100644 --- a/x-pack/plugins/observability/public/components/shared/core_web_vitals/palette_legends.tsx +++ b/x-pack/plugins/observability/public/components/shared/core_web_vitals/palette_legends.tsx @@ -16,8 +16,7 @@ import { } from '@elastic/eui'; import styled from 'styled-components'; import { FormattedMessage } from '@kbn/i18n/react'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; +import { euiLightVars, euiDarkVars } from '@kbn/ui-shared-deps-src/theme'; import { getCoreVitalTooltipMessage, Thresholds } from './core_vital_item'; import { useUiSetting$ } from '../../../../../../../src/plugins/kibana_react/public'; import { diff --git a/x-pack/plugins/osquery/public/application.tsx b/x-pack/plugins/osquery/public/application.tsx index 3e959132e21a8..3e046a138cd4b 100644 --- a/x-pack/plugins/osquery/public/application.tsx +++ b/x-pack/plugins/osquery/public/application.tsx @@ -6,8 +6,7 @@ */ import { EuiErrorBoundary } from '@elastic/eui'; -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars, euiDarkVars } from '@kbn/ui-shared-deps-src/theme'; import React, { useMemo } from 'react'; import ReactDOM from 'react-dom'; import { Router } from 'react-router-dom'; diff --git a/x-pack/plugins/security_solution/public/common/components/and_or_badge/__examples__/index.stories.tsx b/x-pack/plugins/security_solution/public/common/components/and_or_badge/__examples__/index.stories.tsx index 03a6a2653c1de..231f93e896df9 100644 --- a/x-pack/plugins/security_solution/public/common/components/and_or_badge/__examples__/index.stories.tsx +++ b/x-pack/plugins/security_solution/public/common/components/and_or_badge/__examples__/index.stories.tsx @@ -8,7 +8,7 @@ import { storiesOf } from '@storybook/react'; import React, { ReactNode } from 'react'; import { ThemeProvider } from 'styled-components'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; import { AndOrBadge } from '..'; diff --git a/x-pack/plugins/security_solution/public/common/components/conditions_table/index.stories.tsx b/x-pack/plugins/security_solution/public/common/components/conditions_table/index.stories.tsx index 6fe0e6851a098..9efbbc7a3211d 100644 --- a/x-pack/plugins/security_solution/public/common/components/conditions_table/index.stories.tsx +++ b/x-pack/plugins/security_solution/public/common/components/conditions_table/index.stories.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import { storiesOf, addDecorator } from '@storybook/react'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; import { createItems, TEST_COLUMNS } from './test_utils'; import { ConditionsTable } from '.'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.stories.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.stories.tsx index dd7f0f7a13e26..f8697b2f3db79 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.stories.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exception_item/index.stories.tsx @@ -9,7 +9,7 @@ import { storiesOf, addDecorator } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import React from 'react'; import { ThemeProvider } from 'styled-components'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; import { ExceptionItem } from './'; import { getExceptionListItemSchemaMock } from '../../../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.stories.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.stories.tsx index 4f78b49ea266c..de56e0eefc1ac 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.stories.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/viewer/exceptions_viewer_header.stories.tsx @@ -9,7 +9,7 @@ import { storiesOf, addDecorator } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import React from 'react'; import { ThemeProvider } from 'styled-components'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; import { ExceptionListTypeEnum } from '@kbn/securitysolution-io-ts-list-types'; import { ExceptionsViewerHeader } from './exceptions_viewer_header'; diff --git a/x-pack/plugins/security_solution/public/common/components/header_page/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/header_page/index.test.tsx index 47b0871229864..2e25a357e86b1 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_page/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_page/index.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; +import { euiDarkVars } from '@kbn/ui-shared-deps-src/theme'; import { shallow } from 'enzyme'; import React from 'react'; diff --git a/x-pack/plugins/security_solution/public/common/components/header_section/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/header_section/index.test.tsx index cc0ac3e6c2b0c..07a5ad475aed2 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_section/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_section/index.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; +import { euiDarkVars } from '@kbn/ui-shared-deps-src/theme'; import { mount, shallow } from 'enzyme'; import React from 'react'; diff --git a/x-pack/plugins/security_solution/public/common/components/item_details_card/index.stories.tsx b/x-pack/plugins/security_solution/public/common/components/item_details_card/index.stories.tsx index d910d258e7bfe..513ba8ccdc462 100644 --- a/x-pack/plugins/security_solution/public/common/components/item_details_card/index.stories.tsx +++ b/x-pack/plugins/security_solution/public/common/components/item_details_card/index.stories.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import { storiesOf, addDecorator } from '@storybook/react'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; import { ItemDetailsAction, ItemDetailsCard, ItemDetailsPropertySummary } from '.'; diff --git a/x-pack/plugins/security_solution/public/common/components/text_field_value/index.stories.tsx b/x-pack/plugins/security_solution/public/common/components/text_field_value/index.stories.tsx index b3f0de1376396..146ba8ef82505 100644 --- a/x-pack/plugins/security_solution/public/common/components/text_field_value/index.stories.tsx +++ b/x-pack/plugins/security_solution/public/common/components/text_field_value/index.stories.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import { storiesOf, addDecorator } from '@storybook/react'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; import { TextFieldValue } from '.'; diff --git a/x-pack/plugins/security_solution/public/common/components/threat_match/logic_buttons.stories.tsx b/x-pack/plugins/security_solution/public/common/components/threat_match/logic_buttons.stories.tsx index 20a7786f6d09e..6497875ac8d4a 100644 --- a/x-pack/plugins/security_solution/public/common/components/threat_match/logic_buttons.stories.tsx +++ b/x-pack/plugins/security_solution/public/common/components/threat_match/logic_buttons.stories.tsx @@ -9,7 +9,7 @@ import { storiesOf, addDecorator } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import React from 'react'; import { ThemeProvider } from 'styled-components'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; import { LogicButtons } from './logic_buttons'; diff --git a/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar.test.tsx b/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar.test.tsx index 7a9413a92843e..73acaa48983b4 100644 --- a/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/utility_bar/utility_bar.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; +import { euiDarkVars } from '@kbn/ui-shared-deps-src/theme'; import { mount, shallow } from 'enzyme'; import React from 'react'; diff --git a/x-pack/plugins/security_solution/public/common/lib/theme/use_eui_theme.tsx b/x-pack/plugins/security_solution/public/common/lib/theme/use_eui_theme.tsx index e9b66728b9a1d..0057666ba4262 100644 --- a/x-pack/plugins/security_solution/public/common/lib/theme/use_eui_theme.tsx +++ b/x-pack/plugins/security_solution/public/common/lib/theme/use_eui_theme.tsx @@ -5,8 +5,10 @@ * 2.0. */ -import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; -import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; +import { + euiLightVars as lightTheme, + euiDarkVars as darkTheme, +} from '@kbn/ui-shared-deps-src/theme'; import { DEFAULT_DARK_MODE } from '../../../../common/constants'; import { useUiSetting$ } from '../kibana'; diff --git a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx index 7ea93bb7ce8fb..f180dd2baf5f4 100644 --- a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; +import { euiDarkVars } from '@kbn/ui-shared-deps-src/theme'; import { I18nProvider } from '@kbn/i18n/react'; import React from 'react'; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/severity_badge/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/severity_badge/index.tsx index 32075c48c7ff3..728e0ec871e93 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/severity_badge/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/severity_badge/index.tsx @@ -8,7 +8,7 @@ import { upperFirst } from 'lodash/fp'; import React from 'react'; import { EuiHealth } from '@elastic/eui'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; interface Props { value: string; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/data.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/data.tsx index 264e499d9cf86..df50946f058ba 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/data.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_about_rule/data.tsx @@ -7,7 +7,7 @@ import styled from 'styled-components'; import { EuiHealth } from '@elastic/eui'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; import React from 'react'; import { Severity } from '@kbn/securitysolution-io-ts-alerting-types'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.stories.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.stories.tsx index 796ecc30f4033..09321244e0abc 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.stories.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.stories.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import { addDecorator, storiesOf } from '@storybook/react'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; import { EuiCheckbox, EuiSpacer, EuiSwitch, EuiText } from '@elastic/eui'; import { OperatingSystem } from '../../../../../../../common/endpoint/types'; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/index.stories.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/index.stories.tsx index 75323f8b55174..ecc18d5d52fd9 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/index.stories.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/trusted_apps_grid/index.stories.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { Provider } from 'react-redux'; import { ThemeProvider } from 'styled-components'; import { storiesOf } from '@storybook/react'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; import { EuiHorizontalRule } from '@elastic/eui'; import { KibanaContextProvider } from '../../../../../../../../../../src/plugins/kibana_react/public'; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/view_type_toggle/index.stories.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/view_type_toggle/index.stories.tsx index b8f98ebcf78bb..484f17318f839 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/view_type_toggle/index.stories.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/view_type_toggle/index.stories.tsx @@ -8,7 +8,7 @@ import React, { useState } from 'react'; import { ThemeProvider } from 'styled-components'; import { storiesOf, addDecorator } from '@storybook/react'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; import { ViewType } from '../../../state'; import { ViewTypeToggle } from '.'; diff --git a/x-pack/plugins/security_solution/public/network/components/details/index.tsx b/x-pack/plugins/security_solution/public/network/components/details/index.tsx index 0b53a4bfb3fe2..5cd2f4dfd72c8 100644 --- a/x-pack/plugins/security_solution/public/network/components/details/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/details/index.tsx @@ -5,8 +5,10 @@ * 2.0. */ -import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; -import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; +import { + euiLightVars as lightTheme, + euiDarkVars as darkTheme, +} from '@kbn/ui-shared-deps-src/theme'; import React from 'react'; import { DEFAULT_DARK_MODE } from '../../../../common/constants'; diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/tooltip_footer.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/tooltip_footer.tsx index dbb280228e504..a557ee7b8b190 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/tooltip_footer.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/tooltip_footer.tsx @@ -14,7 +14,7 @@ import { EuiIcon, EuiText, } from '@elastic/eui'; -import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars as theme } from '@kbn/ui-shared-deps-src/theme'; import styled from 'styled-components'; import * as i18n from '../translations'; diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx index e73096aa3babf..0708892affe13 100644 --- a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx @@ -6,8 +6,10 @@ */ import { EuiHorizontalRule } from '@elastic/eui'; -import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; -import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; +import { + euiLightVars as lightTheme, + euiDarkVars as darkTheme, +} from '@kbn/ui-shared-deps-src/theme'; import { getOr } from 'lodash/fp'; import React, { useCallback, useMemo } from 'react'; diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_colors.ts b/x-pack/plugins/security_solution/public/resolver/view/use_colors.ts index baf3eff8b391c..f52075cbe4d85 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/use_colors.ts +++ b/x-pack/plugins/security_solution/public/resolver/view/use_colors.ts @@ -5,10 +5,8 @@ * 2.0. */ -import euiThemeAmsterdamDark from '@elastic/eui/dist/eui_theme_amsterdam_dark.json'; -import euiThemeAmsterdamLight from '@elastic/eui/dist/eui_theme_amsterdam_light.json'; +import { darkMode, euiThemeVars } from '@kbn/ui-shared-deps-src/theme'; import { useMemo } from 'react'; -import { useUiSetting } from '../../../../../../src/plugins/kibana_react/public'; type ResolverColorNames = | 'copyableFieldBackground' @@ -31,24 +29,22 @@ type ColorMap = Record; * Get access to Kibana-theme based colors. */ export function useColors(): ColorMap { - const isDarkMode = useUiSetting('theme:darkMode'); - const theme = isDarkMode ? euiThemeAmsterdamDark : euiThemeAmsterdamLight; return useMemo(() => { return { - copyableFieldBackground: theme.euiColorLightShade, - descriptionText: theme.euiTextColor, - full: theme.euiColorFullShade, - graphControls: theme.euiColorDarkestShade, - graphControlsBackground: theme.euiColorEmptyShade, - graphControlsBorderColor: theme.euiColorLightShade, - processBackingFill: `${theme.euiColorPrimary}${isDarkMode ? '1F' : '0F'}`, // Add opacity 0F = 6% , 1F = 12% - resolverBackground: theme.euiColorEmptyShade, - resolverEdge: isDarkMode ? theme.euiColorLightShade : theme.euiColorLightestShade, - resolverBreadcrumbBackground: theme.euiColorLightestShade, - resolverEdgeText: isDarkMode ? theme.euiColorFullShade : theme.euiColorDarkShade, - triggerBackingFill: `${theme.euiColorDanger}${isDarkMode ? '1F' : '0F'}`, - pillStroke: theme.euiColorLightShade, - linkColor: theme.euiLinkColor, + copyableFieldBackground: euiThemeVars.euiColorLightShade, + descriptionText: euiThemeVars.euiTextColor, + full: euiThemeVars.euiColorFullShade, + graphControls: euiThemeVars.euiColorDarkestShade, + graphControlsBackground: euiThemeVars.euiColorEmptyShade, + graphControlsBorderColor: euiThemeVars.euiColorLightShade, + processBackingFill: `${euiThemeVars.euiColorPrimary}${darkMode ? '1F' : '0F'}`, // Add opacity 0F = 6% , 1F = 12% + resolverBackground: euiThemeVars.euiColorEmptyShade, + resolverEdge: darkMode ? euiThemeVars.euiColorLightShade : euiThemeVars.euiColorLightestShade, + resolverBreadcrumbBackground: euiThemeVars.euiColorLightestShade, + resolverEdgeText: darkMode ? euiThemeVars.euiColorFullShade : euiThemeVars.euiColorDarkShade, + triggerBackingFill: `${euiThemeVars.euiColorDanger}${darkMode ? '1F' : '0F'}`, + pillStroke: euiThemeVars.euiColorLightShade, + linkColor: euiThemeVars.euiLinkColor, }; - }, [isDarkMode, theme]); + }, []); } diff --git a/x-pack/plugins/security_solution/public/resolver/view/use_cube_assets.ts b/x-pack/plugins/security_solution/public/resolver/view/use_cube_assets.ts index 774c5f0ce1c74..f5a9c37623c47 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/use_cube_assets.ts +++ b/x-pack/plugins/security_solution/public/resolver/view/use_cube_assets.ts @@ -7,12 +7,10 @@ import { i18n } from '@kbn/i18n'; +import { euiThemeVars } from '@kbn/ui-shared-deps-src/theme'; import { ButtonColor } from '@elastic/eui'; -import euiThemeAmsterdamDark from '@elastic/eui/dist/eui_theme_amsterdam_dark.json'; -import euiThemeAmsterdamLight from '@elastic/eui/dist/eui_theme_amsterdam_light.json'; import { useMemo } from 'react'; import { ResolverProcessType, NodeDataStatus } from '../types'; -import { useUiSetting } from '../../../../../../src/plugins/kibana_react/public'; import { useSymbolIDs } from './use_symbol_ids'; import { useColors } from './use_colors'; @@ -24,8 +22,6 @@ export function useCubeAssets( isProcessTrigger: boolean ): NodeStyleConfig { const SymbolIds = useSymbolIDs(); - const isDarkMode = useUiSetting('theme:darkMode'); - const theme = isDarkMode ? euiThemeAmsterdamDark : euiThemeAmsterdamLight; const colorMap = useColors(); const nodeAssets: NodeStyleMap = useMemo( @@ -39,7 +35,7 @@ export function useCubeAssets( }), isLabelFilled: true, labelButtonFill: 'primary', - strokeColor: theme.euiColorPrimary, + strokeColor: euiThemeVars.euiColorPrimary, }, loadingCube: { backingFill: colorMap.processBackingFill, @@ -50,7 +46,7 @@ export function useCubeAssets( }), isLabelFilled: false, labelButtonFill: 'primary', - strokeColor: theme.euiColorPrimary, + strokeColor: euiThemeVars.euiColorPrimary, }, errorCube: { backingFill: colorMap.processBackingFill, @@ -61,7 +57,7 @@ export function useCubeAssets( }), isLabelFilled: false, labelButtonFill: 'primary', - strokeColor: theme.euiColorPrimary, + strokeColor: euiThemeVars.euiColorPrimary, }, runningTriggerCube: { backingFill: colorMap.triggerBackingFill, @@ -72,7 +68,7 @@ export function useCubeAssets( }), isLabelFilled: true, labelButtonFill: 'danger', - strokeColor: theme.euiColorDanger, + strokeColor: euiThemeVars.euiColorDanger, }, terminatedProcessCube: { backingFill: colorMap.processBackingFill, @@ -86,7 +82,7 @@ export function useCubeAssets( ), isLabelFilled: false, labelButtonFill: 'primary', - strokeColor: theme.euiColorPrimary, + strokeColor: euiThemeVars.euiColorPrimary, }, terminatedTriggerCube: { backingFill: colorMap.triggerBackingFill, @@ -100,10 +96,10 @@ export function useCubeAssets( ), isLabelFilled: false, labelButtonFill: 'danger', - strokeColor: theme.euiColorDanger, + strokeColor: euiThemeVars.euiColorDanger, }, }), - [SymbolIds, colorMap, theme] + [SymbolIds, colorMap] ); if (cubeType === 'terminated') { diff --git a/x-pack/plugins/timelines/public/mock/test_providers.tsx b/x-pack/plugins/timelines/public/mock/test_providers.tsx index 9fa6177cccee1..0fb1afec43627 100644 --- a/x-pack/plugins/timelines/public/mock/test_providers.tsx +++ b/x-pack/plugins/timelines/public/mock/test_providers.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; +import { euiDarkVars } from '@kbn/ui-shared-deps-src/theme'; import { I18nProvider } from '@kbn/i18n/react'; import React from 'react'; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_messages_pane.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_messages_pane.tsx index fc95b818126bc..3f8b0549c219b 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_messages_pane.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_messages_pane.tsx @@ -10,7 +10,7 @@ import React, { useState } from 'react'; import { EuiSpacer, EuiBasicTable } from '@elastic/eui'; // @ts-ignore import { formatDate } from '@elastic/eui/lib/services/format'; -import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars as theme } from '@kbn/ui-shared-deps-src/theme'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/execution_duration_chart.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/execution_duration_chart.tsx index ea8c16d03cc04..6b5a7ee3b3e4d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/execution_duration_chart.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/common/components/execution_duration_chart.tsx @@ -16,7 +16,7 @@ import { EuiIconTip, EuiTitle, } from '@elastic/eui'; -import lightEuiTheme from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars as lightEuiTheme } from '@kbn/ui-shared-deps-src/theme'; import { Axis, BarSeries, Chart, CurveType, LineSeries, Settings } from '@elastic/charts'; import { assign, fill } from 'lodash'; import { formatMillisForDisplay } from '../../../lib/execution_duration_utils'; diff --git a/x-pack/plugins/uptime/public/contexts/uptime_theme_context.tsx b/x-pack/plugins/uptime/public/contexts/uptime_theme_context.tsx index 7798aabc879a6..6df3879ef7407 100644 --- a/x-pack/plugins/uptime/public/contexts/uptime_theme_context.tsx +++ b/x-pack/plugins/uptime/public/contexts/uptime_theme_context.tsx @@ -5,9 +5,8 @@ * 2.0. */ -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import { euiLightVars, euiDarkVars } from '@kbn/ui-shared-deps-src/theme'; import React, { createContext, useMemo } from 'react'; -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme'; import { DARK_THEME, LIGHT_THEME, PartialTheme, Theme } from '@elastic/charts'; import { UptimeAppColors } from '../apps/uptime_app'; From 3ef6e42c46e9893ad44aba0bce7aa9a963ac4ef4 Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 10 Nov 2021 14:29:39 -0700 Subject: [PATCH 16/43] skip suite failing es promotion (#118251) (cherry picked from commit baa658f8a7ce1fec67b176b184a61c4fec52fb7f) --- x-pack/test/functional/apps/ml/model_management/model_list.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/ml/model_management/model_list.ts b/x-pack/test/functional/apps/ml/model_management/model_list.ts index 955639dbe60a4..aac1ad5b1e50b 100644 --- a/x-pack/test/functional/apps/ml/model_management/model_list.ts +++ b/x-pack/test/functional/apps/ml/model_management/model_list.ts @@ -10,7 +10,8 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const ml = getService('ml'); - describe('trained models', function () { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/118251 + describe.skip('trained models', function () { before(async () => { await ml.trainedModels.createTestTrainedModels('classification', 15, true); await ml.trainedModels.createTestTrainedModels('regression', 15); From 139ed0327eb9adaf1a63733b65a1c147f6b165ca Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 10 Nov 2021 16:35:09 -0500 Subject: [PATCH 17/43] [Exploratory view] Show index pattern permission error (#117539) (#118236) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Shahzad --- .../hooks/use_app_index_pattern.tsx | 15 +++++++++++++-- .../shared/exploratory_view/rtl_helpers.tsx | 1 + .../series_editor/report_metric_options.tsx | 19 ++++++++++++++++++- .../utils/observability_index_patterns.ts | 1 - 4 files changed, 32 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx index 8a766075ef8d2..9bd611c05e956 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx @@ -6,6 +6,7 @@ */ import React, { createContext, useContext, Context, useState, useCallback, useMemo } from 'react'; +import { HttpFetchError } from 'kibana/public'; import { IndexPattern } from '../../../../../../../../src/plugins/data/common'; import { AppDataType } from '../types'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; @@ -16,6 +17,7 @@ import { getDataHandler } from '../../../../data_handler'; export interface IndexPatternContext { loading: boolean; indexPatterns: IndexPatternState; + indexPatternErrors: IndexPatternErrors; hasAppData: HasAppDataState; loadIndexPattern: (params: { dataType: AppDataType }) => void; } @@ -28,11 +30,15 @@ interface ProviderProps { type HasAppDataState = Record; export type IndexPatternState = Record; +export type IndexPatternErrors = Record; type LoadingState = Record; export function IndexPatternContextProvider({ children }: ProviderProps) { const [loading, setLoading] = useState({} as LoadingState); const [indexPatterns, setIndexPatterns] = useState({} as IndexPatternState); + const [indexPatternErrors, setIndexPatternErrors] = useState( + {} as IndexPatternErrors + ); const [hasAppData, setHasAppData] = useState({ infra_metrics: null, infra_logs: null, @@ -78,6 +84,9 @@ export function IndexPatternContextProvider({ children }: ProviderProps) { } setLoading((prevState) => ({ ...prevState, [dataType]: false })); } catch (e) { + if ((e as HttpFetchError).body.error === 'Forbidden') { + setIndexPatternErrors((prevState) => ({ ...prevState, [dataType]: e })); + } setLoading((prevState) => ({ ...prevState, [dataType]: false })); } } @@ -91,6 +100,7 @@ export function IndexPatternContextProvider({ children }: ProviderProps) { hasAppData, indexPatterns, loadIndexPattern, + indexPatternErrors, loading: !!Object.values(loading).find((loadingT) => loadingT), }} > @@ -100,7 +110,7 @@ export function IndexPatternContextProvider({ children }: ProviderProps) { } export const useAppIndexPatternContext = (dataType?: AppDataType) => { - const { loading, hasAppData, loadIndexPattern, indexPatterns } = useContext( + const { loading, hasAppData, loadIndexPattern, indexPatterns, indexPatternErrors } = useContext( IndexPatternContext as unknown as Context ); @@ -113,9 +123,10 @@ export const useAppIndexPatternContext = (dataType?: AppDataType) => { hasAppData, loading, indexPatterns, + indexPatternErrors, indexPattern: dataType ? indexPatterns?.[dataType] : undefined, hasData: dataType ? hasAppData?.[dataType] : undefined, loadIndexPattern, }; - }, [dataType, hasAppData, indexPatterns, loadIndexPattern, loading]); + }, [dataType, hasAppData, indexPatternErrors, indexPatterns, loadIndexPattern, loading]); }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx index efca1152e175d..612cbfcc4bfdf 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx @@ -243,6 +243,7 @@ export const mockAppIndexPattern = () => { hasAppData: { ux: true } as any, loadIndexPattern, indexPatterns: { ux: mockIndexPattern } as unknown as Record, + indexPatternErrors: {} as any, }); return { spy, loadIndexPattern }; }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.tsx index eca18f0eb0dd4..410356d0078d8 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.tsx @@ -35,7 +35,7 @@ export function ReportMetricOptions({ seriesId, series, seriesConfig }: Props) { const [showOptions, setShowOptions] = useState(false); const metricOptions = seriesConfig?.metricOptions; - const { indexPatterns, loading } = useAppIndexPatternContext(); + const { indexPatterns, indexPatternErrors, loading } = useAppIndexPatternContext(); const onChange = (value?: string) => { setSeries(seriesId, { @@ -49,6 +49,7 @@ export function ReportMetricOptions({ seriesId, series, seriesConfig }: Props) { } const indexPattern = indexPatterns?.[series.dataType]; + const indexPatternError = indexPatternErrors?.[series.dataType]; const options = (metricOptions ?? []).map(({ label, field, id }) => { let disabled = false; @@ -80,6 +81,17 @@ export function ReportMetricOptions({ seriesId, series, seriesConfig }: Props) { }; }); + if (indexPatternError && !indexPattern && !loading) { + // TODO: Add a link to docs to explain how to add index patterns + return ( + + {indexPatternError.body.error === 'Forbidden' + ? NO_PERMISSIONS + : indexPatternError.body.message} + + ); + } + if (!indexPattern && !loading) { return {NO_DATA_AVAILABLE}; } @@ -152,3 +164,8 @@ const REMOVE_REPORT_METRIC_LABEL = i18n.translate( const NO_DATA_AVAILABLE = i18n.translate('xpack.observability.expView.seriesEditor.noData', { defaultMessage: 'No data available', }); + +const NO_PERMISSIONS = i18n.translate('xpack.observability.expView.seriesEditor.noPermissions', { + defaultMessage: + "Unable to create Index Pattern. You don't have the required permission, please contact your admin.", +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts index b61af3a61c3dc..f591ef63a61fb 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts @@ -86,7 +86,6 @@ export class ObservabilityIndexPatterns { } const appIndicesPattern = getAppIndicesWithPattern(app, indices); - return await this.data.indexPatterns.createAndSave({ title: appIndicesPattern, id: getAppIndexPatternId(app, indices), From e936e4d1616c6823bd73bd4390b0b4c3e4525251 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 10 Nov 2021 16:37:32 -0500 Subject: [PATCH 18/43] [8.0] [Fleet] Show callout when EPR unavailable (#117598) (#118081) * [Fleet] Show callout when EPR unavailable (#117598) * Update src/core/public/doc_links/doc_links_service.ts Co-authored-by: Thomas Neirynck --- ...-plugin-core-public.doclinksstart.links.md | 2 + .../public/doc_links/doc_links_service.ts | 4 + src/core/public/public.api.md | 2 + .../epm/screens/home/available_packages.tsx | 109 ++++++++++++++++-- .../plugins/fleet/server/errors/handlers.ts | 9 +- 5 files changed, 115 insertions(+), 11 deletions(-) diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md index 676f7420c8bb9..b8fc9b85f6bd2 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md @@ -236,6 +236,7 @@ readonly links: { fleetServerAddFleetServer: string; settings: string; settingsFleetServerHostSettings: string; + settingsFleetServerProxySettings: string; troubleshooting: string; elasticAgent: string; datastreams: string; @@ -245,6 +246,7 @@ readonly links: { upgradeElasticAgent712lower: string; learnMoreBlog: string; apiKeysLearnMore: string; + onPremRegistry: string; }>; readonly ecs: { readonly guide: string; diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index f6e2a71449460..1389362168eb2 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -477,6 +477,7 @@ export class DocLinksService { fleetServerAddFleetServer: `${FLEET_DOCS}fleet-server.html#add-fleet-server`, settings: `${FLEET_DOCS}fleet-settings.html#fleet-server-hosts-setting`, settingsFleetServerHostSettings: `${FLEET_DOCS}fleet-settings.html#fleet-server-hosts-setting`, + settingsFleetServerProxySettings: `${KIBANA_DOCS}fleet-settings-kb.html#fleet-data-visualizer-settings`, troubleshooting: `${FLEET_DOCS}fleet-troubleshooting.html`, elasticAgent: `${FLEET_DOCS}elastic-agent-installation.html`, beatsAgentComparison: `${FLEET_DOCS}beats-agent-comparison.html`, @@ -488,6 +489,7 @@ export class DocLinksService { upgradeElasticAgent712lower: `${FLEET_DOCS}upgrade-elastic-agent.html#upgrade-7.12-lower`, learnMoreBlog: `${ELASTIC_WEBSITE_URL}blog/elastic-agent-and-fleet-make-it-easier-to-integrate-your-systems-with-elastic`, apiKeysLearnMore: `${KIBANA_DOCS}api-keys.html`, + onPremRegistry: `${ELASTIC_WEBSITE_URL}guide/en/integrations-developer/current/air-gapped.html`, }, ecs: { guide: `${ELASTIC_WEBSITE_URL}guide/en/ecs/current/index.html`, @@ -746,6 +748,7 @@ export interface DocLinksStart { fleetServerAddFleetServer: string; settings: string; settingsFleetServerHostSettings: string; + settingsFleetServerProxySettings: string; troubleshooting: string; elasticAgent: string; datastreams: string; @@ -755,6 +758,7 @@ export interface DocLinksStart { upgradeElasticAgent712lower: string; learnMoreBlog: string; apiKeysLearnMore: string; + onPremRegistry: string; }>; readonly ecs: { readonly guide: string; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 6367d69c4f4dd..612674cc8df3a 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -703,6 +703,7 @@ export interface DocLinksStart { fleetServerAddFleetServer: string; settings: string; settingsFleetServerHostSettings: string; + settingsFleetServerProxySettings: string; troubleshooting: string; elasticAgent: string; datastreams: string; @@ -712,6 +713,7 @@ export interface DocLinksStart { upgradeElasticAgent712lower: string; learnMoreBlog: string; apiKeysLearnMore: string; + onPremRegistry: string; }>; readonly ecs: { readonly guide: string; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx index ff65304ca475c..62f911ffdbbb7 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx @@ -5,10 +5,12 @@ * 2.0. */ +import type { FunctionComponent } from 'react'; import React, { memo, useMemo, useState } from 'react'; import { useLocation, useHistory, useParams } from 'react-router-dom'; import _ from 'lodash'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { EuiHorizontalRule, EuiFlexItem, @@ -16,6 +18,8 @@ import { EuiSpacer, EuiCard, EuiIcon, + EuiCallOut, + EuiLink, } from '@elastic/eui'; import { useStartServices } from '../../../../hooks'; @@ -52,6 +56,76 @@ import type { CategoryFacet } from './category_facets'; import type { CategoryParams } from '.'; import { getParams, categoryExists, mapToCard } from '.'; +const NoEprCallout: FunctionComponent<{ statusCode?: number }> = ({ + statusCode, +}: { + statusCode?: number; +}) => { + let titleMessage; + let descriptionMessage; + if (statusCode === 502) { + titleMessage = i18n.translate('xpack.fleet.epmList.eprUnavailableBadGatewayCalloutTitle', { + defaultMessage: + 'Kibana cannot reach the Elastic Package Registry, which provides Elastic Agent integrations\n', + }); + descriptionMessage = ( + , + onpremregistry: , + }} + /> + ); + } else { + titleMessage = i18n.translate('xpack.fleet.epmList.eprUnavailable400500CalloutTitle', { + defaultMessage: + 'Kibana cannot connect to the Elastic Package Registry, which provides Elastic Agent integrations\n', + }); + descriptionMessage = ( + , + onpremregistry: , + }} + /> + ); + } + + return ( + +

    {descriptionMessage}

    +
    + ); +}; + +function ProxyLink() { + const { docLinks } = useStartServices(); + + return ( + + {i18n.translate('xpack.fleet.epmList.proxyLinkSnippedText', { + defaultMessage: 'proxy server', + })} + + ); +} + +function OnPremLink() { + const { docLinks } = useStartServices(); + + return ( + + {i18n.translate('xpack.fleet.epmList.onPremLinkSnippetText', { + defaultMessage: 'your own registry', + })} + + ); +} + function getAllCategoriesFromIntegrations(pkg: PackageListItem) { if (!doesPackageHaveIntegrations(pkg)) { return pkg.categories; @@ -133,10 +207,13 @@ export const AvailablePackages: React.FC = memo(() => { history.replace(pagePathGetters.integrations_all({ searchTerm: search })[1]); } - const { data: eprPackages, isLoading: isLoadingAllPackages } = useGetPackages({ + const { + data: eprPackages, + isLoading: isLoadingAllPackages, + error: eprPackageLoadingError, + } = useGetPackages({ category: '', }); - const eprIntegrationList = useMemo( () => packageListToIntegrationsList(eprPackages?.response || []), [eprPackages] @@ -166,18 +243,23 @@ export const AvailablePackages: React.FC = memo(() => { return a.title.localeCompare(b.title); }); - const { data: eprCategories, isLoading: isLoadingCategories } = useGetCategories({ + const { + data: eprCategories, + isLoading: isLoadingCategories, + error: eprCategoryLoadingError, + } = useGetCategories({ include_policy_templates: true, }); const categories = useMemo(() => { - const eprAndCustomCategories: CategoryFacet[] = - isLoadingCategories || !eprCategories - ? [] - : mergeCategoriesAndCount( - eprCategories.response as Array<{ id: string; title: string; count: number }>, - cards - ); + const eprAndCustomCategories: CategoryFacet[] = isLoadingCategories + ? [] + : mergeCategoriesAndCount( + eprCategories + ? (eprCategories.response as Array<{ id: string; title: string; count: number }>) + : [], + cards + ); return [ { ...ALL_CATEGORY, @@ -281,6 +363,12 @@ export const AvailablePackages: React.FC = memo(() => { ); + let noEprCallout; + if (eprPackageLoadingError || eprCategoryLoadingError) { + const error = eprPackageLoadingError || eprCategoryLoadingError; + noEprCallout = ; + } + return ( { setSelectedCategory={setSelectedCategory} onSearchChange={setSearchTerm} showMissingIntegrationMessage + callout={noEprCallout} /> ); }); diff --git a/x-pack/plugins/fleet/server/errors/handlers.ts b/x-pack/plugins/fleet/server/errors/handlers.ts index c47b1a07780ec..e171a5bafba90 100644 --- a/x-pack/plugins/fleet/server/errors/handlers.ts +++ b/x-pack/plugins/fleet/server/errors/handlers.ts @@ -24,7 +24,9 @@ import { IngestManagerError, PackageNotFoundError, PackageUnsupportedMediaTypeError, + RegistryConnectionError, RegistryError, + RegistryResponseError, } from './index'; type IngestErrorHandler = ( @@ -40,7 +42,12 @@ interface IngestErrorHandlerParams { // this type is based on BadRequest values observed while debugging https://github.com/elastic/kibana/issues/75862 const getHTTPResponseCode = (error: IngestManagerError): number => { - if (error instanceof RegistryError) { + if (error instanceof RegistryResponseError) { + // 4xx/5xx's from EPR + return 500; + } + if (error instanceof RegistryConnectionError || error instanceof RegistryError) { + // Connection errors (ie. RegistryConnectionError) / fallback (RegistryError) from EPR return 502; // Bad Gateway } if (error instanceof PackageNotFoundError) { From 2e27239bcb2913cae1ace5abaf18402602dc987d Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 10 Nov 2021 16:48:06 -0500 Subject: [PATCH 19/43] [Enterprise Search] Added a test helper #117948 (#118239) Co-authored-by: Jason Stoltzfus --- .../analytics/analytics_logic.test.ts | 24 ++----- .../api_logs/api_logs_logic.test.ts | 11 ++- .../crawler/crawler_domains_logic.test.ts | 9 +-- .../components/crawler/crawler_logic.test.ts | 25 ++----- .../crawler_single_domain_logic.test.ts | 25 ++----- .../credentials/credentials_logic.test.ts | 31 ++------- .../components/suggestions_logic.test.tsx | 16 ++--- .../curations/curation/curation_logic.test.ts | 17 ++--- .../curations/curations_logic.test.ts | 9 +-- .../curation_suggestion_logic.test.ts | 27 +++----- .../ignored_queries_logic.test.ts | 20 ++---- .../curations_settings_logic.test.ts | 24 ++----- .../documents/document_detail_logic.test.ts | 9 +-- .../engine_overview_logic.test.ts | 16 ++--- .../components/engines/engines_logic.test.ts | 15 +--- .../relevance_tuning_logic.test.ts | 9 +-- .../result_settings_logic.test.ts | 23 ++----- .../role_mappings/role_mappings_logic.test.ts | 22 ++---- .../schema/schema_base_logic.test.ts | 16 ++--- .../search_ui/search_ui_logic.test.ts | 11 ++- .../synonyms/synonyms_logic.test.ts | 9 +-- .../recursively_fetch_engines/index.test.ts | 12 ++-- ...dpoint_inline_editable_table_logic.test.ts | 23 ++----- .../test_helpers/error_handling.ts | 22 ++++++ .../public/applications/test_helpers/index.ts | 1 + .../add_source/add_source_logic.test.ts | 37 ++-------- .../display_settings_logic.test.ts | 16 ++--- .../components/schema/schema_logic.test.ts | 16 ++--- .../synchronization_logic.test.ts | 17 +---- .../content_sources/source_logic.test.ts | 69 ++----------------- .../content_sources/sources_logic.test.ts | 42 ++--------- .../views/groups/group_logic.test.ts | 32 ++------- .../views/groups/groups_logic.test.ts | 23 ++----- .../role_mappings/role_mappings_logic.test.ts | 14 ++-- .../views/security/security_logic.test.ts | 27 ++------ .../views/settings/settings_logic.test.ts | 45 +++--------- 36 files changed, 185 insertions(+), 579 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/test_helpers/error_handling.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_logic.test.ts index 7b877419a2977..62ba44128663a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_logic.test.ts @@ -5,12 +5,7 @@ * 2.0. */ -import { - LogicMounter, - mockKibanaValues, - mockHttpValues, - mockFlashMessageHelpers, -} from '../../../__mocks__/kea_logic'; +import { LogicMounter, mockKibanaValues, mockHttpValues } from '../../../__mocks__/kea_logic'; jest.mock('../engine', () => ({ EngineLogic: { values: { engineName: 'test-engine' } }, @@ -18,6 +13,8 @@ jest.mock('../engine', () => ({ import { nextTick } from '@kbn/test/jest'; +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; + import { DEFAULT_START_DATE, DEFAULT_END_DATE } from './constants'; import { AnalyticsLogic } from './'; @@ -26,7 +23,6 @@ describe('AnalyticsLogic', () => { const { mount } = new LogicMounter(AnalyticsLogic); const { history } = mockKibanaValues; const { http } = mockHttpValues; - const { flashAPIErrors } = mockFlashMessageHelpers; const DEFAULT_VALUES = { dataLoading: true, @@ -197,14 +193,9 @@ describe('AnalyticsLogic', () => { ); }); - it('handles errors', async () => { - http.get.mockReturnValueOnce(Promise.reject('error')); + itShowsServerErrorAsFlashMessage(http.get, () => { mount(); - AnalyticsLogic.actions.loadAnalyticsData(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); @@ -259,14 +250,9 @@ describe('AnalyticsLogic', () => { ); }); - it('handles errors', async () => { - http.get.mockReturnValueOnce(Promise.reject('error')); + itShowsServerErrorAsFlashMessage(http.get, () => { mount(); - AnalyticsLogic.actions.loadQueryData('some-query'); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs_logic.test.ts index 2f3aedc8fa11d..51d51b5aee88c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/api_logs/api_logs_logic.test.ts @@ -17,12 +17,14 @@ import { nextTick } from '@kbn/test/jest'; import { DEFAULT_META } from '../../../shared/constants'; +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; + import { ApiLogsLogic } from './'; describe('ApiLogsLogic', () => { const { mount, unmount } = new LogicMounter(ApiLogsLogic); const { http } = mockHttpValues; - const { flashAPIErrors, flashErrorToast } = mockFlashMessageHelpers; + const { flashErrorToast } = mockFlashMessageHelpers; const DEFAULT_VALUES = { dataLoading: true, @@ -176,14 +178,9 @@ describe('ApiLogsLogic', () => { expect(ApiLogsLogic.actions.updateView).toHaveBeenCalledWith(MOCK_API_RESPONSE); }); - it('handles API errors', async () => { - http.get.mockReturnValueOnce(Promise.reject('error')); + itShowsServerErrorAsFlashMessage(http.get, () => { mount(); - ApiLogsLogic.actions.fetchApiLogs(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_domains_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_domains_logic.test.ts index 6cf2f21fc6d2e..fda96ca5f8381 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_domains_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_domains_logic.test.ts @@ -18,6 +18,8 @@ import { Meta } from '../../../../../common/types'; import { DEFAULT_META } from '../../../shared/constants'; +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers/error_handling'; + import { CrawlerDomainsLogic, CrawlerDomainsValues } from './crawler_domains_logic'; import { CrawlerDataFromServer, CrawlerDomain, CrawlerDomainFromServer } from './types'; import { crawlerDataServerToClient } from './utils'; @@ -193,13 +195,8 @@ describe('CrawlerDomainsLogic', () => { ); }); - it('calls flashApiErrors when there is an error', async () => { - http.delete.mockReturnValue(Promise.reject('error')); - + itShowsServerErrorAsFlashMessage(http.delete, () => { CrawlerDomainsLogic.actions.deleteDomain({ id: '1234' } as CrawlerDomain); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.test.ts index 7ba1adb51bbfb..b2321073f3d95 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_logic.test.ts @@ -14,6 +14,8 @@ import '../../__mocks__/engine_logic.mock'; import { nextTick } from '@kbn/test/jest'; +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; + import { CrawlerDomainsLogic } from './crawler_domains_logic'; import { CrawlerLogic, CrawlerValues } from './crawler_logic'; import { @@ -280,15 +282,8 @@ describe('CrawlerLogic', () => { }); }); - describe('on failure', () => { - it('flashes an error message', async () => { - http.post.mockReturnValueOnce(Promise.reject('error')); - - CrawlerLogic.actions.startCrawl(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); - }); + itShowsServerErrorAsFlashMessage(http.post, () => { + CrawlerLogic.actions.startCrawl(); }); }); @@ -308,16 +303,8 @@ describe('CrawlerLogic', () => { }); }); - describe('on failure', () => { - it('flashes an error message', async () => { - jest.spyOn(CrawlerLogic.actions, 'fetchCrawlerData'); - http.post.mockReturnValueOnce(Promise.reject('error')); - - CrawlerLogic.actions.stopCrawl(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); - }); + itShowsServerErrorAsFlashMessage(http.post, () => { + CrawlerLogic.actions.stopCrawl(); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.test.ts index 03e20ea988f98..547218ad6a2c1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.test.ts @@ -23,6 +23,8 @@ jest.mock('./crawler_logic', () => ({ import { nextTick } from '@kbn/test/jest'; +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; + import { CrawlerLogic } from './crawler_logic'; import { CrawlerSingleDomainLogic, CrawlerSingleDomainValues } from './crawler_single_domain_logic'; import { CrawlerDomain, CrawlerPolicies, CrawlerRules } from './types'; @@ -35,7 +37,7 @@ const DEFAULT_VALUES: CrawlerSingleDomainValues = { describe('CrawlerSingleDomainLogic', () => { const { mount } = new LogicMounter(CrawlerSingleDomainLogic); const { http } = mockHttpValues; - const { flashAPIErrors, flashSuccessToast } = mockFlashMessageHelpers; + const { flashSuccessToast } = mockFlashMessageHelpers; beforeEach(() => { jest.clearAllMocks(); @@ -176,13 +178,8 @@ describe('CrawlerSingleDomainLogic', () => { expect(navigateToUrl).toHaveBeenCalledWith('/engines/some-engine/crawler'); }); - it('calls flashApiErrors when there is an error', async () => { - http.delete.mockReturnValue(Promise.reject('error')); - + itShowsServerErrorAsFlashMessage(http.delete, () => { CrawlerSingleDomainLogic.actions.deleteDomain({ id: '1234' } as CrawlerDomain); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); @@ -218,13 +215,8 @@ describe('CrawlerSingleDomainLogic', () => { }); }); - it('displays any errors to the user', async () => { - http.get.mockReturnValueOnce(Promise.reject('error')); - + itShowsServerErrorAsFlashMessage(http.get, () => { CrawlerSingleDomainLogic.actions.fetchDomainData('507f1f77bcf86cd799439011'); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); @@ -272,16 +264,11 @@ describe('CrawlerSingleDomainLogic', () => { }); }); - it('displays any errors to the user', async () => { - http.put.mockReturnValueOnce(Promise.reject('error')); - + itShowsServerErrorAsFlashMessage(http.put, () => { CrawlerSingleDomainLogic.actions.submitDeduplicationUpdate( { id: '507f1f77bcf86cd799439011' } as CrawlerDomain, { fields: ['title'], enabled: true } ); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts index 3afa531239dc1..decb98d227975 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.test.ts @@ -20,6 +20,7 @@ jest.mock('../../app_logic', () => ({ selectors: { myRole: jest.fn(() => ({})) }, }, })); +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; import { AppLogic } from '../../app_logic'; import { EngineTypes } from '../engine/types'; @@ -31,7 +32,7 @@ import { CredentialsLogic } from './credentials_logic'; describe('CredentialsLogic', () => { const { mount } = new LogicMounter(CredentialsLogic); const { http } = mockHttpValues; - const { clearFlashMessages, flashSuccessToast, flashAPIErrors } = mockFlashMessageHelpers; + const { clearFlashMessages, flashSuccessToast } = mockFlashMessageHelpers; const DEFAULT_VALUES = { activeApiToken: { @@ -1059,14 +1060,9 @@ describe('CredentialsLogic', () => { expect(CredentialsLogic.actions.setCredentialsData).toHaveBeenCalledWith(meta, results); }); - it('handles errors', async () => { + itShowsServerErrorAsFlashMessage(http.get, () => { mount(); - http.get.mockReturnValue(Promise.reject('An error occured')); - CredentialsLogic.actions.fetchCredentials(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('An error occured'); }); }); @@ -1086,14 +1082,9 @@ describe('CredentialsLogic', () => { ); }); - it('handles errors', async () => { + itShowsServerErrorAsFlashMessage(http.get, () => { mount(); - http.get.mockReturnValue(Promise.reject('An error occured')); - CredentialsLogic.actions.fetchDetails(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('An error occured'); }); }); @@ -1113,14 +1104,9 @@ describe('CredentialsLogic', () => { expect(flashSuccessToast).toHaveBeenCalled(); }); - it('handles errors', async () => { + itShowsServerErrorAsFlashMessage(http.delete, () => { mount(); - http.delete.mockReturnValue(Promise.reject('An error occured')); - CredentialsLogic.actions.deleteApiKey(tokenName); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('An error occured'); }); }); @@ -1172,14 +1158,9 @@ describe('CredentialsLogic', () => { expect(flashSuccessToast).toHaveBeenCalled(); }); - it('handles errors', async () => { + itShowsServerErrorAsFlashMessage(http.post, () => { mount(); - http.post.mockReturnValue(Promise.reject('An error occured')); - CredentialsLogic.actions.onApiTokenChange(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('An error occured'); }); describe('token type data', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_logic.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_logic.test.tsx index 3e12aa7b629f0..a3ca646bd9f54 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_logic.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_logic.test.tsx @@ -5,17 +5,15 @@ * 2.0. */ -import { - LogicMounter, - mockFlashMessageHelpers, - mockHttpValues, -} from '../../../../__mocks__/kea_logic'; +import { LogicMounter, mockHttpValues } from '../../../../__mocks__/kea_logic'; import '../../../__mocks__/engine_logic.mock'; import { nextTick } from '@kbn/test/jest'; import { DEFAULT_META } from '../../../../shared/constants'; +import { itShowsServerErrorAsFlashMessage } from '../../../../test_helpers'; + import { SuggestionsAPIResponse, SuggestionsLogic } from './suggestions_logic'; const DEFAULT_VALUES = { @@ -52,7 +50,6 @@ const MOCK_RESPONSE: SuggestionsAPIResponse = { describe('SuggestionsLogic', () => { const { mount } = new LogicMounter(SuggestionsLogic); - const { flashAPIErrors } = mockFlashMessageHelpers; const { http } = mockHttpValues; beforeEach(() => { @@ -140,14 +137,9 @@ describe('SuggestionsLogic', () => { expect(SuggestionsLogic.actions.onSuggestionsLoaded).toHaveBeenCalledWith(MOCK_RESPONSE); }); - it('handles errors', async () => { - http.post.mockReturnValueOnce(Promise.reject('error')); + itShowsServerErrorAsFlashMessage(http.post, () => { mount(); - SuggestionsLogic.actions.loadSuggestions(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.test.ts index 2b51cbb884ff9..644139250c07c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/curation_logic.test.ts @@ -15,6 +15,8 @@ import '../../../__mocks__/engine_logic.mock'; import { nextTick } from '@kbn/test/jest'; +import { itShowsServerErrorAsFlashMessage } from '../../../../test_helpers'; + import { CurationLogic } from './'; describe('CurationLogic', () => { @@ -309,14 +311,8 @@ describe('CurationLogic', () => { expect(CurationLogic.actions.loadCuration).toHaveBeenCalled(); }); - it('flashes any error messages', async () => { - http.put.mockReturnValueOnce(Promise.reject('error')); - mount({ activeQuery: 'some query' }); - + itShowsServerErrorAsFlashMessage(http.put, () => { CurationLogic.actions.convertToManual(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); @@ -336,14 +332,9 @@ describe('CurationLogic', () => { expect(navigateToUrl).toHaveBeenCalledWith('/engines/some-engine/curations'); }); - it('flashes any errors', async () => { - http.delete.mockReturnValueOnce(Promise.reject('error')); + itShowsServerErrorAsFlashMessage(http.delete, () => { mount({}, { curationId: 'cur-404' }); - CurationLogic.actions.deleteCuration(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_logic.test.ts index 44ff66e5f46eb..71d8b65f1a639 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curations_logic.test.ts @@ -17,6 +17,8 @@ import { nextTick } from '@kbn/test/jest'; import { DEFAULT_META } from '../../../shared/constants'; +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; + import { CurationsLogic } from './'; describe('CurationsLogic', () => { @@ -130,14 +132,9 @@ describe('CurationsLogic', () => { ); }); - it('handles errors', async () => { - http.get.mockReturnValueOnce(Promise.reject('error')); + itShowsServerErrorAsFlashMessage(http.get, () => { mount(); - CurationsLogic.actions.loadCurations(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion_logic.test.ts index 171c774d8add2..01d8107067e18 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion_logic.test.ts @@ -16,6 +16,7 @@ import '../../../../__mocks__/engine_logic.mock'; import { nextTick } from '@kbn/test/jest'; +import { itShowsServerErrorAsFlashMessage } from '../../../../../test_helpers'; import { HydratedCurationSuggestion } from '../../types'; import { CurationSuggestionLogic } from './curation_suggestion_logic'; @@ -180,20 +181,6 @@ describe('CurationSuggestionLogic', () => { }); }; - const itHandlesErrors = (httpMethod: any, callback: () => void) => { - it('handles errors', async () => { - httpMethod.mockReturnValueOnce(Promise.reject('error')); - mountLogic({ - suggestion, - }); - - callback(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); - }); - }; - beforeEach(() => { jest.clearAllMocks(); }); @@ -271,7 +258,7 @@ describe('CurationSuggestionLogic', () => { expect(navigateToUrl).toHaveBeenCalledWith('/engines/some-engine/curations'); }); - itHandlesErrors(http.get, () => { + itShowsServerErrorAsFlashMessage(http.get, () => { CurationSuggestionLogic.actions.loadSuggestion(); }); }); @@ -350,7 +337,8 @@ describe('CurationSuggestionLogic', () => { }); }); - itHandlesErrors(http.put, () => { + itShowsServerErrorAsFlashMessage(http.put, () => { + jest.spyOn(global, 'confirm').mockReturnValueOnce(true); CurationSuggestionLogic.actions.acceptSuggestion(); }); @@ -433,7 +421,8 @@ describe('CurationSuggestionLogic', () => { }); }); - itHandlesErrors(http.put, () => { + itShowsServerErrorAsFlashMessage(http.put, () => { + jest.spyOn(global, 'confirm').mockReturnValueOnce(true); CurationSuggestionLogic.actions.acceptAndAutomateSuggestion(); }); @@ -478,7 +467,7 @@ describe('CurationSuggestionLogic', () => { expect(navigateToUrl).toHaveBeenCalledWith('/engines/some-engine/curations'); }); - itHandlesErrors(http.put, () => { + itShowsServerErrorAsFlashMessage(http.put, () => { CurationSuggestionLogic.actions.rejectSuggestion(); }); @@ -523,7 +512,7 @@ describe('CurationSuggestionLogic', () => { expect(navigateToUrl).toHaveBeenCalledWith('/engines/some-engine/curations'); }); - itHandlesErrors(http.put, () => { + itShowsServerErrorAsFlashMessage(http.put, () => { CurationSuggestionLogic.actions.rejectAndDisableSuggestion(); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/ignored_queries_panel/ignored_queries_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/ignored_queries_panel/ignored_queries_logic.test.ts index 8c2545fad651a..af9f876820790 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/ignored_queries_panel/ignored_queries_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/ignored_queries_panel/ignored_queries_logic.test.ts @@ -11,13 +11,13 @@ import { mockHttpValues, } from '../../../../../../../__mocks__/kea_logic'; import '../../../../../../__mocks__/engine_logic.mock'; +import { DEFAULT_META } from '../../../../../../../shared/constants'; +import { itShowsServerErrorAsFlashMessage } from '../../../../../../../test_helpers'; // I don't know why eslint is saying this line is out of order // eslint-disable-next-line import/order import { nextTick } from '@kbn/test/jest'; -import { DEFAULT_META } from '../../../../../../../shared/constants'; - import { IgnoredQueriesLogic } from './ignored_queries_logic'; const DEFAULT_VALUES = { @@ -142,13 +142,9 @@ describe('IgnoredQueriesLogic', () => { ); }); - it('handles errors', async () => { - http.post.mockReturnValueOnce(Promise.reject('error')); - + itShowsServerErrorAsFlashMessage(http.post, () => { + mount(); IgnoredQueriesLogic.actions.loadIgnoredQueries(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); @@ -185,13 +181,9 @@ describe('IgnoredQueriesLogic', () => { expect(flashSuccessToast).toHaveBeenCalledWith(expect.any(String)); }); - it('handles errors', async () => { - http.put.mockReturnValueOnce(Promise.reject('error')); - + itShowsServerErrorAsFlashMessage(http.put, () => { + mount(); IgnoredQueriesLogic.actions.allowIgnoredQuery('test query'); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); it('handles inline errors', async () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings_logic.test.ts index 0d09f2d28f396..e9643f92f2f71 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_settings/curations_settings_logic.test.ts @@ -5,11 +5,7 @@ * 2.0. */ -import { - LogicMounter, - mockHttpValues, - mockFlashMessageHelpers, -} from '../../../../../__mocks__/kea_logic'; +import { LogicMounter, mockHttpValues } from '../../../../../__mocks__/kea_logic'; import '../../../../__mocks__/engine_logic.mock'; jest.mock('../../curations_logic', () => ({ @@ -24,6 +20,7 @@ jest.mock('../../curations_logic', () => ({ import { nextTick } from '@kbn/test/jest'; import { CurationsLogic } from '../..'; +import { itShowsServerErrorAsFlashMessage } from '../../../../../test_helpers'; import { EngineLogic } from '../../../engine'; import { CurationsSettingsLogic } from './curations_settings_logic'; @@ -39,7 +36,6 @@ const DEFAULT_VALUES = { describe('CurationsSettingsLogic', () => { const { mount } = new LogicMounter(CurationsSettingsLogic); const { http } = mockHttpValues; - const { flashAPIErrors } = mockFlashMessageHelpers; beforeEach(() => { jest.clearAllMocks(); @@ -105,14 +101,8 @@ describe('CurationsSettingsLogic', () => { }); }); - it('presents any API errors to the user', async () => { - http.get.mockReturnValueOnce(Promise.reject('error')); - mount(); - + itShowsServerErrorAsFlashMessage(http.get, () => { CurationsSettingsLogic.actions.loadCurationsSettings(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); @@ -223,14 +213,8 @@ describe('CurationsSettingsLogic', () => { expect(CurationsLogic.actions.loadCurations).toHaveBeenCalled(); }); - it('presents any API errors to the user', async () => { - http.put.mockReturnValueOnce(Promise.reject('error')); - mount(); - + itShowsServerErrorAsFlashMessage(http.put, () => { CurationsSettingsLogic.actions.updateCurationsSetting({}); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts index 5705e5ae2ee98..848a85f23c2cb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.test.ts @@ -17,6 +17,8 @@ import { nextTick } from '@kbn/test/jest'; import { InternalSchemaType } from '../../../shared/schema/types'; +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; + import { DocumentDetailLogic } from './document_detail_logic'; describe('DocumentDetailLogic', () => { @@ -117,14 +119,9 @@ describe('DocumentDetailLogic', () => { await nextTick(); }); - it('handles errors', async () => { + itShowsServerErrorAsFlashMessage(http.delete, () => { mount(); - http.delete.mockReturnValue(Promise.reject('An error occured')); - DocumentDetailLogic.actions.deleteDocument('1'); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('An error occured'); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.test.ts index e742fce0532ba..142aab98a643b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_logic.test.ts @@ -5,11 +5,7 @@ * 2.0. */ -import { - LogicMounter, - mockHttpValues, - mockFlashMessageHelpers, -} from '../../../__mocks__/kea_logic'; +import { LogicMounter, mockHttpValues } from '../../../__mocks__/kea_logic'; jest.mock('../engine', () => ({ EngineLogic: { values: { engineName: 'some-engine' } }, @@ -17,12 +13,13 @@ jest.mock('../engine', () => ({ import { nextTick } from '@kbn/test/jest'; +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; + import { EngineOverviewLogic } from './'; describe('EngineOverviewLogic', () => { const { mount } = new LogicMounter(EngineOverviewLogic); const { http } = mockHttpValues; - const { flashAPIErrors } = mockFlashMessageHelpers; const mockEngineMetrics = { documentCount: 10, @@ -83,14 +80,9 @@ describe('EngineOverviewLogic', () => { ); }); - it('handles errors', async () => { + itShowsServerErrorAsFlashMessage(http.get, () => { mount(); - http.get.mockReturnValue(Promise.reject('An error occurred')); - EngineOverviewLogic.actions.loadOverviewMetrics(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('An error occurred'); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_logic.test.ts index f3dc8a378a7d3..d0a227c8c6fbe 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_logic.test.ts @@ -15,6 +15,7 @@ import { nextTick } from '@kbn/test/jest'; import { DEFAULT_META } from '../../../shared/constants'; +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; import { EngineDetails, EngineTypes } from '../engine/types'; import { EnginesLogic } from './'; @@ -171,14 +172,9 @@ describe('EnginesLogic', () => { expect(EnginesLogic.actions.onEnginesLoad).toHaveBeenCalledWith(MOCK_ENGINES_API_RESPONSE); }); - it('handles errors', async () => { - http.get.mockReturnValueOnce(Promise.reject('error')); + itShowsServerErrorAsFlashMessage(http.get, () => { mount(); - EnginesLogic.actions.loadEngines(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); @@ -203,14 +199,9 @@ describe('EnginesLogic', () => { ); }); - it('handles errors', async () => { - http.get.mockReturnValueOnce(Promise.reject('error')); + itShowsServerErrorAsFlashMessage(http.get, () => { mount(); - EnginesLogic.actions.loadMetaEngines(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts index 193c5dbe8ac24..6ac1c27a27959 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning_logic.test.ts @@ -14,6 +14,8 @@ import { mockEngineValues, mockEngineActions } from '../../__mocks__'; import { nextTick } from '@kbn/test/jest'; +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; + import { Boost, BoostOperation, BoostType, FunctionalBoostFunction } from './types'; import { RelevanceTuningLogic } from './'; @@ -319,14 +321,9 @@ describe('RelevanceTuningLogic', () => { }); }); - it('handles errors', async () => { + itShowsServerErrorAsFlashMessage(http.get, () => { mount(); - http.get.mockReturnValueOnce(Promise.reject('error')); - RelevanceTuningLogic.actions.initializeRelevanceTuning(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.test.ts index 94d5e84c67f6d..92cb2346e0a26 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result_settings/result_settings_logic.test.ts @@ -5,11 +5,7 @@ * 2.0. */ -import { - LogicMounter, - mockFlashMessageHelpers, - mockHttpValues, -} from '../../../__mocks__/kea_logic'; +import { LogicMounter, mockHttpValues } from '../../../__mocks__/kea_logic'; import { mockEngineValues } from '../../__mocks__'; import { omit } from 'lodash'; @@ -18,6 +14,8 @@ import { nextTick } from '@kbn/test/jest'; import { Schema, SchemaConflicts, SchemaType } from '../../../shared/schema/types'; +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; + import { ServerFieldResultSettingObject } from './types'; import { ResultSettingsLogic } from '.'; @@ -508,7 +506,6 @@ describe('ResultSettingsLogic', () => { describe('listeners', () => { const { http } = mockHttpValues; - const { flashAPIErrors } = mockFlashMessageHelpers; let confirmSpy: jest.SpyInstance; beforeAll(() => { @@ -844,14 +841,9 @@ describe('ResultSettingsLogic', () => { ); }); - it('handles errors', async () => { + itShowsServerErrorAsFlashMessage(http.get, () => { mount(); - http.get.mockReturnValueOnce(Promise.reject('error')); - ResultSettingsLogic.actions.initializeResultSettingsData(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); @@ -923,14 +915,9 @@ describe('ResultSettingsLogic', () => { ); }); - it('handles errors', async () => { + itShowsServerErrorAsFlashMessage(http.put, () => { mount(); - http.put.mockReturnValueOnce(Promise.reject('error')); - ResultSettingsLogic.actions.saveResultSettings(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); it('does nothing if the user does not confirm', async () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts index 14d97c7dd3f4d..b35865f279817 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/role_mappings_logic.test.ts @@ -23,13 +23,15 @@ import { } from '../../../shared/role_mapping/__mocks__/roles'; import { ANY_AUTH_PROVIDER } from '../../../shared/role_mapping/constants'; +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; + import { RoleMappingsLogic } from './role_mappings_logic'; const emptyUser = { username: '', email: '' }; describe('RoleMappingsLogic', () => { const { http } = mockHttpValues; - const { clearFlashMessages, flashAPIErrors, flashSuccessToast } = mockFlashMessageHelpers; + const { clearFlashMessages, flashSuccessToast } = mockFlashMessageHelpers; const { mount } = new LogicMounter(RoleMappingsLogic); const DEFAULT_VALUES = { attributes: [], @@ -391,12 +393,8 @@ describe('RoleMappingsLogic', () => { expect(setRoleMappingsSpy).toHaveBeenCalledWith(mappingsServerProps); }); - it('handles error', async () => { - http.post.mockReturnValue(Promise.reject('this is an error')); + itShowsServerErrorAsFlashMessage(http.post, () => { RoleMappingsLogic.actions.enableRoleBasedAccess(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); @@ -411,12 +409,8 @@ describe('RoleMappingsLogic', () => { expect(setRoleMappingsDataSpy).toHaveBeenCalledWith(mappingsServerProps); }); - it('handles error', async () => { - http.get.mockReturnValue(Promise.reject('this is an error')); + itShowsServerErrorAsFlashMessage(http.get, () => { RoleMappingsLogic.actions.initializeRoleMappings(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); it('resets roleMapping state', () => { @@ -691,13 +685,9 @@ describe('RoleMappingsLogic', () => { expect(flashSuccessToast).toHaveBeenCalled(); }); - it('handles error', async () => { + itShowsServerErrorAsFlashMessage(http.delete, () => { mount(mappingsServerProps); - http.delete.mockReturnValue(Promise.reject('this is an error')); RoleMappingsLogic.actions.handleDeleteMapping(roleMappingId); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_base_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_base_logic.test.ts index c5611420442c8..5b40b362bc665 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_base_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/schema/schema_base_logic.test.ts @@ -5,23 +5,20 @@ * 2.0. */ -import { - LogicMounter, - mockFlashMessageHelpers, - mockHttpValues, -} from '../../../__mocks__/kea_logic'; +import { LogicMounter, mockHttpValues } from '../../../__mocks__/kea_logic'; import '../../__mocks__/engine_logic.mock'; import { nextTick } from '@kbn/test/jest'; import { SchemaType } from '../../../shared/schema/types'; +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; + import { SchemaBaseLogic } from './schema_base_logic'; describe('SchemaBaseLogic', () => { const { mount } = new LogicMounter(SchemaBaseLogic); const { http } = mockHttpValues; - const { flashAPIErrors } = mockFlashMessageHelpers; const MOCK_SCHEMA = { some_text_field: SchemaType.Text, @@ -99,14 +96,9 @@ describe('SchemaBaseLogic', () => { expect(SchemaBaseLogic.actions.onSchemaLoad).toHaveBeenCalledWith(MOCK_RESPONSE); }); - it('handles errors', async () => { - http.get.mockReturnValueOnce(Promise.reject('error')); + itShowsServerErrorAsFlashMessage(http.get, () => { mount(); - SchemaBaseLogic.actions.loadSchema(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui_logic.test.ts index 33144d4188ec1..49444fbd0c5c5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/search_ui/search_ui_logic.test.ts @@ -14,6 +14,8 @@ import { mockEngineValues } from '../../__mocks__'; import { nextTick } from '@kbn/test/jest'; +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; + import { ActiveField } from './types'; import { SearchUILogic } from './'; @@ -21,7 +23,7 @@ import { SearchUILogic } from './'; describe('SearchUILogic', () => { const { mount } = new LogicMounter(SearchUILogic); const { http } = mockHttpValues; - const { flashAPIErrors, setErrorMessage } = mockFlashMessageHelpers; + const { setErrorMessage } = mockFlashMessageHelpers; const DEFAULT_VALUES = { dataLoading: true, @@ -182,14 +184,9 @@ describe('SearchUILogic', () => { ); }); - it('handles errors', async () => { - http.get.mockReturnValueOnce(Promise.reject('error')); + itShowsServerErrorAsFlashMessage(http.get, () => { mount(); - SearchUILogic.actions.loadFieldData(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/synonyms_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/synonyms_logic.test.ts index 0ff84ad4cb9cb..7376bc11df79e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/synonyms_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/synonyms_logic.test.ts @@ -14,6 +14,8 @@ import '../../__mocks__/engine_logic.mock'; import { nextTick } from '@kbn/test/jest'; +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; + import { SYNONYMS_PAGE_META } from './constants'; import { SynonymsLogic } from './'; @@ -146,14 +148,9 @@ describe('SynonymsLogic', () => { expect(SynonymsLogic.actions.onSynonymsLoad).toHaveBeenCalledWith(MOCK_SYNONYMS_RESPONSE); }); - it('handles errors', async () => { - http.get.mockReturnValueOnce(Promise.reject('error')); + itShowsServerErrorAsFlashMessage(http.get, () => { mount(); - SynonymsLogic.actions.loadSynonyms(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/recursively_fetch_engines/index.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/recursively_fetch_engines/index.test.ts index 555a880d544f4..c03ca8267993a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/utils/recursively_fetch_engines/index.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/utils/recursively_fetch_engines/index.test.ts @@ -5,15 +5,16 @@ * 2.0. */ -import { mockFlashMessageHelpers, mockHttpValues } from '../../../__mocks__/kea_logic'; +import { mockHttpValues } from '../../../__mocks__/kea_logic'; import { nextTick } from '@kbn/test/jest'; +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; + import { recursivelyFetchEngines } from './'; describe('recursivelyFetchEngines', () => { const { http } = mockHttpValues; - const { flashAPIErrors } = mockFlashMessageHelpers; const MOCK_PAGE_1 = { meta: { @@ -100,12 +101,7 @@ describe('recursivelyFetchEngines', () => { }); }); - it('handles errors', async () => { - http.get.mockReturnValueOnce(Promise.reject('error')); - + itShowsServerErrorAsFlashMessage(http.get, () => { recursivelyFetchEngines({ endpoint: '/error', onComplete: MOCK_CALLBACK }); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/generic_endpoint_inline_editable_table/generic_endpoint_inline_editable_table_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/tables/generic_endpoint_inline_editable_table/generic_endpoint_inline_editable_table_logic.test.ts index 5cd4b5af8f517..a77892a70d525 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/tables/generic_endpoint_inline_editable_table/generic_endpoint_inline_editable_table_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/generic_endpoint_inline_editable_table/generic_endpoint_inline_editable_table_logic.test.ts @@ -13,6 +13,8 @@ import { import { nextTick } from '@kbn/test/jest'; +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; + import { GenericEndpointInlineEditableTableLogic } from './generic_endpoint_inline_editable_table_logic'; describe('GenericEndpointInlineEditableTableLogic', () => { @@ -119,14 +121,9 @@ describe('GenericEndpointInlineEditableTableLogic', () => { expect(logic.actions.clearLoading).toHaveBeenCalled(); }); - it('handles errors', async () => { - http.post.mockReturnValueOnce(Promise.reject('error')); + itShowsServerErrorAsFlashMessage(http.post, () => { const logic = mountLogic(); - logic.actions.addItem(item, onSuccess); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); @@ -167,14 +164,9 @@ describe('GenericEndpointInlineEditableTableLogic', () => { expect(logic.actions.clearLoading).toHaveBeenCalled(); }); - it('handles errors', async () => { - http.delete.mockReturnValueOnce(Promise.reject('error')); + itShowsServerErrorAsFlashMessage(http.delete, () => { const logic = mountLogic(); - logic.actions.deleteItem(item, onSuccess); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); @@ -221,14 +213,9 @@ describe('GenericEndpointInlineEditableTableLogic', () => { expect(logic.actions.clearLoading).toHaveBeenCalled(); }); - it('handles errors', async () => { - http.put.mockReturnValueOnce(Promise.reject('error')); + itShowsServerErrorAsFlashMessage(http.put, () => { const logic = mountLogic(); - logic.actions.updateItem(item, onSuccess); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('error'); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/test_helpers/error_handling.ts b/x-pack/plugins/enterprise_search/public/applications/test_helpers/error_handling.ts new file mode 100644 index 0000000000000..4f1f4a40aa503 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/test_helpers/error_handling.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mockFlashMessageHelpers } from '../__mocks__/kea_logic'; + +import { nextTick } from '@kbn/test/jest'; +import { HttpHandler } from 'src/core/public'; + +export const itShowsServerErrorAsFlashMessage = (httpMethod: HttpHandler, callback: () => void) => { + const { flashAPIErrors } = mockFlashMessageHelpers; + it('shows any server errors as flash messages', async () => { + (httpMethod as jest.Mock).mockReturnValueOnce(Promise.reject('error')); + callback(); + await nextTick(); + + expect(flashAPIErrors).toHaveBeenCalledWith('error'); + }); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/test_helpers/index.ts b/x-pack/plugins/enterprise_search/public/applications/test_helpers/index.ts index 35836d5526615..b0705dd7e134b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/test_helpers/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/test_helpers/index.ts @@ -21,3 +21,4 @@ export { // Misc export { expectedAsyncError } from './expected_async_error'; +export { itShowsServerErrorAsFlashMessage } from './error_handling'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts index 5ff3964b8f83a..da4e9cad9e276 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.test.ts @@ -15,6 +15,8 @@ import { sourceConfigData } from '../../../../__mocks__/content_sources.mock'; import { nextTick } from '@kbn/test/jest'; +import { itShowsServerErrorAsFlashMessage } from '../../../../../test_helpers'; + jest.mock('../../../../app_logic', () => ({ AppLogic: { values: { isOrganization: true } }, })); @@ -413,13 +415,8 @@ describe('AddSourceLogic', () => { expect(setSourceConfigDataSpy).toHaveBeenCalledWith(sourceConfigData); }); - it('handles error', async () => { - http.get.mockReturnValue(Promise.reject('this is an error')); - + itShowsServerErrorAsFlashMessage(http.get, () => { AddSourceLogic.actions.getSourceConfigData('github'); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); @@ -474,13 +471,8 @@ describe('AddSourceLogic', () => { ); }); - it('handles error', async () => { - http.get.mockReturnValue(Promise.reject('this is an error')); - + itShowsServerErrorAsFlashMessage(http.get, () => { AddSourceLogic.actions.getSourceConnectData('github', successCallback); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); @@ -506,13 +498,8 @@ describe('AddSourceLogic', () => { expect(setSourceConnectDataSpy).toHaveBeenCalledWith(sourceConnectData); }); - it('handles error', async () => { - http.get.mockReturnValue(Promise.reject('this is an error')); - + itShowsServerErrorAsFlashMessage(http.get, () => { AddSourceLogic.actions.getSourceReConnectData('github'); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); @@ -532,13 +519,8 @@ describe('AddSourceLogic', () => { expect(setPreContentSourceConfigDataSpy).toHaveBeenCalledWith(config); }); - it('handles error', async () => { - http.get.mockReturnValue(Promise.reject('this is an error')); - + itShowsServerErrorAsFlashMessage(http.get, () => { AddSourceLogic.actions.getPreContentSourceConfigData(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); @@ -601,13 +583,8 @@ describe('AddSourceLogic', () => { ); }); - it('handles error', async () => { - http.put.mockReturnValue(Promise.reject('this is an error')); - + itShowsServerErrorAsFlashMessage(http.put, () => { AddSourceLogic.actions.saveSourceConfig(true); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.test.ts index 62e305f72365d..81a97c2d19e16 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.test.ts @@ -15,6 +15,8 @@ import { exampleResult } from '../../../../__mocks__/content_sources.mock'; import { nextTick } from '@kbn/test/jest'; +import { itShowsServerErrorAsFlashMessage } from '../../../../../test_helpers'; + const contentSource = { id: 'source123' }; jest.mock('../../source_logic', () => ({ SourceLogic: { values: { contentSource } }, @@ -31,7 +33,7 @@ import { DisplaySettingsLogic, defaultSearchResultConfig } from './display_setti describe('DisplaySettingsLogic', () => { const { http } = mockHttpValues; const { navigateToUrl } = mockKibanaValues; - const { clearFlashMessages, flashAPIErrors, flashSuccessToast } = mockFlashMessageHelpers; + const { clearFlashMessages, flashSuccessToast } = mockFlashMessageHelpers; const { mount } = new LogicMounter(DisplaySettingsLogic); const { searchResultConfig, exampleDocuments } = exampleResult; @@ -406,12 +408,8 @@ describe('DisplaySettingsLogic', () => { }); }); - it('handles error', async () => { - http.get.mockReturnValue(Promise.reject('this is an error')); + itShowsServerErrorAsFlashMessage(http.get, () => { DisplaySettingsLogic.actions.initializeDisplaySettings(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); @@ -434,12 +432,8 @@ describe('DisplaySettingsLogic', () => { }); }); - it('handles error', async () => { - http.post.mockReturnValue(Promise.reject('this is an error')); + itShowsServerErrorAsFlashMessage(http.post, () => { DisplaySettingsLogic.actions.setServerData(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.test.ts index af9d85237335c..d284f5c741eb3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.test.ts @@ -29,6 +29,7 @@ Object.defineProperty(global.window, 'scrollTo', { value: spyScrollTo }); import { ADD, UPDATE } from '../../../../../shared/constants/operations'; import { defaultErrorMessage } from '../../../../../shared/flash_messages/handle_api_errors'; import { SchemaType } from '../../../../../shared/schema/types'; +import { itShowsServerErrorAsFlashMessage } from '../../../../../test_helpers'; import { AppLogic } from '../../../../app_logic'; import { @@ -40,8 +41,7 @@ import { SchemaLogic, dataTypeOptions } from './schema_logic'; describe('SchemaLogic', () => { const { http } = mockHttpValues; - const { clearFlashMessages, flashAPIErrors, flashSuccessToast, setErrorMessage } = - mockFlashMessageHelpers; + const { clearFlashMessages, flashSuccessToast, setErrorMessage } = mockFlashMessageHelpers; const { mount } = new LogicMounter(SchemaLogic); const defaultValues = { @@ -224,12 +224,8 @@ describe('SchemaLogic', () => { expect(onInitializeSchemaSpy).toHaveBeenCalledWith(serverResponse); }); - it('handles error', async () => { - http.get.mockReturnValue(Promise.reject('this is an error')); + itShowsServerErrorAsFlashMessage(http.get, () => { SchemaLogic.actions.initializeSchema(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); @@ -447,12 +443,8 @@ describe('SchemaLogic', () => { expect(onSchemaSetSuccessSpy).toHaveBeenCalledWith(serverResponse); }); - it('handles error', async () => { - http.post.mockReturnValue(Promise.reject('this is an error')); + itShowsServerErrorAsFlashMessage(http.post, () => { SchemaLogic.actions.setServerField(schema, UPDATE); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_logic.test.ts index 25fb256e85f01..0ccfd6aa63ae4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_logic.test.ts @@ -15,7 +15,7 @@ import { fullContentSources } from '../../../../__mocks__/content_sources.mock'; import { nextTick } from '@kbn/test/jest'; -import { expectedAsyncError } from '../../../../../test_helpers'; +import { itShowsServerErrorAsFlashMessage } from '../../../../../test_helpers'; jest.mock('../../source_logic', () => ({ SourceLogic: { actions: { setContentSource: jest.fn() } }, @@ -34,7 +34,7 @@ import { describe('SynchronizationLogic', () => { const { http } = mockHttpValues; - const { flashAPIErrors, flashSuccessToast } = mockFlashMessageHelpers; + const { flashSuccessToast } = mockFlashMessageHelpers; const { navigateToUrl } = mockKibanaValues; const { mount } = new LogicMounter(SynchronizationLogic); const contentSource = fullContentSources[0]; @@ -328,19 +328,8 @@ describe('SynchronizationLogic', () => { expect(flashSuccessToast).toHaveBeenCalledWith('Source synchronization settings updated.'); }); - it('handles error', async () => { - const error = { - response: { - error: 'this is an error', - status: 400, - }, - }; - const promise = Promise.reject(error); - http.patch.mockReturnValue(promise); + itShowsServerErrorAsFlashMessage(http.patch, () => { SynchronizationLogic.actions.updateServerSettings(body); - await expectedAsyncError(promise); - - expect(flashAPIErrors).toHaveBeenCalledWith(error); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts index fb88360de5df0..be288ea208858 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.test.ts @@ -20,6 +20,7 @@ import { expectedAsyncError } from '../../../test_helpers'; jest.mock('../../app_logic', () => ({ AppLogic: { values: { isOrganization: true } }, })); +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; import { AppLogic } from '../../app_logic'; import { SourceLogic } from './source_logic'; @@ -235,19 +236,8 @@ describe('SourceLogic', () => { expect(onUpdateSummarySpy).toHaveBeenCalledWith(contentSource.summary); }); - it('handles error', async () => { - const error = { - response: { - error: 'this is an error', - status: 400, - }, - }; - const promise = Promise.reject(error); - http.get.mockReturnValue(promise); + itShowsServerErrorAsFlashMessage(http.get, () => { SourceLogic.actions.initializeFederatedSummary(contentSource.id); - await expectedAsyncError(promise); - - expect(flashAPIErrors).toHaveBeenCalledWith(error); }); }); @@ -295,20 +285,8 @@ describe('SourceLogic', () => { expect(actions.setSearchResults).toHaveBeenCalledWith(searchServerResponse); }); - it('handles error', async () => { - const error = { - response: { - error: 'this is an error', - status: 400, - }, - }; - const promise = Promise.reject(error); - http.post.mockReturnValue(promise); - - await searchContentSourceDocuments({ sourceId: contentSource.id }, mockBreakpoint); - await expectedAsyncError(promise); - - expect(flashAPIErrors).toHaveBeenCalledWith(error); + itShowsServerErrorAsFlashMessage(http.post, () => { + searchContentSourceDocuments({ sourceId: contentSource.id }, mockBreakpoint); }); }); @@ -367,19 +345,8 @@ describe('SourceLogic', () => { expect(onUpdateSourceNameSpy).toHaveBeenCalledWith(contentSource.name); }); - it('handles error', async () => { - const error = { - response: { - error: 'this is an error', - status: 400, - }, - }; - const promise = Promise.reject(error); - http.patch.mockReturnValue(promise); + itShowsServerErrorAsFlashMessage(http.patch, () => { SourceLogic.actions.updateContentSource(contentSource.id, contentSource); - await expectedAsyncError(promise); - - expect(flashAPIErrors).toHaveBeenCalledWith(error); }); }); @@ -413,19 +380,8 @@ describe('SourceLogic', () => { expect(setButtonNotLoadingSpy).toHaveBeenCalled(); }); - it('handles error', async () => { - const error = { - response: { - error: 'this is an error', - status: 400, - }, - }; - const promise = Promise.reject(error); - http.delete.mockReturnValue(promise); + itShowsServerErrorAsFlashMessage(http.delete, () => { SourceLogic.actions.removeContentSource(contentSource.id); - await expectedAsyncError(promise); - - expect(flashAPIErrors).toHaveBeenCalledWith(error); }); }); @@ -441,19 +397,8 @@ describe('SourceLogic', () => { expect(initializeSourceSpy).toHaveBeenCalledWith(contentSource.id); }); - it('handles error', async () => { - const error = { - response: { - error: 'this is an error', - status: 400, - }, - }; - const promise = Promise.reject(error); - http.post.mockReturnValue(promise); + itShowsServerErrorAsFlashMessage(http.post, () => { SourceLogic.actions.initializeSourceSynchronization(contentSource.id); - await expectedAsyncError(promise); - - expect(flashAPIErrors).toHaveBeenCalledWith(error); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.test.ts index 8518485c98b24..f7e41f6512017 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.test.ts @@ -12,11 +12,10 @@ import { } from '../../../__mocks__/kea_logic'; import { configuredSources, contentSources } from '../../__mocks__/content_sources.mock'; -import { expectedAsyncError } from '../../../test_helpers'; - jest.mock('../../app_logic', () => ({ AppLogic: { values: { isOrganization: true } }, })); +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; import { AppLogic } from '../../app_logic'; import { SourcesLogic, fetchSourceStatuses, POLLING_INTERVAL } from './sources_logic'; @@ -185,19 +184,8 @@ describe('SourcesLogic', () => { expect(http.get).toHaveBeenCalledWith('/internal/workplace_search/account/sources'); }); - it('handles error', async () => { - const error = { - response: { - error: 'this is an error', - status: 400, - }, - }; - const promise = Promise.reject(error); - http.get.mockReturnValue(promise); + itShowsServerErrorAsFlashMessage(http.get, () => { SourcesLogic.actions.initializeSources(); - await expectedAsyncError(promise); - - expect(flashAPIErrors).toHaveBeenCalledWith(error); }); it('handles early logic unmount gracefully in org context', async () => { @@ -259,19 +247,8 @@ describe('SourcesLogic', () => { ); }); - it('handles error', async () => { - const error = { - response: { - error: 'this is an error', - status: 400, - }, - }; - const promise = Promise.reject(error); - http.put.mockReturnValue(promise); + itShowsServerErrorAsFlashMessage(http.put, () => { SourcesLogic.actions.setSourceSearchability(id, true); - await expectedAsyncError(promise); - - expect(flashAPIErrors).toHaveBeenCalledWith(error); }); }); @@ -367,19 +344,8 @@ describe('SourcesLogic', () => { expect(http.get).toHaveBeenCalledWith('/internal/workplace_search/account/sources/status'); }); - it('handles error', async () => { - const error = { - response: { - error: 'this is an error', - status: 400, - }, - }; - const promise = Promise.reject(error); - http.get.mockReturnValue(promise); + itShowsServerErrorAsFlashMessage(http.get, () => { fetchSourceStatuses(true, mockBreakpoint); - await expectedAsyncError(promise); - - expect(flashAPIErrors).toHaveBeenCalledWith(error); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.test.ts index 6f811ce364290..3048dcedef26f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.test.ts @@ -16,6 +16,7 @@ import { mockGroupValues } from './__mocks__/group_logic.mock'; import { nextTick } from '@kbn/test/jest'; +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; import { GROUPS_PATH } from '../../routes'; import { GroupLogic } from './group_logic'; @@ -24,8 +25,7 @@ describe('GroupLogic', () => { const { mount } = new LogicMounter(GroupLogic); const { http } = mockHttpValues; const { navigateToUrl } = mockKibanaValues; - const { clearFlashMessages, flashAPIErrors, flashSuccessToast, setQueuedErrorMessage } = - mockFlashMessageHelpers; + const { clearFlashMessages, flashSuccessToast, setQueuedErrorMessage } = mockFlashMessageHelpers; const group = groups[0]; const sourceIds = ['123', '124']; @@ -222,13 +222,8 @@ describe('GroupLogic', () => { expect(flashSuccessToast).toHaveBeenCalledWith('Group "group" was successfully deleted.'); }); - it('handles error', async () => { - http.delete.mockReturnValue(Promise.reject('this is an error')); - + itShowsServerErrorAsFlashMessage(http.delete, () => { GroupLogic.actions.deleteGroup(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); @@ -253,13 +248,8 @@ describe('GroupLogic', () => { ); }); - it('handles error', async () => { - http.put.mockReturnValue(Promise.reject('this is an error')); - + itShowsServerErrorAsFlashMessage(http.put, () => { GroupLogic.actions.updateGroupName(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); @@ -284,13 +274,8 @@ describe('GroupLogic', () => { ); }); - it('handles error', async () => { - http.post.mockReturnValue(Promise.reject('this is an error')); - + itShowsServerErrorAsFlashMessage(http.post, () => { GroupLogic.actions.saveGroupSources(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); @@ -322,13 +307,8 @@ describe('GroupLogic', () => { expect(onGroupPrioritiesChangedSpy).toHaveBeenCalledWith(group); }); - it('handles error', async () => { - http.put.mockReturnValue(Promise.reject('this is an error')); - + itShowsServerErrorAsFlashMessage(http.put, () => { GroupLogic.actions.saveGroupSourcePrioritization(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.test.ts index c8b725f7131a6..15951a9f8b9ca 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.test.ts @@ -20,6 +20,8 @@ import { nextTick } from '@kbn/test/jest'; import { JSON_HEADER as headers } from '../../../../../common/constants'; import { DEFAULT_META } from '../../../shared/constants'; +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; + import { GroupsLogic } from './groups_logic'; // We need to mock out the debounced functionality @@ -227,13 +229,8 @@ describe('GroupsLogic', () => { expect(onInitializeGroupsSpy).toHaveBeenCalledWith(groupsResponse); }); - it('handles error', async () => { - http.get.mockReturnValue(Promise.reject('this is an error')); - + itShowsServerErrorAsFlashMessage(http.get, () => { GroupsLogic.actions.initializeGroups(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); @@ -310,13 +307,8 @@ describe('GroupsLogic', () => { expect(setGroupUsersSpy).toHaveBeenCalledWith(users); }); - it('handles error', async () => { - http.get.mockReturnValue(Promise.reject('this is an error')); - + itShowsServerErrorAsFlashMessage(http.get, () => { GroupsLogic.actions.fetchGroupUsers('123'); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); @@ -336,13 +328,8 @@ describe('GroupsLogic', () => { expect(setNewGroupSpy).toHaveBeenCalledWith(groups[0]); }); - it('handles error', async () => { - http.post.mockReturnValue(Promise.reject('this is an error')); - + itShowsServerErrorAsFlashMessage(http.post, () => { GroupsLogic.actions.saveNewGroup(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts index 3f5a63275f05d..b70039636bba0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/role_mappings_logic.test.ts @@ -23,6 +23,8 @@ import { } from '../../../shared/role_mapping/__mocks__/roles'; import { ANY_AUTH_PROVIDER } from '../../../shared/role_mapping/constants'; +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; + import { RoleMappingsLogic } from './role_mappings_logic'; const emptyUser = { username: '', email: '' }; @@ -349,12 +351,8 @@ describe('RoleMappingsLogic', () => { expect(setRoleMappingsSpy).toHaveBeenCalledWith(mappingsServerProps); }); - it('handles error', async () => { - http.post.mockReturnValue(Promise.reject('this is an error')); + itShowsServerErrorAsFlashMessage(http.post, () => { RoleMappingsLogic.actions.enableRoleBasedAccess(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); @@ -369,12 +367,8 @@ describe('RoleMappingsLogic', () => { expect(setRoleMappingsDataSpy).toHaveBeenCalledWith(mappingsServerProps); }); - it('handles error', async () => { - http.get.mockReturnValue(Promise.reject('this is an error')); + itShowsServerErrorAsFlashMessage(http.get, () => { RoleMappingsLogic.actions.initializeRoleMappings(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); it('resets roleMapping state', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security_logic.test.ts index bc45609e9e83d..df9035d57e56b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security_logic.test.ts @@ -5,19 +5,16 @@ * 2.0. */ -import { - LogicMounter, - mockHttpValues, - mockFlashMessageHelpers, -} from '../../../__mocks__/kea_logic'; +import { LogicMounter, mockHttpValues } from '../../../__mocks__/kea_logic'; import { nextTick } from '@kbn/test/jest'; +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; + import { SecurityLogic } from './security_logic'; describe('SecurityLogic', () => { const { http } = mockHttpValues; - const { flashAPIErrors } = mockFlashMessageHelpers; const { mount } = new LogicMounter(SecurityLogic); beforeEach(() => { @@ -124,15 +121,8 @@ describe('SecurityLogic', () => { expect(setServerPropsSpy).toHaveBeenCalledWith(serverProps); }); - it('handles error', async () => { - http.get.mockReturnValue(Promise.reject('this is an error')); - + itShowsServerErrorAsFlashMessage(http.get, () => { SecurityLogic.actions.initializeSourceRestrictions(); - try { - await nextTick(); - } catch { - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); - } }); }); @@ -150,15 +140,8 @@ describe('SecurityLogic', () => { ); }); - it('handles error', async () => { - http.patch.mockReturnValue(Promise.reject('this is an error')); - + itShowsServerErrorAsFlashMessage(http.patch, () => { SecurityLogic.actions.saveSourceRestrictions(); - try { - await nextTick(); - } catch { - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); - } }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.test.ts index ebb790b59c1fa..d98c9efe04d8c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.test.ts @@ -15,6 +15,7 @@ import { configuredSources, oauthApplication } from '../../__mocks__/content_sou import { nextTick } from '@kbn/test/jest'; +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; import { ORG_UPDATED_MESSAGE, OAUTH_APP_UPDATED_MESSAGE } from '../../constants'; import { SettingsLogic } from './settings_logic'; @@ -22,7 +23,7 @@ import { SettingsLogic } from './settings_logic'; describe('SettingsLogic', () => { const { http } = mockHttpValues; const { navigateToUrl } = mockKibanaValues; - const { clearFlashMessages, flashAPIErrors, flashSuccessToast } = mockFlashMessageHelpers; + const { clearFlashMessages, flashSuccessToast } = mockFlashMessageHelpers; const { mount } = new LogicMounter(SettingsLogic); const ORG_NAME = 'myOrg'; const defaultValues = { @@ -127,12 +128,8 @@ describe('SettingsLogic', () => { expect(setServerPropsSpy).toHaveBeenCalledWith(configuredSources); }); - it('handles error', async () => { - http.get.mockReturnValue(Promise.reject('this is an error')); + itShowsServerErrorAsFlashMessage(http.get, () => { SettingsLogic.actions.initializeSettings(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); @@ -150,12 +147,8 @@ describe('SettingsLogic', () => { expect(onInitializeConnectorsSpy).toHaveBeenCalledWith(serverProps); }); - it('handles error', async () => { - http.get.mockReturnValue(Promise.reject('this is an error')); + itShowsServerErrorAsFlashMessage(http.get, () => { SettingsLogic.actions.initializeConnectors(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); @@ -176,12 +169,8 @@ describe('SettingsLogic', () => { expect(setUpdatedNameSpy).toHaveBeenCalledWith({ organizationName: NAME }); }); - it('handles error', async () => { - http.put.mockReturnValue(Promise.reject('this is an error')); + itShowsServerErrorAsFlashMessage(http.put, () => { SettingsLogic.actions.updateOrgName(); - - await nextTick(); - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); @@ -205,12 +194,8 @@ describe('SettingsLogic', () => { expect(setIconSpy).toHaveBeenCalledWith(ICON); }); - it('handles error', async () => { - http.put.mockReturnValue(Promise.reject('this is an error')); + itShowsServerErrorAsFlashMessage(http.put, () => { SettingsLogic.actions.updateOrgIcon(); - - await nextTick(); - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); @@ -234,12 +219,8 @@ describe('SettingsLogic', () => { expect(setLogoSpy).toHaveBeenCalledWith(LOGO); }); - it('handles error', async () => { - http.put.mockReturnValue(Promise.reject('this is an error')); + itShowsServerErrorAsFlashMessage(http.put, () => { SettingsLogic.actions.updateOrgLogo(); - - await nextTick(); - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); @@ -291,12 +272,8 @@ describe('SettingsLogic', () => { expect(flashSuccessToast).toHaveBeenCalledWith(OAUTH_APP_UPDATED_MESSAGE); }); - it('handles error', async () => { - http.put.mockReturnValue(Promise.reject('this is an error')); + itShowsServerErrorAsFlashMessage(http.put, () => { SettingsLogic.actions.updateOauthApplication(); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); @@ -313,12 +290,8 @@ describe('SettingsLogic', () => { expect(flashSuccessToast).toHaveBeenCalled(); }); - it('handles error', async () => { - http.delete.mockReturnValue(Promise.reject('this is an error')); + itShowsServerErrorAsFlashMessage(http.delete, () => { SettingsLogic.actions.deleteSourceConfig(SERVICE_TYPE, NAME); - await nextTick(); - - expect(flashAPIErrors).toHaveBeenCalledWith('this is an error'); }); }); From db327da470a1f9c0d2493e3c62ff0c153e302245 Mon Sep 17 00:00:00 2001 From: Michael Dokolin Date: Wed, 10 Nov 2021 23:54:22 +0100 Subject: [PATCH 20/43] [Reporting] Fix timeouts handling in the screenshotting observable handler (#118186) (#118253) --- .../server/lib/screenshots/observable.test.ts | 2 +- .../server/lib/screenshots/observable.ts | 9 +- .../screenshots/observable_handler.test.ts | 2 +- .../lib/screenshots/observable_handler.ts | 132 ++++++++---------- 4 files changed, 63 insertions(+), 82 deletions(-) diff --git a/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts b/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts index 3dc06996f0f04..3071ecb54dc26 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts @@ -353,7 +353,7 @@ describe('Screenshot Observable Pipeline', () => { }, }, ], - "error": [Error: An error occurred when trying to read the page for visualization panel info: Error: Mock error!], + "error": [Error: The "wait for elements" phase encountered an error: Error: An error occurred when trying to read the page for visualization panel info: Error: Mock error!], "screenshots": Array [ Object { "data": Object { diff --git a/x-pack/plugins/reporting/server/lib/screenshots/observable.ts b/x-pack/plugins/reporting/server/lib/screenshots/observable.ts index 48802eb5e5fbe..d400c423c5e04 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/observable.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/observable.ts @@ -58,9 +58,8 @@ export function getScreenshots$( const screen = new ScreenshotObservableHandler(driver, opts, getTimeouts(captureConfig)); return Rx.from(opts.urlsOrUrlLocatorTuples).pipe( - concatMap((urlOrUrlLocatorTuple, index) => { - return Rx.of(1).pipe( - screen.setupPage(index, urlOrUrlLocatorTuple, apmTrans), + concatMap((urlOrUrlLocatorTuple, index) => + screen.setupPage(index, urlOrUrlLocatorTuple, apmTrans).pipe( catchError((err) => { screen.checkPageIsOpen(); // this fails the job if the browser has closed @@ -69,8 +68,8 @@ export function getScreenshots$( }), takeUntil(exit$), screen.getScreenshots() - ); - }), + ) + ), take(opts.urlsOrUrlLocatorTuples.length), toArray() ); diff --git a/x-pack/plugins/reporting/server/lib/screenshots/observable_handler.test.ts b/x-pack/plugins/reporting/server/lib/screenshots/observable_handler.test.ts index 25a8bed370d86..cb0a513992722 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/observable_handler.test.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/observable_handler.test.ts @@ -95,7 +95,7 @@ describe('ScreenshotObservableHandler', () => { const testPipeline = () => test$.toPromise(); await expect(testPipeline).rejects.toMatchInlineSnapshot( - `[Error: The "Test Config" phase took longer than 0.2 seconds. You may need to increase "test.config.value": TimeoutError: Timeout has occurred]` + `[Error: The "Test Config" phase took longer than 0.2 seconds. You may need to increase "test.config.value"]` ); }); diff --git a/x-pack/plugins/reporting/server/lib/screenshots/observable_handler.ts b/x-pack/plugins/reporting/server/lib/screenshots/observable_handler.ts index 87c247273ef04..1db313b091025 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/observable_handler.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/observable_handler.ts @@ -7,7 +7,7 @@ import apm from 'elastic-apm-node'; import * as Rx from 'rxjs'; -import { catchError, mergeMap, timeout } from 'rxjs/operators'; +import { catchError, mergeMap, switchMapTo, timeoutWith } from 'rxjs/operators'; import { numberToDuration } from '../../../common/schema_utils'; import { UrlOrUrlLocatorTuple } from '../../../common/types'; import { HeadlessChromiumDriver } from '../../browsers'; @@ -33,7 +33,6 @@ export class ScreenshotObservableHandler { private conditionalHeaders: ScreenshotObservableOpts['conditionalHeaders']; private layout: ScreenshotObservableOpts['layout']; private logger: ScreenshotObservableOpts['logger']; - private waitErrorRegistered = false; constructor( private readonly driver: HeadlessChromiumDriver, @@ -50,35 +49,27 @@ export class ScreenshotObservableHandler { */ public waitUntil(phase: PhaseInstance) { const { timeoutValue, label, configValue } = phase; - return (source: Rx.Observable) => { - return source.pipe( - timeout(timeoutValue), - catchError((error: string | Error) => { - if (this.waitErrorRegistered) { - throw error; // do not create a stack of errors within the error - } - - this.logger.error(error); - let throwError = new Error(`The "${label}" phase encountered an error: ${error}`); - - if (error instanceof Rx.TimeoutError) { - throwError = new Error( - `The "${label}" phase took longer than` + - ` ${numberToDuration(timeoutValue).asSeconds()} seconds.` + - ` You may need to increase "${configValue}": ${error}` - ); - } - - this.waitErrorRegistered = true; - this.logger.error(throwError); - throw throwError; - }) + + return (source: Rx.Observable) => + source.pipe( + catchError((error) => { + throw new Error(`The "${label}" phase encountered an error: ${error}`); + }), + timeoutWith( + timeoutValue, + Rx.throwError( + new Error( + `The "${label}" phase took longer than ${numberToDuration( + timeoutValue + ).asSeconds()} seconds. You may need to increase "${configValue}"` + ) + ) + ) ); - }; } private openUrl(index: number, urlOrUrlLocatorTuple: UrlOrUrlLocatorTuple) { - return mergeMap(() => + return Rx.defer(() => openUrl( this.timeouts.openUrl.timeoutValue, this.driver, @@ -87,24 +78,25 @@ export class ScreenshotObservableHandler { this.conditionalHeaders, this.logger ) - ); + ).pipe(this.waitUntil(this.timeouts.openUrl)); } private waitForElements() { const driver = this.driver; const waitTimeout = this.timeouts.waitForElements.timeoutValue; - return (withPageOpen: Rx.Observable) => - withPageOpen.pipe( - mergeMap(() => getNumberOfItems(waitTimeout, driver, this.layout, this.logger)), - mergeMap(async (itemsCount) => { - // set the viewport to the dimentions from the job, to allow elements to flow into the expected layout - const viewport = this.layout.getViewport(itemsCount) || getDefaultViewPort(); - await Promise.all([ - driver.setViewport(viewport, this.logger), - waitForVisualizations(waitTimeout, driver, itemsCount, this.layout, this.logger), - ]); - }) - ); + + return Rx.defer(() => getNumberOfItems(waitTimeout, driver, this.layout, this.logger)).pipe( + mergeMap((itemsCount) => { + // set the viewport to the dimentions from the job, to allow elements to flow into the expected layout + const viewport = this.layout.getViewport(itemsCount) || getDefaultViewPort(); + + return Rx.forkJoin([ + driver.setViewport(viewport, this.logger), + waitForVisualizations(waitTimeout, driver, itemsCount, this.layout, this.logger), + ]); + }), + this.waitUntil(this.timeouts.waitForElements) + ); } private completeRender(apmTrans: apm.Transaction | null) { @@ -112,32 +104,27 @@ export class ScreenshotObservableHandler { const layout = this.layout; const logger = this.logger; - return (withElements: Rx.Observable) => - withElements.pipe( - mergeMap(async () => { - // Waiting till _after_ elements have rendered before injecting our CSS - // allows for them to be displayed properly in many cases - await injectCustomCss(driver, layout, logger); - - const apmPositionElements = apmTrans?.startSpan('position_elements', 'correction'); - // position panel elements for print layout - await layout.positionElements?.(driver, logger); - apmPositionElements?.end(); - - await waitForRenderComplete(this.timeouts.loadDelay, driver, layout, logger); - }), - mergeMap(() => - Promise.all([ - getTimeRange(driver, layout, logger), - getElementPositionAndAttributes(driver, layout, logger), - getRenderErrors(driver, layout, logger), - ]).then(([timeRange, elementsPositionAndAttributes, renderErrors]) => ({ - elementsPositionAndAttributes, - timeRange, - renderErrors, - })) - ) - ); + return Rx.defer(async () => { + // Waiting till _after_ elements have rendered before injecting our CSS + // allows for them to be displayed properly in many cases + await injectCustomCss(driver, layout, logger); + + const apmPositionElements = apmTrans?.startSpan('position_elements', 'correction'); + // position panel elements for print layout + await layout.positionElements?.(driver, logger); + apmPositionElements?.end(); + + await waitForRenderComplete(this.timeouts.loadDelay, driver, layout, logger); + }).pipe( + mergeMap(() => + Rx.forkJoin({ + timeRange: getTimeRange(driver, layout, logger), + elementsPositionAndAttributes: getElementPositionAndAttributes(driver, layout, logger), + renderErrors: getRenderErrors(driver, layout, logger), + }) + ), + this.waitUntil(this.timeouts.renderComplete) + ); } public setupPage( @@ -145,15 +132,10 @@ export class ScreenshotObservableHandler { urlOrUrlLocatorTuple: UrlOrUrlLocatorTuple, apmTrans: apm.Transaction | null ) { - return (initial: Rx.Observable) => - initial.pipe( - this.openUrl(index, urlOrUrlLocatorTuple), - this.waitUntil(this.timeouts.openUrl), - this.waitForElements(), - this.waitUntil(this.timeouts.waitForElements), - this.completeRender(apmTrans), - this.waitUntil(this.timeouts.renderComplete) - ); + return this.openUrl(index, urlOrUrlLocatorTuple).pipe( + switchMapTo(this.waitForElements()), + switchMapTo(this.completeRender(apmTrans)) + ); } public getScreenshots() { From b040096beb1202207bcfb0c45c2489541c494e08 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 10 Nov 2021 20:20:18 -0500 Subject: [PATCH 21/43] Fix margins for various pending suggestions callouts (#118208) (#118265) Co-authored-by: Byron Hulcher --- .../components/curations/components/suggestions_callout.tsx | 5 +++-- .../curations/curation/suggested_documents_callout.tsx | 1 + .../components/suggested_curations_callout.tsx | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_callout.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_callout.tsx index 490e6323290f0..ed46a878f0cea 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_callout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_callout.tsx @@ -28,6 +28,7 @@ interface SuggestionsCalloutProps { description: string; buttonTo: string; lastUpdatedTimestamp: string; // ISO string like '2021-10-04T18:53:02.784Z' + style?: React.CSSProperties; } export const SuggestionsCallout: React.FC = ({ @@ -35,6 +36,7 @@ export const SuggestionsCallout: React.FC = ({ description, buttonTo, lastUpdatedTimestamp, + style, }) => { const { pathname } = useLocation(); @@ -49,7 +51,7 @@ export const SuggestionsCallout: React.FC = ({ return ( <> - +

    {description}

    @@ -80,7 +82,6 @@ export const SuggestionsCallout: React.FC = ({
    - ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/suggested_documents_callout.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/suggested_documents_callout.tsx index ec296089a1086..cd9b57651c00a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/suggested_documents_callout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/suggested_documents_callout.tsx @@ -35,6 +35,7 @@ export const SuggestedDocumentsCallout: React.FC = () => { return ( { return ( Date: Wed, 10 Nov 2021 20:24:49 -0500 Subject: [PATCH 22/43] [Cases] [108671] Fix faulty status api call, if selection is same (#118115) (#118263) * [Cases] [108671] Fix faulty status api call, if selection is same * Add unit test to ensure selecting same status message does not call api Co-authored-by: Kristof-Pierre Cummings Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Kristof C Co-authored-by: Kristof-Pierre Cummings --- .../case_action_bar/status_context_menu.test.tsx | 11 +++++++++++ .../case_action_bar/status_context_menu.tsx | 6 ++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/cases/public/components/case_action_bar/status_context_menu.test.tsx b/x-pack/plugins/cases/public/components/case_action_bar/status_context_menu.test.tsx index 54cbbc5b6841f..93ecf4df997d2 100644 --- a/x-pack/plugins/cases/public/components/case_action_bar/status_context_menu.test.tsx +++ b/x-pack/plugins/cases/public/components/case_action_bar/status_context_menu.test.tsx @@ -60,4 +60,15 @@ describe('SyncAlertsSwitch', () => { expect(onStatusChanged).toHaveBeenCalledWith('in-progress'); }); + + it('it does not call onStatusChanged if selection is same as current status', async () => { + const wrapper = mount( + + ); + + wrapper.find(`[data-test-subj="case-view-status-dropdown"] button`).simulate('click'); + wrapper.find(`[data-test-subj="case-view-status-dropdown-open"] button`).simulate('click'); + + expect(onStatusChanged).not.toHaveBeenCalled(); + }); }); diff --git a/x-pack/plugins/cases/public/components/case_action_bar/status_context_menu.tsx b/x-pack/plugins/cases/public/components/case_action_bar/status_context_menu.tsx index 603efb253f051..ab86f589bfdd0 100644 --- a/x-pack/plugins/cases/public/components/case_action_bar/status_context_menu.tsx +++ b/x-pack/plugins/cases/public/components/case_action_bar/status_context_menu.tsx @@ -33,9 +33,11 @@ const StatusContextMenuComponent: React.FC = ({ const onContextMenuItemClick = useCallback( (status: CaseStatuses) => { closePopover(); - onStatusChanged(status); + if (currentStatus !== status) { + onStatusChanged(status); + } }, - [closePopover, onStatusChanged] + [closePopover, currentStatus, onStatusChanged] ); const panelItems = useMemo( From 427bb03e839ab35026771c01fa9cd0efa5bba496 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 10 Nov 2021 20:37:54 -0500 Subject: [PATCH 23/43] [Security Solution][Endpoint][Admin][Policy] Fixes the endpoint version test on policy details (#118124) (#118269) Co-authored-by: Candace Park <56409205+parkiino@users.noreply.github.com> --- .../apps/endpoint/policy_details.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts index 49206ffb1070e..02c08b1d7a915 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts @@ -309,8 +309,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); it('should show the supported Endpoint version', async () => { - const supportedVersion = await testSubjects.find('policySupportedVersions'); - expect(supportedVersion).to.be('Agent version ' + popupVersionsMap.get('malware')); + expect(await testSubjects.getVisibleText('policySupportedVersions')).to.equal( + 'Agent version ' + popupVersionsMap.get('malware') + ); }); it('should show the custom message text area when the Notify User checkbox is checked', async () => { From 4b07efd597e5e2a75fd6af3850ef9a80f39b5aa9 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 10 Nov 2021 22:25:31 -0500 Subject: [PATCH 24/43] [docs] rewrite docs cli to show logs and use modern apis (#117767) (#117777) Co-authored-by: spalger Co-authored-by: Spencer Co-authored-by: spalger --- scripts/docs.js | 2 +- src/dev/run_build_docs_cli.ts | 64 +++++++++++++++++++++++++++++++++++ src/docs/cli.js | 30 ---------------- src/docs/docs_repo.js | 28 --------------- 4 files changed, 65 insertions(+), 59 deletions(-) create mode 100644 src/dev/run_build_docs_cli.ts delete mode 100644 src/docs/cli.js delete mode 100644 src/docs/docs_repo.js diff --git a/scripts/docs.js b/scripts/docs.js index 6522079c7aca3..f310903b90bac 100644 --- a/scripts/docs.js +++ b/scripts/docs.js @@ -7,4 +7,4 @@ */ require('../src/setup_node_env'); -require('../src/docs/cli'); +require('../src/dev/run_build_docs_cli').runBuildDocsCli(); diff --git a/src/dev/run_build_docs_cli.ts b/src/dev/run_build_docs_cli.ts new file mode 100644 index 0000000000000..aad524b4437d3 --- /dev/null +++ b/src/dev/run_build_docs_cli.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import Path from 'path'; + +import dedent from 'dedent'; +import { run, REPO_ROOT, createFailError } from '@kbn/dev-utils'; + +const DEFAULT_DOC_REPO_PATH = Path.resolve(REPO_ROOT, '..', 'docs'); + +const rel = (path: string) => Path.relative(process.cwd(), path); + +export function runBuildDocsCli() { + run( + async ({ flags, procRunner }) => { + const docRepoPath = + typeof flags.docrepo === 'string' && flags.docrepo + ? Path.resolve(process.cwd(), flags.docrepo) + : DEFAULT_DOC_REPO_PATH; + + try { + await procRunner.run('build_docs', { + cmd: rel(Path.resolve(docRepoPath, 'build_docs')), + args: [ + ['--doc', rel(Path.resolve(REPO_ROOT, 'docs/index.asciidoc'))], + ['--chunk', '1'], + flags.open ? ['--open'] : [], + ].flat(), + cwd: REPO_ROOT, + wait: true, + }); + } catch (error) { + if (error.code === 'ENOENT') { + throw createFailError(dedent` + Unable to run "build_docs" script from docs repo. + Does it exist at [${rel(docRepoPath)}]? + Do you need to pass --docrepo to specify the correct path or clone it there? + `); + } + + throw error; + } + }, + { + description: 'Build the docs and serve them from a docker container', + flags: { + string: ['docrepo'], + boolean: ['open'], + default: { + docrepo: DEFAULT_DOC_REPO_PATH, + }, + help: ` + --docrepo [path] Path to the doc repo, defaults to ${rel(DEFAULT_DOC_REPO_PATH)} + --open Automatically open the built docs in your default browser after building + `, + }, + } + ); +} diff --git a/src/docs/cli.js b/src/docs/cli.js deleted file mode 100644 index ac17c3908f0ca..0000000000000 --- a/src/docs/cli.js +++ /dev/null @@ -1,30 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { execFileSync } from 'child_process'; -import { Command } from 'commander'; - -import { defaultDocsRepoPath, buildDocsScript, buildDocsArgs } from './docs_repo'; - -const cmd = new Command('node scripts/docs'); -cmd - .option('--docrepo [path]', 'local path to the docs repo', defaultDocsRepoPath()) - .option('--open', 'open the docs in the browser', false) - .parse(process.argv); - -try { - execFileSync(buildDocsScript(cmd), buildDocsArgs(cmd)); -} catch (err) { - if (err.code === 'ENOENT') { - console.error(`elastic/docs repo must be cloned to ${cmd.docrepo}`); - } else { - console.error(err.stack); - } - - process.exit(1); -} diff --git a/src/docs/docs_repo.js b/src/docs/docs_repo.js deleted file mode 100644 index 2d3589c444b34..0000000000000 --- a/src/docs/docs_repo.js +++ /dev/null @@ -1,28 +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 - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { resolve } from 'path'; - -const kibanaDir = resolve(__dirname, '..', '..'); - -export function buildDocsScript(cmd) { - return resolve(process.cwd(), cmd.docrepo, 'build_docs'); -} - -export function buildDocsArgs(cmd) { - const docsIndexFile = resolve(kibanaDir, 'docs', 'index.asciidoc'); - let args = ['--doc', docsIndexFile, '--direct_html', '--chunk=1']; - if (cmd.open) { - args = [...args, '--open']; - } - return args; -} - -export function defaultDocsRepoPath() { - return resolve(kibanaDir, '..', 'docs'); -} From f5e58cda92ea2635d2be4cc60775d98bd4224e01 Mon Sep 17 00:00:00 2001 From: spalger Date: Wed, 10 Nov 2021 20:43:54 -0700 Subject: [PATCH 25/43] skip flaky suite (#118272) (cherry picked from commit a03e12bec0088a95075aa7fc7b824b7881369a2e) --- .../feature_controls/saved_objects_management_security.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts b/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts index 9e8c5df1565ee..509d4325cf61b 100644 --- a/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts +++ b/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts @@ -16,7 +16,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { let version: string = ''; const find = getService('find'); - describe('feature controls saved objects management', () => { + // FLAKY: https://github.com/elastic/kibana/issues/118272 + describe.skip('feature controls saved objects management', () => { before(async () => { version = await kibanaServer.version.get(); await kibanaServer.importExport.load( From 26f499beae3eee3bed04f7468f6287207d5f1f70 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Wed, 10 Nov 2021 23:28:03 -0500 Subject: [PATCH 26/43] Sourcerer UI (#117601) (#118270) Co-authored-by: Steph Milovic --- .../index_pattern_table.tsx | 11 + .../app/home/global_header/index.test.tsx | 121 +++++ .../public/app/home/global_header/index.tsx | 27 +- .../components/case_header_page/index.tsx | 4 +- .../__snapshots__/index.test.tsx.snap | 3 - .../common/components/header_page/index.tsx | 7 - .../common/components/sourcerer/helpers.tsx | 126 +++++ .../components/sourcerer/index.test.tsx | 402 ++++++++++----- .../common/components/sourcerer/index.tsx | 362 +++++++------- .../components/sourcerer/translations.ts | 83 +++- .../sourcerer/use_pick_index_patterns.tsx | 167 +++++++ .../common/containers/sourcerer/index.tsx | 31 +- .../public/common/store/sourcerer/helpers.ts | 28 +- .../detection_engine_header_page/index.tsx | 2 +- .../public/overview/pages/overview.tsx | 3 - .../timeline/eql_tab_content/index.tsx | 7 +- .../timeline/query_tab_content/index.tsx | 7 +- .../search_or_filter/pick_events.test.tsx | 220 --------- .../timeline/search_or_filter/pick_events.tsx | 465 ------------------ .../public/timelines/pages/timelines_page.tsx | 3 +- .../public/ueba/pages/details/index.tsx | 1 - .../translations/translations/ja-JP.json | 3 - .../translations/translations/zh-CN.json | 3 - 23 files changed, 1040 insertions(+), 1046 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/app/home/global_header/index.test.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/sourcerer/helpers.tsx create mode 100644 x-pack/plugins/security_solution/public/common/components/sourcerer/use_pick_index_patterns.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.test.tsx delete mode 100644 x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.tsx diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx b/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx index 0ad71d9a23cc2..89230ae03a923 100644 --- a/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx @@ -50,6 +50,13 @@ const title = i18n.translate('indexPatternManagement.dataViewTable.title', { defaultMessage: 'Data views', }); +const securityDataView = i18n.translate( + 'indexPatternManagement.indexPatternTable.badge.securityDataViewTitle', + { + defaultMessage: 'Security Data View', + } +); + interface Props extends RouteComponentProps { canSave: boolean; showCreateDialog?: boolean; @@ -116,6 +123,10 @@ export const IndexPatternTable = ({   + {index.id && index.id === 'security-solution' && ( + {securityDataView} + )} + {index.tags && index.tags.map(({ key: tagKey, name: tagName }) => ( {tagName} diff --git a/x-pack/plugins/security_solution/public/app/home/global_header/index.test.tsx b/x-pack/plugins/security_solution/public/app/home/global_header/index.test.tsx new file mode 100644 index 0000000000000..c16e77e9182f2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/app/home/global_header/index.test.tsx @@ -0,0 +1,121 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; +import { render } from '@testing-library/react'; +import { useLocation } from 'react-router-dom'; +import { GlobalHeader } from '.'; +import { SecurityPageName } from '../../../../common/constants'; +import { + createSecuritySolutionStorageMock, + mockGlobalState, + SUB_PLUGINS_REDUCER, + TestProviders, +} from '../../../common/mock'; +import { TimelineId } from '../../../../common/types/timeline'; +import { createStore } from '../../../common/store'; +import { kibanaObservable } from '../../../../../timelines/public/mock'; +import { sourcererPaths } from '../../../common/containers/sourcerer'; + +jest.mock('react-router-dom', () => { + const actual = jest.requireActual('react-router-dom'); + return { ...actual, useLocation: jest.fn().mockReturnValue({ pathname: '' }) }; +}); + +jest.mock('../../../common/lib/kibana', () => { + const originalModule = jest.requireActual('../../../common/lib/kibana'); + return { + ...originalModule, + useKibana: jest + .fn() + .mockReturnValue({ services: { http: { basePath: { prepend: jest.fn() } } } }), + useUiSetting$: jest.fn().mockReturnValue([]), + }; +}); + +jest.mock('react-reverse-portal', () => ({ + InPortal: ({ children }: { children: React.ReactNode }) => <>{children}, + OutPortal: ({ children }: { children: React.ReactNode }) => <>{children}, + createPortalNode: () => ({ unmount: jest.fn() }), +})); + +describe('global header', () => { + const mockSetHeaderActionMenu = jest.fn(); + const state = { + ...mockGlobalState, + timeline: { + ...mockGlobalState.timeline, + timelineById: { + [TimelineId.active]: { + ...mockGlobalState.timeline.timelineById.test, + show: false, + }, + }, + }, + }; + const { storage } = createSecuritySolutionStorageMock(); + const store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + + it('has add data link', () => { + (useLocation as jest.Mock).mockReturnValue([ + { pageName: SecurityPageName.overview, detailName: undefined }, + ]); + const { getByText } = render( + + + + ); + expect(getByText('Add integrations')).toBeInTheDocument(); + }); + + it.each(sourcererPaths)('shows sourcerer on %s page', (pathname) => { + (useLocation as jest.Mock).mockReturnValue({ pathname }); + + const { getByTestId } = render( + + + + ); + expect(getByTestId('sourcerer-trigger')).toBeInTheDocument(); + }); + + it('shows sourcerer on rule details page', () => { + (useLocation as jest.Mock).mockReturnValue({ pathname: sourcererPaths[2] }); + + const { getByTestId } = render( + + + + ); + expect(getByTestId('sourcerer-trigger')).toBeInTheDocument(); + }); + + it('shows no sourcerer if timeline is open', () => { + const mockstate = { + ...mockGlobalState, + timeline: { + ...mockGlobalState.timeline, + timelineById: { + [TimelineId.active]: { + ...mockGlobalState.timeline.timelineById.test, + show: true, + }, + }, + }, + }; + const mockStore = createStore(mockstate, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + + (useLocation as jest.Mock).mockReturnValue({ pathname: sourcererPaths[2] }); + + const { queryByTestId } = render( + + + + ); + + expect(queryByTestId('sourcerer-trigger')).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/app/home/global_header/index.tsx b/x-pack/plugins/security_solution/public/app/home/global_header/index.tsx index 41e441fd4110f..6afcc649da5f3 100644 --- a/x-pack/plugins/security_solution/public/app/home/global_header/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/global_header/index.tsx @@ -5,14 +5,14 @@ * 2.0. */ import { - EuiHeaderSection, - EuiHeaderLinks, EuiHeaderLink, + EuiHeaderLinks, + EuiHeaderSection, EuiHeaderSectionItem, } from '@elastic/eui'; import React, { useEffect, useMemo } from 'react'; import { useLocation } from 'react-router-dom'; -import { createPortalNode, OutPortal, InPortal } from 'react-reverse-portal'; +import { createPortalNode, InPortal, OutPortal } from 'react-reverse-portal'; import { i18n } from '@kbn/i18n'; import { AppMountParameters } from '../../../../../../../src/core/public'; @@ -21,6 +21,12 @@ import { MlPopover } from '../../../common/components/ml_popover/ml_popover'; import { useKibana } from '../../../common/lib/kibana'; import { ADD_DATA_PATH } from '../../../../common/constants'; import { isDetectionsPath } from '../../../../public/helpers'; +import { Sourcerer } from '../../../common/components/sourcerer'; +import { TimelineId } from '../../../../common/types/timeline'; +import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; +import { timelineSelectors } from '../../../timelines/store/timeline'; +import { useShallowEqualSelector } from '../../../common/hooks/use_selector'; +import { getScopeFromPath, showSourcererByPath } from '../../../common/containers/sourcerer'; const BUTTON_ADD_DATA = i18n.translate('xpack.securitySolution.globalHeader.buttonAddData', { defaultMessage: 'Add integrations', @@ -40,6 +46,16 @@ export const GlobalHeader = React.memo( } = useKibana().services; const { pathname } = useLocation(); + const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); + const showTimeline = useShallowEqualSelector( + (state) => (getTimeline(state, TimelineId.active) ?? timelineDefaults).show + ); + + const sourcererScope = getScopeFromPath(pathname); + const showSourcerer = showSourcererByPath(pathname); + + const href = useMemo(() => prepend(ADD_DATA_PATH), [prepend]); + useEffect(() => { setHeaderActionMenu((element) => { const mount = toMountPoint(); @@ -65,11 +81,14 @@ export const GlobalHeader = React.memo( {BUTTON_ADD_DATA} + {showSourcerer && !showTimeline && ( + + )} diff --git a/x-pack/plugins/security_solution/public/cases/components/case_header_page/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_header_page/index.tsx index 53bc20af5e491..c1eb11ea5182d 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_header_page/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_header_page/index.tsx @@ -9,8 +9,6 @@ import React from 'react'; import { HeaderPage, HeaderPageProps } from '../../../common/components/header_page'; -const CaseHeaderPageComponent: React.FC = (props) => ( - -); +const CaseHeaderPageComponent: React.FC = (props) => ; export const CaseHeaderPage = React.memo(CaseHeaderPageComponent); diff --git a/x-pack/plugins/security_solution/public/common/components/header_page/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/header_page/__snapshots__/index.test.tsx.snap index d00bd7040c164..9e5b265c187cf 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_page/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/header_page/__snapshots__/index.test.tsx.snap @@ -33,9 +33,6 @@ exports[`HeaderPage it renders 1`] = ` Test supplement

    - = ({ border, children, draggableArguments, - hideSourcerer = false, isLoading, - sourcererScope = SourcererScopeName.default, subtitle, subtitle2, title, @@ -149,7 +143,6 @@ const HeaderPageComponent: React.FC = ({ {children} )} - {!hideSourcerer && } {/* Manually add a 'padding-bottom' to header */} diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/helpers.tsx new file mode 100644 index 0000000000000..af21a018ee47a --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/helpers.tsx @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { + EuiSuperSelectOption, + EuiIcon, + EuiBadge, + EuiButtonEmpty, + EuiFormRow, + EuiFormRowProps, +} from '@elastic/eui'; +import styled from 'styled-components'; + +import { sourcererModel } from '../../store/sourcerer'; + +import * as i18n from './translations'; + +export const FormRow = styled(EuiFormRow)` + display: ${({ $expandAdvancedOptions }) => ($expandAdvancedOptions ? 'flex' : 'none')}; + max-width: none; +`; + +export const StyledFormRow = styled(EuiFormRow)` + max-width: none; +`; + +export const StyledButton = styled(EuiButtonEmpty)` + &:enabled:focus, + &:focus { + background-color: transparent; + } +`; + +export const ResetButton = styled(EuiButtonEmpty)` + width: fit-content; + &:enabled:focus, + &:focus { + background-color: transparent; + } +`; + +export const PopoverContent = styled.div` + width: 600px; +`; + +export const StyledBadge = styled(EuiBadge)` + margin-left: 8px; +`; + +interface GetDataViewSelectOptionsProps { + dataViewId: string; + defaultDataView: sourcererModel.KibanaDataView; + isModified: boolean; + isOnlyDetectionAlerts: boolean; + kibanaDataViews: sourcererModel.KibanaDataView[]; +} + +export const getDataViewSelectOptions = ({ + dataViewId, + defaultDataView, + isModified, + isOnlyDetectionAlerts, + kibanaDataViews, +}: GetDataViewSelectOptionsProps): Array> => + isOnlyDetectionAlerts + ? [ + { + inputDisplay: ( + + {i18n.SIEM_SECURITY_DATA_VIEW_LABEL} + + {i18n.ALERTS_BADGE_TITLE} + + + ), + value: defaultDataView.id, + }, + ] + : kibanaDataViews.map(({ title, id }) => ({ + inputDisplay: + id === defaultDataView.id ? ( + + {i18n.SECURITY_DEFAULT_DATA_VIEW_LABEL} + {isModified && id === dataViewId && ( + {i18n.MODIFIED_BADGE_TITLE} + )} + + ) : ( + + {title} + {isModified && id === dataViewId && ( + {i18n.MODIFIED_BADGE_TITLE} + )} + + ), + value: id, + })); + +interface GetTooltipContent { + isOnlyDetectionAlerts: boolean; + isPopoverOpen: boolean; + selectedPatterns: string[]; + signalIndexName: string | null; +} + +export const getTooltipContent = ({ + isOnlyDetectionAlerts, + isPopoverOpen, + selectedPatterns, + signalIndexName, +}: GetTooltipContent): string | null => { + if (isPopoverOpen || (isOnlyDetectionAlerts && !signalIndexName)) { + return null; + } + return (isOnlyDetectionAlerts ? [signalIndexName] : selectedPatterns).join(', '); +}; + +export const getPatternListWithoutSignals = ( + patternList: string[], + signalIndexName: string | null +): string[] => patternList.filter((p) => p !== signalIndexName); diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx index 1b23d23c5eb62..c2da7e78d64e0 100644 --- a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx @@ -17,7 +17,7 @@ import { SUB_PLUGINS_REDUCER, TestProviders, } from '../../mock'; -import { createStore, State } from '../../store'; +import { createStore } from '../../store'; import { EuiSuperSelectOption } from '@elastic/eui/src/components/form/super_select/super_select_control'; const mockDispatch = jest.fn(); @@ -45,31 +45,55 @@ const defaultProps = { scope: sourcererModel.SourcererScopeName.default, }; -describe('Sourcerer component', () => { - const state: State = mockGlobalState; - const { id, patternList, title } = state.sourcerer.defaultDataView; - const patternListNoSignals = patternList - .filter((p) => p !== state.sourcerer.signalIndexName) - .sort(); - const checkOptionsAndSelections = (wrapper: ReactWrapper, patterns: string[]) => ({ - availableOptionCount: wrapper.find(`[data-test-subj="sourcerer-combo-option"]`).length, - optionsSelected: patterns.every((pattern) => - wrapper - .find(`[data-test-subj="sourcerer-combo-box"] span[title="${pattern}"]`) - .first() - .exists() - ), - }); +const checkOptionsAndSelections = (wrapper: ReactWrapper, patterns: string[]) => ({ + availableOptionCount: wrapper.find(`[data-test-subj="sourcerer-combo-option"]`).length, + optionsSelected: patterns.every((pattern) => + wrapper.find(`[data-test-subj="sourcerer-combo-box"] span[title="${pattern}"]`).first().exists() + ), +}); +const { id, patternList, title } = mockGlobalState.sourcerer.defaultDataView; +const patternListNoSignals = patternList + .filter((p) => p !== mockGlobalState.sourcerer.signalIndexName) + .sort(); +let store: ReturnType; +describe('Sourcerer component', () => { const { storage } = createSecuritySolutionStorageMock(); - let store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); beforeEach(() => { - store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + store = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); jest.clearAllMocks(); jest.restoreAllMocks(); }); + it('renders data view title', () => { + const wrapper = mount( + + + + ); + wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click'); + expect(wrapper.find(`[data-test-subj="sourcerer-title"]`).first().text()).toEqual( + 'Data view selection' + ); + }); + + it('renders a toggle for advanced options', () => { + const testProps = { + ...defaultProps, + showAlertsOnlyCheckbox: true, + }; + const wrapper = mount( + + + + ); + wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click'); + expect( + wrapper.find(`[data-test-subj="sourcerer-advanced-options-toggle"]`).first().text() + ).toEqual('Advanced options'); + }); + it('renders tooltip', () => { const wrapper = mount( @@ -119,25 +143,25 @@ describe('Sourcerer component', () => { it('Removes duplicate options from title', () => { store = createStore( { - ...state, + ...mockGlobalState, sourcerer: { - ...state.sourcerer, + ...mockGlobalState.sourcerer, defaultDataView: { - ...state.sourcerer.defaultDataView, + ...mockGlobalState.sourcerer.defaultDataView, id: '1234', title: 'filebeat-*,auditbeat-*,auditbeat-*,auditbeat-*,auditbeat-*', patternList: ['filebeat-*', 'auditbeat-*'], }, kibanaDataViews: [ { - ...state.sourcerer.defaultDataView, + ...mockGlobalState.sourcerer.defaultDataView, id: '1234', title: 'filebeat-*,auditbeat-*,auditbeat-*,auditbeat-*,auditbeat-*', patternList: ['filebeat-*', 'auditbeat-*'], }, ], sourcererScopes: { - ...state.sourcerer.sourcererScopes, + ...mockGlobalState.sourcerer.sourcererScopes, [SourcererScopeName.default]: { ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default], loading: false, @@ -170,25 +194,25 @@ describe('Sourcerer component', () => { it('Disables options with no data', () => { store = createStore( { - ...state, + ...mockGlobalState, sourcerer: { - ...state.sourcerer, + ...mockGlobalState.sourcerer, defaultDataView: { - ...state.sourcerer.defaultDataView, + ...mockGlobalState.sourcerer.defaultDataView, id: '1234', title: 'filebeat-*,auditbeat-*,fakebeat-*', patternList: ['filebeat-*', 'auditbeat-*'], }, kibanaDataViews: [ { - ...state.sourcerer.defaultDataView, + ...mockGlobalState.sourcerer.defaultDataView, id: '1234', title: 'filebeat-*,auditbeat-*,fakebeat-*', patternList: ['filebeat-*', 'auditbeat-*'], }, ], sourcererScopes: { - ...state.sourcerer.sourcererScopes, + ...mockGlobalState.sourcerer.sourcererScopes, [SourcererScopeName.default]: { ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default], loading: false, @@ -225,15 +249,15 @@ describe('Sourcerer component', () => { sourcerer: { ...mockGlobalState.sourcerer, kibanaDataViews: [ - state.sourcerer.defaultDataView, + mockGlobalState.sourcerer.defaultDataView, { - ...state.sourcerer.defaultDataView, + ...mockGlobalState.sourcerer.defaultDataView, id: '1234', title: 'auditbeat-*', patternList: ['auditbeat-*'], }, { - ...state.sourcerer.defaultDataView, + ...mockGlobalState.sourcerer.defaultDataView, id: '12347', title: 'packetbeat-*', patternList: ['packetbeat-*'], @@ -244,9 +268,8 @@ describe('Sourcerer component', () => { [SourcererScopeName.default]: { ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default], loading: false, - patternList, selectedDataViewId: id, - selectedPatterns: patternList.slice(0, 2), + selectedPatterns: patternListNoSignals.slice(0, 2), }, }, }, @@ -260,7 +283,7 @@ describe('Sourcerer component', () => { ); wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click'); wrapper.find(`[data-test-subj="comboBoxInput"]`).first().simulate('click'); - expect(checkOptionsAndSelections(wrapper, patternList.slice(0, 2))).toEqual({ + expect(checkOptionsAndSelections(wrapper, patternListNoSignals.slice(0, 2))).toEqual({ // should hide signal index availableOptionCount: title.split(',').length - 3, optionsSelected: true, @@ -272,15 +295,15 @@ describe('Sourcerer component', () => { sourcerer: { ...mockGlobalState.sourcerer, kibanaDataViews: [ - state.sourcerer.defaultDataView, + mockGlobalState.sourcerer.defaultDataView, { - ...state.sourcerer.defaultDataView, + ...mockGlobalState.sourcerer.defaultDataView, id: '1234', title: 'auditbeat-*', patternList: ['auditbeat-*'], }, { - ...state.sourcerer.defaultDataView, + ...mockGlobalState.sourcerer.defaultDataView, id: '12347', title: 'packetbeat-*', patternList: ['packetbeat-*'], @@ -305,7 +328,7 @@ describe('Sourcerer component', () => { ); - wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click'); + wrapper.find(`[data-test-subj="timeline-sourcerer-trigger"]`).first().simulate('click'); wrapper.find(`[data-test-subj="comboBoxInput"]`).first().simulate('click'); expect(checkOptionsAndSelections(wrapper, patternList.slice(0, 2))).toEqual({ // should show every option except fakebeat-* @@ -316,25 +339,25 @@ describe('Sourcerer component', () => { it('onSave dispatches setSelectedDataView', async () => { store = createStore( { - ...state, + ...mockGlobalState, sourcerer: { - ...state.sourcerer, + ...mockGlobalState.sourcerer, kibanaDataViews: [ - state.sourcerer.defaultDataView, + mockGlobalState.sourcerer.defaultDataView, { - ...state.sourcerer.defaultDataView, + ...mockGlobalState.sourcerer.defaultDataView, id: '1234', title: 'filebeat-*', patternList: ['filebeat-*'], }, ], sourcererScopes: { - ...state.sourcerer.sourcererScopes, + ...mockGlobalState.sourcerer.sourcererScopes, [SourcererScopeName.default]: { ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default], loading: false, selectedDataViewId: id, - selectedPatterns: patternList.slice(0, 2), + selectedPatterns: patternListNoSignals.slice(0, 2), }, }, }, @@ -350,13 +373,12 @@ describe('Sourcerer component', () => { ); wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click'); wrapper.find(`[data-test-subj="comboBoxInput"]`).first().simulate('click'); - expect(checkOptionsAndSelections(wrapper, patternList.slice(0, 2))).toEqual({ + expect(checkOptionsAndSelections(wrapper, patternListNoSignals.slice(0, 2))).toEqual({ availableOptionCount: title.split(',').length - 3, optionsSelected: true, }); - wrapper.find(`[data-test-subj="sourcerer-combo-option"]`).first().simulate('click'); - expect(checkOptionsAndSelections(wrapper, patternList.slice(0, 3))).toEqual({ + expect(checkOptionsAndSelections(wrapper, patternListNoSignals.slice(0, 3))).toEqual({ availableOptionCount: title.split(',').length - 4, optionsSelected: true, }); @@ -367,7 +389,7 @@ describe('Sourcerer component', () => { sourcererActions.setSelectedDataView({ id: SourcererScopeName.default, selectedDataViewId: id, - selectedPatterns: patternList.slice(0, 3), + selectedPatterns: patternListNoSignals.slice(0, 3), }) ); }); @@ -387,7 +409,7 @@ describe('Sourcerer component', () => { wrapper .find( - `[data-test-subj="sourcerer-combo-box"] [title="${patternList[0]}"] button.euiBadge__iconButton` + `[data-test-subj="sourcerer-combo-box"] [title="${patternListNoSignals[0]}"] button.euiBadge__iconButton` ) .first() .simulate('click'); @@ -407,13 +429,13 @@ describe('Sourcerer component', () => { it('disables saving when no index patterns are selected', () => { store = createStore( { - ...state, + ...mockGlobalState, sourcerer: { - ...state.sourcerer, + ...mockGlobalState.sourcerer, kibanaDataViews: [ - state.sourcerer.defaultDataView, + mockGlobalState.sourcerer.defaultDataView, { - ...state.sourcerer.defaultDataView, + ...mockGlobalState.sourcerer.defaultDataView, id: '1234', title: 'auditbeat-*', patternList: ['auditbeat-*'], @@ -434,72 +456,21 @@ describe('Sourcerer component', () => { wrapper.find('[data-test-subj="comboBoxClearButton"]').first().simulate('click'); expect(wrapper.find('[data-test-subj="sourcerer-save"]').first().prop('disabled')).toBeTruthy(); }); - it('Selects a different index pattern', async () => { - const state2 = { - ...mockGlobalState, - sourcerer: { - ...mockGlobalState.sourcerer, - kibanaDataViews: [ - state.sourcerer.defaultDataView, - { - ...state.sourcerer.defaultDataView, - id: '1234', - title: 'fakebeat-*,neatbeat-*', - patternList: ['fakebeat-*'], - }, - ], - sourcererScopes: { - ...mockGlobalState.sourcerer.sourcererScopes, - [SourcererScopeName.default]: { - ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default], - loading: false, - patternList, - selectedDataViewId: id, - selectedPatterns: patternList.slice(0, 2), - }, - }, - }, - }; - - store = createStore(state2, SUB_PLUGINS_REDUCER, kibanaObservable, storage); - const wrapper = mount( - - - - ); - wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click'); - wrapper.find(`button[data-test-subj="sourcerer-select"]`).first().simulate('click'); - - wrapper.find(`[data-test-subj="dataView-option-super"]`).first().simulate('click'); - expect(checkOptionsAndSelections(wrapper, ['fakebeat-*'])).toEqual({ - availableOptionCount: 0, - optionsSelected: true, - }); - wrapper.find(`[data-test-subj="sourcerer-save"]`).first().simulate('click'); - - expect(mockDispatch).toHaveBeenCalledWith( - sourcererActions.setSelectedDataView({ - id: SourcererScopeName.default, - selectedDataViewId: '1234', - selectedPatterns: ['fakebeat-*'], - }) - ); - }); it('Does display signals index on timeline sourcerer', () => { const state2 = { ...mockGlobalState, sourcerer: { ...mockGlobalState.sourcerer, kibanaDataViews: [ - state.sourcerer.defaultDataView, + mockGlobalState.sourcerer.defaultDataView, { - ...state.sourcerer.defaultDataView, + ...mockGlobalState.sourcerer.defaultDataView, id: '1234', title: 'auditbeat-*', patternList: ['auditbeat-*'], }, { - ...state.sourcerer.defaultDataView, + ...mockGlobalState.sourcerer.defaultDataView, id: '12347', title: 'packetbeat-*', patternList: ['packetbeat-*'], @@ -510,9 +481,8 @@ describe('Sourcerer component', () => { [SourcererScopeName.timeline]: { ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline], loading: false, - patternList, selectedDataViewId: id, - selectedPatterns: patternList.slice(0, 2), + selectedPatterns: patternListNoSignals.slice(0, 2), }, }, }, @@ -524,9 +494,9 @@ describe('Sourcerer component', () => { ); - wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click'); + wrapper.find(`[data-test-subj="timeline-sourcerer-trigger"]`).first().simulate('click'); wrapper.find(`[data-test-subj="comboBoxToggleListButton"]`).first().simulate('click'); - expect(wrapper.find(`[data-test-subj="sourcerer-combo-option"]`).at(6).text()).toEqual( + expect(wrapper.find(`[data-test-subj="sourcerer-combo-option"]`).at(0).text()).toEqual( mockGlobalState.sourcerer.signalIndexName ); }); @@ -536,15 +506,15 @@ describe('Sourcerer component', () => { sourcerer: { ...mockGlobalState.sourcerer, kibanaDataViews: [ - state.sourcerer.defaultDataView, + mockGlobalState.sourcerer.defaultDataView, { - ...state.sourcerer.defaultDataView, + ...mockGlobalState.sourcerer.defaultDataView, id: '1234', title: 'auditbeat-*', patternList: ['auditbeat-*'], }, { - ...state.sourcerer.defaultDataView, + ...mockGlobalState.sourcerer.defaultDataView, id: '12347', title: 'packetbeat-*', patternList: ['packetbeat-*'], @@ -555,9 +525,8 @@ describe('Sourcerer component', () => { [SourcererScopeName.default]: { ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default], loading: false, - patternList, selectedDataViewId: id, - selectedPatterns: patternList.slice(0, 2), + selectedPatterns: patternListNoSignals.slice(0, 2), }, }, }, @@ -581,3 +550,204 @@ describe('Sourcerer component', () => { ).toBeFalsy(); }); }); + +describe('sourcerer on alerts page or rules details page', () => { + let wrapper: ReactWrapper; + const { storage } = createSecuritySolutionStorageMock(); + store = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + const testProps = { + scope: sourcererModel.SourcererScopeName.detections, + }; + + beforeAll(() => { + wrapper = mount( + + + + ); + wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click'); + wrapper.find(`[data-test-subj="sourcerer-advanced-options-toggle"]`).first().simulate('click'); + }); + + it('renders an alerts badge in sourcerer button', () => { + expect(wrapper.find(`[data-test-subj="sourcerer-alerts-badge"]`).first().text()).toEqual( + 'Alerts' + ); + }); + + it('renders a callout', () => { + expect(wrapper.find(`[data-test-subj="sourcerer-callout"]`).first().text()).toEqual( + 'Data view cannot be modified on this page' + ); + }); + + it('disable data view selector', () => { + expect( + wrapper.find(`[data-test-subj="sourcerer-select"]`).first().prop('disabled') + ).toBeTruthy(); + }); + + it('data view selector is default to Security Data View', () => { + expect( + wrapper.find(`[data-test-subj="sourcerer-select"]`).first().prop('valueOfSelected') + ).toEqual('security-solution'); + }); + + it('renders an alert badge in data view selector', () => { + expect(wrapper.find(`[data-test-subj="security-alerts-option-badge"]`).first().text()).toEqual( + 'Alerts' + ); + }); + + it('disable index pattern selector', () => { + expect( + wrapper.find(`[data-test-subj="sourcerer-combo-box"]`).first().prop('disabled') + ).toBeTruthy(); + }); + + it('shows signal index as index pattern option', () => { + expect(wrapper.find(`[data-test-subj="sourcerer-combo-box"]`).first().prop('options')).toEqual([ + { disabled: false, label: '.siem-signals-spacename', value: '.siem-signals-spacename' }, + ]); + }); + + it('does not render reset button', () => { + expect(wrapper.find(`[data-test-subj="sourcerer-reset"]`).exists()).toBeFalsy(); + }); + + it('does not render save button', () => { + expect(wrapper.find(`[data-test-subj="sourcerer-save"]`).exists()).toBeFalsy(); + }); +}); + +describe('timeline sourcerer', () => { + let wrapper: ReactWrapper; + const { storage } = createSecuritySolutionStorageMock(); + store = createStore(mockGlobalState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + const testProps = { + scope: sourcererModel.SourcererScopeName.timeline, + }; + + beforeAll(() => { + wrapper = mount( + + + + ); + wrapper.find(`[data-test-subj="timeline-sourcerer-trigger"]`).first().simulate('click'); + wrapper + .find( + `[data-test-subj="timeline-sourcerer-popover"] [data-test-subj="sourcerer-advanced-options-toggle"]` + ) + .first() + .simulate('click'); + }); + + it('renders "alerts only" checkbox', () => { + wrapper + .find( + `[data-test-subj="timeline-sourcerer-popover"] [data-test-subj="sourcerer-alert-only-checkbox"]` + ) + .first() + .simulate('click'); + expect(wrapper.find(`[data-test-subj="sourcerer-alert-only-checkbox"]`).first().text()).toEqual( + 'Show only detection alerts' + ); + }); + + it('data view selector is enabled', () => { + expect( + wrapper + .find(`[data-test-subj="timeline-sourcerer-popover"] [data-test-subj="sourcerer-select"]`) + .first() + .prop('disabled') + ).toBeFalsy(); + }); + + it('data view selector is default to Security Default Data View', () => { + expect( + wrapper + .find(`[data-test-subj="timeline-sourcerer-popover"] [data-test-subj="sourcerer-select"]`) + .first() + .prop('valueOfSelected') + ).toEqual('security-solution'); + }); + + it('index pattern selector is enabled', () => { + expect( + wrapper + .find( + `[data-test-subj="timeline-sourcerer-popover"] [data-test-subj="sourcerer-combo-box"]` + ) + .first() + .prop('disabled') + ).toBeFalsy(); + }); + + it('render reset button', () => { + expect(wrapper.find(`[data-test-subj="sourcerer-reset"]`).exists()).toBeTruthy(); + }); + + it('render save button', () => { + expect(wrapper.find(`[data-test-subj="sourcerer-save"]`).exists()).toBeTruthy(); + }); +}); + +describe('Sourcerer integration tests', () => { + const state = { + ...mockGlobalState, + sourcerer: { + ...mockGlobalState.sourcerer, + kibanaDataViews: [ + mockGlobalState.sourcerer.defaultDataView, + { + ...mockGlobalState.sourcerer.defaultDataView, + id: '1234', + title: 'fakebeat-*,neatbeat-*', + patternList: ['fakebeat-*'], + }, + ], + sourcererScopes: { + ...mockGlobalState.sourcerer.sourcererScopes, + [SourcererScopeName.default]: { + ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default], + loading: false, + selectedDataViewId: id, + selectedPatterns: patternListNoSignals.slice(0, 2), + }, + }, + }, + }; + + const { storage } = createSecuritySolutionStorageMock(); + + beforeEach(() => { + store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + it('Selects a different index pattern', async () => { + const wrapper = mount( + + + + ); + wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click'); + wrapper.find(`button[data-test-subj="sourcerer-select"]`).first().simulate('click'); + + wrapper.find(`[data-test-subj="dataView-option-super"]`).first().simulate('click'); + expect(checkOptionsAndSelections(wrapper, ['fakebeat-*'])).toEqual({ + availableOptionCount: 0, + optionsSelected: true, + }); + wrapper.find(`[data-test-subj="sourcerer-save"]`).first().simulate('click'); + + expect(mockDispatch).toHaveBeenCalledWith( + sourcererActions.setSelectedDataView({ + id: SourcererScopeName.default, + selectedDataViewId: '1234', + selectedPatterns: ['fakebeat-*'], + }) + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx index 6f32282c53040..6f223cbb4aa30 100644 --- a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx @@ -7,48 +7,52 @@ import { EuiButton, - EuiButtonEmpty, + EuiCallOut, + EuiCheckbox, EuiComboBox, - EuiComboBoxOptionOption, EuiFlexGroup, EuiFlexItem, - EuiIcon, + EuiForm, EuiPopover, EuiPopoverTitle, EuiSpacer, EuiSuperSelect, - EuiText, EuiToolTip, } from '@elastic/eui'; import deepEqual from 'fast-deep-equal'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useDispatch } from 'react-redux'; -import styled from 'styled-components'; import * as i18n from './translations'; import { sourcererActions, sourcererModel, sourcererSelectors } from '../../store/sourcerer'; -import { getScopePatternListSelection } from '../../store/sourcerer/helpers'; import { useDeepEqualSelector } from '../../hooks/use_selector'; import { SourcererScopeName } from '../../store/sourcerer/model'; +import { usePickIndexPatterns } from './use_pick_index_patterns'; +import { + FormRow, + getDataViewSelectOptions, + getTooltipContent, + PopoverContent, + ResetButton, + StyledBadge, + StyledButton, + StyledFormRow, +} from './helpers'; -const PopoverContent = styled.div` - width: 600px; -`; - -const ResetButton = styled(EuiButtonEmpty)` - width: fit-content; -`; interface SourcererComponentProps { scope: sourcererModel.SourcererScopeName; } -const getPatternListWithoutSignals = ( - patternList: string[], - signalIndexName: string | null -): string[] => patternList.filter((p) => p !== signalIndexName); - export const Sourcerer = React.memo(({ scope: scopeId }) => { const dispatch = useDispatch(); + const isDetectionsSourcerer = scopeId === SourcererScopeName.detections; + const isTimelineSourcerer = scopeId === SourcererScopeName.timeline; + + const [isOnlyDetectionAlertsChecked, setIsOnlyDetectionAlertsChecked] = useState(false); + + const isOnlyDetectionAlerts: boolean = + isDetectionsSourcerer || (isTimelineSourcerer && isOnlyDetectionAlertsChecked); + const sourcererScopeSelector = useMemo(() => sourcererSelectors.getSourcererScopeSelector(), []); const { defaultDataView, @@ -58,55 +62,39 @@ export const Sourcerer = React.memo(({ scope: scopeId } } = useDeepEqualSelector((state) => sourcererScopeSelector(state, scopeId)); const [isPopoverOpen, setPopoverIsOpen] = useState(false); - const [dataViewId, setDataViewId] = useState(selectedDataViewId ?? defaultDataView.id); - const { patternList, selectablePatterns } = useMemo(() => { - const theDataView = kibanaDataViews.find((dataView) => dataView.id === dataViewId); - return theDataView != null - ? scopeId === SourcererScopeName.default - ? { - patternList: getPatternListWithoutSignals( - theDataView.title - .split(',') - // remove duplicates patterns from selector - .filter((pattern, i, self) => self.indexOf(pattern) === i), - signalIndexName - ), - selectablePatterns: getPatternListWithoutSignals( - theDataView.patternList, - signalIndexName - ), - } - : { - patternList: theDataView.title - .split(',') - // remove duplicates patterns from selector - .filter((pattern, i, self) => self.indexOf(pattern) === i), - selectablePatterns: theDataView.patternList, - } - : { patternList: [], selectablePatterns: [] }; - }, [kibanaDataViews, scopeId, signalIndexName, dataViewId]); - - const selectableOptions = useMemo( - () => - patternList.map((indexName) => ({ - label: indexName, - value: indexName, - disabled: !selectablePatterns.includes(indexName), - })), - [selectablePatterns, patternList] - ); - - const [selectedOptions, setSelectedOptions] = useState>>( - selectedPatterns.map((indexName) => ({ - label: indexName, - value: indexName, - })) + const { + isModified, + onChangeCombo, + renderOption, + selectableOptions, + selectedOptions, + setIndexPatternsByDataView, + } = usePickIndexPatterns({ + dataViewId, + defaultDataViewId: defaultDataView.id, + isOnlyDetectionAlerts, + kibanaDataViews, + scopeId, + selectedPatterns, + signalIndexName, + }); + const onCheckboxChanged = useCallback( + (e) => { + setIsOnlyDetectionAlertsChecked(e.target.checked); + setDataViewId(defaultDataView.id); + setIndexPatternsByDataView(defaultDataView.id, e.target.checked); + }, + [defaultDataView.id, setIndexPatternsByDataView] ); const isSavingDisabled = useMemo(() => selectedOptions.length === 0, [selectedOptions]); + const [expandAdvancedOptions, setExpandAdvancedOptions] = useState(false); - const setPopoverIsOpenCb = useCallback(() => setPopoverIsOpen((prevState) => !prevState), []); + const setPopoverIsOpenCb = useCallback(() => { + setPopoverIsOpen((prevState) => !prevState); + setExpandAdvancedOptions(false); // we always want setExpandAdvancedOptions collapsed by default when popover opened + }, []); const onChangeDataView = useCallback( (newSelectedDataView: string, newSelectedPatterns: string[]) => { dispatch( @@ -120,90 +108,64 @@ export const Sourcerer = React.memo(({ scope: scopeId } [dispatch, scopeId] ); - const renderOption = useCallback( - ({ value }) => {value}, - [] - ); - - const onChangeCombo = useCallback((newSelectedOptions) => { - setSelectedOptions(newSelectedOptions); - }, []); - const onChangeSuper = useCallback( (newSelectedOption) => { setDataViewId(newSelectedOption); - setSelectedOptions( - getScopePatternListSelection( - kibanaDataViews.find((dataView) => dataView.id === newSelectedOption), - scopeId, - signalIndexName, - newSelectedOption === defaultDataView.id - ).map((indexSelected: string) => ({ - label: indexSelected, - value: indexSelected, - })) - ); + setIndexPatternsByDataView(newSelectedOption); }, - [defaultDataView.id, kibanaDataViews, scopeId, signalIndexName] + [setIndexPatternsByDataView] ); const resetDataSources = useCallback(() => { setDataViewId(defaultDataView.id); - setSelectedOptions( - getScopePatternListSelection(defaultDataView, scopeId, signalIndexName, true).map( - (indexSelected: string) => ({ - label: indexSelected, - value: indexSelected, - }) - ) - ); - }, [defaultDataView, scopeId, signalIndexName]); + setIndexPatternsByDataView(defaultDataView.id); + setIsOnlyDetectionAlertsChecked(false); + }, [defaultDataView.id, setIndexPatternsByDataView]); const handleSaveIndices = useCallback(() => { - onChangeDataView( - dataViewId, - selectedOptions.map((so) => so.label) - ); + const patterns = selectedOptions.map((so) => so.label); + onChangeDataView(dataViewId, patterns); setPopoverIsOpen(false); }, [onChangeDataView, dataViewId, selectedOptions]); const handleClosePopOver = useCallback(() => { setPopoverIsOpen(false); + setExpandAdvancedOptions(false); }, []); const trigger = useMemo( () => ( - - {i18n.SOURCERER} - + {i18n.DATA_VIEW} + {isModified === 'modified' && {i18n.MODIFIED_BADGE_TITLE}} + {isModified === 'alerts' && ( + + {i18n.ALERTS_BADGE_TITLE} + + )} + ), - [setPopoverIsOpenCb, loading] + [isTimelineSourcerer, loading, setPopoverIsOpenCb, isModified] ); const dataViewSelectOptions = useMemo( () => - kibanaDataViews.map(({ title, id }) => ({ - inputDisplay: - id === defaultDataView.id ? ( - - {i18n.SIEM_DATA_VIEW_LABEL} - - ) : ( - - {title} - - ), - value: id, - })), - [defaultDataView.id, kibanaDataViews] + getDataViewSelectOptions({ + dataViewId, + defaultDataView, + isModified: isModified === 'modified', + isOnlyDetectionAlerts, + kibanaDataViews, + }), + [dataViewId, defaultDataView, isModified, isOnlyDetectionAlerts, kibanaDataViews] ); useEffect(() => { @@ -213,18 +175,16 @@ export const Sourcerer = React.memo(({ scope: scopeId } : prevSelectedOption ); }, [selectedDataViewId]); - useEffect(() => { - setSelectedOptions( - selectedPatterns.map((indexName) => ({ - label: indexName, - value: indexName, - })) - ); - }, [selectedPatterns]); const tooltipContent = useMemo( - () => (isPopoverOpen ? null : selectedPatterns.join(', ')), - [selectedPatterns, isPopoverOpen] + () => + getTooltipContent({ + isOnlyDetectionAlerts, + isPopoverOpen, + selectedPatterns, + signalIndexName, + }), + [isPopoverOpen, isOnlyDetectionAlerts, signalIndexName, selectedPatterns] ); const buttonWithTooptip = useMemo(() => { @@ -237,67 +197,117 @@ export const Sourcerer = React.memo(({ scope: scopeId } ); }, [trigger, tooltipContent]); + const onExpandAdvancedOptionsClicked = useCallback(() => { + setExpandAdvancedOptions((prevState) => !prevState); + }, []); + return ( - - <>{i18n.SELECT_INDEX_PATTERNS} + + <>{i18n.SELECT_DATA_VIEW} + {isOnlyDetectionAlerts && ( + + )} - {i18n.INDEX_PATTERNS_SELECTION_LABEL} - - - - - - - - - {i18n.INDEX_PATTERNS_RESET} - - - - + {isTimelineSourcerer && ( + + + + )} + + + - {i18n.SAVE_INDEX_PATTERNS} - - - + onChange={onChangeSuper} + options={dataViewSelectOptions} + placeholder={i18n.PICK_INDEX_PATTERNS} + valueOfSelected={dataViewId} + /> + + + + + + {i18n.INDEX_PATTERNS_ADVANCED_OPTIONS_TITLE} + + {expandAdvancedOptions && } + + + + + {!isDetectionsSourcerer && ( + + + + + {i18n.INDEX_PATTERNS_RESET} + + + + + {i18n.SAVE_INDEX_PATTERNS} + + + + + )} + + ); diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/translations.ts b/x-pack/plugins/security_solution/public/common/components/sourcerer/translations.ts index 03fdc5d191719..fcf465ebfc9ef 100644 --- a/x-pack/plugins/security_solution/public/common/components/sourcerer/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/translations.ts @@ -7,29 +7,85 @@ import { i18n } from '@kbn/i18n'; -export const SOURCERER = i18n.translate('xpack.securitySolution.indexPatterns.dataSourcesLabel', { - defaultMessage: 'Data sources', +export const CALL_OUT_TITLE = i18n.translate('xpack.securitySolution.indexPatterns.callOutTitle', { + defaultMessage: 'Data view cannot be modified on this page', }); -export const SIEM_DATA_VIEW_LABEL = i18n.translate( - 'xpack.securitySolution.indexPatterns.kipLabel', +export const CALL_OUT_TIMELINE_TITLE = i18n.translate( + 'xpack.securitySolution.indexPatterns.callOutTimelineTitle', { - defaultMessage: 'Default Security Data View', + defaultMessage: 'Data view cannot be modified when show only detection alerts is selected', } ); -export const SELECT_INDEX_PATTERNS = i18n.translate('xpack.securitySolution.indexPatterns.help', { - defaultMessage: 'Data sources selection', +export const DATA_VIEW = i18n.translate('xpack.securitySolution.indexPatterns.dataViewLabel', { + defaultMessage: 'Data view', }); +export const MODIFIED_BADGE_TITLE = i18n.translate( + 'xpack.securitySolution.indexPatterns.modifiedBadgeTitle', + { + defaultMessage: 'Modified', + } +); + +export const ALERTS_BADGE_TITLE = i18n.translate( + 'xpack.securitySolution.indexPatterns.alertsBadgeTitle', + { + defaultMessage: 'Alerts', + } +); + +export const SECURITY_DEFAULT_DATA_VIEW_LABEL = i18n.translate( + 'xpack.securitySolution.indexPatterns.securityDefaultDataViewLabel', + { + defaultMessage: 'Security Default Data View', + } +); + +export const SIEM_SECURITY_DATA_VIEW_LABEL = i18n.translate( + 'xpack.securitySolution.indexPatterns.securityDataViewLabel', + { + defaultMessage: 'Security Data View', + } +); + +export const SELECT_DATA_VIEW = i18n.translate( + 'xpack.securitySolution.indexPatterns.selectDataView', + { + defaultMessage: 'Data view selection', + } +); export const SAVE_INDEX_PATTERNS = i18n.translate('xpack.securitySolution.indexPatterns.save', { defaultMessage: 'Save', }); -export const INDEX_PATTERNS_SELECTION_LABEL = i18n.translate( - 'xpack.securitySolution.indexPatterns.selectionLabel', +export const INDEX_PATTERNS_CHOOSE_DATA_VIEW_LABEL = i18n.translate( + 'xpack.securitySolution.indexPatterns.chooseDataViewLabel', + { + defaultMessage: 'Choose data view', + } +); + +export const INDEX_PATTERNS_ADVANCED_OPTIONS_TITLE = i18n.translate( + 'xpack.securitySolution.indexPatterns.advancedOptionsTitle', + { + defaultMessage: 'Advanced options', + } +); + +export const INDEX_PATTERNS_LABEL = i18n.translate( + 'xpack.securitySolution.indexPatterns.indexPatternsLabel', { - defaultMessage: 'Choose the source of the data on this page', + defaultMessage: 'Index patterns', + } +); + +export const INDEX_PATTERNS_DESCRIPTIONS = i18n.translate( + 'xpack.securitySolution.indexPatterns.descriptionsLabel', + { + defaultMessage: + 'These are the index patterns currently selected. Filtering out index patterns from your data view can help improve overall performance.', } ); @@ -54,3 +110,10 @@ export const PICK_INDEX_PATTERNS = i18n.translate( defaultMessage: 'Pick index patterns', } ); + +export const ALERTS_CHECKBOX_LABEL = i18n.translate( + 'xpack.securitySolution.indexPatterns.onlyDetectionAlertsLabel', + { + defaultMessage: 'Show only detection alerts', + } +); diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/use_pick_index_patterns.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/use_pick_index_patterns.tsx new file mode 100644 index 0000000000000..2ed2319499398 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/use_pick_index_patterns.tsx @@ -0,0 +1,167 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { EuiComboBoxOptionOption } from '@elastic/eui'; +import { getScopePatternListSelection } from '../../store/sourcerer/helpers'; +import { sourcererModel } from '../../store/sourcerer'; +import { getPatternListWithoutSignals } from './helpers'; +import { SourcererScopeName } from '../../store/sourcerer/model'; + +interface UsePickIndexPatternsProps { + dataViewId: string; + defaultDataViewId: string; + isOnlyDetectionAlerts: boolean; + kibanaDataViews: sourcererModel.SourcererModel['kibanaDataViews']; + scopeId: sourcererModel.SourcererScopeName; + selectedPatterns: string[]; + signalIndexName: string | null; +} + +export type ModifiedTypes = 'modified' | 'alerts' | ''; + +interface UsePickIndexPatterns { + isModified: ModifiedTypes; + onChangeCombo: (newSelectedDataViewId: Array>) => void; + renderOption: ({ value }: EuiComboBoxOptionOption) => React.ReactElement; + selectableOptions: Array>; + selectedOptions: Array>; + setIndexPatternsByDataView: (newSelectedDataViewId: string, isAlerts?: boolean) => void; +} + +const patternListToOptions = (patternList: string[], selectablePatterns?: string[]) => + patternList.sort().map((s) => ({ + label: s, + value: s, + ...(selectablePatterns != null ? { disabled: !selectablePatterns.includes(s) } : {}), + })); + +export const usePickIndexPatterns = ({ + dataViewId, + defaultDataViewId, + isOnlyDetectionAlerts, + kibanaDataViews, + scopeId, + selectedPatterns, + signalIndexName, +}: UsePickIndexPatternsProps): UsePickIndexPatterns => { + const alertsOptions = useMemo( + () => (signalIndexName ? patternListToOptions([signalIndexName]) : []), + [signalIndexName] + ); + + const { patternList, selectablePatterns } = useMemo(() => { + if (isOnlyDetectionAlerts && signalIndexName) { + return { + patternList: [signalIndexName], + selectablePatterns: [signalIndexName], + }; + } + const theDataView = kibanaDataViews.find((dataView) => dataView.id === dataViewId); + return theDataView != null + ? scopeId === sourcererModel.SourcererScopeName.default + ? { + patternList: getPatternListWithoutSignals( + theDataView.title + .split(',') + // remove duplicates patterns from selector + .filter((pattern, i, self) => self.indexOf(pattern) === i), + signalIndexName + ), + selectablePatterns: getPatternListWithoutSignals( + theDataView.patternList, + signalIndexName + ), + } + : { + patternList: theDataView.title + .split(',') + // remove duplicates patterns from selector + .filter((pattern, i, self) => self.indexOf(pattern) === i), + selectablePatterns: theDataView.patternList, + } + : { patternList: [], selectablePatterns: [] }; + }, [dataViewId, isOnlyDetectionAlerts, kibanaDataViews, scopeId, signalIndexName]); + + const selectableOptions = useMemo( + () => patternListToOptions(patternList, selectablePatterns), + [patternList, selectablePatterns] + ); + const [selectedOptions, setSelectedOptions] = useState>>( + isOnlyDetectionAlerts ? alertsOptions : patternListToOptions(selectedPatterns) + ); + + const getDefaultSelectedOptionsByDataView = useCallback( + (id: string, isAlerts: boolean = false): Array> => + scopeId === SourcererScopeName.detections || isAlerts + ? alertsOptions + : patternListToOptions( + getScopePatternListSelection( + kibanaDataViews.find((dataView) => dataView.id === id), + scopeId, + signalIndexName, + id === defaultDataViewId + ) + ), + [alertsOptions, kibanaDataViews, scopeId, signalIndexName, defaultDataViewId] + ); + + const defaultSelectedPatternsAsOptions = useMemo( + () => getDefaultSelectedOptionsByDataView(dataViewId), + [dataViewId, getDefaultSelectedOptionsByDataView] + ); + + const [isModified, setIsModified] = useState<'modified' | 'alerts' | ''>(''); + const onSetIsModified = useCallback( + (patterns?: string[]) => { + if (isOnlyDetectionAlerts) { + return setIsModified('alerts'); + } + const modifiedPatterns = patterns != null ? patterns : selectedPatterns; + const isPatternsModified = + defaultSelectedPatternsAsOptions.length !== modifiedPatterns.length || + !defaultSelectedPatternsAsOptions.every((option) => + modifiedPatterns.find((pattern) => option.value === pattern) + ); + return setIsModified(isPatternsModified ? 'modified' : ''); + }, + [defaultSelectedPatternsAsOptions, isOnlyDetectionAlerts, selectedPatterns] + ); + + // when scope updates, check modified to set/remove alerts label + useEffect(() => { + setSelectedOptions( + scopeId === SourcererScopeName.detections + ? alertsOptions + : patternListToOptions(selectedPatterns) + ); + onSetIsModified(selectedPatterns); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [scopeId, selectedPatterns]); + + const onChangeCombo = useCallback((newSelectedOptions) => { + setSelectedOptions(newSelectedOptions); + }, []); + + const renderOption = useCallback( + ({ value }) => {value}, + [] + ); + + const setIndexPatternsByDataView = (newSelectedDataViewId: string, isAlerts?: boolean) => { + setSelectedOptions(getDefaultSelectedOptionsByDataView(newSelectedDataViewId, isAlerts)); + }; + + return { + isModified, + onChangeCombo, + renderOption, + selectableOptions, + selectedOptions, + setIndexPatternsByDataView, + }; +}; diff --git a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx index 4ca8bf037261a..2edfc1336269f 100644 --- a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx @@ -13,7 +13,15 @@ import { sourcererActions, sourcererSelectors } from '../../store/sourcerer'; import { SelectedDataView, SourcererScopeName } from '../../store/sourcerer/model'; import { useUserInfo } from '../../../detections/components/user_info'; import { timelineSelectors } from '../../../timelines/store/timeline'; -import { ALERTS_PATH, CASES_PATH, RULES_PATH, UEBA_PATH } from '../../../../common/constants'; +import { + ALERTS_PATH, + CASES_PATH, + HOSTS_PATH, + NETWORK_PATH, + OVERVIEW_PATH, + RULES_PATH, + UEBA_PATH, +} from '../../../../common/constants'; import { TimelineId } from '../../../../common'; import { useDeepEqualSelector } from '../../hooks/use_selector'; import { getScopePatternListSelection } from '../../store/sourcerer/helpers'; @@ -300,3 +308,24 @@ export const getScopeFromPath = ( }) == null ? SourcererScopeName.default : SourcererScopeName.detections; + +export const sourcererPaths = [ + ALERTS_PATH, + `${RULES_PATH}/id/:id`, + HOSTS_PATH, + NETWORK_PATH, + OVERVIEW_PATH, + UEBA_PATH, +]; + +export const showSourcererByPath = (pathname: string): boolean => + matchPath(pathname, { + path: sourcererPaths, + strict: false, + }) != null; + +export const isAlertsOrRulesDetailsPage = (pathname: string): boolean => + matchPath(pathname, { + path: [ALERTS_PATH, `${RULES_PATH}/id/:id`], + strict: false, + }) != null; diff --git a/x-pack/plugins/security_solution/public/common/store/sourcerer/helpers.ts b/x-pack/plugins/security_solution/public/common/store/sourcerer/helpers.ts index 1b4efa72127f3..c99ed720c7f00 100644 --- a/x-pack/plugins/security_solution/public/common/store/sourcerer/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/store/sourcerer/helpers.ts @@ -37,15 +37,7 @@ export const getScopePatternListSelection = ( // set to signalIndexName whether or not it exists yet in the patternList return (signalIndexName != null ? [signalIndexName] : []).sort(); case SourcererScopeName.timeline: - return ( - signalIndexName != null - ? [ - // remove signalIndexName in case its already in there and add it whether or not it exists yet in the patternList - ...patternList.filter((index) => index !== signalIndexName), - signalIndexName, - ] - : patternList - ).sort(); + return patternList.sort(); } }; @@ -96,16 +88,14 @@ export const validateSelectedPatterns = ( selectedDataViewId: dataView?.id ?? null, selectedPatterns, ...(isEmpty(selectedPatterns) - ? id === SourcererScopeName.timeline - ? defaultDataViewByEventType({ state, eventType }) - : { - selectedPatterns: getScopePatternListSelection( - dataView ?? state.defaultDataView, - id, - state.signalIndexName, - (dataView ?? state.defaultDataView).id === state.defaultDataView.id - ), - } + ? { + selectedPatterns: getScopePatternListSelection( + dataView ?? state.defaultDataView, + id, + state.signalIndexName, + (dataView ?? state.defaultDataView).id === state.defaultDataView.id + ), + } : {}), loading: false, }, diff --git a/x-pack/plugins/security_solution/public/detections/components/detection_engine_header_page/index.tsx b/x-pack/plugins/security_solution/public/detections/components/detection_engine_header_page/index.tsx index 92911ab285375..44f27b690fbc7 100644 --- a/x-pack/plugins/security_solution/public/detections/components/detection_engine_header_page/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/detection_engine_header_page/index.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { HeaderPage, HeaderPageProps } from '../../../common/components/header_page'; const DetectionEngineHeaderPageComponent: React.FC = (props) => ( - + ); export const DetectionEngineHeaderPage = React.memo(DetectionEngineHeaderPageComponent); diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx index 3a98f062db65d..67ee6c55ac06f 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx @@ -28,8 +28,6 @@ import { EndpointNotice } from '../components/endpoint_notice'; import { useMessagesStorage } from '../../common/containers/local_storage/use_messages_storage'; import { ENDPOINT_METADATA_INDEX } from '../../../common/constants'; import { useSourcererDataView } from '../../common/containers/sourcerer'; -import { Sourcerer } from '../../common/components/sourcerer'; -import { SourcererScopeName } from '../../common/store/sourcerer/model'; import { useDeepEqualSelector } from '../../common/hooks/use_selector'; import { ThreatIntelLinkPanel } from '../components/overview_cti_links'; import { useIsThreatIntelModuleEnabled } from '../containers/overview_cti_links/use_is_threat_intel_module_enabled'; @@ -96,7 +94,6 @@ const OverviewComponent = () => { )} - diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx index 6cb0e6f2e7982..4bd963b21a7f1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/eql_tab_content/index.tsx @@ -42,7 +42,6 @@ import { requiredFieldsForActions } from '../../../../detections/components/aler import { ExitFullScreen } from '../../../../common/components/exit_full_screen'; import { SuperDatePicker } from '../../../../common/components/super_date_picker'; import { EventDetailsWidthProvider } from '../../../../common/components/events_viewer/event_details_width_context'; -import { PickEventType } from '../search_or_filter/pick_events'; import { inputsModel, inputsSelectors, State } from '../../../../common/store'; import { sourcererActions } from '../../../../common/store/sourcerer'; import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; @@ -57,6 +56,7 @@ import { DetailsPanel } from '../../side_panel'; import { EqlQueryBarTimeline } from '../query_bar/eql'; import { defaultControlColumn } from '../body/control_columns'; import { Sort } from '../body/sort'; +import { Sourcerer } from '../../../../common/components/sourcerer'; const TimelineHeaderContainer = styled.div` margin-top: 6px; @@ -283,10 +283,7 @@ export const EqlTabContentComponent: React.FC = ({ - + diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx index 5f6f2796d4ba9..6d53e7194306c 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/query_tab_content/index.tsx @@ -46,7 +46,6 @@ import { import { requiredFieldsForActions } from '../../../../detections/components/alerts_table/default_config'; import { SuperDatePicker } from '../../../../common/components/super_date_picker'; import { EventDetailsWidthProvider } from '../../../../common/components/events_viewer/event_details_width_context'; -import { PickEventType } from '../search_or_filter/pick_events'; import { inputsModel, inputsSelectors, State } from '../../../../common/store'; import { sourcererActions } from '../../../../common/store/sourcerer'; import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; @@ -61,6 +60,7 @@ import { DetailsPanel } from '../../side_panel'; import { ExitFullScreen } from '../../../../common/components/exit_full_screen'; import { defaultControlColumn } from '../body/control_columns'; import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; +import { Sourcerer } from '../../../../common/components/sourcerer'; const TimelineHeaderContainer = styled.div` margin-top: 6px; @@ -358,10 +358,7 @@ export const QueryTabContentComponent: React.FC = ({ - + diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.test.tsx deleted file mode 100644 index 47ea0f781f7c3..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.test.tsx +++ /dev/null @@ -1,220 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { fireEvent, render, within } from '@testing-library/react'; -import { EuiToolTip } from '@elastic/eui'; - -import React from 'react'; -import { PickEventType } from './pick_events'; -import { - createSecuritySolutionStorageMock, - kibanaObservable, - mockGlobalState, - mockSourcererState, - SUB_PLUGINS_REDUCER, - TestProviders, -} from '../../../../common/mock'; -import { TimelineEventsType } from '../../../../../common'; -import { createStore } from '../../../../common/store'; -import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; - -jest.mock('@elastic/eui', () => { - const actual = jest.requireActual('@elastic/eui'); - return { - ...actual, - EuiToolTip: jest.fn(), - }; -}); - -describe('Pick Events/Timeline Sourcerer', () => { - const defaultProps = { - eventType: 'all' as TimelineEventsType, - onChangeEventTypeAndIndexesName: jest.fn(), - }; - const initialPatterns = [ - ...mockSourcererState.defaultDataView.patternList.filter( - (p) => p !== mockSourcererState.signalIndexName - ), - mockSourcererState.signalIndexName, - ]; - const { storage } = createSecuritySolutionStorageMock(); - - // const state = { - // ...mockGlobalState, - // sourcerer: { - // ...mockGlobalState.sourcerer, - // kibanaIndexPatterns: [ - // { id: '1234', title: 'auditbeat-*' }, - // { id: '9100', title: 'filebeat-*' }, - // { id: '9100', title: 'auditbeat-*,filebeat-*' }, - // { id: '5678', title: 'auditbeat-*,.siem-signals-default' }, - // ], - // configIndexPatterns: - // mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline].selectedPatterns, - // signalIndexName: mockGlobalState.sourcerer.signalIndexName, - // sourcererScopes: { - // ...mockGlobalState.sourcerer.sourcererScopes, - // [SourcererScopeName.timeline]: { - // ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline], - // loading: false, - // selectedPatterns: ['filebeat-*'], - // }, - // }, - // }, - // }; - // const store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); - const state = { - ...mockGlobalState, - sourcerer: { - ...mockGlobalState.sourcerer, - sourcererScopes: { - ...mockGlobalState.sourcerer.sourcererScopes, - [SourcererScopeName.timeline]: { - ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline], - loading: false, - selectedDataViewId: mockGlobalState.sourcerer.defaultDataView.id, - selectedPatterns: ['filebeat-*'], - }, - }, - }, - }; - const store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); - const mockTooltip = ({ - tooltipContent, - children, - }: { - tooltipContent: string; - children: React.ReactElement; - }) => ( -
    - {tooltipContent} - {children} -
    - ); - - beforeAll(() => { - (EuiToolTip as unknown as jest.Mock).mockImplementation(mockTooltip); - }); - beforeEach(() => { - jest.clearAllMocks(); - jest.restoreAllMocks(); - }); - it('renders', () => { - const wrapper = render( - - - - ); - fireEvent.click(wrapper.getByTestId('sourcerer-timeline-trigger')); - expect(wrapper.getByTestId('timeline-sourcerer').textContent).toEqual( - initialPatterns.sort().join('') - ); - fireEvent.click(wrapper.getByTestId(`sourcerer-accordion`)); - fireEvent.click(wrapper.getByTestId('comboBoxToggleListButton')); - const optionNodes = wrapper.getAllByTestId('sourcerer-option'); - expect(optionNodes.length).toBe(1); - }); - it('Removes duplicate options from options list', () => { - const store2 = createStore( - { - ...mockGlobalState, - sourcerer: { - ...mockGlobalState.sourcerer, - defaultDataView: { - ...mockGlobalState.sourcerer.defaultDataView, - id: '1234', - title: 'filebeat-*,auditbeat-*,auditbeat-*,auditbeat-*,auditbeat-*', - patternList: ['filebeat-*', 'auditbeat-*'], - }, - kibanaDataViews: [ - { - ...mockGlobalState.sourcerer.defaultDataView, - id: '1234', - title: 'filebeat-*,auditbeat-*,auditbeat-*,auditbeat-*,auditbeat-*', - patternList: ['filebeat-*', 'auditbeat-*'], - }, - ], - sourcererScopes: { - ...mockGlobalState.sourcerer.sourcererScopes, - [SourcererScopeName.timeline]: { - ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline], - loading: false, - selectedDataViewId: '1234', - selectedPatterns: ['filebeat-*'], - }, - }, - }, - }, - SUB_PLUGINS_REDUCER, - kibanaObservable, - storage - ); - const wrapper = render( - - - - ); - fireEvent.click(wrapper.getByTestId(`sourcerer-timeline-trigger`)); - fireEvent.click(wrapper.getByTestId(`sourcerer-accordion`)); - fireEvent.click(wrapper.getByTestId(`comboBoxToggleListButton`)); - expect( - wrapper.getByTestId('comboBoxOptionsList timeline-sourcerer-optionsList').textContent - ).toEqual('auditbeat-*'); - }); - - it('renders tooltip', () => { - render( - - - - ); - - expect((EuiToolTip as unknown as jest.Mock).mock.calls[0][0].content).toEqual( - initialPatterns - .filter((p) => p != null) - .sort() - .join(', ') - ); - }); - - it('renders popover button inside tooltip', () => { - const wrapper = render( - - - - ); - const tooltip = wrapper.getByTestId('timeline-sourcerer-tooltip'); - expect(within(tooltip).getByTestId('sourcerer-timeline-trigger')).toBeTruthy(); - }); - - it('correctly filters options', () => { - const wrapper = render( - - - - ); - fireEvent.click(wrapper.getByTestId('sourcerer-timeline-trigger')); - fireEvent.click(wrapper.getByTestId('comboBoxToggleListButton')); - fireEvent.click(wrapper.getByTestId('sourcerer-accordion')); - const optionNodes = wrapper.getAllByTestId('sourcerer-option'); - expect(optionNodes.length).toBe(9); - }); - it('reset button works', () => { - const wrapper = render( - - - - ); - fireEvent.click(wrapper.getByTestId('sourcerer-timeline-trigger')); - expect(wrapper.getByTestId('timeline-sourcerer').textContent).toEqual('filebeat-*'); - - fireEvent.click(wrapper.getByTestId('sourcerer-reset')); - expect(wrapper.getByTestId('timeline-sourcerer').textContent).toEqual( - initialPatterns.sort().join('') - ); - }); -}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.tsx deleted file mode 100644 index 791993d67135d..0000000000000 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.tsx +++ /dev/null @@ -1,465 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - EuiAccordion, - EuiButton, - EuiButtonEmpty, - EuiRadioGroup, - EuiComboBox, - EuiComboBoxOptionOption, - EuiHealth, - EuiIcon, - EuiFlexGroup, - EuiFlexItem, - EuiPopover, - EuiPopoverTitle, - EuiSpacer, - EuiText, - EuiToolTip, - EuiSuperSelect, -} from '@elastic/eui'; -import deepEqual from 'fast-deep-equal'; -import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; -import styled from 'styled-components'; - -import { sourcererSelectors } from '../../../../common/store'; -import { SourcererScopeName } from '../../../../common/store/sourcerer/model'; -import { TimelineEventsType } from '../../../../../common'; -import * as i18n from './translations'; -import { getScopePatternListSelection } from '../../../../common/store/sourcerer/helpers'; -import { SIEM_DATA_VIEW_LABEL } from '../../../../common/components/sourcerer/translations'; -import { useDeepEqualSelector } from '../../../../common/hooks/use_selector'; - -const PopoverContent = styled.div` - width: 600px; -`; - -const ResetButton = styled(EuiButtonEmpty)` - width: fit-content; -`; - -const MyEuiButton = styled(EuiButton)` - .euiHealth { - vertical-align: middle; - } -`; - -const AllEuiHealth = styled(EuiHealth)` - margin-left: -2px; - svg { - stroke: #fff; - stroke-width: 1px; - stroke-linejoin: round; - width: 19px; - height: 19px; - margin-top: 1px; - z-index: 1; - } -`; - -const WarningEuiHealth = styled(EuiHealth)` - margin-left: -17px; - svg { - z-index: 0; - } -`; - -const AdvancedSettings = styled(EuiText)` - color: ${({ theme }) => theme.eui.euiColorPrimary}; -`; - -const ConfigHelper = styled(EuiText)` - margin-left: 4px; -`; - -const Filter = styled(EuiRadioGroup)` - margin-left: 4px; -`; - -const PickEventContainer = styled.div` - .euiSuperSelect { - width: 170px; - max-width: 170px; - button.euiSuperSelectControl { - padding-top: 3px; - } - } -`; - -const getEventTypeOptions = (isCustomDisabled: boolean = true, isDefaultPattern: boolean) => [ - { - id: 'all', - label: ( - - {i18n.ALL_EVENT} - - ), - }, - { - id: 'raw', - label: {i18n.RAW_EVENT}, - disabled: !isDefaultPattern, - }, - { - id: 'alert', - label: {i18n.DETECTION_ALERTS_EVENT}, - disabled: !isDefaultPattern, - }, - { - id: 'custom', - label: <>{i18n.CUSTOM_INDEX_PATTERNS}, - disabled: isCustomDisabled, - }, -]; - -interface PickEventTypeProps { - eventType: TimelineEventsType; - onChangeEventTypeAndIndexesName: ( - value: TimelineEventsType, - indexNames: string[], - dataViewId: string - ) => void; -} - -// AKA TimelineSourcerer -const PickEventTypeComponents: React.FC = ({ - eventType = 'all', - onChangeEventTypeAndIndexesName, -}) => { - const [isPopoverOpen, setPopover] = useState(false); - const [showAdvanceSettings, setAdvanceSettings] = useState(eventType === 'custom'); - const [filterEventType, setFilterEventType] = useState(eventType); - const sourcererScopeSelector = useMemo(() => sourcererSelectors.getSourcererScopeSelector(), []); - const { - defaultDataView, - kibanaDataViews, - signalIndexName, - sourcererScope: { loading, selectedPatterns, selectedDataViewId }, - }: sourcererSelectors.SourcererScopeSelector = useDeepEqualSelector((state) => - sourcererScopeSelector(state, SourcererScopeName.timeline) - ); - - const [dataViewId, setDataViewId] = useState(selectedDataViewId ?? ''); - const { patternList, selectablePatterns } = useMemo(() => { - const theDataView = kibanaDataViews.find((dataView) => dataView.id === dataViewId); - return theDataView != null - ? { - patternList: theDataView.title - .split(',') - // remove duplicates patterns from selector - .filter((pattern, i, self) => self.indexOf(pattern) === i), - selectablePatterns: theDataView.patternList, - } - : { patternList: [], selectablePatterns: [] }; - }, [kibanaDataViews, dataViewId]); - const [selectedOptions, setSelectedOptions] = useState>>( - selectedPatterns.map((indexName) => ({ - label: indexName, - value: indexName, - })) - ); - const isSavingDisabled = useMemo(() => selectedOptions.length === 0, [selectedOptions]); - const selectableOptions = useMemo( - () => - patternList.map((indexName) => ({ - label: indexName, - value: indexName, - 'data-test-subj': 'sourcerer-option', - disabled: !selectablePatterns.includes(indexName), - })), - [selectablePatterns, patternList] - ); - - const onChangeFilter = useCallback( - (filter) => { - setFilterEventType(filter); - if (filter === 'all' || filter === 'kibana') { - setSelectedOptions( - selectablePatterns.map((indexSelected) => ({ - label: indexSelected, - value: indexSelected, - })) - ); - } else if (filter === 'raw') { - setSelectedOptions( - (signalIndexName == null - ? selectablePatterns - : selectablePatterns.filter((index) => index !== signalIndexName) - ).map((indexSelected) => ({ - label: indexSelected, - value: indexSelected, - })) - ); - } else if (filter === 'alert') { - setSelectedOptions([ - { - label: signalIndexName ?? '', - value: signalIndexName ?? '', - }, - ]); - } - }, - [selectablePatterns, signalIndexName] - ); - - const onChangeCombo = useCallback( - (newSelectedOptions: Array>) => { - const localSelectedPatterns = newSelectedOptions - .map((nso) => nso.label) - .sort() - .join(); - if (localSelectedPatterns === selectablePatterns.sort().join()) { - setFilterEventType('all'); - } else if ( - dataViewId === defaultDataView.id && - localSelectedPatterns === - selectablePatterns - .filter((index) => index !== signalIndexName) - .sort() - .join() - ) { - setFilterEventType('raw'); - } else if (dataViewId === defaultDataView.id && localSelectedPatterns === signalIndexName) { - setFilterEventType('alert'); - } else { - setFilterEventType('custom'); - } - - setSelectedOptions(newSelectedOptions); - }, - [defaultDataView.id, dataViewId, selectablePatterns, signalIndexName] - ); - - const onChangeSuper = useCallback( - (newSelectedOption) => { - setFilterEventType('all'); - setDataViewId(newSelectedOption); - setSelectedOptions( - getScopePatternListSelection( - kibanaDataViews.find((dataView) => dataView.id === newSelectedOption), - SourcererScopeName.timeline, - signalIndexName, - newSelectedOption === defaultDataView.id - ).map((indexSelected: string) => ({ - label: indexSelected, - value: indexSelected, - })) - ); - }, - [defaultDataView.id, kibanaDataViews, signalIndexName] - ); - - const togglePopover = useCallback( - () => setPopover((prevIsPopoverOpen) => !prevIsPopoverOpen), - [] - ); - - const closePopover = useCallback(() => setPopover(false), []); - - const handleSaveIndices = useCallback(() => { - onChangeEventTypeAndIndexesName( - filterEventType, - selectedOptions.map((so) => so.label), - dataViewId - ); - setPopover(false); - }, [dataViewId, filterEventType, onChangeEventTypeAndIndexesName, selectedOptions]); - - const resetDataSources = useCallback(() => { - setDataViewId(defaultDataView.id); - setSelectedOptions( - getScopePatternListSelection( - defaultDataView, - SourcererScopeName.timeline, - signalIndexName, - true - ).map((indexSelected: string) => ({ - label: indexSelected, - value: indexSelected, - })) - ); - setFilterEventType(eventType); - }, [defaultDataView, eventType, signalIndexName]); - - const dataViewSelectOptions = useMemo( - () => - kibanaDataViews.map(({ title, id }) => ({ - inputDisplay: - id === defaultDataView.id ? ( - - {SIEM_DATA_VIEW_LABEL} - - ) : ( - - {title} - - ), - value: id, - })), - [defaultDataView.id, kibanaDataViews] - ); - - const filterOptions = useMemo( - () => getEventTypeOptions(filterEventType !== 'custom', dataViewId === defaultDataView.id), - [defaultDataView.id, filterEventType, dataViewId] - ); - - const button = useMemo(() => { - const options = getEventTypeOptions(true, dataViewId === defaultDataView.id); - return ( - - {options.find((opt) => opt.id === eventType)?.label} - - ); - }, [defaultDataView.id, eventType, dataViewId, loading, togglePopover]); - - const tooltipContent = useMemo( - () => (isPopoverOpen ? null : selectedPatterns.sort().join(', ')), - [isPopoverOpen, selectedPatterns] - ); - - const buttonWithTooptip = useMemo(() => { - return tooltipContent ? ( - - {button} - - ) : ( - button - ); - }, [button, tooltipContent]); - - const ButtonContent = useMemo( - () => ( - - {showAdvanceSettings - ? i18n.HIDE_INDEX_PATTERNS_ADVANCED_SETTINGS - : i18n.SHOW_INDEX_PATTERNS_ADVANCED_SETTINGS} - - ), - [showAdvanceSettings] - ); - - useEffect(() => { - const newSelectedOptions = selectedPatterns.map((indexSelected) => ({ - label: indexSelected, - value: indexSelected, - })); - setSelectedOptions((prevSelectedOptions) => { - if (!deepEqual(newSelectedOptions, prevSelectedOptions)) { - return newSelectedOptions; - } - return prevSelectedOptions; - }); - }, [selectedPatterns]); - - useEffect(() => { - setFilterEventType((prevFilter) => (prevFilter !== eventType ? eventType : prevFilter)); - setAdvanceSettings(eventType === 'custom'); - }, [eventType]); - - return ( - - - - - <>{i18n.SELECT_INDEX_PATTERNS} - - - - - - <> - - - - - - - {!showAdvanceSettings && ( - <> - - - {i18n.CONFIGURE_INDEX_PATTERNS} - - - )} - - - - - {i18n.DATA_SOURCES_RESET} - - - - - {i18n.SAVE_INDEX_PATTERNS} - - - - - - - ); -}; - -export const PickEventType = memo(PickEventTypeComponents); diff --git a/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx b/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx index 3a9b9b0d2693e..e59af74d9a478 100644 --- a/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx +++ b/x-pack/plugins/security_solution/public/timelines/pages/timelines_page.tsx @@ -46,7 +46,7 @@ export const TimelinesPageComponent: React.FC = () => { {indicesExist ? ( <> - + {capabilitiesCanUserCRUD && ( @@ -93,6 +93,7 @@ export const TimelinesPageComponent: React.FC = () => { ) : ( + )} diff --git a/x-pack/plugins/security_solution/public/ueba/pages/details/index.tsx b/x-pack/plugins/security_solution/public/ueba/pages/details/index.tsx index 72122aba3c4aa..51c06fffb7b63 100644 --- a/x-pack/plugins/security_solution/public/ueba/pages/details/index.tsx +++ b/x-pack/plugins/security_solution/public/ueba/pages/details/index.tsx @@ -99,7 +99,6 @@ const UebaDetailsComponent: React.FC = ({ detailName, uebaDeta 高度な設定で構成できます。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 1a1c9b549fc5f..a575f9a1afdfd 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -22449,13 +22449,10 @@ "xpack.securitySolution.hostsTable.unit": "{totalCount, plural, other {个主机}}", "xpack.securitySolution.hostsTable.versionTitle": "版本", "xpack.securitySolution.hoverActions.showTopTooltip": "显示排名靠前的{fieldName}", - "xpack.securitySolution.indexPatterns.dataSourcesLabel": "数据源", "xpack.securitySolution.indexPatterns.disabled": "在此页面上建议使用已禁用的索引模式,但是首先需要在 Kibana 索引模式设置中配置这些模式", - "xpack.securitySolution.indexPatterns.help": "数据源的选择", "xpack.securitySolution.indexPatterns.pickIndexPatternsCombo": "选取索引模式", "xpack.securitySolution.indexPatterns.resetButton": "重置", "xpack.securitySolution.indexPatterns.save": "保存", - "xpack.securitySolution.indexPatterns.selectionLabel": "在此页面上选择数据源", "xpack.securitySolution.insert.timeline.insertTimelineButton": "插入时间线链接", "xpack.securitySolution.inspect.modal.closeTitle": "关闭", "xpack.securitySolution.inspect.modal.indexPatternDescription": "连接到 Elasticsearch 索引的索引模式。可以在“Kibana”>“高级设置”中配置这些索引。", From fe1e60fecb004bbdff08d9524d1e5057a404c674 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Wed, 10 Nov 2021 23:57:56 -0700 Subject: [PATCH 27/43] [Reporting] Fix unhandled promise rejection thrown when launching Chromium (#118119) (#118284) * [Reporting] Fix unhandled promise rejection thrown when launching Chromium * comment correction * simplify * unit test * Update x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts Co-authored-by: Michael Dokolin Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Michael Dokolin Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Michael Dokolin --- .../chromium/driver_factory/index.test.ts | 76 +++++++++++++++++++ .../browsers/chromium/driver_factory/index.ts | 46 +++++------ 2 files changed, 97 insertions(+), 25 deletions(-) create mode 100644 x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.test.ts diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.test.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.test.ts new file mode 100644 index 0000000000000..dae692fae8825 --- /dev/null +++ b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.test.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import puppeteer from 'puppeteer'; +import * as Rx from 'rxjs'; +import { take } from 'rxjs/operators'; +import { HeadlessChromiumDriverFactory } from '.'; +import type { ReportingCore } from '../../..'; +import { + createMockConfigSchema, + createMockLevelLogger, + createMockReportingCore, +} from '../../../test_helpers'; + +jest.mock('puppeteer'); + +const mock = (browserDriverFactory: HeadlessChromiumDriverFactory) => { + browserDriverFactory.getBrowserLogger = jest.fn(() => new Rx.Observable()); + browserDriverFactory.getProcessLogger = jest.fn(() => new Rx.Observable()); + browserDriverFactory.getPageExit = jest.fn(() => new Rx.Observable()); + return browserDriverFactory; +}; + +describe('class HeadlessChromiumDriverFactory', () => { + let reporting: ReportingCore; + const logger = createMockLevelLogger(); + const path = 'path/to/headless_shell'; + + beforeEach(async () => { + (puppeteer as jest.Mocked).launch.mockResolvedValue({ + newPage: jest.fn().mockResolvedValue({ + target: jest.fn(() => ({ + createCDPSession: jest.fn().mockResolvedValue({ + send: jest.fn(), + }), + })), + emulateTimezone: jest.fn(), + setDefaultTimeout: jest.fn(), + }), + close: jest.fn(), + process: jest.fn(), + } as unknown as puppeteer.Browser); + + reporting = await createMockReportingCore( + createMockConfigSchema({ + capture: { + browser: { chromium: { proxy: {} } }, + timeouts: { openUrl: 50000 }, + }, + }) + ); + }); + + it('createPage returns browser driver and process exit observable', async () => { + const factory = mock(new HeadlessChromiumDriverFactory(reporting, path, logger)); + const utils = await factory.createPage({}).pipe(take(1)).toPromise(); + expect(utils).toHaveProperty('driver'); + expect(utils).toHaveProperty('exit$'); + }); + + it('createPage rejects if Puppeteer launch fails', async () => { + (puppeteer as jest.Mocked).launch.mockRejectedValue( + `Puppeteer Launch mock fail.` + ); + const factory = mock(new HeadlessChromiumDriverFactory(reporting, path, logger)); + expect(() => + factory.createPage({}).pipe(take(1)).toPromise() + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Error spawning Chromium browser! Puppeteer Launch mock fail."` + ); + }); +}); diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts index 264e673d2bf74..2aef62f59985b 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts +++ b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts @@ -23,7 +23,7 @@ import { LevelLogger } from '../../../lib'; import { safeChildProcess } from '../../safe_child_process'; import { HeadlessChromiumDriver } from '../driver'; import { args } from './args'; -import { getMetrics, Metrics } from './metrics'; +import { getMetrics } from './metrics'; type BrowserConfig = CaptureConfig['browser']['chromium']; @@ -35,7 +35,7 @@ export class HeadlessChromiumDriverFactory { private getChromiumArgs: () => string[]; private core: ReportingCore; - constructor(core: ReportingCore, binaryPath: string, logger: LevelLogger) { + constructor(core: ReportingCore, binaryPath: string, private logger: LevelLogger) { this.core = core; this.binaryPath = binaryPath; const config = core.getConfig(); @@ -62,7 +62,7 @@ export class HeadlessChromiumDriverFactory { */ createPage( { browserTimezone }: { browserTimezone?: string }, - pLogger: LevelLogger + pLogger = this.logger ): Rx.Observable<{ driver: HeadlessChromiumDriver; exit$: Rx.Observable }> { // FIXME: 'create' is deprecated return Rx.Observable.create(async (observer: InnerSubscriber) => { @@ -72,10 +72,7 @@ export class HeadlessChromiumDriverFactory { const chromiumArgs = this.getChromiumArgs(); logger.debug(`Chromium launch args set to: ${chromiumArgs}`); - let browser: puppeteer.Browser; - let page: puppeteer.Page; - let devTools: puppeteer.CDPSession | undefined; - let startMetrics: Metrics | undefined; + let browser: puppeteer.Browser | null = null; try { browser = await puppeteer.launch({ @@ -89,29 +86,28 @@ export class HeadlessChromiumDriverFactory { TZ: browserTimezone, }, }); + } catch (err) { + observer.error(new Error(`Error spawning Chromium browser! ${err}`)); + return; + } - page = await browser.newPage(); - devTools = await page.target().createCDPSession(); + const page = await browser.newPage(); + const devTools = await page.target().createCDPSession(); - await devTools.send('Performance.enable', { timeDomain: 'timeTicks' }); - startMetrics = await devTools.send('Performance.getMetrics'); + await devTools.send('Performance.enable', { timeDomain: 'timeTicks' }); + const startMetrics = await devTools.send('Performance.getMetrics'); - // Log version info for debugging / maintenance - const versionInfo = await devTools.send('Browser.getVersion'); - logger.debug(`Browser version: ${JSON.stringify(versionInfo)}`); + // Log version info for debugging / maintenance + const versionInfo = await devTools.send('Browser.getVersion'); + logger.debug(`Browser version: ${JSON.stringify(versionInfo)}`); - await page.emulateTimezone(browserTimezone); + await page.emulateTimezone(browserTimezone); - // Set the default timeout for all navigation methods to the openUrl timeout (30 seconds) - // All waitFor methods have their own timeout config passed in to them - page.setDefaultTimeout(durationToNumber(this.captureConfig.timeouts.openUrl)); + // Set the default timeout for all navigation methods to the openUrl timeout + // All waitFor methods have their own timeout config passed in to them + page.setDefaultTimeout(durationToNumber(this.captureConfig.timeouts.openUrl)); - logger.debug(`Browser page driver created`); - } catch (err) { - observer.error(new Error(`Error spawning Chromium browser!`)); - observer.error(err); - throw err; - } + logger.debug(`Browser page driver created`); const childProcess = { async kill() { @@ -134,7 +130,7 @@ export class HeadlessChromiumDriverFactory { } try { - await browser.close(); + await browser?.close(); } catch (err) { // do not throw logger.error(err); From 01ec98c8f4f1e1d7898f3ee8bb18efa9928a59c6 Mon Sep 17 00:00:00 2001 From: Yuliia Naumenko Date: Wed, 10 Nov 2021 23:06:37 -0800 Subject: [PATCH 28/43] [8.0] [Alerting UI] Fixed bug Search is not reset in Create rule flyout (#117807) (#117892) * [Alerting UI] Fixed bug Search is not reset in Create rule flyout (#117807) * [Alerting UI] Fixed bug Search is not reset in Create rule flyout * fixed types * fixed test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../sections/alert_form/alert_form.tsx | 7 ++++++- .../alert_create_flyout.ts | 20 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx index 3f6bf3c955e8b..24bd266179c6f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx @@ -764,7 +764,12 @@ export const AlertForm = ({ setInputText(e.target.value)} + onChange={(e) => { + setInputText(e.target.value); + if (e.target.value === '') { + setSearchText(''); + } + }} onKeyUp={(e) => { if (e.keyCode === ENTER_KEY) { setSearchText(inputText); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts index 51aae6f4d134c..b208826ec7aa4 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alert_create_flyout.ts @@ -18,6 +18,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const find = getService('find'); const retry = getService('retry'); const comboBox = getService('comboBox'); + const browser = getService('browser'); async function getAlertsByName(name: string) { const { @@ -291,5 +292,24 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await testSubjects.missingOrFail('testQuerySuccess'); await testSubjects.existOrFail('testQueryError'); }); + + it('should show all rule types on click euiFormControlLayoutClearButton', async () => { + await pageObjects.triggersActionsUI.clickCreateAlertButton(); + await testSubjects.setValue('alertNameInput', 'alertName'); + const ruleTypeSearchBox = await find.byCssSelector('[data-test-subj="alertSearchField"]'); + await ruleTypeSearchBox.type('notexisting rule type'); + await ruleTypeSearchBox.pressKeys(browser.keys.ENTER); + + const ruleTypes = await find.allByCssSelector('.triggersActionsUI__alertTypeNodeHeading'); + expect(ruleTypes).to.have.length(0); + + const searchClearButton = await find.byCssSelector('.euiFormControlLayoutClearButton'); + await searchClearButton.click(); + + const ruleTypesClearFilter = await find.allByCssSelector( + '.triggersActionsUI__alertTypeNodeHeading' + ); + expect(ruleTypesClearFilter.length).to.above(0); + }); }); }; From 99b22070ee1364247a4292bed8c474635c39aefd Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 11 Nov 2021 02:12:02 -0500 Subject: [PATCH 29/43] [CI] Delete node_modules in between bootstrap attempts (#117588) (#118279) Co-authored-by: Brian Seeders --- .buildkite/scripts/bootstrap.sh | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.buildkite/scripts/bootstrap.sh b/.buildkite/scripts/bootstrap.sh index df38c105d2fd3..272cd0a086170 100755 --- a/.buildkite/scripts/bootstrap.sh +++ b/.buildkite/scripts/bootstrap.sh @@ -6,7 +6,17 @@ source .buildkite/scripts/common/util.sh source .buildkite/scripts/common/setup_bazel.sh echo "--- yarn install and bootstrap" -retry 2 15 yarn kbn bootstrap +if ! yarn kbn bootstrap; then + echo "bootstrap failed, trying again in 15 seconds" + sleep 15 + + # Most bootstrap failures will result in a problem inside node_modules that does not get fixed on the next bootstrap + # So, we should just delete node_modules in between attempts + rm -rf node_modules + + echo "--- yarn install and bootstrap, attempt 2" + yarn kbn bootstrap +fi ### ### upload ts-refs-cache artifacts as quickly as possible so they are available for download From 9ad458ac64e4e189bde888a47a5c7fefe960636b Mon Sep 17 00:00:00 2001 From: Michael Dokolin Date: Thu, 11 Nov 2021 08:45:41 +0100 Subject: [PATCH 30/43] [Reporting] Fix task manager not to run tasks before Kibana is available (#118206) (#118287) --- x-pack/plugins/reporting/server/core.ts | 18 +++++++++++++++++- x-pack/plugins/reporting/server/plugin.ts | 2 +- .../create_mock_reportingplugin.ts | 3 ++- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/reporting/server/core.ts b/x-pack/plugins/reporting/server/core.ts index e89ba6af3e28f..bc74f5463ba33 100644 --- a/x-pack/plugins/reporting/server/core.ts +++ b/x-pack/plugins/reporting/server/core.ts @@ -7,7 +7,7 @@ import Hapi from '@hapi/hapi'; import * as Rx from 'rxjs'; -import { first, map, take } from 'rxjs/operators'; +import { filter, first, map, take } from 'rxjs/operators'; import { ScreenshotModePluginSetup } from 'src/plugins/screenshot_mode/server'; import { BasePath, @@ -17,6 +17,8 @@ import { PluginInitializerContext, SavedObjectsClientContract, SavedObjectsServiceStart, + ServiceStatusLevels, + StatusServiceSetup, UiSettingsServiceStart, } from '../../../../src/core/server'; import { PluginStart as DataPluginStart } from '../../../../src/plugins/data/server'; @@ -44,6 +46,7 @@ export interface ReportingInternalSetup { taskManager: TaskManagerSetupContract; screenshotMode: ScreenshotModePluginSetup; logger: LevelLogger; + status: StatusServiceSetup; } export interface ReportingInternalStart { @@ -111,12 +114,25 @@ export class ReportingCore { this.pluginStart$.next(startDeps); // trigger the observer this.pluginStartDeps = startDeps; // cache + await this.assertKibanaIsAvailable(); + const { taskManager } = startDeps; const { executeTask, monitorTask } = this; // enable this instance to generate reports and to monitor for pending reports await Promise.all([executeTask.init(taskManager), monitorTask.init(taskManager)]); } + private async assertKibanaIsAvailable(): Promise { + const { status } = this.getPluginSetupDeps(); + + await status.overall$ + .pipe( + filter((current) => current.level === ServiceStatusLevels.available), + first() + ) + .toPromise(); + } + /* * Blocks the caller until setup is done */ diff --git a/x-pack/plugins/reporting/server/plugin.ts b/x-pack/plugins/reporting/server/plugin.ts index 07d61ff1630fc..8969a698a8ce4 100644 --- a/x-pack/plugins/reporting/server/plugin.ts +++ b/x-pack/plugins/reporting/server/plugin.ts @@ -52,7 +52,6 @@ export class ReportingPlugin const router = http.createRouter(); const basePath = http.basePath; - reportingCore.pluginSetup({ screenshotMode, features, @@ -63,6 +62,7 @@ export class ReportingPlugin spaces, taskManager, logger: this.logger, + status: core.status, }); registerUiSettings(core); diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts index d62cc750ccfcc..c05b2c54aeabf 100644 --- a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts +++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts @@ -11,7 +11,7 @@ jest.mock('../browsers'); import _ from 'lodash'; import * as Rx from 'rxjs'; -import { coreMock, elasticsearchServiceMock } from 'src/core/server/mocks'; +import { coreMock, elasticsearchServiceMock, statusServiceMock } from 'src/core/server/mocks'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { dataPluginMock } from 'src/plugins/data/server/mocks'; import { FieldFormatsRegistry } from 'src/plugins/field_formats/common'; @@ -45,6 +45,7 @@ export const createMockPluginSetup = (setupMock?: any): ReportingInternalSetup = licensing: { license$: Rx.of({ isAvailable: true, isActive: true, type: 'basic' }) } as any, taskManager: taskManagerMock.createSetup(), logger: createMockLevelLogger(), + status: statusServiceMock.createSetupContract(), ...setupMock, }; }; From c0e289ccb4a5791c06861e5bb3c9cb777adfa702 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 11 Nov 2021 03:49:08 -0500 Subject: [PATCH 31/43] [Lens] fix passing 0 as static value (#118032) (#118291) * [Lens] fix passing 0 as static value * allow computed static_value to be passed * Update x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.tsx Co-authored-by: Marco Liberati * ci fix Co-authored-by: Marco Liberati Co-authored-by: Marta Bondyra Co-authored-by: Marco Liberati --- .../indexpattern.test.ts | 2 +- .../definitions/static_value.test.tsx | 30 +++++++++++++++++++ .../operations/definitions/static_value.tsx | 4 +-- .../reference_line_helpers.test.ts | 26 ++++++++++++++++ .../reference_line_helpers.tsx | 15 +++++----- 5 files changed, 66 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts index 1dfc7d40f6f3e..5a405bb2328eb 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.test.ts @@ -1696,7 +1696,7 @@ describe('IndexPattern Data Source', () => { isBucketed: false, label: 'Static value: 0', operationType: 'static_value', - params: { value: 0 }, + params: { value: '0' }, references: [], scale: 'ratio', }, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.test.tsx index 1c574fe69611c..816324f9f8fb5 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.test.tsx @@ -338,6 +338,36 @@ describe('static_value', () => { expect(input.prop('value')).toEqual('23'); }); + it('should allow 0 as initial value', () => { + const updateLayerSpy = jest.fn(); + const zeroLayer = { + ...layer, + columns: { + ...layer.columns, + col2: { + ...layer.columns.col2, + operationType: 'static_value', + references: [], + params: { + value: '0', + }, + }, + }, + } as IndexPatternLayer; + const instance = shallow( + + ); + + const input = instance.find('[data-test-subj="lns-indexPattern-static_value-input"]'); + expect(input.prop('value')).toEqual('0'); + }); + it('should update state on change', async () => { const updateLayerSpy = jest.fn(); const instance = mount( diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx index 26be4e7b114da..d663358e62a84 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx @@ -95,7 +95,7 @@ export const staticValueOperation: OperationDefinition< arguments: { id: [columnId], name: [label || defaultLabel], - expression: [isValidNumber(params.value) ? params.value! : String(defaultValue)], + expression: [String(isValidNumber(params.value) ? params.value! : defaultValue)], }, }, ]; @@ -118,7 +118,7 @@ export const staticValueOperation: OperationDefinition< operationType: 'static_value', isBucketed: false, scale: 'ratio', - params: { ...previousParams, value: previousParams.value ?? String(defaultValue) }, + params: { ...previousParams, value: String(previousParams.value ?? defaultValue) }, references: [], }; }, diff --git a/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.test.ts b/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.test.ts index 42caca7fa2e09..9f48b8c8c36e4 100644 --- a/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.test.ts @@ -120,6 +120,32 @@ describe('reference_line helpers', () => { ).toBe(100); }); + it('should return 0 as result of calculation', () => { + expect( + getStaticValue( + [ + { + layerId: 'id-a', + seriesType: 'area', + layerType: 'data', + accessors: ['a'], + yConfig: [{ forAccessor: 'a', axisMode: 'right' }], + } as XYLayerConfig, + ], + 'yRight', + { + activeData: getActiveData([ + { + id: 'id-a', + rows: [{ a: -30 }, { a: 10 }], + }, + ]), + }, + hasAllNumberHistogram + ) + ).toBe(0); + }); + it('should work for no yConfig defined and fallback to left axis', () => { expect( getStaticValue( diff --git a/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.tsx b/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.tsx index 53a2d4bcc7222..127bf02b81f89 100644 --- a/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.tsx @@ -104,15 +104,14 @@ export function getStaticValue( ) { return fallbackValue; } - return ( - computeStaticValueForGroup( - filteredLayers, - accessors, - activeData, - groupId !== 'x', // histogram axis should compute the min based on the current data - groupId !== 'x' - ) || fallbackValue + const computedValue = computeStaticValueForGroup( + filteredLayers, + accessors, + activeData, + groupId !== 'x', // histogram axis should compute the min based on the current data + groupId !== 'x' ); + return computedValue ?? fallbackValue; } function getAccessorCriteriaForGroup( From 7e695cda589ea65d945e3db9d7d31384b30eabec Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 11 Nov 2021 04:59:16 -0500 Subject: [PATCH 32/43] Only set ignored_throttled when required (#118185) (#118295) Co-authored-by: Tim Roes --- x-pack/plugins/graph/server/routes/search.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/graph/server/routes/search.ts b/x-pack/plugins/graph/server/routes/search.ts index 2f792dd399ccf..92f3d7a02b072 100644 --- a/x-pack/plugins/graph/server/routes/search.ts +++ b/x-pack/plugins/graph/server/routes/search.ts @@ -49,7 +49,7 @@ export function registerSearchRoute({ index: request.body.index, body: request.body.body, track_total_hits: true, - ignore_throttled: !includeFrozen, + ...(includeFrozen ? { ignore_throttled: false } : {}), }) ).body, }, From 51956998de9959f3cd13974257df9a9ceb1454d5 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 11 Nov 2021 05:18:12 -0500 Subject: [PATCH 33/43] [Fleet] Package telemetry (#117978) (#118298) * renamed upgrade event * added package update events * fixed tests * fixed tests * fixed for async flow * fixed jest test * added unit tests * changed to logger.debug in sender.ts Co-authored-by: Julia Bardi <90178898+juliaElastic@users.noreply.github.com> --- .../services/epm/packages/install.test.ts | 252 ++++++++++++++++++ .../server/services/epm/packages/install.ts | 66 ++++- .../server/services/package_policy.test.ts | 2 +- .../fleet/server/services/package_policy.ts | 22 +- ...e_usage.test.ts => upgrade_sender.test.ts} | 24 +- .../{upgrade_usage.ts => upgrade_sender.ts} | 30 ++- .../fleet/server/telemetry/sender.test.ts | 26 +- .../plugins/fleet/server/telemetry/sender.ts | 4 +- .../plugins/fleet/server/telemetry/types.ts | 4 +- 9 files changed, 389 insertions(+), 41 deletions(-) create mode 100644 x-pack/plugins/fleet/server/services/epm/packages/install.test.ts rename x-pack/plugins/fleet/server/services/{upgrade_usage.test.ts => upgrade_sender.test.ts} (73%) rename x-pack/plugins/fleet/server/services/{upgrade_usage.ts => upgrade_sender.ts} (69%) diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/install.test.ts new file mode 100644 index 0000000000000..a9bb235c22cb8 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/epm/packages/install.test.ts @@ -0,0 +1,252 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { savedObjectsClientMock } from 'src/core/server/mocks'; + +import type { ElasticsearchClient } from 'kibana/server'; + +import * as Registry from '../registry'; + +import { sendTelemetryEvents } from '../../upgrade_sender'; + +import { licenseService } from '../../license'; + +import { installPackage } from './install'; +import * as install from './_install_package'; +import * as obj from './index'; + +jest.mock('../../app_context', () => { + return { + appContextService: { + getLogger: jest.fn(() => { + return { error: jest.fn(), debug: jest.fn(), warn: jest.fn() }; + }), + getTelemetryEventsSender: jest.fn(), + }, + }; +}); +jest.mock('./index'); +jest.mock('../registry'); +jest.mock('../../upgrade_sender'); +jest.mock('../../license'); +jest.mock('../../upgrade_sender'); +jest.mock('./cleanup'); +jest.mock('./_install_package', () => { + return { + _installPackage: jest.fn(() => Promise.resolve()), + }; +}); +jest.mock('../kibana/index_pattern/install', () => { + return { + installIndexPatterns: jest.fn(() => Promise.resolve()), + }; +}); +jest.mock('../archive', () => { + return { + parseAndVerifyArchiveEntries: jest.fn(() => + Promise.resolve({ packageInfo: { name: 'apache', version: '1.3.0' } }) + ), + unpackBufferToCache: jest.fn(), + setPackageInfo: jest.fn(), + }; +}); + +describe('install', () => { + beforeEach(() => { + jest.spyOn(Registry, 'splitPkgKey').mockImplementation((pkgKey: string) => { + const [pkgName, pkgVersion] = pkgKey.split('-'); + return { pkgName, pkgVersion }; + }); + jest + .spyOn(Registry, 'fetchFindLatestPackage') + .mockImplementation(() => Promise.resolve({ version: '1.3.0' } as any)); + jest + .spyOn(Registry, 'getRegistryPackage') + .mockImplementation(() => Promise.resolve({ packageInfo: { license: 'basic' } } as any)); + }); + + describe('registry', () => { + it('should send telemetry on install failure, out of date', async () => { + await installPackage({ + installSource: 'registry', + pkgkey: 'apache-1.1.0', + savedObjectsClient: savedObjectsClientMock.create(), + esClient: {} as ElasticsearchClient, + }); + + expect(sendTelemetryEvents).toHaveBeenCalledWith(expect.anything(), undefined, { + currentVersion: 'not_installed', + dryRun: false, + errorMessage: 'apache-1.1.0 is out-of-date and cannot be installed or updated', + eventType: 'package-install', + installType: 'install', + newVersion: '1.1.0', + packageName: 'apache', + status: 'failure', + }); + }); + + it('should send telemetry on install failure, license error', async () => { + jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(false); + await installPackage({ + installSource: 'registry', + pkgkey: 'apache-1.3.0', + savedObjectsClient: savedObjectsClientMock.create(), + esClient: {} as ElasticsearchClient, + }); + + expect(sendTelemetryEvents).toHaveBeenCalledWith(expect.anything(), undefined, { + currentVersion: 'not_installed', + dryRun: false, + errorMessage: 'Requires basic license', + eventType: 'package-install', + installType: 'install', + newVersion: '1.3.0', + packageName: 'apache', + status: 'failure', + }); + }); + + it('should send telemetry on install success', async () => { + jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(true); + await installPackage({ + installSource: 'registry', + pkgkey: 'apache-1.3.0', + savedObjectsClient: savedObjectsClientMock.create(), + esClient: {} as ElasticsearchClient, + }); + + expect(sendTelemetryEvents).toHaveBeenCalledWith(expect.anything(), undefined, { + currentVersion: 'not_installed', + dryRun: false, + eventType: 'package-install', + installType: 'install', + newVersion: '1.3.0', + packageName: 'apache', + status: 'success', + }); + }); + + it('should send telemetry on update success', async () => { + jest + .spyOn(obj, 'getInstallationObject') + .mockImplementationOnce(() => Promise.resolve({ attributes: { version: '1.2.0' } } as any)); + jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(true); + await installPackage({ + installSource: 'registry', + pkgkey: 'apache-1.3.0', + savedObjectsClient: savedObjectsClientMock.create(), + esClient: {} as ElasticsearchClient, + }); + + expect(sendTelemetryEvents).toHaveBeenCalledWith(expect.anything(), undefined, { + currentVersion: '1.2.0', + dryRun: false, + eventType: 'package-install', + installType: 'update', + newVersion: '1.3.0', + packageName: 'apache', + status: 'success', + }); + }); + + it('should send telemetry on install failure, async error', async () => { + jest + .spyOn(install, '_installPackage') + .mockImplementation(() => Promise.reject(new Error('error'))); + jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(true); + await installPackage({ + installSource: 'registry', + pkgkey: 'apache-1.3.0', + savedObjectsClient: savedObjectsClientMock.create(), + esClient: {} as ElasticsearchClient, + }); + + expect(sendTelemetryEvents).toHaveBeenCalledWith(expect.anything(), undefined, { + currentVersion: 'not_installed', + dryRun: false, + errorMessage: 'error', + eventType: 'package-install', + installType: 'install', + newVersion: '1.3.0', + packageName: 'apache', + status: 'failure', + }); + }); + }); + + describe('upload', () => { + it('should send telemetry on install failure', async () => { + jest + .spyOn(obj, 'getInstallationObject') + .mockImplementationOnce(() => Promise.resolve({ attributes: { version: '1.2.0' } } as any)); + await installPackage({ + installSource: 'upload', + archiveBuffer: {} as Buffer, + contentType: '', + savedObjectsClient: savedObjectsClientMock.create(), + esClient: {} as ElasticsearchClient, + }); + + expect(sendTelemetryEvents).toHaveBeenCalledWith(expect.anything(), undefined, { + currentVersion: '1.2.0', + dryRun: false, + errorMessage: + 'Package upload only supports fresh installations. Package apache is already installed, please uninstall first.', + eventType: 'package-install', + installType: 'update', + newVersion: '1.3.0', + packageName: 'apache', + status: 'failure', + }); + }); + + it('should send telemetry on install success', async () => { + await installPackage({ + installSource: 'upload', + archiveBuffer: {} as Buffer, + contentType: '', + savedObjectsClient: savedObjectsClientMock.create(), + esClient: {} as ElasticsearchClient, + }); + + expect(sendTelemetryEvents).toHaveBeenCalledWith(expect.anything(), undefined, { + currentVersion: 'not_installed', + dryRun: false, + eventType: 'package-install', + installType: 'install', + newVersion: '1.3.0', + packageName: 'apache', + status: 'success', + }); + }); + + it('should send telemetry on install failure, async error', async () => { + jest + .spyOn(install, '_installPackage') + .mockImplementation(() => Promise.reject(new Error('error'))); + await installPackage({ + installSource: 'upload', + archiveBuffer: {} as Buffer, + contentType: '', + savedObjectsClient: savedObjectsClientMock.create(), + esClient: {} as ElasticsearchClient, + }); + + expect(sendTelemetryEvents).toHaveBeenCalledWith(expect.anything(), undefined, { + currentVersion: 'not_installed', + dryRun: false, + errorMessage: 'error', + eventType: 'package-install', + installType: 'install', + newVersion: '1.3.0', + packageName: 'apache', + status: 'failure', + }); + }); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install.ts b/x-pack/plugins/fleet/server/services/epm/packages/install.ts index f57965614adc6..42f4663dc21e3 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install.ts @@ -41,6 +41,9 @@ import { toAssetReference } from '../kibana/assets/install'; import type { ArchiveAsset } from '../kibana/assets/install'; import { installIndexPatterns } from '../kibana/index_pattern/install'; +import type { PackageUpdateEvent } from '../../upgrade_sender'; +import { sendTelemetryEvents, UpdateEventType } from '../../upgrade_sender'; + import { isUnremovablePackage, getInstallation, getInstallationObject } from './index'; import { removeInstallation } from './remove'; import { getPackageSavedObjects } from './get'; @@ -203,6 +206,26 @@ interface InstallRegistryPackageParams { force?: boolean; } +function getTelemetryEvent(pkgName: string, pkgVersion: string): PackageUpdateEvent { + return { + packageName: pkgName, + currentVersion: 'unknown', + newVersion: pkgVersion, + status: 'failure', + dryRun: false, + eventType: UpdateEventType.PACKAGE_INSTALL, + installType: 'unknown', + }; +} + +function sendEvent(telemetryEvent: PackageUpdateEvent) { + sendTelemetryEvents( + appContextService.getLogger(), + appContextService.getTelemetryEventsSender(), + telemetryEvent + ); +} + async function installPackageFromRegistry({ savedObjectsClient, pkgkey, @@ -216,6 +239,8 @@ async function installPackageFromRegistry({ // if an error happens during getInstallType, report that we don't know let installType: InstallType = 'unknown'; + const telemetryEvent: PackageUpdateEvent = getTelemetryEvent(pkgName, pkgVersion); + try { // get the currently installed package const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName }); @@ -248,6 +273,9 @@ async function installPackageFromRegistry({ } } + telemetryEvent.installType = installType; + telemetryEvent.currentVersion = installedPkg?.attributes.version || 'not_installed'; + // if the requested version is out-of-date of the latest package version, check if we allow it // if we don't allow it, return an error if (semverLt(pkgVersion, latestPackage.version)) { @@ -267,7 +295,12 @@ async function installPackageFromRegistry({ const { paths, packageInfo } = await Registry.getRegistryPackage(pkgName, pkgVersion); if (!licenseService.hasAtLeast(packageInfo.license || 'basic')) { - return { error: new Error(`Requires ${packageInfo.license} license`), installType }; + const err = new Error(`Requires ${packageInfo.license} license`); + sendEvent({ + ...telemetryEvent, + errorMessage: err.message, + }); + return { error: err, installType }; } // try installing the package, if there was an error, call error handler and rethrow @@ -287,6 +320,10 @@ async function installPackageFromRegistry({ pkgName: packageInfo.name, currentVersion: packageInfo.version, }); + sendEvent({ + ...telemetryEvent, + status: 'success', + }); return { assets, status: 'installed', installType }; }) .catch(async (err: Error) => { @@ -299,9 +336,17 @@ async function installPackageFromRegistry({ installedPkg, esClient, }); + sendEvent({ + ...telemetryEvent, + errorMessage: err.message, + }); return { error: err, installType }; }); } catch (e) { + sendEvent({ + ...telemetryEvent, + errorMessage: e.message, + }); return { error: e, installType, @@ -324,6 +369,7 @@ async function installPackageByUpload({ }: InstallUploadedArchiveParams): Promise { // if an error happens during getInstallType, report that we don't know let installType: InstallType = 'unknown'; + const telemetryEvent: PackageUpdateEvent = getTelemetryEvent('', ''); try { const { packageInfo } = await parseAndVerifyArchiveEntries(archiveBuffer, contentType); @@ -333,6 +379,12 @@ async function installPackageByUpload({ }); installType = getInstallType({ pkgVersion: packageInfo.version, installedPkg }); + + telemetryEvent.packageName = packageInfo.name; + telemetryEvent.newVersion = packageInfo.version; + telemetryEvent.installType = installType; + telemetryEvent.currentVersion = installedPkg?.attributes.version || 'not_installed'; + if (installType !== 'install') { throw new PackageOperationNotSupportedError( `Package upload only supports fresh installations. Package ${packageInfo.name} is already installed, please uninstall first.` @@ -364,12 +416,24 @@ async function installPackageByUpload({ installSource, }) .then((assets) => { + sendEvent({ + ...telemetryEvent, + status: 'success', + }); return { assets, status: 'installed', installType }; }) .catch(async (err: Error) => { + sendEvent({ + ...telemetryEvent, + errorMessage: err.message, + }); return { error: err, installType }; }); } catch (e) { + sendEvent({ + ...telemetryEvent, + errorMessage: e.message, + }); return { error: e, installType }; } } diff --git a/x-pack/plugins/fleet/server/services/package_policy.test.ts b/x-pack/plugins/fleet/server/services/package_policy.test.ts index dcc00251e70f4..36976bea4a970 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.test.ts @@ -134,7 +134,7 @@ jest.mock('./epm/packages/cleanup', () => { }; }); -jest.mock('./upgrade_usage', () => { +jest.mock('./upgrade_sender', () => { return { sendTelemetryEvents: jest.fn(), }; diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index af5596964740a..20434e8290457 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -67,8 +67,8 @@ import { compileTemplate } from './epm/agent/agent'; import { normalizeKuery } from './saved_object'; import { appContextService } from '.'; import { removeOldAssets } from './epm/packages/cleanup'; -import type { PackagePolicyUpgradeUsage } from './upgrade_usage'; -import { sendTelemetryEvents } from './upgrade_usage'; +import type { PackageUpdateEvent, UpdateEventType } from './upgrade_sender'; +import { sendTelemetryEvents } from './upgrade_sender'; export type InputsOverride = Partial & { vars?: Array; @@ -423,12 +423,13 @@ class PackagePolicyService { }); if (packagePolicy.package.version !== currentVersion) { - const upgradeTelemetry: PackagePolicyUpgradeUsage = { - package_name: packagePolicy.package.name, - current_version: currentVersion || 'unknown', - new_version: packagePolicy.package.version, + const upgradeTelemetry: PackageUpdateEvent = { + packageName: packagePolicy.package.name, + currentVersion: currentVersion || 'unknown', + newVersion: packagePolicy.package.version, status: 'success', dryRun: false, + eventType: 'package-policy-upgrade' as UpdateEventType, }; sendTelemetryEvents( appContextService.getLogger(), @@ -668,13 +669,14 @@ class PackagePolicyService { const hasErrors = 'errors' in updatedPackagePolicy; if (packagePolicy.package.version !== packageInfo.version) { - const upgradeTelemetry: PackagePolicyUpgradeUsage = { - package_name: packageInfo.name, - current_version: packagePolicy.package.version, - new_version: packageInfo.version, + const upgradeTelemetry: PackageUpdateEvent = { + packageName: packageInfo.name, + currentVersion: packagePolicy.package.version, + newVersion: packageInfo.version, status: hasErrors ? 'failure' : 'success', error: hasErrors ? updatedPackagePolicy.errors : undefined, dryRun: true, + eventType: 'package-policy-upgrade' as UpdateEventType, }; sendTelemetryEvents( appContextService.getLogger(), diff --git a/x-pack/plugins/fleet/server/services/upgrade_usage.test.ts b/x-pack/plugins/fleet/server/services/upgrade_sender.test.ts similarity index 73% rename from x-pack/plugins/fleet/server/services/upgrade_usage.test.ts rename to x-pack/plugins/fleet/server/services/upgrade_sender.test.ts index 5445ad233eddc..c8a64a7172b39 100644 --- a/x-pack/plugins/fleet/server/services/upgrade_usage.test.ts +++ b/x-pack/plugins/fleet/server/services/upgrade_sender.test.ts @@ -11,8 +11,8 @@ import { loggingSystemMock } from 'src/core/server/mocks'; import type { TelemetryEventsSender } from '../telemetry/sender'; import { createMockTelemetryEventsSender } from '../telemetry/__mocks__'; -import { sendTelemetryEvents, capErrorSize } from './upgrade_usage'; -import type { PackagePolicyUpgradeUsage } from './upgrade_usage'; +import { sendTelemetryEvents, capErrorSize, UpdateEventType } from './upgrade_sender'; +import type { PackageUpdateEvent } from './upgrade_sender'; describe('sendTelemetryEvents', () => { let eventsTelemetryMock: jest.Mocked; @@ -24,23 +24,24 @@ describe('sendTelemetryEvents', () => { }); it('should queue telemetry events with generic error', () => { - const upgardeMessage: PackagePolicyUpgradeUsage = { - package_name: 'aws', - current_version: '0.6.1', - new_version: '1.3.0', + const upgradeMessage: PackageUpdateEvent = { + packageName: 'aws', + currentVersion: '0.6.1', + newVersion: '1.3.0', status: 'failure', error: [ { key: 'queueUrl', message: ['Queue URL is required'] }, { message: 'Invalid format' }, ], dryRun: true, + eventType: UpdateEventType.PACKAGE_POLICY_UPGRADE, }; - sendTelemetryEvents(loggerMock, eventsTelemetryMock, upgardeMessage); + sendTelemetryEvents(loggerMock, eventsTelemetryMock, upgradeMessage); expect(eventsTelemetryMock.queueTelemetryEvents).toHaveBeenCalledWith('fleet-upgrades', [ { - current_version: '0.6.1', + currentVersion: '0.6.1', error: [ { key: 'queueUrl', @@ -50,11 +51,12 @@ describe('sendTelemetryEvents', () => { message: 'Invalid format', }, ], - error_message: ['Field is required', 'Invalid format'], - new_version: '1.3.0', - package_name: 'aws', + errorMessage: ['Field is required', 'Invalid format'], + newVersion: '1.3.0', + packageName: 'aws', status: 'failure', dryRun: true, + eventType: 'package-policy-upgrade', }, ]); }); diff --git a/x-pack/plugins/fleet/server/services/upgrade_usage.ts b/x-pack/plugins/fleet/server/services/upgrade_sender.ts similarity index 69% rename from x-pack/plugins/fleet/server/services/upgrade_usage.ts rename to x-pack/plugins/fleet/server/services/upgrade_sender.ts index 68bb126496e01..9069ab68b55a3 100644 --- a/x-pack/plugins/fleet/server/services/upgrade_usage.ts +++ b/x-pack/plugins/fleet/server/services/upgrade_sender.ts @@ -8,15 +8,23 @@ import type { Logger } from 'src/core/server'; import type { TelemetryEventsSender } from '../telemetry/sender'; +import type { InstallType } from '../types'; -export interface PackagePolicyUpgradeUsage { - package_name: string; - current_version: string; - new_version: string; +export interface PackageUpdateEvent { + packageName: string; + currentVersion: string; + newVersion: string; status: 'success' | 'failure'; - error?: UpgradeError[]; dryRun?: boolean; - error_message?: string[]; + errorMessage?: string[] | string; + error?: UpgradeError[]; + eventType: UpdateEventType; + installType?: InstallType; +} + +export enum UpdateEventType { + PACKAGE_POLICY_UPGRADE = 'package-policy-upgrade', + PACKAGE_INSTALL = 'package-install', } export interface UpgradeError { @@ -30,19 +38,19 @@ export const FLEET_UPGRADES_CHANNEL_NAME = 'fleet-upgrades'; export function sendTelemetryEvents( logger: Logger, eventsTelemetry: TelemetryEventsSender | undefined, - upgradeUsage: PackagePolicyUpgradeUsage + upgradeEvent: PackageUpdateEvent ) { if (eventsTelemetry === undefined) { return; } try { - const cappedErrors = capErrorSize(upgradeUsage.error || [], MAX_ERROR_SIZE); + const cappedErrors = capErrorSize(upgradeEvent.error || [], MAX_ERROR_SIZE); eventsTelemetry.queueTelemetryEvents(FLEET_UPGRADES_CHANNEL_NAME, [ { - ...upgradeUsage, - error: upgradeUsage.error ? cappedErrors : undefined, - error_message: makeErrorGeneric(cappedErrors), + ...upgradeEvent, + error: upgradeEvent.error ? cappedErrors : undefined, + errorMessage: upgradeEvent.errorMessage || makeErrorGeneric(cappedErrors), }, ]); } catch (exc) { diff --git a/x-pack/plugins/fleet/server/telemetry/sender.test.ts b/x-pack/plugins/fleet/server/telemetry/sender.test.ts index 8fe4c6e150ff9..a1ba0693bf3f3 100644 --- a/x-pack/plugins/fleet/server/telemetry/sender.test.ts +++ b/x-pack/plugins/fleet/server/telemetry/sender.test.ts @@ -15,6 +15,8 @@ import type { InfoResponse } from '@elastic/elasticsearch/lib/api/types'; import { loggingSystemMock } from 'src/core/server/mocks'; +import { UpdateEventType } from '../services/upgrade_sender'; + import { TelemetryEventsSender } from './sender'; jest.mock('axios', () => { @@ -38,7 +40,13 @@ describe('TelemetryEventsSender', () => { describe('queueTelemetryEvents', () => { it('queues two events', () => { sender.queueTelemetryEvents('fleet-upgrades', [ - { package_name: 'system', current_version: '0.3', new_version: '1.0', status: 'success' }, + { + packageName: 'system', + currentVersion: '0.3', + newVersion: '1.0', + status: 'success', + eventType: UpdateEventType.PACKAGE_POLICY_UPGRADE, + }, ]); expect(sender['queuesPerChannel']['fleet-upgrades']).toBeDefined(); }); @@ -54,7 +62,13 @@ describe('TelemetryEventsSender', () => { }; sender.queueTelemetryEvents('fleet-upgrades', [ - { package_name: 'apache', current_version: '0.3', new_version: '1.0', status: 'success' }, + { + packageName: 'apache', + currentVersion: '0.3', + newVersion: '1.0', + status: 'success', + eventType: UpdateEventType.PACKAGE_POLICY_UPGRADE, + }, ]); sender['sendEvents'] = jest.fn(); @@ -74,7 +88,13 @@ describe('TelemetryEventsSender', () => { sender['telemetryStart'] = telemetryStart; sender.queueTelemetryEvents('fleet-upgrades', [ - { package_name: 'system', current_version: '0.3', new_version: '1.0', status: 'success' }, + { + packageName: 'system', + currentVersion: '0.3', + newVersion: '1.0', + status: 'success', + eventType: UpdateEventType.PACKAGE_POLICY_UPGRADE, + }, ]); sender['sendEvents'] = jest.fn(); diff --git a/x-pack/plugins/fleet/server/telemetry/sender.ts b/x-pack/plugins/fleet/server/telemetry/sender.ts index 3bda17fbd1d79..e7413872b6245 100644 --- a/x-pack/plugins/fleet/server/telemetry/sender.ts +++ b/x-pack/plugins/fleet/server/telemetry/sender.ts @@ -138,7 +138,7 @@ export class TelemetryEventsSender { clusterInfo?.version?.number ); } catch (err) { - this.logger.warn(`Error sending telemetry events data: ${err}`); + this.logger.debug(`Error sending telemetry events data: ${err}`); queue.clearEvents(); } } @@ -175,7 +175,7 @@ export class TelemetryEventsSender { }); this.logger.debug(`Events sent!. Response: ${resp.status} ${JSON.stringify(resp.data)}`); } catch (err) { - this.logger.warn( + this.logger.debug( `Error sending events: ${err.response.status} ${JSON.stringify(err.response.data)}` ); } diff --git a/x-pack/plugins/fleet/server/telemetry/types.ts b/x-pack/plugins/fleet/server/telemetry/types.ts index 4351546ecdf02..3b6478d68fba7 100644 --- a/x-pack/plugins/fleet/server/telemetry/types.ts +++ b/x-pack/plugins/fleet/server/telemetry/types.ts @@ -5,11 +5,11 @@ * 2.0. */ -import type { PackagePolicyUpgradeUsage } from '../services/upgrade_usage'; +import type { PackageUpdateEvent } from '../services/upgrade_sender'; export interface FleetTelemetryChannelEvents { // channel name => event type - 'fleet-upgrades': PackagePolicyUpgradeUsage; + 'fleet-upgrades': PackageUpdateEvent; } export type FleetTelemetryChannel = keyof FleetTelemetryChannelEvents; From cdafd322c7d4ee3fc290cbef3b469815c598a856 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 11 Nov 2021 05:53:45 -0500 Subject: [PATCH 34/43] [Actionable Observability] Add links to navigate from alerts table to rule (#118035) (#118301) [Actionable Observability] Add links to navigate from alerts table and flyout to rule generate it Co-authored-by: Ersin Erdal <92688503+ersin-erdal@users.noreply.github.com> --- .../components/app/section/alerts/index.tsx | 9 +- .../observability/public/config/index.ts | 9 ++ .../observability/public/config/paths.ts | 19 ++++ .../public/config/translations.ts | 104 ++++++++++++++++++ .../pages/alerts/alerts_flyout/index.tsx | 53 +++++---- .../pages/alerts/alerts_table_t_grid.tsx | 58 +++++++--- .../public/pages/alerts/translations.ts | 61 ---------- .../services/observability/alerts/common.ts | 17 +++ .../apps/observability/alerts/index.ts | 40 +++---- 9 files changed, 244 insertions(+), 126 deletions(-) create mode 100644 x-pack/plugins/observability/public/config/index.ts create mode 100644 x-pack/plugins/observability/public/config/paths.ts create mode 100644 x-pack/plugins/observability/public/config/translations.ts delete mode 100644 x-pack/plugins/observability/public/pages/alerts/translations.ts diff --git a/x-pack/plugins/observability/public/components/app/section/alerts/index.tsx b/x-pack/plugins/observability/public/components/app/section/alerts/index.tsx index 44f699c6c390b..cf3ac2b6c7be5 100644 --- a/x-pack/plugins/observability/public/components/app/section/alerts/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/alerts/index.tsx @@ -24,6 +24,7 @@ import { EuiSelect } from '@elastic/eui'; import { uniqBy } from 'lodash'; import { Alert } from '../../../../../../alerting/common'; import { usePluginContext } from '../../../../hooks/use_plugin_context'; +import { paths } from '../../../../config'; const ALL_TYPES = 'ALL_TYPES'; const allTypes = { @@ -41,8 +42,8 @@ export function AlertsSection({ alerts }: Props) { const { config, core } = usePluginContext(); const [filter, setFilter] = useState(ALL_TYPES); const manageLink = config.unsafe.alertingExperience.enabled - ? core.http.basePath.prepend(`/app/observability/alerts`) - : core.http.basePath.prepend(`/app/management/insightsAndAlerting/triggersActions/rules`); + ? core.http.basePath.prepend(paths.observability.alerts) + : core.http.basePath.prepend(paths.management.rules); const filterOptions = uniqBy(alerts, (alert) => alert.consumer).map(({ consumer }) => ({ value: consumer, text: consumer, @@ -89,9 +90,7 @@ export function AlertsSection({ alerts }: Props) { {alert.name} diff --git a/x-pack/plugins/observability/public/config/index.ts b/x-pack/plugins/observability/public/config/index.ts new file mode 100644 index 0000000000000..fc6300acc4716 --- /dev/null +++ b/x-pack/plugins/observability/public/config/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { paths } from './paths'; +export { translations } from './translations'; diff --git a/x-pack/plugins/observability/public/config/paths.ts b/x-pack/plugins/observability/public/config/paths.ts new file mode 100644 index 0000000000000..57bbc95fef40b --- /dev/null +++ b/x-pack/plugins/observability/public/config/paths.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const paths = { + observability: { + alerts: '/app/observability/alerts', + }, + management: { + rules: '/app/management/insightsAndAlerting/triggersActions/rules', + ruleDetails: (ruleId: string) => + `/app/management/insightsAndAlerting/triggersActions/rule/${encodeURI(ruleId)}`, + alertDetails: (alertId: string) => + `/app/management/insightsAndAlerting/triggersActions/alert/${encodeURI(alertId)}`, + }, +}; diff --git a/x-pack/plugins/observability/public/config/translations.ts b/x-pack/plugins/observability/public/config/translations.ts new file mode 100644 index 0000000000000..265787ede4473 --- /dev/null +++ b/x-pack/plugins/observability/public/config/translations.ts @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const translations = { + alertsTable: { + viewDetailsTextLabel: i18n.translate('xpack.observability.alertsTable.viewDetailsTextLabel', { + defaultMessage: 'View details', + }), + viewInAppTextLabel: i18n.translate('xpack.observability.alertsTable.viewInAppTextLabel', { + defaultMessage: 'View in app', + }), + moreActionsTextLabel: i18n.translate('xpack.observability.alertsTable.moreActionsTextLabel', { + defaultMessage: 'More actions', + }), + notEnoughPermissions: i18n.translate('xpack.observability.alertsTable.notEnoughPermissions', { + defaultMessage: 'Additional privileges required', + }), + statusColumnDescription: i18n.translate( + 'xpack.observability.alertsTGrid.statusColumnDescription', + { + defaultMessage: 'Alert Status', + } + ), + lastUpdatedColumnDescription: i18n.translate( + 'xpack.observability.alertsTGrid.lastUpdatedColumnDescription', + { + defaultMessage: 'Last updated', + } + ), + durationColumnDescription: i18n.translate( + 'xpack.observability.alertsTGrid.durationColumnDescription', + { + defaultMessage: 'Duration', + } + ), + reasonColumnDescription: i18n.translate( + 'xpack.observability.alertsTGrid.reasonColumnDescription', + { + defaultMessage: 'Reason', + } + ), + actionsTextLabel: i18n.translate('xpack.observability.alertsTable.actionsTextLabel', { + defaultMessage: 'Actions', + }), + loadingTextLabel: i18n.translate('xpack.observability.alertsTable.loadingTextLabel', { + defaultMessage: 'loading alerts', + }), + footerTextLabel: i18n.translate('xpack.observability.alertsTable.footerTextLabel', { + defaultMessage: 'alerts', + }), + showingAlertsTitle: (totalAlerts: number) => + i18n.translate('xpack.observability.alertsTable.showingAlertsTitle', { + values: { totalAlerts }, + defaultMessage: '{totalAlerts, plural, =1 {alert} other {alerts}}', + }), + viewRuleDetailsButtonText: i18n.translate( + 'xpack.observability.alertsTable.viewRuleDetailsButtonText', + { + defaultMessage: 'View rule details', + } + ), + }, + alertsFlyout: { + statusLabel: i18n.translate('xpack.observability.alertsFlyout.statusLabel', { + defaultMessage: 'Status', + }), + lastUpdatedLabel: i18n.translate('xpack.observability.alertsFlyout.lastUpdatedLabel', { + defaultMessage: 'Last updated', + }), + durationLabel: i18n.translate('xpack.observability.alertsFlyout.durationLabel', { + defaultMessage: 'Duration', + }), + expectedValueLabel: i18n.translate('xpack.observability.alertsFlyout.expectedValueLabel', { + defaultMessage: 'Expected value', + }), + actualValueLabel: i18n.translate('xpack.observability.alertsFlyout.actualValueLabel', { + defaultMessage: 'Actual value', + }), + ruleTypeLabel: i18n.translate('xpack.observability.alertsFlyout.ruleTypeLabel', { + defaultMessage: 'Rule type', + }), + reasonTitle: i18n.translate('xpack.observability.alertsFlyout.reasonTitle', { + defaultMessage: 'Reason', + }), + viewRulesDetailsLinkText: i18n.translate( + 'xpack.observability.alertsFlyout.viewRulesDetailsLinkText', + { + defaultMessage: 'View rule details', + } + ), + documentSummaryTitle: i18n.translate('xpack.observability.alertsFlyout.documentSummaryTitle', { + defaultMessage: 'Document Summary', + }), + viewInAppButtonText: i18n.translate('xpack.observability.alertsFlyout.viewInAppButtonText', { + defaultMessage: 'View in app', + }), + }, +}; diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx index 034b7522b9136..41f107437d23b 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts_flyout/index.tsx @@ -15,11 +15,12 @@ import { EuiFlyoutFooter, EuiFlyoutHeader, EuiFlyoutProps, + EuiLink, EuiSpacer, EuiText, EuiTitle, + EuiHorizontalRule, } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; import type { ALERT_DURATION as ALERT_DURATION_TYPED, ALERT_EVALUATION_THRESHOLD as ALERT_EVALUATION_THRESHOLD_TYPED, @@ -47,6 +48,7 @@ import type { ObservabilityRuleTypeRegistry } from '../../../rules/create_observ import { parseAlert } from '../parse_alert'; import { AlertStatusIndicator } from '../../../components/shared/alert_status_indicator'; import { ExperimentalBadge } from '../../../components/shared/experimental_badge'; +import { translations, paths } from '../../../config'; type AlertsFlyoutProps = { alert?: TopAlert; @@ -77,6 +79,7 @@ export function AlertsFlyout({ const { services } = useKibana(); const { http } = services; const prepend = http?.basePath.prepend; + const decoratedAlerts = useMemo(() => { const parseObservabilityAlert = parseAlert(observabilityRuleTypeRegistry); return (alerts ?? []).map(parseObservabilityAlert); @@ -90,11 +93,12 @@ export function AlertsFlyout({ return null; } + const ruleId = alertData.fields['kibana.alert.rule.uuid'] ?? null; + const linkToRule = ruleId && prepend ? prepend(paths.management.ruleDetails(ruleId)) : null; + const overviewListItems = [ { - title: i18n.translate('xpack.observability.alertsFlyout.statusLabel', { - defaultMessage: 'Status', - }), + title: translations.alertsFlyout.statusLabel, description: ( {moment(alertData.start).format(dateFormat)} ), }, { - title: i18n.translate('xpack.observability.alertsFlyout.durationLabel', { - defaultMessage: 'Duration', - }), + title: translations.alertsFlyout.durationLabel, description: asDuration(alertData.fields[ALERT_DURATION], { extended: true }), }, { - title: i18n.translate('xpack.observability.alertsFlyout.expectedValueLabel', { - defaultMessage: 'Expected value', - }), + title: translations.alertsFlyout.expectedValueLabel, description: alertData.fields[ALERT_EVALUATION_THRESHOLD] ?? '-', }, { - title: i18n.translate('xpack.observability.alertsFlyout.actualValueLabel', { - defaultMessage: 'Actual value', - }), + title: translations.alertsFlyout.actualValueLabel, description: alertData.fields[ALERT_EVALUATION_VALUE] ?? '-', }, { - title: i18n.translate('xpack.observability.alertsFlyout.ruleTypeLabel', { - defaultMessage: 'Rule type', - }), + title: translations.alertsFlyout.ruleTypeLabel, description: alertData.fields[ALERT_RULE_CATEGORY] ?? '-', }, ]; return ( - +

    {alertData.fields[ALERT_RULE_NAME]}

    - - {alertData.reason}
    + +

    {translations.alertsFlyout.reasonTitle}

    +
    + + {alertData.reason} + {!!linkToRule && ( + + {translations.alertsFlyout.viewRulesDetailsLinkText} + + )} + + +

    {translations.alertsFlyout.documentSummaryTitle}

    +
    + - View in app + {translations.alertsFlyout.viewInAppButtonText}
    diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx index a5b229e92f69d..523d0f19be2be 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx @@ -34,10 +34,12 @@ import { EuiDataGridColumn, EuiFlexGroup, EuiFlexItem, + EuiContextMenuItem, EuiContextMenuPanel, EuiPopover, EuiToolTip, } from '@elastic/eui'; + import styled from 'styled-components'; import React, { Suspense, useMemo, useState, useCallback, useEffect } from 'react'; import usePrevious from 'react-use/lib/usePrevious'; @@ -65,7 +67,7 @@ import { getDefaultCellActions } from './default_cell_actions'; import { LazyAlertsFlyout } from '../..'; import { parseAlert } from './parse_alert'; import { CoreStart } from '../../../../../../src/core/public'; -import { translations } from './translations'; +import { translations, paths } from '../../config'; const ALERT_DURATION: typeof ALERT_DURATION_TYPED = ALERT_DURATION_NON_TYPED; const ALERT_REASON: typeof ALERT_REASON_TYPED = ALERT_REASON_NON_TYPED; @@ -115,25 +117,25 @@ export const columns: Array< > = [ { columnHeaderType: 'not-filtered', - displayAsText: translations.statusColumnDescription, + displayAsText: translations.alertsTable.statusColumnDescription, id: ALERT_STATUS, initialWidth: 110, }, { columnHeaderType: 'not-filtered', - displayAsText: translations.lastUpdatedColumnDescription, + displayAsText: translations.alertsTable.lastUpdatedColumnDescription, id: TIMESTAMP, initialWidth: 230, }, { columnHeaderType: 'not-filtered', - displayAsText: translations.durationColumnDescription, + displayAsText: translations.alertsTable.durationColumnDescription, id: ALERT_DURATION, initialWidth: 116, }, { columnHeaderType: 'not-filtered', - displayAsText: translations.reasonColumnDescription, + displayAsText: translations.alertsTable.reasonColumnDescription, id: ALERT_REASON, linkField: '*', }, @@ -188,6 +190,7 @@ function ObservabilityActions({ const toggleActionsPopover = useCallback((id) => { setActionsPopover((current) => (current ? null : id)); }, []); + const casePermissions = useGetUserCasesPermissions(); const event = useMemo(() => { return { @@ -219,6 +222,9 @@ function ObservabilityActions({ onUpdateFailure: onAlertStatusUpdated, }); + const ruleId = alert.fields['kibana.alert.rule.uuid'] ?? null; + const linkToRule = ruleId ? prepend(paths.management.ruleDetails(ruleId)) : null; + const actionsMenuItems = useMemo(() => { return [ ...(casePermissions?.crud @@ -240,37 +246,56 @@ function ObservabilityActions({ ] : []), ...(alertPermissions.crud ? statusActionItems : []), + ...(!!linkToRule + ? [ + + {translations.alertsTable.viewRuleDetailsButtonText} + , + ] + : []), ]; - }, [afterCaseSelection, casePermissions, timelines, event, statusActionItems, alertPermissions]); + }, [ + afterCaseSelection, + casePermissions, + timelines, + event, + statusActionItems, + alertPermissions, + linkToRule, + ]); const actionsToolTip = actionsMenuItems.length <= 0 - ? translations.notEnoughPermissions - : translations.moreActionsTextLabel; + ? translations.alertsTable.notEnoughPermissions + : translations.alertsTable.moreActionsTextLabel; return ( <> - + setFlyoutAlert(alert)} data-test-subj="openFlyoutButton" - aria-label={translations.viewDetailsTextLabel} + aria-label={translations.alertsTable.viewDetailsTextLabel} /> - + @@ -280,7 +305,6 @@ function ObservabilityActions({ { - return {translations.actionsTextLabel}; + return {translations.alertsTable.actionsTextLabel}; }, rowCellRender: (actionProps: ActionProps) => { return ( @@ -377,8 +401,8 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) { hasAlertsCrudPermissions, indexNames, itemsPerPageOptions: [10, 25, 50], - loadingText: translations.loadingTextLabel, - footerText: translations.footerTextLabel, + loadingText: translations.alertsTable.loadingTextLabel, + footerText: translations.alertsTable.footerTextLabel, query: { query: `${ALERT_WORKFLOW_STATUS}: ${workflowStatus}${kuery !== '' ? ` and ${kuery}` : ''}`, language: 'kuery', @@ -399,7 +423,7 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) { filterStatus: workflowStatus as AlertWorkflowStatus, leadingControlColumns, trailingControlColumns, - unit: (totalAlerts: number) => translations.showingAlertsTitle(totalAlerts), + unit: (totalAlerts: number) => translations.alertsTable.showingAlertsTitle(totalAlerts), }; }, [ casePermissions, diff --git a/x-pack/plugins/observability/public/pages/alerts/translations.ts b/x-pack/plugins/observability/public/pages/alerts/translations.ts deleted file mode 100644 index 4578987e839a0..0000000000000 --- a/x-pack/plugins/observability/public/pages/alerts/translations.ts +++ /dev/null @@ -1,61 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export const translations = { - viewDetailsTextLabel: i18n.translate('xpack.observability.alertsTable.viewDetailsTextLabel', { - defaultMessage: 'View details', - }), - viewInAppTextLabel: i18n.translate('xpack.observability.alertsTable.viewInAppTextLabel', { - defaultMessage: 'View in app', - }), - moreActionsTextLabel: i18n.translate('xpack.observability.alertsTable.moreActionsTextLabel', { - defaultMessage: 'More actions', - }), - notEnoughPermissions: i18n.translate('xpack.observability.alertsTable.notEnoughPermissions', { - defaultMessage: 'Additional privileges required', - }), - statusColumnDescription: i18n.translate( - 'xpack.observability.alertsTGrid.statusColumnDescription', - { - defaultMessage: 'Alert Status', - } - ), - lastUpdatedColumnDescription: i18n.translate( - 'xpack.observability.alertsTGrid.lastUpdatedColumnDescription', - { - defaultMessage: 'Last updated', - } - ), - durationColumnDescription: i18n.translate( - 'xpack.observability.alertsTGrid.durationColumnDescription', - { - defaultMessage: 'Duration', - } - ), - reasonColumnDescription: i18n.translate( - 'xpack.observability.alertsTGrid.reasonColumnDescription', - { - defaultMessage: 'Reason', - } - ), - actionsTextLabel: i18n.translate('xpack.observability.alertsTable.actionsTextLabel', { - defaultMessage: 'Actions', - }), - loadingTextLabel: i18n.translate('xpack.observability.alertsTable.loadingTextLabel', { - defaultMessage: 'loading alerts', - }), - footerTextLabel: i18n.translate('xpack.observability.alertsTable.footerTextLabel', { - defaultMessage: 'alerts', - }), - showingAlertsTitle: (totalAlerts: number) => - i18n.translate('xpack.observability.alertsTable.showingAlertsTitle', { - values: { totalAlerts }, - defaultMessage: '{totalAlerts, plural, =1 {alert} other {alerts}}', - }), -}; diff --git a/x-pack/test/functional/services/observability/alerts/common.ts b/x-pack/test/functional/services/observability/alerts/common.ts index 373f7558f8739..dd7d49af4fe5a 100644 --- a/x-pack/test/functional/services/observability/alerts/common.ts +++ b/x-pack/test/functional/services/observability/alerts/common.ts @@ -18,6 +18,9 @@ const DATE_WITH_DATA = { const ALERTS_FLYOUT_SELECTOR = 'alertsFlyout'; const FILTER_FOR_VALUE_BUTTON_SELECTOR = 'filterForValue'; const ALERTS_TABLE_CONTAINER_SELECTOR = 'events-viewer-panel'; +const VIEW_RULE_DETAILS_SELECTOR = 'viewRuleDetails'; +const VIEW_RULE_DETAILS_FLYOUT_SELECTOR = 'viewRuleDetailsFlyout'; + const ACTION_COLUMN_INDEX = 1; type WorkflowStatus = 'open' | 'acknowledged' | 'closed'; @@ -150,6 +153,10 @@ export function ObservabilityAlertsCommonProvider({ return await testSubjects.existOrFail('alertsFlyoutViewInAppButton'); }; + const getAlertsFlyoutViewRuleDetailsLinkOrFail = async () => { + return await testSubjects.existOrFail('viewRuleDetailsFlyout'); + }; + const getAlertsFlyoutDescriptionListTitles = async (): Promise => { const flyout = await getAlertsFlyout(); return await testSubjects.findAllDescendant('alertsFlyoutDescriptionListTitle', flyout); @@ -179,6 +186,13 @@ export function ObservabilityAlertsCommonProvider({ await actionsOverflowButton.click(); }; + const viewRuleDetailsButtonClick = async () => { + return await (await testSubjects.find(VIEW_RULE_DETAILS_SELECTOR)).click(); + }; + const viewRuleDetailsLinkClick = async () => { + return await (await testSubjects.find(VIEW_RULE_DETAILS_FLYOUT_SELECTOR)).click(); + }; + // Workflow status const setWorkflowStatusForRow = async (rowIndex: number, workflowStatus: WorkflowStatus) => { await openActionsMenuForRow(rowIndex); @@ -259,5 +273,8 @@ export function ObservabilityAlertsCommonProvider({ navigateWithoutFilter, getExperimentalDisclaimer, getActionsButtonByIndex, + viewRuleDetailsButtonClick, + viewRuleDetailsLinkClick, + getAlertsFlyoutViewRuleDetailsLinkOrFail, }; } diff --git a/x-pack/test/observability_functional/apps/observability/alerts/index.ts b/x-pack/test/observability_functional/apps/observability/alerts/index.ts index 3190a151cb47b..2b760b65a1c46 100644 --- a/x-pack/test/observability_functional/apps/observability/alerts/index.ts +++ b/x-pack/test/observability_functional/apps/observability/alerts/index.ts @@ -20,8 +20,9 @@ const TOTAL_ALERTS_CELL_COUNT = 198; export default ({ getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); + const find = getService('find'); - describe('Observability alerts', function () { + describe('Observability alerts 1', function () { this.tags('includeFirefox'); const testSubjects = getService('testSubjects'); @@ -178,6 +179,10 @@ export default ({ getService }: FtrProviderContext) => { it('Displays a View in App button', async () => { await observability.alerts.common.getAlertsFlyoutViewInAppButtonOrFail(); }); + + it('Displays a View rule details link', async () => { + await observability.alerts.common.getAlertsFlyoutViewRuleDetailsLinkOrFail(); + }); }); }); @@ -213,28 +218,23 @@ export default ({ getService }: FtrProviderContext) => { }); }); }); - }); - describe('Actions Button', () => { - before(async () => { - await observability.users.setTestUserRole( - observability.users.defineBasicObservabilityRole({ - observabilityCases: ['read'], - logs: ['read'], - }) - ); - await esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_and_logs'); - await observability.alerts.common.navigateToTimeWithData(); - }); + describe('Actions Button', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_and_logs'); + await observability.alerts.common.navigateToTimeWithData(); + }); - after(async () => { - await observability.users.restoreDefaultTestUserRole(); - await esArchiver.unload('x-pack/test/functional/es_archives/infra/metrics_and_logs'); - }); + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/infra/metrics_and_logs'); + }); - it('Is disabled when a user has only read privilages', async () => { - const actionsButton = await observability.alerts.common.getActionsButtonByIndex(0); - expect(await actionsButton.getAttribute('disabled')).to.be('true'); + it('Opens rule details page when click on "View Rule Details"', async () => { + const actionsButton = await observability.alerts.common.getActionsButtonByIndex(0); + await actionsButton.click(); + await observability.alerts.common.viewRuleDetailsButtonClick(); + expect(await find.existsByCssSelector('[title="Rules and Connectors"]')).to.eql(true); + }); }); }); }); From 7a5882c8eecdb697ceb0e9e2bbe56a25f9af6ae9 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 11 Nov 2021 06:19:23 -0500 Subject: [PATCH 35/43] rename Analyze data labels to Explore data (#118096) (#118275) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Dominique Clarke --- .../public/components/app/RumDashboard/ActionMenu/index.tsx | 4 ++-- .../templates/apm_service_template/analyze_data_button.tsx | 4 ++-- .../public/components/shared/exploratory_view/index.tsx | 2 +- .../components/common/header/action_menu_content.test.tsx | 4 ++-- .../public/components/common/header/action_menu_content.tsx | 6 +++--- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx index 3bf21de7487de..2a1badd0ae1d8 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx @@ -19,14 +19,14 @@ import { InspectorHeaderLink } from '../../../shared/apm_header_action_menu/insp import { SERVICE_NAME } from '../../../../../common/elasticsearch_fieldnames'; const ANALYZE_DATA = i18n.translate('xpack.apm.analyzeDataButtonLabel', { - defaultMessage: 'Analyze data', + defaultMessage: 'Explore data', }); const ANALYZE_MESSAGE = i18n.translate( 'xpack.apm.analyzeDataButtonLabel.message', { defaultMessage: - 'EXPERIMENTAL - Analyze Data allows you to select and filter result data in any dimension and look for the cause or impact of performance problems.', + 'EXPERIMENTAL - Explore Data allows you to select and filter result data in any dimension and look for the cause or impact of performance problems.', } ); diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.tsx index a4fc964a444c9..5fa37050e71a6 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.tsx @@ -79,12 +79,12 @@ export function AnalyzeDataButton() { position="top" content={i18n.translate('xpack.apm.analyzeDataButton.tooltip', { defaultMessage: - 'EXPERIMENTAL - Analyze Data allows you to select and filter result data in any dimension, and look for the cause or impact of performance problems', + 'EXPERIMENTAL - Explore Data allows you to select and filter result data in any dimension, and look for the cause or impact of performance problems', })} > {i18n.translate('xpack.apm.analyzeDataButton.label', { - defaultMessage: 'Analyze data', + defaultMessage: 'Explore data', })} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx index 3de29b02853e8..1fc38ab79de7f 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx @@ -36,7 +36,7 @@ export function ExploratoryViewPage({ useBreadcrumbs([ { text: i18n.translate('xpack.observability.overview.exploratoryView', { - defaultMessage: 'Analyze data', + defaultMessage: 'Explore data', }), }, ]); diff --git a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.test.tsx b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.test.tsx index 76b9378ca4ff6..47dc2084a788f 100644 --- a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.test.tsx +++ b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.test.tsx @@ -35,11 +35,11 @@ describe('ActionMenuContent', () => { const { getByLabelText, getByText } = render(); const analyzeAnchor = getByLabelText( - 'Navigate to the "Analyze Data" view to visualize Synthetics/User data' + 'Navigate to the "Explore Data" view to visualize Synthetics/User data' ); expect(analyzeAnchor.getAttribute('href')).toContain('/app/observability/exploratory-view'); - expect(getByText('Analyze data')); + expect(getByText('Explore data')); }); it('renders Add Data link', () => { diff --git a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx index 26f9e28101ea4..7b510432f773b 100644 --- a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx +++ b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx @@ -26,12 +26,12 @@ const ADD_DATA_LABEL = i18n.translate('xpack.uptime.addDataButtonLabel', { }); const ANALYZE_DATA = i18n.translate('xpack.uptime.analyzeDataButtonLabel', { - defaultMessage: 'Analyze data', + defaultMessage: 'Explore data', }); const ANALYZE_MESSAGE = i18n.translate('xpack.uptime.analyzeDataButtonLabel.message', { defaultMessage: - 'EXPERIMENTAL - Analyze Data allows you to select and filter result data in any dimension and look for the cause or impact of performance problems.', + 'EXPERIMENTAL - Explore Data allows you to select and filter result data in any dimension and look for the cause or impact of performance problems.', }); export function ActionMenuContent(): React.ReactElement { @@ -87,7 +87,7 @@ export function ActionMenuContent(): React.ReactElement { {ANALYZE_MESSAGE}

    }> Date: Thu, 11 Nov 2021 06:22:31 -0500 Subject: [PATCH 36/43] [Observability] [Exploratory View] prevent chart from rerendering on report type changes (#118085) (#118153) Co-authored-by: Dominique Clarke --- .../hooks/use_lens_attributes.test.tsx | 73 +++++++++++++++++++ .../hooks/use_lens_attributes.ts | 6 +- .../hooks/use_series_storage.test.tsx | 61 +++++++++++++++- .../hooks/use_series_storage.tsx | 13 ++-- 4 files changed, 142 insertions(+), 11 deletions(-) create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.test.tsx diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.test.tsx new file mode 100644 index 0000000000000..3334b69e5becc --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.test.tsx @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { allSeriesKey, reportTypeKey, UrlStorageContextProvider } from './use_series_storage'; +import { renderHook } from '@testing-library/react-hooks'; +import { useLensAttributes } from './use_lens_attributes'; +import { ReportTypes } from '../configurations/constants'; +import { mockIndexPattern } from '../rtl_helpers'; +import { createKbnUrlStateStorage } from '../../../../../../../../src/plugins/kibana_utils/public'; +import { TRANSACTION_DURATION } from '../configurations/constants/elasticsearch_fieldnames'; +import * as lensAttributes from '../configurations/lens_attributes'; +import * as indexPattern from './use_app_index_pattern'; +import * as theme from '../../../../hooks/use_theme'; + +const mockSingleSeries = [ + { + name: 'performance-distribution', + dataType: 'ux', + breakdown: 'user_agent.name', + time: { from: 'now-15m', to: 'now' }, + selectedMetricField: TRANSACTION_DURATION, + reportDefinitions: { 'service.name': ['elastic-co'] }, + }, +]; + +describe('useExpViewTimeRange', function () { + const storage = createKbnUrlStateStorage({ useHash: false }); + // @ts-ignore + jest.spyOn(indexPattern, 'useAppIndexPatternContext').mockReturnValue({ + indexPatterns: { + ux: mockIndexPattern, + apm: mockIndexPattern, + mobile: mockIndexPattern, + infra_logs: mockIndexPattern, + infra_metrics: mockIndexPattern, + synthetics: mockIndexPattern, + }, + }); + jest.spyOn(theme, 'useTheme').mockReturnValue({ + // @ts-ignore + eui: { + euiColorVis1: '#111111', + }, + }); + const lensAttributesSpy = jest.spyOn(lensAttributes, 'LensAttributes'); + + function Wrapper({ children }: { children: JSX.Element }) { + return {children}; + } + + it('updates lens attributes with report type from storage', async function () { + await storage.set(allSeriesKey, mockSingleSeries); + await storage.set(reportTypeKey, ReportTypes.KPI); + + renderHook(() => useLensAttributes(), { + wrapper: Wrapper, + }); + + expect(lensAttributesSpy).toBeCalledWith( + expect.arrayContaining([ + expect.objectContaining({ + seriesConfig: expect.objectContaining({ reportType: ReportTypes.KPI }), + }), + ]) + ); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts index ae3d57b3c9652..f81494e8f9ac7 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts @@ -13,6 +13,7 @@ import { AllSeries, allSeriesKey, convertAllShortSeries, + reportTypeKey, useSeriesStorage, } from './use_series_storage'; import { getDefaultConfigs } from '../configurations/default_configs'; @@ -93,11 +94,12 @@ export const useLensAttributes = (): TypedLensByValueInput['attributes'] | null return useMemo(() => { // we only use the data from url to apply, since that gets updated to apply changes const allSeriesT: AllSeries = convertAllShortSeries(storage.get(allSeriesKey) ?? []); + const reportTypeT: ReportViewType = storage.get(reportTypeKey) as ReportViewType; - if (isEmpty(indexPatterns) || isEmpty(allSeriesT) || !reportType) { + if (isEmpty(indexPatterns) || isEmpty(allSeriesT) || !reportTypeT) { return null; } - const layerConfigs = getLayerConfigs(allSeriesT, reportType, theme, indexPatterns); + const layerConfigs = getLayerConfigs(allSeriesT, reportTypeT, theme, indexPatterns); if (layerConfigs.length < 1) { return null; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx index 1d23796b5bf55..6abb0416d0908 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx @@ -9,9 +9,10 @@ import React, { useEffect } from 'react'; import { act, renderHook } from '@testing-library/react-hooks'; import { Route, Router } from 'react-router-dom'; import { render } from '@testing-library/react'; -import { UrlStorageContextProvider, useSeriesStorage } from './use_series_storage'; +import { UrlStorageContextProvider, useSeriesStorage, reportTypeKey } from './use_series_storage'; import { getHistoryFromUrl } from '../rtl_helpers'; import type { AppDataType } from '../types'; +import { ReportTypes } from '../configurations/constants'; import * as useTrackMetric from '../../../../hooks/use_track_metric'; const mockSingleSeries = [ @@ -163,6 +164,64 @@ describe('userSeriesStorage', function () { ]); }); + it('sets reportType when calling applyChanges', () => { + const setStorage = jest.fn(); + function wrapper({ children }: { children: React.ReactElement }) { + return ( + + key === 'sr' ? mockMultipleSeries : 'kpi-over-time' + ), + set: setStorage, + }} + > + {children} + + ); + } + const { result } = renderHook(() => useSeriesStorage(), { wrapper }); + + act(() => { + result.current.setReportType(ReportTypes.DISTRIBUTION); + }); + + act(() => { + result.current.applyChanges(); + }); + + expect(setStorage).toBeCalledWith(reportTypeKey, ReportTypes.DISTRIBUTION); + }); + + it('returns reportType in state, not url storage, from hook', () => { + const setStorage = jest.fn(); + function wrapper({ children }: { children: React.ReactElement }) { + return ( + + key === 'sr' ? mockMultipleSeries : 'kpi-over-time' + ), + set: setStorage, + }} + > + {children} + + ); + } + const { result } = renderHook(() => useSeriesStorage(), { wrapper }); + + act(() => { + result.current.setReportType(ReportTypes.DISTRIBUTION); + }); + + expect(result.current.reportType).toEqual(ReportTypes.DISTRIBUTION); + }); + it('ensures that telemetry is called', () => { const trackEvent = jest.fn(); jest.spyOn(useTrackMetric, 'useUiTracker').mockReturnValue(trackEvent); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx index 2e8369bd1ddd4..3fca13f7978d6 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx @@ -32,7 +32,7 @@ export interface SeriesContextValue { setSeries: (seriesIndex: number, newValue: SeriesUrl) => void; getSeries: (seriesIndex: number) => SeriesUrl | undefined; removeSeries: (seriesIndex: number) => void; - setReportType: (reportType: string) => void; + setReportType: (reportType: ReportViewType) => void; storage: IKbnUrlStateStorage | ISessionStorageStateStorage; reportType: ReportViewType; } @@ -59,8 +59,8 @@ export function UrlStorageContextProvider({ const [lastRefresh, setLastRefresh] = useState(() => Date.now()); - const [reportType, setReportType] = useState( - () => (storage as IKbnUrlStateStorage).get(reportTypeKey) ?? '' + const [reportType, setReportType] = useState( + () => ((storage as IKbnUrlStateStorage).get(reportTypeKey) ?? '') as ReportViewType ); const [firstSeries, setFirstSeries] = useState(); @@ -97,10 +97,6 @@ export function UrlStorageContextProvider({ }); }, []); - useEffect(() => { - (storage as IKbnUrlStateStorage).set(reportTypeKey, reportType); - }, [reportType, storage]); - const removeSeries = useCallback((seriesIndex: number) => { setAllSeries((prevAllSeries) => prevAllSeries.filter((seriesT, index) => index !== seriesIndex) @@ -117,6 +113,7 @@ export function UrlStorageContextProvider({ const applyChanges = useCallback( (onApply?: () => void) => { const allShortSeries = allSeries.map((series) => convertToShortUrl(series)); + (storage as IKbnUrlStateStorage).set(reportTypeKey, reportType); (storage as IKbnUrlStateStorage).set(allSeriesKey, allShortSeries); setLastRefresh(Date.now()); @@ -140,7 +137,7 @@ export function UrlStorageContextProvider({ lastRefresh, setLastRefresh, setReportType, - reportType: storage.get(reportTypeKey) as ReportViewType, + reportType, firstSeries: firstSeries!, }; return {children}; From 74f1a86880152e5d620628790042671de371a099 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 11 Nov 2021 06:43:26 -0500 Subject: [PATCH 37/43] [Stack Monitoring] Update security messaging around alerting (#117505) (#118305) * Add a getSecurityHealth helper to alerting and update messaging around alerting in Stack Monitoring Co-authored-by: ymao1 Co-authored-by: Kerry Gallagher <471693+Kerry350@users.noreply.github.com> Co-authored-by: ymao1 --- .../server/lib/get_security_health.test.ts | 84 +++++++++++++++++++ .../server/lib/get_security_health.ts | 38 +++++++++ x-pack/plugins/alerting/server/mocks.ts | 1 + x-pack/plugins/alerting/server/plugin.ts | 12 +++ .../plugins/alerting/server/routes/health.ts | 20 ++--- .../alerting/server/routes/legacy/health.ts | 19 ++--- .../public/alerts/lib/alerts_toast.tsx | 6 +- .../elasticsearch/verify_alerting_security.ts | 49 ----------- x-pack/plugins/monitoring/server/plugin.ts | 1 + .../server/routes/api/v1/alerts/enable.ts | 9 +- x-pack/plugins/monitoring/server/types.ts | 2 + 11 files changed, 160 insertions(+), 81 deletions(-) create mode 100644 x-pack/plugins/alerting/server/lib/get_security_health.test.ts create mode 100644 x-pack/plugins/alerting/server/lib/get_security_health.ts delete mode 100644 x-pack/plugins/monitoring/server/lib/elasticsearch/verify_alerting_security.ts diff --git a/x-pack/plugins/alerting/server/lib/get_security_health.test.ts b/x-pack/plugins/alerting/server/lib/get_security_health.test.ts new file mode 100644 index 0000000000000..1253e0c3379b5 --- /dev/null +++ b/x-pack/plugins/alerting/server/lib/get_security_health.test.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getSecurityHealth } from './get_security_health'; + +const createDependencies = ( + isSecurityEnabled: boolean | null, + canEncrypt: boolean, + apiKeysEnabled: boolean +) => { + const isEsSecurityEnabled = async () => isSecurityEnabled; + const isAbleToEncrypt = async () => canEncrypt; + const areApikeysEnabled = async () => apiKeysEnabled; + + const deps: [() => Promise, () => Promise, () => Promise] = [ + isEsSecurityEnabled, + isAbleToEncrypt, + areApikeysEnabled, + ]; + + return deps; +}; + +describe('Get security health', () => { + describe('Correctly returns the overall security health', () => { + test('When ES security enabled status cannot be determined', async () => { + const deps = createDependencies(null, true, true); + const securityHealth = await getSecurityHealth(...deps); + expect(securityHealth).toEqual({ + isSufficientlySecure: false, + hasPermanentEncryptionKey: true, + }); + }); + + test('When ES security is disabled', async () => { + const deps = createDependencies(false, true, true); + const securityHealth = await getSecurityHealth(...deps); + expect(securityHealth).toEqual({ + isSufficientlySecure: true, + hasPermanentEncryptionKey: true, + }); + }); + + test('When ES security is enabled, and API keys are disabled', async () => { + const deps = createDependencies(true, true, false); + const securityHealth = await getSecurityHealth(...deps); + expect(securityHealth).toEqual({ + isSufficientlySecure: false, + hasPermanentEncryptionKey: true, + }); + }); + + test('When ES security is enabled, and API keys are enabled', async () => { + const deps = createDependencies(true, true, true); + const securityHealth = await getSecurityHealth(...deps); + expect(securityHealth).toEqual({ + isSufficientlySecure: true, + hasPermanentEncryptionKey: true, + }); + }); + + test('With encryption enabled', async () => { + const deps = createDependencies(true, true, true); + const securityHealth = await getSecurityHealth(...deps); + expect(securityHealth).toEqual({ + isSufficientlySecure: true, + hasPermanentEncryptionKey: true, + }); + }); + + test('With encryption disabled', async () => { + const deps = createDependencies(true, false, true); + const securityHealth = await getSecurityHealth(...deps); + expect(securityHealth).toEqual({ + isSufficientlySecure: true, + hasPermanentEncryptionKey: false, + }); + }); + }); +}); diff --git a/x-pack/plugins/alerting/server/lib/get_security_health.ts b/x-pack/plugins/alerting/server/lib/get_security_health.ts new file mode 100644 index 0000000000000..1a2097221433b --- /dev/null +++ b/x-pack/plugins/alerting/server/lib/get_security_health.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface SecurityHealth { + isSufficientlySecure: boolean; + hasPermanentEncryptionKey: boolean; +} + +export const getSecurityHealth = async ( + isEsSecurityEnabled: () => Promise, + isAbleToEncrypt: () => Promise, + areApiKeysEnabled: () => Promise +) => { + const esSecurityIsEnabled = await isEsSecurityEnabled(); + const apiKeysAreEnabled = await areApiKeysEnabled(); + const ableToEncrypt = await isAbleToEncrypt(); + + let isSufficientlySecure: boolean; + + if (esSecurityIsEnabled === null) { + isSufficientlySecure = false; + } else { + // if esSecurityIsEnabled = true, then areApiKeysEnabled must be true to enable alerting + // if esSecurityIsEnabled = false, then it does not matter what areApiKeysEnabled is + isSufficientlySecure = !esSecurityIsEnabled || (esSecurityIsEnabled && apiKeysAreEnabled); + } + + const securityHealth: SecurityHealth = { + isSufficientlySecure, + hasPermanentEncryptionKey: ableToEncrypt, + }; + + return securityHealth; +}; diff --git a/x-pack/plugins/alerting/server/mocks.ts b/x-pack/plugins/alerting/server/mocks.ts index 639ba166e00a8..7fb748a305037 100644 --- a/x-pack/plugins/alerting/server/mocks.ts +++ b/x-pack/plugins/alerting/server/mocks.ts @@ -19,6 +19,7 @@ export { rulesClientMock }; const createSetupMock = () => { const mock: jest.Mocked = { registerType: jest.fn(), + getSecurityHealth: jest.fn(), }; return mock; }; diff --git a/x-pack/plugins/alerting/server/plugin.ts b/x-pack/plugins/alerting/server/plugin.ts index f0703defbca3d..982d8907d9b9d 100644 --- a/x-pack/plugins/alerting/server/plugin.ts +++ b/x-pack/plugins/alerting/server/plugin.ts @@ -65,6 +65,7 @@ import { AlertsConfig } from './config'; import { getHealth } from './health/get_health'; import { AlertingAuthorizationClientFactory } from './alerting_authorization_client_factory'; import { AlertingAuthorization } from './authorization'; +import { getSecurityHealth, SecurityHealth } from './lib/get_security_health'; export const EVENT_LOG_PROVIDER = 'alerting'; export const EVENT_LOG_ACTIONS = { @@ -99,6 +100,7 @@ export interface PluginSetupContract { RecoveryActionGroupId > ): void; + getSecurityHealth: () => Promise; } export interface PluginStartContract { @@ -315,6 +317,16 @@ export class AlertingPlugin { ruleTypeRegistry.register(alertType); } }, + getSecurityHealth: async () => { + return await getSecurityHealth( + async () => (this.licenseState ? this.licenseState.getIsSecurityEnabled() : null), + async () => plugins.encryptedSavedObjects.canEncrypt, + async () => { + const [, { security }] = await core.getStartServices(); + return security?.authc.apiKeys.areAPIKeysEnabled() ?? false; + } + ); + }, }; } diff --git a/x-pack/plugins/alerting/server/routes/health.ts b/x-pack/plugins/alerting/server/routes/health.ts index fa09213dada3a..4f3ed2b542611 100644 --- a/x-pack/plugins/alerting/server/routes/health.ts +++ b/x-pack/plugins/alerting/server/routes/health.ts @@ -14,6 +14,7 @@ import { BASE_ALERTING_API_PATH, AlertingFrameworkHealth, } from '../types'; +import { getSecurityHealth } from '../lib/get_security_health'; const rewriteBodyRes: RewriteResponseCase = ({ isSufficientlySecure, @@ -44,23 +45,16 @@ export const healthRoute = ( router.handleLegacyErrors( verifyAccessAndContext(licenseState, async function (context, req, res) { try { - const isEsSecurityEnabled: boolean | null = licenseState.getIsSecurityEnabled(); - const areApiKeysEnabled = await context.alerting.areApiKeysEnabled(); const alertingFrameworkHeath = await context.alerting.getFrameworkHealth(); - let isSufficientlySecure; - if (isEsSecurityEnabled === null) { - isSufficientlySecure = false; - } else { - // if isEsSecurityEnabled = true, then areApiKeysEnabled must be true to enable alerting - // if isEsSecurityEnabled = false, then it does not matter what areApiKeysEnabled is - isSufficientlySecure = - !isEsSecurityEnabled || (isEsSecurityEnabled && areApiKeysEnabled); - } + const securityHealth = await getSecurityHealth( + async () => (licenseState ? licenseState.getIsSecurityEnabled() : null), + async () => encryptedSavedObjects.canEncrypt, + context.alerting.areApiKeysEnabled + ); const frameworkHealth: AlertingFrameworkHealth = { - isSufficientlySecure, - hasPermanentEncryptionKey: encryptedSavedObjects.canEncrypt, + ...securityHealth, alertingFrameworkHeath, }; diff --git a/x-pack/plugins/alerting/server/routes/legacy/health.ts b/x-pack/plugins/alerting/server/routes/legacy/health.ts index 8c654f103ea86..abea724b63c6f 100644 --- a/x-pack/plugins/alerting/server/routes/legacy/health.ts +++ b/x-pack/plugins/alerting/server/routes/legacy/health.ts @@ -12,6 +12,7 @@ import { verifyApiAccess } from '../../lib/license_api_access'; import { AlertingFrameworkHealth } from '../../types'; import { EncryptedSavedObjectsPluginSetup } from '../../../../encrypted_saved_objects/server'; import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage'; +import { getSecurityHealth } from '../../lib/get_security_health'; export function healthRoute( router: AlertingRouter, @@ -31,22 +32,16 @@ export function healthRoute( } trackLegacyRouteUsage('health', usageCounter); try { - const isEsSecurityEnabled: boolean | null = licenseState.getIsSecurityEnabled(); const alertingFrameworkHeath = await context.alerting.getFrameworkHealth(); - const areApiKeysEnabled = await context.alerting.areApiKeysEnabled(); - let isSufficientlySecure; - if (isEsSecurityEnabled === null) { - isSufficientlySecure = false; - } else { - // if isEsSecurityEnabled = true, then areApiKeysEnabled must be true to enable alerting - // if isEsSecurityEnabled = false, then it does not matter what areApiKeysEnabled is - isSufficientlySecure = !isEsSecurityEnabled || (isEsSecurityEnabled && areApiKeysEnabled); - } + const securityHealth = await getSecurityHealth( + async () => (licenseState ? licenseState.getIsSecurityEnabled() : null), + async () => encryptedSavedObjects.canEncrypt, + context.alerting.areApiKeysEnabled + ); const frameworkHealth: AlertingFrameworkHealth = { - isSufficientlySecure, - hasPermanentEncryptionKey: encryptedSavedObjects.canEncrypt, + ...securityHealth, alertingFrameworkHeath, }; diff --git a/x-pack/plugins/monitoring/public/alerts/lib/alerts_toast.tsx b/x-pack/plugins/monitoring/public/alerts/lib/alerts_toast.tsx index d752ec154089b..10c8f7155134b 100644 --- a/x-pack/plugins/monitoring/public/alerts/lib/alerts_toast.tsx +++ b/x-pack/plugins/monitoring/public/alerts/lib/alerts_toast.tsx @@ -18,7 +18,7 @@ export interface EnableAlertResponse { disabledWatcherClusterAlerts?: boolean; } -const showTlsAndEncryptionError = () => { +const showApiKeyAndEncryptionError = () => { const settingsUrl = Legacy.shims.docLinks.links.alerting.generalSettings; Legacy.shims.toastNotifications.addWarning({ @@ -32,7 +32,7 @@ const showTlsAndEncryptionError = () => {

    {i18n.translate('xpack.monitoring.healthCheck.tlsAndEncryptionError', { - defaultMessage: `Stack monitoring alerts require Transport Layer Security between Kibana and Elasticsearch, and an encryption key in your kibana.yml file.`, + defaultMessage: `Stack Monitoring rules require API keys to be enabled and an encryption key to be configured.`, })}

    @@ -97,7 +97,7 @@ export const showAlertsToast = (response: EnableAlertResponse) => { response; if (isSufficientlySecure === false || hasPermanentEncryptionKey === false) { - showTlsAndEncryptionError(); + showApiKeyAndEncryptionError(); } else if (disabledWatcherClusterAlerts === false) { showUnableToDisableWatcherClusterAlertsError(); } else if (disabledWatcherClusterAlerts === true) { diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_alerting_security.ts b/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_alerting_security.ts deleted file mode 100644 index f5f9c80e0e4d3..0000000000000 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/verify_alerting_security.ts +++ /dev/null @@ -1,49 +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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { RequestHandlerContext } from 'kibana/server'; -import { EncryptedSavedObjectsPluginSetup } from '../../../../encrypted_saved_objects/server'; - -export interface AlertingFrameworkHealth { - isSufficientlySecure: boolean; - hasPermanentEncryptionKey: boolean; -} - -export interface XPackUsageSecurity { - security?: { - enabled?: boolean; - ssl?: { - http?: { - enabled?: boolean; - }; - }; - }; -} - -export class AlertingSecurity { - public static readonly getSecurityHealth = async ( - context: RequestHandlerContext, - encryptedSavedObjects?: EncryptedSavedObjectsPluginSetup - ): Promise => { - const { - security: { - enabled: isSecurityEnabled = false, - ssl: { http: { enabled: isTLSEnabled = false } = {} } = {}, - } = {}, - } = ( - await context.core.elasticsearch.client.asInternalUser.transport.request({ - method: 'GET', - path: '/_xpack/usage', - }) - ).body as XPackUsageSecurity; - - return { - isSufficientlySecure: !isSecurityEnabled || (isSecurityEnabled && isTLSEnabled), - hasPermanentEncryptionKey: encryptedSavedObjects?.canEncrypt === true, - }; - }; -} diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index 557a9b5e2a3d2..ff07ea0f4a26d 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -202,6 +202,7 @@ export class MonitoringPlugin router, licenseService: this.licenseService, encryptedSavedObjects: plugins.encryptedSavedObjects, + alerting: plugins.alerting, logger: this.log, }); initInfraSource(config, plugins.infra); diff --git a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts index 6724819c30d56..7185d399b3534 100644 --- a/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts +++ b/x-pack/plugins/monitoring/server/routes/api/v1/alerts/enable.ts @@ -11,7 +11,6 @@ import { AlertsFactory } from '../../../../alerts'; import { LegacyServer, RouteDependencies } from '../../../../types'; import { ALERT_ACTION_TYPE_LOG } from '../../../../../common/constants'; import { ActionResult } from '../../../../../../actions/common'; -import { AlertingSecurity } from '../../../../lib/elasticsearch/verify_alerting_security'; import { disableWatcherClusterAlerts } from '../../../../lib/alerts/disable_watcher_cluster_alerts'; import { AlertTypeParams, SanitizedAlert } from '../../../../../../alerting/common'; @@ -38,12 +37,14 @@ export function enableAlertsRoute(server: LegacyServer, npRoute: RouteDependenci const alerts = AlertsFactory.getAll(); if (alerts.length) { - const { isSufficientlySecure, hasPermanentEncryptionKey } = - await AlertingSecurity.getSecurityHealth(context, npRoute.encryptedSavedObjects); + const { isSufficientlySecure, hasPermanentEncryptionKey } = npRoute.alerting + ?.getSecurityHealth + ? await npRoute.alerting?.getSecurityHealth() + : { isSufficientlySecure: false, hasPermanentEncryptionKey: false }; if (!isSufficientlySecure || !hasPermanentEncryptionKey) { server.log.info( - `Skipping alert creation for "${context.infra.spaceId}" space; Stack monitoring alerts require Transport Layer Security between Kibana and Elasticsearch, and an encryption key in your kibana.yml file.` + `Skipping rule creation for "${context.infra.spaceId}" space; Stack Monitoring rules require API keys to be enabled and an encryption key to be configured.` ); return response.ok({ body: { diff --git a/x-pack/plugins/monitoring/server/types.ts b/x-pack/plugins/monitoring/server/types.ts index 14071aafaea12..14023ccce41ae 100644 --- a/x-pack/plugins/monitoring/server/types.ts +++ b/x-pack/plugins/monitoring/server/types.ts @@ -28,6 +28,7 @@ import { PluginSetupContract as AlertingPluginSetupContract, } from '../../alerting/server'; import { InfraPluginSetup, InfraRequestHandlerContext } from '../../infra/server'; +import { PluginSetupContract as AlertingPluginSetup } from '../../alerting/server'; import { LicensingPluginStart } from '../../licensing/server'; import { PluginSetupContract as FeaturesPluginSetupContract } from '../../features/server'; import { EncryptedSavedObjectsPluginSetup } from '../../encrypted_saved_objects/server'; @@ -80,6 +81,7 @@ export interface RouteDependencies { router: IRouter; licenseService: MonitoringLicenseService; encryptedSavedObjects?: EncryptedSavedObjectsPluginSetup; + alerting?: AlertingPluginSetup; logger: Logger; } From 9a6bbbd35800e6ca3d1a996179a6a715a7ee3729 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 11 Nov 2021 07:21:02 -0500 Subject: [PATCH 38/43] Mark index as hidden within index template (#117839) (#118312) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Kerry Gallagher --- .../server/rule_data_plugin_service/resource_installer.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.ts index 3798506eeacd1..bfdec28a50987 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.ts @@ -309,6 +309,7 @@ export class ResourceInstaller { template: { settings: { + hidden: true, 'index.lifecycle': { name: ilmPolicyName, // TODO: fix the types in the ES package, they don't include rollover_alias??? From e671b459b039c74805f48dac4da6da82e9705d86 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 11 Nov 2021 07:47:27 -0500 Subject: [PATCH 39/43] Change Import Rules Modal Description (#118216) (#118316) Co-authored-by: Vitalii Dmyterko <92328789+vitaliidm@users.noreply.github.com> --- .../detections/pages/detection_engine/rules/translations.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts index 15ff91cac5096..ca1b1f57b8399 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts @@ -551,7 +551,8 @@ export const IMPORT_RULE_BTN_TITLE = i18n.translate( export const SELECT_RULE = i18n.translate( 'xpack.securitySolution.detectionEngine.components.importRuleModal.selectRuleDescription', { - defaultMessage: 'Select Security rules (as exported from the Detection Rules page) to import', + defaultMessage: + 'Select rules and actions (as exported from the Security > Rules page) to import', } ); From 79cb1913d39f2b66bfaaf9406346e785bc40310b Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 11 Nov 2021 07:48:47 -0500 Subject: [PATCH 40/43] [User Experience] Add error boundary to prevent UX dashboard from crashing the application (#117583) (#117912) * wrap UX dashboard into an error boundary (fixes #117543) * refactor APM root app tests to reuse coreMock Before this change, the tests for the root application component of the APM app were manually mocking the `coreStart` objects required to render the component. After this change, these tests will now reuse the relevant `coreMock` methods. * refactor: fix typo on createAppMountParameters test utility Co-authored-by: Lucas Fernandes da Costa Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Lucas F. da Costa Co-authored-by: Lucas Fernandes da Costa --- dev_docs/tutorials/testing_plugins.mdx | 8 +- src/core/public/mocks.ts | 2 +- .../public/application/application.test.tsx | 72 +++++++++---- .../plugins/apm/public/application/uxApp.tsx | 5 +- .../apm_plugin/mock_apm_plugin_context.tsx | 101 +++++++----------- .../public/applications/index.test.tsx | 2 +- .../capture_url/capture_url_app.test.ts | 4 +- 7 files changed, 105 insertions(+), 89 deletions(-) diff --git a/dev_docs/tutorials/testing_plugins.mdx b/dev_docs/tutorials/testing_plugins.mdx index b8f8029e7b16c..044d610aa3489 100644 --- a/dev_docs/tutorials/testing_plugins.mdx +++ b/dev_docs/tutorials/testing_plugins.mdx @@ -458,7 +458,7 @@ describe('Plugin', () => { const [coreStartMock, startDepsMock] = await coreSetup.getStartServices(); const unmountMock = jest.fn(); renderAppMock.mockReturnValue(unmountMock); - const params = coreMock.createAppMountParamters('/fake/base/path'); + const params = coreMock.createAppMountParameters('/fake/base/path'); new Plugin(coreMock.createPluginInitializerContext()).setup(coreSetup); // Grab registered mount function @@ -528,7 +528,7 @@ import { renderApp } from './application'; describe('renderApp', () => { it('mounts and unmounts UI', () => { - const params = coreMock.createAppMountParamters('/fake/base/path'); + const params = coreMock.createAppMountParameters('/fake/base/path'); const core = coreMock.createStart(); // Verify some expected DOM element is rendered into the element @@ -540,7 +540,7 @@ describe('renderApp', () => { }); it('unsubscribes from uiSettings', () => { - const params = coreMock.createAppMountParamters('/fake/base/path'); + const params = coreMock.createAppMountParameters('/fake/base/path'); const core = coreMock.createStart(); // Create a fake Subject you can use to monitor observers const settings$ = new Subject(); @@ -555,7 +555,7 @@ describe('renderApp', () => { }); it('resets chrome visibility', () => { - const params = coreMock.createAppMountParamters('/fake/base/path'); + const params = coreMock.createAppMountParameters('/fake/base/path'); const core = coreMock.createStart(); // Verify stateful Core API was called on mount diff --git a/src/core/public/mocks.ts b/src/core/public/mocks.ts index bd7623beba651..39d2dc3d5c497 100644 --- a/src/core/public/mocks.ts +++ b/src/core/public/mocks.ts @@ -169,5 +169,5 @@ export const coreMock = { createStart: createCoreStartMock, createPluginInitializerContext: pluginInitializerContextMock, createStorage: createStorageMock, - createAppMountParamters: createAppMountParametersMock, + createAppMountParameters: createAppMountParametersMock, }; diff --git a/x-pack/plugins/apm/public/application/application.test.tsx b/x-pack/plugins/apm/public/application/application.test.tsx index 56e7b4684acde..12170ac20b7df 100644 --- a/x-pack/plugins/apm/public/application/application.test.tsx +++ b/x-pack/plugins/apm/public/application/application.test.tsx @@ -7,24 +7,31 @@ import React from 'react'; import { act } from '@testing-library/react'; +import { EuiErrorBoundary } from '@elastic/eui'; +import { mount } from 'enzyme'; import { createMemoryHistory } from 'history'; import { Observable } from 'rxjs'; -import { CoreStart, DocLinksStart, HttpStart } from 'src/core/public'; +import { AppMountParameters, DocLinksStart, HttpStart } from 'src/core/public'; import { mockApmPluginContextValue } from '../context/apm_plugin/mock_apm_plugin_context'; import { createCallApmApi } from '../services/rest/createCallApmApi'; -import { renderApp } from './'; +import { renderApp as renderApmApp } from './'; +import { UXAppRoot } from './uxApp'; import { disableConsoleWarning } from '../utils/testHelpers'; import { dataPluginMock } from 'src/plugins/data/public/mocks'; import { embeddablePluginMock } from 'src/plugins/embeddable/public/mocks'; -import { ApmPluginStartDeps } from '../plugin'; +import { ApmPluginSetupDeps, ApmPluginStartDeps } from '../plugin'; +import { RumHome } from '../components/app/RumDashboard/RumHome'; jest.mock('../services/rest/data_view', () => ({ createStaticDataView: () => Promise.resolve(undefined), })); -describe('renderApp', () => { - let mockConsole: jest.SpyInstance; +jest.mock('../components/app/RumDashboard/RumHome', () => ({ + RumHome: () =>

    Home Mock

    , +})); +describe('renderApp (APM)', () => { + let mockConsole: jest.SpyInstance; beforeAll(() => { // The RUM agent logs an unnecessary message here. There's a couple open // issues need to be fixed to get the ability to turn off all of the logging: @@ -40,11 +47,15 @@ describe('renderApp', () => { mockConsole.mockRestore(); }); - it('renders the app', () => { - const { core, config, observabilityRuleTypeRegistry } = - mockApmPluginContextValue; + const getApmMountProps = () => { + const { + core: coreStart, + config, + observabilityRuleTypeRegistry, + corePlugins, + } = mockApmPluginContextValue; - const plugins = { + const pluginsSetup = { licensing: { license$: new Observable() }, triggersActionsUi: { actionTypeRegistry: {}, ruleTypeRegistry: {} }, data: { @@ -99,7 +110,7 @@ describe('renderApp', () => { } as unknown as ApmPluginStartDeps; jest.spyOn(window, 'scrollTo').mockReturnValueOnce(undefined); - createCallApmApi(core as unknown as CoreStart); + createCallApmApi(coreStart); jest .spyOn(window.console, 'warn') @@ -111,17 +122,24 @@ describe('renderApp', () => { } }); + return { + coreStart, + pluginsSetup: pluginsSetup as unknown as ApmPluginSetupDeps, + appMountParameters: appMountParameters as unknown as AppMountParameters, + pluginsStart, + config, + observabilityRuleTypeRegistry, + corePlugins, + }; + }; + + it('renders the app', () => { + const mountProps = getApmMountProps(); + let unmount: () => void; act(() => { - unmount = renderApp({ - coreStart: core as any, - pluginsSetup: plugins as any, - appMountParameters: appMountParameters as any, - pluginsStart, - config, - observabilityRuleTypeRegistry, - }); + unmount = renderApmApp(mountProps); }); expect(() => { @@ -129,3 +147,21 @@ describe('renderApp', () => { }).not.toThrowError(); }); }); + +describe('renderUxApp', () => { + it('has an error boundary for the UXAppRoot', async () => { + const uxMountProps = mockApmPluginContextValue; + + const wrapper = mount(); + + wrapper + .find(RumHome) + .simulateError(new Error('Oh no, an unexpected error!')); + + expect(wrapper.find(RumHome)).toHaveLength(0); + expect(wrapper.find(EuiErrorBoundary)).toHaveLength(1); + expect(wrapper.find(EuiErrorBoundary).text()).toMatch( + /Error: Oh no, an unexpected error!/ + ); + }); +}); diff --git a/x-pack/plugins/apm/public/application/uxApp.tsx b/x-pack/plugins/apm/public/application/uxApp.tsx index 4c4d68839a2f4..cfb1a5c354c2d 100644 --- a/x-pack/plugins/apm/public/application/uxApp.tsx +++ b/x-pack/plugins/apm/public/application/uxApp.tsx @@ -6,6 +6,7 @@ */ import { euiLightVars, euiDarkVars } from '@kbn/ui-shared-deps-src/theme'; +import { EuiErrorBoundary } from '@elastic/eui'; import { AppMountParameters, CoreStart } from 'kibana/public'; import React from 'react'; import ReactDOM from 'react-dom'; @@ -132,7 +133,9 @@ export function UXAppRoot({ - + + + diff --git a/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx b/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx index abdab939f4a0a..b519388a8bac7 100644 --- a/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx +++ b/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx @@ -6,11 +6,11 @@ */ import React, { ReactNode, useMemo } from 'react'; -import { Observable, of } from 'rxjs'; import { RouterProvider } from '@kbn/typed-react-router-config'; import { useHistory } from 'react-router-dom'; import { createMemoryHistory, History } from 'history'; import { merge } from 'lodash'; +import { coreMock } from '../../../../../../src/core/public/mocks'; import { UrlService } from '../../../../../../src/plugins/share/common/url_service'; import { createObservabilityRuleTypeRegistryMock } from '../../../../observability/public'; import { ApmPluginContext, ApmPluginContextValue } from './apm_plugin_context'; @@ -20,72 +20,43 @@ import { createCallApmApi } from '../../services/rest/createCallApmApi'; import { apmRouter } from '../../components/routing/apm_route_config'; import { MlLocatorDefinition } from '../../../../ml/public'; -const uiSettings: Record = { - [UI_SETTINGS.TIMEPICKER_QUICK_RANGES]: [ - { - from: 'now/d', - to: 'now/d', - display: 'Today', - }, - { - from: 'now/w', - to: 'now/w', - display: 'This week', - }, - ], - [UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS]: { - from: 'now-15m', - to: 'now', - }, - [UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS]: { - pause: false, - value: 100000, - }, -}; +const coreStart = coreMock.createStart({ basePath: '/basepath' }); -const mockCore = { +const mockCore = merge({}, coreStart, { application: { capabilities: { apm: {}, ml: {}, }, - currentAppId$: new Observable(), - getUrlForApp: (appId: string) => '', - navigateToUrl: (url: string) => {}, - }, - chrome: { - docTitle: { change: () => {} }, - setBreadcrumbs: () => {}, - setHelpExtension: () => {}, - setBadge: () => {}, - }, - docLinks: { - DOC_LINK_VERSION: '0', - ELASTIC_WEBSITE_URL: 'https://www.elastic.co/', - links: { - apm: {}, - }, - }, - http: { - basePath: { - prepend: (path: string) => `/basepath${path}`, - get: () => `/basepath`, - }, - }, - i18n: { - Context: ({ children }: { children: ReactNode }) => children, - }, - notifications: { - toasts: { - addWarning: () => {}, - addDanger: () => {}, - }, }, uiSettings: { - get: (key: string) => uiSettings[key], - get$: (key: string) => of(mockCore.uiSettings.get(key)), + get: (key: string) => { + const uiSettings: Record = { + [UI_SETTINGS.TIMEPICKER_QUICK_RANGES]: [ + { + from: 'now/d', + to: 'now/d', + display: 'Today', + }, + { + from: 'now/w', + to: 'now/w', + display: 'This week', + }, + ], + [UI_SETTINGS.TIMEPICKER_TIME_DEFAULTS]: { + from: 'now-15m', + to: 'now', + }, + [UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS]: { + pause: false, + value: 100000, + }, + }; + return uiSettings[key]; + }, }, -}; +}); const mockConfig: ConfigSchema = { serviceMapEnabled: true, @@ -118,16 +89,22 @@ const mockPlugin = { }, }; -const mockAppMountParameters = { - setHeaderActionMenu: () => {}, +const mockCorePlugins = { + embeddable: {}, + inspector: {}, + maps: {}, + observability: {}, + data: {}, }; export const mockApmPluginContextValue = { - appMountParameters: mockAppMountParameters, + appMountParameters: coreMock.createAppMountParameters('/basepath'), config: mockConfig, core: mockCore, plugins: mockPlugin, observabilityRuleTypeRegistry: createObservabilityRuleTypeRegistryMock(), + corePlugins: mockCorePlugins, + deps: {}, }; export function MockApmPluginContextWrapper({ @@ -135,7 +112,7 @@ export function MockApmPluginContextWrapper({ value = {} as ApmPluginContextValue, history, }: { - children?: React.ReactNode; + children?: ReactNode; value?: ApmPluginContextValue; history?: History; }) { diff --git a/x-pack/plugins/enterprise_search/public/applications/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/index.test.tsx index 5a82f15a6cf04..356a3c26b910e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.test.tsx @@ -23,7 +23,7 @@ import { renderApp, renderHeaderActions } from './'; describe('renderApp', () => { const kibanaDeps = { - params: coreMock.createAppMountParamters(), + params: coreMock.createAppMountParameters(), core: coreMock.createStart(), plugins: { licensing: licensingMock.createStart(), diff --git a/x-pack/plugins/security/public/authentication/capture_url/capture_url_app.test.ts b/x-pack/plugins/security/public/authentication/capture_url/capture_url_app.test.ts index 44fd5ab195341..0283df88fdb87 100644 --- a/x-pack/plugins/security/public/authentication/capture_url/capture_url_app.test.ts +++ b/x-pack/plugins/security/public/authentication/capture_url/capture_url_app.test.ts @@ -59,7 +59,7 @@ describe('captureURLApp', () => { captureURLApp.create(coreSetupMock); const [[{ mount }]] = coreSetupMock.application.register.mock.calls; - await mount(coreMock.createAppMountParamters()); + await mount(coreMock.createAppMountParameters()); expect(mockLocationReplace).toHaveBeenCalledTimes(1); expect(mockLocationReplace).toHaveBeenCalledWith( @@ -77,7 +77,7 @@ describe('captureURLApp', () => { captureURLApp.create(coreSetupMock); const [[{ mount }]] = coreSetupMock.application.register.mock.calls; - await mount(coreMock.createAppMountParamters()); + await mount(coreMock.createAppMountParameters()); expect(mockLocationReplace).toHaveBeenCalledTimes(1); expect(mockLocationReplace).toHaveBeenCalledWith( From 79cb334ae8ae50e8f78ee8c8453a3c6296e1ec43 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Thu, 11 Nov 2021 14:37:43 +0100 Subject: [PATCH 41/43] [Reporting] Fix browser diagnostic Jest test flakiness (#118003) (#118325) * mock and reduce the wait time for reading diagnoistic logs * remove comment * run test suite 42 times - REVERT THIS * Revert "run test suite 42 times - REVERT THIS" This reverts commit f9474aa1dbc4f215708ba6bdcb5ede1ed72411bd. * mock Rx.timer instead, revert previous solution * added comment * remove for loop Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../server/routes/diagnostic/browser.test.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/browser.test.ts b/x-pack/plugins/reporting/server/routes/diagnostic/browser.test.ts index 7b4cc2008a676..a27ce6a49b1a2 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/browser.test.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/browser.test.ts @@ -10,6 +10,7 @@ import { spawn } from 'child_process'; import { createInterface } from 'readline'; import { setupServer } from 'src/core/server/test_utils'; import supertest from 'supertest'; +import * as Rx from 'rxjs'; import { ReportingCore } from '../..'; import { createMockConfigSchema, @@ -28,8 +29,10 @@ type SetupServerReturn = UnwrapPromise>; const devtoolMessage = 'DevTools listening on (ws://localhost:4000)'; const fontNotFoundMessage = 'Could not find the default font'; -// FLAKY: https://github.com/elastic/kibana/issues/89369 -describe.skip('POST /diagnose/browser', () => { +const wait = (ms: number): Rx.Observable<0> => + Rx.from(new Promise<0>((resolve) => setTimeout(() => resolve(0), ms))); + +describe('POST /diagnose/browser', () => { jest.setTimeout(6000); const reportingSymbol = Symbol('reporting'); const mockLogger = createMockLevelLogger(); @@ -53,6 +56,9 @@ describe.skip('POST /diagnose/browser', () => { () => ({ usesUiCapabilities: () => false }) ); + // Make all uses of 'Rx.timer' return an observable that completes in 50ms + jest.spyOn(Rx, 'timer').mockImplementation(() => wait(50)); + core = await createMockReportingCore( config, createMockPluginSetup({ @@ -79,6 +85,7 @@ describe.skip('POST /diagnose/browser', () => { }); afterEach(async () => { + jest.restoreAllMocks(); await server.stop(); }); From 85126573927d536bd72f903a514317ebcb056630 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 11 Nov 2021 08:57:06 -0500 Subject: [PATCH 42/43] [APM] Removes warning about Stack Monitoring in Fleet migration (#117086) (#118289) (#118329) Co-authored-by: Oliver Gupte --- .../components/app/Settings/schema/confirm_switch_modal.tsx | 6 ------ x-pack/plugins/translations/translations/ja-JP.json | 1 - x-pack/plugins/translations/translations/zh-CN.json | 1 - 3 files changed, 8 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/Settings/schema/confirm_switch_modal.tsx b/x-pack/plugins/apm/public/components/app/Settings/schema/confirm_switch_modal.tsx index a13f31e8a6566..1847ea90bd7fa 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/schema/confirm_switch_modal.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/schema/confirm_switch_modal.tsx @@ -58,12 +58,6 @@ export function ConfirmSwitchModal({ }} confirmButtonDisabled={!isConfirmChecked} > -

    - {i18n.translate('xpack.apm.settings.schema.confirm.descriptionText', { - defaultMessage: - 'Please note Stack monitoring is not currently supported with Fleet-managed APM.', - })} -

    {!hasUnsupportedConfigs && (

    {i18n.translate( diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index ba8b2d1bc0615..955eb2c21a98b 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -6633,7 +6633,6 @@ "xpack.apm.settings.schema.confirm.apmServerSettingsCloudLinkText": "クラウドでAPMサーバー設定に移動", "xpack.apm.settings.schema.confirm.cancelText": "キャンセル", "xpack.apm.settings.schema.confirm.checkboxLabel": "データストリームに切り替えることを確認する", - "xpack.apm.settings.schema.confirm.descriptionText": "現在、スタック監視はFleetで管理されたAPMではサポートされていません。", "xpack.apm.settings.schema.confirm.irreversibleWarning.message": "移行中には一時的にAPMデータ収集に影響する可能性があります。移行プロセスは数分で完了します。", "xpack.apm.settings.schema.confirm.irreversibleWarning.title": "データストリームへの切り替えは元に戻せません。", "xpack.apm.settings.schema.confirm.switchButtonText": "データストリームに切り替える", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index a575f9a1afdfd..7beb480b4b2b0 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -6686,7 +6686,6 @@ "xpack.apm.settings.schema.confirm.apmServerSettingsCloudLinkText": "前往 Cloud 中的 APM Server 设置", "xpack.apm.settings.schema.confirm.cancelText": "取消", "xpack.apm.settings.schema.confirm.checkboxLabel": "我确认我想切换到数据流", - "xpack.apm.settings.schema.confirm.descriptionText": "请注意 Fleet 托管的 APM 当前不支持堆栈监测。", "xpack.apm.settings.schema.confirm.irreversibleWarning.message": "迁移在进行中时,可能会暂时影响 APM 数据收集。迁移的过程应只需花费几分钟。", "xpack.apm.settings.schema.confirm.irreversibleWarning.title": "切换到数据流是不可逆的操作", "xpack.apm.settings.schema.confirm.switchButtonText": "切换到数据流", From 9ac0a34f0ae3e37aac071ccb734d2a81006adda8 Mon Sep 17 00:00:00 2001 From: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Date: Thu, 11 Nov 2021 09:36:32 -0500 Subject: [PATCH 43/43] [APM] Reinstate ML multi-metric job (#117836) (#118333) Closes #101734. This reverts commit 008421f. Additionally, incorporate suggested changes from #101734 (comment). Co-authored-by: Dario Gieselaar --- .../anomaly_detection/apm_ml_anomaly_query.ts | 27 +++++ .../anomaly_detection/apm_ml_detectors.ts | 12 +++ .../index.ts} | 6 +- .../create_anomaly_detection_jobs.ts | 20 ++-- .../lib/service_map/get_service_anomalies.ts | 4 +- .../transactions/get_anomaly_data/fetcher.ts | 4 +- .../modules/apm_transaction/manifest.json | 18 ++-- .../apm_transaction/ml/apm_tx_metrics.json | 53 ++++++++++ .../ml/datafeed_apm_tx_metrics.json | 98 +++++++++++++++++++ ...tafeed_high_mean_transaction_duration.json | 17 ---- .../ml/high_mean_transaction_duration.json | 35 ------- .../apis/ml/modules/recognize_module.ts | 2 +- .../apis/ml/modules/setup_module.ts | 10 +- 13 files changed, 225 insertions(+), 81 deletions(-) create mode 100644 x-pack/plugins/apm/common/anomaly_detection/apm_ml_anomaly_query.ts create mode 100644 x-pack/plugins/apm/common/anomaly_detection/apm_ml_detectors.ts rename x-pack/plugins/apm/common/{anomaly_detection.ts => anomaly_detection/index.ts} (93%) create mode 100644 x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/apm_tx_metrics.json create mode 100644 x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_apm_tx_metrics.json delete mode 100644 x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_high_mean_transaction_duration.json delete mode 100644 x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/high_mean_transaction_duration.json diff --git a/x-pack/plugins/apm/common/anomaly_detection/apm_ml_anomaly_query.ts b/x-pack/plugins/apm/common/anomaly_detection/apm_ml_anomaly_query.ts new file mode 100644 index 0000000000000..00fb4b5ee4a54 --- /dev/null +++ b/x-pack/plugins/apm/common/anomaly_detection/apm_ml_anomaly_query.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ApmMlDetectorIndex } from './apm_ml_detectors'; + +export function apmMlAnomalyQuery(detectorIndex: ApmMlDetectorIndex) { + return [ + { + bool: { + filter: [ + { + terms: { + result_type: ['model_plot', 'record'], + }, + }, + { + term: { detector_index: detectorIndex }, + }, + ], + }, + }, + ]; +} diff --git a/x-pack/plugins/apm/common/anomaly_detection/apm_ml_detectors.ts b/x-pack/plugins/apm/common/anomaly_detection/apm_ml_detectors.ts new file mode 100644 index 0000000000000..7c68232122408 --- /dev/null +++ b/x-pack/plugins/apm/common/anomaly_detection/apm_ml_detectors.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const enum ApmMlDetectorIndex { + txLatency = 0, + txThroughput = 1, + txFailureRate = 2, +} diff --git a/x-pack/plugins/apm/common/anomaly_detection.ts b/x-pack/plugins/apm/common/anomaly_detection/index.ts similarity index 93% rename from x-pack/plugins/apm/common/anomaly_detection.ts rename to x-pack/plugins/apm/common/anomaly_detection/index.ts index 43a779407d2a4..cdb1c62b9eed5 100644 --- a/x-pack/plugins/apm/common/anomaly_detection.ts +++ b/x-pack/plugins/apm/common/anomaly_detection/index.ts @@ -6,12 +6,12 @@ */ import { i18n } from '@kbn/i18n'; -import { ANOMALY_SEVERITY } from './ml_constants'; +import { ANOMALY_SEVERITY } from '../ml_constants'; import { getSeverityType, getSeverityColor as mlGetSeverityColor, -} from '../../ml/common'; -import { ServiceHealthStatus } from './service_health_status'; +} from '../../../ml/common'; +import { ServiceHealthStatus } from '../service_health_status'; export interface ServiceAnomalyStats { transactionType?: string; 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 9d6abad0ff6a6..7277a12c2bf14 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 @@ -5,21 +5,21 @@ * 2.0. */ +import Boom from '@hapi/boom'; import { Logger } from 'kibana/server'; -import uuid from 'uuid/v4'; import { snakeCase } from 'lodash'; -import Boom from '@hapi/boom'; import moment from 'moment'; +import uuid from 'uuid/v4'; import { ML_ERRORS } from '../../../common/anomaly_detection'; -import { ProcessorEvent } from '../../../common/processor_event'; -import { environmentQuery } from '../../../common/utils/environment_query'; -import { Setup } from '../helpers/setup_request'; import { - TRANSACTION_DURATION, + METRICSET_NAME, PROCESSOR_EVENT, } from '../../../common/elasticsearch_fieldnames'; -import { APM_ML_JOB_GROUP, ML_MODULE_ID_APM_TRANSACTION } from './constants'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { environmentQuery } from '../../../common/utils/environment_query'; import { withApmSpan } from '../../utils/with_apm_span'; +import { Setup } from '../helpers/setup_request'; +import { APM_ML_JOB_GROUP, ML_MODULE_ID_APM_TRANSACTION } from './constants'; import { getAnomalyDetectionJobs } from './get_anomaly_detection_jobs'; export async function createAnomalyDetectionJobs( @@ -92,8 +92,8 @@ async function createAnomalyDetectionJob({ query: { bool: { filter: [ - { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, - { exists: { field: TRANSACTION_DURATION } }, + { term: { [PROCESSOR_EVENT]: ProcessorEvent.metric } }, + { term: { [METRICSET_NAME]: 'transaction' } }, ...environmentQuery(environment), ], }, @@ -105,7 +105,7 @@ async function createAnomalyDetectionJob({ job_tags: { environment, // identifies this as an APM ML job & facilitates future migrations - apm_ml_version: 2, + apm_ml_version: 3, }, }, }, diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts index 2ed1966dcacbd..2aa2f5c6eead5 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts @@ -22,6 +22,8 @@ import { rangeQuery } from '../../../../observability/server'; import { withApmSpan } from '../../utils/with_apm_span'; import { getMlJobsWithAPMGroup } from '../anomaly_detection/get_ml_jobs_with_apm_group'; import { Setup } from '../helpers/setup_request'; +import { apmMlAnomalyQuery } from '../../../common/anomaly_detection/apm_ml_anomaly_query'; +import { ApmMlDetectorIndex } from '../../../common/anomaly_detection/apm_ml_detectors'; export const DEFAULT_ANOMALIES: ServiceAnomaliesResponse = { mlJobIds: [], @@ -56,7 +58,7 @@ export async function getServiceAnomalies({ query: { bool: { filter: [ - { terms: { result_type: ['model_plot', 'record'] } }, + ...apmMlAnomalyQuery(ApmMlDetectorIndex.txLatency), ...rangeQuery( Math.min(end - 30 * 60 * 1000, start), end, diff --git a/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/fetcher.ts index 2fcbf5842d746..dd723f24abe1b 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/fetcher.ts @@ -12,6 +12,8 @@ import { rangeQuery } from '../../../../../observability/server'; import { asMutableArray } from '../../../../common/utils/as_mutable_array'; import { withApmSpan } from '../../../utils/with_apm_span'; import { Setup } from '../../helpers/setup_request'; +import { apmMlAnomalyQuery } from '../../../../common/anomaly_detection/apm_ml_anomaly_query'; +import { ApmMlDetectorIndex } from '../../../../common/anomaly_detection/apm_ml_detectors'; export type ESResponse = Exclude< PromiseReturnType, @@ -40,7 +42,7 @@ export function anomalySeriesFetcher({ query: { bool: { filter: [ - { terms: { result_type: ['model_plot', 'record'] } }, + ...apmMlAnomalyQuery(ApmMlDetectorIndex.txLatency), { term: { partition_field_value: serviceName } }, { term: { by_field_value: transactionType } }, ...rangeQuery(start, end, 'timestamp'), diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/manifest.json index f8feaef3be5f8..3332fad66b3e2 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/manifest.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/manifest.json @@ -1,29 +1,29 @@ { "id": "apm_transaction", "title": "APM", - "description": "Detect anomalies in transactions from your APM services.", + "description": "Detect anomalies in transaction latency, throughput and failure rate from your APM services for metric data.", "type": "Transaction data", "logoFile": "logo.json", - "defaultIndexPattern": "apm-*-transaction", + "defaultIndexPattern": "apm-*-metric,metrics-apm*", "query": { "bool": { "filter": [ - { "term": { "processor.event": "transaction" } }, - { "exists": { "field": "transaction.duration" } } + { "term": { "processor.event": "metric" } }, + { "term": { "metricset.name": "transaction" } } ] } }, "jobs": [ { - "id": "high_mean_transaction_duration", - "file": "high_mean_transaction_duration.json" + "id": "apm_tx_metrics", + "file": "apm_tx_metrics.json" } ], "datafeeds": [ { - "id": "datafeed-high_mean_transaction_duration", - "file": "datafeed_high_mean_transaction_duration.json", - "job_id": "high_mean_transaction_duration" + "id": "datafeed-apm_tx_metrics", + "file": "datafeed_apm_tx_metrics.json", + "job_id": "apm_tx_metrics" } ] } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/apm_tx_metrics.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/apm_tx_metrics.json new file mode 100644 index 0000000000000..f93b4fb009a14 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/apm_tx_metrics.json @@ -0,0 +1,53 @@ +{ + "job_type": "anomaly_detector", + "groups": [ + "apm" + ], + "description": "Detects anomalies in transaction latency, throughput and error percentage for metric data.", + "analysis_config": { + "bucket_span": "15m", + "summary_count_field_name" : "doc_count", + "detectors" : [ + { + "detector_description" : "high latency by transaction type for an APM service", + "function" : "high_mean", + "field_name" : "transaction_latency", + "by_field_name" : "transaction.type", + "partition_field_name" : "service.name" + }, + { + "detector_description" : "transaction throughput for an APM service", + "function" : "mean", + "field_name" : "transaction_throughput", + "by_field_name" : "transaction.type", + "partition_field_name" : "service.name" + }, + { + "detector_description" : "failed transaction rate for an APM service", + "function" : "high_mean", + "field_name" : "failed_transaction_rate", + "by_field_name" : "transaction.type", + "partition_field_name" : "service.name" + } + ], + "influencers" : [ + "transaction.type", + "service.name" + ] + }, + "analysis_limits": { + "model_memory_limit": "32mb" + }, + "data_description": { + "time_field" : "@timestamp", + "time_format" : "epoch_ms" + }, + "model_plot_config": { + "enabled" : true, + "annotations_enabled" : true + }, + "results_index_name" : "custom-apm", + "custom_settings": { + "created_by": "ml-module-apm-transaction" + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_apm_tx_metrics.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_apm_tx_metrics.json new file mode 100644 index 0000000000000..4d19cdc9f533d --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_apm_tx_metrics.json @@ -0,0 +1,98 @@ +{ + "job_id": "JOB_ID", + "indices": [ + "INDEX_PATTERN_NAME" + ], + "chunking_config" : { + "mode" : "off" + }, + "query": { + "bool": { + "filter": [ + { "term": { "processor.event": "metric" } }, + { "term": { "metricset.name": "transaction" } } + ] + } + }, + "aggregations" : { + "buckets" : { + "composite" : { + "size" : 5000, + "sources" : [ + { + "date" : { + "date_histogram" : { + "field" : "@timestamp", + "fixed_interval" : "90s" + } + } + }, + { + "transaction.type" : { + "terms" : { + "field" : "transaction.type" + } + } + }, + { + "service.name" : { + "terms" : { + "field" : "service.name" + } + } + } + ] + }, + "aggs" : { + "@timestamp" : { + "max" : { + "field" : "@timestamp" + } + }, + "transaction_throughput" : { + "rate" : { + "unit" : "minute" + } + }, + "transaction_latency" : { + "avg" : { + "field" : "transaction.duration.histogram" + } + }, + "error_count" : { + "filter" : { + "term" : { + "event.outcome" : "failure" + } + }, + "aggs" : { + "actual_error_count" : { + "value_count" : { + "field" : "event.outcome" + } + } + } + }, + "success_count" : { + "filter" : { + "term" : { + "event.outcome" : "success" + } + } + }, + "failed_transaction_rate" : { + "bucket_script" : { + "buckets_path" : { + "failure_count" : "error_count>_count", + "success_count" : "success_count>_count" + }, + "script" : "if ((params.failure_count + params.success_count)==0){return 0;}else{return 100 * (params.failure_count/(params.failure_count + params.success_count));}" + } + } + } + } + }, + "indices_options": { + "ignore_unavailable": true + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_high_mean_transaction_duration.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_high_mean_transaction_duration.json deleted file mode 100644 index 882bd93fd937d..0000000000000 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_high_mean_transaction_duration.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "job_id": "JOB_ID", - "indices": [ - "INDEX_PATTERN_NAME" - ], - "query": { - "bool": { - "filter": [ - { "term": { "processor.event": "transaction" } }, - { "exists": { "field": "transaction.duration.us" } } - ] - } - }, - "indices_options": { - "ignore_unavailable": true - } -} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/high_mean_transaction_duration.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/high_mean_transaction_duration.json deleted file mode 100644 index 77284cb275cd8..0000000000000 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/high_mean_transaction_duration.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "job_type": "anomaly_detector", - "groups": [ - "apm" - ], - "description": "Detect transaction duration anomalies across transaction types for your APM services.", - "analysis_config": { - "bucket_span": "15m", - "detectors": [ - { - "detector_description": "high duration by transaction type for an APM service", - "function": "high_mean", - "field_name": "transaction.duration.us", - "by_field_name": "transaction.type", - "partition_field_name": "service.name" - } - ], - "influencers": [ - "transaction.type", - "service.name" - ] - }, - "analysis_limits": { - "model_memory_limit": "32mb" - }, - "data_description": { - "time_field": "@timestamp" - }, - "model_plot_config": { - "enabled": true - }, - "custom_settings": { - "created_by": "ml-module-apm-transaction" - } -} diff --git a/x-pack/test/api_integration/apis/ml/modules/recognize_module.ts b/x-pack/test/api_integration/apis/ml/modules/recognize_module.ts index 2742fbff294c0..00b820a025c8b 100644 --- a/x-pack/test/api_integration/apis/ml/modules/recognize_module.ts +++ b/x-pack/test/api_integration/apis/ml/modules/recognize_module.ts @@ -44,7 +44,7 @@ export default ({ getService }: FtrProviderContext) => { user: USER.ML_POWERUSER, expected: { responseCode: 200, - moduleIds: ['apm_jsbase', 'apm_transaction', 'apm_nodejs'], + moduleIds: ['apm_jsbase', 'apm_nodejs'], }, }, { diff --git a/x-pack/test/api_integration/apis/ml/modules/setup_module.ts b/x-pack/test/api_integration/apis/ml/modules/setup_module.ts index c538fc0d9cb55..07a02911c6778 100644 --- a/x-pack/test/api_integration/apis/ml/modules/setup_module.ts +++ b/x-pack/test/api_integration/apis/ml/modules/setup_module.ts @@ -187,9 +187,11 @@ export default ({ getService }: FtrProviderContext) => { dashboards: [] as string[], }, }, + // Set startDatafeed and estimateModelMemory to false for the APM transaction test + // until there is a new data set available with metric data. { testTitleSuffix: - 'for apm_transaction with prefix, startDatafeed true and estimateModelMemory true', + 'for apm_transaction with prefix, startDatafeed false and estimateModelMemory false', sourceDataArchive: 'x-pack/test/functional/es_archives/ml/module_apm', indexPattern: { name: 'ft_module_apm', timeField: '@timestamp' }, module: 'apm_transaction', @@ -197,14 +199,14 @@ export default ({ getService }: FtrProviderContext) => { requestBody: { prefix: 'pf5_', indexPatternName: 'ft_module_apm', - startDatafeed: true, - end: Date.now(), + startDatafeed: false, + estimateModelMemory: false, }, expected: { responseCode: 200, jobs: [ { - jobId: 'pf5_high_mean_transaction_duration', + jobId: 'pf5_apm_tx_metrics', jobState: JOB_STATE.CLOSED, datafeedState: DATAFEED_STATE.STOPPED, },