From fe6dae987e885df3b49aca0de439da40075b8122 Mon Sep 17 00:00:00 2001 From: Victor Martinez Date: Tue, 9 Feb 2021 08:45:19 +0000 Subject: [PATCH 01/28] [APM-UI][E2E] use withGithubStatus step (#90651) --- .ci/end2end.groovy | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/.ci/end2end.groovy b/.ci/end2end.groovy index a89ff166bf32e..87b64437deafc 100644 --- a/.ci/end2end.groovy +++ b/.ci/end2end.groovy @@ -121,15 +121,9 @@ pipeline { } def notifyStatus(String description, String status) { - notify(context: 'end2end-for-apm-ui', description: description, status: status, targetUrl: getBlueoceanTabURL('pipeline')) + withGithubStatus.notify('end2end-for-apm-ui', description, status, getBlueoceanTabURL('pipeline')) } def notifyTestStatus(String description, String status) { - notify(context: 'end2end-for-apm-ui', description: description, status: status, targetUrl: getBlueoceanTabURL('tests')) -} - -def notify(Map args = [:]) { - retryWithSleep(retries: 2, seconds: 5, backoff: true) { - githubNotify(context: args.context, description: args.description, status: args.status, targetUrl: args.targetUrl) - } + withGithubStatus.notify('end2end-for-apm-ui', description, status, getBlueoceanTabURL('tests')) } From f94aacecd9843d62fe42ade16d39c9b9ca1c7dbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yulia=20=C4=8Cech?= <6585477+yuliacech@users.noreply.github.com> Date: Tue, 9 Feb 2021 09:50:37 +0100 Subject: [PATCH 02/28] [ILM] Delete phase redesign (rework) (#90291) * Phases redesign * Fixed scss file * Fixed errors * Added changes for phase blocks * Added styles adjustments Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../edit_policy/edit_policy.helpers.tsx | 23 ++- .../edit_policy/edit_policy.test.ts | 10 +- .../__jest__/components/edit_policy.test.tsx | 37 +++- .../edit_policy/components/active_badge.tsx | 18 -- .../active_highlight/active_highlight.scss | 16 -- .../sections/edit_policy/components/index.ts | 4 +- .../index.ts | 2 +- .../infinity_icon.svg.tsx | 0 .../infinity_icon.tsx} | 14 +- .../components/phase_footer/index.ts | 8 + .../components/phase_footer/phase_footer.tsx | 94 ++++++++++ .../components/phase_icon/index.ts | 8 + .../components/phase_icon/phase_icon.scss | 33 ++++ .../components/phase_icon/phase_icon.tsx | 32 ++++ .../phases/delete_phase/delete_phase.scss | 11 ++ .../phases/delete_phase/delete_phase.tsx | 142 ++++++-------- .../components/phases/phase/phase.scss | 27 +++ .../components/phases/phase/phase.tsx | 174 +++++++++--------- .../phases/phase/phase_error_indicator.tsx | 3 +- .../phases/shared_fields/forcemerge_field.tsx | 37 ++-- .../min_age_field/min_age_field.tsx | 7 +- .../searchable_snapshot_field.tsx | 1 - .../shared_fields/snapshot_policies_field.tsx | 119 ++++++++---- .../components/timeline/timeline.tsx | 19 +- .../sections/edit_policy/edit_policy.tsx | 16 +- .../edit_policy/form/components/form.tsx | 5 +- .../sections/edit_policy/form/index.ts | 6 + .../form/phase_timings_context.tsx | 73 ++++++++ .../sections/edit_policy/form/schema.ts | 14 +- .../sections/edit_policy/i18n_texts.ts | 10 + ...absolute_timing_to_relative_timing.test.ts | 18 +- .../lib/absolute_timing_to_relative_timing.ts | 6 +- .../translations/translations/ja-JP.json | 3 - .../translations/translations/zh-CN.json | 3 - 34 files changed, 672 insertions(+), 321 deletions(-) delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/active_badge.tsx delete mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/active_highlight/active_highlight.scss rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/{active_highlight => infinity_icon}/index.ts (82%) rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/{timeline => infinity_icon}/infinity_icon.svg.tsx (100%) rename x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/{active_highlight/active_highlight.tsx => infinity_icon/infinity_icon.tsx} (51%) create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_footer/index.ts create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_footer/phase_footer.tsx create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_icon/index.ts create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_icon/phase_icon.scss create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_icon/phase_icon.tsx create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/delete_phase.scss create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase/phase.scss create mode 100644 x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/phase_timings_context.tsx diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx index a59c4d9878aea..dc375f6370048 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx @@ -248,6 +248,27 @@ export const setup = async (arg?: { appServicesContext: Partial { + const enablePhase = async () => { + await act(async () => { + find('enableDeletePhaseButton').simulate('click'); + }); + component.update(); + }; + + const disablePhase = async () => { + await act(async () => { + find('disableDeletePhaseButton').simulate('click'); + }); + component.update(); + }; + + return { + enablePhase, + disablePhase, + }; + }; + return { ...testBed, actions: { @@ -303,7 +324,7 @@ export const setup = async (arg?: { appServicesContext: Partial', () => { // Set max docs to test whether we keep the unknown fields in that object after serializing await actions.hot.setMaxDocs('1000'); // Remove the delete phase to ensure that we also correctly remove data - await actions.delete.enable(false); + await actions.delete.disablePhase(); await actions.savePolicy(); const latestRequest = server.requests[server.requests.length - 1]; @@ -89,7 +89,7 @@ describe('', () => { unknown_setting: true, }, }, - min_age: '0ms', + min_age: '0d', }, }, }); @@ -255,7 +255,7 @@ describe('', () => { "priority": 50, }, }, - "min_age": "0ms", + "min_age": "0d", } `); }); @@ -310,7 +310,7 @@ describe('', () => { "number_of_shards": 123, }, }, - "min_age": "0ms", + "min_age": "0d", }, }, } @@ -839,7 +839,7 @@ describe('', () => { expect(actions.timeline.hasColdPhase()).toBe(true); expect(actions.timeline.hasDeletePhase()).toBe(false); - await actions.delete.enable(true); + await actions.delete.enablePhase(); expect(actions.timeline.hasHotPhase()).toBe(true); expect(actions.timeline.hasWarmPhase()).toBe(true); expect(actions.timeline.hasColdPhase()).toBe(true); diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx index a9a351e394f7f..7c199e2ced765 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/components/edit_policy.test.tsx @@ -99,6 +99,13 @@ const activatePhase = async (rendered: ReactWrapper, phase: string) => { }); rendered.update(); }; +const activateDeletePhase = async (rendered: ReactWrapper) => { + const testSubject = `enableDeletePhaseButton`; + await act(async () => { + await findTestSubject(rendered, testSubject).simulate('click'); + }); + rendered.update(); +}; const openNodeAttributesSection = async (rendered: ReactWrapper, phase: string) => { const getControls = () => findTestSubject(rendered, `${phase}-dataTierAllocationControls`); await act(async () => { @@ -454,6 +461,11 @@ describe('edit policy', () => { waitForFormLibValidation(rendered); expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); }); + + test("doesn't show min age input", async () => { + const rendered = mountWithIntl(component); + expect(findTestSubject(rendered, 'hot-selectedMinimumAge').exists()).toBeFalsy(); + }); }); describe('warm phase', () => { beforeEach(() => { @@ -670,6 +682,13 @@ describe('edit policy', () => { expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); expect(findTestSubject(rendered, 'defaultAllocationNotice').exists()).toBeFalsy(); }); + + test('shows min age input only when enabled', async () => { + const rendered = mountWithIntl(component); + expect(findTestSubject(rendered, 'warm-selectedMinimumAge').exists()).toBeFalsy(); + await activatePhase(rendered, 'warm'); + expect(findTestSubject(rendered, 'warm-selectedMinimumAge').exists()).toBeTruthy(); + }); }); describe('cold phase', () => { beforeEach(() => { @@ -807,13 +826,20 @@ describe('edit policy', () => { expect(rendered.find('.euiLoadingSpinner').exists()).toBeFalsy(); expect(findTestSubject(rendered, 'defaultAllocationNotice').exists()).toBeFalsy(); }); + + test('shows min age input only when enabled', async () => { + const rendered = mountWithIntl(component); + expect(findTestSubject(rendered, 'cold-selectedMinimumAge').exists()).toBeFalsy(); + await activatePhase(rendered, 'cold'); + expect(findTestSubject(rendered, 'cold-selectedMinimumAge').exists()).toBeTruthy(); + }); }); describe('delete phase', () => { test('should allow 0 for phase timing', async () => { const rendered = mountWithIntl(component); await noRollover(rendered); await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'delete'); + await activateDeletePhase(rendered); await setPhaseAfter(rendered, 'delete', '0'); waitForFormLibValidation(rendered); expectedErrorMessages(rendered, []); @@ -822,11 +848,18 @@ describe('edit policy', () => { const rendered = mountWithIntl(component); await noRollover(rendered); await setPolicyName(rendered, 'mypolicy'); - await activatePhase(rendered, 'delete'); + await activateDeletePhase(rendered); await setPhaseAfter(rendered, 'delete', '-1'); waitForFormLibValidation(rendered); expectedErrorMessages(rendered, [i18nTexts.editPolicy.errors.nonNegativeNumberRequired]); }); + + test('is hidden when disabled', async () => { + const rendered = mountWithIntl(component); + expect(findTestSubject(rendered, 'delete-phaseContent').exists()).toBeFalsy(); + await activateDeletePhase(rendered); + expect(findTestSubject(rendered, 'delete-phaseContent').exists()).toBeTruthy(); + }); }); describe('not on cloud', () => { beforeEach(() => { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/active_badge.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/active_badge.tsx deleted file mode 100644 index f3a6ee7276cde..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/active_badge.tsx +++ /dev/null @@ -1,18 +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 React from 'react'; -import { EuiBadge } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; - -export const ActiveBadge = () => { - return ( - - - - ); -}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/active_highlight/active_highlight.scss b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/active_highlight/active_highlight.scss deleted file mode 100644 index 96ca0c3a61067..0000000000000 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/active_highlight/active_highlight.scss +++ /dev/null @@ -1,16 +0,0 @@ -.ilmActivePhaseHighlight { - border-left: $euiBorderWidthThin solid $euiColorLightShade; - height: 100%; - - &.hotPhase.active { - border-left-color: $euiColorVis9_behindText; - } - - &.warmPhase.active { - border-left-color: $euiColorVis5_behindText; - } - - &.coldPhase.active { - border-left-color: $euiColorVis1_behindText; - } -} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts index a84d15e6c19da..dc4f1e31d3696 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/index.ts @@ -5,14 +5,12 @@ * 2.0. */ -export { ActiveBadge } from './active_badge'; export { LearnMoreLink } from './learn_more_link'; export { OptionalLabel } from './optional_label'; export { PolicyJsonFlyout } from './policy_json_flyout'; export { DescribedFormRow, ToggleFieldWithDescribedFormRow } from './described_form_row'; export { FieldLoadingError } from './field_loading_error'; -export { ActiveHighlight } from './active_highlight'; export { Timeline } from './timeline'; export { FormErrorsCallout } from './form_errors_callout'; - +export { PhaseFooter } from './phase_footer'; export * from './phases'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/active_highlight/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/infinity_icon/index.ts similarity index 82% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/active_highlight/index.ts rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/infinity_icon/index.ts index c8d3b6540dc3d..850f3e4e07aed 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/active_highlight/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/infinity_icon/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { ActiveHighlight } from './active_highlight'; +export { InfinityIcon } from './infinity_icon'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/infinity_icon.svg.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/infinity_icon/infinity_icon.svg.tsx similarity index 100% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/infinity_icon.svg.tsx rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/infinity_icon/infinity_icon.svg.tsx diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/active_highlight/active_highlight.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/infinity_icon/infinity_icon.tsx similarity index 51% rename from x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/active_highlight/active_highlight.tsx rename to x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/infinity_icon/infinity_icon.tsx index bae73c3cefa5d..435e6a909acd1 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/active_highlight/active_highlight.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/infinity_icon/infinity_icon.tsx @@ -6,13 +6,9 @@ */ import React, { FunctionComponent } from 'react'; +import { EuiIcon, EuiIconProps } from '@elastic/eui'; +import { InfinityIconSvg } from './infinity_icon.svg'; -import './active_highlight.scss'; - -interface Props { - phase: 'hot' | 'warm' | 'cold'; - enabled: boolean; -} -export const ActiveHighlight: FunctionComponent = ({ phase, enabled }) => { - return
; -}; +export const InfinityIcon: FunctionComponent> = (props) => ( + +); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_footer/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_footer/index.ts new file mode 100644 index 0000000000000..724904a1f188e --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_footer/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { PhaseFooter } from './phase_footer'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_footer/phase_footer.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_footer/phase_footer.tsx new file mode 100644 index 0000000000000..82f0725bfe7d0 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_footer/phase_footer.tsx @@ -0,0 +1,94 @@ +/* + * 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, { FunctionComponent } from 'react'; + +import { i18n } from '@kbn/i18n'; + +import { EuiText, EuiButtonGroup, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { PhasesExceptDelete } from '../../../../../../common/types'; + +import { usePhaseTimings } from '../../form'; + +import { InfinityIconSvg } from '../infinity_icon/infinity_icon.svg'; + +interface Props { + phase: PhasesExceptDelete; +} + +export const PhaseFooter: FunctionComponent = ({ phase }) => { + const { + isDeletePhaseEnabled, + setDeletePhaseEnabled: setValue, + [phase]: phaseConfiguration, + } = usePhaseTimings(); + + if (!phaseConfiguration.isFinalDataPhase) { + return null; + } + + const phaseDescription = isDeletePhaseEnabled + ? i18n.translate('xpack.indexLifecycleMgmt.editPolicy.phaseTiming.beforeDeleteDescription', { + defaultMessage: 'Data will be deleted after this phase', + }) + : i18n.translate('xpack.indexLifecycleMgmt.editPolicy.phaseTiming.foreverTimingDescription', { + defaultMessage: 'Data will remain in this phase forever', + }); + + const selectedButton = isDeletePhaseEnabled + ? 'ilmEnableDeletePhaseButton' + : 'ilmDisableDeletePhaseButton'; + + const buttons = [ + { + id: `ilmDisableDeletePhaseButton`, + label: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.deletePhase.disablePhaseButtonLabel', + { + defaultMessage: 'Keep data in this phase forever', + } + ), + iconType: InfinityIconSvg, + }, + { + id: `ilmEnableDeletePhaseButton`, + label: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.deletePhase.enablePhaseButtonLabel', + { + defaultMessage: 'Delete data after this phase', + } + ), + iconType: 'trash', + 'data-test-subj': 'enableDeletePhaseButton', + }, + ]; + + return ( + + + + {phaseDescription} + + + + { + setValue(id === 'ilmEnableDeletePhaseButton'); + }} + isIconOnly={true} + /> + + + ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_icon/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_icon/index.ts new file mode 100644 index 0000000000000..26fda5d929284 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_icon/index.ts @@ -0,0 +1,8 @@ +/* + * 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 { PhaseIcon } from './phase_icon'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_icon/phase_icon.scss b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_icon/phase_icon.scss new file mode 100644 index 0000000000000..7c6a5aefdde6e --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_icon/phase_icon.scss @@ -0,0 +1,33 @@ +.ilmPhaseIcon { + width: $euiSizeXL; + height: $euiSizeXL; + display: flex; + justify-content: center; + align-items: center; + border-radius: 50%; + background-color: $euiColorLightestShade; + &--disabled { + margin-top: $euiSizeS; + width: $euiSize; + height: $euiSize; + } + &--delete { + background-color: $euiColorLightShade; + } + &__inner--hot { + fill: $euiColorVis9_behindText; + } + &__inner--warm { + fill: $euiColorVis5_behindText; + } + &__inner--cold { + fill: $euiColorVis1_behindText; + } + &__inner--delete { + fill: $euiColorDarkShade; + } + + &__inner--disabled { + fill: $euiColorMediumShade; + } +} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_icon/phase_icon.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_icon/phase_icon.tsx new file mode 100644 index 0000000000000..8c0a0bcca1d76 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phase_icon/phase_icon.tsx @@ -0,0 +1,32 @@ +/* + * 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, { FunctionComponent } from 'react'; +import { EuiIcon } from '@elastic/eui'; +import { Phases } from '../../../../../../common/types'; +import './phase_icon.scss'; +interface Props { + enabled: boolean; + phase: string & keyof Phases; +} +export const PhaseIcon: FunctionComponent = ({ enabled, phase }) => { + return ( +
+ {enabled ? ( + + ) : ( + + )} +
+ ); +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/delete_phase.scss b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/delete_phase.scss new file mode 100644 index 0000000000000..60a39c7f1e9a6 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/delete_phase.scss @@ -0,0 +1,11 @@ +.ilmDeletePhase { + .euiCommentEvent { + &__header { + padding: $euiSize; + background-color: $euiColorEmptyShade; + } + &__body { + padding: $euiSize; + } + } +} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/delete_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/delete_phase.tsx index c2da9246effb7..c65699ca12690 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/delete_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/delete_phase/delete_phase.tsx @@ -5,107 +5,85 @@ * 2.0. */ -import React, { FunctionComponent, Fragment } from 'react'; +import React, { FunctionComponent } from 'react'; import { get } from 'lodash'; +import { + EuiFlexItem, + EuiFlexGroup, + EuiTitle, + EuiButtonEmpty, + EuiSpacer, + EuiText, + EuiComment, +} from '@elastic/eui'; + import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiDescribedFormGroup, EuiTextColor, EuiFormRow } from '@elastic/eui'; -import { useFormData, ToggleField } from '../../../../../../shared_imports'; +import { useFormData } from '../../../../../../shared_imports'; -import { UseField } from '../../../form'; +import { i18nTexts } from '../../../i18n_texts'; -import { ActiveBadge, LearnMoreLink, OptionalLabel } from '../../index'; +import { usePhaseTimings } from '../../../form'; import { MinAgeField, SnapshotPoliciesField } from '../shared_fields'; +import './delete_phase.scss'; +import { PhaseIcon } from '../../phase_icon'; +import { PhaseErrorIndicator } from '../phase/phase_error_indicator'; const formFieldPaths = { enabled: '_meta.delete.enabled', }; export const DeletePhase: FunctionComponent = () => { + const { setDeletePhaseEnabled } = usePhaseTimings(); const [formData] = useFormData({ watch: formFieldPaths.enabled, }); const enabled = get(formData, formFieldPaths.enabled); - return ( -
- -

- -

{' '} - {enabled && } -
- } - titleSize="s" - description={ - -

- -

- -
- } - fullWidth - > - {enabled && } - - {enabled ? ( - - - - } - description={ - - {' '} - - - } - titleSize="xs" - fullWidth + if (!enabled) { + return null; + } + const phaseTitle = ( + + + +

{i18nTexts.editPolicy.titles.delete}

+
+
+ + + setDeletePhaseEnabled(false)} + data-test-subj={'disableDeletePhaseButton'} > - - - - - } - > - - -
- ) : null} -
+ + + + + + + + + ); + + return ( + } + className="ilmDeletePhase ilmPhase" + timelineIcon={} + > + + {i18nTexts.editPolicy.descriptions.delete} + + + + ); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase/phase.scss b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase/phase.scss new file mode 100644 index 0000000000000..15f2dc508a365 --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase/phase.scss @@ -0,0 +1,27 @@ +.ilmPhase { + .euiCommentEvent { + &__header { + padding: $euiSize; + } + &__body { + padding: $euiSize; + } + } + .ilmSettingsButton { + color: $euiColorPrimary; + padding: $euiSizeS; + } + .euiCommentTimeline { + padding-top: $euiSize; + &::before { + height: calc(100% + #{$euiSizeXXL}); + } + } + &--enabled { + .euiCommentEvent { + &__header { + background-color: $euiColorEmptyShade; + } + } + } +} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase/phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase/phase.tsx index f7e0f8e20e050..0ac6f6922ec1e 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase/phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase/phase.tsx @@ -5,126 +5,120 @@ * 2.0. */ -import React, { FunctionComponent, useState } from 'react'; +import React, { FunctionComponent } from 'react'; import { EuiFlexGroup, EuiFlexItem, - EuiPanel, EuiTitle, - EuiSpacer, EuiText, - EuiButtonEmpty, + EuiComment, + EuiAccordion, + EuiSpacer, + EuiBadge, } from '@elastic/eui'; import { get } from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; +import { PhasesExceptDelete } from '../../../../../../../common/types'; import { ToggleField, useFormData } from '../../../../../../shared_imports'; import { i18nTexts } from '../../../i18n_texts'; +import { FormInternal } from '../../../types'; + import { UseField } from '../../../form'; -import { ActiveHighlight } from '../../active_highlight'; -import { MinAgeField } from '../shared_fields'; import { PhaseErrorIndicator } from './phase_error_indicator'; +import { MinAgeField } from '../shared_fields'; +import { PhaseIcon } from '../../phase_icon'; +import { PhaseFooter } from '../../phase_footer'; +import './phase.scss'; + interface Props { - phase: 'hot' | 'warm' | 'cold'; + phase: PhasesExceptDelete; } export const Phase: FunctionComponent = ({ children, phase }) => { const enabledPath = `_meta.${phase}.enabled`; - const [formData] = useFormData({ + const [formData] = useFormData({ watch: [enabledPath], }); + const isHotPhase = phase === 'hot'; // hot phase is always enabled - const enabled = get(formData, enabledPath) || phase === 'hot'; + const enabled = get(formData, enabledPath) || isHotPhase; - const [isShowingSettings, setShowingSettings] = useState(false); - return ( - + const phaseTitle = ( + + {!isHotPhase && ( + + + + )} - + +

{i18nTexts.editPolicy.titles[phase]}

+
- - - - - - {phase !== 'hot' && ( - - - - )} - - - - -

{i18nTexts.editPolicy.titles[phase]}

-
-
- - - -
-
-
-
- {enabled && ( - - - - {phase !== 'hot' && } - - - { - setShowingSettings(!isShowingSettings); - }} - size="xs" - iconType="controlsVertical" - iconSide="left" - aria-controls={`${phase}-phaseContent`} - > - - - - - - )} -
- - - {i18nTexts.editPolicy.descriptions[phase]} - - - {enabled && ( -
- - {children} -
- )} -
+ {isHotPhase && ( + + + + + + )} + +
); + + // @ts-ignore + const minAge = !isHotPhase && enabled ? : null; + + return ( + } + className={`ilmPhase ${enabled ? 'ilmPhase--enabled' : ''}`} + > + + {i18nTexts.editPolicy.descriptions[phase]} + + + {enabled && ( + <> + + + } + buttonClassName="ilmSettingsButton" + extraAction={} + > + + {children} + + + )} + + ); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase/phase_error_indicator.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase/phase_error_indicator.tsx index 98fdfe73ecbd8..647f12669cf77 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase/phase_error_indicator.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/phase/phase_error_indicator.tsx @@ -9,10 +9,11 @@ import { i18n } from '@kbn/i18n'; import React, { FunctionComponent, memo } from 'react'; import { EuiIconTip } from '@elastic/eui'; +import { Phases } from '../../../../../../../common/types'; import { useFormErrorsContext } from '../../../form'; interface Props { - phase: 'hot' | 'warm' | 'cold'; + phase: string & keyof Phases; } const i18nTexts = { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx index 2d5f5babe1e2a..bbdcbbf4759ef 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx @@ -8,7 +8,9 @@ import React, { useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { CheckBoxField, NumericField } from '../../../../../../shared_imports'; +import uuid from 'uuid'; +import { EuiCheckbox, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiIconTip } from '@elastic/eui'; +import { NumericField } from '../../../../../../shared_imports'; import { i18nTexts } from '../../../i18n_texts'; @@ -67,16 +69,29 @@ export const ForcemergeField: React.FunctionComponent = ({ phase }) => { }, }} /> - + + + {(field) => ( + + + + + + + + + + )} + ); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_field/min_age_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_field/min_age_field.tsx index 60af830356ab9..2f1a058f5a943 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_field/min_age_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/min_age_field/min_age_field.tsx @@ -75,7 +75,12 @@ export const MinAgeField: FunctionComponent = ({ phase }): React.ReactEle const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); return ( - + = ({ phase }) => config={{ defaultValue: cloud?.isCloudEnabled ? CLOUD_DEFAULT_REPO : undefined, - label: i18nTexts.editPolicy.searchableSnapshotsFieldLabel, validations: [ { validator: emptyField( diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/snapshot_policies_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/snapshot_policies_field.tsx index 2cbd5cea6165a..f9c973d14b3e2 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/snapshot_policies_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/snapshot_policies_field.tsx @@ -10,7 +10,13 @@ import { get } from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { EuiCallOut, EuiComboBoxOptionOption, EuiLink, EuiSpacer } from '@elastic/eui'; +import { + EuiCallOut, + EuiComboBoxOptionOption, + EuiDescribedFormGroup, + EuiLink, + EuiSpacer, +} from '@elastic/eui'; import { ComboBoxField, useFormData } from '../../../../../../shared_imports'; import { useLoadSnapshotPolicies } from '../../../../../services/api'; @@ -18,7 +24,7 @@ import { useLoadSnapshotPolicies } from '../../../../../services/api'; import { useEditPolicyContext } from '../../../edit_policy_context'; import { UseField } from '../../../form'; -import { FieldLoadingError } from '../../'; +import { FieldLoadingError, LearnMoreLink, OptionalLabel } from '../../'; const waitForSnapshotFormField = 'phases.delete.actions.wait_for_snapshot.policy'; @@ -137,43 +143,78 @@ export const SnapshotPoliciesField: React.FunctionComponent = () => { } return ( - <> - path={waitForSnapshotFormField}> - {(field) => { - const singleSelectionArray: [selectedSnapshot?: string] = field.value - ? [field.value] - : []; + + + + } + description={ + <> + {' '} + + + } + titleSize="xs" + fullWidth + > + <> + + path={waitForSnapshotFormField} + componentProps={{ + label: ( + <> + + + + ), + }} + > + {(field) => { + const singleSelectionArray: [selectedSnapshot?: string] = field.value + ? [field.value] + : []; - return ( - { - field.setValue(newOption); - }, - onChange: (options: EuiComboBoxOptionOption[]) => { - if (options.length > 0) { - field.setValue(options[0].label); - } else { - field.setValue(''); - } - }, - }} - /> - ); - }} - - {calloutContent} - + return ( + { + field.setValue(newOption); + }, + onChange: (options: EuiComboBoxOptionOption[]) => { + if (options.length > 0) { + field.setValue(options[0].label); + } else { + field.setValue(''); + } + }, + }} + /> + ); + }} + + {calloutContent} + + ); }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx index 3ebd5935b8d3f..2d83009bd4df4 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx @@ -6,15 +6,10 @@ */ import { i18n } from '@kbn/i18n'; + import React, { FunctionComponent, memo } from 'react'; -import { - EuiIcon, - EuiIconProps, - EuiFlexGroup, - EuiFlexItem, - EuiTitle, - EuiIconTip, -} from '@elastic/eui'; + +import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiIconTip } from '@elastic/eui'; import { PhasesExceptDelete } from '../../../../../../common/types'; @@ -25,15 +20,13 @@ import { AbsoluteTimings, } from '../../lib'; -import './timeline.scss'; -import { InfinityIconSvg } from './infinity_icon.svg'; +import { InfinityIcon } from '../infinity_icon'; + import { TimelinePhaseText } from './components'; const exists = (v: unknown) => v != null; -const InfinityIcon: FunctionComponent> = (props) => ( - -); +import './timeline.scss'; const toPercent = (n: number, total: number) => (n / total) * 100; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx index 749327a2dd441..0c7b5565372a5 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/edit_policy.tsx @@ -239,19 +239,19 @@ export const EditPolicy: React.FunctionComponent = ({ history }) => { - +
+ - + - + - + - + - - - + +
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/components/form.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/components/form.tsx index 429ae37b76013..be8243cab289f 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/components/form.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/components/form.tsx @@ -11,6 +11,7 @@ import { Form as LibForm, FormHook } from '../../../../../shared_imports'; import { ConfigurationIssuesProvider } from '../configuration_issues_context'; import { FormErrorsProvider } from '../form_errors_context'; +import { PhaseTimingsProvider } from '../phase_timings_context'; interface Props { form: FormHook; @@ -19,7 +20,9 @@ interface Props { export const Form: FunctionComponent = ({ form, children }) => ( - {children} + + {children} + ); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/index.ts index 753148f55db42..734a12a72bd30 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/index.ts @@ -21,3 +21,9 @@ export { } from './configuration_issues_context'; export { FormErrorsProvider, useFormErrorsContext } from './form_errors_context'; + +export { + PhaseTimingsProvider, + usePhaseTimings, + PhaseTimingConfiguration, +} from './phase_timings_context'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/phase_timings_context.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/phase_timings_context.tsx new file mode 100644 index 0000000000000..92cc8eeead91a --- /dev/null +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/phase_timings_context.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, { createContext, FunctionComponent, useContext } from 'react'; +import { useFormData } from '../../../../shared_imports'; +import { FormInternal } from '../types'; +import { UseField } from './index'; + +export interface PhaseTimingConfiguration { + /** + * Whether this is the final, non-delete, phase. + */ + isFinalDataPhase: boolean; +} + +const getPhaseTimingConfiguration = ( + formData: FormInternal +): { + hot: PhaseTimingConfiguration; + warm: PhaseTimingConfiguration; + cold: PhaseTimingConfiguration; +} => { + const isWarmPhaseEnabled = formData?._meta?.warm?.enabled; + const isColdPhaseEnabled = formData?._meta?.cold?.enabled; + return { + hot: { isFinalDataPhase: !isWarmPhaseEnabled && !isColdPhaseEnabled }, + warm: { isFinalDataPhase: isWarmPhaseEnabled && !isColdPhaseEnabled }, + cold: { isFinalDataPhase: isColdPhaseEnabled }, + }; +}; +export interface PhaseTimings { + hot: PhaseTimingConfiguration; + warm: PhaseTimingConfiguration; + cold: PhaseTimingConfiguration; + isDeletePhaseEnabled: boolean; + setDeletePhaseEnabled: (enabled: boolean) => void; +} + +const PhaseTimingsContext = createContext(null as any); + +export const PhaseTimingsProvider: FunctionComponent = ({ children }) => { + const [formData] = useFormData({ + watch: ['_meta.warm.enabled', '_meta.cold.enabled', '_meta.delete.enabled'], + }); + + return ( + + {(field) => { + return ( + + {children} + + ); + }} + + ); +}; +export const usePhaseTimings = () => { + const ctx = useContext(PhaseTimingsContext); + if (!ctx) throw new Error('Cannot use phase timings outside of phase timings context'); + + return ctx; +}; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts index ee84be231f4cc..600a660657863 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/form/schema.ts @@ -70,7 +70,7 @@ export const schema: FormSchema = { ), }, minAgeUnit: { - defaultValue: 'ms', + defaultValue: 'd', }, bestCompression: { label: i18nTexts.editPolicy.bestCompressionFieldLabel, @@ -361,6 +361,18 @@ export const schema: FormSchema = { }, ], }, + actions: { + wait_for_snapshot: { + policy: { + label: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.waitForSnapshot.snapshotPolicyFieldLabel', + { + defaultMessage: 'Policy name (optional)', + } + ), + }, + }, + }, }, }, }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts index 55af738d7d7ae..5deba8607cd52 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts @@ -188,6 +188,9 @@ export const i18nTexts = { cold: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.coldPhase.coldPhaseTitle', { defaultMessage: 'Cold phase', }), + delete: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.deletePhase.deletePhaseTitle', { + defaultMessage: 'Delete Data', + }), }, descriptions: { hot: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.hotPhase.hotPhaseDescription', { @@ -202,6 +205,13 @@ export const i18nTexts = { defaultMessage: 'You are querying your index less frequently, so you can allocate shards on significantly less performant hardware. Because your queries are slower, you can reduce the number of replicas.', }), + delete: i18n.translate( + 'xpack.indexLifecycleMgmt.editPolicy.deletePhase.deletePhaseDescription', + { + defaultMessage: + 'You no longer need your index. You can define when it is safe to delete it.', + } + ), }, }, }; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.test.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.test.ts index 9f96bbfb25c72..7ec20cc2a5966 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.test.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.test.ts @@ -289,7 +289,7 @@ describe('Conversion of absolute policy timing to relative timing', () => { }, }) ) - ).toEqual({ total: 'Forever', hot: 'Forever', warm: undefined, cold: undefined }); + ).toEqual({ total: 'forever', hot: 'forever', warm: undefined, cold: undefined }); }); test('hot, then always warm', () => { @@ -308,7 +308,7 @@ describe('Conversion of absolute policy timing to relative timing', () => { }, }) ) - ).toEqual({ total: 'Forever', hot: 'Less than a day', warm: 'Forever', cold: undefined }); + ).toEqual({ total: 'forever', hot: 'less than a day', warm: 'forever', cold: undefined }); }); test('hot, then warm, then always cold', () => { @@ -333,10 +333,10 @@ describe('Conversion of absolute policy timing to relative timing', () => { }) ) ).toEqual({ - total: 'Forever', + total: 'forever', hot: '30 days', warm: '4 days', - cold: 'Forever', + cold: 'forever', }); }); @@ -357,7 +357,7 @@ describe('Conversion of absolute policy timing to relative timing', () => { }, }) ) - ).toEqual({ total: 'Forever', hot: '34 days', warm: undefined, cold: 'Forever' }); + ).toEqual({ total: 'forever', hot: '34 days', warm: undefined, cold: 'forever' }); }); }); @@ -445,7 +445,7 @@ describe('Conversion of absolute policy timing to relative timing', () => { total: '61 days', hot: '24 days', warm: '37 days', - cold: 'Less than a day', + cold: 'less than a day', }); }); @@ -474,7 +474,7 @@ describe('Conversion of absolute policy timing to relative timing', () => { total: '61 days', hot: '61 days', warm: undefined, - cold: 'Less than a day', + cold: 'less than a day', }); }); @@ -506,8 +506,8 @@ describe('Conversion of absolute policy timing to relative timing', () => { ).toEqual({ total: '61 days', hot: '61 days', - warm: 'Less than a day', - cold: 'Less than a day', + warm: 'less than a day', + cold: 'less than a day', }); }); }); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts index 10c26702e81f1..73ff8c76b9233 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts @@ -35,11 +35,11 @@ type MinAgePhase = 'warm' | 'cold' | 'delete'; type Phase = 'hot' | MinAgePhase; const i18nTexts = { - forever: i18n.translate('xpack.indexLifecycleMgmt.relativeTiming.Forever', { - defaultMessage: 'Forever', + forever: i18n.translate('xpack.indexLifecycleMgmt.relativeTiming.forever', { + defaultMessage: 'forever', }), lessThanADay: i18n.translate('xpack.indexLifecycleMgmt.relativeTiming.lessThanADay', { - defaultMessage: 'Less than a day', + defaultMessage: 'less than a day', }), day: i18n.translate('xpack.indexLifecycleMgmt.relativeTiming.day', { defaultMessage: 'day', diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 168eb14966493..c264e807ea234 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9401,7 +9401,6 @@ "xpack.idxMgmt.unfreezeIndicesAction.successfullyUnfrozeIndicesMessage": "[{indexNames}] の凍結が解除されました", "xpack.idxMgmt.updateIndexSettingsAction.settingsSuccessUpdateMessage": "インデックス {indexName} の設定が更新されました", "xpack.idxMgmt.validators.string.invalidJSONError": "無効な JSON フォーマット。", - "xpack.indexLifecycleMgmt.activePhaseMessage": "アクティブ", "xpack.indexLifecycleMgmt.addLifecyclePolicyActionButtonLabel": "ライフサイクルポリシーを追加", "xpack.indexLifecycleMgmt.appTitle": "インデックスライフサイクルポリシー", "xpack.indexLifecycleMgmt.breadcrumb.editPolicyLabel": "ポリシーの編集", @@ -9450,8 +9449,6 @@ "xpack.indexLifecycleMgmt.editPolicy.deletePhase.customPolicyLink": "新しいポリシーを作成", "xpack.indexLifecycleMgmt.editPolicy.deletePhase.customPolicyMessage": "既存のスナップショットポリシーの名前を入力するか、この名前で{link}。", "xpack.indexLifecycleMgmt.editPolicy.deletePhase.customPolicyTitle": "ポリシー名が見つかりません", - "xpack.indexLifecycleMgmt.editPolicy.deletePhase.deletePhaseDescriptionText": "今後インデックスは必要ありません。 いつ安全に削除できるかを定義できます。", - "xpack.indexLifecycleMgmt.editPolicy.deletePhase.deletePhaseLabel": "削除フェーズ", "xpack.indexLifecycleMgmt.editPolicy.deletePhase.noPoliciesCreatedLink": "スナップショットライフサイクルポリシーを作成", "xpack.indexLifecycleMgmt.editPolicy.deletePhase.noPoliciesCreatedMessage": "{link}して、クラスタースナップショットの作成と削除を自動化します。", "xpack.indexLifecycleMgmt.editPolicy.deletePhase.noPoliciesCreatedTitle": "スナップショットポリシーが見つかりません", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 129deb575a52f..a2fe8e81e4635 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9425,7 +9425,6 @@ "xpack.idxMgmt.unfreezeIndicesAction.successfullyUnfrozeIndicesMessage": "成功取消冻结:[{indexNames}]", "xpack.idxMgmt.updateIndexSettingsAction.settingsSuccessUpdateMessage": "已成功更新索引 {indexName} 的设置", "xpack.idxMgmt.validators.string.invalidJSONError": "JSON 格式无效。", - "xpack.indexLifecycleMgmt.activePhaseMessage": "活动", "xpack.indexLifecycleMgmt.addLifecyclePolicyActionButtonLabel": "添加生命周期策略", "xpack.indexLifecycleMgmt.appTitle": "索引生命周期策略", "xpack.indexLifecycleMgmt.breadcrumb.editPolicyLabel": "编辑策略", @@ -9474,8 +9473,6 @@ "xpack.indexLifecycleMgmt.editPolicy.deletePhase.customPolicyLink": "创建新策略", "xpack.indexLifecycleMgmt.editPolicy.deletePhase.customPolicyMessage": "输入现有快照策略的名称,或使用此名称{link}。", "xpack.indexLifecycleMgmt.editPolicy.deletePhase.customPolicyTitle": "未找到策略名称", - "xpack.indexLifecycleMgmt.editPolicy.deletePhase.deletePhaseDescriptionText": "您不再需要自己的索引。 您可以定义安全删除它的时间。", - "xpack.indexLifecycleMgmt.editPolicy.deletePhase.deletePhaseLabel": "删除阶段", "xpack.indexLifecycleMgmt.editPolicy.deletePhase.noPoliciesCreatedLink": "创建快照生命周期策略", "xpack.indexLifecycleMgmt.editPolicy.deletePhase.noPoliciesCreatedMessage": "{link}以自动创建和删除集群快照。", "xpack.indexLifecycleMgmt.editPolicy.deletePhase.noPoliciesCreatedTitle": "找不到快照策略", From 7b5d62fd551ec1c3d5455453eefb902779564097 Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Tue, 9 Feb 2021 11:09:22 +0200 Subject: [PATCH 03/28] [Search Sessions] Enable extend from management (#90558) * Enable extend from management * fix extend jest test --- .../public/search/session/sessions_client.ts | 4 +-- .../components/actions/extend_button.tsx | 4 ++- .../search/sessions_mgmt/lib/api.test.ts | 16 +++++++++++- .../public/search/sessions_mgmt/lib/api.ts | 25 ++++++++++++------- 4 files changed, 36 insertions(+), 13 deletions(-) diff --git a/src/plugins/data/public/search/session/sessions_client.ts b/src/plugins/data/public/search/session/sessions_client.ts index dcfc529f99b2b..1742db9d033bd 100644 --- a/src/plugins/data/public/search/session/sessions_client.ts +++ b/src/plugins/data/public/search/session/sessions_client.ts @@ -68,9 +68,9 @@ export class SessionsClient { }); } - public extend(sessionId: string, keepAlive: string): Promise { + public extend(sessionId: string, expires: string): Promise { return this.http!.post(`/internal/session/${encodeURIComponent(sessionId)}/_extend`, { - body: JSON.stringify({ keepAlive }), + body: JSON.stringify({ expires }), }); } diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/extend_button.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/extend_button.tsx index 1e2678912ce99..06459db154f4a 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/extend_button.tsx +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/extend_button.tsx @@ -58,7 +58,9 @@ const ExtendConfirm = ({ onCancel={onConfirmDismiss} onConfirm={async () => { setIsLoading(true); - await api.sendExtend(id, `${extendByDuration.asMilliseconds()}ms`); + await api.sendExtend(id, `${newExpiration.toISOString()}`); + setIsLoading(false); + onConfirmDismiss(); onActionComplete(); }} confirmButtonText={confirm} diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.test.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.test.ts index 86acbcdb53001..0fa13ac145223 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.test.ts +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.test.ts @@ -168,7 +168,7 @@ describe('Search Sessions Management API', () => { describe('extend', () => { beforeEach(() => { - sessionsClient.find = jest.fn().mockImplementation(async () => { + sessionsClient.extend = jest.fn().mockImplementation(async () => { return { saved_objects: [ { @@ -188,6 +188,20 @@ describe('Search Sessions Management API', () => { }); await api.sendExtend('my-id', '5d'); + expect(sessionsClient.extend).toHaveBeenCalledTimes(1); + expect(mockCoreStart.notifications.toasts.addSuccess).toHaveBeenCalled(); + }); + + test('displays error on reject', async () => { + sessionsClient.extend = jest.fn().mockRejectedValue({}); + const api = new SearchSessionsMgmtAPI(sessionsClient, mockConfig, { + urls: mockUrls, + notifications: mockCoreStart.notifications, + application: mockCoreStart.application, + }); + await api.sendExtend('my-id', '5d'); + + expect(sessionsClient.extend).toHaveBeenCalledTimes(1); expect(mockCoreStart.notifications.toasts.addError).toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts index 264556f91cc37..42e9384cce2d8 100644 --- a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts @@ -166,9 +166,6 @@ export class SearchSessionsMgmtAPI { }), }); } catch (err) { - // eslint-disable-next-line no-console - console.error(err); - this.deps.notifications.toasts.addError(err, { title: i18n.translate('xpack.data.mgmt.searchSessions.api.deletedError', { defaultMessage: 'Failed to delete the search session!', @@ -178,11 +175,21 @@ export class SearchSessionsMgmtAPI { } // Extend - public async sendExtend(id: string, ttl: string): Promise { - this.deps.notifications.toasts.addError(new Error('Not implemented'), { - title: i18n.translate('xpack.data.mgmt.searchSessions.api.extendError', { - defaultMessage: 'Failed to extend the session expiration!', - }), - }); + public async sendExtend(id: string, expires: string): Promise { + try { + await this.sessionsClient.extend(id, expires); + + this.deps.notifications.toasts.addSuccess({ + title: i18n.translate('xpack.data.mgmt.searchSessions.api.extended', { + defaultMessage: 'The search session was extended.', + }), + }); + } catch (err) { + this.deps.notifications.toasts.addError(err, { + title: i18n.translate('xpack.data.mgmt.searchSessions.api.extendError', { + defaultMessage: 'Failed to extend the search session!', + }), + }); + } } } From a0d4b04155032a11eacfb445100490802e947d39 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 9 Feb 2021 12:28:43 +0200 Subject: [PATCH 04/28] [Security Solution][Case] ServiceNow SIR Connector (#88655) Co-authored-by: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> --- .../servicenow/api.test.ts | 78 +++ .../builtin_action_types/servicenow/api.ts | 3 +- .../servicenow/index.test.ts | 120 +++++ .../builtin_action_types/servicenow/index.ts | 47 +- .../servicenow/translations.ts | 2 +- .../builtin_action_types/servicenow/types.ts | 1 + x-pack/plugins/case/common/api/cases/case.ts | 29 +- .../plugins/case/common/api/cases/comment.ts | 9 + .../case/common/api/cases/configure.ts | 4 - .../case/common/api/connectors/index.ts | 37 +- .../case/common/api/connectors/mappings.ts | 157 +----- .../{servicenow.ts => servicenow_itsm.ts} | 4 +- .../common/api/connectors/servicenow_sir.ts | 20 + x-pack/plugins/case/common/api/helpers.ts | 6 +- x-pack/plugins/case/common/constants.ts | 9 +- .../plugins/case/server/client/alerts/get.ts | 31 ++ .../case/server/client/alerts/types.ts | 19 + .../plugins/case/server/client/cases/get.ts | 45 ++ .../plugins/case/server/client/cases/mock.ts | 191 +++++++ .../plugins/case/server/client/cases/push.ts | 266 ++++++++++ .../plugins/case/server/client/cases/types.ts | 81 +++ .../configure => client/cases}/utils.test.ts | 467 ++++++++++++------ .../cases/configure => client/cases}/utils.ts | 238 ++++++--- .../case/server/client/configure/mock.ts | 4 +- .../case/server/client/configure/utils.ts | 10 +- .../plugins/case/server/client/index.test.ts | 86 ++-- x-pack/plugins/case/server/client/index.ts | 86 +--- x-pack/plugins/case/server/client/mocks.ts | 24 +- x-pack/plugins/case/server/client/types.ts | 51 +- .../case/server/client/user_actions/get.ts | 31 ++ .../case/server/connectors/case/index.ts | 3 +- .../plugins/case/server/connectors/index.ts | 48 +- .../jira/external_service_formatter.test.ts | 35 ++ .../jira/external_service_formatter.ts | 29 ++ .../external_service_formatter.test.ts | 26 + .../resilient/external_service_formatter.ts | 19 + .../connectors/servicenow/itsm_formatter.ts | 19 + .../servicenow/itsm_formmater.test.ts | 26 + .../servicenow/sir_formatter.test.ts | 164 ++++++ .../connectors/servicenow/sir_formatter.ts | 88 ++++ .../plugins/case/server/connectors/types.ts | 54 ++ x-pack/plugins/case/server/plugin.ts | 17 +- .../__fixtures__/create_mock_so_repository.ts | 14 + .../server/routes/api/__fixtures__/index.ts | 1 + .../api/__fixtures__/mock_actions_client.ts | 34 ++ .../api/__fixtures__/mock_saved_objects.ts | 47 +- .../routes/api/__fixtures__/route_contexts.ts | 25 +- .../routes/api/__mocks__/request_responses.ts | 29 +- .../api/cases/comments/delete_comment.test.ts | 8 +- .../api/cases/comments/get_comment.test.ts | 8 +- .../api/cases/comments/patch_comment.test.ts | 36 +- .../api/cases/comments/post_comment.test.ts | 40 +- .../api/cases/configure/get_configure.test.ts | 10 +- .../api/cases/configure/get_configure.ts | 4 +- .../cases/configure/get_connectors.test.ts | 16 +- .../api/cases/configure/get_connectors.ts | 13 +- .../server/routes/api/cases/configure/mock.ts | 76 --- .../cases/configure/patch_configure.test.ts | 14 +- .../api/cases/configure/patch_configure.ts | 2 +- .../cases/configure/post_configure.test.ts | 34 +- .../api/cases/configure/post_configure.ts | 4 +- .../configure/post_push_to_service.test.ts | 106 ---- .../cases/configure/post_push_to_service.ts | 81 --- .../routes/api/cases/delete_cases.test.ts | 16 +- .../routes/api/cases/find_cases.test.ts | 16 +- .../server/routes/api/cases/get_case.test.ts | 28 +- .../case/server/routes/api/cases/get_case.ts | 46 +- .../routes/api/cases/patch_cases.test.ts | 36 +- .../server/routes/api/cases/post_case.test.ts | 20 +- .../server/routes/api/cases/push_case.test.ts | 421 ++++++++++++++-- .../case/server/routes/api/cases/push_case.ts | 203 +------- .../api/cases/status/get_status.test.ts | 14 +- .../user_actions/get_all_user_actions.ts | 26 +- .../plugins/case/server/routes/api/index.ts | 6 +- .../plugins/case/server/routes/api/utils.ts | 18 +- .../case/server/services/alerts/index.ts | 44 ++ x-pack/plugins/case/server/services/mocks.ts | 1 + .../cases/connector_options.spec.ts | 7 + .../security_solution/cypress/objects/case.ts | 71 +++ .../cypress/screens/case_details.ts | 4 +- .../cypress/screens/edit_connector.ts | 2 +- .../cases/components/case_view/index.test.tsx | 13 +- .../cases/components/case_view/index.tsx | 2 - .../configure_cases/connectors.test.tsx | 4 +- .../components/configure_cases/index.test.tsx | 16 +- .../components/connector_selector/form.tsx | 18 +- .../{settings => connectors}/card.tsx | 6 +- .../case/{fields.tsx => alert_fields.tsx} | 0 .../cases/components/connectors/case/index.ts | 2 +- .../cases/components/connectors/config.ts | 34 +- .../connectors/connectors_registry.ts | 57 +++ .../{settings => connectors}/fields_form.tsx | 20 +- .../cases/components/connectors/index.ts | 46 ++ .../jira/__mocks__/api.ts | 0 .../{settings => connectors}/jira/api.test.ts | 0 .../{settings => connectors}/jira/api.ts | 0 .../jira/case_fields.test.tsx} | 2 +- .../jira/case_fields.tsx} | 19 +- .../{settings => connectors}/jira/index.ts | 6 +- .../jira/search_issues.tsx | 0 .../jira/translations.ts | 22 +- .../{settings => connectors}/jira/types.ts | 0 .../use_get_fields_by_issue_type.test.tsx | 0 .../jira/use_get_fields_by_issue_type.tsx | 0 .../jira/use_get_issue_types.test.tsx | 0 .../jira/use_get_issue_types.tsx | 0 .../jira/use_get_issues.test.tsx | 0 .../jira/use_get_issues.tsx | 0 .../jira/use_get_single_issue.test.tsx | 0 .../jira/use_get_single_issue.tsx | 0 .../cases/components/connectors/mock.ts | 109 ++++ .../resilient/__mocks__/api.ts | 21 +- .../{settings => connectors}/resilient/api.ts | 0 .../resilient/case_fields.test.tsx} | 2 +- .../resilient/case_fields.tsx} | 24 +- .../resilient/index.ts | 6 +- .../resilient/translations.ts | 10 +- .../resilient/types.ts | 0 .../resilient/use_get_incident_types.test.tsx | 0 .../resilient/use_get_incident_types.tsx | 0 .../resilient/use_get_severity.test.tsx | 0 .../resilient/use_get_severity.tsx | 0 .../connectors/servicenow/__mocks__/api.ts | 19 + .../connectors/servicenow/api.test.ts | 40 ++ .../components/connectors/servicenow/api.ts | 31 ++ .../components/connectors/servicenow/index.ts | 35 ++ .../servicenow_itsm_case_fields.test.tsx} | 62 ++- .../servicenow_itsm_case_fields.tsx | 164 ++++++ .../servicenow_sir_case_fields.test.tsx | 198 ++++++++ .../servicenow/servicenow_sir_case_fields.tsx | 293 +++++++++++ .../connectors/servicenow/translations.ts | 99 ++++ .../components/connectors/servicenow/types.ts | 18 + .../servicenow/use_get_choices.test.tsx | 144 ++++++ .../connectors/servicenow/use_get_choices.tsx | 99 ++++ .../cases/components/connectors/types.ts | 31 +- .../components/create/connector.test.tsx | 58 +-- .../cases/components/create/connector.tsx | 29 +- .../components/create/form_context.test.tsx | 62 +-- .../cases/components/create/form_context.tsx | 10 +- .../cases/components/create/index.test.tsx | 20 +- .../cases/components/edit_connector/index.tsx | 8 +- .../public/cases/components/settings/index.ts | 48 -- .../public/cases/components/settings/mock.ts | 21 - .../components/settings/servicenow/fields.tsx | 130 ----- .../components/settings/servicenow/index.ts | 25 - .../settings/servicenow/translations.ts | 49 -- .../components/settings/settings_registry.ts | 57 --- .../public/cases/components/settings/types.ts | 33 -- .../use_push_to_service/index.test.tsx | 38 +- .../components/use_push_to_service/index.tsx | 18 +- .../components/user_action_tree/index.tsx | 2 +- .../public/cases/containers/__mocks__/api.ts | 12 +- .../public/cases/containers/api.test.tsx | 106 ++-- .../public/cases/containers/api.ts | 39 +- .../public/cases/containers/mock.ts | 43 +- .../public/cases/containers/translations.ts | 7 - .../use_post_push_to_service.test.tsx | 304 +----------- .../containers/use_post_push_to_service.tsx | 219 ++------ .../public/cases/containers/utils.ts | 8 - .../translations/translations/ja-JP.json | 24 - .../translations/translations/zh-CN.json | 24 - .../builtin_action_types/jira/config.ts | 19 - .../builtin_action_types/jira/jira.tsx | 5 +- .../builtin_action_types/resilient/config.ts | 19 - .../resilient/resilient.tsx | 5 +- .../builtin_action_types/servicenow/config.ts | 31 -- .../servicenow/servicenow.tsx | 13 +- .../servicenow_itsm_params.test.tsx | 47 +- .../servicenow/translations.ts | 6 +- .../public/common/index.ts | 9 +- .../actions_simulators/server/plugin.ts | 3 + .../basic/tests/cases/push_case.ts | 167 +++++-- .../user_actions/get_all_user_actions.ts | 12 +- .../case_api_integration/common/lib/utils.ts | 4 +- 174 files changed, 4847 insertions(+), 2824 deletions(-) create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts rename x-pack/plugins/case/common/api/connectors/{servicenow.ts => servicenow_itsm.ts} (76%) create mode 100644 x-pack/plugins/case/common/api/connectors/servicenow_sir.ts create mode 100644 x-pack/plugins/case/server/client/alerts/get.ts create mode 100644 x-pack/plugins/case/server/client/alerts/types.ts create mode 100644 x-pack/plugins/case/server/client/cases/get.ts create mode 100644 x-pack/plugins/case/server/client/cases/mock.ts create mode 100644 x-pack/plugins/case/server/client/cases/push.ts create mode 100644 x-pack/plugins/case/server/client/cases/types.ts rename x-pack/plugins/case/server/{routes/api/cases/configure => client/cases}/utils.test.ts (52%) rename x-pack/plugins/case/server/{routes/api/cases/configure => client/cases}/utils.ts (50%) create mode 100644 x-pack/plugins/case/server/client/user_actions/get.ts create mode 100644 x-pack/plugins/case/server/connectors/jira/external_service_formatter.test.ts create mode 100644 x-pack/plugins/case/server/connectors/jira/external_service_formatter.ts create mode 100644 x-pack/plugins/case/server/connectors/resilient/external_service_formatter.test.ts create mode 100644 x-pack/plugins/case/server/connectors/resilient/external_service_formatter.ts create mode 100644 x-pack/plugins/case/server/connectors/servicenow/itsm_formatter.ts create mode 100644 x-pack/plugins/case/server/connectors/servicenow/itsm_formmater.test.ts create mode 100644 x-pack/plugins/case/server/connectors/servicenow/sir_formatter.test.ts create mode 100644 x-pack/plugins/case/server/connectors/servicenow/sir_formatter.ts create mode 100644 x-pack/plugins/case/server/connectors/types.ts create mode 100644 x-pack/plugins/case/server/routes/api/__fixtures__/mock_actions_client.ts delete mode 100644 x-pack/plugins/case/server/routes/api/cases/configure/mock.ts delete mode 100644 x-pack/plugins/case/server/routes/api/cases/configure/post_push_to_service.test.ts delete mode 100644 x-pack/plugins/case/server/routes/api/cases/configure/post_push_to_service.ts rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/card.tsx (89%) rename x-pack/plugins/security_solution/public/cases/components/connectors/case/{fields.tsx => alert_fields.tsx} (100%) create mode 100644 x-pack/plugins/security_solution/public/cases/components/connectors/connectors_registry.ts rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/fields_form.tsx (64%) rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/jira/__mocks__/api.ts (100%) rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/jira/api.test.ts (100%) rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/jira/api.ts (100%) rename x-pack/plugins/security_solution/public/cases/components/{settings/jira/fields.test.tsx => connectors/jira/case_fields.test.tsx} (99%) rename x-pack/plugins/security_solution/public/cases/components/{settings/jira/fields.tsx => connectors/jira/case_fields.tsx} (91%) rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/jira/index.ts (77%) rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/jira/search_issues.tsx (100%) rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/jira/translations.ts (60%) rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/jira/types.ts (100%) rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/jira/use_get_fields_by_issue_type.test.tsx (100%) rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/jira/use_get_fields_by_issue_type.tsx (100%) rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/jira/use_get_issue_types.test.tsx (100%) rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/jira/use_get_issue_types.tsx (100%) rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/jira/use_get_issues.test.tsx (100%) rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/jira/use_get_issues.tsx (100%) rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/jira/use_get_single_issue.test.tsx (100%) rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/jira/use_get_single_issue.tsx (100%) create mode 100644 x-pack/plugins/security_solution/public/cases/components/connectors/mock.ts rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/resilient/__mocks__/api.ts (70%) rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/resilient/api.ts (100%) rename x-pack/plugins/security_solution/public/cases/components/{settings/resilient/fields.test.tsx => connectors/resilient/case_fields.test.tsx} (99%) rename x-pack/plugins/security_solution/public/cases/components/{settings/resilient/fields.tsx => connectors/resilient/case_fields.tsx} (90%) rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/resilient/index.ts (77%) rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/resilient/translations.ts (67%) rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/resilient/types.ts (100%) rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/resilient/use_get_incident_types.test.tsx (100%) rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/resilient/use_get_incident_types.tsx (100%) rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/resilient/use_get_severity.test.tsx (100%) rename x-pack/plugins/security_solution/public/cases/components/{settings => connectors}/resilient/use_get_severity.tsx (100%) create mode 100644 x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/__mocks__/api.ts create mode 100644 x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/api.test.ts create mode 100644 x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/api.ts create mode 100644 x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/index.ts rename x-pack/plugins/security_solution/public/cases/components/{settings/servicenow/fields.test.tsx => connectors/servicenow/servicenow_itsm_case_fields.test.tsx} (52%) create mode 100644 x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_itsm_case_fields.tsx create mode 100644 x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_sir_case_fields.test.tsx create mode 100644 x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_sir_case_fields.tsx create mode 100644 x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/translations.ts create mode 100644 x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/types.ts create mode 100644 x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/use_get_choices.test.tsx create mode 100644 x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/use_get_choices.tsx delete mode 100644 x-pack/plugins/security_solution/public/cases/components/settings/index.ts delete mode 100644 x-pack/plugins/security_solution/public/cases/components/settings/mock.ts delete mode 100644 x-pack/plugins/security_solution/public/cases/components/settings/servicenow/fields.tsx delete mode 100644 x-pack/plugins/security_solution/public/cases/components/settings/servicenow/index.ts delete mode 100644 x-pack/plugins/security_solution/public/cases/components/settings/servicenow/translations.ts delete mode 100644 x-pack/plugins/security_solution/public/cases/components/settings/settings_registry.ts delete mode 100644 x-pack/plugins/security_solution/public/cases/components/settings/types.ts delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/config.ts delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/config.ts delete mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/config.ts diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts index 7ad6ec337bca1..662b1ce46a07b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts @@ -16,6 +16,7 @@ describe('api', () => { beforeEach(() => { externalService = externalServiceMock.create(); + jest.clearAllMocks(); }); describe('create incident', () => { @@ -26,6 +27,7 @@ describe('api', () => { params, secrets: {}, logger: mockedLogger, + commentFieldKey: 'comments', }); expect(res).toEqual({ @@ -57,6 +59,7 @@ describe('api', () => { params, secrets: {}, logger: mockedLogger, + commentFieldKey: 'comments', }); expect(res).toEqual({ @@ -77,6 +80,7 @@ describe('api', () => { params, secrets: { username: 'elastic', password: 'elastic' }, logger: mockedLogger, + commentFieldKey: 'comments', }); expect(externalService.createIncident).toHaveBeenCalledWith({ @@ -99,6 +103,7 @@ describe('api', () => { params, secrets: {}, logger: mockedLogger, + commentFieldKey: 'comments', }); expect(externalService.updateIncident).toHaveBeenCalledTimes(2); expect(externalService.updateIncident).toHaveBeenNthCalledWith(1, { @@ -125,6 +130,41 @@ describe('api', () => { incidentId: 'incident-1', }); }); + + test('it post comments to different comment field key', async () => { + const params = { ...apiParams, incident: { ...apiParams.incident, externalId: null } }; + await api.pushToService({ + externalService, + params, + secrets: {}, + logger: mockedLogger, + commentFieldKey: 'work_notes', + }); + expect(externalService.updateIncident).toHaveBeenCalledTimes(2); + expect(externalService.updateIncident).toHaveBeenNthCalledWith(1, { + incident: { + severity: '1', + urgency: '2', + impact: '3', + work_notes: 'A comment', + description: 'Incident description', + short_description: 'Incident title', + }, + incidentId: 'incident-1', + }); + + expect(externalService.updateIncident).toHaveBeenNthCalledWith(2, { + incident: { + severity: '1', + urgency: '2', + impact: '3', + work_notes: 'Another comment', + description: 'Incident description', + short_description: 'Incident title', + }, + incidentId: 'incident-1', + }); + }); }); describe('update incident', () => { @@ -134,6 +174,7 @@ describe('api', () => { params: apiParams, secrets: {}, logger: mockedLogger, + commentFieldKey: 'comments', }); expect(res).toEqual({ @@ -161,6 +202,7 @@ describe('api', () => { params, secrets: {}, logger: mockedLogger, + commentFieldKey: 'comments', }); expect(res).toEqual({ @@ -178,6 +220,7 @@ describe('api', () => { params, secrets: {}, logger: mockedLogger, + commentFieldKey: 'comments', }); expect(externalService.updateIncident).toHaveBeenCalledWith({ @@ -200,6 +243,7 @@ describe('api', () => { params, secrets: {}, logger: mockedLogger, + commentFieldKey: 'comments', }); expect(externalService.updateIncident).toHaveBeenCalledTimes(3); expect(externalService.updateIncident).toHaveBeenNthCalledWith(1, { @@ -225,6 +269,40 @@ describe('api', () => { incidentId: 'incident-2', }); }); + + test('it post comments to different comment field key', async () => { + const params = { ...apiParams }; + await api.pushToService({ + externalService, + params, + secrets: {}, + logger: mockedLogger, + commentFieldKey: 'work_notes', + }); + expect(externalService.updateIncident).toHaveBeenCalledTimes(3); + expect(externalService.updateIncident).toHaveBeenNthCalledWith(1, { + incident: { + severity: '1', + urgency: '2', + impact: '3', + description: 'Incident description', + short_description: 'Incident title', + }, + incidentId: 'incident-3', + }); + + expect(externalService.updateIncident).toHaveBeenNthCalledWith(2, { + incident: { + severity: '1', + urgency: '2', + impact: '3', + work_notes: 'A comment', + description: 'Incident description', + short_description: 'Incident title', + }, + incidentId: 'incident-2', + }); + }); }); describe('getFields', () => { diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts index 3aa1e50dc2aeb..4120c07c32303 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts @@ -25,6 +25,7 @@ const pushToServiceHandler = async ({ externalService, params, secrets, + commentFieldKey, }: PushToServiceApiHandlerArgs): Promise => { const { comments } = params; let res: PushToServiceResponse; @@ -53,7 +54,7 @@ const pushToServiceHandler = async ({ incidentId: res.id, incident: { ...incident, - comments: currentComment.comment, + [commentFieldKey]: currentComment.comment, }, }); res.comments = [ diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts new file mode 100644 index 0000000000000..e7e2b2bc4118e --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.test.ts @@ -0,0 +1,120 @@ +/* + * 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 { actionsMock } from '../../mocks'; +import { createActionTypeRegistry } from '../index.test'; +import { + ServiceNowPublicConfigurationType, + ServiceNowSecretConfigurationType, + ExecutorParams, + PushToServiceResponse, +} from './types'; +import { + ServiceNowActionType, + ServiceNowITSMActionTypeId, + ServiceNowSIRActionTypeId, + ServiceNowActionTypeExecutorOptions, +} from '.'; +import { api } from './api'; + +jest.mock('./api', () => ({ + api: { + getChoices: jest.fn(), + getFields: jest.fn(), + getIncident: jest.fn(), + handshake: jest.fn(), + pushToService: jest.fn(), + }, +})); + +const services = actionsMock.createServices(); + +describe('ServiceNow', () => { + const config = { apiUrl: 'https://instance.com' }; + const secrets = { username: 'username', password: 'password' }; + const params = { + subAction: 'pushToService', + subActionParams: { + incident: { + short_description: 'An incident', + description: 'This is serious', + }, + }, + }; + + beforeEach(() => { + (api.pushToService as jest.Mock).mockResolvedValue({ id: 'some-id' }); + }); + + describe('ServiceNow ITSM', () => { + let actionType: ServiceNowActionType; + + beforeAll(() => { + const { actionTypeRegistry } = createActionTypeRegistry(); + actionType = actionTypeRegistry.get< + ServiceNowPublicConfigurationType, + ServiceNowSecretConfigurationType, + ExecutorParams, + PushToServiceResponse | {} + >(ServiceNowITSMActionTypeId); + }); + + describe('execute()', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('it pass the correct comment field key', async () => { + const actionId = 'some-action-id'; + const executorOptions = ({ + actionId, + config, + secrets, + params, + services, + } as unknown) as ServiceNowActionTypeExecutorOptions; + await actionType.executor(executorOptions); + expect((api.pushToService as jest.Mock).mock.calls[0][0].commentFieldKey).toBe('comments'); + }); + }); + }); + + describe('ServiceNow SIR', () => { + let actionType: ServiceNowActionType; + + beforeAll(() => { + const { actionTypeRegistry } = createActionTypeRegistry(); + actionType = actionTypeRegistry.get< + ServiceNowPublicConfigurationType, + ServiceNowSecretConfigurationType, + ExecutorParams, + PushToServiceResponse | {} + >(ServiceNowSIRActionTypeId); + }); + + describe('execute()', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('it pass the correct comment field key', async () => { + const actionId = 'some-action-id'; + const executorOptions = ({ + actionId, + config, + secrets, + params, + services, + } as unknown) as ServiceNowActionTypeExecutorOptions; + await actionType.executor(executorOptions); + expect((api.pushToService as jest.Mock).mock.calls[0][0].commentFieldKey).toBe( + 'work_notes' + ); + }); + }); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts index cf9cef3c776c7..f6be7c90820a2 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts @@ -47,15 +47,21 @@ const serviceNowSIRTable = 'sn_si_incident'; export const ServiceNowITSMActionTypeId = '.servicenow'; export const ServiceNowSIRActionTypeId = '.servicenow-sir'; -// action type definition -export function getServiceNowITSMActionType( - params: GetActionTypeParams -): ActionType< +export type ServiceNowActionType = ActionType< ServiceNowPublicConfigurationType, ServiceNowSecretConfigurationType, ExecutorParams, PushToServiceResponse | {} -> { +>; + +export type ServiceNowActionTypeExecutorOptions = ActionTypeExecutorOptions< + ServiceNowPublicConfigurationType, + ServiceNowSecretConfigurationType, + ExecutorParams +>; + +// action type definition +export function getServiceNowITSMActionType(params: GetActionTypeParams): ServiceNowActionType { const { logger, configurationUtilities } = params; return { id: ServiceNowITSMActionTypeId, @@ -74,14 +80,7 @@ export function getServiceNowITSMActionType( }; } -export function getServiceNowSIRActionType( - params: GetActionTypeParams -): ActionType< - ServiceNowPublicConfigurationType, - ServiceNowSecretConfigurationType, - ExecutorParams, - PushToServiceResponse | {} -> { +export function getServiceNowSIRActionType(params: GetActionTypeParams): ServiceNowActionType { const { logger, configurationUtilities } = params; return { id: ServiceNowSIRActionTypeId, @@ -96,7 +95,12 @@ export function getServiceNowSIRActionType( }), params: ExecutorParamsSchemaSIR, }, - executor: curry(executor)({ logger, configurationUtilities, table: serviceNowSIRTable }), + executor: curry(executor)({ + logger, + configurationUtilities, + table: serviceNowSIRTable, + commentFieldKey: 'work_notes', + }), }; } @@ -107,12 +111,14 @@ async function executor( logger, configurationUtilities, table, - }: { logger: Logger; configurationUtilities: ActionsConfigurationUtilities; table: string }, - execOptions: ActionTypeExecutorOptions< - ServiceNowPublicConfigurationType, - ServiceNowSecretConfigurationType, - ExecutorParams - > + commentFieldKey = 'comments', + }: { + logger: Logger; + configurationUtilities: ActionsConfigurationUtilities; + table: string; + commentFieldKey?: string; + }, + execOptions: ServiceNowActionTypeExecutorOptions ): Promise> { const { actionId, config, params, secrets } = execOptions; const { subAction, subActionParams } = params; @@ -147,6 +153,7 @@ async function executor( params: pushToServiceParams, secrets, logger, + commentFieldKey, }); logger.debug(`response push to service for incident id: ${data.id}`); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts index 2110e9425fe6c..b46e118a7235f 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts @@ -16,7 +16,7 @@ export const SERVICENOW_ITSM = i18n.translate('xpack.actions.builtin.serviceNowI }); export const SERVICENOW_SIR = i18n.translate('xpack.actions.builtin.serviceNowSIRTitle', { - defaultMessage: 'ServiceNow SIR', + defaultMessage: 'ServiceNow SecOps', }); export const ALLOWED_HOSTS_ERROR = (message: string) => diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts index 8de3f911106c0..1c0b2c9c62eee 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts @@ -121,6 +121,7 @@ export interface PushToServiceApiHandlerArgs extends ExternalServiceApiHandlerAr params: PushToServiceApiParams; secrets: Record; logger: Logger; + commentFieldKey: string; } export interface GetIncidentApiHandlerArgs extends ExternalServiceApiHandlerArgs { diff --git a/x-pack/plugins/case/common/api/cases/case.ts b/x-pack/plugins/case/common/api/cases/case.ts index cd13b10846f12..bebd261fb7b9b 100644 --- a/x-pack/plugins/case/common/api/cases/case.ts +++ b/x-pack/plugins/case/common/api/cases/case.ts @@ -80,8 +80,6 @@ export const CasePostRequestRt = rt.type({ settings: SettingsRt, }); -export const CaseExternalServiceRequestRt = CaseExternalServiceBasicRt; - export const CasesFindRequestRt = rt.partial({ tags: rt.union([rt.array(rt.string), rt.string]), status: CaseStatusRt, @@ -126,6 +124,31 @@ export const CasePatchRequestRt = rt.intersection([ export const CasesPatchRequestRt = rt.type({ cases: rt.array(CasePatchRequestRt) }); export const CasesResponseRt = rt.array(CaseResponseRt); +export const CasePushRequestParamsRt = rt.type({ + case_id: rt.string, + connector_id: rt.string, +}); + +export const ExternalServiceResponseRt = rt.intersection([ + rt.type({ + title: rt.string, + id: rt.string, + pushedDate: rt.string, + url: rt.string, + }), + rt.partial({ + comments: rt.array( + rt.intersection([ + rt.type({ + commentId: rt.string, + pushedDate: rt.string, + }), + rt.partial({ externalCommentId: rt.string }), + ]) + ), + }), +]); + export type CaseAttributes = rt.TypeOf; export type CasePostRequest = rt.TypeOf; export type CaseResponse = rt.TypeOf; @@ -133,8 +156,8 @@ export type CasesResponse = rt.TypeOf; export type CasesFindResponse = rt.TypeOf; export type CasePatchRequest = rt.TypeOf; export type CasesPatchRequest = rt.TypeOf; -export type CaseExternalServiceRequest = rt.TypeOf; export type CaseFullExternalService = rt.TypeOf; +export type ExternalServiceResponse = rt.TypeOf; export type ESCaseAttributes = Omit & { connector: ESCaseConnector }; export type ESCasePatchRequest = Omit & { diff --git a/x-pack/plugins/case/common/api/cases/comment.ts b/x-pack/plugins/case/common/api/cases/comment.ts index 0670526e0df9c..7c9b31f496e54 100644 --- a/x-pack/plugins/case/common/api/cases/comment.ts +++ b/x-pack/plugins/case/common/api/cases/comment.ts @@ -45,6 +45,14 @@ export const CommentResponseRt = rt.intersection([ }), ]); +export const CommentResponseTypeAlertsRt = rt.intersection([ + AttributesTypeAlertsRt, + rt.type({ + id: rt.string, + version: rt.string, + }), +]); + export const AllCommentsResponseRT = rt.array(CommentResponseRt); export const CommentPatchRequestRt = rt.intersection([ @@ -84,6 +92,7 @@ export const AllCommentsResponseRt = rt.array(CommentResponseRt); export type CommentAttributes = rt.TypeOf; export type CommentRequest = rt.TypeOf; export type CommentResponse = rt.TypeOf; +export type CommentResponseAlertsType = rt.TypeOf; export type AllCommentsResponse = rt.TypeOf; export type CommentsResponse = rt.TypeOf; export type CommentPatchRequest = rt.TypeOf; diff --git a/x-pack/plugins/case/common/api/cases/configure.ts b/x-pack/plugins/case/common/api/cases/configure.ts index cb3a8b68082dc..b5a89efde1767 100644 --- a/x-pack/plugins/case/common/api/cases/configure.ts +++ b/x-pack/plugins/case/common/api/cases/configure.ts @@ -7,13 +7,9 @@ import * as rt from 'io-ts'; -import { ActionResult, ActionType } from '../../../../actions/common'; import { UserRT } from '../user'; import { CaseConnectorRt, ConnectorMappingsRt, ESCaseConnector } from '../connectors'; -export type ActionConnector = ActionResult; -export type ActionTypeConnector = ActionType; - // TODO: we will need to add this type rt.literal('close-by-third-party') const ClosureTypeRT = rt.union([rt.literal('close-by-user'), rt.literal('close-by-pushing')]); diff --git a/x-pack/plugins/case/common/api/connectors/index.ts b/x-pack/plugins/case/common/api/connectors/index.ts index 5fead4c8bd9c5..f9b7c8b12c2cd 100644 --- a/x-pack/plugins/case/common/api/connectors/index.ts +++ b/x-pack/plugins/case/common/api/connectors/index.ts @@ -7,25 +7,34 @@ import * as rt from 'io-ts'; +import { ActionResult, ActionType } from '../../../../actions/common'; import { JiraFieldsRT } from './jira'; import { ResilientFieldsRT } from './resilient'; -import { ServiceNowFieldsRT } from './servicenow'; +import { ServiceNowITSMFieldsRT } from './servicenow_itsm'; +import { ServiceNowSIRFieldsRT } from './servicenow_sir'; export * from './jira'; -export * from './servicenow'; +export * from './servicenow_itsm'; +export * from './servicenow_sir'; export * from './resilient'; export * from './mappings'; +export type ActionConnector = ActionResult; +export type ActionTypeConnector = ActionType; + export const ConnectorFieldsRt = rt.union([ JiraFieldsRT, ResilientFieldsRT, - ServiceNowFieldsRT, + ServiceNowITSMFieldsRT, + ServiceNowSIRFieldsRT, rt.null, ]); + export enum ConnectorTypes { jira = '.jira', resilient = '.resilient', - servicenow = '.servicenow', + serviceNowITSM = '.servicenow', + serviceNowSIR = '.servicenow-sir', none = '.none', } @@ -39,9 +48,14 @@ const ConnectorResillientTypeFieldsRt = rt.type({ fields: rt.union([ResilientFieldsRT, rt.null]), }); -const ConnectorServiceNowTypeFieldsRt = rt.type({ - type: rt.literal(ConnectorTypes.servicenow), - fields: rt.union([ServiceNowFieldsRT, rt.null]), +const ConnectorServiceNowITSMTypeFieldsRt = rt.type({ + type: rt.literal(ConnectorTypes.serviceNowITSM), + fields: rt.union([ServiceNowITSMFieldsRT, rt.null]), +}); + +const ConnectorServiceNowSIRTypeFieldsRt = rt.type({ + type: rt.literal(ConnectorTypes.serviceNowSIR), + fields: rt.union([ServiceNowSIRFieldsRT, rt.null]), }); const ConnectorNoneTypeFieldsRt = rt.type({ @@ -52,7 +66,8 @@ const ConnectorNoneTypeFieldsRt = rt.type({ export const ConnectorTypeFieldsRt = rt.union([ ConnectorJiraTypeFieldsRt, ConnectorResillientTypeFieldsRt, - ConnectorServiceNowTypeFieldsRt, + ConnectorServiceNowITSMTypeFieldsRt, + ConnectorServiceNowSIRTypeFieldsRt, ConnectorNoneTypeFieldsRt, ]); @@ -66,6 +81,12 @@ export const CaseConnectorRt = rt.intersection([ export type CaseConnector = rt.TypeOf; export type ConnectorTypeFields = rt.TypeOf; +export type ConnectorJiraTypeFields = rt.TypeOf; +export type ConnectorResillientTypeFields = rt.TypeOf; +export type ConnectorServiceNowITSMTypeFields = rt.TypeOf< + typeof ConnectorServiceNowITSMTypeFieldsRt +>; +export type ConnectorServiceNowSIRTypeFields = rt.TypeOf; // we need to change these types back and forth for storing in ES (arrays overwrite, objects merge) export type ConnectorFields = rt.TypeOf; diff --git a/x-pack/plugins/case/common/api/connectors/mappings.ts b/x-pack/plugins/case/common/api/connectors/mappings.ts index 38e3434f0e7a8..3d2013af47688 100644 --- a/x-pack/plugins/case/common/api/connectors/mappings.ts +++ b/x-pack/plugins/case/common/api/connectors/mappings.ts @@ -5,42 +5,7 @@ * 2.0. */ -/* eslint-disable @kbn/eslint/no-restricted-paths */ - import * as rt from 'io-ts'; -import { - PushToServiceApiParams as JiraPushToServiceApiParams, - Incident as JiraIncident, -} from '../../../../actions/server/builtin_action_types/jira/types'; -import { - PushToServiceApiParams as ResilientPushToServiceApiParams, - Incident as ResilientIncident, -} from '../../../../actions/server/builtin_action_types/resilient/types'; -import { - PushToServiceApiParamsITSM as ServiceNowITSMPushToServiceApiParams, - ServiceNowITSMIncident, -} from '../../../../actions/server/builtin_action_types/servicenow/types'; -import { ResilientFieldsRT } from './resilient'; -import { ServiceNowFieldsRT } from './servicenow'; -import { JiraFieldsRT } from './jira'; - -// Formerly imported from security_solution -export interface ElasticUser { - readonly email?: string | null; - readonly fullName?: string | null; - readonly username?: string | null; -} - -export { - JiraPushToServiceApiParams, - ResilientPushToServiceApiParams, - ServiceNowITSMPushToServiceApiParams, -}; -export type Incident = JiraIncident | ResilientIncident | ServiceNowITSMIncident; -export type PushToServiceApiParams = - | JiraPushToServiceApiParams - | ResilientPushToServiceApiParams - | ServiceNowITSMPushToServiceApiParams; const ActionTypeRT = rt.union([ rt.literal('append'), @@ -52,6 +17,7 @@ const CaseFieldRT = rt.union([ rt.literal('description'), rt.literal('comments'), ]); + const ThirdPartyFieldRT = rt.union([rt.string, rt.literal('not_mapped')]); export type ActionType = rt.TypeOf; export type CaseField = rt.TypeOf; @@ -62,9 +28,11 @@ export const ConnectorMappingsAttributesRT = rt.type({ source: CaseFieldRT, target: ThirdPartyFieldRT, }); + export const ConnectorMappingsRt = rt.type({ mappings: rt.array(ConnectorMappingsAttributesRT), }); + export type ConnectorMappingsAttributes = rt.TypeOf; export type ConnectorMappings = rt.TypeOf; @@ -76,125 +44,12 @@ const ConnectorFieldRt = rt.type({ required: rt.boolean, type: FieldTypeRT, }); + export type ConnectorField = rt.TypeOf; -export const ConnectorRequestParamsRt = rt.type({ - connector_id: rt.string, -}); -export const GetFieldsRequestQueryRt = rt.type({ - connector_type: rt.string, -}); + const GetFieldsResponseRt = rt.type({ defaultMappings: rt.array(ConnectorMappingsAttributesRT), fields: rt.array(ConnectorFieldRt), }); -export type GetFieldsResponse = rt.TypeOf; - -export type ExternalServiceParams = Record; - -export interface PipedField { - actionType: string; - key: string; - pipes: string[]; - value: string; -} -export interface PrepareFieldsForTransformArgs { - defaultPipes: string[]; - mappings: ConnectorMappingsAttributes[]; - params: ServiceConnectorCaseParams; -} -export interface EntityInformation { - createdAt: string; - createdBy: ElasticUser; - updatedAt: string | null; - updatedBy: ElasticUser | null; -} -export interface TransformerArgs { - date?: string; - previousValue?: string; - user?: string; - value: string; -} - -export type Transformer = (args: TransformerArgs) => TransformerArgs; -export interface TransformFieldsArgs { - currentIncident?: S; - fields: PipedField[]; - params: P; -} - -export const ServiceConnectorUserParams = rt.type({ - fullName: rt.union([rt.string, rt.null]), - username: rt.string, -}); - -export const ServiceConnectorCommentParamsRt = rt.type({ - commentId: rt.string, - comment: rt.string, - createdAt: rt.string, - createdBy: ServiceConnectorUserParams, - updatedAt: rt.union([rt.string, rt.null]), - updatedBy: rt.union([ServiceConnectorUserParams, rt.null]), -}); -export const ServiceConnectorBasicCaseParamsRt = rt.type({ - comments: rt.union([rt.array(ServiceConnectorCommentParamsRt), rt.null]), - createdAt: rt.string, - createdBy: ServiceConnectorUserParams, - description: rt.union([rt.string, rt.null]), - externalId: rt.union([rt.string, rt.null]), - savedObjectId: rt.string, - title: rt.string, - updatedAt: rt.union([rt.string, rt.null]), - updatedBy: rt.union([ServiceConnectorUserParams, rt.null]), -}); - -export const ConnectorPartialFieldsRt = rt.partial({ - ...JiraFieldsRT.props, - ...ResilientFieldsRT.props, - ...ServiceNowFieldsRT.props, -}); - -export const ServiceConnectorCaseParamsRt = rt.intersection([ - ServiceConnectorBasicCaseParamsRt, - ConnectorPartialFieldsRt, -]); -export const ServiceConnectorCaseResponseRt = rt.intersection([ - rt.type({ - title: rt.string, - id: rt.string, - pushedDate: rt.string, - url: rt.string, - }), - rt.partial({ - comments: rt.array( - rt.intersection([ - rt.type({ - commentId: rt.string, - pushedDate: rt.string, - }), - rt.partial({ externalCommentId: rt.string }), - ]) - ), - }), -]); -export type ServiceConnectorBasicCaseParams = rt.TypeOf; -export type ServiceConnectorCaseParams = rt.TypeOf; -export type ServiceConnectorCaseResponse = rt.TypeOf; -export type ServiceConnectorCommentParams = rt.TypeOf; - -export const PostPushRequestRt = rt.type({ - connector_type: rt.string, - params: ServiceConnectorCaseParamsRt, -}); - -export type PostPushRequest = rt.TypeOf; - -export interface SimpleComment { - comment: string; - commentId: string; -} - -export interface MapIncident { - incident: ExternalServiceParams; - comments: SimpleComment[]; -} +export type GetFieldsResponse = rt.TypeOf; diff --git a/x-pack/plugins/case/common/api/connectors/servicenow.ts b/x-pack/plugins/case/common/api/connectors/servicenow_itsm.ts similarity index 76% rename from x-pack/plugins/case/common/api/connectors/servicenow.ts rename to x-pack/plugins/case/common/api/connectors/servicenow_itsm.ts index fc4e8f9aa09a3..2e86a26971aaa 100644 --- a/x-pack/plugins/case/common/api/connectors/servicenow.ts +++ b/x-pack/plugins/case/common/api/connectors/servicenow_itsm.ts @@ -7,10 +7,10 @@ import * as rt from 'io-ts'; -export const ServiceNowFieldsRT = rt.type({ +export const ServiceNowITSMFieldsRT = rt.type({ impact: rt.union([rt.string, rt.null]), severity: rt.union([rt.string, rt.null]), urgency: rt.union([rt.string, rt.null]), }); -export type ServiceNowFieldsType = rt.TypeOf; +export type ServiceNowITSMFieldsType = rt.TypeOf; diff --git a/x-pack/plugins/case/common/api/connectors/servicenow_sir.ts b/x-pack/plugins/case/common/api/connectors/servicenow_sir.ts new file mode 100644 index 0000000000000..749abdea87437 --- /dev/null +++ b/x-pack/plugins/case/common/api/connectors/servicenow_sir.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as rt from 'io-ts'; + +export const ServiceNowSIRFieldsRT = rt.type({ + category: rt.union([rt.string, rt.null]), + destIp: rt.union([rt.boolean, rt.null]), + malwareHash: rt.union([rt.boolean, rt.null]), + malwareUrl: rt.union([rt.boolean, rt.null]), + priority: rt.union([rt.string, rt.null]), + sourceIp: rt.union([rt.boolean, rt.null]), + subcategory: rt.union([rt.string, rt.null]), +}); + +export type ServiceNowSIRFieldsType = rt.TypeOf; diff --git a/x-pack/plugins/case/common/api/helpers.ts b/x-pack/plugins/case/common/api/helpers.ts index f9de74f45de46..24c4756a1596b 100644 --- a/x-pack/plugins/case/common/api/helpers.ts +++ b/x-pack/plugins/case/common/api/helpers.ts @@ -10,7 +10,7 @@ import { CASE_COMMENTS_URL, CASE_USER_ACTIONS_URL, CASE_COMMENT_DETAILS_URL, - CASE_CONFIGURE_PUSH_URL, + CASE_PUSH_URL, } from '../constants'; export const getCaseDetailsUrl = (id: string): string => { @@ -28,6 +28,6 @@ export const getCaseCommentDetailsUrl = (caseId: string, commentId: string): str export const getCaseUserActionUrl = (id: string): string => { return CASE_USER_ACTIONS_URL.replace('{case_id}', id); }; -export const getCaseConfigurePushUrl = (id: string): string => { - return CASE_CONFIGURE_PUSH_URL.replace('{connector_id}', id); +export const getCasePushUrl = (caseId: string, connectorId: string): string => { + return CASE_PUSH_URL.replace('{case_id}', caseId).replace('{connector_id}', connectorId); }; diff --git a/x-pack/plugins/case/common/constants.ts b/x-pack/plugins/case/common/constants.ts index 231ff9ef2dc4d..92dd2312f1ecf 100644 --- a/x-pack/plugins/case/common/constants.ts +++ b/x-pack/plugins/case/common/constants.ts @@ -15,10 +15,9 @@ export const CASES_URL = '/api/cases'; export const CASE_DETAILS_URL = `${CASES_URL}/{case_id}`; export const CASE_CONFIGURE_URL = `${CASES_URL}/configure`; export const CASE_CONFIGURE_CONNECTORS_URL = `${CASE_CONFIGURE_URL}/connectors`; -export const CASE_CONFIGURE_CONNECTOR_DETAILS_URL = `${CASE_CONFIGURE_CONNECTORS_URL}/{connector_id}`; -export const CASE_CONFIGURE_PUSH_URL = `${CASE_CONFIGURE_CONNECTOR_DETAILS_URL}/push`; export const CASE_COMMENTS_URL = `${CASE_DETAILS_URL}/comments`; export const CASE_COMMENT_DETAILS_URL = `${CASE_DETAILS_URL}/comments/{comment_id}`; +export const CASE_PUSH_URL = `${CASE_DETAILS_URL}/connector/{connector_id}/_push`; export const CASE_REPORTERS_URL = `${CASES_URL}/reporters`; export const CASE_STATUS_URL = `${CASES_URL}/status`; export const CASE_TAGS_URL = `${CASES_URL}/tags`; @@ -30,12 +29,14 @@ export const CASE_USER_ACTIONS_URL = `${CASE_DETAILS_URL}/user_actions`; export const ACTION_URL = '/api/actions'; export const ACTION_TYPES_URL = '/api/actions/list_action_types'; -export const SERVICENOW_ACTION_TYPE_ID = '.servicenow'; +export const SERVICENOW_ITSM_ACTION_TYPE_ID = '.servicenow'; +export const SERVICENOW_SIR_ACTION_TYPE_ID = '.servicenow-sir'; export const JIRA_ACTION_TYPE_ID = '.jira'; export const RESILIENT_ACTION_TYPE_ID = '.resilient'; export const SUPPORTED_CONNECTORS = [ - SERVICENOW_ACTION_TYPE_ID, + SERVICENOW_ITSM_ACTION_TYPE_ID, + SERVICENOW_SIR_ACTION_TYPE_ID, JIRA_ACTION_TYPE_ID, RESILIENT_ACTION_TYPE_ID, ]; diff --git a/x-pack/plugins/case/server/client/alerts/get.ts b/x-pack/plugins/case/server/client/alerts/get.ts new file mode 100644 index 0000000000000..718dd327aa08c --- /dev/null +++ b/x-pack/plugins/case/server/client/alerts/get.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import Boom from '@hapi/boom'; +import { CaseClientGetAlerts, CaseClientFactoryArguments } from '../types'; +import { CaseClientGetAlertsResponse } from './types'; + +export const get = ({ alertsService, request, context }: CaseClientFactoryArguments) => async ({ + ids, +}: CaseClientGetAlerts): Promise => { + const securitySolutionClient = context?.securitySolution?.getAppClient(); + if (securitySolutionClient == null) { + throw Boom.notFound('securitySolutionClient client have not been found'); + } + + if (ids.length === 0) { + return []; + } + + const index = securitySolutionClient.getSignalsIndex(); + const alerts = await alertsService.getAlerts({ ids, index, request }); + return alerts.hits.hits.map((alert) => ({ + id: alert._id, + index: alert._index, + ...alert._source, + })); +}; diff --git a/x-pack/plugins/case/server/client/alerts/types.ts b/x-pack/plugins/case/server/client/alerts/types.ts new file mode 100644 index 0000000000000..7b9d4a8856f48 --- /dev/null +++ b/x-pack/plugins/case/server/client/alerts/types.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. + */ + +interface Alert { + id: string; + index: string; + destination?: { + ip: string; + }; + source?: { + ip: string; + }; +} + +export type CaseClientGetAlertsResponse = Alert[]; diff --git a/x-pack/plugins/case/server/client/cases/get.ts b/x-pack/plugins/case/server/client/cases/get.ts new file mode 100644 index 0000000000000..c1901ccaae511 --- /dev/null +++ b/x-pack/plugins/case/server/client/cases/get.ts @@ -0,0 +1,45 @@ +/* + * 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 { flattenCaseSavedObject } from '../../routes/api/utils'; +import { CaseResponseRt, CaseResponse } from '../../../common/api'; +import { CaseClientGet, CaseClientFactoryArguments } from '../types'; + +export const get = ({ savedObjectsClient, caseService }: CaseClientFactoryArguments) => async ({ + id, + includeComments = false, +}: CaseClientGet): Promise => { + const theCase = await caseService.getCase({ + client: savedObjectsClient, + caseId: id, + }); + + if (!includeComments) { + return CaseResponseRt.encode( + flattenCaseSavedObject({ + savedObject: theCase, + }) + ); + } + + const theComments = await caseService.getAllCaseComments({ + client: savedObjectsClient, + caseId: id, + options: { + sortField: 'created_at', + sortOrder: 'asc', + }, + }); + + return CaseResponseRt.encode( + flattenCaseSavedObject({ + savedObject: theCase, + comments: theComments.saved_objects, + totalComment: theComments.total, + }) + ); +}; diff --git a/x-pack/plugins/case/server/client/cases/mock.ts b/x-pack/plugins/case/server/client/cases/mock.ts new file mode 100644 index 0000000000000..57e2d4373a52b --- /dev/null +++ b/x-pack/plugins/case/server/client/cases/mock.ts @@ -0,0 +1,191 @@ +/* + * 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 { + CommentResponse, + CommentType, + ConnectorMappingsAttributes, + CaseUserActionsResponse, +} from '../../../common/api'; + +import { BasicParams } from './types'; + +export const updateUser = { + updated_at: '2020-03-13T08:34:53.450Z', + updated_by: { full_name: 'Another User', username: 'another', email: 'elastic@elastic.co' }, +}; + +const entity = { + createdAt: '2020-03-13T08:34:53.450Z', + createdBy: { full_name: 'Elastic User', username: 'elastic', email: 'elastic@elastic.co' }, + updatedAt: null, + updatedBy: null, +}; + +export const comment: CommentResponse = { + id: 'mock-comment-1', + comment: 'Wow, good luck catching that bad meanie!', + type: CommentType.user as const, + created_at: '2019-11-25T21:55:00.177Z', + created_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + pushed_at: null, + pushed_by: null, + updated_at: '2019-11-25T21:55:00.177Z', + updated_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + version: 'WzEsMV0=', +}; + +export const commentAlert: CommentResponse = { + id: 'mock-comment-1', + alertId: 'alert-id-1', + index: 'alert-index-1', + type: CommentType.alert as const, + created_at: '2019-11-25T21:55:00.177Z', + created_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + pushed_at: null, + pushed_by: null, + updated_at: '2019-11-25T21:55:00.177Z', + updated_by: { + full_name: 'elastic', + email: 'testemail@elastic.co', + username: 'elastic', + }, + version: 'WzEsMV0=', +}; + +export const defaultPipes = ['informationCreated']; +export const basicParams: BasicParams = { + description: 'a description', + title: 'a title', + ...entity, +}; + +export const mappings: ConnectorMappingsAttributes[] = [ + { + source: 'title', + target: 'short_description', + action_type: 'overwrite', + }, + { + source: 'description', + target: 'description', + action_type: 'append', + }, + { + source: 'comments', + target: 'comments', + action_type: 'append', + }, +]; + +export const userActions: CaseUserActionsResponse = [ + { + action_field: ['description', 'status', 'tags', 'title', 'connector', 'settings'], + action: 'create', + action_at: '2021-02-03T17:41:03.771Z', + action_by: { + email: 'elastic@elastic.co', + full_name: 'Elastic', + username: 'elastic', + }, + new_value: + '{"title":"Case SIR","tags":["sir"],"description":"testing sir","connector":{"id":"456","name":"ServiceNow SN","type":".servicenow-sir","fields":{"category":"Denial of Service","destIp":true,"malwareHash":true,"malwareUrl":true,"priority":"2","sourceIp":true,"subcategory":"45"}},"settings":{"syncAlerts":true}}', + old_value: null, + action_id: 'fd830c60-6646-11eb-a291-51bf6b175a53', + case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', + comment_id: null, + }, + { + action_field: ['pushed'], + action: 'push-to-service', + action_at: '2021-02-03T17:41:26.108Z', + action_by: { + email: 'elastic@elastic.co', + full_name: 'Elastic', + username: 'elastic', + }, + new_value: + '{"pushed_at":"2021-02-03T17:41:26.108Z","pushed_by":{"username":"elastic","full_name":"Elastic","email":"elastic@elastic.co"},"connector_id":"456","connector_name":"ServiceNow SN","external_id":"external-id","external_title":"SIR0010037","external_url":"https://dev92273.service-now.com/nav_to.do?uri=sn_si_incident.do?sys_id=external-id"}', + old_value: null, + action_id: '0a801750-6647-11eb-a291-51bf6b175a53', + case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', + comment_id: null, + }, + { + action_field: ['comment'], + action: 'create', + action_at: '2021-02-03T17:44:21.067Z', + action_by: { + email: 'elastic@elastic.co', + full_name: 'Elastic', + username: 'elastic', + }, + new_value: '{"type":"alert","alertId":"alert-id-1","index":".siem-signals-default-000008"}', + old_value: null, + action_id: '7373eb60-6647-11eb-a291-51bf6b175a53', + case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', + comment_id: 'comment-alert-1', + }, + { + action_field: ['comment'], + action: 'create', + action_at: '2021-02-03T17:44:33.078Z', + action_by: { + email: 'elastic@elastic.co', + full_name: 'Elastic', + username: 'elastic', + }, + new_value: '{"type":"alert","alertId":"alert-id-2","index":".siem-signals-default-000008"}', + old_value: null, + action_id: '7abc6410-6647-11eb-a291-51bf6b175a53', + case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', + comment_id: 'comment-alert-2', + }, + { + action_field: ['pushed'], + action: 'push-to-service', + action_at: '2021-02-03T17:45:29.400Z', + action_by: { + email: 'elastic@elastic.co', + full_name: 'Elastic', + username: 'elastic', + }, + new_value: + '{"pushed_at":"2021-02-03T17:45:29.400Z","pushed_by":{"username":"elastic","full_name":"Elastic","email":"elastic@elastic.co"},"connector_id":"456","connector_name":"ServiceNow SN","external_id":"external-id","external_title":"SIR0010037","external_url":"https://dev92273.service-now.com/nav_to.do?uri=sn_si_incident.do?sys_id=external-id"}', + old_value: null, + action_id: '9b91d8f0-6647-11eb-a291-51bf6b175a53', + case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', + comment_id: null, + }, + { + action_field: ['comment'], + action: 'create', + action_at: '2021-02-03T17:48:30.616Z', + action_by: { + email: 'elastic@elastic.co', + full_name: 'Elastic', + username: 'elastic', + }, + new_value: '{"comment":"a comment!","type":"user"}', + old_value: null, + action_id: '0818e5e0-6648-11eb-a291-51bf6b175a53', + case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', + comment_id: 'comment-user-1', + }, +]; diff --git a/x-pack/plugins/case/server/client/cases/push.ts b/x-pack/plugins/case/server/client/cases/push.ts new file mode 100644 index 0000000000000..f329fb4d00d07 --- /dev/null +++ b/x-pack/plugins/case/server/client/cases/push.ts @@ -0,0 +1,266 @@ +/* + * 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 Boom, { isBoom, Boom as BoomType } from '@hapi/boom'; + +import { SavedObjectsBulkUpdateResponse, SavedObjectsUpdateResponse } from 'kibana/server'; +import { flattenCaseSavedObject } from '../../routes/api/utils'; + +import { + ActionConnector, + CaseResponseRt, + CaseResponse, + CaseStatuses, + ExternalServiceResponse, + ESCaseAttributes, + CommentAttributes, +} from '../../../common/api'; +import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; + +import { CaseClientPush, CaseClientFactoryArguments } from '../types'; +import { createIncident, getCommentContextFromAttributes, isCommentAlertType } from './utils'; + +const createError = (e: Error | BoomType, message: string): Error | BoomType => { + if (isBoom(e)) { + e.message = message; + e.output.payload.message = message; + return e; + } + + return Error(message); +}; + +export const push = ({ + savedObjectsClient, + caseService, + caseConfigureService, + userActionService, + request, + response, +}: CaseClientFactoryArguments) => async ({ + actionsClient, + caseClient, + caseId, + connectorId, +}: CaseClientPush): Promise => { + /* Start of push to external service */ + let theCase; + let connector; + let userActions; + let alerts; + let connectorMappings; + let externalServiceIncident; + + try { + [theCase, connector, userActions] = await Promise.all([ + caseClient.get({ id: caseId, includeComments: true }), + actionsClient.get({ id: connectorId }), + caseClient.getUserActions({ caseId }), + ]); + } catch (e) { + const message = `Error getting case and/or connector and/or user actions: ${e.message}`; + throw createError(e, message); + } + + // We need to change the logic when we support subcases + if (theCase?.status === CaseStatuses.closed) { + throw Boom.conflict( + `This case ${theCase.title} is closed. You can not pushed if the case is closed.` + ); + } + + try { + alerts = await caseClient.getAlerts({ + ids: theCase?.comments?.filter(isCommentAlertType).map((comment) => comment.alertId) ?? [], + }); + } catch (e) { + throw new Error(`Error getting alerts for case with id ${theCase.id}: ${e.message}`); + } + + try { + connectorMappings = await caseClient.getMappings({ + actionsClient, + caseClient, + connectorId: connector.id, + connectorType: connector.actionTypeId, + }); + } catch (e) { + const message = `Error getting mapping for connector with id ${connector.id}: ${e.message}`; + throw createError(e, message); + } + + try { + externalServiceIncident = await createIncident({ + actionsClient, + theCase, + userActions, + connector: connector as ActionConnector, + mappings: connectorMappings, + alerts, + }); + } catch (e) { + const message = `Error creating incident for case with id ${theCase.id}: ${e.message}`; + throw createError(e, message); + } + + const pushRes = await actionsClient.execute({ + actionId: connector?.id ?? '', + params: { + subAction: 'pushToService', + subActionParams: externalServiceIncident, + }, + }); + + if (pushRes.status === 'error') { + throw Boom.failedDependency( + pushRes.serviceMessage ?? pushRes.message ?? 'Error pushing to service' + ); + } + + /* End of push to external service */ + + /* Start of update case with push information */ + let user; + let myCase; + let myCaseConfigure; + let comments; + + try { + [user, myCase, myCaseConfigure, comments] = await Promise.all([ + caseService.getUser({ request, response }), + caseService.getCase({ + client: savedObjectsClient, + caseId, + }), + caseConfigureService.find({ client: savedObjectsClient }), + caseService.getAllCaseComments({ + client: savedObjectsClient, + caseId, + options: { + fields: [], + page: 1, + perPage: theCase?.totalComment ?? 0, + }, + }), + ]); + } catch (e) { + const message = `Error getting user and/or case and/or case configuration and/or case comments: ${e.message}`; + throw createError(e, message); + } + + // eslint-disable-next-line @typescript-eslint/naming-convention + const { username, full_name, email } = user; + const pushedDate = new Date().toISOString(); + const externalServiceResponse = pushRes.data as ExternalServiceResponse; + + const externalService = { + pushed_at: pushedDate, + pushed_by: { username, full_name, email }, + connector_id: connector.id, + connector_name: connector.name, + external_id: externalServiceResponse.id, + external_title: externalServiceResponse.title, + external_url: externalServiceResponse.url, + }; + + let updatedCase: SavedObjectsUpdateResponse; + let updatedComments: SavedObjectsBulkUpdateResponse; + + try { + [updatedCase, updatedComments] = await Promise.all([ + caseService.patchCase({ + client: savedObjectsClient, + caseId, + updatedAttributes: { + ...(myCaseConfigure.total > 0 && + myCaseConfigure.saved_objects[0].attributes.closure_type === 'close-by-pushing' + ? { + status: CaseStatuses.closed, + closed_at: pushedDate, + closed_by: { email, full_name, username }, + } + : {}), + external_service: externalService, + updated_at: pushedDate, + updated_by: { username, full_name, email }, + }, + version: myCase.version, + }), + + caseService.patchComments({ + client: savedObjectsClient, + comments: comments.saved_objects + .filter((comment) => comment.attributes.pushed_at == null) + .map((comment) => ({ + commentId: comment.id, + updatedAttributes: { + pushed_at: pushedDate, + pushed_by: { username, full_name, email }, + }, + version: comment.version, + })), + }), + + userActionService.postUserActions({ + client: savedObjectsClient, + actions: [ + ...(myCaseConfigure.total > 0 && + myCaseConfigure.saved_objects[0].attributes.closure_type === 'close-by-pushing' + ? [ + buildCaseUserActionItem({ + action: 'update', + actionAt: pushedDate, + actionBy: { username, full_name, email }, + caseId, + fields: ['status'], + newValue: CaseStatuses.closed, + oldValue: myCase.attributes.status, + }), + ] + : []), + buildCaseUserActionItem({ + action: 'push-to-service', + actionAt: pushedDate, + actionBy: { username, full_name, email }, + caseId, + fields: ['pushed'], + newValue: JSON.stringify(externalService), + }), + ], + }), + ]); + } catch (e) { + const message = `Error updating case and/or comments and/or creating user action: ${e.message}`; + throw createError(e, message); + } + /* End of update case with push information */ + + return CaseResponseRt.encode( + flattenCaseSavedObject({ + savedObject: { + ...myCase, + ...updatedCase, + attributes: { ...myCase.attributes, ...updatedCase?.attributes }, + references: myCase.references, + }, + comments: comments.saved_objects.map((origComment) => { + const updatedComment = updatedComments.saved_objects.find((c) => c.id === origComment.id); + return { + ...origComment, + ...updatedComment, + attributes: { + ...origComment.attributes, + ...updatedComment?.attributes, + ...getCommentContextFromAttributes(origComment.attributes), + }, + version: updatedComment?.version ?? origComment.version, + references: origComment?.references ?? [], + }; + }), + }) + ); +}; diff --git a/x-pack/plugins/case/server/client/cases/types.ts b/x-pack/plugins/case/server/client/cases/types.ts new file mode 100644 index 0000000000000..f1d56e7132bd1 --- /dev/null +++ b/x-pack/plugins/case/server/client/cases/types.ts @@ -0,0 +1,81 @@ +/* + * 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. + */ + +/* eslint-disable @kbn/eslint/no-restricted-paths */ +import { + PushToServiceApiParams as JiraPushToServiceApiParams, + Incident as JiraIncident, +} from '../../../../actions/server/builtin_action_types/jira/types'; +import { + PushToServiceApiParams as ResilientPushToServiceApiParams, + Incident as ResilientIncident, +} from '../../../../actions/server/builtin_action_types/resilient/types'; +import { + PushToServiceApiParamsITSM as ServiceNowITSMPushToServiceApiParams, + PushToServiceApiParamsSIR as ServiceNowSIRPushToServiceApiParams, + ServiceNowITSMIncident, +} from '../../../../actions/server/builtin_action_types/servicenow/types'; +import { CaseResponse, ConnectorMappingsAttributes } from '../../../common/api'; + +export type Incident = JiraIncident | ResilientIncident | ServiceNowITSMIncident; +export type PushToServiceApiParams = + | JiraPushToServiceApiParams + | ResilientPushToServiceApiParams + | ServiceNowITSMPushToServiceApiParams + | ServiceNowSIRPushToServiceApiParams; + +export type ExternalServiceParams = Record; + +export interface BasicParams { + title: CaseResponse['title']; + description: CaseResponse['description']; + createdAt: CaseResponse['created_at']; + createdBy: CaseResponse['created_by']; + updatedAt: CaseResponse['updated_at']; + updatedBy: CaseResponse['updated_by']; +} + +export interface PipedField { + actionType: string; + key: string; + pipes: string[]; + value: string; +} +export interface PrepareFieldsForTransformArgs { + defaultPipes: string[]; + mappings: ConnectorMappingsAttributes[]; + params: { title: string; description: string }; +} +export interface EntityInformation { + createdAt: CaseResponse['created_at']; + createdBy: CaseResponse['created_by']; + updatedAt: CaseResponse['updated_at']; + updatedBy: CaseResponse['updated_by']; +} +export interface TransformerArgs { + date?: string; + previousValue?: string; + user?: string; + value: string; +} + +export type Transformer = (args: TransformerArgs) => TransformerArgs; +export interface TransformFieldsArgs { + currentIncident?: S; + fields: PipedField[]; + params: P; +} + +export interface ExternalServiceComment { + comment: string; + commentId: string; +} + +export interface MapIncident { + incident: ExternalServiceParams; + comments: ExternalServiceComment[]; +} diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/utils.test.ts b/x-pack/plugins/case/server/client/cases/utils.test.ts similarity index 52% rename from x-pack/plugins/case/server/routes/api/cases/configure/utils.test.ts rename to x-pack/plugins/case/server/client/cases/utils.test.ts index 5114703c60963..dca2c34602678 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/utils.test.ts +++ b/x-pack/plugins/case/server/client/cases/utils.test.ts @@ -5,34 +5,45 @@ * 2.0. */ +import { actionsClientMock } from '../../../../actions/server/actions_client.mock'; +import { flattenCaseSavedObject } from '../../routes/api/utils'; +import { mockCases } from '../../routes/api/__fixtures__'; + +import { BasicParams, ExternalServiceParams, Incident } from './types'; +import { + comment as commentObj, + mappings, + defaultPipes, + basicParams, + userActions, + commentAlert, +} from './mock'; + import { - mapIncident, + createIncident, + getLatestPushInfo, prepareFieldsForTransformation, - serviceFormatter, transformComments, transformers, transformFields, } from './utils'; -import { comment as commentObj, mappings, defaultPipes, params, updateUser } from './mock'; -import { - ConnectorTypes, - ExternalServiceParams, - Incident, - ServiceConnectorCaseParams, -} from '../../../../../common/api/connectors'; -import { actionsClientMock } from '../../../../../../actions/server/actions_client.mock'; -import { mappings as mappingsMock } from '../../../../client/configure/mock'; -const formatComment = { commentId: commentObj.commentId, comment: commentObj.comment }; -const serviceNowParams = params[ConnectorTypes.servicenow] as ServiceConnectorCaseParams; -describe('api/cases/configure/utils', () => { +const formatComment = { + commentId: commentObj.id, + comment: 'Wow, good luck catching that bad meanie!', +}; + +const params = { ...basicParams }; + +describe('utils', () => { describe('prepareFieldsForTransformation', () => { test('prepare fields with defaults', () => { const res = prepareFieldsForTransformation({ defaultPipes, - params: serviceNowParams, + params, mappings, }); + expect(res).toEqual([ { actionType: 'overwrite', @@ -53,8 +64,9 @@ describe('api/cases/configure/utils', () => { const res = prepareFieldsForTransformation({ defaultPipes: ['myTestPipe'], mappings, - params: serviceNowParams, + params, }); + expect(res).toEqual([ { actionType: 'overwrite', @@ -71,16 +83,17 @@ describe('api/cases/configure/utils', () => { ]); }); }); + describe('transformFields', () => { test('transform fields for creation correctly', () => { const fields = prepareFieldsForTransformation({ defaultPipes, mappings, - params: serviceNowParams, + params, }); - const res = transformFields({ - params: serviceNowParams, + const res = transformFields({ + params, fields, }); @@ -92,18 +105,19 @@ describe('api/cases/configure/utils', () => { test('transform fields for update correctly', () => { const fields = prepareFieldsForTransformation({ - params: serviceNowParams, + params, mappings, defaultPipes: ['informationUpdated'], }); - const res = transformFields({ + const res = transformFields({ params: { - ...serviceNowParams, + ...params, updatedAt: '2020-03-15T08:34:53.450Z', updatedBy: { username: 'anotherUser', - fullName: 'Another User', + full_name: 'Another User', + email: 'elastic@elastic.co', }, }, fields, @@ -112,6 +126,7 @@ describe('api/cases/configure/utils', () => { description: 'first description (created at 2020-03-13T08:34:53.450Z by Elastic User)', }, }); + expect(res).toEqual({ short_description: 'a title (updated at 2020-03-15T08:34:53.450Z by Another User)', description: @@ -121,13 +136,13 @@ describe('api/cases/configure/utils', () => { test('add newline character to description', () => { const fields = prepareFieldsForTransformation({ - params: serviceNowParams, + params, mappings, defaultPipes: ['informationUpdated'], }); - const res = transformFields({ - params: serviceNowParams, + const res = transformFields({ + params, fields, currentIncident: { short_description: 'first title', @@ -141,13 +156,13 @@ describe('api/cases/configure/utils', () => { const fields = prepareFieldsForTransformation({ defaultPipes, mappings, - params: serviceNowParams, + params, }); - const res = transformFields({ + const res = transformFields({ params: { - ...serviceNowParams, - createdBy: { fullName: '', username: 'elastic' }, + ...params, + createdBy: { full_name: '', username: 'elastic', email: 'elastic@elastic.co' }, }, fields, }); @@ -162,14 +177,14 @@ describe('api/cases/configure/utils', () => { const fields = prepareFieldsForTransformation({ defaultPipes: ['informationUpdated'], mappings, - params: serviceNowParams, + params, }); - const res = transformFields({ + const res = transformFields({ params: { - ...serviceNowParams, + ...params, updatedAt: '2020-03-15T08:34:53.450Z', - updatedBy: { username: 'anotherUser', fullName: '' }, + updatedBy: { username: 'anotherUser', full_name: '', email: 'elastic@elastic.co' }, }, fields, }); @@ -180,6 +195,7 @@ describe('api/cases/configure/utils', () => { }); }); }); + describe('transformComments', () => { test('transform creation comments', () => { const comments = [commentObj]; @@ -187,7 +203,7 @@ describe('api/cases/configure/utils', () => { expect(res).toEqual([ { ...formatComment, - comment: `${formatComment.comment} (created at ${comments[0].createdAt} by ${comments[0].createdBy.fullName})`, + comment: `${formatComment.comment} (created at ${comments[0].created_at} by ${comments[0].created_by.full_name})`, }, ]); }); @@ -196,14 +212,19 @@ describe('api/cases/configure/utils', () => { const comments = [ { ...commentObj, - ...updateUser, + updated_at: '2020-03-13T08:34:53.450Z', + updated_by: { + full_name: 'Another User', + username: 'another', + email: 'elastic@elastic.co', + }, }, ]; const res = transformComments(comments, ['informationUpdated']); expect(res).toEqual([ { ...formatComment, - comment: `${formatComment.comment} (updated at ${updateUser.updatedAt} by ${updateUser.updatedBy.fullName})`, + comment: `${formatComment.comment} (updated at ${comments[0].updated_at} by ${comments[0].updated_by.full_name})`, }, ]); }); @@ -214,19 +235,19 @@ describe('api/cases/configure/utils', () => { expect(res).toEqual([ { ...formatComment, - comment: `${formatComment.comment} (added at ${comments[0].createdAt} by ${comments[0].createdBy.fullName})`, + comment: `${formatComment.comment} (added at ${comments[0].created_at} by ${comments[0].created_by.full_name})`, }, ]); }); test('transform comments without fullname', () => { - const comments = [{ ...commentObj, createdBy: { username: commentObj.createdBy.username } }]; - // @ts-ignore testing no fullName + const comments = [{ ...commentObj, createdBy: { username: commentObj.created_by.username } }]; + // @ts-ignore testing no full_name const res = transformComments(comments, ['informationAdded']); expect(res).toEqual([ { ...formatComment, - comment: `${formatComment.comment} (added at ${comments[0].createdAt} by ${comments[0].createdBy.username})`, + comment: `${formatComment.comment} (added at ${comments[0].created_at} by ${comments[0].created_by.username})`, }, ]); }); @@ -235,15 +256,15 @@ describe('api/cases/configure/utils', () => { const comments = [ { ...commentObj, - updatedAt: '2020-04-13T08:34:53.450Z', - updatedBy: { fullName: 'Elastic2', username: 'elastic' }, + updated_at: '2020-04-13T08:34:53.450Z', + updated_by: { full_name: 'Elastic2', username: 'elastic', email: 'elastic@elastic.co' }, }, ]; const res = transformComments(comments, ['informationAdded']); expect(res).toEqual([ { ...formatComment, - comment: `${formatComment.comment} (added at ${comments[0].updatedAt} by ${comments[0].updatedBy.fullName})`, + comment: `${formatComment.comment} (added at ${comments[0].updated_at} by ${comments[0].updated_by.full_name})`, }, ]); }); @@ -252,19 +273,20 @@ describe('api/cases/configure/utils', () => { const comments = [ { ...commentObj, - updatedAt: '2020-04-13T08:34:53.450Z', - updatedBy: { fullName: '', username: 'elastic2' }, + updated_at: '2020-04-13T08:34:53.450Z', + updated_by: { full_name: '', username: 'elastic2', email: 'elastic@elastic.co' }, }, ]; const res = transformComments(comments, ['informationAdded']); expect(res).toEqual([ { ...formatComment, - comment: `${formatComment.comment} (added at ${comments[0].updatedAt} by ${comments[0].updatedBy.username})`, + comment: `${formatComment.comment} (added at ${comments[0].updated_at} by ${comments[0].updated_by.username})`, }, ]); }); }); + describe('transformers', () => { const { informationCreated, informationUpdated, informationAdded, append } = transformers; describe('informationCreated', () => { @@ -389,142 +411,291 @@ describe('api/cases/configure/utils', () => { }); }); }); - describe('mapIncident', () => { + + describe('createIncident', () => { let actionsMock = actionsClientMock.create(); - it('maps an external incident', async () => { - const res = await mapIncident( - actionsMock, - '123', - ConnectorTypes.servicenow, - mappingsMock[ConnectorTypes.servicenow], - serviceNowParams - ); + const theCase = { + ...flattenCaseSavedObject({ + savedObject: mockCases[0], + }), + comments: [commentObj], + totalComments: 1, + }; + + const connector = { + id: '456', + actionTypeId: '.jira', + name: 'Connector without isCaseOwned', + config: { + apiUrl: 'https://elastic.jira.com', + }, + isPreconfigured: false, + }; + + it('creates an external incident', async () => { + const res = await createIncident({ + actionsClient: actionsMock, + theCase, + userActions: [], + connector, + mappings, + alerts: [], + }); + expect(res).toEqual({ incident: { - description: 'a description (created at 2020-03-13T08:34:53.450Z by Elastic User)', + priority: null, + labels: ['defacement'], + issueType: null, + parent: null, + short_description: + 'Super Bad Security Issue (created at 2019-11-25T21:54:48.952Z by elastic)', + description: + 'This is a brand new case of a bad meanie defacing data (created at 2019-11-25T21:54:48.952Z by elastic)', externalId: null, - impact: '3', - severity: '1', - short_description: 'a title (created at 2020-03-13T08:34:53.450Z by Elastic User)', - urgency: '2', }, - comments: [ + comments: [], + }); + }); + + it('it creates comments correctly', async () => { + const res = await createIncident({ + actionsClient: actionsMock, + theCase: { + ...theCase, + comments: [{ ...commentObj, id: 'comment-user-1' }], + }, + userActions, + connector, + mappings, + alerts: [], + }); + + expect(res.comments).toEqual([ + { + comment: + 'Wow, good luck catching that bad meanie! (added at 2019-11-25T21:55:00.177Z by elastic)', + commentId: 'comment-user-1', + }, + ]); + }); + + it('it does NOT creates comments when mapping is nothing', async () => { + const res = await createIncident({ + actionsClient: actionsMock, + theCase: { + ...theCase, + comments: [{ ...commentObj, id: 'comment-user-1' }], + }, + userActions, + connector, + mappings: [ + mappings[0], + mappings[1], { - comment: 'first comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', - commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', + source: 'comments', + target: 'comments', + action_type: 'nothing', }, ], + alerts: [], }); + + expect(res.comments).toEqual([]); }); - it('throws error if invalid service', async () => { - await mapIncident( - actionsMock, - '123', - 'invalid', - mappingsMock[ConnectorTypes.servicenow], - serviceNowParams - ).catch((e) => { - expect(e).not.toBeNull(); - expect(e).toEqual(new Error(`Invalid service`)); + + it('it creates comments of type alert correctly', async () => { + const res = await createIncident({ + actionsClient: actionsMock, + theCase: { + ...theCase, + comments: [ + { ...commentObj, id: 'comment-user-1' }, + { ...commentAlert, id: 'comment-alert-1' }, + { ...commentAlert, id: 'comment-alert-2' }, + ], + }, + // Remove second push + userActions: userActions.filter((item, index) => index !== 4), + connector, + mappings: [ + ...mappings, + { + source: 'comments', + target: 'comments', + action_type: 'nothing', + }, + ], + alerts: [], }); + + expect(res.comments).toEqual([ + { + comment: + 'Wow, good luck catching that bad meanie! (added at 2019-11-25T21:55:00.177Z by elastic)', + commentId: 'comment-user-1', + }, + { + comment: + 'Alert with id alert-id-1 added to case (added at 2019-11-25T21:55:00.177Z by elastic)', + commentId: 'comment-alert-1', + }, + { + comment: + 'Alert with id alert-id-1 added to case (added at 2019-11-25T21:55:00.177Z by elastic)', + commentId: 'comment-alert-2', + }, + ]); }); + it('updates an existing incident', async () => { const existingIncidentData = { - description: 'fun description', - impact: '3', - severity: '3', + priority: null, + issueType: null, + parent: null, short_description: 'fun title', - urgency: '3', + description: 'fun description', }; + const execute = jest.fn().mockReturnValue(existingIncidentData); actionsMock = { ...actionsMock, execute }; - const res = await mapIncident( - actionsMock, - '123', - ConnectorTypes.servicenow, - mappingsMock[ConnectorTypes.servicenow], - { ...serviceNowParams, externalId: '123' } - ); + + const res = await createIncident({ + actionsClient: actionsMock, + theCase, + userActions, + connector, + mappings, + alerts: [], + }); + expect(res).toEqual({ incident: { - description: 'a description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - externalId: '123', - impact: '3', - severity: '1', - short_description: 'a title (updated at 2020-03-13T08:34:53.450Z by Elastic User)', - urgency: '2', + priority: null, + labels: ['defacement'], + issueType: null, + parent: null, + description: + 'fun description \r\nThis is a brand new case of a bad meanie defacing data (updated at 2019-11-25T21:54:48.952Z by elastic)', + externalId: 'external-id', + short_description: + 'Super Bad Security Issue (updated at 2019-11-25T21:54:48.952Z by elastic)', }, - comments: [ - { - comment: 'first comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', - commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - }, - ], + comments: [], }); }); + it('throws error when existing incident throws', async () => { + expect.assertions(2); const execute = jest.fn().mockImplementation(() => { throw new Error('exception'); }); + actionsMock = { ...actionsMock, execute }; - await mapIncident( - actionsMock, - '123', - ConnectorTypes.servicenow, - mappingsMock[ConnectorTypes.servicenow], - { ...serviceNowParams, externalId: '123' } - ).catch((e) => { + createIncident({ + actionsClient: actionsMock, + theCase, + userActions, + connector, + mappings, + alerts: [], + }).catch((e) => { expect(e).not.toBeNull(); expect(e).toEqual( new Error( - `Retrieving Incident by id 123 from ServiceNow failed with exception: Error: exception` + `Retrieving Incident by id external-id from .jira failed with exception: Error: exception` ) ); }); }); - }); - const connectors = [ - { - name: ConnectorTypes.jira, - result: { - incident: { - issueType: '10003', - parent: '5002', - priority: 'Highest', - }, - thirdPartyName: 'Jira', - }, - }, - { - name: ConnectorTypes.resilient, - result: { - incident: { - incidentTypes: ['10003'], - severityCode: '1', - }, - thirdPartyName: 'Resilient', - }, - }, - { - name: ConnectorTypes.servicenow, - result: { - incident: { - impact: '3', - severity: '1', - urgency: '2', - }, - thirdPartyName: 'ServiceNow', - }, - }, - ]; - describe('serviceFormatter', () => { - connectors.forEach((c) => - it(`formats ${c.name}`, () => { - const caseParams = params[c.name] as ServiceConnectorCaseParams; - const res = serviceFormatter(c.name, caseParams); - expect(res).toEqual(c.result); - }) - ); + it('throws error if connector is not supported', async () => { + expect.assertions(2); + createIncident({ + actionsClient: actionsMock, + theCase, + userActions, + connector: { ...connector, actionTypeId: 'not-supported' }, + mappings, + alerts: [], + }).catch((e) => { + expect(e).not.toBeNull(); + expect(e).toEqual(new Error('Invalid external service')); + }); + }); + + describe('getLatestPushInfo', () => { + it('it returns the latest push information correctly', async () => { + const res = getLatestPushInfo('456', userActions); + expect(res).toEqual({ + index: 4, + pushedInfo: { + connector_id: '456', + connector_name: 'ServiceNow SN', + external_id: 'external-id', + external_title: 'SIR0010037', + external_url: + 'https://dev92273.service-now.com/nav_to.do?uri=sn_si_incident.do?sys_id=external-id', + pushed_at: '2021-02-03T17:45:29.400Z', + pushed_by: { + email: 'elastic@elastic.co', + full_name: 'Elastic', + username: 'elastic', + }, + }, + }); + }); + + it('it returns null when there are not actions', async () => { + const res = getLatestPushInfo('456', []); + expect(res).toBe(null); + }); + + it('it returns null when there are no push user action', async () => { + const res = getLatestPushInfo('456', [userActions[0]]); + expect(res).toBe(null); + }); + + it('it returns the correct push information when with multiple push on different connectors', async () => { + const res = getLatestPushInfo('456', [ + ...userActions.slice(0, 3), + { + action_field: ['pushed'], + action: 'push-to-service', + action_at: '2021-02-03T17:45:29.400Z', + action_by: { + email: 'elastic@elastic.co', + full_name: 'Elastic', + username: 'elastic', + }, + new_value: + // The connector id is 123 + '{"pushed_at":"2021-02-03T17:45:29.400Z","pushed_by":{"username":"elastic","full_name":"Elastic","email":"elastic@elastic.co"},"connector_id":"123","connector_name":"ServiceNow SN","external_id":"external-id","external_title":"SIR0010037","external_url":"https://dev92273.service-now.com/nav_to.do?uri=sn_si_incident.do?sys_id=external-id"}', + old_value: null, + action_id: '9b91d8f0-6647-11eb-a291-51bf6b175a53', + case_id: 'fcdedd20-6646-11eb-a291-51bf6b175a53', + comment_id: null, + }, + ]); + + expect(res).toEqual({ + index: 1, + pushedInfo: { + connector_id: '456', + connector_name: 'ServiceNow SN', + external_id: 'external-id', + external_title: 'SIR0010037', + external_url: + 'https://dev92273.service-now.com/nav_to.do?uri=sn_si_incident.do?sys_id=external-id', + pushed_at: '2021-02-03T17:41:26.108Z', + pushed_by: { + email: 'elastic@elastic.co', + full_name: 'Elastic', + username: 'elastic', + }, + }, + }); + }); + }); }); }); diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/utils.ts b/x-pack/plugins/case/server/client/cases/utils.ts similarity index 50% rename from x-pack/plugins/case/server/routes/api/cases/configure/utils.ts rename to x-pack/plugins/case/server/client/cases/utils.ts index 01a1a580bd78f..6974fd4ffa288 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/utils.ts +++ b/x-pack/plugins/case/server/client/cases/utils.ts @@ -8,46 +8,118 @@ import { i18n } from '@kbn/i18n'; import { flow } from 'lodash'; import { - ServiceConnectorCaseParams, - ServiceConnectorCommentParams, + ActionConnector, + CaseResponse, + CaseFullExternalService, + CaseUserActionsResponse, + CommentResponse, + CommentResponseAlertsType, + CommentType, ConnectorMappingsAttributes, ConnectorTypes, + CommentAttributes, + CommentRequestUserType, + CommentRequestAlertType, +} from '../../../common/api'; +import { ActionsClient } from '../../../../actions/server'; +import { externalServiceFormatters, FormatterConnectorTypes } from '../../connectors'; +import { CaseClientGetAlertsResponse } from '../../client/alerts/types'; +import { + BasicParams, EntityInformation, ExternalServiceParams, + ExternalServiceComment, Incident, - JiraPushToServiceApiParams, MapIncident, PipedField, PrepareFieldsForTransformArgs, PushToServiceApiParams, - ResilientPushToServiceApiParams, - ServiceNowITSMPushToServiceApiParams, - SimpleComment, Transformer, TransformerArgs, TransformFieldsArgs, -} from '../../../../../common/api'; -import { ActionsClient } from '../../../../../../actions/server'; -export const mapIncident = async ( - actionsClient: ActionsClient, +} from './types'; + +export const getLatestPushInfo = ( connectorId: string, - connectorType: string, - mappings: ConnectorMappingsAttributes[], - params: ServiceConnectorCaseParams -): Promise => { - const { comments: caseComments, externalId } = params; + userActions: CaseUserActionsResponse +): { index: number; pushedInfo: CaseFullExternalService } | null => { + for (const [index, action] of [...userActions].reverse().entries()) { + if (action.action === 'push-to-service' && action.new_value) + try { + const pushedInfo = JSON.parse(action.new_value); + if (pushedInfo.connector_id === connectorId) { + // We returned the index of the element in the userActions array. + // As we traverse the userActions in reverse we need to calculate the index of a normal traversal + return { index: userActions.length - index - 1, pushedInfo }; + } + } catch (e) { + // Silence JSON parse errors + } + } + + return null; +}; + +const isConnectorSupported = (connectorId: string): connectorId is FormatterConnectorTypes => + Object.values(ConnectorTypes).includes(connectorId as ConnectorTypes); + +const getCommentContent = (comment: CommentResponse): string => { + if (comment.type === CommentType.user) { + return comment.comment; + } else if (comment.type === CommentType.alert) { + return `Alert with id ${comment.alertId} added to case`; + } + + return ''; +}; + +interface CreateIncidentArgs { + actionsClient: ActionsClient; + theCase: CaseResponse; + userActions: CaseUserActionsResponse; + connector: ActionConnector; + mappings: ConnectorMappingsAttributes[]; + alerts: CaseClientGetAlertsResponse; +} + +export const createIncident = async ({ + actionsClient, + theCase, + userActions, + connector, + mappings, + alerts, +}: CreateIncidentArgs): Promise => { + const { + comments: caseComments, + title, + description, + created_at: createdAt, + created_by: createdBy, + updated_at: updatedAt, + updated_by: updatedBy, + } = theCase; + + if (!isConnectorSupported(connector.actionTypeId)) { + throw new Error('Invalid external service'); + } + + const params = { title, description, createdAt, createdBy, updatedAt, updatedBy }; + const latestPushInfo = getLatestPushInfo(connector.id, userActions); + const externalId = latestPushInfo?.pushedInfo?.external_id ?? null; const defaultPipes = externalId ? ['informationUpdated'] : ['informationCreated']; let currentIncident: ExternalServiceParams | undefined; - const service = serviceFormatter(connectorType, params); - if (service == null) { - throw new Error(`Invalid service`); - } - const thirdPartyName = service.thirdPartyName; - let incident: Partial = service.incident; + + const externalServiceFields = externalServiceFormatters[connector.actionTypeId].format( + theCase, + alerts + ); + let incident: Partial = { ...externalServiceFields }; + if (externalId) { try { currentIncident = ((await actionsClient.execute({ - actionId: connectorId, + actionId: connector.id, params: { subAction: 'getIncident', subActionParams: { externalId }, @@ -55,80 +127,56 @@ export const mapIncident = async ( })) as unknown) as ExternalServiceParams | undefined; } catch (ex) { throw new Error( - `Retrieving Incident by id ${externalId} from ${thirdPartyName} failed with exception: ${ex}` + `Retrieving Incident by id ${externalId} from ${connector.actionTypeId} failed with exception: ${ex}` ); } } + const fields = prepareFieldsForTransformation({ defaultPipes, mappings, params, }); - const transformedFields = transformFields< - ServiceConnectorCaseParams, - ExternalServiceParams, - Incident - >({ + + const transformedFields = transformFields({ params, fields, currentIncident, }); + incident = { ...incident, ...transformedFields, externalId }; - let comments: SimpleComment[] = []; - if (caseComments && Array.isArray(caseComments) && caseComments.length > 0) { + + const commentsIdsToBeUpdated = new Set( + userActions + .slice(latestPushInfo?.index ?? 0) + .filter( + (action, index) => + Array.isArray(action.action_field) && action.action_field[0] === 'comment' + ) + .map((action) => action.comment_id) + ); + const commentsToBeUpdated = caseComments?.filter((comment) => + commentsIdsToBeUpdated.has(comment.id) + ); + + let comments: ExternalServiceComment[] = []; + if (commentsToBeUpdated && Array.isArray(commentsToBeUpdated) && commentsToBeUpdated.length > 0) { const commentsMapping = mappings.find((m) => m.source === 'comments'); if (commentsMapping?.action_type !== 'nothing') { - comments = transformComments(caseComments, ['informationAdded']); + comments = transformComments(commentsToBeUpdated, ['informationAdded']); } } return { incident, comments }; }; -export const serviceFormatter = ( - connectorType: string, - params: unknown -): { thirdPartyName: string; incident: Partial } | null => { - switch (connectorType) { - case ConnectorTypes.jira: - const { - priority, - labels, - issueType, - parent, - } = params as JiraPushToServiceApiParams['incident']; - return { - incident: { priority, labels, issueType, parent }, - thirdPartyName: 'Jira', - }; - case ConnectorTypes.resilient: - const { incidentTypes, severityCode } = params as ResilientPushToServiceApiParams['incident']; - return { - incident: { incidentTypes, severityCode }, - thirdPartyName: 'Resilient', - }; - case ConnectorTypes.servicenow: - const { - severity, - urgency, - impact, - } = params as ServiceNowITSMPushToServiceApiParams['incident']; - return { - incident: { severity, urgency, impact }, - thirdPartyName: 'ServiceNow', - }; - default: - return null; - } -}; - export const getEntity = (entity: EntityInformation): string => (entity.updatedBy != null - ? entity.updatedBy.fullName - ? entity.updatedBy.fullName + ? entity.updatedBy.full_name + ? entity.updatedBy.full_name : entity.updatedBy.username : entity.createdBy != null - ? entity.createdBy.fullName - ? entity.createdBy.fullName + ? entity.createdBy.full_name + ? entity.createdBy.full_name : entity.createdBy.username : '') ?? ''; @@ -160,6 +208,7 @@ export const FIELD_INFORMATION = ( }); } }; + export const transformers: Record = { informationCreated: ({ value, date, user, ...rest }: TransformerArgs): TransformerArgs => ({ value: `${value} ${FIELD_INFORMATION('create', date, user)}`, @@ -178,6 +227,7 @@ export const transformers: Record = { ...rest, }), }; + export const prepareFieldsForTransformation = ({ defaultPipes, mappings, @@ -226,14 +276,46 @@ export const transformFields = < }; export const transformComments = ( - comments: ServiceConnectorCommentParams[], + comments: CaseResponse['comments'] = [], pipes: string[] -): SimpleComment[] => +): ExternalServiceComment[] => comments.map((c) => ({ comment: flow(...pipes.map((p) => transformers[p]))({ - value: c.comment, - date: c.updatedAt ?? c.createdAt, - user: getEntity(c), + value: getCommentContent(c), + date: c.updated_at ?? c.created_at, + user: getEntity({ + createdAt: c.created_at, + createdBy: c.created_by, + updatedAt: c.updated_at, + updatedBy: c.updated_by, + }), }).value, - commentId: c.commentId, + commentId: c.id, })); + +export const isCommentAlertType = ( + comment: CommentResponse +): comment is CommentResponseAlertsType => comment.type === CommentType.alert; + +export const getCommentContextFromAttributes = ( + attributes: CommentAttributes +): CommentRequestUserType | CommentRequestAlertType => { + switch (attributes.type) { + case CommentType.user: + return { + type: CommentType.user, + comment: attributes.comment, + }; + case CommentType.alert: + return { + type: CommentType.alert, + alertId: attributes.alertId, + index: attributes.index, + }; + default: + return { + type: CommentType.user, + comment: '', + }; + } +}; diff --git a/x-pack/plugins/case/server/client/configure/mock.ts b/x-pack/plugins/case/server/client/configure/mock.ts index 46df0a7ac6756..4d0c384e23e27 100644 --- a/x-pack/plugins/case/server/client/configure/mock.ts +++ b/x-pack/plugins/case/server/client/configure/mock.ts @@ -70,7 +70,7 @@ export const mappings: TestMappings = { action_type: 'append', }, ], - [ConnectorTypes.servicenow]: [ + [ConnectorTypes.serviceNowITSM]: [ { source: 'title', target: 'short_description', @@ -611,7 +611,7 @@ export const formatFieldsTestData: FormatFieldsTestData[] = [ { id: 'upon_reject', name: 'Upon reject', required: false, type: 'text' }, ], fields: serviceNowFields, - type: ConnectorTypes.servicenow, + type: ConnectorTypes.serviceNowITSM, }, ]; export const mockGetFieldsResponse = { diff --git a/x-pack/plugins/case/server/client/configure/utils.ts b/x-pack/plugins/case/server/client/configure/utils.ts index 2fc9e3d17801c..7e91c2ae5a4d7 100644 --- a/x-pack/plugins/case/server/client/configure/utils.ts +++ b/x-pack/plugins/case/server/client/configure/utils.ts @@ -70,7 +70,9 @@ export const formatFields = (theData: unknown, theType: string): ConnectorField[ return normalizeJiraFields(theData as JiraGetFieldsResponse); case ConnectorTypes.resilient: return normalizeResilientFields(theData as ResilientGetFieldsResponse); - case ConnectorTypes.servicenow: + case ConnectorTypes.serviceNowITSM: + return normalizeServiceNowFields(theData as ServiceNowGetFieldsResponse); + case ConnectorTypes.serviceNowSIR: return normalizeServiceNowFields(theData as ServiceNowGetFieldsResponse); default: return []; @@ -97,10 +99,14 @@ const getPreferredFields = (theType: string) => { } else if (theType === ConnectorTypes.resilient) { title = 'name'; description = 'description'; - } else if (theType === ConnectorTypes.servicenow) { + } else if ( + theType === ConnectorTypes.serviceNowITSM || + theType === ConnectorTypes.serviceNowSIR + ) { title = 'short_description'; description = 'description'; } + return { title, description }; }; diff --git a/x-pack/plugins/case/server/client/index.test.ts b/x-pack/plugins/case/server/client/index.test.ts index 095dc5102b720..4daa4d1c0bd8b 100644 --- a/x-pack/plugins/case/server/client/index.test.ts +++ b/x-pack/plugins/case/server/client/index.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { KibanaRequest } from 'kibana/server'; +import { KibanaRequest, kibanaResponseFactory } from '../../../../../src/core/server'; import { savedObjectsClientMock } from '../../../../../src/core/server/mocks'; import { createCaseClient } from '.'; import { @@ -17,29 +17,48 @@ import { } from '../services/mocks'; import { create } from './cases/create'; +import { get } from './cases/get'; import { update } from './cases/update'; +import { push } from './cases/push'; import { addComment } from './comments/add'; +import { getFields } from './configure/get_fields'; +import { getMappings } from './configure/get_mappings'; import { updateAlertsStatus } from './alerts/update_status'; +import { get as getUserActions } from './user_actions/get'; +import { get as getAlerts } from './alerts/get'; import type { CasesRequestHandlerContext } from '../types'; jest.mock('./cases/create'); jest.mock('./cases/update'); +jest.mock('./cases/get'); +jest.mock('./cases/push'); jest.mock('./comments/add'); jest.mock('./alerts/update_status'); +jest.mock('./alerts/get'); +jest.mock('./user_actions/get'); +jest.mock('./configure/get_fields'); +jest.mock('./configure/get_mappings'); const caseConfigureService = createConfigureServiceMock(); const alertsService = createAlertServiceMock(); const caseService = createCaseServiceMock(); const connectorMappingsService = connectorMappingsServiceMock(); const request = {} as KibanaRequest; +const response = kibanaResponseFactory; const savedObjectsClient = savedObjectsClientMock.create(); const userActionService = createUserActionServiceMock(); const context = {} as CasesRequestHandlerContext; const createMock = create as jest.Mock; +const getMock = get as jest.Mock; const updateMock = update as jest.Mock; +const pushMock = push as jest.Mock; const addCommentMock = addComment as jest.Mock; const updateAlertsStatusMock = updateAlertsStatus as jest.Mock; +const getAlertsStatusMock = getAlerts as jest.Mock; +const getFieldsMock = getFields as jest.Mock; +const getMappingsMock = getMappings as jest.Mock; +const getUserActionsMock = getUserActions as jest.Mock; describe('createCaseClient()', () => { test('it creates the client correctly', async () => { @@ -50,49 +69,34 @@ describe('createCaseClient()', () => { connectorMappingsService, context, request, + response, savedObjectsClient, userActionService, }); - expect(createMock).toHaveBeenCalledWith({ - alertsService, - caseConfigureService, - caseService, - connectorMappingsService, - request, - savedObjectsClient, - userActionService, - }); - - expect(updateMock).toHaveBeenCalledWith({ - alertsService, - caseConfigureService, - caseService, - connectorMappingsService, - request, - savedObjectsClient, - userActionService, - }); - - expect(addCommentMock).toHaveBeenCalledWith({ - alertsService, - caseConfigureService, - caseService, - connectorMappingsService, - request, - savedObjectsClient, - userActionService, - }); - - expect(updateAlertsStatusMock).toHaveBeenCalledWith({ - alertsService, - caseConfigureService, - caseService, - connectorMappingsService, - context, - request, - savedObjectsClient, - userActionService, - }); + [ + createMock, + getMock, + updateMock, + pushMock, + addCommentMock, + updateAlertsStatusMock, + getAlertsStatusMock, + getFieldsMock, + getMappingsMock, + getUserActionsMock, + ].forEach((method) => + expect(method).toHaveBeenCalledWith({ + caseConfigureService, + caseService, + connectorMappingsService, + request, + response, + savedObjectsClient, + userActionService, + alertsService, + context, + }) + ); }); }); diff --git a/x-pack/plugins/case/server/client/index.ts b/x-pack/plugins/case/server/client/index.ts index 1b9d3ce7ecb08..e15b9fc766562 100644 --- a/x-pack/plugins/case/server/client/index.ts +++ b/x-pack/plugins/case/server/client/index.ts @@ -5,73 +5,41 @@ * 2.0. */ -import { CaseClientFactoryArguments, CaseClient } from './types'; +import { + CaseClientFactoryArguments, + CaseClient, + CaseClientFactoryMethods, + CaseClientMethods, +} from './types'; import { create } from './cases/create'; +import { get } from './cases/get'; import { update } from './cases/update'; +import { push } from './cases/push'; import { addComment } from './comments/add'; import { getFields } from './configure/get_fields'; import { getMappings } from './configure/get_mappings'; import { updateAlertsStatus } from './alerts/update_status'; +import { get as getUserActions } from './user_actions/get'; +import { get as getAlerts } from './alerts/get'; export { CaseClient } from './types'; -export const createCaseClient = ({ - caseConfigureService, - caseService, - connectorMappingsService, - request, - savedObjectsClient, - userActionService, - alertsService, - context, -}: CaseClientFactoryArguments): CaseClient => { - return { - create: create({ - alertsService, - caseConfigureService, - caseService, - connectorMappingsService, - request, - savedObjectsClient, - userActionService, - }), - update: update({ - alertsService, - caseConfigureService, - caseService, - connectorMappingsService, - request, - savedObjectsClient, - userActionService, - }), - addComment: addComment({ - alertsService, - caseConfigureService, - caseService, - connectorMappingsService, - request, - savedObjectsClient, - userActionService, - }), - getFields: getFields(), - getMappings: getMappings({ - alertsService, - caseConfigureService, - caseService, - connectorMappingsService, - request, - savedObjectsClient, - userActionService, - }), - updateAlertsStatus: updateAlertsStatus({ - alertsService, - caseConfigureService, - caseService, - connectorMappingsService, - context, - request, - savedObjectsClient, - userActionService, - }), +export const createCaseClient = (args: CaseClientFactoryArguments): CaseClient => { + const methods: CaseClientFactoryMethods = { + create, + get, + update, + push, + addComment, + getAlerts, + getFields, + getMappings, + getUserActions, + updateAlertsStatus, }; + + return (Object.keys(methods) as CaseClientMethods[]).reduce((client, method) => { + client[method] = methods[method](args); + return client; + }, {} as CaseClient); }; diff --git a/x-pack/plugins/case/server/client/mocks.ts b/x-pack/plugins/case/server/client/mocks.ts index 0d7f3972e58e7..b2a07e36b3aed 100644 --- a/x-pack/plugins/case/server/client/mocks.ts +++ b/x-pack/plugins/case/server/client/mocks.ts @@ -6,9 +6,9 @@ */ import { omit } from 'lodash/fp'; -import { KibanaRequest } from 'kibana/server'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { KibanaRequest, kibanaResponseFactory } from '../../../../../src/core/server/http'; import { loggingSystemMock } from '../../../../../src/core/server/mocks'; -import { actionsClientMock } from '../../../actions/server/mocks'; import { AlertServiceContract, CaseConfigureService, @@ -17,17 +17,20 @@ import { ConnectorMappingsService, } from '../services'; import { CaseClient } from './types'; -import { authenticationMock } from '../routes/api/__fixtures__'; +import { authenticationMock, createActionsClient } from '../routes/api/__fixtures__'; import { createCaseClient } from '.'; -import { getActions } from '../routes/api/__mocks__/request_responses'; import type { CasesRequestHandlerContext } from '../types'; export type CaseClientMock = jest.Mocked; export const createCaseClientMock = (): CaseClientMock => ({ addComment: jest.fn(), create: jest.fn(), + get: jest.fn(), + push: jest.fn(), + getAlerts: jest.fn(), getFields: jest.fn(), getMappings: jest.fn(), + getUserActions: jest.fn(), update: jest.fn(), updateAlertsStatus: jest.fn(), }); @@ -47,10 +50,10 @@ export const createCaseClientWithMockSavedObjectsClient = async ({ alertsService: jest.Mocked; }; }> => { - const actionsMock = actionsClientMock.create(); - actionsMock.getAll.mockImplementation(() => Promise.resolve(getActions())); + const actionsMock = createActionsClient(); const log = loggingSystemMock.create().get('case'); const request = {} as KibanaRequest; + const response = kibanaResponseFactory; const caseServicePlugin = new CaseService(log); const caseConfigureServicePlugin = new CaseConfigureService(log); @@ -63,11 +66,15 @@ export const createCaseClientWithMockSavedObjectsClient = async ({ const connectorMappingsService = await connectorMappingsServicePlugin.setup(); const userActionService = { - postUserActions: jest.fn(), getUserActions: jest.fn(), + postUserActions: jest.fn(), }; - const alertsService = { initialize: jest.fn(), updateAlertsStatus: jest.fn() }; + const alertsService = { + initialize: jest.fn(), + updateAlertsStatus: jest.fn(), + getAlerts: jest.fn(), + }; const context = { core: { @@ -89,6 +96,7 @@ export const createCaseClientWithMockSavedObjectsClient = async ({ const caseClient = createCaseClient({ savedObjectsClient, request, + response, caseService, caseConfigureService, connectorMappingsService, diff --git a/x-pack/plugins/case/server/client/types.ts b/x-pack/plugins/case/server/client/types.ts index a3466e26294f8..8778aa46a2d24 100644 --- a/x-pack/plugins/case/server/client/types.ts +++ b/x-pack/plugins/case/server/client/types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { KibanaRequest, SavedObjectsClientContract } from 'kibana/server'; +import { KibanaRequest, KibanaResponseFactory, SavedObjectsClientContract } from 'kibana/server'; import { ActionsClient } from '../../../actions/server'; import { CasePostRequest, @@ -16,6 +16,7 @@ import { CommentRequest, ConnectorMappingsAttributes, GetFieldsResponse, + CaseUserActionsResponse, } from '../../common/api'; import { CaseConfigureServiceSetup, @@ -25,6 +26,7 @@ import { } from '../services'; import { ConnectorMappingsServiceSetup } from '../services/connector_mappings'; import type { CasesRequestHandlerContext } from '../types'; +import { CaseClientGetAlertsResponse } from './alerts/types'; export interface CaseClientCreate { theCase: CasePostRequest; @@ -35,6 +37,18 @@ export interface CaseClientUpdate { cases: CasesPatchRequest; } +export interface CaseClientGet { + id: string; + includeComments?: boolean; +} + +export interface CaseClientPush { + actionsClient: ActionsClient; + caseClient: CaseClient; + caseId: string; + connectorId: string; +} + export interface CaseClientAddComment { caseClient: CaseClient; caseId: string; @@ -46,11 +60,27 @@ export interface CaseClientUpdateAlertsStatus { status: CaseStatuses; } +export interface CaseClientGetAlerts { + ids: string[]; +} + +export interface CaseClientGetUserActions { + caseId: string; +} + +export interface MappingsClient { + actionsClient: ActionsClient; + caseClient: CaseClient; + connectorId: string; + connectorType: string; +} + export interface CaseClientFactoryArguments { caseConfigureService: CaseConfigureServiceSetup; caseService: CaseServiceSetup; connectorMappingsService: ConnectorMappingsServiceSetup; request: KibanaRequest; + response: KibanaResponseFactory; savedObjectsClient: SavedObjectsClientContract; userActionService: CaseUserActionServiceSetup; alertsService: AlertServiceContract; @@ -65,15 +95,22 @@ export interface ConfigureFields { export interface CaseClient { addComment: (args: CaseClientAddComment) => Promise; create: (args: CaseClientCreate) => Promise; + get: (args: CaseClientGet) => Promise; + getAlerts: (args: CaseClientGetAlerts) => Promise; getFields: (args: ConfigureFields) => Promise; getMappings: (args: MappingsClient) => Promise; + getUserActions: (args: CaseClientGetUserActions) => Promise; + push: (args: CaseClientPush) => Promise; update: (args: CaseClientUpdate) => Promise; updateAlertsStatus: (args: CaseClientUpdateAlertsStatus) => Promise; } -export interface MappingsClient { - actionsClient: ActionsClient; - caseClient: CaseClient; - connectorId: string; - connectorType: string; -} +export type CaseClientFactoryMethod = ( + factoryArgs: CaseClientFactoryArguments +) => (methodArgs: any) => Promise; + +export type CaseClientMethods = keyof CaseClient; + +export type CaseClientFactoryMethods = { + [K in CaseClientMethods]: CaseClientFactoryMethod; +}; diff --git a/x-pack/plugins/case/server/client/user_actions/get.ts b/x-pack/plugins/case/server/client/user_actions/get.ts new file mode 100644 index 0000000000000..e83a9e3484262 --- /dev/null +++ b/x-pack/plugins/case/server/client/user_actions/get.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CASE_SAVED_OBJECT, CASE_COMMENT_SAVED_OBJECT } from '../../saved_object_types'; +import { CaseUserActionsResponseRt, CaseUserActionsResponse } from '../../../common/api'; +import { CaseClientGetUserActions, CaseClientFactoryArguments } from '../types'; + +export const get = ({ + savedObjectsClient, + userActionService, +}: CaseClientFactoryArguments) => async ({ + caseId, +}: CaseClientGetUserActions): Promise => { + const userActions = await userActionService.getUserActions({ + client: savedObjectsClient, + caseId, + }); + + return CaseUserActionsResponseRt.encode( + userActions.saved_objects.map((ua) => ({ + ...ua.attributes, + action_id: ua.id, + case_id: ua.references.find((r) => r.type === CASE_SAVED_OBJECT)?.id ?? '', + comment_id: ua.references.find((r) => r.type === CASE_COMMENT_SAVED_OBJECT)?.id ?? null, + })) + ); +}; diff --git a/x-pack/plugins/case/server/connectors/case/index.ts b/x-pack/plugins/case/server/connectors/case/index.ts index 01446942c33c6..9907aa5b3cd3a 100644 --- a/x-pack/plugins/case/server/connectors/case/index.ts +++ b/x-pack/plugins/case/server/connectors/case/index.ts @@ -7,7 +7,7 @@ import { curry } from 'lodash'; -import { KibanaRequest } from 'kibana/server'; +import { KibanaRequest, kibanaResponseFactory } from '../../../../../../src/core/server'; import { ActionTypeExecutorResult } from '../../../../actions/common'; import { CasePatchRequest, CasePostRequest } from '../../../common/api'; import { createCaseClient } from '../../client'; @@ -73,6 +73,7 @@ async function executor( const caseClient = createCaseClient({ savedObjectsClient, request: {} as KibanaRequest, + response: kibanaResponseFactory, caseService, caseConfigureService, connectorMappingsService, diff --git a/x-pack/plugins/case/server/connectors/index.ts b/x-pack/plugins/case/server/connectors/index.ts index 100511e271b02..00809d81ca5f2 100644 --- a/x-pack/plugins/case/server/connectors/index.ts +++ b/x-pack/plugins/case/server/connectors/index.ts @@ -5,43 +5,14 @@ * 2.0. */ -import { Logger } from 'kibana/server'; -import { - ActionTypeConfig, - ActionTypeSecrets, - ActionTypeParams, - ActionType, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../actions/server/types'; -import { - CaseServiceSetup, - CaseConfigureServiceSetup, - CaseUserActionServiceSetup, - ConnectorMappingsServiceSetup, - AlertServiceContract, -} from '../services'; - +import { RegisterConnectorsArgs, ExternalServiceFormatterMapper } from './types'; import { getActionType as getCaseConnector } from './case'; +import { serviceNowITSMExternalServiceFormatter } from './servicenow/itsm_formatter'; +import { serviceNowSIRExternalServiceFormatter } from './servicenow/sir_formatter'; +import { jiraExternalServiceFormatter } from './jira/external_service_formatter'; +import { resilientExternalServiceFormatter } from './resilient/external_service_formatter'; -export interface GetActionTypeParams { - logger: Logger; - caseService: CaseServiceSetup; - caseConfigureService: CaseConfigureServiceSetup; - connectorMappingsService: ConnectorMappingsServiceSetup; - userActionService: CaseUserActionServiceSetup; - alertsService: AlertServiceContract; -} - -export interface RegisterConnectorsArgs extends GetActionTypeParams { - actionsRegisterType< - Config extends ActionTypeConfig = ActionTypeConfig, - Secrets extends ActionTypeSecrets = ActionTypeSecrets, - Params extends ActionTypeParams = ActionTypeParams, - ExecutorResultData = void - >( - actionType: ActionType - ): void; -} +export * from './types'; export const registerConnectors = ({ actionsRegisterType, @@ -63,3 +34,10 @@ export const registerConnectors = ({ }) ); }; + +export const externalServiceFormatters: ExternalServiceFormatterMapper = { + '.servicenow': serviceNowITSMExternalServiceFormatter, + '.servicenow-sir': serviceNowSIRExternalServiceFormatter, + '.jira': jiraExternalServiceFormatter, + '.resilient': resilientExternalServiceFormatter, +}; diff --git a/x-pack/plugins/case/server/connectors/jira/external_service_formatter.test.ts b/x-pack/plugins/case/server/connectors/jira/external_service_formatter.test.ts new file mode 100644 index 0000000000000..0bfaf7cdbd9e3 --- /dev/null +++ b/x-pack/plugins/case/server/connectors/jira/external_service_formatter.test.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CaseResponse } from '../../../common/api'; +import { jiraExternalServiceFormatter } from './external_service_formatter'; + +describe('Jira formatter', () => { + const theCase = { + tags: ['tag'], + connector: { fields: { priority: 'High', issueType: 'Task', parent: null } }, + } as CaseResponse; + + it('it formats correctly', async () => { + const res = await jiraExternalServiceFormatter.format(theCase, []); + expect(res).toEqual({ ...theCase.connector.fields, labels: theCase.tags }); + }); + + it('it formats correctly when fields do not exist ', async () => { + const invalidFields = { tags: ['tag'], connector: { fields: null } } as CaseResponse; + const res = await jiraExternalServiceFormatter.format(invalidFields, []); + expect(res).toEqual({ priority: null, issueType: null, parent: null, labels: theCase.tags }); + }); + + it('it replace white spaces with hyphens on tags', async () => { + const res = await jiraExternalServiceFormatter.format( + { ...theCase, tags: ['a tag with spaces'] }, + [] + ); + expect(res).toEqual({ ...theCase.connector.fields, labels: ['a-tag-with-spaces'] }); + }); +}); diff --git a/x-pack/plugins/case/server/connectors/jira/external_service_formatter.ts b/x-pack/plugins/case/server/connectors/jira/external_service_formatter.ts new file mode 100644 index 0000000000000..74376d295fea5 --- /dev/null +++ b/x-pack/plugins/case/server/connectors/jira/external_service_formatter.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { JiraFieldsType, ConnectorJiraTypeFields } from '../../../common/api'; +import { ExternalServiceFormatter } from '../types'; + +interface ExternalServiceParams extends JiraFieldsType { + labels: string[]; +} + +const format: ExternalServiceFormatter['format'] = (theCase) => { + const { priority = null, issueType = null, parent = null } = + (theCase.connector.fields as ConnectorJiraTypeFields['fields']) ?? {}; + return { + priority, + // Jira do not allows empty spaces on labels. We replace white spaces with hyphens + labels: theCase.tags.map((tag) => tag.replace(/\s+/g, '-')), + issueType, + parent, + }; +}; + +export const jiraExternalServiceFormatter: ExternalServiceFormatter = { + format, +}; diff --git a/x-pack/plugins/case/server/connectors/resilient/external_service_formatter.test.ts b/x-pack/plugins/case/server/connectors/resilient/external_service_formatter.test.ts new file mode 100644 index 0000000000000..01280e9692b5e --- /dev/null +++ b/x-pack/plugins/case/server/connectors/resilient/external_service_formatter.test.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CaseResponse } from '../../../common/api'; +import { resilientExternalServiceFormatter } from './external_service_formatter'; + +describe('IBM Resilient formatter', () => { + const theCase = { + connector: { fields: { incidentTypes: ['2'], severityCode: '2' } }, + } as CaseResponse; + + it('it formats correctly', async () => { + const res = await resilientExternalServiceFormatter.format(theCase, []); + expect(res).toEqual({ ...theCase.connector.fields }); + }); + + it('it formats correctly when fields do not exist ', async () => { + const invalidFields = { tags: ['a tag'], connector: { fields: null } } as CaseResponse; + const res = await resilientExternalServiceFormatter.format(invalidFields, []); + expect(res).toEqual({ incidentTypes: null, severityCode: null }); + }); +}); diff --git a/x-pack/plugins/case/server/connectors/resilient/external_service_formatter.ts b/x-pack/plugins/case/server/connectors/resilient/external_service_formatter.ts new file mode 100644 index 0000000000000..76554dce32797 --- /dev/null +++ b/x-pack/plugins/case/server/connectors/resilient/external_service_formatter.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. + */ + +import { ResilientFieldsType, ConnectorResillientTypeFields } from '../../../common/api'; +import { ExternalServiceFormatter } from '../types'; + +const format: ExternalServiceFormatter['format'] = (theCase) => { + const { incidentTypes = null, severityCode = null } = + (theCase.connector.fields as ConnectorResillientTypeFields['fields']) ?? {}; + return { incidentTypes, severityCode }; +}; + +export const resilientExternalServiceFormatter: ExternalServiceFormatter = { + format, +}; diff --git a/x-pack/plugins/case/server/connectors/servicenow/itsm_formatter.ts b/x-pack/plugins/case/server/connectors/servicenow/itsm_formatter.ts new file mode 100644 index 0000000000000..60faa82a9e3fa --- /dev/null +++ b/x-pack/plugins/case/server/connectors/servicenow/itsm_formatter.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. + */ + +import { ServiceNowITSMFieldsType, ConnectorServiceNowITSMTypeFields } from '../../../common/api'; +import { ExternalServiceFormatter } from '../types'; + +const format: ExternalServiceFormatter['format'] = (theCase) => { + const { severity = null, urgency = null, impact = null } = + (theCase.connector.fields as ConnectorServiceNowITSMTypeFields['fields']) ?? {}; + return { severity, urgency, impact }; +}; + +export const serviceNowITSMExternalServiceFormatter: ExternalServiceFormatter = { + format, +}; diff --git a/x-pack/plugins/case/server/connectors/servicenow/itsm_formmater.test.ts b/x-pack/plugins/case/server/connectors/servicenow/itsm_formmater.test.ts new file mode 100644 index 0000000000000..033f184c7e751 --- /dev/null +++ b/x-pack/plugins/case/server/connectors/servicenow/itsm_formmater.test.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CaseResponse } from '../../../common/api'; +import { serviceNowITSMExternalServiceFormatter } from './itsm_formatter'; + +describe('ITSM formatter', () => { + const theCase = { + connector: { fields: { severity: '2', urgency: '2', impact: '2' } }, + } as CaseResponse; + + it('it formats correctly', async () => { + const res = await serviceNowITSMExternalServiceFormatter.format(theCase, []); + expect(res).toEqual(theCase.connector.fields); + }); + + it('it formats correctly when fields do not exist ', async () => { + const invalidFields = { connector: { fields: null } } as CaseResponse; + const res = await serviceNowITSMExternalServiceFormatter.format(invalidFields, []); + expect(res).toEqual({ severity: null, urgency: null, impact: null }); + }); +}); diff --git a/x-pack/plugins/case/server/connectors/servicenow/sir_formatter.test.ts b/x-pack/plugins/case/server/connectors/servicenow/sir_formatter.test.ts new file mode 100644 index 0000000000000..4faca62c6e706 --- /dev/null +++ b/x-pack/plugins/case/server/connectors/servicenow/sir_formatter.test.ts @@ -0,0 +1,164 @@ +/* + * 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 { CaseResponse } from '../../../common/api'; +import { serviceNowSIRExternalServiceFormatter } from './sir_formatter'; + +describe('ITSM formatter', () => { + const theCase = { + connector: { + fields: { + destIp: true, + sourceIp: true, + category: 'Denial of Service', + subcategory: 'Inbound DDos', + malwareHash: true, + malwareUrl: true, + priority: '2 - High', + }, + }, + } as CaseResponse; + + it('it formats correctly without alerts', async () => { + const res = await serviceNowSIRExternalServiceFormatter.format(theCase, []); + expect(res).toEqual({ + dest_ip: null, + source_ip: null, + category: 'Denial of Service', + subcategory: 'Inbound DDos', + malware_hash: null, + malware_url: null, + priority: '2 - High', + }); + }); + + it('it formats correctly when fields do not exist ', async () => { + const invalidFields = { connector: { fields: null } } as CaseResponse; + const res = await serviceNowSIRExternalServiceFormatter.format(invalidFields, []); + expect(res).toEqual({ + dest_ip: null, + source_ip: null, + category: null, + subcategory: null, + malware_hash: null, + malware_url: null, + priority: null, + }); + }); + + it('it formats correctly with alerts', async () => { + const alerts = [ + { + id: 'alert-1', + index: 'index-1', + destination: { ip: '192.168.1.1' }, + source: { ip: '192.168.1.2' }, + file: { + hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' }, + }, + url: { full: 'https://attack.com' }, + }, + { + id: 'alert-2', + index: 'index-2', + destination: { ip: '192.168.1.4' }, + source: { ip: '192.168.1.3' }, + file: { + hash: { sha256: '60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c752' }, + }, + url: { full: 'https://attack.com/api' }, + }, + ]; + const res = await serviceNowSIRExternalServiceFormatter.format(theCase, alerts); + expect(res).toEqual({ + dest_ip: '192.168.1.1,192.168.1.4', + source_ip: '192.168.1.2,192.168.1.3', + category: 'Denial of Service', + subcategory: 'Inbound DDos', + malware_hash: + '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08,60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c752', + malware_url: 'https://attack.com,https://attack.com/api', + priority: '2 - High', + }); + }); + + it('it handles duplicates correctly', async () => { + const alerts = [ + { + id: 'alert-1', + index: 'index-1', + destination: { ip: '192.168.1.1' }, + source: { ip: '192.168.1.2' }, + file: { + hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' }, + }, + url: { full: 'https://attack.com' }, + }, + { + id: 'alert-2', + index: 'index-2', + destination: { ip: '192.168.1.1' }, + source: { ip: '192.168.1.3' }, + file: { + hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' }, + }, + url: { full: 'https://attack.com/api' }, + }, + ]; + const res = await serviceNowSIRExternalServiceFormatter.format(theCase, alerts); + expect(res).toEqual({ + dest_ip: '192.168.1.1', + source_ip: '192.168.1.2,192.168.1.3', + category: 'Denial of Service', + subcategory: 'Inbound DDos', + malware_hash: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08', + malware_url: 'https://attack.com,https://attack.com/api', + priority: '2 - High', + }); + }); + + it('it formats correctly when field is not selected', async () => { + const alerts = [ + { + id: 'alert-1', + index: 'index-1', + destination: { ip: '192.168.1.1' }, + source: { ip: '192.168.1.2' }, + file: { + hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' }, + }, + url: { full: 'https://attack.com' }, + }, + { + id: 'alert-2', + index: 'index-2', + destination: { ip: '192.168.1.1' }, + source: { ip: '192.168.1.3' }, + file: { + hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' }, + }, + url: { full: 'https://attack.com/api' }, + }, + ]; + + const newCase = { + ...theCase, + connector: { fields: { ...theCase.connector.fields, destIp: false, malwareHash: false } }, + } as CaseResponse; + + const res = await serviceNowSIRExternalServiceFormatter.format(newCase, alerts); + expect(res).toEqual({ + dest_ip: null, + source_ip: '192.168.1.2,192.168.1.3', + category: 'Denial of Service', + subcategory: 'Inbound DDos', + malware_hash: null, + malware_url: 'https://attack.com,https://attack.com/api', + priority: '2 - High', + }); + }); +}); diff --git a/x-pack/plugins/case/server/connectors/servicenow/sir_formatter.ts b/x-pack/plugins/case/server/connectors/servicenow/sir_formatter.ts new file mode 100644 index 0000000000000..d2458e6c7ae53 --- /dev/null +++ b/x-pack/plugins/case/server/connectors/servicenow/sir_formatter.ts @@ -0,0 +1,88 @@ +/* + * 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 { get } from 'lodash/fp'; +import { ConnectorServiceNowSIRTypeFields } from '../../../common/api'; +import { ExternalServiceFormatter } from '../types'; +interface ExternalServiceParams { + dest_ip: string | null; + source_ip: string | null; + category: string | null; + subcategory: string | null; + malware_hash: string | null; + malware_url: string | null; + priority: string | null; +} +type SirFieldKey = 'dest_ip' | 'source_ip' | 'malware_hash' | 'malware_url'; +type AlertFieldMappingAndValues = Record< + string, + { alertPath: string; sirFieldKey: SirFieldKey; add: boolean } +>; +const format: ExternalServiceFormatter['format'] = (theCase, alerts) => { + const { + destIp = null, + sourceIp = null, + category = null, + subcategory = null, + malwareHash = null, + malwareUrl = null, + priority = null, + } = (theCase.connector.fields as ConnectorServiceNowSIRTypeFields['fields']) ?? {}; + const alertFieldMapping: AlertFieldMappingAndValues = { + destIp: { alertPath: 'destination.ip', sirFieldKey: 'dest_ip', add: !!destIp }, + sourceIp: { alertPath: 'source.ip', sirFieldKey: 'source_ip', add: !!sourceIp }, + malwareHash: { alertPath: 'file.hash.sha256', sirFieldKey: 'malware_hash', add: !!malwareHash }, + malwareUrl: { alertPath: 'url.full', sirFieldKey: 'malware_url', add: !!malwareUrl }, + }; + + const manageDuplicate: Record> = { + dest_ip: new Set(), + source_ip: new Set(), + malware_hash: new Set(), + malware_url: new Set(), + }; + + let sirFields: Record = { + dest_ip: null, + source_ip: null, + malware_hash: null, + malware_url: null, + }; + + const fieldsToAdd = (Object.keys(alertFieldMapping) as SirFieldKey[]).filter( + (key) => alertFieldMapping[key].add + ); + + if (fieldsToAdd.length > 0) { + sirFields = alerts.reduce>((acc, alert) => { + fieldsToAdd.forEach((alertField) => { + const field = get(alertFieldMapping[alertField].alertPath, alert); + if (field && !manageDuplicate[alertFieldMapping[alertField].sirFieldKey].has(field)) { + manageDuplicate[alertFieldMapping[alertField].sirFieldKey].add(field); + acc = { + ...acc, + [alertFieldMapping[alertField].sirFieldKey]: `${ + acc[alertFieldMapping[alertField].sirFieldKey] != null + ? `${acc[alertFieldMapping[alertField].sirFieldKey]},${field}` + : field + }`, + }; + } + }); + return acc; + }, sirFields); + } + + return { + ...sirFields, + category, + subcategory, + priority, + }; +}; +export const serviceNowSIRExternalServiceFormatter: ExternalServiceFormatter = { + format, +}; diff --git a/x-pack/plugins/case/server/connectors/types.ts b/x-pack/plugins/case/server/connectors/types.ts new file mode 100644 index 0000000000000..8e7eb91ad2dc6 --- /dev/null +++ b/x-pack/plugins/case/server/connectors/types.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 { Logger } from 'kibana/server'; +import { + ActionTypeConfig, + ActionTypeSecrets, + ActionTypeParams, + ActionType, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../actions/server/types'; +import { CaseResponse, ConnectorTypes } from '../../common/api'; +import { CaseClientGetAlertsResponse } from '../client/alerts/types'; +import { + CaseServiceSetup, + CaseConfigureServiceSetup, + CaseUserActionServiceSetup, + ConnectorMappingsServiceSetup, + AlertServiceContract, +} from '../services'; + +export interface GetActionTypeParams { + logger: Logger; + caseService: CaseServiceSetup; + caseConfigureService: CaseConfigureServiceSetup; + connectorMappingsService: ConnectorMappingsServiceSetup; + userActionService: CaseUserActionServiceSetup; + alertsService: AlertServiceContract; +} + +export interface RegisterConnectorsArgs extends GetActionTypeParams { + actionsRegisterType< + Config extends ActionTypeConfig = ActionTypeConfig, + Secrets extends ActionTypeSecrets = ActionTypeSecrets, + Params extends ActionTypeParams = ActionTypeParams, + ExecutorResultData = void + >( + actionType: ActionType + ): void; +} + +export type FormatterConnectorTypes = Exclude; + +export interface ExternalServiceFormatter { + format: (theCase: CaseResponse, alerts: CaseClientGetAlertsResponse) => TExternalServiceParams; +} + +export type ExternalServiceFormatterMapper = { + [x in FormatterConnectorTypes]: ExternalServiceFormatter; +}; diff --git a/x-pack/plugins/case/server/plugin.ts b/x-pack/plugins/case/server/plugin.ts index 8b4fdc73dab44..5d05db165f637 100644 --- a/x-pack/plugins/case/server/plugin.ts +++ b/x-pack/plugins/case/server/plugin.ts @@ -5,7 +5,13 @@ * 2.0. */ -import { IContextProvider, KibanaRequest, Logger, PluginInitializerContext } from 'kibana/server'; +import { + IContextProvider, + KibanaRequest, + KibanaResponseFactory, + Logger, + PluginInitializerContext, +} from 'kibana/server'; import { CoreSetup, CoreStart } from 'src/core/server'; import { SecurityPluginSetup } from '../../security/server'; @@ -123,11 +129,13 @@ export class CasePlugin { const getCaseClientWithRequestAndContext = async ( context: CasesRequestHandlerContext, - request: KibanaRequest + request: KibanaRequest, + response: KibanaResponseFactory ) => { return createCaseClient({ savedObjectsClient: core.savedObjects.getScopedClient(request), request, + response, caseService: this.caseService!, caseConfigureService: this.caseConfigureService!, connectorMappingsService: this.connectorMappingsService!, @@ -161,7 +169,7 @@ export class CasePlugin { userActionService: CaseUserActionServiceSetup; alertsService: AlertServiceContract; }): IContextProvider => { - return async (context, request) => { + return async (context, request, response) => { const [{ savedObjects }] = await core.getStartServices(); return { getCaseClient: () => { @@ -172,8 +180,9 @@ export class CasePlugin { connectorMappingsService, userActionService, alertsService, - request, context, + request, + response, }); }, }; diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/create_mock_so_repository.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/create_mock_so_repository.ts index 8dc970d235fea..18730effdf55a 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/create_mock_so_repository.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/create_mock_so_repository.ts @@ -17,6 +17,7 @@ import { CASE_SAVED_OBJECT, CASE_CONFIGURE_SAVED_OBJECT, CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT, + CASE_USER_ACTION_SAVED_OBJECT, } from '../../../saved_object_types'; export const createMockSavedObjectsRepository = ({ @@ -24,11 +25,13 @@ export const createMockSavedObjectsRepository = ({ caseCommentSavedObject = [], caseConfigureSavedObject = [], caseMappingsSavedObject = [], + caseUserActionsSavedObject = [], }: { caseSavedObject?: any[]; caseCommentSavedObject?: any[]; caseConfigureSavedObject?: any[]; caseMappingsSavedObject?: any[]; + caseUserActionsSavedObject?: any[]; } = {}) => { const mockSavedObjectsClientContract = ({ bulkGet: jest.fn((objects: SavedObjectsBulkGetObject[]) => { @@ -57,6 +60,7 @@ export const createMockSavedObjectsRepository = ({ }), }; }), + bulkCreate: jest.fn(), bulkUpdate: jest.fn((objects: Array>) => { return { saved_objects: objects.map(({ id, type, attributes }) => { @@ -136,6 +140,16 @@ export const createMockSavedObjectsRepository = ({ saved_objects: caseCommentSavedObject, }; } + + if (findArgs.type === CASE_USER_ACTION_SAVED_OBJECT) { + return { + page: 1, + per_page: 5, + total: caseUserActionsSavedObject.length, + saved_objects: caseUserActionsSavedObject, + }; + } + return { page: 1, per_page: 5, diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/index.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/index.ts index 5e2c29f29a3e7..1abd44aec1552 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/index.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/index.ts @@ -10,3 +10,4 @@ export { createMockSavedObjectsRepository } from './create_mock_so_repository'; export { createRouteContext } from './route_contexts'; export { authenticationMock } from './authc_mock'; export { createRoute } from './mock_router'; +export { createActionsClient } from './mock_actions_client'; diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_actions_client.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_actions_client.ts new file mode 100644 index 0000000000000..d153c328cbb91 --- /dev/null +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_actions_client.ts @@ -0,0 +1,34 @@ +/* + * 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 { SavedObjectsErrorHelpers } from 'src/core/server'; +import { actionsClientMock } from '../../../../../actions/server/mocks'; +import { + getActions, + getActionTypes, + getActionExecuteResults, +} from '../__mocks__/request_responses'; + +export const createActionsClient = () => { + const actionsMock = actionsClientMock.create(); + actionsMock.getAll.mockImplementation(() => Promise.resolve(getActions())); + actionsMock.listTypes.mockImplementation(() => Promise.resolve(getActionTypes())); + actionsMock.get.mockImplementation(({ id }) => { + const actions = getActions(); + const action = actions.find((a) => a.id === id); + if (action) { + return Promise.resolve(action); + } else { + return Promise.reject(SavedObjectsErrorHelpers.createGenericNotFoundError('action', id)); + } + }); + actionsMock.execute.mockImplementation(({ actionId }) => + Promise.resolve(getActionExecuteResults(actionId)) + ); + + return actionsMock; +}; diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts index 4ac5004eb3dfd..514f77a8f953d 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/mock_saved_objects.ts @@ -8,6 +8,7 @@ import { SavedObject } from 'kibana/server'; import { CaseStatuses, + CaseUserActionAttributes, CommentAttributes, CommentType, ConnectorMappings, @@ -15,7 +16,10 @@ import { ESCaseAttributes, ESCasesConfigureAttributes, } from '../../../../common/api'; -import { CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT } from '../../../saved_object_types'; +import { + CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT, + CASE_USER_ACTION_SAVED_OBJECT, +} from '../../../saved_object_types'; import { mappings } from '../../../client/configure/mock'; export const mockCases: Array> = [ @@ -424,3 +428,44 @@ export const mockCaseMappings: Array> = [ references: [], }, ]; + +export const mockUserActions: Array> = [ + { + type: CASE_USER_ACTION_SAVED_OBJECT, + id: 'mock-user-actions-1', + attributes: { + action_field: ['description', 'status', 'tags', 'title', 'connector', 'settings'], + action: 'create', + action_at: '2021-02-03T17:41:03.771Z', + action_by: { + email: 'elastic@elastic.co', + full_name: 'Elastic', + username: 'elastic', + }, + new_value: + '{"title":"A case","tags":["case"],"description":"Yeah!","connector":{"id":"connector-od","name":"My Connector","type":".servicenow-sir","fields":{"category":"Denial of Service","destIp":true,"malwareHash":true,"malwareUrl":true,"priority":"2","sourceIp":true,"subcategory":"45"}},"settings":{"syncAlerts":true}}', + old_value: null, + }, + version: 'WzYsMV0=', + references: [], + }, + { + type: CASE_USER_ACTION_SAVED_OBJECT, + id: 'mock-user-actions-2', + attributes: { + action_field: ['comment'], + action: 'create', + action_at: '2021-02-03T17:44:21.067Z', + action_by: { + email: 'elastic@elastic.co', + full_name: 'Elastic', + username: 'elastic', + }, + new_value: + '{"type":"alert","alertId":"cec3da90fb37a44407145adf1593f3b0d5ad94c4654201f773d63b5d4706128e","index":".siem-signals-default-000008"}', + old_value: null, + }, + version: 'WzYsMV0=', + references: [], + }, +]; diff --git a/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts b/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts index 9f7258fc7edaf..74665ffdc5b16 100644 --- a/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts +++ b/x-pack/plugins/case/server/routes/api/__fixtures__/route_contexts.ts @@ -5,24 +5,25 @@ * 2.0. */ -import { KibanaRequest } from 'src/core/server'; -import { loggingSystemMock, elasticsearchServiceMock } from 'src/core/server/mocks'; -import { actionsClientMock } from '../../../../../actions/server/mocks'; +import { KibanaRequest, kibanaResponseFactory } from '../../../../../../../src/core/server'; +import { + loggingSystemMock, + elasticsearchServiceMock, +} from '../../../../../../../src/core/server/mocks'; import { createCaseClient } from '../../../client'; import { AlertService, CaseService, CaseConfigureService, ConnectorMappingsService, + CaseUserActionService, } from '../../../services'; -import { getActions, getActionTypes } from '../__mocks__/request_responses'; import { authenticationMock } from '../__fixtures__'; import type { CasesRequestHandlerContext } from '../../../types'; +import { createActionsClient } from './mock_actions_client'; export const createRouteContext = async (client: any, badAuth = false) => { - const actionsMock = actionsClientMock.create(); - actionsMock.getAll.mockImplementation(() => Promise.resolve(getActions())); - actionsMock.listTypes.mockImplementation(() => Promise.resolve(getActionTypes())); + const actionsMock = createActionsClient(); const log = loggingSystemMock.create().get('case'); const esClientMock = elasticsearchServiceMock.createClusterClient(); @@ -30,11 +31,13 @@ export const createRouteContext = async (client: any, badAuth = false) => { const caseServicePlugin = new CaseService(log); const caseConfigureServicePlugin = new CaseConfigureService(log); const connectorMappingsServicePlugin = new ConnectorMappingsService(log); + const caseUserActionsServicePlugin = new CaseUserActionService(log); const caseService = await caseServicePlugin.setup({ authentication: badAuth ? authenticationMock.createInvalid() : authenticationMock.create(), }); const caseConfigureService = await caseConfigureServicePlugin.setup(); + const userActionService = await caseUserActionsServicePlugin.setup(); const alertsService = new AlertService(); alertsService.initialize(esClientMock); @@ -59,16 +62,14 @@ export const createRouteContext = async (client: any, badAuth = false) => { const caseClient = createCaseClient({ savedObjectsClient: client, request: {} as KibanaRequest, + response: kibanaResponseFactory, caseService, caseConfigureService, connectorMappingsService, - userActionService: { - postUserActions: jest.fn(), - getUserActions: jest.fn(), - }, + userActionService, alertsService, context, }); - return context; + return { context, services: { userActionService } }; }; diff --git a/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts b/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts index f2109167527c7..ae14b44e7dffe 100644 --- a/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts +++ b/x-pack/plugins/case/server/routes/api/__mocks__/request_responses.ts @@ -10,11 +10,9 @@ import { CasePostRequest, CasesConfigureRequest, ConnectorTypes, - PostPushRequest, } from '../../../../common/api'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { FindActionResult } from '../../../../../actions/server/types'; -import { params } from '../cases/configure/mock'; export const newCase: CasePostRequest = { title: 'My new case', @@ -74,6 +72,16 @@ export const getActions = (): FindActionResult[] => [ isPreconfigured: false, referencedByCount: 0, }, + { + id: 'for-mock-case-id-3', + actionTypeId: '.jira', + name: 'For mock case id 3', + config: { + apiUrl: 'https://elastic.jira.com', + }, + isPreconfigured: false, + referencedByCount: 0, + }, ]; export const getActionTypes = (): ActionTypeConnector[] => [ @@ -119,6 +127,18 @@ export const getActionTypes = (): ActionTypeConnector[] => [ }, ]; +export const getActionExecuteResults = (actionId = '123') => ({ + status: 'ok' as const, + data: { + title: 'RJ2-200', + id: '10663', + pushedDate: '2020-12-17T00:32:40.738Z', + url: 'https://siem-kibana.atlassian.net/browse/RJ2-200', + comments: [], + }, + actionId, +}); + export const newConfiguration: CasesConfigureRequest = { connector: { id: '456', @@ -129,11 +149,6 @@ export const newConfiguration: CasesConfigureRequest = { closure_type: 'close-by-pushing', }; -export const newPostPushRequest: PostPushRequest = { - params: params[ConnectorTypes.jira], - connector_type: ConnectorTypes.jira, -}; - export const executePushResponse = { status: 'ok', data: { diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.test.ts b/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.test.ts index 9454f582e50c6..dcbcd7b9e246d 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/delete_comment.test.ts @@ -33,14 +33,14 @@ describe('DELETE comment', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(204); }); it(`returns an error when thrown from deleteComment service`, async () => { @@ -53,14 +53,14 @@ describe('DELETE comment', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(404); }); }); diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.test.ts b/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.test.ts index a1f4b8c2583cf..8ee43eaba8a82 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/get_comment.test.ts @@ -34,14 +34,14 @@ describe('GET comment', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); const myPayload = mockCaseComments.find((s) => s.id === 'mock-comment-1'); expect(myPayload).not.toBeUndefined(); @@ -59,13 +59,13 @@ describe('GET comment', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(404); }); }); diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.test.ts b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.test.ts index 3bd8a688e1bba..33dc24d776c70 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/patch_comment.test.ts @@ -41,14 +41,14 @@ describe('PATCH comment', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); expect(response.payload.comments[response.payload.comments.length - 1].comment).toEqual( 'Update my comment' @@ -71,14 +71,14 @@ describe('PATCH comment', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); expect(response.payload.comments[response.payload.comments.length - 1].alertId).toEqual( 'new-id' @@ -102,14 +102,14 @@ describe('PATCH comment', () => { body: requestAttributes, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(400); expect(response.payload.isBoom).toEqual(true); } @@ -130,14 +130,14 @@ describe('PATCH comment', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(400); expect(response.payload.isBoom).toEqual(true); } @@ -161,14 +161,14 @@ describe('PATCH comment', () => { body: requestAttributes, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(400); expect(response.payload.isBoom).toEqual(true); } @@ -190,14 +190,14 @@ describe('PATCH comment', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(400); expect(response.payload.isBoom).toEqual(true); } @@ -219,14 +219,14 @@ describe('PATCH comment', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(400); expect(response.payload.isBoom).toEqual(true); expect(response.payload.message).toEqual('You cannot change the type of the comment.'); @@ -247,14 +247,14 @@ describe('PATCH comment', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(409); }); @@ -273,14 +273,14 @@ describe('PATCH comment', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(404); expect(response.payload.isBoom).toEqual(true); }); diff --git a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts index 54699415cd984..0ab038a62ac77 100644 --- a/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/comments/post_comment.test.ts @@ -43,14 +43,14 @@ describe('POST comment', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); expect(response.payload.comments[response.payload.comments.length - 1].id).toEqual( 'mock-comment' @@ -71,14 +71,14 @@ describe('POST comment', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); expect(response.payload.comments[response.payload.comments.length - 1].id).toEqual( 'mock-comment' @@ -95,14 +95,14 @@ describe('POST comment', () => { body: {}, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(400); expect(response.payload.isBoom).toEqual(true); }); @@ -124,14 +124,14 @@ describe('POST comment', () => { body: requestAttributes, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(400); expect(response.payload.isBoom).toEqual(true); } @@ -152,14 +152,14 @@ describe('POST comment', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(400); expect(response.payload.isBoom).toEqual(true); } @@ -183,14 +183,14 @@ describe('POST comment', () => { body: requestAttributes, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(400); expect(response.payload.isBoom).toEqual(true); } @@ -212,14 +212,14 @@ describe('POST comment', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(400); expect(response.payload.isBoom).toEqual(true); } @@ -238,14 +238,14 @@ describe('POST comment', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(404); expect(response.payload.isBoom).toEqual(true); }); @@ -262,14 +262,14 @@ describe('POST comment', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(400); expect(response.payload.isBoom).toEqual(true); }); @@ -289,7 +289,7 @@ describe('POST comment', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, @@ -297,7 +297,7 @@ describe('POST comment', () => { true ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); expect(response.payload.comments[response.payload.comments.length - 1]).toEqual({ comment: 'Wow, good luck catching that bad meanie!', diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.test.ts index ddcbb3522f986..ff4216a05ae58 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.test.ts @@ -34,7 +34,7 @@ describe('GET configuration', () => { method: 'get', }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, caseMappingsSavedObject: mockCaseMappings, @@ -57,7 +57,7 @@ describe('GET configuration', () => { method: 'get', }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: [{ ...mockCaseConfigure[0], version: undefined }], caseMappingsSavedObject: mockCaseMappings, @@ -98,7 +98,7 @@ describe('GET configuration', () => { method: 'get', }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: [], }) @@ -116,7 +116,7 @@ describe('GET configuration', () => { method: 'get', }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: [{ ...mockCaseConfigure[0], id: 'throw-error-find' }], }) @@ -133,7 +133,7 @@ describe('GET configuration', () => { method: 'get', }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, caseMappingsSavedObject: [], diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts index 0f74b7291dd81..17972e129a825 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_configure.ts @@ -33,9 +33,9 @@ export function initGetCaseConfigure({ caseConfigureService, router }: RouteDeps throw Boom.badRequest('RouteHandlerContext is not registered for cases'); } const caseClient = context.case.getCaseClient(); - const actionsClient = await context.actions?.getActionsClient(); + const actionsClient = context.actions?.getActionsClient(); if (actionsClient == null) { - throw Boom.notFound('Action client have not been found'); + throw Boom.notFound('Action client not found'); } try { mappings = await caseClient.getMappings({ diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts index 1e37918d7766a..3fa0fe2f83f79 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.test.ts @@ -32,7 +32,7 @@ describe('GET connectors', () => { method: 'get', }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, caseMappingsSavedObject: mockCaseMappings, @@ -54,7 +54,7 @@ describe('GET connectors', () => { method: 'get', }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, caseMappingsSavedObject: mockCaseMappings, @@ -106,6 +106,16 @@ describe('GET connectors', () => { isPreconfigured: false, referencedByCount: 0, }, + { + id: 'for-mock-case-id-3', + actionTypeId: '.jira', + name: 'For mock case id 3', + config: { + apiUrl: 'https://elastic.jira.com', + }, + isPreconfigured: false, + referencedByCount: 0, + }, ]); }); @@ -115,7 +125,7 @@ describe('GET connectors', () => { method: 'get', }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, caseMappingsSavedObject: mockCaseMappings, diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts index fb0595f858d4e..0a368e0276bb5 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts @@ -14,18 +14,15 @@ import { FindActionResult } from '../../../../../../actions/server/types'; import { CASE_CONFIGURE_CONNECTORS_URL, - SERVICENOW_ACTION_TYPE_ID, - JIRA_ACTION_TYPE_ID, - RESILIENT_ACTION_TYPE_ID, + SUPPORTED_CONNECTORS, } from '../../../../../common/constants'; const isConnectorSupported = ( action: FindActionResult, actionTypes: Record ): boolean => - [SERVICENOW_ACTION_TYPE_ID, JIRA_ACTION_TYPE_ID, RESILIENT_ACTION_TYPE_ID].includes( - action.actionTypeId - ) && actionTypes[action.actionTypeId]?.enabledInLicense; + SUPPORTED_CONNECTORS.includes(action.actionTypeId) && + actionTypes[action.actionTypeId]?.enabledInLicense; /* * Be aware that this api will only return 20 connectors @@ -39,10 +36,10 @@ export function initCaseConfigureGetActionConnector({ router }: RouteDeps) { }, async (context, request, response) => { try { - const actionsClient = await context.actions?.getActionsClient(); + const actionsClient = context.actions?.getActionsClient(); if (actionsClient == null) { - throw Boom.notFound('Action client have not been found'); + throw Boom.notFound('Action client not found'); } const actionTypes = (await actionsClient.listTypes()).reduce( diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/mock.ts b/x-pack/plugins/case/server/routes/api/cases/configure/mock.ts deleted file mode 100644 index 9959a3e4acee6..0000000000000 --- a/x-pack/plugins/case/server/routes/api/cases/configure/mock.ts +++ /dev/null @@ -1,76 +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 { - ServiceConnectorCaseParams, - ServiceConnectorCommentParams, - ConnectorMappingsAttributes, - ConnectorTypes, -} from '../../../../../common/api/connectors'; -export const updateUser = { - updatedAt: '2020-03-13T08:34:53.450Z', - updatedBy: { fullName: 'Another User', username: 'another' }, -}; -const entity = { - createdAt: '2020-03-13T08:34:53.450Z', - createdBy: { fullName: 'Elastic User', username: 'elastic' }, - updatedAt: null, - updatedBy: null, -}; -export const comment: ServiceConnectorCommentParams = { - comment: 'first comment', - commentId: 'b5b4c4d0-574e-11ea-9e2e-21b90f8a9631', - ...entity, -}; -export const defaultPipes = ['informationCreated']; -const basicParams = { - comments: [comment], - description: 'a description', - title: 'a title', - savedObjectId: '1231231231232', - externalId: null, -}; -export const params = { - [ConnectorTypes.jira]: { - ...basicParams, - issueType: '10003', - priority: 'Highest', - parent: '5002', - ...entity, - } as ServiceConnectorCaseParams, - [ConnectorTypes.resilient]: { - ...basicParams, - incidentTypes: ['10003'], - severityCode: '1', - ...entity, - } as ServiceConnectorCaseParams, - [ConnectorTypes.servicenow]: { - ...basicParams, - impact: '3', - severity: '1', - urgency: '2', - ...entity, - } as ServiceConnectorCaseParams, - [ConnectorTypes.none]: {}, -}; -export const mappings: ConnectorMappingsAttributes[] = [ - { - source: 'title', - target: 'short_description', - action_type: 'overwrite', - }, - { - source: 'description', - target: 'description', - action_type: 'append', - }, - { - source: 'comments', - target: 'comments', - action_type: 'append', - }, -]; diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.test.ts index c67a1c064a82f..f43f561e30e10 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.test.ts @@ -42,7 +42,7 @@ describe('PATCH configuration', () => { }, }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, caseMappingsSavedObject: mockCaseMappings, @@ -76,7 +76,7 @@ describe('PATCH configuration', () => { }, }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, caseMappingsSavedObject: mockCaseMappings, @@ -115,7 +115,7 @@ describe('PATCH configuration', () => { }, }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, caseMappingsSavedObject: mockCaseMappings, @@ -153,7 +153,7 @@ describe('PATCH configuration', () => { }, }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, caseMappingsSavedObject: [], @@ -193,7 +193,7 @@ describe('PATCH configuration', () => { }, }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: [], }) @@ -215,7 +215,7 @@ describe('PATCH configuration', () => { }, }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, caseMappingsSavedObject: mockCaseMappings, @@ -243,7 +243,7 @@ describe('PATCH configuration', () => { }, }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, caseMappingsSavedObject: mockCaseMappings, diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts index f847c4f776bf0..6925f116136b3 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/patch_configure.ts @@ -66,7 +66,7 @@ export function initPatchCaseConfigure({ caseConfigureService, caseService, rout throw Boom.badRequest('RouteHandlerContext is not registered for cases'); } const caseClient = context.case.getCaseClient(); - const actionsClient = await context.actions?.getActionsClient(); + const actionsClient = context.actions?.getActionsClient(); if (actionsClient == null) { throw Boom.notFound('Action client have not been found'); } diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.test.ts index 0a7f3ef488fce..7dcb7d1fa12ca 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.test.ts @@ -40,7 +40,7 @@ describe('POST configuration', () => { body: newConfiguration, }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, caseMappingsSavedObject: mockCaseMappings, @@ -73,7 +73,7 @@ describe('POST configuration', () => { body: newConfiguration, }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, caseMappingsSavedObject: [], @@ -113,7 +113,7 @@ describe('POST configuration', () => { body: newConfiguration, }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, caseMappingsSavedObject: mockCaseMappings, @@ -154,7 +154,7 @@ describe('POST configuration', () => { }, }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, caseMappingsSavedObject: mockCaseMappings, @@ -180,7 +180,7 @@ describe('POST configuration', () => { }, }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, caseMappingsSavedObject: mockCaseMappings, @@ -206,7 +206,7 @@ describe('POST configuration', () => { }, }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, caseMappingsSavedObject: mockCaseMappings, @@ -232,7 +232,7 @@ describe('POST configuration', () => { }, }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, caseMappingsSavedObject: mockCaseMappings, @@ -258,7 +258,7 @@ describe('POST configuration', () => { }, }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, caseMappingsSavedObject: mockCaseMappings, @@ -282,7 +282,7 @@ describe('POST configuration', () => { caseMappingsSavedObject: mockCaseMappings, }); - const context = await createRouteContext(savedObjectRepository); + const { context } = await createRouteContext(savedObjectRepository); const res = await routeHandler(context, req, kibanaResponseFactory); @@ -302,7 +302,7 @@ describe('POST configuration', () => { caseMappingsSavedObject: mockCaseMappings, }); - const context = await createRouteContext(savedObjectRepository); + const { context } = await createRouteContext(savedObjectRepository); const res = await routeHandler(context, req, kibanaResponseFactory); @@ -325,7 +325,7 @@ describe('POST configuration', () => { caseMappingsSavedObject: mockCaseMappings, }); - const context = await createRouteContext(savedObjectRepository); + const { context } = await createRouteContext(savedObjectRepository); const res = await routeHandler(context, req, kibanaResponseFactory); @@ -341,7 +341,7 @@ describe('POST configuration', () => { body: newConfiguration, }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: [{ ...mockCaseConfigure[0], id: 'throw-error-find' }], }) @@ -359,7 +359,7 @@ describe('POST configuration', () => { body: newConfiguration, }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: [{ ...mockCaseConfigure[0], id: 'throw-error-delete' }], }) @@ -384,7 +384,7 @@ describe('POST configuration', () => { }, }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, caseMappingsSavedObject: mockCaseMappings, @@ -411,7 +411,7 @@ describe('POST configuration', () => { }, }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, caseMappingsSavedObject: mockCaseMappings, @@ -437,7 +437,7 @@ describe('POST configuration', () => { }, }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, caseMappingsSavedObject: mockCaseMappings, @@ -459,7 +459,7 @@ describe('POST configuration', () => { }, }); - const context = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseConfigureSavedObject: mockCaseConfigure, caseMappingsSavedObject: mockCaseMappings, diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts index 8e5fd95facc3d..0bcf2ac18740f 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/post_configure.ts @@ -39,9 +39,9 @@ export function initPostCaseConfigure({ caseConfigureService, caseService, route throw Boom.badRequest('RouteHandlerContext is not registered for cases'); } const caseClient = context.case.getCaseClient(); - const actionsClient = await context.actions?.getActionsClient(); + const actionsClient = context.actions?.getActionsClient(); if (actionsClient == null) { - throw Boom.notFound('Action client have not been found'); + throw Boom.notFound('Action client not found'); } const client = context.core.savedObjects.client; const query = pipe( diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/post_push_to_service.test.ts b/x-pack/plugins/case/server/routes/api/cases/configure/post_push_to_service.test.ts deleted file mode 100644 index e382813dbf0c5..0000000000000 --- a/x-pack/plugins/case/server/routes/api/cases/configure/post_push_to_service.test.ts +++ /dev/null @@ -1,106 +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 { kibanaResponseFactory, RequestHandler } from 'src/core/server'; -import { httpServerMock } from 'src/core/server/mocks'; - -import { - createMockSavedObjectsRepository, - createRoute, - createRouteContext, - mockCaseMappings, -} from '../../__fixtures__'; - -import { initPostPushToService } from './post_push_to_service'; -import { executePushResponse, newPostPushRequest } from '../../__mocks__/request_responses'; -import { CASE_CONFIGURE_PUSH_URL } from '../../../../../common/constants'; -import type { CasesRequestHandlerContext } from '../../../../types'; - -describe('Post push to service', () => { - let routeHandler: RequestHandler; - const req = httpServerMock.createKibanaRequest({ - path: `${CASE_CONFIGURE_PUSH_URL}`, - method: 'post', - params: { - connector_id: '666', - }, - body: newPostPushRequest, - }); - let context: CasesRequestHandlerContext; - beforeAll(async () => { - routeHandler = await createRoute(initPostPushToService, 'post'); - const spyOnDate = jest.spyOn(global, 'Date') as jest.SpyInstance<{}, []>; - spyOnDate.mockImplementation(() => ({ - toISOString: jest.fn().mockReturnValue('2020-04-09T09:43:51.778Z'), - })); - context = await createRouteContext( - createMockSavedObjectsRepository({ - caseMappingsSavedObject: mockCaseMappings, - }) - ); - }); - - it('Happy path - posts success', async () => { - const betterContext = ({ - ...context, - actions: { - ...context.actions, - getActionsClient: () => { - const actions = context!.actions!.getActionsClient(); - return { - ...actions, - execute: jest.fn().mockImplementation(({ actionId }) => { - return { - status: 'ok', - data: { - title: 'RJ2-200', - id: '10663', - pushedDate: '2020-12-17T00:32:40.738Z', - url: 'https://siem-kibana.atlassian.net/browse/RJ2-200', - comments: [], - }, - actionId, - }; - }), - }; - }, - }, - } as unknown) as CasesRequestHandlerContext; - - const res = await routeHandler(betterContext, req, kibanaResponseFactory); - - expect(res.status).toEqual(200); - expect(res.payload).toEqual({ - ...executePushResponse, - actionId: '666', - }); - }); - it('Unhappy path - context case missing', async () => { - const betterContext = ({ - ...context, - case: null, - } as unknown) as CasesRequestHandlerContext; - - const res = await routeHandler(betterContext, req, kibanaResponseFactory); - expect(res.status).toEqual(400); - expect(res.payload.isBoom).toBeTruthy(); - expect(res.payload.output.payload.message).toEqual( - 'RouteHandlerContext is not registered for cases' - ); - }); - it('Unhappy path - context actions missing', async () => { - const betterContext = ({ - ...context, - actions: null, - } as unknown) as CasesRequestHandlerContext; - - const res = await routeHandler(betterContext, req, kibanaResponseFactory); - expect(res.status).toEqual(404); - expect(res.payload.isBoom).toBeTruthy(); - expect(res.payload.output.payload.message).toEqual('Action client have not been found'); - }); -}); diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/post_push_to_service.ts b/x-pack/plugins/case/server/routes/api/cases/configure/post_push_to_service.ts deleted file mode 100644 index b8ba1a9ccb6ef..0000000000000 --- a/x-pack/plugins/case/server/routes/api/cases/configure/post_push_to_service.ts +++ /dev/null @@ -1,81 +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 { pipe } from 'fp-ts/lib/pipeable'; -import { fold } from 'fp-ts/lib/Either'; -import { identity } from 'fp-ts/lib/function'; -import Boom from '@hapi/boom'; -import { RouteDeps } from '../../types'; -import { escapeHatch, wrapError } from '../../utils'; - -import { CASE_CONFIGURE_PUSH_URL } from '../../../../../common/constants'; -import { - ConnectorRequestParamsRt, - PostPushRequestRt, - throwErrors, -} from '../../../../../common/api'; -import { mapIncident } from './utils'; - -export function initPostPushToService({ router }: RouteDeps) { - router.post( - { - path: CASE_CONFIGURE_PUSH_URL, - validate: { - params: escapeHatch, - body: escapeHatch, - }, - }, - async (context, request, response) => { - try { - if (!context.case) { - throw Boom.badRequest('RouteHandlerContext is not registered for cases'); - } - const caseClient = context.case.getCaseClient(); - const actionsClient = await context.actions?.getActionsClient(); - if (actionsClient == null) { - throw Boom.notFound('Action client have not been found'); - } - const params = pipe( - ConnectorRequestParamsRt.decode(request.params), - fold(throwErrors(Boom.badRequest), identity) - ); - const body = pipe( - PostPushRequestRt.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); - - const myConnectorMappings = await caseClient.getMappings({ - actionsClient, - caseClient, - connectorId: params.connector_id, - connectorType: body.connector_type, - }); - - const res = await mapIncident( - actionsClient, - params.connector_id, - body.connector_type, - myConnectorMappings, - body.params - ); - const pushRes = await actionsClient.execute({ - actionId: params.connector_id, - params: { - subAction: 'pushToService', - subActionParams: res, - }, - }); - - return response.ok({ - body: pushRes, - }); - } catch (error) { - return response.customError(wrapError(error)); - } - } - ); -} diff --git a/x-pack/plugins/case/server/routes/api/cases/delete_cases.test.ts b/x-pack/plugins/case/server/routes/api/cases/delete_cases.test.ts index 84e452ea8e871..d588950bec9aa 100644 --- a/x-pack/plugins/case/server/routes/api/cases/delete_cases.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/delete_cases.test.ts @@ -33,14 +33,14 @@ describe('DELETE case', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(204); }); it(`returns an error when thrown from deleteCase service`, async () => { @@ -52,14 +52,14 @@ describe('DELETE case', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(404); }); it(`returns an error when thrown from getAllCaseComments service`, async () => { @@ -71,14 +71,14 @@ describe('DELETE case', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCasesErrorTriggerData, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(400); }); it(`returns an error when thrown from deleteComment service`, async () => { @@ -90,14 +90,14 @@ describe('DELETE case', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCasesErrorTriggerData, caseCommentSavedObject: mockCasesErrorTriggerData, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(400); }); }); diff --git a/x-pack/plugins/case/server/routes/api/cases/find_cases.test.ts b/x-pack/plugins/case/server/routes/api/cases/find_cases.test.ts index acd7de1e8643e..ca9f731ca5010 100644 --- a/x-pack/plugins/case/server/routes/api/cases/find_cases.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/find_cases.test.ts @@ -30,13 +30,13 @@ describe('FIND all cases', () => { method: 'get', }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); expect(response.payload.cases).toHaveLength(4); // mockSavedObjectsRepository do not support filters and returns all cases every time. @@ -51,13 +51,13 @@ describe('FIND all cases', () => { method: 'get', }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); expect(response.payload.cases[2].connector.id).toEqual('123'); }); @@ -68,13 +68,13 @@ describe('FIND all cases', () => { method: 'get', }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: [mockCaseNoConnectorId], }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); expect(response.payload.cases[0].connector.id).toEqual('none'); }); @@ -85,14 +85,14 @@ describe('FIND all cases', () => { method: 'get', }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: [mockCaseNoConnectorId], caseConfigureSavedObject: mockCaseConfigure, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); expect(response.payload.cases[0].connector.id).toEqual('none'); }); diff --git a/x-pack/plugins/case/server/routes/api/cases/get_case.test.ts b/x-pack/plugins/case/server/routes/api/cases/get_case.test.ts index 7aa6f110a0079..968dd0424fe3f 100644 --- a/x-pack/plugins/case/server/routes/api/cases/get_case.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/get_case.test.ts @@ -40,13 +40,13 @@ describe('GET case', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); const savedObject = (mockCases.find( (s) => s.id === 'mock-id-1' ) as unknown) as SavedObject; @@ -71,13 +71,13 @@ describe('GET case', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(404); expect(response.payload.isBoom).toEqual(true); @@ -95,14 +95,14 @@ describe('GET case', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); expect(response.payload.comments).toHaveLength(5); @@ -120,13 +120,13 @@ describe('GET case', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCasesErrorTriggerData, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(400); }); @@ -143,13 +143,13 @@ describe('GET case', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: [mockCaseNoConnectorId], }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); expect(response.payload.connector).toEqual({ @@ -172,14 +172,14 @@ describe('GET case', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: [mockCaseNoConnectorId], caseConfigureSavedObject: mockCaseConfigure, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); expect(response.payload.connector).toEqual({ @@ -202,14 +202,14 @@ describe('GET case', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseConfigureSavedObject: mockCaseConfigure, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); expect(response.payload.connector).toEqual({ diff --git a/x-pack/plugins/case/server/routes/api/cases/get_case.ts b/x-pack/plugins/case/server/routes/api/cases/get_case.ts index f563fc274b18b..55377d93e528d 100644 --- a/x-pack/plugins/case/server/routes/api/cases/get_case.ts +++ b/x-pack/plugins/case/server/routes/api/cases/get_case.ts @@ -7,9 +7,8 @@ import { schema } from '@kbn/config-schema'; -import { CaseResponseRt } from '../../../../common/api'; import { RouteDeps } from '../types'; -import { flattenCaseSavedObject, wrapError } from '../utils'; +import { wrapError } from '../utils'; import { CASE_DETAILS_URL } from '../../../../common/constants'; export function initGetCaseApi({ caseConfigureService, caseService, router }: RouteDeps) { @@ -26,44 +25,17 @@ export function initGetCaseApi({ caseConfigureService, caseService, router }: Ro }, }, async (context, request, response) => { - try { - const client = context.core.savedObjects.client; - const includeComments = JSON.parse(request.query.includeComments); - - const [theCase] = await Promise.all([ - caseService.getCase({ - client, - caseId: request.params.case_id, - }), - ]); - - if (!includeComments) { - return response.ok({ - body: CaseResponseRt.encode( - flattenCaseSavedObject({ - savedObject: theCase, - }) - ), - }); - } + if (!context.case) { + return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); + } - const theComments = await caseService.getAllCaseComments({ - client, - caseId: request.params.case_id, - options: { - sortField: 'created_at', - sortOrder: 'asc', - }, - }); + const caseClient = context.case.getCaseClient(); + const includeComments = JSON.parse(request.query.includeComments); + const id = request.params.case_id; + try { return response.ok({ - body: CaseResponseRt.encode( - flattenCaseSavedObject({ - savedObject: theCase, - comments: theComments.saved_objects, - totalComment: theComments.total, - }) - ), + body: await caseClient.get({ id, includeComments }), }); } catch (error) { return response.customError(wrapError(error)); diff --git a/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts b/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts index 95f7e5bb19a01..6d1134b15b65e 100644 --- a/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/patch_cases.test.ts @@ -44,13 +44,13 @@ describe('PATCH cases', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); expect(response.payload).toEqual([ { @@ -97,14 +97,14 @@ describe('PATCH cases', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseConfigureSavedObject: mockCaseConfigure, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); expect(response.payload).toEqual([ { @@ -151,13 +151,13 @@ describe('PATCH cases', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); expect(response.payload).toEqual([ { @@ -204,13 +204,13 @@ describe('PATCH cases', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: [mockCaseNoConnectorId], }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); expect(response.payload[0].connector.id).toEqual('none'); }); @@ -230,13 +230,13 @@ describe('PATCH cases', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); expect(response.payload[0].connector.id).toEqual('123'); }); @@ -261,13 +261,13 @@ describe('PATCH cases', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); expect(response.payload[0].connector).toEqual({ id: '456', @@ -292,13 +292,13 @@ describe('PATCH cases', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(409); }); @@ -317,14 +317,14 @@ describe('PATCH cases', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseCommentSavedObject: mockCaseComments, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(406); }); @@ -343,13 +343,13 @@ describe('PATCH cases', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(404); expect(response.payload.isBoom).toEqual(true); }); diff --git a/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts b/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts index 997516d2e30b6..292e2c6775a80 100644 --- a/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/post_case.test.ts @@ -49,13 +49,13 @@ describe('POST cases', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); expect(response.payload.id).toEqual('mock-it'); expect(response.payload.status).toEqual('open'); @@ -88,14 +88,14 @@ describe('POST cases', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseConfigureSavedObject: mockCaseConfigure, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); expect(response.payload.connector).toEqual({ id: '123', @@ -121,13 +121,13 @@ describe('POST cases', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(400); }); @@ -146,13 +146,13 @@ describe('POST cases', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(400); expect(response.payload.isBoom).toEqual(true); }); @@ -179,7 +179,7 @@ describe('POST cases', () => { }, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, caseConfigureSavedObject: mockCaseConfigure, @@ -187,7 +187,7 @@ describe('POST cases', () => { true ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); expect(response.payload).toEqual({ closed_at: null, diff --git a/x-pack/plugins/case/server/routes/api/cases/push_case.test.ts b/x-pack/plugins/case/server/routes/api/cases/push_case.test.ts index 549195966b2a7..49801ea4e2f3e 100644 --- a/x-pack/plugins/case/server/routes/api/cases/push_case.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/push_case.test.ts @@ -13,63 +13,187 @@ import { createRoute, createRouteContext, mockCases, + mockCaseConfigure, + mockCaseMappings, + mockUserActions, + mockCaseComments, } from '../__fixtures__'; -import { initPushCaseUserActionApi } from './push_case'; -import { CASE_DETAILS_URL } from '../../../../common/constants'; -import { mockCaseConfigure } from '../__fixtures__/mock_saved_objects'; +import { initPushCaseApi } from './push_case'; +import { CasesRequestHandlerContext } from '../../../types'; +import { getCasePushUrl } from '../../../../common/api/helpers'; describe('Push case', () => { let routeHandler: RequestHandler; const mockDate = '2019-11-25T21:54:48.952Z'; - const caseExternalServiceRequestBody = { - connector_id: 'connector_id', - connector_name: 'connector_name', - external_id: 'external_id', - external_title: 'external_title', - external_url: 'external_url', - }; + const caseId = 'mock-id-3'; + const connectorId = '123'; + const path = getCasePushUrl(caseId, connectorId); + beforeAll(async () => { - routeHandler = await createRoute(initPushCaseUserActionApi, 'post'); + routeHandler = await createRoute(initPushCaseApi, 'post'); const spyOnDate = jest.spyOn(global, 'Date') as jest.SpyInstance<{}, []>; spyOnDate.mockImplementation(() => ({ toISOString: jest.fn().mockReturnValue(mockDate), })); }); + it(`Pushes a case`, async () => { const request = httpServerMock.createKibanaRequest({ - path: `${CASE_DETAILS_URL}/_push`, + path, + method: 'post', + params: { + case_id: caseId, + connector_id: connectorId, + }, + body: {}, + }); + + const { context } = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseMappingsSavedObject: mockCaseMappings, + caseConfigureSavedObject: mockCaseConfigure, + caseUserActionsSavedObject: mockUserActions, + }) + ); + + const response = await routeHandler(context, request, kibanaResponseFactory); + expect(response.status).toEqual(200); + expect(response.payload.external_service).toEqual({ + connector_id: connectorId, + connector_name: 'ServiceNow', + external_id: '10663', + external_title: 'RJ2-200', + external_url: 'https://siem-kibana.atlassian.net/browse/RJ2-200', + pushed_at: mockDate, + pushed_by: { + email: 'd00d@awesome.com', + full_name: 'Awesome D00d', + username: 'awesome', + }, + }); + }); + + it(`Pushes a case with comments`, async () => { + const request = httpServerMock.createKibanaRequest({ + path, + method: 'post', + params: { + case_id: caseId, + connector_id: connectorId, + }, + body: {}, + }); + + const { context } = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseMappingsSavedObject: mockCaseMappings, + caseConfigureSavedObject: mockCaseConfigure, + caseUserActionsSavedObject: mockUserActions, + caseCommentSavedObject: [mockCaseComments[0]], + }) + ); + + const response = await routeHandler(context, request, kibanaResponseFactory); + expect(response.status).toEqual(200); + expect(response.payload.comments[0].pushed_at).toEqual(mockDate); + expect(response.payload.comments[0].pushed_by).toEqual({ + email: 'd00d@awesome.com', + full_name: 'Awesome D00d', + username: 'awesome', + }); + }); + + it(`Filters comments with type alert correctly`, async () => { + const request = httpServerMock.createKibanaRequest({ + path, method: 'post', params: { - case_id: 'mock-id-3', + case_id: caseId, + connector_id: connectorId, }, - body: caseExternalServiceRequestBody, + body: {}, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, + caseMappingsSavedObject: mockCaseMappings, + caseConfigureSavedObject: mockCaseConfigure, + caseUserActionsSavedObject: mockUserActions, + caseCommentSavedObject: [mockCaseComments[0], mockCaseComments[3]], }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const caseClient = context.case.getCaseClient(); + caseClient.getAlerts = jest.fn().mockResolvedValue([]); + + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); - expect(response.payload.external_service.pushed_at).toEqual(mockDate); - expect(response.payload.external_service.connector_id).toEqual('connector_id'); - expect(response.payload.closed_at).toEqual(null); + expect(caseClient.getAlerts).toHaveBeenCalledWith({ ids: ['test-id'] }); + }); + + it(`Calls execute with correct arguments`, async () => { + const request = httpServerMock.createKibanaRequest({ + path, + method: 'post', + params: { + case_id: caseId, + connector_id: 'for-mock-case-id-3', + }, + body: {}, + }); + + const { context } = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseMappingsSavedObject: mockCaseMappings, + caseConfigureSavedObject: mockCaseConfigure, + caseUserActionsSavedObject: mockUserActions, + }) + ); + + const actionsClient = context.actions.getActionsClient(); + + await routeHandler(context, request, kibanaResponseFactory); + expect(actionsClient.execute).toHaveBeenCalledWith({ + actionId: 'for-mock-case-id-3', + params: { + subAction: 'pushToService', + subActionParams: { + incident: { + issueType: 'Task', + parent: null, + priority: 'High', + labels: ['LOLBins'], + summary: 'Another bad one (created at 2019-11-25T22:32:17.947Z by elastic)', + description: + 'Oh no, a bad meanie going LOLBins all over the place! (created at 2019-11-25T22:32:17.947Z by elastic)', + externalId: null, + }, + comments: [], + }, + }, + }); }); + it(`Pushes a case and closes when closure_type: 'close-by-pushing'`, async () => { const request = httpServerMock.createKibanaRequest({ - path: `${CASE_DETAILS_URL}/_push`, + path, method: 'post', params: { - case_id: 'mock-id-3', + case_id: caseId, + connector_id: connectorId, }, - body: caseExternalServiceRequestBody, + body: {}, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, + caseMappingsSavedObject: mockCaseMappings, + caseUserActionsSavedObject: mockUserActions, caseConfigureSavedObject: [ { ...mockCaseConfigure[0], @@ -82,30 +206,259 @@ describe('Push case', () => { }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(200); - expect(response.payload.external_service.pushed_at).toEqual(mockDate); - expect(response.payload.external_service.connector_id).toEqual('connector_id'); expect(response.payload.closed_at).toEqual(mockDate); }); - it(`Returns an error if pushCaseUserAction throws`, async () => { + it(`post the correct user action`, async () => { + const request = httpServerMock.createKibanaRequest({ + path, + method: 'post', + params: { + case_id: caseId, + connector_id: connectorId, + }, + body: {}, + }); + + const { context, services } = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseMappingsSavedObject: mockCaseMappings, + caseConfigureSavedObject: mockCaseConfigure, + caseUserActionsSavedObject: mockUserActions, + }) + ); + + services.userActionService.postUserActions = jest.fn(); + const postUserActions = services.userActionService.postUserActions as jest.Mock; + + const response = await routeHandler(context, request, kibanaResponseFactory); + expect(response.status).toEqual(200); + expect(postUserActions.mock.calls[0][0].actions[0].attributes).toEqual({ + action: 'push-to-service', + action_at: '2019-11-25T21:54:48.952Z', + action_by: { + email: 'd00d@awesome.com', + full_name: 'Awesome D00d', + username: 'awesome', + }, + action_field: ['pushed'], + new_value: + '{"pushed_at":"2019-11-25T21:54:48.952Z","pushed_by":{"username":"awesome","full_name":"Awesome D00d","email":"d00d@awesome.com"},"connector_id":"123","connector_name":"ServiceNow","external_id":"10663","external_title":"RJ2-200","external_url":"https://siem-kibana.atlassian.net/browse/RJ2-200"}', + old_value: null, + }); + }); + + it('Unhappy path - case id is missing', async () => { const request = httpServerMock.createKibanaRequest({ - path: `${CASE_DETAILS_URL}/_push`, + path, method: 'post', - body: { - notagoodbody: 'Throw an error', + params: { + connector_id: connectorId, + }, + body: {}, + }); + + const { context } = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseMappingsSavedObject: mockCaseMappings, + caseConfigureSavedObject: mockCaseConfigure, + caseUserActionsSavedObject: mockUserActions, + }) + ); + + const res = await routeHandler(context, request, kibanaResponseFactory); + expect(res.status).toEqual(400); + }); + + it('Unhappy path - connector id is missing', async () => { + const request = httpServerMock.createKibanaRequest({ + path, + method: 'post', + params: { + case_id: caseId, }, + body: {}, }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, + caseMappingsSavedObject: mockCaseMappings, + caseConfigureSavedObject: mockCaseConfigure, + caseUserActionsSavedObject: mockUserActions, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); - expect(response.status).toEqual(400); - expect(response.payload.isBoom).toEqual(true); + const res = await routeHandler(context, request, kibanaResponseFactory); + expect(res.status).toEqual(400); + }); + + it('Unhappy path - case does not exists', async () => { + const request = httpServerMock.createKibanaRequest({ + path, + method: 'post', + params: { + case_id: 'not-exist', + connector_id: connectorId, + }, + body: {}, + }); + + const { context } = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseMappingsSavedObject: mockCaseMappings, + caseConfigureSavedObject: mockCaseConfigure, + caseUserActionsSavedObject: mockUserActions, + }) + ); + + const res = await routeHandler(context, request, kibanaResponseFactory); + expect(res.status).toEqual(404); + }); + + it('Unhappy path - connector does not exists', async () => { + const request = httpServerMock.createKibanaRequest({ + path, + method: 'post', + params: { + case_id: caseId, + connector_id: 'not-exists', + }, + body: {}, + }); + + const { context } = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseMappingsSavedObject: mockCaseMappings, + caseConfigureSavedObject: mockCaseConfigure, + caseUserActionsSavedObject: mockUserActions, + }) + ); + + const res = await routeHandler(context, request, kibanaResponseFactory); + expect(res.status).toEqual(404); + }); + + it('Unhappy path - cannot push to a closed case', async () => { + const request = httpServerMock.createKibanaRequest({ + path, + method: 'post', + params: { + case_id: 'mock-id-4', + connector_id: connectorId, + }, + body: {}, + }); + + const { context } = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseMappingsSavedObject: mockCaseMappings, + caseConfigureSavedObject: mockCaseConfigure, + caseUserActionsSavedObject: mockUserActions, + }) + ); + + const res = await routeHandler(context, request, kibanaResponseFactory); + expect(res.status).toEqual(409); + expect(res.payload.output.payload.message).toBe( + 'This case Another bad one is closed. You can not pushed if the case is closed.' + ); + }); + + it('Unhappy path - throws when external service returns an error', async () => { + const request = httpServerMock.createKibanaRequest({ + path, + method: 'post', + params: { + case_id: caseId, + connector_id: connectorId, + }, + body: {}, + }); + + const { context } = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseMappingsSavedObject: mockCaseMappings, + caseConfigureSavedObject: mockCaseConfigure, + caseUserActionsSavedObject: mockUserActions, + }) + ); + + const actionsClient = context.actions.getActionsClient(); + (actionsClient.execute as jest.Mock).mockResolvedValue({ + status: 'error', + }); + + const res = await routeHandler(context, request, kibanaResponseFactory); + expect(res.status).toEqual(424); + expect(res.payload.output.payload.message).toBe('Error pushing to service'); + }); + + it('Unhappy path - context case missing', async () => { + const request = httpServerMock.createKibanaRequest({ + path, + method: 'post', + params: { + case_id: caseId, + connector_id: connectorId, + }, + body: {}, + }); + + const { context } = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseMappingsSavedObject: mockCaseMappings, + caseConfigureSavedObject: mockCaseConfigure, + caseUserActionsSavedObject: mockUserActions, + }) + ); + + const betterContext = ({ + ...context, + case: null, + } as unknown) as CasesRequestHandlerContext; + + const res = await routeHandler(betterContext, request, kibanaResponseFactory); + expect(res.status).toEqual(400); + expect(res.payload).toEqual('RouteHandlerContext is not registered for cases'); + }); + + it('Unhappy path - context actions missing', async () => { + const request = httpServerMock.createKibanaRequest({ + path, + method: 'post', + params: { + case_id: caseId, + connector_id: connectorId, + }, + body: {}, + }); + + const { context } = await createRouteContext( + createMockSavedObjectsRepository({ + caseSavedObject: mockCases, + caseMappingsSavedObject: mockCaseMappings, + caseConfigureSavedObject: mockCaseConfigure, + caseUserActionsSavedObject: mockUserActions, + }) + ); + + const betterContext = ({ + ...context, + actions: null, + } as unknown) as CasesRequestHandlerContext; + + const res = await routeHandler(betterContext, request, kibanaResponseFactory); + expect(res.status).toEqual(400); + expect(res.payload).toEqual('Action client not found'); }); }); diff --git a/x-pack/plugins/case/server/routes/api/cases/push_case.ts b/x-pack/plugins/case/server/routes/api/cases/push_case.ts index 218b1f16b9aab..6d670c38bbf85 100644 --- a/x-pack/plugins/case/server/routes/api/cases/push_case.ts +++ b/x-pack/plugins/case/server/routes/api/cases/push_case.ts @@ -5,204 +5,51 @@ * 2.0. */ -import { schema } from '@kbn/config-schema'; import Boom from '@hapi/boom'; -import isEmpty from 'lodash/isEmpty'; import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; -import { - flattenCaseSavedObject, - wrapError, - escapeHatch, - getCommentContextFromAttributes, -} from '../utils'; +import { wrapError, escapeHatch } from '../utils'; -import { - CaseExternalServiceRequestRt, - CaseResponseRt, - throwErrors, - CaseStatuses, -} from '../../../../common/api'; -import { buildCaseUserActionItem } from '../../../services/user_actions/helpers'; +import { throwErrors, CasePushRequestParamsRt } from '../../../../common/api'; import { RouteDeps } from '../types'; -import { CASE_DETAILS_URL } from '../../../../common/constants'; +import { CASE_PUSH_URL } from '../../../../common/constants'; -export function initPushCaseUserActionApi({ - caseConfigureService, - caseService, - router, - userActionService, -}: RouteDeps) { +export function initPushCaseApi({ router }: RouteDeps) { router.post( { - path: `${CASE_DETAILS_URL}/_push`, + path: CASE_PUSH_URL, validate: { - params: schema.object({ - case_id: schema.string(), - }), + params: escapeHatch, body: escapeHatch, }, }, async (context, request, response) => { - try { - const client = context.core.savedObjects.client; - const actionsClient = await context.actions?.getActionsClient(); - - const caseId = request.params.case_id; - const query = pipe( - CaseExternalServiceRequestRt.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); - - if (actionsClient == null) { - throw Boom.notFound('Action client have not been found'); - } - - // eslint-disable-next-line @typescript-eslint/naming-convention - const { username, full_name, email } = await caseService.getUser({ request, response }); - - const pushedDate = new Date().toISOString(); - - const [myCase, myCaseConfigure, totalCommentsFindByCases, connectors] = await Promise.all([ - caseService.getCase({ - client, - caseId: request.params.case_id, - }), - caseConfigureService.find({ client }), - caseService.getAllCaseComments({ - client, - caseId, - options: { - fields: [], - page: 1, - perPage: 1, - }, - }), - actionsClient.getAll(), - ]); - - if (myCase.attributes.status === CaseStatuses.closed) { - throw Boom.conflict( - `This case ${myCase.attributes.title} is closed. You can not pushed if the case is closed.` - ); - } - - const comments = await caseService.getAllCaseComments({ - client, - caseId, - options: { - fields: [], - page: 1, - perPage: totalCommentsFindByCases.total, - }, - }); - - const externalService = { - pushed_at: pushedDate, - pushed_by: { username, full_name, email }, - ...query, - }; + if (!context.case) { + return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); + } - const updateConnector = myCase.attributes.connector; + const caseClient = context.case.getCaseClient(); + const actionsClient = context.actions?.getActionsClient(); - if ( - isEmpty(updateConnector) || - (updateConnector != null && updateConnector.id === 'none') || - !connectors.some((connector) => connector.id === updateConnector.id) - ) { - throw Boom.notFound('Connector not found or set to none'); - } + if (actionsClient == null) { + return response.badRequest({ body: 'Action client not found' }); + } - const [updatedCase, updatedComments] = await Promise.all([ - caseService.patchCase({ - client, - caseId, - updatedAttributes: { - ...(myCaseConfigure.total > 0 && - myCaseConfigure.saved_objects[0].attributes.closure_type === 'close-by-pushing' - ? { - status: CaseStatuses.closed, - closed_at: pushedDate, - closed_by: { email, full_name, username }, - } - : {}), - external_service: externalService, - updated_at: pushedDate, - updated_by: { username, full_name, email }, - }, - version: myCase.version, - }), - caseService.patchComments({ - client, - comments: comments.saved_objects - .filter((comment) => comment.attributes.pushed_at == null) - .map((comment) => ({ - commentId: comment.id, - updatedAttributes: { - pushed_at: pushedDate, - pushed_by: { username, full_name, email }, - }, - version: comment.version, - })), - }), - userActionService.postUserActions({ - client, - actions: [ - ...(myCaseConfigure.total > 0 && - myCaseConfigure.saved_objects[0].attributes.closure_type === 'close-by-pushing' - ? [ - buildCaseUserActionItem({ - action: 'update', - actionAt: pushedDate, - actionBy: { username, full_name, email }, - caseId, - fields: ['status'], - newValue: CaseStatuses.closed, - oldValue: myCase.attributes.status, - }), - ] - : []), - buildCaseUserActionItem({ - action: 'push-to-service', - actionAt: pushedDate, - actionBy: { username, full_name, email }, - caseId, - fields: ['pushed'], - newValue: JSON.stringify(externalService), - }), - ], - }), - ]); + try { + const params = pipe( + CasePushRequestParamsRt.decode(request.params), + fold(throwErrors(Boom.badRequest), identity) + ); return response.ok({ - body: CaseResponseRt.encode( - flattenCaseSavedObject({ - savedObject: { - ...myCase, - ...updatedCase, - attributes: { ...myCase.attributes, ...updatedCase?.attributes }, - references: myCase.references, - }, - comments: comments.saved_objects.map((origComment) => { - const updatedComment = updatedComments.saved_objects.find( - (c) => c.id === origComment.id - ); - return { - ...origComment, - ...updatedComment, - attributes: { - ...origComment.attributes, - ...updatedComment?.attributes, - ...getCommentContextFromAttributes(origComment.attributes), - }, - version: updatedComment?.version ?? origComment.version, - references: origComment?.references ?? [], - }; - }), - }) - ), + body: await caseClient.push({ + caseClient, + actionsClient, + caseId: params.case_id, + connectorId: params.connector_id, + }), }); } catch (error) { return response.customError(wrapError(error)); diff --git a/x-pack/plugins/case/server/routes/api/cases/status/get_status.test.ts b/x-pack/plugins/case/server/routes/api/cases/status/get_status.test.ts index e8761ad69dcca..9644162629f24 100644 --- a/x-pack/plugins/case/server/routes/api/cases/status/get_status.test.ts +++ b/x-pack/plugins/case/server/routes/api/cases/status/get_status.test.ts @@ -36,24 +36,24 @@ describe('GET status', () => { method: 'get', }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: mockCases, }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); - expect(theContext.core.savedObjects.client.find).toHaveBeenNthCalledWith(1, { + const response = await routeHandler(context, request, kibanaResponseFactory); + expect(context.core.savedObjects.client.find).toHaveBeenNthCalledWith(1, { ...findArgs, filter: 'cases.attributes.status: open', }); - expect(theContext.core.savedObjects.client.find).toHaveBeenNthCalledWith(2, { + expect(context.core.savedObjects.client.find).toHaveBeenNthCalledWith(2, { ...findArgs, filter: 'cases.attributes.status: in-progress', }); - expect(theContext.core.savedObjects.client.find).toHaveBeenNthCalledWith(3, { + expect(context.core.savedObjects.client.find).toHaveBeenNthCalledWith(3, { ...findArgs, filter: 'cases.attributes.status: closed', }); @@ -71,13 +71,13 @@ describe('GET status', () => { method: 'get', }); - const theContext = await createRouteContext( + const { context } = await createRouteContext( createMockSavedObjectsRepository({ caseSavedObject: [{ ...mockCases[0], id: 'throw-error-find' }], }) ); - const response = await routeHandler(theContext, request, kibanaResponseFactory); + const response = await routeHandler(context, request, kibanaResponseFactory); expect(response.status).toEqual(404); }); }); diff --git a/x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts b/x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts index 346eec3dde752..06e929cc40e6b 100644 --- a/x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts +++ b/x-pack/plugins/case/server/routes/api/cases/user_actions/get_all_user_actions.ts @@ -7,13 +7,11 @@ import { schema } from '@kbn/config-schema'; -import { CaseUserActionsResponseRt } from '../../../../../common/api'; -import { CASE_SAVED_OBJECT, CASE_COMMENT_SAVED_OBJECT } from '../../../../saved_object_types'; import { RouteDeps } from '../../types'; import { wrapError } from '../../utils'; import { CASE_USER_ACTIONS_URL } from '../../../../../common/constants'; -export function initGetAllUserActionsApi({ userActionService, router }: RouteDeps) { +export function initGetAllUserActionsApi({ router }: RouteDeps) { router.get( { path: CASE_USER_ACTIONS_URL, @@ -24,22 +22,16 @@ export function initGetAllUserActionsApi({ userActionService, router }: RouteDep }, }, async (context, request, response) => { + if (!context.case) { + return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); + } + + const caseClient = context.case.getCaseClient(); + const caseId = request.params.case_id; + try { - const client = context.core.savedObjects.client; - const userActions = await userActionService.getUserActions({ - client, - caseId: request.params.case_id, - }); return response.ok({ - body: CaseUserActionsResponseRt.encode( - userActions.saved_objects.map((ua) => ({ - ...ua.attributes, - action_id: ua.id, - case_id: ua.references.find((r) => r.type === CASE_SAVED_OBJECT)?.id ?? '', - comment_id: - ua.references.find((r) => r.type === CASE_COMMENT_SAVED_OBJECT)?.id ?? null, - })) - ), + body: await caseClient.getUserActions({ caseId }), }); } catch (error) { return response.customError(wrapError(error)); diff --git a/x-pack/plugins/case/server/routes/api/index.ts b/x-pack/plugins/case/server/routes/api/index.ts index c399364ea35ec..00660e08bbd83 100644 --- a/x-pack/plugins/case/server/routes/api/index.ts +++ b/x-pack/plugins/case/server/routes/api/index.ts @@ -10,7 +10,7 @@ import { initFindCasesApi } from '././cases/find_cases'; import { initGetCaseApi } from './cases/get_case'; import { initPatchCasesApi } from './cases/patch_cases'; import { initPostCaseApi } from './cases/post_case'; -import { initPushCaseUserActionApi } from './cases/push_case'; +import { initPushCaseApi } from './cases/push_case'; import { initGetReportersApi } from './cases/reporters/get_reporters'; import { initGetCasesStatusApi } from './cases/status/get_status'; import { initGetTagsApi } from './cases/tags/get_tags'; @@ -28,7 +28,6 @@ import { initCaseConfigureGetActionConnector } from './cases/configure/get_conne import { initGetCaseConfigure } from './cases/configure/get_configure'; import { initPatchCaseConfigure } from './cases/configure/patch_configure'; import { initPostCaseConfigure } from './cases/configure/post_configure'; -import { initPostPushToService } from './cases/configure/post_push_to_service'; import { RouteDeps } from './types'; @@ -39,7 +38,7 @@ export function initCaseApi(deps: RouteDeps) { initGetCaseApi(deps); initPatchCasesApi(deps); initPostCaseApi(deps); - initPushCaseUserActionApi(deps); + initPushCaseApi(deps); initGetAllUserActionsApi(deps); // Comments initDeleteCommentApi(deps); @@ -54,7 +53,6 @@ export function initCaseApi(deps: RouteDeps) { initGetCaseConfigure(deps); initPatchCaseConfigure(deps); initPostCaseConfigure(deps); - initPostPushToService(deps); // Reporters initGetReportersApi(deps); // Status diff --git a/x-pack/plugins/case/server/routes/api/utils.ts b/x-pack/plugins/case/server/routes/api/utils.ts index b7e556daffbd9..e2751c05d880a 100644 --- a/x-pack/plugins/case/server/routes/api/utils.ts +++ b/x-pack/plugins/case/server/routes/api/utils.ts @@ -191,11 +191,11 @@ export const sortToSnake = (sortField: string): SortFieldCase => { export const escapeHatch = schema.object({}, { unknowns: 'allow' }); -const isUserContext = (context: CommentRequest): context is CommentRequestUserType => { +export const isUserContext = (context: CommentRequest): context is CommentRequestUserType => { return context.type === CommentType.user; }; -const isAlertContext = (context: CommentRequest): context is CommentRequestAlertType => { +export const isAlertContext = (context: CommentRequest): context is CommentRequestAlertType => { return context.type === CommentType.alert; }; @@ -206,17 +206,3 @@ export const decodeComment = (comment: CommentRequest) => { pipe(excess(ContextTypeAlertRt).decode(comment), fold(throwErrors(badRequest), identity)); } }; - -export const getCommentContextFromAttributes = ( - attributes: CommentAttributes -): CommentRequestUserType | CommentRequestAlertType => - isUserContext(attributes) - ? { - type: CommentType.user, - comment: attributes.comment, - } - : { - type: CommentType.alert, - alertId: attributes.alertId, - index: attributes.index, - }; diff --git a/x-pack/plugins/case/server/services/alerts/index.ts b/x-pack/plugins/case/server/services/alerts/index.ts index 4f0d415f23b50..2776d6b40761e 100644 --- a/x-pack/plugins/case/server/services/alerts/index.ts +++ b/x-pack/plugins/case/server/services/alerts/index.ts @@ -19,6 +19,24 @@ interface UpdateAlertsStatusArgs { index: string; } +interface GetAlertsArgs { + request: KibanaRequest; + ids: string[]; + index: string; +} + +interface Alert { + _id: string; + _index: string; + _source: Record; +} + +interface AlertsResponse { + hits: { + hits: Alert[]; + }; +} + export class AlertService { private isInitialized = false; private esClient?: IClusterClient; @@ -55,4 +73,30 @@ export class AlertService { return result; } + + public async getAlerts({ request, ids, index }: GetAlertsArgs): Promise { + if (!this.isInitialized) { + throw new Error('AlertService not initialized'); + } + + // The above check makes sure that esClient is defined. + const result = await this.esClient!.asScoped(request).asCurrentUser.search({ + index, + body: { + query: { + bool: { + filter: { + bool: { + should: ids.map((_id) => ({ match: { _id } })), + minimum_should_match: 1, + }, + }, + }, + }, + }, + ignore_unavailable: true, + }); + + return result.body; + } } diff --git a/x-pack/plugins/case/server/services/mocks.ts b/x-pack/plugins/case/server/services/mocks.ts index 7c8b44b297362..0b3615793ef85 100644 --- a/x-pack/plugins/case/server/services/mocks.ts +++ b/x-pack/plugins/case/server/services/mocks.ts @@ -59,4 +59,5 @@ export const createUserActionServiceMock = (): CaseUserActionServiceMock => ({ export const createAlertServiceMock = (): AlertServiceMock => ({ initialize: jest.fn(), updateAlertsStatus: jest.fn(), + getAlerts: jest.fn(), }); diff --git a/x-pack/plugins/security_solution/cypress/integration/cases/connector_options.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases/connector_options.spec.ts index 1380cfd9fca98..95b555c2acae6 100644 --- a/x-pack/plugins/security_solution/cypress/integration/cases/connector_options.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/cases/connector_options.spec.ts @@ -31,6 +31,13 @@ describe('Cases connector incident fields', () => { beforeEach(() => { cleanKibana(); cy.intercept('GET', '/api/cases/configure/connectors/_find', mockConnectorsResponse); + cy.intercept('POST', `/api/actions/action/${connectorIds.sn}/_execute`, (req) => { + const response = + req.body.params.subAction === 'getChoices' + ? executeResponses.servicenow.choices + : { status: 'ok', data: [] }; + req.reply(response); + }); cy.intercept('POST', `/api/actions/action/${connectorIds.jira}/_execute`, (req) => { const response = req.body.params.subAction === 'issueTypes' diff --git a/x-pack/plugins/security_solution/cypress/objects/case.ts b/x-pack/plugins/security_solution/cypress/objects/case.ts index b6c73cd37140c..7a3ce2cb00dfa 100644 --- a/x-pack/plugins/security_solution/cypress/objects/case.ts +++ b/x-pack/plugins/security_solution/cypress/objects/case.ts @@ -113,6 +113,77 @@ export const mockConnectorsResponse = [ }, ]; export const executeResponses = { + servicenow: { + choices: { + status: 'ok', + data: [ + { + dependent_value: '', + label: 'Priviledge Escalation', + value: 'Priviledge Escalation', + element: 'category', + }, + { + dependent_value: '', + label: 'Criminal activity/investigation', + value: 'Criminal activity/investigation', + element: 'category', + }, + { + dependent_value: '', + label: 'Denial of Service', + value: 'Denial of Service', + element: 'category', + }, + { + dependent_value: 'Denial of Service', + label: 'Inbound or outbound', + value: '12', + element: 'subcategory', + }, + { + dependent_value: 'Denial of Service', + label: 'Single or distributed (DoS or DDoS)', + value: '26', + element: 'subcategory', + }, + { + dependent_value: 'Denial of Service', + label: 'Inbound DDos', + value: 'inbound_ddos', + element: 'subcategory', + }, + ...['severity', 'urgency', 'impact', 'priority'] + .map((element) => [ + { + dependent_value: '', + label: '1 - Critical', + value: '1', + element, + }, + { + dependent_value: '', + label: '2 - High', + value: '2', + element, + }, + { + dependent_value: '', + label: '3 - Moderate', + value: '3', + element, + }, + { + dependent_value: '', + label: '4 - Low', + value: '4', + element, + }, + ]) + .flat(), + ], + }, + }, jira: { issueTypes: { status: 'ok', diff --git a/x-pack/plugins/security_solution/cypress/screens/case_details.ts b/x-pack/plugins/security_solution/cypress/screens/case_details.ts index 9ca7a99f9df16..ef8f45b222dd0 100644 --- a/x-pack/plugins/security_solution/cypress/screens/case_details.ts +++ b/x-pack/plugins/security_solution/cypress/screens/case_details.ts @@ -30,9 +30,9 @@ export const CASE_DETAILS_USER_ACTION_DESCRIPTION_USERNAME = export const CASE_DETAILS_USERNAMES = '[data-test-subj="case-view-username"]'; -export const CONNECTOR_CARD_DETAILS = '[data-test-subj="settings-connector-card"]'; +export const CONNECTOR_CARD_DETAILS = '[data-test-subj="connector-card"]'; -export const CONNECTOR_TITLE = '[data-test-subj="settings-connector-card"] span.euiTitle'; +export const CONNECTOR_TITLE = '[data-test-subj="connector-card"] span.euiTitle'; export const DELETE_CASE_BTN = '[data-test-subj="property-actions-trash"]'; diff --git a/x-pack/plugins/security_solution/cypress/screens/edit_connector.ts b/x-pack/plugins/security_solution/cypress/screens/edit_connector.ts index b25b8c11ff830..5b353983e5a92 100644 --- a/x-pack/plugins/security_solution/cypress/screens/edit_connector.ts +++ b/x-pack/plugins/security_solution/cypress/screens/edit_connector.ts @@ -7,7 +7,7 @@ import { connectorIds } from '../objects/case'; -export const CONNECTOR_RESILIENT = `[data-test-subj="connector-settings-resilient"]`; +export const CONNECTOR_RESILIENT = `[data-test-subj="connector-fields-resilient"]`; export const CONNECTOR_SELECTOR = '[data-test-subj="dropdown-connectors"]'; diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx index 511bc682e5504..e74b66eeeb9f0 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.test.tsx @@ -107,7 +107,7 @@ describe('CaseView ', () => { const fetchCaseUserActions = jest.fn(); const fetchCase = jest.fn(); const updateCase = jest.fn(); - const postPushToService = jest.fn(); + const pushCaseToExternalService = jest.fn(); const data = caseProps.caseData; const defaultGetCase = { @@ -144,7 +144,10 @@ describe('CaseView ', () => { jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation); useGetCaseUserActionsMock.mockImplementation(() => defaultUseGetCaseUserActions); - usePostPushToServiceMock.mockImplementation(() => ({ isLoading: false, postPushToService })); + usePostPushToServiceMock.mockImplementation(() => ({ + isLoading: false, + pushCaseToExternalService, + })); useConnectorsMock.mockImplementation(() => ({ connectors: connectorsMock, loading: false })); useQueryAlertsMock.mockImplementation(() => ({ loading: false, @@ -378,7 +381,7 @@ describe('CaseView ', () => { wrapper.update(); - expect(postPushToService).toHaveBeenCalled(); + expect(pushCaseToExternalService).toHaveBeenCalled(); }); }); @@ -508,7 +511,7 @@ describe('CaseView ', () => { connector: { id: 'servicenow-1', name: 'SN 1', - type: ConnectorTypes.servicenow, + type: ConnectorTypes.serviceNowITSM, fields: null, }, }} @@ -556,7 +559,7 @@ describe('CaseView ', () => { connector: { id: 'servicenow-1', name: 'SN 1', - type: ConnectorTypes.servicenow, + type: ConnectorTypes.serviceNowITSM, fields: null, }, }} diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx index 2f39a5a2951b2..e690a01dca54b 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx @@ -297,7 +297,6 @@ export const CaseComponent = React.memo( updateCase: handleUpdateCase, userCanCrud, isValidConnector: isLoadingConnectors ? true : isValidConnector, - alerts, }); const onSubmitConnector = useCallback( @@ -397,7 +396,6 @@ export const CaseComponent = React.memo( ); } }, [dispatch]); - return ( <> diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors.test.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors.test.tsx index ef0c7cfcfa2d6..371ff3528f4f0 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/connectors.test.tsx @@ -72,7 +72,7 @@ describe('Connectors', () => { const newWrapper = mount( , { wrappingComponent: TestProviders, @@ -99,7 +99,7 @@ describe('Connectors', () => { const newWrapper = mount( , { wrappingComponent: TestProviders, diff --git a/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.test.tsx index 23cefce1bacd2..8e317d57dd9ac 100644 --- a/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/configure_cases/index.test.tsx @@ -186,14 +186,14 @@ describe('ConfigureCases', () => { connector: { id: 'servicenow-1', name: 'unchanged', - type: ConnectorTypes.servicenow, + type: ConnectorTypes.serviceNowITSM, fields: null, }, currentConfiguration: { connector: { id: 'servicenow-1', name: 'unchanged', - type: ConnectorTypes.servicenow, + type: ConnectorTypes.serviceNowITSM, fields: null, }, closureType: 'close-by-user', @@ -271,7 +271,7 @@ describe('ConfigureCases', () => { connector: { id: 'servicenow-1', name: 'unchanged', - type: ConnectorTypes.servicenow, + type: ConnectorTypes.serviceNowITSM, fields: null, }, closureType: 'close-by-user', @@ -331,7 +331,7 @@ describe('ConfigureCases', () => { connector: { id: 'servicenow-1', name: 'SN', - type: ConnectorTypes.servicenow, + type: ConnectorTypes.serviceNowITSM, fields: null, }, persistLoading: true, @@ -450,7 +450,7 @@ describe('ConfigureCases', () => { connector: { id: 'servicenow-1', name: 'My connector', - type: ConnectorTypes.servicenow, + type: ConnectorTypes.serviceNowITSM, fields: null, }, })) @@ -493,7 +493,7 @@ describe('closure options', () => { connector: { id: 'servicenow-1', name: 'My connector', - type: ConnectorTypes.servicenow, + type: ConnectorTypes.serviceNowITSM, fields: null, }, currentConfiguration: { @@ -522,7 +522,7 @@ describe('closure options', () => { connector: { id: 'servicenow-1', name: 'My connector', - type: ConnectorTypes.servicenow, + type: ConnectorTypes.serviceNowITSM, fields: null, }, closureType: 'close-by-pushing', @@ -546,7 +546,7 @@ describe('user interactions', () => { connector: { id: 'resilient-2', name: 'unchanged', - type: ConnectorTypes.servicenow, + type: ConnectorTypes.serviceNowITSM, fields: null, }, closureType: 'close-by-user', diff --git a/x-pack/plugins/security_solution/public/cases/components/connector_selector/form.tsx b/x-pack/plugins/security_solution/public/cases/components/connector_selector/form.tsx index 0aaac9c30feb9..d5f5530acde9b 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connector_selector/form.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connector_selector/form.tsx @@ -5,13 +5,13 @@ * 2.0. */ -import React from 'react'; +import React, { useCallback } from 'react'; import { isEmpty } from 'lodash/fp'; import { EuiFormRow } from '@elastic/eui'; import { FieldHook, getFieldValidityAndErrorMessage } from '../../../shared_imports'; import { ConnectorsDropdown } from '../configure_cases/connectors_dropdown'; -import { ActionConnector } from '../../../../../case/common/api/cases'; +import { ActionConnector } from '../../../../../case/common/api'; interface ConnectorSelectorProps { connectors: ActionConnector[]; @@ -21,6 +21,7 @@ interface ConnectorSelectorProps { idAria: string; isEdit: boolean; isLoading: boolean; + handleChange?: (newValue: string) => void; } export const ConnectorSelector = ({ connectors, @@ -30,8 +31,19 @@ export const ConnectorSelector = ({ idAria, isEdit = true, isLoading = false, + handleChange, }: ConnectorSelectorProps) => { const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + const onChange = useCallback( + (val: string) => { + if (handleChange) { + handleChange(val); + } + field.setValue(val); + }, + [handleChange, field] + ); + return isEdit ? ( diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/card.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/card.tsx similarity index 89% rename from x-pack/plugins/security_solution/public/cases/components/settings/card.tsx rename to x-pack/plugins/security_solution/public/cases/components/connectors/card.tsx index 36679cd2452bd..03f909948370d 100644 --- a/x-pack/plugins/security_solution/public/cases/components/settings/card.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/card.tsx @@ -9,7 +9,7 @@ import React, { memo, useMemo } from 'react'; import { EuiCard, EuiIcon, EuiLoadingSpinner } from '@elastic/eui'; import styled from 'styled-components'; -import { connectorsConfiguration } from '../connectors'; +import { connectorsConfiguration } from '.'; import { ConnectorTypes } from '../../../../../case/common/api/connectors'; interface ConnectorCardProps { @@ -51,10 +51,10 @@ const ConnectorCardDisplay: React.FC = ({ ); return ( <> - {isLoading && } + {isLoading && } {!isLoading && ( ({ config: { errors: {} }, secrets: { errors: {} } }), validateParams, actionConnectorFields: null, - actionParamsFields: lazy(() => import('./fields')), + actionParamsFields: lazy(() => import('./alert_fields')), }; } diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/config.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/config.ts index 7be49720fc075..1d12d4b98a823 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/config.ts +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/config.ts @@ -5,17 +5,35 @@ * 2.0. */ -/* eslint-disable @kbn/eslint/no-restricted-paths */ - import { - ServiceNowITSMConnectorConfiguration, - JiraConnectorConfiguration, - ResilientConnectorConfiguration, + getResilientActionType, + getServiceNowITSMActionType, + getServiceNowSIRActionType, + getJiraActionType, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../../triggers_actions_ui/public/common'; import { ConnectorConfiguration } from './types'; +const resilient = getResilientActionType(); +const serviceNowITSM = getServiceNowITSMActionType(); +const serviceNowSIR = getServiceNowSIRActionType(); +const jira = getJiraActionType(); + export const connectorsConfiguration: Record = { - '.servicenow': ServiceNowITSMConnectorConfiguration as ConnectorConfiguration, - '.jira': JiraConnectorConfiguration as ConnectorConfiguration, - '.resilient': ResilientConnectorConfiguration as ConnectorConfiguration, + '.servicenow': { + name: serviceNowITSM.actionTypeTitle ?? '', + logo: serviceNowITSM.iconClass, + }, + '.servicenow-sir': { + name: serviceNowSIR.actionTypeTitle ?? '', + logo: serviceNowSIR.iconClass, + }, + '.jira': { + name: jira.actionTypeTitle ?? '', + logo: jira.iconClass, + }, + '.resilient': { + name: resilient.actionTypeTitle ?? '', + logo: resilient.iconClass, + }, }; diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/connectors_registry.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/connectors_registry.ts new file mode 100644 index 0000000000000..d6896a8ac8c80 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/connectors_registry.ts @@ -0,0 +1,57 @@ +/* + * 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 { CaseConnector, CaseConnectorsRegistry } from './types'; + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +export const createCaseConnectorsRegistry = (): CaseConnectorsRegistry => { + const connectors: Map> = new Map(); + + const registry: CaseConnectorsRegistry = { + has: (id: string) => connectors.has(id), + register: (connector: CaseConnector) => { + if (connectors.has(connector.id)) { + throw new Error( + i18n.translate( + 'xpack.securitySolution.caseConnectorsRegistry.register.duplicateCaseConnectorErrorMessage', + { + defaultMessage: 'Object type "{id}" is already registered.', + values: { + id: connector.id, + }, + } + ) + ); + } + + connectors.set(connector.id, connector); + }, + get: (id: string): CaseConnector => { + if (!connectors.has(id)) { + throw new Error( + i18n.translate( + 'xpack.securitySolution.caseConnectorsRegistry.get.missingCaseConnectorErrorMessage', + { + defaultMessage: 'Object type "{id}" is not registered.', + values: { + id, + }, + } + ) + ); + } + return connectors.get(id)!; + }, + list: () => { + return Array.from(connectors).map(([id, connector]) => connector); + }, + }; + + return registry; +}; diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/fields_form.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/fields_form.tsx similarity index 64% rename from x-pack/plugins/security_solution/public/cases/components/settings/fields_form.tsx rename to x-pack/plugins/security_solution/public/cases/components/connectors/fields_form.tsx index 6b1a0cac8d9cd..41ed99e0f6768 100644 --- a/x-pack/plugins/security_solution/public/cases/components/settings/fields_form.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/fields_form.tsx @@ -8,24 +8,22 @@ import React, { memo, Suspense } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; -import { CaseSettingsConnector, SettingFieldsProps } from './types'; -import { getCaseSettings } from '.'; +import { CaseActionConnector, ConnectorFieldsProps } from './types'; +import { getCaseConnectors } from '.'; import { ConnectorTypeFields } from '../../../../../case/common/api/connectors'; -interface Props extends Omit, 'connector'> { - connector: CaseSettingsConnector | null; +interface Props extends Omit, 'connector'> { + connector: CaseActionConnector | null; } -const SettingFieldsFormComponent: React.FC = ({ connector, isEdit, onChange, fields }) => { - const { caseSettingsRegistry } = getCaseSettings(); +const ConnectorFieldsFormComponent: React.FC = ({ connector, isEdit, onChange, fields }) => { + const { caseConnectorsRegistry } = getCaseConnectors(); if (connector == null || connector.actionTypeId == null || connector.actionTypeId === '.none') { return null; } - const { caseSettingFieldsComponent: FieldsComponent } = caseSettingsRegistry.get( - connector.actionTypeId - ); + const { fieldsComponent: FieldsComponent } = caseConnectorsRegistry.get(connector.actionTypeId); return ( <> @@ -39,7 +37,7 @@ const SettingFieldsFormComponent: React.FC = ({ connector, isEdit, onChan
} > -
+
= ({ connector, isEdit, onChan ); }; -export const SettingFieldsForm = memo(SettingFieldsFormComponent); +export const ConnectorFieldsForm = memo(ConnectorFieldsFormComponent); diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/index.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/index.ts index 96cb215557c24..267126fc6ec8b 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/index.ts +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/index.ts @@ -5,7 +5,53 @@ * 2.0. */ +import { CaseConnectorsRegistry } from './types'; +import { createCaseConnectorsRegistry } from './connectors_registry'; +import { getCaseConnector as getJiraCaseConnector } from './jira'; +import { getCaseConnector as getResilientCaseConnector } from './resilient'; +import { getServiceNowITSMCaseConnector, getServiceNowSIRCaseConnector } from './servicenow'; +import { + JiraFieldsType, + ServiceNowITSMFieldsType, + ServiceNowSIRFieldsType, + ResilientFieldsType, +} from '../../../../../case/common/api/connectors'; + export { getActionType as getCaseConnectorUI } from './case'; export * from './config'; export * from './types'; + +interface GetCaseConnectorsReturn { + caseConnectorsRegistry: CaseConnectorsRegistry; +} + +class CaseConnectors { + private caseConnectorsRegistry: CaseConnectorsRegistry; + + constructor() { + this.caseConnectorsRegistry = createCaseConnectorsRegistry(); + this.init(); + } + + private init() { + this.caseConnectorsRegistry.register(getJiraCaseConnector()); + this.caseConnectorsRegistry.register(getResilientCaseConnector()); + this.caseConnectorsRegistry.register( + getServiceNowITSMCaseConnector() + ); + this.caseConnectorsRegistry.register(getServiceNowSIRCaseConnector()); + } + + registry(): CaseConnectorsRegistry { + return this.caseConnectorsRegistry; + } +} + +const caseConnectors = new CaseConnectors(); + +export const getCaseConnectors = (): GetCaseConnectorsReturn => { + return { + caseConnectorsRegistry: caseConnectors.registry(), + }; +}; diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/jira/__mocks__/api.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/__mocks__/api.ts similarity index 100% rename from x-pack/plugins/security_solution/public/cases/components/settings/jira/__mocks__/api.ts rename to x-pack/plugins/security_solution/public/cases/components/connectors/jira/__mocks__/api.ts diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/jira/api.test.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/api.test.ts similarity index 100% rename from x-pack/plugins/security_solution/public/cases/components/settings/jira/api.test.ts rename to x-pack/plugins/security_solution/public/cases/components/connectors/jira/api.test.ts diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/jira/api.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/api.ts similarity index 100% rename from x-pack/plugins/security_solution/public/cases/components/settings/jira/api.ts rename to x-pack/plugins/security_solution/public/cases/components/connectors/jira/api.ts diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/jira/fields.test.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/case_fields.test.tsx similarity index 99% rename from x-pack/plugins/security_solution/public/cases/components/settings/jira/fields.test.tsx rename to x-pack/plugins/security_solution/public/cases/components/connectors/jira/case_fields.test.tsx index 0c590d0ecd7ad..b151d41c4cdd8 100644 --- a/x-pack/plugins/security_solution/public/cases/components/settings/jira/fields.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/case_fields.test.tsx @@ -12,7 +12,7 @@ import { omit } from 'lodash/fp'; import { connector, issues } from '../mock'; import { useGetIssueTypes } from './use_get_issue_types'; import { useGetFieldsByIssueType } from './use_get_fields_by_issue_type'; -import Fields from './fields'; +import Fields from './case_fields'; import { waitFor } from '@testing-library/dom'; import { useGetSingleIssue } from './use_get_single_issue'; import { useGetIssues } from './use_get_issues'; diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/jira/fields.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/case_fields.tsx similarity index 91% rename from x-pack/plugins/security_solution/public/cases/components/settings/jira/fields.tsx rename to x-pack/plugins/security_solution/public/cases/components/connectors/jira/case_fields.tsx index 6409fe71a85fc..d768b552b78b4 100644 --- a/x-pack/plugins/security_solution/public/cases/components/settings/jira/fields.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/case_fields.tsx @@ -5,25 +5,26 @@ * 2.0. */ -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useMemo, useEffect, useRef } from 'react'; import { map } from 'lodash/fp'; import { EuiFormRow, EuiSelect, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import * as i18n from './translations'; import { ConnectorTypes, JiraFieldsType } from '../../../../../../case/common/api/connectors'; import { useKibana } from '../../../../common/lib/kibana'; -import { SettingFieldsProps } from '../types'; +import { ConnectorFieldsProps } from '../types'; import { useGetIssueTypes } from './use_get_issue_types'; import { useGetFieldsByIssueType } from './use_get_fields_by_issue_type'; import { SearchIssues } from './search_issues'; import { ConnectorCard } from '../card'; -const JiraSettingFieldsComponent: React.FunctionComponent> = ({ +const JiraFieldsComponent: React.FunctionComponent> = ({ connector, fields, isEdit = true, onChange, }) => { + const init = useRef(true); const { issueType = null, priority = null, parent = null } = fields ?? {}; const { http, notifications } = useKibana().services; @@ -138,8 +139,16 @@ const JiraSettingFieldsComponent: React.FunctionComponent { + if (init.current) { + init.current = false; + onChange({ issueType, priority, parent }); + } + }, [issueType, onChange, parent, priority]); + return isEdit ? ( -
+
=> { +export const getCaseConnector = (): CaseConnector => { return { id: '.jira', - caseSettingFieldsComponent: lazy(() => import('./fields')), + fieldsComponent: lazy(() => import('./case_fields')), }; }; diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/jira/search_issues.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/search_issues.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/cases/components/settings/jira/search_issues.tsx rename to x-pack/plugins/security_solution/public/cases/components/connectors/jira/search_issues.tsx diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/jira/translations.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/translations.ts similarity index 60% rename from x-pack/plugins/security_solution/public/cases/components/settings/jira/translations.ts rename to x-pack/plugins/security_solution/public/cases/components/connectors/jira/translations.ts index 65fe339aceb67..07f8f5b984cdd 100644 --- a/x-pack/plugins/security_solution/public/cases/components/settings/jira/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/translations.ts @@ -8,69 +8,69 @@ import { i18n } from '@kbn/i18n'; export const ISSUE_TYPES_API_ERROR = i18n.translate( - 'xpack.securitySolution.components.settings.jira.unableToGetIssueTypesMessage', + 'xpack.securitySolution.components.connectors.jira.unableToGetIssueTypesMessage', { defaultMessage: 'Unable to get issue types', } ); export const FIELDS_API_ERROR = i18n.translate( - 'xpack.securitySolution.components.settings.jira.unableToGetFieldsMessage', + 'xpack.securitySolution.components.connectors.jira.unableToGetFieldsMessage', { - defaultMessage: 'Unable to get fields', + defaultMessage: 'Unable to get connectors', } ); export const ISSUES_API_ERROR = i18n.translate( - 'xpack.securitySolution.components.settings.jira.unableToGetIssuesMessage', + 'xpack.securitySolution.components.connectors.jira.unableToGetIssuesMessage', { defaultMessage: 'Unable to get issues', } ); export const GET_ISSUE_API_ERROR = (id: string) => - i18n.translate('xpack.securitySolution.components.settings.jira.unableToGetIssueMessage', { + i18n.translate('xpack.securitySolution.components.connectors.jira.unableToGetIssueMessage', { defaultMessage: 'Unable to get issue with id {id}', values: { id }, }); export const SEARCH_ISSUES_COMBO_BOX_ARIA_LABEL = i18n.translate( - 'xpack.securitySolution.components.settings.jira.searchIssuesComboBoxAriaLabel', + 'xpack.securitySolution.components.connectors.jira.searchIssuesComboBoxAriaLabel', { defaultMessage: 'Type to search', } ); export const SEARCH_ISSUES_PLACEHOLDER = i18n.translate( - 'xpack.securitySolution.components.settings.jira.searchIssuesComboBoxPlaceholder', + 'xpack.securitySolution.components.connectors.jira.searchIssuesComboBoxPlaceholder', { defaultMessage: 'Type to search', } ); export const SEARCH_ISSUES_LOADING = i18n.translate( - 'xpack.securitySolution.components.settings.jira.searchIssuesLoading', + 'xpack.securitySolution.components.connectors.jira.searchIssuesLoading', { defaultMessage: 'Loading...', } ); export const PRIORITY = i18n.translate( - 'xpack.securitySolution.case.settings.jira.prioritySelectFieldLabel', + 'xpack.securitySolution.case.connectors.jira.prioritySelectFieldLabel', { defaultMessage: 'Priority', } ); export const ISSUE_TYPE = i18n.translate( - 'xpack.securitySolution.case.settings.jira.issueTypesSelectFieldLabel', + 'xpack.securitySolution.case.connectors.jira.issueTypesSelectFieldLabel', { defaultMessage: 'Issue type', } ); export const PARENT_ISSUE = i18n.translate( - 'xpack.securitySolution.case.settings.jira.parentIssueSearchLabel', + 'xpack.securitySolution.case.connectors.jira.parentIssueSearchLabel', { defaultMessage: 'Parent issue', } diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/jira/types.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/types.ts similarity index 100% rename from x-pack/plugins/security_solution/public/cases/components/settings/jira/types.ts rename to x-pack/plugins/security_solution/public/cases/components/connectors/jira/types.ts diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/jira/use_get_fields_by_issue_type.test.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_fields_by_issue_type.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/cases/components/settings/jira/use_get_fields_by_issue_type.test.tsx rename to x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_fields_by_issue_type.test.tsx diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/jira/use_get_fields_by_issue_type.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_fields_by_issue_type.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/cases/components/settings/jira/use_get_fields_by_issue_type.tsx rename to x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_fields_by_issue_type.tsx diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/jira/use_get_issue_types.test.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_issue_types.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/cases/components/settings/jira/use_get_issue_types.test.tsx rename to x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_issue_types.test.tsx diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/jira/use_get_issue_types.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_issue_types.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/cases/components/settings/jira/use_get_issue_types.tsx rename to x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_issue_types.tsx diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/jira/use_get_issues.test.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_issues.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/cases/components/settings/jira/use_get_issues.test.tsx rename to x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_issues.test.tsx diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/jira/use_get_issues.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_issues.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/cases/components/settings/jira/use_get_issues.tsx rename to x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_issues.tsx diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/jira/use_get_single_issue.test.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_single_issue.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/cases/components/settings/jira/use_get_single_issue.test.tsx rename to x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_single_issue.test.tsx diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/jira/use_get_single_issue.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_single_issue.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/cases/components/settings/jira/use_get_single_issue.tsx rename to x-pack/plugins/security_solution/public/cases/components/connectors/jira/use_get_single_issue.tsx diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/mock.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/mock.ts new file mode 100644 index 0000000000000..04e7338025258 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/mock.ts @@ -0,0 +1,109 @@ +/* + * 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 connector = { + id: '123', + name: 'My connector', + actionTypeId: '.jira', + config: {}, + isPreconfigured: false, +}; + +export const issues = [ + { id: 'personId', title: 'Person Task', key: 'personKey' }, + { id: 'womanId', title: 'Woman Task', key: 'womanKey' }, + { id: 'manId', title: 'Man Task', key: 'manKey' }, + { id: 'cameraId', title: 'Camera Task', key: 'cameraKey' }, + { id: 'tvId', title: 'TV Task', key: 'tvKey' }, +]; + +export const choices = [ + { + dependent_value: '', + label: 'Priviledge Escalation', + value: 'Priviledge Escalation', + element: 'category', + }, + { + dependent_value: '', + label: 'Criminal activity/investigation', + value: 'Criminal activity/investigation', + element: 'category', + }, + { + dependent_value: '', + label: 'Denial of Service', + value: 'Denial of Service', + element: 'category', + }, + { + dependent_value: 'Denial of Service', + label: 'Inbound or outbound', + value: '12', + element: 'subcategory', + }, + { + dependent_value: 'Denial of Service', + label: 'Single or distributed (DoS or DDoS)', + value: '26', + element: 'subcategory', + }, + { + dependent_value: 'Denial of Service', + label: 'Inbound DDos', + value: 'inbound_ddos', + element: 'subcategory', + }, + ...['severity', 'urgency', 'impact', 'priority'] + .map((element) => [ + { + dependent_value: '', + label: '1 - Critical', + value: '1', + element, + }, + { + dependent_value: '', + label: '2 - High', + value: '2', + element, + }, + { + dependent_value: '', + label: '3 - Moderate', + value: '3', + element, + }, + { + dependent_value: '', + label: '4 - Low', + value: '4', + element, + }, + ]) + .flat(), +]; + +export const severity = [ + { + id: 4, + name: 'Low', + }, + { + id: 5, + name: 'Medium', + }, + { + id: 6, + name: 'High', + }, +]; + +export const incidentTypes = [ + { id: 17, name: 'Communication error (fax; email)' }, + { id: 1001, name: 'Custom type' }, +]; diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/resilient/__mocks__/api.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/__mocks__/api.ts similarity index 70% rename from x-pack/plugins/security_solution/public/cases/components/settings/resilient/__mocks__/api.ts rename to x-pack/plugins/security_solution/public/cases/components/connectors/resilient/__mocks__/api.ts index f4397eaf1877c..c27248288907d 100644 --- a/x-pack/plugins/security_solution/public/cases/components/settings/resilient/__mocks__/api.ts +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/__mocks__/api.ts @@ -5,29 +5,10 @@ * 2.0. */ +import { incidentTypes, severity } from '../../mock'; import { Props } from '../api'; import { ResilientIncidentTypes, ResilientSeverity } from '../types'; -const severity = [ - { - id: 4, - name: 'Low', - }, - { - id: 5, - name: 'Medium', - }, - { - id: 6, - name: 'High', - }, -]; - -const incidentTypes = [ - { id: 17, name: 'Communication error (fax; email)' }, - { id: 1001, name: 'Custom type' }, -]; - export const getIncidentTypes = async (props: Props): Promise<{ data: ResilientIncidentTypes }> => Promise.resolve({ data: incidentTypes }); diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/resilient/api.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/api.ts similarity index 100% rename from x-pack/plugins/security_solution/public/cases/components/settings/resilient/api.ts rename to x-pack/plugins/security_solution/public/cases/components/connectors/resilient/api.ts diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/resilient/fields.test.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/case_fields.test.tsx similarity index 99% rename from x-pack/plugins/security_solution/public/cases/components/settings/resilient/fields.test.tsx rename to x-pack/plugins/security_solution/public/cases/components/connectors/resilient/case_fields.test.tsx index 9095f3b56f2c3..dd13083288020 100644 --- a/x-pack/plugins/security_solution/public/cases/components/settings/resilient/fields.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/case_fields.test.tsx @@ -13,7 +13,7 @@ import { waitFor } from '@testing-library/react'; import { connector } from '../mock'; import { useGetIncidentTypes } from './use_get_incident_types'; import { useGetSeverity } from './use_get_severity'; -import Fields from './fields'; +import Fields from './case_fields'; jest.mock('../../../../common/lib/kibana'); jest.mock('./use_get_incident_types'); diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/resilient/fields.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/case_fields.tsx similarity index 90% rename from x-pack/plugins/security_solution/public/cases/components/settings/resilient/fields.tsx rename to x-pack/plugins/security_solution/public/cases/components/connectors/resilient/case_fields.tsx index f79ce8a4a5630..8c62f5285c257 100644 --- a/x-pack/plugins/security_solution/public/cases/components/settings/resilient/fields.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/case_fields.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useMemo, useCallback, useEffect } from 'react'; +import React, { useMemo, useCallback, useEffect, useRef } from 'react'; import { EuiComboBox, EuiComboBoxOptionOption, @@ -16,8 +16,7 @@ import { } from '@elastic/eui'; import { useKibana } from '../../../../common/lib/kibana'; -import { SettingFieldsProps } from '../types'; - +import { ConnectorFieldsProps } from '../types'; import { useGetIncidentTypes } from './use_get_incident_types'; import { useGetSeverity } from './use_get_severity'; @@ -25,9 +24,10 @@ import * as i18n from './translations'; import { ConnectorTypes, ResilientFieldsType } from '../../../../../../case/common/api/connectors'; import { ConnectorCard } from '../card'; -const ResilientSettingFieldsComponent: React.FunctionComponent< - SettingFieldsProps +const ResilientFieldsComponent: React.FunctionComponent< + ConnectorFieldsProps > = ({ isEdit = true, fields, connector, onChange }) => { + const init = useRef(true); const { incidentTypes = null, severityCode = null } = fields ?? {}; const { http, notifications } = useKibana().services; @@ -136,14 +136,16 @@ const ResilientSettingFieldsComponent: React.FunctionComponent< } }, [incidentTypes, onFieldChange]); - // We need to set them up at initialization + // Set field at initialization useEffect(() => { - onChange({ incidentTypes, severityCode }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + if (init.current) { + init.current = false; + onChange({ incidentTypes, severityCode }); + } + }, [incidentTypes, onChange, severityCode]); return isEdit ? ( - + => { +export const getCaseConnector = (): CaseConnector => { return { id: '.resilient', - caseSettingFieldsComponent: lazy(() => import('./fields')), + fieldsComponent: lazy(() => import('./case_fields')), }; }; diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/resilient/translations.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/translations.ts similarity index 67% rename from x-pack/plugins/security_solution/public/cases/components/settings/resilient/translations.ts rename to x-pack/plugins/security_solution/public/cases/components/connectors/resilient/translations.ts index 648baf840884b..32a72c3803708 100644 --- a/x-pack/plugins/security_solution/public/cases/components/settings/resilient/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/translations.ts @@ -8,35 +8,35 @@ import { i18n } from '@kbn/i18n'; export const INCIDENT_TYPES_API_ERROR = i18n.translate( - 'xpack.securitySolution.case.settings.resilient.unableToGetIncidentTypesMessage', + 'xpack.securitySolution.case.connectors.resilient.unableToGetIncidentTypesMessage', { defaultMessage: 'Unable to get incident types', } ); export const SEVERITY_API_ERROR = i18n.translate( - 'xpack.securitySolution.case.settings.resilient.unableToGetSeverityMessage', + 'xpack.securitySolution.case.connectors.resilient.unableToGetSeverityMessage', { defaultMessage: 'Unable to get severity', } ); export const INCIDENT_TYPES_PLACEHOLDER = i18n.translate( - 'xpack.securitySolution.case.settings.resilient.incidentTypesPlaceholder', + 'xpack.securitySolution.case.connectors.resilient.incidentTypesPlaceholder', { defaultMessage: 'Choose types', } ); export const INCIDENT_TYPES_LABEL = i18n.translate( - 'xpack.securitySolution.case.settings.resilient.incidentTypesLabel', + 'xpack.securitySolution.case.connectors.resilient.incidentTypesLabel', { defaultMessage: 'Incident Types', } ); export const SEVERITY_LABEL = i18n.translate( - 'xpack.securitySolution.case.settings.resilient.severityLabel', + 'xpack.securitySolution.case.connectors.resilient.severityLabel', { defaultMessage: 'Severity', } diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/resilient/types.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/types.ts similarity index 100% rename from x-pack/plugins/security_solution/public/cases/components/settings/resilient/types.ts rename to x-pack/plugins/security_solution/public/cases/components/connectors/resilient/types.ts diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/resilient/use_get_incident_types.test.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/use_get_incident_types.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/cases/components/settings/resilient/use_get_incident_types.test.tsx rename to x-pack/plugins/security_solution/public/cases/components/connectors/resilient/use_get_incident_types.test.tsx diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/resilient/use_get_incident_types.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/use_get_incident_types.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/cases/components/settings/resilient/use_get_incident_types.tsx rename to x-pack/plugins/security_solution/public/cases/components/connectors/resilient/use_get_incident_types.tsx diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/resilient/use_get_severity.test.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/use_get_severity.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/cases/components/settings/resilient/use_get_severity.test.tsx rename to x-pack/plugins/security_solution/public/cases/components/connectors/resilient/use_get_severity.test.tsx diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/resilient/use_get_severity.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/resilient/use_get_severity.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/cases/components/settings/resilient/use_get_severity.tsx rename to x-pack/plugins/security_solution/public/cases/components/connectors/resilient/use_get_severity.tsx diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/__mocks__/api.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/__mocks__/api.ts new file mode 100644 index 0000000000000..215e3d6f92e6d --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/__mocks__/api.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. + */ + +import { choices } from '../../mock'; +import { GetChoicesProps } from '../api'; +import { Choice } from '../types'; + +export const choicesResponse = { + status: 'ok', + data: choices, +}; + +export const getChoices = async ( + props: GetChoicesProps +): Promise<{ status: string; data: Choice[] }> => Promise.resolve(choicesResponse); diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/api.test.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/api.test.ts new file mode 100644 index 0000000000000..6a6bb7e947997 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/api.test.ts @@ -0,0 +1,40 @@ +/* + * 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 { httpServiceMock } from '../../../../../../../../src/core/public/mocks'; +import { getChoices } from './api'; +import { choices } from '../mock'; + +const choicesResponse = { + status: 'ok', + data: choices, +}; + +describe('ServiceNow API', () => { + const http = httpServiceMock.createStartContract(); + + beforeEach(() => jest.resetAllMocks()); + + describe('getChoices', () => { + test('should call get choices API', async () => { + const abortCtrl = new AbortController(); + http.post.mockResolvedValueOnce(choicesResponse); + const res = await getChoices({ + http, + signal: abortCtrl.signal, + connectorId: 'test', + fields: ['priority'], + }); + + expect(res).toEqual(choicesResponse); + expect(http.post).toHaveBeenCalledWith('/api/actions/action/test/_execute', { + body: '{"params":{"subAction":"getChoices","subActionParams":{"fields":["priority"]}}}', + signal: abortCtrl.signal, + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/api.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/api.ts new file mode 100644 index 0000000000000..d91ad9f8762bd --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/api.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { HttpSetup } from 'kibana/public'; +import { ActionTypeExecutorResult } from '../../../../../../actions/common'; +import { Choice } from './types'; + +export const BASE_ACTION_API_PATH = '/api/actions'; + +export interface GetChoicesProps { + http: HttpSetup; + signal: AbortSignal; + connectorId: string; + fields: string[]; +} + +export async function getChoices({ http, signal, connectorId, fields }: GetChoicesProps) { + return http.post>( + `${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, + { + body: JSON.stringify({ + params: { subAction: 'getChoices', subActionParams: { fields } }, + }), + signal, + } + ); +} diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/index.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/index.ts new file mode 100644 index 0000000000000..81bd81124599f --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/index.ts @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { lazy } from 'react'; + +import { CaseConnector } from '../types'; +import { + ServiceNowITSMFieldsType, + ServiceNowSIRFieldsType, +} from '../../../../../../case/common/api/connectors'; +import * as i18n from './translations'; + +export const getServiceNowITSMCaseConnector = (): CaseConnector => { + return { + id: '.servicenow', + fieldsComponent: lazy(() => import('./servicenow_itsm_case_fields')), + }; +}; + +export const getServiceNowSIRCaseConnector = (): CaseConnector => { + return { + id: '.servicenow-sir', + fieldsComponent: lazy(() => import('./servicenow_sir_case_fields')), + }; +}; + +export const serviceNowITSMFieldLabels = { + impact: i18n.IMPACT, + severity: i18n.SEVERITY, + urgency: i18n.URGENCY, +}; diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/servicenow/fields.test.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_itsm_case_fields.test.tsx similarity index 52% rename from x-pack/plugins/security_solution/public/cases/components/settings/servicenow/fields.test.tsx rename to x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_itsm_case_fields.test.tsx index 2e56e21aa8e98..555ed0dcbb161 100644 --- a/x-pack/plugins/security_solution/public/cases/components/settings/servicenow/fields.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_itsm_case_fields.test.tsx @@ -6,36 +6,74 @@ */ import React from 'react'; -import { mount } from 'enzyme'; -import Fields from './fields'; -import { connector } from '../mock'; -import { waitFor } from '@testing-library/dom'; +import { waitFor, act } from '@testing-library/react'; import { EuiSelect } from '@elastic/eui'; +import { mount } from 'enzyme'; + +import { connector, choices as mockChoices } from '../mock'; +import { Choice } from './types'; +import Fields from './servicenow_itsm_case_fields'; + +let onChoicesSuccess = (c: Choice[]) => {}; -describe('ServiceNow Fields', () => { +jest.mock('../../../../common/lib/kibana'); +jest.mock('./use_get_choices', () => ({ + useGetChoices: (args: { onSuccess: () => void }) => { + onChoicesSuccess = args.onSuccess; + return { isLoading: false, mockChoices }; + }, +})); + +describe('ServiceNowITSM Fields', () => { const fields = { severity: '1', urgency: '2', impact: '3' }; const onChange = jest.fn(); + beforeEach(() => { jest.clearAllMocks(); }); + it('all params fields are rendered - isEdit: true', () => { const wrapper = mount(); - expect(wrapper.find('[data-test-subj="severitySelect"]').first().prop('value')).toEqual('1'); - expect(wrapper.find('[data-test-subj="urgencySelect"]').first().prop('value')).toEqual('2'); - expect(wrapper.find('[data-test-subj="impactSelect"]').first().prop('value')).toEqual('3'); + expect(wrapper.find('[data-test-subj="severitySelect"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="urgencySelect"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="impactSelect"]').exists()).toBeTruthy(); }); - test('all params fields are rendered - isEdit: false', () => { + it('all params fields are rendered - isEdit: false', () => { const wrapper = mount( ); + act(() => { + onChoicesSuccess(mockChoices); + }); + expect(wrapper.find('[data-test-subj="card-list-item"]').at(0).text()).toEqual( - 'Urgency: Medium' + 'Urgency: 2 - High' ); expect(wrapper.find('[data-test-subj="card-list-item"]').at(1).text()).toEqual( - 'Severity: High' + 'Severity: 1 - Critical' + ); + expect(wrapper.find('[data-test-subj="card-list-item"]').at(2).text()).toEqual( + 'Impact: 3 - Moderate' + ); + }); + + it('it transforms the options correctly', async () => { + const wrapper = mount(); + act(() => { + onChoicesSuccess(mockChoices); + }); + + wrapper.update(); + const testers = ['severity', 'urgency', 'impact']; + testers.forEach((subj) => + expect(wrapper.find(`[data-test-subj="${subj}Select"]`).first().prop('options')).toEqual([ + { value: '1', text: '1 - Critical' }, + { value: '2', text: '2 - High' }, + { value: '3', text: '3 - Moderate' }, + { value: '4', text: '4 - Low' }, + ]) ); - expect(wrapper.find('[data-test-subj="card-list-item"]').at(2).text()).toEqual('Impact: Low'); }); describe('onChange calls', () => { diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_itsm_case_fields.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_itsm_case_fields.tsx new file mode 100644 index 0000000000000..e278492b57148 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_itsm_case_fields.tsx @@ -0,0 +1,164 @@ +/* + * 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, useRef } from 'react'; +import { EuiFormRow, EuiSelect, EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import * as i18n from './translations'; + +import { ConnectorFieldsProps } from '../types'; +import { + ConnectorTypes, + ServiceNowITSMFieldsType, +} from '../../../../../../case/common/api/connectors'; +import { useKibana } from '../../../../common/lib/kibana'; +import { ConnectorCard } from '../card'; +import { useGetChoices } from './use_get_choices'; +import { Options, Choice } from './types'; + +const useGetChoicesFields = ['urgency', 'severity', 'impact']; +const defaultOptions: Options = { + urgency: [], + severity: [], + impact: [], +}; + +const ServiceNowITSMFieldsComponent: React.FunctionComponent< + ConnectorFieldsProps +> = ({ isEdit = true, fields, connector, onChange }) => { + const init = useRef(true); + const { severity = null, urgency = null, impact = null } = fields ?? {}; + const { http, notifications } = useKibana().services; + const [options, setOptions] = useState(defaultOptions); + + const listItems = useMemo( + () => [ + ...(urgency != null && urgency.length > 0 + ? [ + { + title: i18n.URGENCY, + description: options.urgency.find((option) => `${option.value}` === urgency)?.text, + }, + ] + : []), + ...(severity != null && severity.length > 0 + ? [ + { + title: i18n.SEVERITY, + description: options.severity.find((option) => `${option.value}` === severity)?.text, + }, + ] + : []), + ...(impact != null && impact.length > 0 + ? [ + { + title: i18n.IMPACT, + description: options.impact.find((option) => `${option.value}` === impact)?.text, + }, + ] + : []), + ], + [urgency, options.urgency, options.severity, options.impact, severity, impact] + ); + + const onChoicesSuccess = (choices: Choice[]) => + setOptions( + choices.reduce( + (acc, choice) => ({ + ...acc, + [choice.element]: [ + ...(acc[choice.element] != null ? acc[choice.element] : []), + { value: choice.value, text: choice.label }, + ], + }), + defaultOptions + ) + ); + + const { isLoading: isLoadingChoices } = useGetChoices({ + http, + toastNotifications: notifications.toasts, + connector, + fields: useGetChoicesFields, + onSuccess: onChoicesSuccess, + }); + + const onChangeCb = useCallback( + ( + key: keyof ServiceNowITSMFieldsType, + value: ServiceNowITSMFieldsType[keyof ServiceNowITSMFieldsType] + ) => { + onChange({ ...fields, [key]: value }); + }, + [fields, onChange] + ); + + // Set field at initialization + useEffect(() => { + if (init.current) { + init.current = false; + onChange({ urgency, severity, impact }); + } + }, [impact, onChange, severity, urgency]); + + return isEdit ? ( +
+ + onChangeCb('urgency', e.target.value)} + /> + + + + + + onChangeCb('severity', e.target.value)} + /> + + + + + onChangeCb('impact', e.target.value)} + /> + + + +
+ ) : ( + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { ServiceNowITSMFieldsComponent as default }; diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_sir_case_fields.test.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_sir_case_fields.test.tsx new file mode 100644 index 0000000000000..7d785406afec8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_sir_case_fields.test.tsx @@ -0,0 +1,198 @@ +/* + * 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 { mount } from 'enzyme'; +import { waitFor, act } from '@testing-library/react'; +import { EuiSelect } from '@elastic/eui'; + +import { connector, choices as mockChoices } from '../mock'; +import { Choice } from './types'; +import Fields from './servicenow_sir_case_fields'; + +let onChoicesSuccess = (c: Choice[]) => {}; + +jest.mock('../../../../common/lib/kibana'); +jest.mock('./use_get_choices', () => ({ + useGetChoices: (args: { onSuccess: () => void }) => { + onChoicesSuccess = args.onSuccess; + return { isLoading: false, mockChoices }; + }, +})); + +describe('ServiceNowSIR Fields', () => { + const fields = { + destIp: true, + sourceIp: true, + malwareHash: true, + malwareUrl: true, + priority: '1', + category: 'Denial of Service', + subcategory: '26', + }; + const onChange = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('all params fields are rendered - isEdit: true', () => { + const wrapper = mount(); + expect(wrapper.find('[data-test-subj="destIpCheckbox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="sourceIpCheckbox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="malwareUrlCheckbox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="malwareHashCheckbox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="prioritySelect"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="categorySelect"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="subcategorySelect"]').exists()).toBeTruthy(); + }); + + test('all params fields are rendered - isEdit: false', () => { + const wrapper = mount( + + ); + act(() => { + onChoicesSuccess(mockChoices); + }); + wrapper.update(); + + expect(wrapper.find('[data-test-subj="card-list-item"]').at(0).text()).toEqual( + 'Destination IP: Yes' + ); + expect(wrapper.find('[data-test-subj="card-list-item"]').at(1).text()).toEqual( + 'Source IP: Yes' + ); + expect(wrapper.find('[data-test-subj="card-list-item"]').at(2).text()).toEqual( + 'Malware URL: Yes' + ); + expect(wrapper.find('[data-test-subj="card-list-item"]').at(3).text()).toEqual( + 'Malware Hash: Yes' + ); + expect(wrapper.find('[data-test-subj="card-list-item"]').at(4).text()).toEqual( + 'Priority: 1 - Critical' + ); + expect(wrapper.find('[data-test-subj="card-list-item"]').at(5).text()).toEqual( + 'Category: Denial of Service' + ); + expect(wrapper.find('[data-test-subj="card-list-item"]').at(6).text()).toEqual( + 'Subcategory: Single or distributed (DoS or DDoS)' + ); + }); + + test('it transforms the categories to options correctly', async () => { + const wrapper = mount(); + act(() => { + onChoicesSuccess(mockChoices); + }); + + wrapper.update(); + expect(wrapper.find('[data-test-subj="categorySelect"]').first().prop('options')).toEqual([ + { value: 'Priviledge Escalation', text: 'Priviledge Escalation' }, + { + value: 'Criminal activity/investigation', + text: 'Criminal activity/investigation', + }, + { value: 'Denial of Service', text: 'Denial of Service' }, + ]); + }); + + test('it transforms the subcategories to options correctly', async () => { + const wrapper = mount(); + act(() => { + onChoicesSuccess(mockChoices); + }); + + wrapper.update(); + expect(wrapper.find('[data-test-subj="subcategorySelect"]').first().prop('options')).toEqual([ + { + text: 'Inbound or outbound', + value: '12', + }, + { + text: 'Single or distributed (DoS or DDoS)', + value: '26', + }, + { + text: 'Inbound DDos', + value: 'inbound_ddos', + }, + ]); + }); + + test('it transforms the priorities to options correctly', async () => { + const wrapper = mount(); + act(() => { + onChoicesSuccess(mockChoices); + }); + + wrapper.update(); + expect(wrapper.find('[data-test-subj="prioritySelect"]').first().prop('options')).toEqual([ + { + text: '1 - Critical', + value: '1', + }, + { + text: '2 - High', + value: '2', + }, + { + text: '3 - Moderate', + value: '3', + }, + { + text: '4 - Low', + value: '4', + }, + ]); + }); + + describe('onChange calls', () => { + const wrapper = mount(); + + act(() => { + onChoicesSuccess(mockChoices); + }); + wrapper.update(); + + expect(onChange).toHaveBeenCalledWith(fields); + + const checkbox = ['destIp', 'sourceIp', 'malwareHash', 'malwareUrl']; + checkbox.forEach((subj) => + test(`${subj.toUpperCase()}`, async () => { + await waitFor(() => { + wrapper + .find(`[data-test-subj="${subj}Checkbox"] input`) + .first() + .simulate('change', { target: { checked: false } }); + expect(onChange).toHaveBeenCalledWith({ + ...fields, + [subj]: false, + }); + }); + }) + ); + + const testers = ['priority', 'category', 'subcategory']; + testers.forEach((subj) => + test(`${subj.toUpperCase()}`, async () => { + await waitFor(() => { + const select = wrapper.find(EuiSelect).filter(`[data-test-subj="${subj}Select"]`)!; + select.prop('onChange')!({ + target: { + value: '9', + }, + } as React.ChangeEvent); + }); + wrapper.update(); + expect(onChange).toHaveBeenCalledWith({ + ...fields, + [subj]: '9', + }); + }) + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_sir_case_fields.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_sir_case_fields.tsx new file mode 100644 index 0000000000000..96db43fe261ac --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/servicenow_sir_case_fields.tsx @@ -0,0 +1,293 @@ +/* + * 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, useRef } from 'react'; +import { + EuiFormRow, + EuiSelect, + EuiFlexGroup, + EuiFlexItem, + EuiSelectOption, + EuiCheckbox, +} from '@elastic/eui'; + +import { + ConnectorTypes, + ServiceNowSIRFieldsType, +} from '../../../../../../case/common/api/connectors'; +import { useKibana } from '../../../../common/lib/kibana'; +import { ConnectorFieldsProps } from '../types'; +import { ConnectorCard } from '../card'; +import { useGetChoices } from './use_get_choices'; +import { Choice, Fields } from './types'; + +import * as i18n from './translations'; + +const useGetChoicesFields = ['category', 'subcategory', 'priority']; +const defaultFields: Fields = { + category: [], + subcategory: [], + priority: [], +}; + +const choicesToEuiOptions = (choices: Choice[]): EuiSelectOption[] => + choices.map((choice) => ({ value: choice.value, text: choice.label })); + +const ServiceNowSIRFieldsComponent: React.FunctionComponent< + ConnectorFieldsProps +> = ({ isEdit = true, fields, connector, onChange }) => { + const init = useRef(true); + const { + category = null, + destIp = true, + malwareHash = true, + malwareUrl = true, + priority = null, + sourceIp = true, + subcategory = null, + } = fields ?? {}; + + const { http, notifications } = useKibana().services; + + const [choices, setChoices] = useState(defaultFields); + + const onChangeCb = useCallback( + ( + key: keyof ServiceNowSIRFieldsType, + value: ServiceNowSIRFieldsType[keyof ServiceNowSIRFieldsType] + ) => { + onChange({ ...fields, [key]: value }); + }, + [fields, onChange] + ); + + const onChoicesSuccess = (values: Choice[]) => { + setChoices( + values.reduce( + (acc, value) => ({ + ...acc, + [value.element]: [...(acc[value.element] != null ? acc[value.element] : []), value], + }), + defaultFields + ) + ); + }; + + const { isLoading: isLoadingChoices } = useGetChoices({ + http, + toastNotifications: notifications.toasts, + connector, + fields: useGetChoicesFields, + onSuccess: onChoicesSuccess, + }); + + const categoryOptions = useMemo(() => choicesToEuiOptions(choices.category), [choices.category]); + const priorityOptions = useMemo(() => choicesToEuiOptions(choices.priority), [choices.priority]); + + const subcategoryOptions = useMemo( + () => + choicesToEuiOptions( + choices.subcategory.filter((choice) => choice.dependent_value === category) + ), + [choices.subcategory, category] + ); + + const listItems = useMemo( + () => [ + ...(destIp != null && destIp + ? [ + { + title: i18n.DEST_IP, + description: i18n.ALERT_FIELD_ENABLED_TEXT, + }, + ] + : []), + ...(sourceIp != null && sourceIp + ? [ + { + title: i18n.SOURCE_IP, + description: i18n.ALERT_FIELD_ENABLED_TEXT, + }, + ] + : []), + ...(malwareUrl != null && malwareUrl + ? [ + { + title: i18n.MALWARE_URL, + description: i18n.ALERT_FIELD_ENABLED_TEXT, + }, + ] + : []), + ...(malwareHash != null && malwareHash + ? [ + { + title: i18n.MALWARE_HASH, + description: i18n.ALERT_FIELD_ENABLED_TEXT, + }, + ] + : []), + ...(priority != null && priority.length > 0 + ? [ + { + title: i18n.PRIORITY, + description: priorityOptions.find((option) => `${option.value}` === priority)?.text, + }, + ] + : []), + ...(category != null && category.length > 0 + ? [ + { + title: i18n.CATEGORY, + description: categoryOptions.find((option) => `${option.value}` === category)?.text, + }, + ] + : []), + ...(subcategory != null && subcategory.length > 0 + ? [ + { + title: i18n.SUBCATEGORY, + description: subcategoryOptions.find((option) => `${option.value}` === subcategory) + ?.text, + }, + ] + : []), + ], + [ + category, + categoryOptions, + destIp, + malwareHash, + malwareUrl, + priority, + priorityOptions, + sourceIp, + subcategory, + subcategoryOptions, + ] + ); + + // Set field at initialization + useEffect(() => { + if (init.current) { + init.current = false; + onChange({ category, destIp, malwareHash, malwareUrl, priority, sourceIp, subcategory }); + } + }, [category, destIp, malwareHash, malwareUrl, onChange, priority, sourceIp, subcategory]); + + return isEdit ? ( +
+ + + + <> + + + onChangeCb('destIp', e.target.checked)} + /> + + + onChangeCb('sourceIp', e.target.checked)} + /> + + + + + onChangeCb('malwareUrl', e.target.checked)} + /> + + + onChangeCb('malwareHash', e.target.checked)} + /> + + + + + + + + + + onChangeCb('priority', e.target.value)} + /> + + + + + + + onChangeCb('category', e.target.value)} + /> + + + + + onChangeCb('subcategory', e.target.value)} + /> + + + +
+ ) : ( + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { ServiceNowSIRFieldsComponent as default }; diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/translations.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/translations.ts new file mode 100644 index 0000000000000..0867dc41eeb78 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/translations.ts @@ -0,0 +1,99 @@ +/* + * 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 URGENCY = i18n.translate( + 'xpack.securitySolution.components.connectors.serviceNow.urgencySelectFieldLabel', + { + defaultMessage: 'Urgency', + } +); + +export const SEVERITY = i18n.translate( + 'xpack.securitySolution.components.connectors.serviceNow.severitySelectFieldLabel', + { + defaultMessage: 'Severity', + } +); + +export const IMPACT = i18n.translate( + 'xpack.securitySolution.components.connectors.serviceNow.impactSelectFieldLabel', + { + defaultMessage: 'Impact', + } +); + +export const CHOICES_API_ERROR = i18n.translate( + 'xpack.securitySolution.components.connectors.serviceNow.unableToGetChoicesMessage', + { + defaultMessage: 'Unable to get choices', + } +); + +export const MALWARE_URL = i18n.translate( + 'xpack.securitySolution.components.connectors.serviceNow.malwareURLTitle', + { + defaultMessage: 'Malware URL', + } +); + +export const MALWARE_HASH = i18n.translate( + 'xpack.securitySolution.components.connectors.serviceNow.malwareHashTitle', + { + defaultMessage: 'Malware Hash', + } +); + +export const CATEGORY = i18n.translate( + 'xpack.securitySolution.components.connectors.serviceNow.categoryTitle', + { + defaultMessage: 'Category', + } +); + +export const SUBCATEGORY = i18n.translate( + 'xpack.securitySolution.components.connectors.serviceNow.subcategoryTitle', + { + defaultMessage: 'Subcategory', + } +); + +export const SOURCE_IP = i18n.translate( + 'xpack.securitySolution.components.connectors.serviceNow.sourceIPTitle', + { + defaultMessage: 'Source IP', + } +); + +export const DEST_IP = i18n.translate( + 'xpack.securitySolution.components.connectors.serviceNow.destinationIPTitle', + { + defaultMessage: 'Destination IP', + } +); + +export const PRIORITY = i18n.translate( + 'xpack.securitySolution.components.connectors.serviceNow.prioritySelectFieldTitle', + { + defaultMessage: 'Priority', + } +); + +export const ALERT_FIELDS_LABEL = i18n.translate( + 'xpack.securitySolution.components.connectors.serviceNow.alertFieldsTitle', + { + defaultMessage: 'Fields associated with alerts', + } +); + +export const ALERT_FIELD_ENABLED_TEXT = i18n.translate( + 'xpack.securitySolution.components.connectors.serviceNow.alertFieldEnabledText', + { + defaultMessage: 'Yes', + } +); diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/types.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/types.ts new file mode 100644 index 0000000000000..deceeed29482b --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/types.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiSelectOption } from '@elastic/eui'; + +export interface Choice { + value: string; + label: string; + dependent_value: string; + element: string; +} + +export type Fields = Record; +export type Options = Record; diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/use_get_choices.test.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/use_get_choices.test.tsx new file mode 100644 index 0000000000000..2492fbaaf5a83 --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/use_get_choices.test.tsx @@ -0,0 +1,144 @@ +/* + * 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 { renderHook } from '@testing-library/react-hooks'; + +import { useKibana } from '../../../../common/lib/kibana'; +import { ActionConnector } from '../../../containers/types'; +import { choices } from '../mock'; +import { useGetChoices, UseGetChoices, UseGetChoicesProps } from './use_get_choices'; +import * as api from './api'; + +jest.mock('./api'); +jest.mock('../../../../common/lib/kibana'); + +const useKibanaMock = useKibana as jest.Mocked; +const onSuccess = jest.fn(); +const fields = ['priority']; + +const connector = { + secrets: { + username: 'user', + password: 'pass', + }, + id: 'test', + actionTypeId: '.servicenow', + name: 'ServiceNow', + isPreconfigured: false, + config: { + apiUrl: 'https://dev94428.service-now.com/', + }, +} as ActionConnector; + +describe('useGetChoices', () => { + const { services } = useKibanaMock(); + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('init', async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetChoices({ + http: services.http, + connector, + toastNotifications: services.notifications.toasts, + fields, + onSuccess, + }) + ); + + await waitForNextUpdate(); + + expect(result.current).toEqual({ + isLoading: false, + choices, + }); + }); + + it('returns an empty array when connector is not presented', async () => { + const { result } = renderHook(() => + useGetChoices({ + http: services.http, + connector: undefined, + toastNotifications: services.notifications.toasts, + fields, + onSuccess, + }) + ); + + expect(result.current).toEqual({ + isLoading: false, + choices: [], + }); + }); + + it('it calls onSuccess', async () => { + const { waitForNextUpdate } = renderHook(() => + useGetChoices({ + http: services.http, + connector, + toastNotifications: services.notifications.toasts, + fields, + onSuccess, + }) + ); + + await waitForNextUpdate(); + + expect(onSuccess).toHaveBeenCalledWith(choices); + }); + + it('it displays an error when service fails', async () => { + const spyOnGetChoices = jest.spyOn(api, 'getChoices'); + spyOnGetChoices.mockResolvedValue( + Promise.resolve({ + actionId: 'test', + status: 'error', + serviceMessage: 'An error occurred', + }) + ); + + const { waitForNextUpdate } = renderHook(() => + useGetChoices({ + http: services.http, + connector, + toastNotifications: services.notifications.toasts, + fields, + onSuccess, + }) + ); + + await waitForNextUpdate(); + + expect(services.notifications.toasts.addDanger).toHaveBeenCalledWith({ + text: 'An error occurred', + title: 'Unable to get choices', + }); + }); + + it('it displays an error when http throws an error', async () => { + const spyOnGetChoices = jest.spyOn(api, 'getChoices'); + spyOnGetChoices.mockImplementation(() => { + throw new Error('An error occurred'); + }); + + renderHook(() => + useGetChoices({ + http: services.http, + connector, + toastNotifications: services.notifications.toasts, + fields, + onSuccess, + }) + ); + + expect(services.notifications.toasts.addDanger).toHaveBeenCalledWith({ + text: 'An error occurred', + title: 'Unable to get choices', + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/use_get_choices.tsx b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/use_get_choices.tsx new file mode 100644 index 0000000000000..16e905bdabfee --- /dev/null +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/servicenow/use_get_choices.tsx @@ -0,0 +1,99 @@ +/* + * 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 { useState, useEffect, useRef } from 'react'; +import { HttpSetup, ToastsApi } from 'kibana/public'; +import { ActionConnector } from '../../../containers/types'; +import { getChoices } from './api'; +import { Choice } from './types'; +import * as i18n from './translations'; + +export interface UseGetChoicesProps { + http: HttpSetup; + toastNotifications: Pick< + ToastsApi, + 'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError' + >; + connector?: ActionConnector; + fields: string[]; + onSuccess?: (choices: Choice[]) => void; +} + +export interface UseGetChoices { + choices: Choice[]; + isLoading: boolean; +} + +export const useGetChoices = ({ + http, + connector, + toastNotifications, + fields, + onSuccess, +}: UseGetChoicesProps): UseGetChoices => { + const [isLoading, setIsLoading] = useState(false); + const [choices, setChoices] = useState([]); + const abortCtrl = useRef(new AbortController()); + + useEffect(() => { + let didCancel = false; + const fetchData = async () => { + if (!connector) { + setIsLoading(false); + return; + } + + abortCtrl.current = new AbortController(); + setIsLoading(true); + + try { + const res = await getChoices({ + http, + signal: abortCtrl.current.signal, + connectorId: connector.id, + fields, + }); + + if (!didCancel) { + setIsLoading(false); + setChoices(res.data ?? []); + if (res.status && res.status === 'error') { + toastNotifications.addDanger({ + title: i18n.CHOICES_API_ERROR, + text: `${res.serviceMessage ?? res.message}`, + }); + } else if (onSuccess) { + onSuccess(res.data ?? []); + } + } + } catch (error) { + if (!didCancel) { + setIsLoading(false); + toastNotifications.addDanger({ + title: i18n.CHOICES_API_ERROR, + text: error.message, + }); + } + } + }; + + abortCtrl.current.abort(); + fetchData(); + + return () => { + didCancel = true; + setIsLoading(false); + abortCtrl.current.abort(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [http, connector, toastNotifications, fields]); + + return { + choices, + isLoading, + }; +}; diff --git a/x-pack/plugins/security_solution/public/cases/components/connectors/types.ts b/x-pack/plugins/security_solution/public/cases/components/connectors/types.ts index 808e185eabb6f..46c707197fdb4 100644 --- a/x-pack/plugins/security_solution/public/cases/components/connectors/types.ts +++ b/x-pack/plugins/security_solution/public/cases/components/connectors/types.ts @@ -5,14 +5,17 @@ * 2.0. */ -import { ActionType } from '../../../../../triggers_actions_ui/public'; +import React from 'react'; import { ActionType as ThirdPartySupportedActions, CaseField, + ActionConnector, + ConnectorTypeFields, } from '../../../../../case/common/api'; export { ThirdPartyField as AllThirdPartyFields } from '../../../../../case/common/api'; +export type CaseActionConnector = ActionConnector; export interface ThirdPartyField { label: string; @@ -21,6 +24,30 @@ export interface ThirdPartyField { defaultActionType: ThirdPartySupportedActions; } -export interface ConnectorConfiguration extends ActionType { +export interface ConnectorConfiguration { + name: string; logo: string; } + +export interface CaseConnector { + id: string; + fieldsComponent: React.LazyExoticComponent< + React.ComponentType> + > | null; +} + +export interface CaseConnectorsRegistry { + has: (id: string) => boolean; + register: ( + connector: CaseConnector + ) => void; + get: (id: string) => CaseConnector; + list: () => CaseConnector[]; +} + +export interface ConnectorFieldsProps { + isEdit?: boolean; + connector: CaseActionConnector; + fields: TFields; + onChange: (fields: TFields) => void; +} diff --git a/x-pack/plugins/security_solution/public/cases/components/create/connector.test.tsx b/x-pack/plugins/security_solution/public/cases/components/create/connector.test.tsx index 2a361a2f6cdce..236c13e5afc08 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/connector.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/connector.test.tsx @@ -14,8 +14,10 @@ import { useForm, Form, FormHook } from '../../../shared_imports'; import { connectorsMock } from '../../containers/mock'; import { Connector } from './connector'; import { useConnectors } from '../../containers/configure/use_connectors'; -import { useGetIncidentTypes } from '../settings/resilient/use_get_incident_types'; -import { useGetSeverity } from '../settings/resilient/use_get_severity'; +import { useGetIncidentTypes } from '../connectors/resilient/use_get_incident_types'; +import { useGetSeverity } from '../connectors/resilient/use_get_severity'; +import { useGetChoices } from '../connectors/servicenow/use_get_choices'; +import { incidentTypes, severity, choices } from '../connectors/mock'; import { schema, FormProps } from './schema'; jest.mock('../../../common/lib/kibana', () => { @@ -29,43 +31,28 @@ jest.mock('../../../common/lib/kibana', () => { }; }); jest.mock('../../containers/configure/use_connectors'); -jest.mock('../settings/resilient/use_get_incident_types'); -jest.mock('../settings/resilient/use_get_severity'); +jest.mock('../connectors/resilient/use_get_incident_types'); +jest.mock('../connectors/resilient/use_get_severity'); +jest.mock('../connectors/servicenow/use_get_choices'); const useConnectorsMock = useConnectors as jest.Mock; const useGetIncidentTypesMock = useGetIncidentTypes as jest.Mock; const useGetSeverityMock = useGetSeverity as jest.Mock; +const useGetChoicesMock = useGetChoices as jest.Mock; const useGetIncidentTypesResponse = { isLoading: false, - incidentTypes: [ - { - id: 19, - name: 'Malware', - }, - { - id: 21, - name: 'Denial of Service', - }, - ], + incidentTypes, }; const useGetSeverityResponse = { isLoading: false, - severity: [ - { - id: 4, - name: 'Low', - }, - { - id: 5, - name: 'Medium', - }, - { - id: 6, - name: 'High', - }, - ], + severity, +}; + +const useGetChoicesResponse = { + isLoading: false, + choices, }; describe('Connector', () => { @@ -90,6 +77,7 @@ describe('Connector', () => { useConnectorsMock.mockReturnValue({ loading: false, connectors: connectorsMock }); useGetIncidentTypesMock.mockReturnValue(useGetIncidentTypesResponse); useGetSeverityMock.mockReturnValue(useGetSeverityResponse); + useGetChoicesMock.mockReturnValue(useGetChoicesResponse); }); it('it renders', async () => { @@ -100,7 +88,7 @@ describe('Connector', () => { ); expect(wrapper.find(`[data-test-subj="caseConnectors"]`).exists()).toBeTruthy(); - expect(wrapper.find(`[data-test-subj="connector-settings"]`).exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="connector-fields"]`).exists()).toBeTruthy(); await waitFor(() => { expect(wrapper.find(`button[data-test-subj="dropdown-connectors"]`).first().text()).toBe( @@ -108,10 +96,10 @@ describe('Connector', () => { ); }); - await waitFor(() => { - wrapper.update(); - expect(wrapper.find(`[data-test-subj="connector-settings-sn"]`).exists()).toBeTruthy(); - }); + // await waitFor(() => { + // wrapper.update(); + // expect(wrapper.find(`[data-test-subj="connector-fields-sn"]`).exists()).toBeTruthy(); + // }); }); it('it is loading when fetching connectors', async () => { @@ -163,7 +151,7 @@ describe('Connector', () => { ); await waitFor(() => { - expect(wrapper.find(`[data-test-subj="connector-settings-resilient"]`).exists()).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="connector-fields-resilient"]`).exists()).toBeFalsy(); wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); wrapper.find(`button[data-test-subj="dropdown-connector-resilient-2"]`).simulate('click'); wrapper.update(); @@ -171,7 +159,7 @@ describe('Connector', () => { await waitFor(() => { wrapper.update(); - expect(wrapper.find(`[data-test-subj="connector-settings-resilient"]`).exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="connector-fields-resilient"]`).exists()).toBeTruthy(); }); act(() => { diff --git a/x-pack/plugins/security_solution/public/cases/components/create/connector.tsx b/x-pack/plugins/security_solution/public/cases/components/create/connector.tsx index 4a8b25f4f7b45..5e7972aec9d4b 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/connector.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/connector.tsx @@ -5,13 +5,13 @@ * 2.0. */ -import React, { memo, useEffect } from 'react'; +import React, { memo, useCallback } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { UseField, useFormData, FieldHook } from '../../../shared_imports'; +import { UseField, useFormData, FieldHook, useFormContext } from '../../../shared_imports'; import { useConnectors } from '../../containers/configure/use_connectors'; import { ConnectorSelector } from '../connector_selector/form'; -import { SettingFieldsForm } from '../settings/fields_form'; +import { ConnectorFieldsForm } from '../connectors/fields_form'; import { ActionConnector } from '../../containers/types'; import { getConnectorById } from '../configure_cases/utils'; import { FormProps } from './schema'; @@ -20,25 +20,19 @@ interface Props { isLoading: boolean; } -interface SettingsFieldProps { +interface ConnectorsFieldProps { connectors: ActionConnector[]; field: FieldHook; isEdit: boolean; } -const SettingsField = ({ connectors, isEdit, field }: SettingsFieldProps) => { +const ConnectorFields = ({ connectors, isEdit, field }: ConnectorsFieldProps) => { const [{ connectorId }] = useFormData({ watch: ['connectorId'] }); const { setValue } = field; const connector = getConnectorById(connectorId, connectors) ?? null; - useEffect(() => { - if (connectorId) { - setValue(null); - } - }, [setValue, connectorId]); - return ( - { }; const ConnectorComponent: React.FC = ({ isLoading }) => { + const { getFields } = useFormContext(); const { loading: isLoadingConnectors, connectors } = useConnectors(); + const handleConnectorChange = useCallback( + (newConnector) => { + const { fields } = getFields(); + fields.setValue(null); + }, + [getFields] + ); return ( @@ -58,6 +60,7 @@ const ConnectorComponent: React.FC = ({ isLoading }) => { component={ConnectorSelector} componentProps={{ connectors, + handleChange: handleConnectorChange, dataTestSubj: 'caseConnectors', disabled: isLoading || isLoadingConnectors, idAria: 'caseConnectors', @@ -68,7 +71,7 @@ const ConnectorComponent: React.FC = ({ isLoading }) => { { @@ -189,7 +186,7 @@ describe('Create case', () => { connector: { id: 'servicenow-1', name: 'SN', - type: ConnectorTypes.servicenow, + type: ConnectorTypes.serviceNowITSM, fields: null, }, persistLoading: false, @@ -237,7 +234,7 @@ describe('Create case', () => { connector: { id: 'not-exist', name: 'SN', - type: ConnectorTypes.servicenow, + type: ConnectorTypes.serviceNowITSM, fields: null, }, persistLoading: false, @@ -261,7 +258,7 @@ describe('Create case', () => { wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); await waitFor(() => { expect(postCase).toBeCalledWith(sampleData); - expect(postPushToService).not.toHaveBeenCalled(); + expect(pushCaseToExternalService).not.toHaveBeenCalled(); }); }); }); @@ -283,13 +280,13 @@ describe('Create case', () => { ); fillForm(wrapper); - expect(wrapper.find(`[data-test-subj="connector-settings-jira"]`).exists()).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="connector-fields-jira"]`).exists()).toBeFalsy(); wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); wrapper.find(`button[data-test-subj="dropdown-connector-jira-1"]`).simulate('click'); await waitFor(() => { wrapper.update(); - expect(wrapper.find(`[data-test-subj="connector-settings-jira"]`).exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="connector-fields-jira"]`).exists()).toBeTruthy(); }); wrapper @@ -318,17 +315,14 @@ describe('Create case', () => { fields: { issueType: '10007', parent: null, priority: '2' }, }, }); - expect(postPushToService).toHaveBeenCalledWith({ + expect(pushCaseToExternalService).toHaveBeenCalledWith({ caseId: sampleId, - caseServices: {}, connector: { id: 'jira-1', name: 'Jira', type: '.jira', fields: { issueType: '10007', parent: null, priority: '2' }, }, - alerts: {}, - updateCase: noop, }); expect(onFormSubmitSuccess).toHaveBeenCalledWith({ id: sampleId, @@ -353,15 +347,13 @@ describe('Create case', () => { ); fillForm(wrapper); - expect(wrapper.find(`[data-test-subj="connector-settings-resilient"]`).exists()).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="connector-fields-resilient"]`).exists()).toBeFalsy(); wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); wrapper.find(`button[data-test-subj="dropdown-connector-resilient-2"]`).simulate('click'); await waitFor(() => { wrapper.update(); - expect( - wrapper.find(`[data-test-subj="connector-settings-resilient"]`).exists() - ).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="connector-fields-resilient"]`).exists()).toBeTruthy(); }); act(() => { @@ -390,17 +382,14 @@ describe('Create case', () => { }, }); - expect(postPushToService).toHaveBeenCalledWith({ + expect(pushCaseToExternalService).toHaveBeenCalledWith({ caseId: sampleId, - caseServices: {}, connector: { id: 'resilient-2', name: 'My Connector 2', type: '.resilient', fields: { incidentTypes: ['19'], severityCode: '4' }, }, - alerts: {}, - updateCase: noop, }); expect(onFormSubmitSuccess).toHaveBeenCalledWith({ @@ -426,10 +415,10 @@ describe('Create case', () => { ); fillForm(wrapper); - expect(wrapper.find(`[data-test-subj="connector-settings-sn"]`).exists()).toBeFalsy(); + expect(wrapper.find(`[data-test-subj="connector-fields-sn"]`).exists()).toBeFalsy(); wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); wrapper.find(`button[data-test-subj="dropdown-connector-servicenow-1"]`).simulate('click'); - expect(wrapper.find(`[data-test-subj="connector-settings-sn"]`).exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="connector-fields-sn"]`).exists()).toBeTruthy(); ['severitySelect', 'urgencySelect', 'impactSelect'].forEach((subj) => { wrapper @@ -453,17 +442,14 @@ describe('Create case', () => { }, }); - expect(postPushToService).toHaveBeenCalledWith({ + expect(pushCaseToExternalService).toHaveBeenCalledWith({ caseId: sampleId, - caseServices: {}, connector: { id: 'servicenow-1', name: 'My Connector', type: '.servicenow', fields: { impact: '2', severity: '2', urgency: '2' }, }, - alerts: {}, - updateCase: noop, }); expect(onFormSubmitSuccess).toHaveBeenCalledWith({ diff --git a/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx b/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx index 20ec1e9177cd3..cc38e07cf49e4 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/form_context.tsx @@ -6,7 +6,6 @@ */ import React, { useCallback, useEffect, useMemo } from 'react'; -import { noop } from 'lodash/fp'; import { schema, FormProps } from './schema'; import { Form, useForm } from '../../../shared_imports'; import { @@ -38,7 +37,7 @@ export const FormContext: React.FC = ({ children, onSuccess }) => { const { connectors } = useConnectors(); const { connector: configurationConnector } = useCaseConfigure(); const { postCase } = usePostCase(); - const { postPushToService } = usePostPushToService(); + const { pushCaseToExternalService } = usePostPushToService(); const connectorId = useMemo( () => @@ -67,12 +66,9 @@ export const FormContext: React.FC = ({ children, onSuccess }) => { }); if (updatedCase?.id && dataConnectorId !== 'none') { - await postPushToService({ + await pushCaseToExternalService({ caseId: updatedCase.id, - caseServices: {}, connector: connectorToUpdate, - alerts: {}, - updateCase: noop, }); } @@ -81,7 +77,7 @@ export const FormContext: React.FC = ({ children, onSuccess }) => { } } }, - [connectors, postCase, onSuccess, postPushToService] + [connectors, postCase, onSuccess, pushCaseToExternalService] ); const { form } = useForm({ diff --git a/x-pack/plugins/security_solution/public/cases/components/create/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/create/index.test.tsx index f43aecdc123a6..7172d227f492e 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/create/index.test.tsx @@ -16,10 +16,10 @@ import { useGetTags } from '../../containers/use_get_tags'; import { useConnectors } from '../../containers/configure/use_connectors'; import { useCaseConfigure } from '../../containers/configure/use_configure'; import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router'; -import { useGetIncidentTypes } from '../settings/resilient/use_get_incident_types'; -import { useGetSeverity } from '../settings/resilient/use_get_severity'; -import { useGetIssueTypes } from '../settings/jira/use_get_issue_types'; -import { useGetFieldsByIssueType } from '../settings/jira/use_get_fields_by_issue_type'; +import { useGetIncidentTypes } from '../connectors/resilient/use_get_incident_types'; +import { useGetSeverity } from '../connectors/resilient/use_get_severity'; +import { useGetIssueTypes } from '../connectors/jira/use_get_issue_types'; +import { useGetFieldsByIssueType } from '../connectors/jira/use_get_fields_by_issue_type'; import { useCaseConfigureResponse } from '../configure_cases/__mock__'; import { useInsertTimeline } from '../use_insert_timeline'; import { @@ -37,12 +37,12 @@ jest.mock('../../containers/api'); jest.mock('../../containers/use_get_tags'); jest.mock('../../containers/configure/use_connectors'); jest.mock('../../containers/configure/use_configure'); -jest.mock('../settings/resilient/use_get_incident_types'); -jest.mock('../settings/resilient/use_get_severity'); -jest.mock('../settings/jira/use_get_issue_types'); -jest.mock('../settings/jira/use_get_fields_by_issue_type'); -jest.mock('../settings/jira/use_get_single_issue'); -jest.mock('../settings/jira/use_get_issues'); +jest.mock('../connectors/resilient/use_get_incident_types'); +jest.mock('../connectors/resilient/use_get_severity'); +jest.mock('../connectors/jira/use_get_issue_types'); +jest.mock('../connectors/jira/use_get_fields_by_issue_type'); +jest.mock('../connectors/jira/use_get_single_issue'); +jest.mock('../connectors/jira/use_get_issues'); jest.mock('../use_insert_timeline'); const useConnectorsMock = useConnectors as jest.Mock; diff --git a/x-pack/plugins/security_solution/public/cases/components/edit_connector/index.tsx b/x-pack/plugins/security_solution/public/cases/components/edit_connector/index.tsx index 21a87e3a64ac0..34dcacaf42a98 100644 --- a/x-pack/plugins/security_solution/public/cases/components/edit_connector/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/edit_connector/index.tsx @@ -23,8 +23,8 @@ import { noop } from 'lodash/fp'; import { Form, UseField, useForm } from '../../../shared_imports'; import { ConnectorTypeFields } from '../../../../../case/common/api/connectors'; import { ConnectorSelector } from '../connector_selector/form'; -import { ActionConnector } from '../../../../../case/common/api/cases'; -import { SettingFieldsForm } from '../settings/fields_form'; +import { ActionConnector } from '../../../../../case/common/api'; +import { ConnectorFieldsForm } from '../connectors/fields_form'; import { getConnectorById } from '../configure_cases/utils'; import { CaseUserActions } from '../../containers/types'; import { schema } from './schema'; @@ -244,7 +244,7 @@ export const EditConnector = React.memo( - + {(currentConnector == null || currentConnector?.id === 'none') && // Connector is none or not defined. !(currentConnector === null && selectedConnector !== 'none') && // Connector has not been deleted. !editConnector && ( @@ -252,7 +252,7 @@ export const EditConnector = React.memo( {i18n.NO_CONNECTOR} )} - (getJiraCaseSetting()); - this.caseSettingsRegistry.register(getResilientCaseSetting()); - this.caseSettingsRegistry.register(getServiceNowCaseSetting()); - } - - registry(): CaseSettingsRegistry { - return this.caseSettingsRegistry; - } -} - -const caseSettings = new CaseSettings(); - -export const getCaseSettings = (): GetCaseSettingReturn => { - return { - caseSettingsRegistry: caseSettings.registry(), - }; -}; diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/mock.ts b/x-pack/plugins/security_solution/public/cases/components/settings/mock.ts deleted file mode 100644 index 69f30b488d9a6..0000000000000 --- a/x-pack/plugins/security_solution/public/cases/components/settings/mock.ts +++ /dev/null @@ -1,21 +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. - */ - -export const connector = { - id: '123', - name: 'My connector', - actionTypeId: '.jira', - config: {}, - isPreconfigured: false, -}; -export const issues = [ - { id: 'personId', title: 'Person Task', key: 'personKey' }, - { id: 'womanId', title: 'Woman Task', key: 'womanKey' }, - { id: 'manId', title: 'Man Task', key: 'manKey' }, - { id: 'cameraId', title: 'Camera Task', key: 'cameraKey' }, - { id: 'tvId', title: 'TV Task', key: 'tvKey' }, -]; diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/servicenow/fields.tsx b/x-pack/plugins/security_solution/public/cases/components/settings/servicenow/fields.tsx deleted file mode 100644 index 161e4d44cd572..0000000000000 --- a/x-pack/plugins/security_solution/public/cases/components/settings/servicenow/fields.tsx +++ /dev/null @@ -1,130 +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 React, { useCallback, useEffect, useMemo } from 'react'; -import { EuiFormRow, EuiSelect, EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import * as i18n from './translations'; - -import { SettingFieldsProps } from '../types'; -import { ConnectorTypes, ServiceNowFieldsType } from '../../../../../../case/common/api/connectors'; -import { ConnectorCard } from '../card'; - -const selectOptions = [ - { - value: '1', - text: i18n.SEVERITY_HIGH, - }, - { - value: '2', - text: i18n.SEVERITY_MEDIUM, - }, - { - value: '3', - text: i18n.SEVERITY_LOW, - }, -]; - -const ServiceNowSettingFieldsComponent: React.FunctionComponent< - SettingFieldsProps -> = ({ isEdit = true, fields, connector, onChange }) => { - const { severity = null, urgency = null, impact = null } = fields ?? {}; - - const listItems = useMemo( - () => [ - ...(urgency != null && urgency.length > 0 - ? [ - { - title: i18n.URGENCY, - description: selectOptions.find((option) => `${option.value}` === urgency)?.text, - }, - ] - : []), - ...(severity != null && severity.length > 0 - ? [ - { - title: i18n.SEVERITY, - description: selectOptions.find((option) => `${option.value}` === severity)?.text, - }, - ] - : []), - ...(impact != null && impact.length > 0 - ? [ - { - title: i18n.IMPACT, - description: selectOptions.find((option) => `${option.value}` === impact)?.text, - }, - ] - : []), - ], - [urgency, severity, impact] - ); - - // We need to set them up at initialization - useEffect(() => { - onChange({ impact, severity, urgency }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const onChangeCb = useCallback( - (key: keyof ServiceNowFieldsType, value: ServiceNowFieldsType[keyof ServiceNowFieldsType]) => { - onChange({ ...fields, [key]: value }); - }, - [fields, onChange] - ); - - return isEdit ? ( - - - onChangeCb('urgency', e.target.value)} - /> - - - - - - onChangeCb('severity', e.target.value)} - /> - - - - - onChangeCb('impact', e.target.value)} - /> - - - - - ) : ( - - ); -}; - -// eslint-disable-next-line import/no-default-export -export { ServiceNowSettingFieldsComponent as default }; diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/servicenow/index.ts b/x-pack/plugins/security_solution/public/cases/components/settings/servicenow/index.ts deleted file mode 100644 index 70d1bf89ce7c8..0000000000000 --- a/x-pack/plugins/security_solution/public/cases/components/settings/servicenow/index.ts +++ /dev/null @@ -1,25 +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 { lazy } from 'react'; - -import { CaseSetting } from '../types'; -import { ServiceNowFieldsType } from '../../../../../../case/common/api/connectors'; -import * as i18n from './translations'; - -export const getCaseSetting = (): CaseSetting => { - return { - id: '.servicenow', - caseSettingFieldsComponent: lazy(() => import('./fields')), - }; -}; - -export const fieldLabels = { - impact: i18n.IMPACT, - severity: i18n.SEVERITY, - urgency: i18n.URGENCY, -}; diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/servicenow/translations.ts b/x-pack/plugins/security_solution/public/cases/components/settings/servicenow/translations.ts deleted file mode 100644 index 6db239541851e..0000000000000 --- a/x-pack/plugins/security_solution/public/cases/components/settings/servicenow/translations.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 { i18n } from '@kbn/i18n'; - -export const SEVERITY_HIGH = i18n.translate( - 'xpack.securitySolution.components.settings.servicenow.severitySelectHighOptionLabel', - { - defaultMessage: 'High', - } -); -export const SEVERITY_MEDIUM = i18n.translate( - 'xpack.securitySolution.components.settings.servicenow.severitySelectMediumOptionLabel', - { - defaultMessage: 'Medium', - } -); - -export const SEVERITY_LOW = i18n.translate( - 'xpack.securitySolution.components.settings.servicenow.severitySelectLowOptionLabel', - { - defaultMessage: 'Low', - } -); - -export const URGENCY = i18n.translate( - 'xpack.securitySolution.components.settings.serviceNow.urgencySelectFieldLabel', - { - defaultMessage: 'Urgency', - } -); - -export const SEVERITY = i18n.translate( - 'xpack.securitySolution.components.settings.serviceNow.severitySelectFieldLabel', - { - defaultMessage: 'Severity', - } -); - -export const IMPACT = i18n.translate( - 'xpack.securitySolution.components.settings.serviceNow.impactSelectFieldLabel', - { - defaultMessage: 'Impact', - } -); diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/settings_registry.ts b/x-pack/plugins/security_solution/public/cases/components/settings/settings_registry.ts deleted file mode 100644 index a5580aaf587b2..0000000000000 --- a/x-pack/plugins/security_solution/public/cases/components/settings/settings_registry.ts +++ /dev/null @@ -1,57 +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'; -import { CaseSetting, CaseSettingsRegistry } from './types'; - -/* eslint-disable @typescript-eslint/no-explicit-any */ - -export const createCaseSettingsRegistry = (): CaseSettingsRegistry => { - const settings: Map> = new Map(); - - const registry: CaseSettingsRegistry = { - has: (id: string) => settings.has(id), - register: (setting: CaseSetting) => { - if (settings.has(setting.id)) { - throw new Error( - i18n.translate( - 'xpack.securitySolution.caseSettingsRegistry.register.duplicateCaseSettingErrorMessage', - { - defaultMessage: 'Object type "{id}" is already registered.', - values: { - id: setting.id, - }, - } - ) - ); - } - - settings.set(setting.id, setting); - }, - get: (id: string): CaseSetting => { - if (!settings.has(id)) { - throw new Error( - i18n.translate( - 'xpack.securitySolution.caseSettingsRegistry.get.missingCaseSettingErrorMessage', - { - defaultMessage: 'Object type "{id}" is not registered.', - values: { - id, - }, - } - ) - ); - } - return settings.get(id)!; - }, - list: () => { - return Array.from(settings).map(([id, setting]) => setting); - }, - }; - - return registry; -}; diff --git a/x-pack/plugins/security_solution/public/cases/components/settings/types.ts b/x-pack/plugins/security_solution/public/cases/components/settings/types.ts deleted file mode 100644 index 9f212b1999e3d..0000000000000 --- a/x-pack/plugins/security_solution/public/cases/components/settings/types.ts +++ /dev/null @@ -1,33 +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 React from 'react'; -import { ActionConnector } from '../../../../../case/common/api'; - -import { ConnectorTypeFields } from '../../../../../case/common/api/connectors'; -export type CaseSettingsConnector = ActionConnector; - -export interface CaseSetting { - id: string; - caseSettingFieldsComponent: React.LazyExoticComponent< - React.ComponentType> - > | null; -} - -export interface CaseSettingsRegistry { - has: (id: string) => boolean; - register: (setting: CaseSetting) => void; - get: (id: string) => CaseSetting; - list: () => CaseSetting[]; -} - -export interface SettingFieldsProps { - isEdit?: boolean; - connector: CaseSettingsConnector; - fields: TFields; - onChange: (fields: TFields) => void; -} diff --git a/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.test.tsx b/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.test.tsx index 63838b1bc6b8d..b8048afb083f1 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.test.tsx @@ -39,10 +39,10 @@ jest.mock('../../containers/configure/api'); describe('usePushToService', () => { const caseId = '12345'; const updateCase = jest.fn(); - const postPushToService = jest.fn(); + const pushCaseToExternalService = jest.fn(); const mockPostPush = { isLoading: false, - postPushToService, + pushCaseToExternalService, }; const mockConnector = connectorsMock[0]; @@ -61,7 +61,7 @@ describe('usePushToService', () => { connector: { id: mockConnector.id, name: mockConnector.name, - type: ConnectorTypes.servicenow, + type: ConnectorTypes.serviceNowITSM, fields: null, }, caseId, @@ -71,19 +71,6 @@ describe('usePushToService', () => { updateCase, userCanCrud: true, isValidConnector: true, - alerts: { - 'alert-id-1': { - _id: 'alert-id-1', - _index: 'alert-index-1', - '@timestamp': '2020-11-20T15:35:28.373Z', - rule: { - id: 'rule-id-1', - name: 'Awesome rule', - from: 'now-360s', - to: 'now', - }, - }, - }, }; beforeEach(() => { @@ -105,28 +92,13 @@ describe('usePushToService', () => { ); await waitForNextUpdate(); result.current.pushButton.props.children.props.onClick(); - expect(postPushToService).toBeCalledWith({ + expect(pushCaseToExternalService).toBeCalledWith({ caseId, - caseServices, connector: { fields: null, id: 'servicenow-1', name: 'My Connector', - type: ConnectorTypes.servicenow, - }, - updateCase, - alerts: { - 'alert-id-1': { - _id: 'alert-id-1', - _index: 'alert-index-1', - '@timestamp': '2020-11-20T15:35:28.373Z', - rule: { - id: 'rule-id-1', - name: 'Awesome rule', - from: 'now-360s', - to: 'now', - }, - }, + type: ConnectorTypes.serviceNowITSM, }, }); expect(result.current.pushCallouts).toBeNull(); diff --git a/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.tsx b/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.tsx index ed03ce36bf26c..21067a3e69969 100644 --- a/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/use_push_to_service/index.tsx @@ -22,7 +22,6 @@ import { CaseServices } from '../../containers/use_get_case_user_actions'; import { LinkAnchor } from '../../../common/components/links'; import { SecurityPageName } from '../../../app/types'; import { ErrorMessage } from '../callout/types'; -import { Alert } from '../case_view'; export interface UsePushToService { caseId: string; @@ -33,7 +32,6 @@ export interface UsePushToService { updateCase: (newCase: Case) => void; userCanCrud: boolean; isValidConnector: boolean; - alerts: Record; } export interface ReturnUsePushToService { @@ -50,25 +48,25 @@ export const usePushToService = ({ updateCase, userCanCrud, isValidConnector, - alerts, }: UsePushToService): ReturnUsePushToService => { const history = useHistory(); const { formatUrl, search: urlSearch } = useFormatUrl(SecurityPageName.case); - const { isLoading, postPushToService } = usePostPushToService(); + const { isLoading, pushCaseToExternalService } = usePostPushToService(); const { isLoading: loadingLicense, actionLicense } = useGetActionLicense(); - const handlePushToService = useCallback(() => { + const handlePushToService = useCallback(async () => { if (connector.id != null && connector.id !== 'none') { - postPushToService({ + const theCase = await pushCaseToExternalService({ caseId, - caseServices, connector, - updateCase, - alerts, }); + + if (theCase != null) { + updateCase(theCase); + } } - }, [alerts, caseId, caseServices, connector, postPushToService, updateCase]); + }, [caseId, connector, pushCaseToExternalService, updateCase]); const goToConfigureCases = useCallback( (ev) => { diff --git a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx index c5d7610aed9ba..4a567a38dc9f2 100644 --- a/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/user_action_tree/index.tsx @@ -24,7 +24,7 @@ import { Case, CaseUserActions } from '../../containers/types'; import { useUpdateComment } from '../../containers/use_update_comment'; import { useCurrentUser } from '../../../common/lib/kibana'; import { AddComment, AddCommentRefObject } from '../add_comment'; -import { ActionConnector, CommentType } from '../../../../../case/common/api/cases'; +import { ActionConnector, CommentType } from '../../../../../case/common/api'; import { CaseServices } from '../../containers/use_get_case_user_actions'; import { parseString } from '../../containers/utils'; import { Alert, OnUpdateFields } from '../case_view'; diff --git a/x-pack/plugins/security_solution/public/cases/containers/__mocks__/api.ts b/x-pack/plugins/security_solution/public/cases/containers/__mocks__/api.ts index 13b9bc670a4fd..ab761309fa6ad 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/__mocks__/api.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/__mocks__/api.ts @@ -25,16 +25,12 @@ import { caseUserActions, pushedCase, respReporters, - serviceConnector, tags, } from '../mock'; import { - CaseExternalServiceRequest, CasePatchRequest, CasePostRequest, CommentRequest, - ServiceConnectorCaseParams, - ServiceConnectorCaseResponse, User, CaseStatuses, } from '../../../../../case/common/api'; @@ -110,15 +106,9 @@ export const deleteCases = async (caseIds: string[], signal: AbortSignal): Promi export const pushCase = async ( caseId: string, - push: CaseExternalServiceRequest, - signal: AbortSignal -): Promise => Promise.resolve(pushedCase); - -export const pushToService = async ( connectorId: string, - casePushParams: ServiceConnectorCaseParams, signal: AbortSignal -): Promise => Promise.resolve(serviceConnector); +): Promise => Promise.resolve(pushedCase); export const getActionLicense = async (signal: AbortSignal): Promise => Promise.resolve(actionLicenses); diff --git a/x-pack/plugins/security_solution/public/cases/containers/api.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/api.test.tsx index b3e92f24ce2b3..ee63749b49435 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/api.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/api.test.tsx @@ -25,7 +25,6 @@ import { postCase, postComment, pushCase, - pushToService, } from './api'; import { @@ -34,26 +33,20 @@ import { basicCase, allCasesSnake, basicCaseSnake, - actionTypeExecutorResult, pushedCaseSnake, casesStatus, casesSnake, cases, caseUserActions, pushedCase, - pushSnake, reporters, respReporters, - serviceConnector, - casePushParams, tags, caseUserActionsSnake, casesStatusSnake, } from './mock'; import { DEFAULT_FILTER_OPTIONS, DEFAULT_QUERY_PARAMS } from './use_get_cases'; -import * as i18n from './translations'; -import { getCaseConfigurePushUrl } from '../../../../case/common/api/helpers'; const abortCtrl = new AbortController(); const mockKibanaServices = KibanaServices.get as jest.Mock; @@ -84,11 +77,13 @@ describe('Case Configuration API', () => { expect(resp).toEqual(''); }); }); + describe('getActionLicense', () => { beforeEach(() => { fetchMock.mockClear(); fetchMock.mockResolvedValue(actionLicenses); }); + test('check url, method, signal', async () => { await getActionLicense(abortCtrl.signal); expect(fetchMock).toHaveBeenCalledWith(`/api/actions/list_action_types`, { @@ -102,6 +97,7 @@ describe('Case Configuration API', () => { expect(resp).toEqual(actionLicenses); }); }); + describe('getCase', () => { beforeEach(() => { fetchMock.mockClear(); @@ -123,6 +119,7 @@ describe('Case Configuration API', () => { expect(resp).toEqual(basicCase); }); }); + describe('getCases', () => { beforeEach(() => { fetchMock.mockClear(); @@ -145,6 +142,7 @@ describe('Case Configuration API', () => { signal: abortCtrl.signal, }); }); + test('correctly applies filters', async () => { await getCases({ filterOptions: { @@ -169,6 +167,7 @@ describe('Case Configuration API', () => { signal: abortCtrl.signal, }); }); + test('tags with weird chars get handled gracefully', async () => { const weirdTags: string[] = ['(', '"double"']; @@ -205,6 +204,7 @@ describe('Case Configuration API', () => { expect(resp).toEqual({ ...allCases }); }); }); + describe('getCasesStatus', () => { beforeEach(() => { fetchMock.mockClear(); @@ -223,6 +223,7 @@ describe('Case Configuration API', () => { expect(resp).toEqual(casesStatus); }); }); + describe('getCaseUserActions', () => { beforeEach(() => { fetchMock.mockClear(); @@ -242,6 +243,7 @@ describe('Case Configuration API', () => { expect(resp).toEqual(caseUserActions); }); }); + describe('getReporters', () => { beforeEach(() => { fetchMock.mockClear(); @@ -261,6 +263,7 @@ describe('Case Configuration API', () => { expect(resp).toEqual(respReporters); }); }); + describe('getTags', () => { beforeEach(() => { fetchMock.mockClear(); @@ -280,6 +283,7 @@ describe('Case Configuration API', () => { expect(resp).toEqual(tags); }); }); + describe('patchCase', () => { beforeEach(() => { fetchMock.mockClear(); @@ -307,6 +311,7 @@ describe('Case Configuration API', () => { expect(resp).toEqual({ ...[basicCase] }); }); }); + describe('patchCasesStatus', () => { beforeEach(() => { fetchMock.mockClear(); @@ -334,6 +339,7 @@ describe('Case Configuration API', () => { expect(resp).toEqual({ ...cases }); }); }); + describe('patchComment', () => { beforeEach(() => { fetchMock.mockClear(); @@ -371,6 +377,7 @@ describe('Case Configuration API', () => { expect(resp).toEqual(basicCase); }); }); + describe('postCase', () => { beforeEach(() => { fetchMock.mockClear(); @@ -405,6 +412,7 @@ describe('Case Configuration API', () => { expect(resp).toEqual(basicCase); }); }); + describe('postComment', () => { beforeEach(() => { fetchMock.mockClear(); @@ -429,88 +437,30 @@ describe('Case Configuration API', () => { expect(resp).toEqual(basicCase); }); }); + describe('pushCase', () => { + const connectorId = 'connectorId'; + beforeEach(() => { fetchMock.mockClear(); fetchMock.mockResolvedValue(pushedCaseSnake); }); test('check url, method, signal', async () => { - await pushCase(basicCase.id, pushSnake, abortCtrl.signal); - expect(fetchMock).toHaveBeenCalledWith(`${CASES_URL}/${basicCase.id}/_push`, { - method: 'POST', - body: JSON.stringify(pushSnake), - signal: abortCtrl.signal, - }); + await pushCase(basicCase.id, connectorId, abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith( + `${CASES_URL}/${basicCase.id}/connector/${connectorId}/_push`, + { + method: 'POST', + body: JSON.stringify({}), + signal: abortCtrl.signal, + } + ); }); test('happy path', async () => { - const resp = await pushCase(basicCase.id, pushSnake, abortCtrl.signal); + const resp = await pushCase(basicCase.id, connectorId, abortCtrl.signal); expect(resp).toEqual(pushedCase); }); }); - describe('pushToService', () => { - beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(actionTypeExecutorResult); - }); - const connectorId = 'connectorId'; - test('check url, method, signal', async () => { - await pushToService(connectorId, ConnectorTypes.jira, casePushParams, abortCtrl.signal); - expect(fetchMock).toHaveBeenCalledWith(`${getCaseConfigurePushUrl(connectorId)}`, { - method: 'POST', - body: JSON.stringify({ - connector_type: ConnectorTypes.jira, - params: casePushParams, - }), - signal: abortCtrl.signal, - }); - }); - - test('happy path', async () => { - const resp = await pushToService( - connectorId, - ConnectorTypes.jira, - casePushParams, - abortCtrl.signal - ); - expect(resp).toEqual(serviceConnector); - }); - - test('unhappy path - serviceMessage', async () => { - const theError = 'the error'; - fetchMock.mockResolvedValue({ - ...actionTypeExecutorResult, - status: 'error', - serviceMessage: theError, - message: 'not it', - }); - await expect( - pushToService(connectorId, ConnectorTypes.jira, casePushParams, abortCtrl.signal) - ).rejects.toMatchObject({ message: theError }); - }); - - test('unhappy path - message', async () => { - const theError = 'the error'; - fetchMock.mockResolvedValue({ - ...actionTypeExecutorResult, - status: 'error', - message: theError, - }); - await expect( - pushToService(connectorId, ConnectorTypes.jira, casePushParams, abortCtrl.signal) - ).rejects.toMatchObject({ message: theError }); - }); - - test('unhappy path - no message', async () => { - const theError = i18n.ERROR_PUSH_TO_SERVICE; - fetchMock.mockResolvedValue({ - ...actionTypeExecutorResult, - status: 'error', - }); - await expect( - pushToService(connectorId, ConnectorTypes.jira, casePushParams, abortCtrl.signal) - ).rejects.toMatchObject({ message: theError }); - }); - }); }); diff --git a/x-pack/plugins/security_solution/public/cases/containers/api.ts b/x-pack/plugins/security_solution/public/cases/containers/api.ts index 22e6c92da8ceb..00a45aadd2ae0 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/api.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/api.ts @@ -6,7 +6,6 @@ */ import { - CaseExternalServiceRequest, CasePatchRequest, CasePostRequest, CaseResponse, @@ -17,8 +16,6 @@ import { CaseUserActionsResponse, CommentRequest, CommentType, - ServiceConnectorCaseParams, - ServiceConnectorCaseResponse, User, } from '../../../../case/common/api'; @@ -32,7 +29,7 @@ import { import { getCaseCommentsUrl, - getCaseConfigurePushUrl, + getCasePushUrl, getCaseDetailsUrl, getCaseUserActionUrl, } from '../../../../case/common/api/helpers'; @@ -59,10 +56,8 @@ import { decodeCasesFindResponse, decodeCasesStatusResponse, decodeCaseUserActionsResponse, - decodeServiceConnectorCaseResponse, } from './utils'; -import * as i18n from './translations'; -import { ActionTypeExecutorResult } from '../../../../actions/common'; + export const getCase = async ( caseId: string, includeComments: boolean = true, @@ -231,41 +226,19 @@ export const deleteCases = async (caseIds: string[], signal: AbortSignal): Promi export const pushCase = async ( caseId: string, - push: CaseExternalServiceRequest, + connectorId: string, signal: AbortSignal ): Promise => { const response = await KibanaServices.get().http.fetch( - `${getCaseDetailsUrl(caseId)}/_push`, + getCasePushUrl(caseId, connectorId), { method: 'POST', - body: JSON.stringify(push), + body: JSON.stringify({}), signal, } ); - return convertToCamelCase(decodeCaseResponse(response)); -}; -export const pushToService = async ( - connectorId: string, - connectorType: string, - casePushParams: ServiceConnectorCaseParams, - signal: AbortSignal -): Promise => { - const response = await KibanaServices.get().http.fetch< - ActionTypeExecutorResult> - >(`${getCaseConfigurePushUrl(connectorId)}`, { - method: 'POST', - body: JSON.stringify({ - connector_type: connectorType, - params: casePushParams, - }), - signal, - }); - - if (response.status === 'error') { - throw new Error(response.serviceMessage ?? response.message ?? i18n.ERROR_PUSH_TO_SERVICE); - } - return decodeServiceConnectorCaseResponse(response.data); + return convertToCamelCase(decodeCaseResponse(response)); }; export const getActionLicense = async (signal: AbortSignal): Promise => { diff --git a/x-pack/plugins/security_solution/public/cases/containers/mock.ts b/x-pack/plugins/security_solution/public/cases/containers/mock.ts index 06983a92b9ea1..444a87a57d251 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/mock.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/mock.ts @@ -9,7 +9,6 @@ import { ActionLicense, AllCases, Case, CasesStatus, CaseUserActions, Comment } import { CommentResponse, - ServiceConnectorCaseResponse, CaseStatuses, UserAction, UserActionField, @@ -29,17 +28,13 @@ const basicCommentId = 'basic-comment-id'; const basicCreatedAt = '2020-02-19T23:06:33.798Z'; const basicUpdatedAt = '2020-02-20T15:02:57.995Z'; const laterTime = '2020-02-28T15:02:57.995Z'; + export const elasticUser = { fullName: 'Leslie Knope', username: 'lknope', email: 'leslie.knope@elastic.co', }; -export const serviceConnectorUser = { - fullName: 'Leslie Knope', - username: 'lknope', -}; - export const tags: string[] = ['coke', 'pepsi']; export const basicComment: Comment = { @@ -136,19 +131,6 @@ export const pushedCase: Case = { externalService: basicPush, }; -export const serviceConnector: ServiceConnectorCaseResponse = { - title: '123', - id: '444', - pushedDate: basicUpdatedAt, - url: 'connector.com', - comments: [ - { - commentId: basicCommentId, - pushedDate: basicUpdatedAt, - }, - ], -}; - const basicAction = { actionAt: basicCreatedAt, actionBy: elasticUser, @@ -158,25 +140,6 @@ const basicAction = { commentId: null, }; -export const casePushParams = { - savedObjectId: basicCaseId, - createdAt: basicCreatedAt, - createdBy: elasticUser, - externalId: null, - title: 'what a cool value', - commentId: null, - updatedAt: basicCreatedAt, - updatedBy: elasticUser, - description: 'nice', - comments: null, -}; - -export const actionTypeExecutorResult = { - actionId: 'string', - status: 'ok', - data: serviceConnector, -}; - export const cases: Case[] = [ basicCase, { ...pushedCase, id: '1', totalComment: 0, comments: [] }, @@ -192,6 +155,7 @@ export const allCases: AllCases = { total: 10, ...casesStatus, }; + export const actionLicenses: ActionLicense[] = [ { id: '.servicenow', @@ -215,6 +179,7 @@ export const elasticUserSnake = { username: 'lknope', email: 'leslie.knope@elastic.co', }; + export const basicCommentSnake: CommentResponse = { comment: 'Solve this fast!', type: CommentType.user, @@ -260,11 +225,13 @@ export const pushSnake = { external_title: 'external title', external_url: 'basicPush.com', }; + export const basicPushSnake = { ...pushSnake, pushed_at: basicUpdatedAt, pushed_by: elasticUserSnake, }; + export const pushedCaseSnake = { ...basicCaseSnake, external_service: basicPushSnake, diff --git a/x-pack/plugins/security_solution/public/cases/containers/translations.ts b/x-pack/plugins/security_solution/public/cases/containers/translations.ts index 9525d125435e7..75939b46b1f77 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/translations.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/translations.ts @@ -62,13 +62,6 @@ export const SUCCESS_SEND_TO_EXTERNAL_SERVICE = (serviceName: string) => defaultMessage: 'Successfully sent to { serviceName }', }); -export const ERROR_PUSH_TO_SERVICE = i18n.translate( - 'xpack.securitySolution.case.configure.errorPushingToService', - { - defaultMessage: 'Error pushing to service', - } -); - export const ERROR_GET_FIELDS = i18n.translate( 'xpack.securitySolution.case.configure.errorGetFields', { diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.test.tsx index 8845e285ee910..5f09ac404ca64 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.test.tsx @@ -6,112 +6,22 @@ */ import { renderHook, act } from '@testing-library/react-hooks'; -import { - formatServiceRequestData, - usePostPushToService, - UsePostPushToService, -} from './use_post_push_to_service'; -import { - basicCase, - basicComment, - basicPush, - pushedCase, - serviceConnector, - serviceConnectorUser, -} from './mock'; +import { usePostPushToService, UsePostPushToService } from './use_post_push_to_service'; +import { pushedCase } from './mock'; import * as api from './api'; -import { CaseServices } from './use_get_case_user_actions'; -import { CaseConnector, ConnectorTypes, CommentType } from '../../../../case/common/api'; -import moment from 'moment'; +import { CaseConnector, ConnectorTypes } from '../../../../case/common/api'; + jest.mock('./api'); -jest.mock('../../common/components/link_to', () => { - const originalModule = jest.requireActual('../../common/components/link_to'); - return { - ...originalModule, - getTimelineTabsUrl: jest.fn(), - useFormatUrl: jest.fn().mockReturnValue({ formatUrl: jest.fn(), search: 'urlSearch' }), - }; -}); + describe('usePostPushToService', () => { const abortCtrl = new AbortController(); - const updateCase = jest.fn(); - const formatUrl = jest.fn(); - - const samplePush = { - caseId: pushedCase.id, - caseServices: { - '123': { - ...basicPush, - firstPushIndex: 1, - lastPushIndex: 1, - commentsToUpdate: [basicComment.id], - hasDataToPush: false, - }, - }, - connector: { - id: '123', - name: 'connector name', - type: ConnectorTypes.jira, - fields: { issueType: 'Task', priority: 'Low', parent: null }, - } as CaseConnector, - updateCase, - alerts: { - 'alert-id-1': { - _id: 'alert-id-1', - _index: 'alert-index-1', - '@timestamp': '2020-11-20T15:35:28.373Z', - rule: { - id: 'rule-id-1', - name: 'Awesome rule', - from: 'now-360s', - to: 'now', - }, - }, - }, - }; - - const sampleServiceRequestData = { - savedObjectId: pushedCase.id, - createdAt: pushedCase.createdAt, - createdBy: serviceConnectorUser, - comments: [ - { - commentId: basicComment.id, - comment: basicComment.type === CommentType.user ? basicComment.comment : '', - createdAt: basicComment.createdAt, - createdBy: serviceConnectorUser, - updatedAt: null, - updatedBy: null, - }, - ], - externalId: basicPush.externalId, - description: pushedCase.description, - title: pushedCase.title, - updatedAt: pushedCase.updatedAt, - updatedBy: serviceConnectorUser, - issueType: 'Task', - parent: null, - priority: 'Low', - }; - - const sampleCaseServices = { - '123': { - ...basicPush, - firstPushIndex: 1, - lastPushIndex: 1, - commentsToUpdate: [basicComment.id], - hasDataToPush: true, - }, - '456': { - ...basicPush, - connectorId: '456', - externalId: 'other_external_id', - firstPushIndex: 4, - commentsToUpdate: [basicComment.id], - lastPushIndex: 6, - hasDataToPush: false, - }, - }; + const connector = { + id: '123', + name: 'connector name', + type: ConnectorTypes.jira, + fields: { issueType: 'Task', priority: 'Low', parent: null }, + } as CaseConnector; + const caseId = pushedCase.id; it('init', async () => { await act(async () => { @@ -120,98 +30,24 @@ describe('usePostPushToService', () => { ); await waitForNextUpdate(); expect(result.current).toEqual({ - serviceData: null, - pushedCaseData: null, isLoading: false, isError: false, - postPushToService: result.current.postPushToService, + pushCaseToExternalService: result.current.pushCaseToExternalService, }); }); }); it('calls pushCase with correct arguments', async () => { - const spyOnPushCase = jest.spyOn(api, 'pushCase'); - - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - usePostPushToService() - ); - await waitForNextUpdate(); - result.current.postPushToService(samplePush); - await waitForNextUpdate(); - expect(spyOnPushCase).toBeCalledWith( - samplePush.caseId, - { - connector_id: samplePush.connector.id, - connector_name: samplePush.connector.name, - external_id: serviceConnector.id, - external_title: serviceConnector.title, - external_url: serviceConnector.url, - }, - abortCtrl.signal - ); - }); - }); - - it('calls pushToService with correct arguments', async () => { - const spyOnPushToService = jest.spyOn(api, 'pushToService'); - - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - usePostPushToService() - ); - await waitForNextUpdate(); - result.current.postPushToService(samplePush); - await waitForNextUpdate(); - expect(spyOnPushToService).toBeCalledWith( - samplePush.connector.id, - samplePush.connector.type, - formatServiceRequestData({ - myCase: basicCase, - connector: samplePush.connector, - caseServices: sampleCaseServices as CaseServices, - alerts: samplePush.alerts, - formatUrl, - }), - abortCtrl.signal - ); - }); - }); - - it('calls pushToService with correct arguments when no push history', async () => { - const samplePush2 = { - caseId: pushedCase.id, - caseServices: {}, - connector: { - name: 'connector name', - id: 'none', - type: ConnectorTypes.none, - fields: null, - }, - alerts: samplePush.alerts, - updateCase, - }; - const spyOnPushToService = jest.spyOn(api, 'pushToService'); + const spyOnPushToService = jest.spyOn(api, 'pushCase'); await act(async () => { const { result, waitForNextUpdate } = renderHook(() => usePostPushToService() ); await waitForNextUpdate(); - result.current.postPushToService(samplePush2); + result.current.pushCaseToExternalService({ caseId, connector }); await waitForNextUpdate(); - expect(spyOnPushToService).toBeCalledWith( - samplePush2.connector.id, - samplePush2.connector.type, - formatServiceRequestData({ - myCase: basicCase, - connector: samplePush2.connector, - caseServices: {}, - alerts: samplePush.alerts, - formatUrl, - }), - abortCtrl.signal - ); + expect(spyOnPushToService).toBeCalledWith(caseId, connector.id, abortCtrl.signal); }); }); @@ -221,120 +57,29 @@ describe('usePostPushToService', () => { usePostPushToService() ); await waitForNextUpdate(); - result.current.postPushToService(samplePush); + result.current.pushCaseToExternalService({ caseId, connector }); await waitForNextUpdate(); expect(result.current).toEqual({ - serviceData: serviceConnector, - pushedCaseData: pushedCase, isLoading: false, isError: false, - postPushToService: result.current.postPushToService, + pushCaseToExternalService: result.current.pushCaseToExternalService, }); }); }); - it('set isLoading to true when deleting cases', async () => { + it('set isLoading to true when pushing case', async () => { await act(async () => { const { result, waitForNextUpdate } = renderHook(() => usePostPushToService() ); await waitForNextUpdate(); - result.current.postPushToService(samplePush); + result.current.pushCaseToExternalService({ caseId, connector }); expect(result.current.isLoading).toBe(true); }); }); - it('formatServiceRequestData - current connector', () => { - const caseServices = sampleCaseServices; - const result = formatServiceRequestData({ - myCase: pushedCase, - connector: samplePush.connector, - caseServices, - alerts: samplePush.alerts, - formatUrl, - }); - expect(result).toEqual(sampleServiceRequestData); - }); - - it('formatServiceRequestData - connector with history', () => { - const caseServices = sampleCaseServices; - const connector = { - id: '456', - name: 'connector 2', - type: ConnectorTypes.jira, - fields: { issueType: 'Task', priority: 'High', parent: 'RJ-01' }, - }; - const result = formatServiceRequestData({ - myCase: pushedCase, - connector: connector as CaseConnector, - caseServices, - alerts: samplePush.alerts, - formatUrl, - }); - expect(result).toEqual({ - ...sampleServiceRequestData, - ...connector.fields, - externalId: 'other_external_id', - }); - }); - - it('formatServiceRequestData - new connector', () => { - const caseServices = { - '123': sampleCaseServices['123'], - }; - - const connector = { - id: '456', - name: 'connector 2', - type: ConnectorTypes.jira, - fields: { issueType: 'Task', priority: 'High', parent: null }, - }; - - const result = formatServiceRequestData({ - myCase: pushedCase, - connector: connector as CaseConnector, - caseServices, - alerts: samplePush.alerts, - formatUrl, - }); - - expect(result).toEqual({ - ...sampleServiceRequestData, - ...connector.fields, - externalId: null, - }); - }); - - it('formatServiceRequestData - Alert comment content', () => { - const mockDuration = moment.duration(1); - jest.spyOn(moment, 'duration').mockReturnValue(mockDuration); - formatUrl.mockReturnValue('https://app.com/detections'); - const caseServices = sampleCaseServices; - const result = formatServiceRequestData({ - myCase: { - ...pushedCase, - comments: [ - { - ...pushedCase.comments[0], - type: CommentType.alert, - alertId: 'alert-id-1', - index: 'alert-index-1', - }, - ], - }, - connector: samplePush.connector, - caseServices, - alerts: samplePush.alerts, - formatUrl, - }); - - expect(result.comments![0].comment).toEqual( - '[Alert](https://app.com/detections?filters=!((%27$state%27:(store:appState),meta:(alias:!n,disabled:!f,key:_id,negate:!f,params:(query:alert-id-1),type:phrase),query:(match:(_id:(query:alert-id-1,type:phrase)))))&sourcerer=(default:!())&timerange=(global:(linkTo:!(timeline),timerange:(from:%272020-11-20T15:35:28.372Z%27,kind:absolute,to:%272020-11-20T15:35:28.373Z%27)),timeline:(linkTo:!(global),timerange:(from:%272020-11-20T15:35:28.372Z%27,kind:absolute,to:%272020-11-20T15:35:28.373Z%27)))) added to case.' - ); - }); - it('unhappy path', async () => { - const spyOnPushToService = jest.spyOn(api, 'pushToService'); + const spyOnPushToService = jest.spyOn(api, 'pushCase'); spyOnPushToService.mockImplementation(() => { throw new Error('Something went wrong'); }); @@ -344,15 +89,12 @@ describe('usePostPushToService', () => { usePostPushToService() ); await waitForNextUpdate(); - result.current.postPushToService(samplePush); - await waitForNextUpdate(); + result.current.pushCaseToExternalService({ caseId, connector }); expect(result.current).toEqual({ - serviceData: null, - pushedCaseData: null, isLoading: false, isError: true, - postPushToService: result.current.postPushToService, + pushCaseToExternalService: result.current.pushCaseToExternalService, }); }); }); diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.tsx index c5b4f52e73125..03d881d7934e9 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_post_push_to_service.tsx @@ -5,41 +5,23 @@ * 2.0. */ -import { useReducer, useCallback } from 'react'; -import moment from 'moment'; -import dateMath from '@elastic/datemath'; - -import { - ServiceConnectorCaseResponse, - ServiceConnectorCaseParams, - CaseConnector, - CommentType, -} from '../../../../case/common/api'; -import { SecurityPageName } from '../../app/types'; -import { useFormatUrl, FormatUrl, getRuleDetailsUrl } from '../../common/components/link_to'; +import { useReducer, useCallback, useRef, useEffect } from 'react'; +import { CaseConnector } from '../../../../case/common/api'; import { errorToToaster, useStateToaster, displaySuccessToast, } from '../../common/components/toasters'; -import { Alert } from '../components/case_view'; -import { getCase, pushToService, pushCase } from './api'; +import { pushCase } from './api'; import * as i18n from './translations'; -import { Case, Comment } from './types'; -import { CaseServices } from './use_get_case_user_actions'; +import { Case } from './types'; interface PushToServiceState { - serviceData: ServiceConnectorCaseResponse | null; - pushedCaseData: Case | null; isLoading: boolean; isError: boolean; } -type Action = - | { type: 'FETCH_INIT' } - | { type: 'FETCH_SUCCESS_PUSH_SERVICE'; payload: ServiceConnectorCaseResponse | null } - | { type: 'FETCH_SUCCESS_PUSH_CASE'; payload: Case | null } - | { type: 'FETCH_FAILURE' }; +type Action = { type: 'FETCH_INIT' } | { type: 'FETCH_SUCCESS' } | { type: 'FETCH_FAILURE' }; const dataFetchReducer = (state: PushToServiceState, action: Action): PushToServiceState => { switch (action.type) { @@ -49,19 +31,11 @@ const dataFetchReducer = (state: PushToServiceState, action: Action): PushToServ isLoading: true, isError: false, }; - case 'FETCH_SUCCESS_PUSH_SERVICE': - return { - ...state, - isLoading: false, - isError: false, - serviceData: action.payload ?? null, - }; - case 'FETCH_SUCCESS_PUSH_CASE': + case 'FETCH_SUCCESS': return { ...state, isLoading: false, isError: false, - pushedCaseData: action.payload ?? null, }; case 'FETCH_FAILURE': return { @@ -77,72 +51,45 @@ const dataFetchReducer = (state: PushToServiceState, action: Action): PushToServ interface PushToServiceRequest { caseId: string; connector: CaseConnector; - caseServices: CaseServices; - alerts: Record; - updateCase: (newCase: Case) => void; } export interface UsePostPushToService extends PushToServiceState { - postPushToService: ({ + pushCaseToExternalService: ({ caseId, - caseServices, connector, - alerts, - updateCase, - }: PushToServiceRequest) => void; + }: PushToServiceRequest) => Promise; } export const usePostPushToService = (): UsePostPushToService => { const [state, dispatch] = useReducer(dataFetchReducer, { - serviceData: null, - pushedCaseData: null, isLoading: false, isError: false, }); const [, dispatchToaster] = useStateToaster(); - const { formatUrl } = useFormatUrl(SecurityPageName.detections); + const cancel = useRef(false); + const abortCtrl = useRef(new AbortController()); - const postPushToService = useCallback( - async ({ caseId, caseServices, connector, alerts, updateCase }: PushToServiceRequest) => { - let cancel = false; - const abortCtrl = new AbortController(); + const pushCaseToExternalService = useCallback( + async ({ caseId, connector }: PushToServiceRequest) => { try { dispatch({ type: 'FETCH_INIT' }); - const casePushData = await getCase(caseId, true, abortCtrl.signal); - const responseService = await pushToService( - connector.id, - connector.type, - formatServiceRequestData({ - myCase: casePushData, - connector, - caseServices, - alerts, - formatUrl, - }), - abortCtrl.signal - ); - const responseCase = await pushCase( - caseId, - { - connector_id: connector.id, - connector_name: connector.name, - external_id: responseService.id, - external_title: responseService.title, - external_url: responseService.url, - }, - abortCtrl.signal - ); - if (!cancel) { - dispatch({ type: 'FETCH_SUCCESS_PUSH_SERVICE', payload: responseService }); - dispatch({ type: 'FETCH_SUCCESS_PUSH_CASE', payload: responseCase }); - updateCase(responseCase); + abortCtrl.current.abort(); + cancel.current = false; + abortCtrl.current = new AbortController(); + + const response = await pushCase(caseId, connector.id, abortCtrl.current.signal); + + if (!cancel.current) { + dispatch({ type: 'FETCH_SUCCESS' }); displaySuccessToast( i18n.SUCCESS_SEND_TO_EXTERNAL_SERVICE(connector.name), dispatchToaster ); } + + return response; } catch (error) { - if (!cancel) { + if (!cancel.current) { errorToToaster({ title: i18n.ERROR_TITLE, error: error.body && error.body.message ? new Error(error.body.message) : error, @@ -151,123 +98,17 @@ export const usePostPushToService = (): UsePostPushToService => { dispatch({ type: 'FETCH_FAILURE' }); } } - return () => { - cancel = true; - abortCtrl.abort(); - }; }, // eslint-disable-next-line react-hooks/exhaustive-deps [] ); - return { ...state, postPushToService }; -}; - -export const determineToAndFrom = (alert: Alert) => { - const ellapsedTimeRule = moment.duration( - moment().diff(dateMath.parse(alert.rule?.from != null ? alert.rule.from : 'now-0s')) - ); + useEffect(() => { + return () => { + abortCtrl.current.abort(); + cancel.current = true; + }; + }, []); - const from = moment(alert['@timestamp'] ?? new Date()) - .subtract(ellapsedTimeRule) - .toISOString(); - const to = moment(alert['@timestamp'] ?? new Date()).toISOString(); - - return { to, from }; -}; - -const getAlertFilterUrl = (alert: Alert): string => { - const { to, from } = determineToAndFrom(alert); - return `?filters=!((%27$state%27:(store:appState),meta:(alias:!n,disabled:!f,key:_id,negate:!f,params:(query:${alert._id}),type:phrase),query:(match:(_id:(query:${alert._id},type:phrase)))))&sourcerer=(default:!())&timerange=(global:(linkTo:!(timeline),timerange:(from:%27${from}%27,kind:absolute,to:%27${to}%27)),timeline:(linkTo:!(global),timerange:(from:%27${from}%27,kind:absolute,to:%27${to}%27)))`; -}; - -const getCommentContent = ( - comment: Comment, - alerts: Record, - formatUrl: FormatUrl -): string => { - if (comment.type === CommentType.user) { - return comment.comment; - } else if (comment.type === CommentType.alert) { - const alert = alerts[comment.alertId]; - const ruleDetailsLink = formatUrl(getRuleDetailsUrl(alert.rule.id), { - absolute: true, - skipSearch: true, - }); - - return `[${i18n.ALERT}](${ruleDetailsLink}${getAlertFilterUrl(alert)}) ${ - i18n.ALERT_ADDED_TO_CASE - }.`; - } - - return ''; -}; - -export const formatServiceRequestData = ({ - myCase, - connector, - caseServices, - alerts, - formatUrl, -}: { - myCase: Case; - connector: CaseConnector; - caseServices: CaseServices; - alerts: Record; - formatUrl: FormatUrl; -}): ServiceConnectorCaseParams => { - const { - id: caseId, - createdAt, - createdBy, - comments, - description, - title, - updatedAt, - updatedBy, - } = myCase; - const actualExternalService = caseServices[connector.id] ?? null; - - return { - savedObjectId: caseId, - createdAt, - createdBy: { - fullName: createdBy.fullName ?? null, - username: createdBy?.username ?? '', - }, - comments: comments - .filter( - (c) => - actualExternalService == null || actualExternalService.commentsToUpdate.includes(c.id) - ) - .map((c) => ({ - commentId: c.id, - comment: getCommentContent(c, alerts, formatUrl), - createdAt: c.createdAt, - createdBy: { - fullName: c.createdBy.fullName ?? null, - username: c.createdBy.username ?? '', - }, - updatedAt: c.updatedAt, - updatedBy: - c.updatedBy != null - ? { - fullName: c.updatedBy.fullName ?? null, - username: c.updatedBy.username ?? '', - } - : null, - })), - description, - externalId: actualExternalService?.externalId ?? null, - title, - ...(connector.fields ?? {}), - updatedAt, - updatedBy: - updatedBy != null - ? { - fullName: updatedBy.fullName ?? null, - username: updatedBy.username ?? '', - } - : null, - }; + return { ...state, pushCaseToExternalService }; }; diff --git a/x-pack/plugins/security_solution/public/cases/containers/utils.ts b/x-pack/plugins/security_solution/public/cases/containers/utils.ts index 4311390ae9b49..297c7e35981ac 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/utils.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/utils.ts @@ -26,8 +26,6 @@ import { CaseConfigureResponseRt, CaseUserActionsResponse, CaseUserActionsResponseRt, - ServiceConnectorCaseResponseRt, - ServiceConnectorCaseResponse, CommentType, CasePatchRequest, } from '../../../../case/common/api'; @@ -107,12 +105,6 @@ export const decodeCaseUserActionsResponse = (respUserActions?: CaseUserActionsR fold(throwErrors(createToasterPlainError), identity) ); -export const decodeServiceConnectorCaseResponse = (respPushCase?: ServiceConnectorCaseResponse) => - pipe( - ServiceConnectorCaseResponseRt.decode(respPushCase), - fold(throwErrors(createToasterPlainError), identity) - ); - export const valueToUpdateIsSettings = ( key: UpdateByKey['updateKey'], value: UpdateByKey['updateValue'] diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index c264e807ea234..5e61f58e7afac 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -17352,7 +17352,6 @@ "xpack.securitySolution.case.components.connectors.case.optionAddToExistingCase": "既存のケースに追加", "xpack.securitySolution.case.components.connectors.case.selectMessageText": "ケースを作成または更新します。", "xpack.securitySolution.case.configure.errorGetFields": "サービスからのフィールドの取得中にエラーが発生しました", - "xpack.securitySolution.case.configure.errorPushingToService": "サービスへのプッシュエラー", "xpack.securitySolution.case.configure.successSaveToast": "保存された外部接続設定", "xpack.securitySolution.case.configureCases.addNewConnector": "新しいコネクターを追加", "xpack.securitySolution.case.configureCases.blankMappings": "1 つ以上のフィールドを { connectorName } にマッピングする必要があります", @@ -17401,14 +17400,6 @@ "xpack.securitySolution.case.pageTitle": "ケース", "xpack.securitySolution.case.readOnlySavedObjectDescription": "ケースを表示する権限のみが付与されています。ケースを開いて更新する必要がある場合は、Kibana管理者に連絡してください。", "xpack.securitySolution.case.readOnlySavedObjectTitle": "新しいケースを開いたり、既存のケースを更新したりすることはできません", - "xpack.securitySolution.case.settings.jira.issueTypesSelectFieldLabel": "問題タイプ", - "xpack.securitySolution.case.settings.jira.parentIssueSearchLabel": "親問題", - "xpack.securitySolution.case.settings.jira.prioritySelectFieldLabel": "優先度", - "xpack.securitySolution.case.settings.resilient.incidentTypesLabel": "インシデントタイプ", - "xpack.securitySolution.case.settings.resilient.incidentTypesPlaceholder": "タイプを選択", - "xpack.securitySolution.case.settings.resilient.severityLabel": "深刻度", - "xpack.securitySolution.case.settings.resilient.unableToGetIncidentTypesMessage": "インシデントタイプを取得できません", - "xpack.securitySolution.case.settings.resilient.unableToGetSeverityMessage": "深刻度を取得できません", "xpack.securitySolution.case.settings.syncAlertsSwitchLabelOff": "オフ", "xpack.securitySolution.case.settings.syncAlertsSwitchLabelOn": "オン", "xpack.securitySolution.case.status.closed": "終了", @@ -17421,8 +17412,6 @@ "xpack.securitySolution.case.timeline.actions.addToCaseAriaLabel": "アラートをケースに関連付ける", "xpack.securitySolution.case.timeline.actions.addToCaseTooltip": "ケースに追加", "xpack.securitySolution.case.timeline.actions.caseCreatedSuccessToast": "アラートが「{title}」に追加されました", - "xpack.securitySolution.caseSettingsRegistry.get.missingCaseSettingErrorMessage": "オブジェクトタイプ「{id}」は登録されていません。", - "xpack.securitySolution.caseSettingsRegistry.register.duplicateCaseSettingErrorMessage": "オブジェクトタイプ「{id}」は既に登録されています。", "xpack.securitySolution.certificate.fingerprint.clientCertLabel": "クライアント証明書", "xpack.securitySolution.certificate.fingerprint.serverCertLabel": "サーバー証明書", "xpack.securitySolution.chart.allOthersGroupingLabel": "その他すべて", @@ -17510,19 +17499,6 @@ "xpack.securitySolution.components.mlPopup.upgradeButtonLabel": "サブスクリプションオプション", "xpack.securitySolution.components.mlPopup.upgradeDescription": "SIEMの異常検出機能にアクセスするには、ライセンスをプラチナに更新するか、30日間の無料トライアルを開始するか、AWS、GCP、またはAzureで{cloudLink}にサインアップしてください。その後、機械学習ジョブを実行して異常を表示できます。", "xpack.securitySolution.components.mlPopup.upgradeTitle": "Elastic Platinum へのアップグレード", - "xpack.securitySolution.components.settings.jira.searchIssuesComboBoxAriaLabel": "入力して検索", - "xpack.securitySolution.components.settings.jira.searchIssuesComboBoxPlaceholder": "入力して検索", - "xpack.securitySolution.components.settings.jira.searchIssuesLoading": "読み込み中…", - "xpack.securitySolution.components.settings.jira.unableToGetFieldsMessage": "フィールドを取得できません", - "xpack.securitySolution.components.settings.jira.unableToGetIssueMessage": "ID {id}の問題を取得できません", - "xpack.securitySolution.components.settings.jira.unableToGetIssuesMessage": "問題を取得できません", - "xpack.securitySolution.components.settings.jira.unableToGetIssueTypesMessage": "問題タイプを取得できません", - "xpack.securitySolution.components.settings.serviceNow.impactSelectFieldLabel": "インパクト", - "xpack.securitySolution.components.settings.serviceNow.severitySelectFieldLabel": "深刻度", - "xpack.securitySolution.components.settings.servicenow.severitySelectHighOptionLabel": "高", - "xpack.securitySolution.components.settings.servicenow.severitySelectLowOptionLabel": "低", - "xpack.securitySolution.components.settings.servicenow.severitySelectMediumOptionLabel": "中", - "xpack.securitySolution.components.settings.serviceNow.urgencySelectFieldLabel": "緊急", "xpack.securitySolution.components.stepDefineRule.ruleTypeField.subscriptionsLink": "プラチナサブスクリプション", "xpack.securitySolution.containers.anomalies.errorFetchingAnomaliesData": "異常データをクエリできませんでした", "xpack.securitySolution.containers.anomalies.stackByJobId": "ジョブ", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index a2fe8e81e4635..14e26395ad3ce 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -17396,7 +17396,6 @@ "xpack.securitySolution.case.components.connectors.case.optionAddToExistingCase": "添加到现有案例", "xpack.securitySolution.case.components.connectors.case.selectMessageText": "创建或更新案例。", "xpack.securitySolution.case.configure.errorGetFields": "从服务中获取字段时出错", - "xpack.securitySolution.case.configure.errorPushingToService": "推送到服务时出错", "xpack.securitySolution.case.configure.successSaveToast": "已保存外部连接设置", "xpack.securitySolution.case.configureCases.addNewConnector": "添加新连接器", "xpack.securitySolution.case.configureCases.blankMappings": "至少一个字段需映射到 { connectorName }", @@ -17445,14 +17444,6 @@ "xpack.securitySolution.case.pageTitle": "案例", "xpack.securitySolution.case.readOnlySavedObjectDescription": "您仅有权查看案例。如果需要创建和更新案例,请联系您的 Kibana 管理员。", "xpack.securitySolution.case.readOnlySavedObjectTitle": "您无法创建新案例或更新现有案例", - "xpack.securitySolution.case.settings.jira.issueTypesSelectFieldLabel": "问题类型", - "xpack.securitySolution.case.settings.jira.parentIssueSearchLabel": "父问题", - "xpack.securitySolution.case.settings.jira.prioritySelectFieldLabel": "优先级", - "xpack.securitySolution.case.settings.resilient.incidentTypesLabel": "事件类型", - "xpack.securitySolution.case.settings.resilient.incidentTypesPlaceholder": "选择类型", - "xpack.securitySolution.case.settings.resilient.severityLabel": "严重性", - "xpack.securitySolution.case.settings.resilient.unableToGetIncidentTypesMessage": "无法获取事件类型", - "xpack.securitySolution.case.settings.resilient.unableToGetSeverityMessage": "无法获取严重性", "xpack.securitySolution.case.settings.syncAlertsSwitchLabelOff": "关闭", "xpack.securitySolution.case.settings.syncAlertsSwitchLabelOn": "开启", "xpack.securitySolution.case.status.closed": "已关闭", @@ -17465,8 +17456,6 @@ "xpack.securitySolution.case.timeline.actions.addToCaseAriaLabel": "将告警附加到案例", "xpack.securitySolution.case.timeline.actions.addToCaseTooltip": "添加到案例", "xpack.securitySolution.case.timeline.actions.caseCreatedSuccessToast": "告警已添加到“{title}”", - "xpack.securitySolution.caseSettingsRegistry.get.missingCaseSettingErrorMessage": "对象类型“{id}”未注册。", - "xpack.securitySolution.caseSettingsRegistry.register.duplicateCaseSettingErrorMessage": "已注册对象类型“{id}”。", "xpack.securitySolution.certificate.fingerprint.clientCertLabel": "客户端证书", "xpack.securitySolution.certificate.fingerprint.serverCertLabel": "服务器证书", "xpack.securitySolution.chart.allOthersGroupingLabel": "所有其他", @@ -17554,19 +17543,6 @@ "xpack.securitySolution.components.mlPopup.upgradeButtonLabel": "订阅计划", "xpack.securitySolution.components.mlPopup.upgradeDescription": "要访问 SIEM 的异常检测功能,必须将您的许可证更新到白金级、开始 30 天免费试用或在 AWS、GCP 或 Azure 中实施{cloudLink}。然后便可以运行 Machine Learning 作业并查看异常。", "xpack.securitySolution.components.mlPopup.upgradeTitle": "升级到 Elastic 白金级", - "xpack.securitySolution.components.settings.jira.searchIssuesComboBoxAriaLabel": "键入内容进行搜索", - "xpack.securitySolution.components.settings.jira.searchIssuesComboBoxPlaceholder": "键入内容进行搜索", - "xpack.securitySolution.components.settings.jira.searchIssuesLoading": "正在加载……", - "xpack.securitySolution.components.settings.jira.unableToGetFieldsMessage": "无法获取字段", - "xpack.securitySolution.components.settings.jira.unableToGetIssueMessage": "无法获取 ID 为 {id} 的问题", - "xpack.securitySolution.components.settings.jira.unableToGetIssuesMessage": "无法获取问题", - "xpack.securitySolution.components.settings.jira.unableToGetIssueTypesMessage": "无法获取问题类型", - "xpack.securitySolution.components.settings.serviceNow.impactSelectFieldLabel": "影响", - "xpack.securitySolution.components.settings.serviceNow.severitySelectFieldLabel": "严重性", - "xpack.securitySolution.components.settings.servicenow.severitySelectHighOptionLabel": "高", - "xpack.securitySolution.components.settings.servicenow.severitySelectLowOptionLabel": "低", - "xpack.securitySolution.components.settings.servicenow.severitySelectMediumOptionLabel": "中", - "xpack.securitySolution.components.settings.serviceNow.urgencySelectFieldLabel": "紧急性", "xpack.securitySolution.components.stepDefineRule.ruleTypeField.subscriptionsLink": "白金级订阅", "xpack.securitySolution.containers.anomalies.errorFetchingAnomaliesData": "无法查询异常数据", "xpack.securitySolution.containers.anomalies.stackByJobId": "作业", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/config.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/config.ts deleted file mode 100644 index df35990da8c0c..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/config.ts +++ /dev/null @@ -1,19 +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 * as i18n from './translations'; -import logo from './logo.svg'; - -export const connectorConfiguration = { - id: '.jira', - name: i18n.JIRA_TITLE, - logo, - enabled: true, - enabledInConfig: true, - enabledInLicense: true, - minimumLicenseRequired: 'gold', -}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx index 26b37278003c3..ba6a5fa2079dc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx @@ -11,7 +11,6 @@ import { ActionTypeModel, ConnectorValidationResult, } from '../../../../types'; -import { connectorConfiguration } from './config'; import logo from './logo.svg'; import { JiraActionConnector, JiraConfig, JiraSecrets, JiraActionParams } from './types'; import * as i18n from './translations'; @@ -63,10 +62,10 @@ const validateConnector = ( export function getActionType(): ActionTypeModel { return { - id: connectorConfiguration.id, + id: '.jira', iconClass: logo, selectMessage: i18n.JIRA_DESC, - actionTypeTitle: connectorConfiguration.name, + actionTypeTitle: i18n.JIRA_TITLE, validateConnector, actionConnectorFields: lazy(() => import('./jira_connectors')), validateParams: (actionParams: JiraActionParams): GenericValidationResult => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/config.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/config.ts deleted file mode 100644 index 03b434283cd6e..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/config.ts +++ /dev/null @@ -1,19 +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 * as i18n from './translations'; -import logo from './logo.svg'; - -export const connectorConfiguration = { - id: '.resilient', - name: i18n.TITLE, - logo, - enabled: true, - enabledInConfig: true, - enabledInLicense: true, - minimumLicenseRequired: 'platinum', -}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.tsx index 3e1eafdfebca8..a8fe5e8ae4b6a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.tsx @@ -11,7 +11,6 @@ import { ActionTypeModel, ConnectorValidationResult, } from '../../../../types'; -import { connectorConfiguration } from './config'; import logo from './logo.svg'; import { ResilientActionConnector, @@ -72,10 +71,10 @@ export function getActionType(): ActionTypeModel< ResilientActionParams > { return { - id: connectorConfiguration.id, + id: '.resilient', iconClass: logo, selectMessage: i18n.DESC, - actionTypeTitle: connectorConfiguration.name, + actionTypeTitle: i18n.TITLE, validateConnector, actionConnectorFields: lazy(() => import('./resilient_connectors')), validateParams: (actionParams: ResilientActionParams): GenericValidationResult => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/config.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/config.ts deleted file mode 100644 index 3e629261a29ba..0000000000000 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/config.ts +++ /dev/null @@ -1,31 +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 * as i18n from './translations'; -import logo from './logo.svg'; - -export const serviceNowITSMConfiguration = { - id: '.servicenow', - name: i18n.SERVICENOW_ITSM_TITLE, - desc: i18n.SERVICENOW_ITSM_DESC, - logo, - enabled: true, - enabledInConfig: true, - enabledInLicense: true, - minimumLicenseRequired: 'platinum', -}; - -export const serviceNowSIRConfiguration = { - id: '.servicenow-sir', - name: i18n.SERVICENOW_SIR_TITLE, - desc: i18n.SERVICENOW_SIR_DESC, - logo, - enabled: true, - enabledInConfig: true, - enabledInLicense: true, - minimumLicenseRequired: 'platinum', -}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx index 82d7f028a3e3d..b1664656c0d14 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx @@ -11,7 +11,6 @@ import { ActionTypeModel, ConnectorValidationResult, } from '../../../../types'; -import { serviceNowITSMConfiguration, serviceNowSIRConfiguration } from './config'; import logo from './logo.svg'; import { ServiceNowActionConnector, @@ -68,10 +67,10 @@ export function getServiceNowITSMActionType(): ActionTypeModel< ServiceNowITSMActionParams > { return { - id: serviceNowITSMConfiguration.id, + id: '.servicenow', iconClass: logo, - selectMessage: serviceNowITSMConfiguration.desc, - actionTypeTitle: serviceNowITSMConfiguration.name, + selectMessage: i18n.SERVICENOW_ITSM_DESC, + actionTypeTitle: i18n.SERVICENOW_ITSM_TITLE, validateConnector, actionConnectorFields: lazy(() => import('./servicenow_connectors')), validateParams: ( @@ -103,10 +102,10 @@ export function getServiceNowSIRActionType(): ActionTypeModel< ServiceNowSIRActionParams > { return { - id: serviceNowSIRConfiguration.id, + id: '.servicenow-sir', iconClass: logo, - selectMessage: serviceNowSIRConfiguration.desc, - actionTypeTitle: serviceNowSIRConfiguration.name, + selectMessage: i18n.SERVICENOW_SIR_DESC, + actionTypeTitle: i18n.SERVICENOW_SIR_TITLE, validateConnector, actionConnectorFields: lazy(() => import('./servicenow_connectors')), validateParams: (actionParams: ServiceNowSIRActionParams): GenericValidationResult => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.test.tsx index a55811ffa8ffd..bfc32ef67e46f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.test.tsx @@ -153,49 +153,22 @@ describe('ServiceNowITSMParamsFields renders', () => { }); }); - test('it transforms the urgencies to options correctly', async () => { + test('it transforms the options correctly', async () => { const wrapper = mount(); act(() => { onChoices(useGetChoicesResponse.choices); }); wrapper.update(); - expect(wrapper.find('[data-test-subj="urgencySelect"]').first().prop('options')).toEqual([ - { value: '1', text: '1 - Critical' }, - { value: '2', text: '2 - High' }, - { value: '3', text: '3 - Moderate' }, - { value: '4', text: '4 - Low' }, - ]); - }); - - test('it transforms the severities to options correctly', async () => { - const wrapper = mount(); - act(() => { - onChoices(useGetChoicesResponse.choices); - }); - - wrapper.update(); - expect(wrapper.find('[data-test-subj="severitySelect"]').first().prop('options')).toEqual([ - { value: '1', text: '1 - Critical' }, - { value: '2', text: '2 - High' }, - { value: '3', text: '3 - Moderate' }, - { value: '4', text: '4 - Low' }, - ]); - }); - - test('it transforms the impacts to options correctly', async () => { - const wrapper = mount(); - act(() => { - onChoices(useGetChoicesResponse.choices); - }); - - wrapper.update(); - expect(wrapper.find('[data-test-subj="impactSelect"]').first().prop('options')).toEqual([ - { value: '1', text: '1 - Critical' }, - { value: '2', text: '2 - High' }, - { value: '3', text: '3 - Moderate' }, - { value: '4', text: '4 - Low' }, - ]); + const testers = ['severity', 'urgency', 'impact']; + testers.forEach((subj) => + expect(wrapper.find(`[data-test-subj="${subj}Select"]`).first().prop('options')).toEqual([ + { value: '1', text: '1 - Critical' }, + { value: '2', text: '2 - High' }, + { value: '3', text: '3 - Moderate' }, + { value: '4', text: '4 - Low' }, + ]) + ); }); describe('UI updates', () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts index 1e1ba99633995..288b6e629112d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts @@ -17,7 +17,7 @@ export const SERVICENOW_ITSM_DESC = i18n.translate( export const SERVICENOW_SIR_DESC = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNowSIR.selectMessageText', { - defaultMessage: 'Create an incident in ServiceNow SIR.', + defaultMessage: 'Create an incident in ServiceNow SecOps.', } ); @@ -31,7 +31,7 @@ export const SERVICENOW_ITSM_TITLE = i18n.translate( export const SERVICENOW_SIR_TITLE = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNowSIR.actionTypeTitle', { - defaultMessage: 'ServiceNow SIR', + defaultMessage: 'ServiceNow SecOps', } ); @@ -172,7 +172,7 @@ export const MALWARE_URL_LABEL = i18n.translate( export const MALWARE_HASH_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.malwareHashTitle', { - defaultMessage: 'Malware hash', + defaultMessage: 'Malware Hash', } ); diff --git a/x-pack/plugins/triggers_actions_ui/public/common/index.ts b/x-pack/plugins/triggers_actions_ui/public/common/index.ts index f16f1dc1bc1cf..01470bdddf4d7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/common/index.ts @@ -11,6 +11,9 @@ export * from './index_controls'; export * from './lib'; export * from './types'; -export { serviceNowITSMConfiguration as ServiceNowITSMConnectorConfiguration } from '../application/components/builtin_action_types/servicenow/config'; -export { connectorConfiguration as JiraConnectorConfiguration } from '../application/components/builtin_action_types/jira/config'; -export { connectorConfiguration as ResilientConnectorConfiguration } from '../application/components/builtin_action_types/resilient/config'; +export { + getServiceNowITSMActionType, + getServiceNowSIRActionType, +} from '../application/components/builtin_action_types/servicenow'; +export { getJiraActionType } from '../application/components/builtin_action_types/jira'; +export { getResilientActionType } from '../application/components/builtin_action_types/resilient'; diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts index 7c4ee7b9b0de7..878507bcf4afc 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts @@ -39,6 +39,9 @@ export function getAllExternalServiceSimulatorPaths(): string[] { getExternalServiceSimulatorPath(service) ); allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.SERVICENOW}/api/now/v2/table/incident`); + allPaths.push( + `/api/_${NAME}/${ExternalServiceSimulator.SERVICENOW}/api/now/v2/table/incident/123` + ); allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.SERVICENOW}/api/now/v2/table/sys_choice`); allPaths.push( `/api/_${NAME}/${ExternalServiceSimulator.SERVICENOW}/api/now/v2/table/sys_dictionary` diff --git a/x-pack/test/case_api_integration/basic/tests/cases/push_case.ts b/x-pack/test/case_api_integration/basic/tests/cases/push_case.ts index fe891dc6c5f34..ef7c57b3b4749 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/push_case.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/push_case.ts @@ -59,7 +59,7 @@ export default ({ getService }: FtrProviderContext): void => { .expect(200); actionsRemover.add('default', connector.id, 'action', 'actions'); - const { body: configure } = await supertest + await supertest .post(CASE_CONFIGURE_URL) .set('kbn-xsrf', 'true') .send( @@ -70,6 +70,7 @@ export default ({ getService }: FtrProviderContext): void => { }) ) .expect(200); + const { body: postedCase } = await supertest .post(CASES_URL) .set('kbn-xsrf', 'true') @@ -79,25 +80,34 @@ export default ({ getService }: FtrProviderContext): void => { id: connector.id, name: connector.name, type: connector.actionTypeId, - fields: { urgency: null, impact: null, severity: null }, + fields: { urgency: '2', impact: '2', severity: '2' }, }).connector, }) .expect(200); const { body } = await supertest - .post(`${CASES_URL}/${postedCase.id}/_push`) + .post(`${CASES_URL}/${postedCase.id}/connector/${connector.id}/_push`) .set('kbn-xsrf', 'true') - .send({ - connector_id: configure.connector.id, - connector_name: configure.connector.name, - external_id: 'external_id', - external_title: 'external_title', - external_url: 'external_url', - }) + .send({}) .expect(200); - expect(body.connector.id).to.eql(configure.connector.id); - expect(body.external_service.pushed_by).to.eql(defaultUser); + // eslint-disable-next-line @typescript-eslint/naming-convention + const { pushed_at, external_url, ...rest } = body.external_service; + + expect(rest).to.eql({ + pushed_by: defaultUser, + connector_id: connector.id, + connector_name: connector.name, + external_id: '123', + external_title: 'INC01', + }); + + // external_url is of the form http://elastic:changeme@localhost:5620 which is different between various environments like Jekins + expect( + external_url.includes( + 'api/_actions-FTS-external-service-simulators/servicenow/nav_to.do?uri=incident.do?sys_id=123' + ) + ).to.equal(true); }); it('pushes a comment appropriately', async () => { @@ -112,7 +122,7 @@ export default ({ getService }: FtrProviderContext): void => { actionsRemover.add('default', connector.id, 'action', 'actions'); - const { body: configure } = await supertest + await supertest .post(CASE_CONFIGURE_URL) .set('kbn-xsrf', 'true') .send( @@ -133,79 +143,134 @@ export default ({ getService }: FtrProviderContext): void => { id: connector.id, name: connector.name, type: connector.actionTypeId, - fields: { urgency: null, impact: null, severity: null }, + fields: { urgency: '2', impact: '2', severity: '2' }, }).connector, }) .expect(200); await supertest - .post(`${CASES_URL}/${postedCase.id}/_push`) + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentUserReq) + .expect(200); + + const { body } = await supertest + .post(`${CASES_URL}/${postedCase.id}/connector/${connector.id}/_push`) + .set('kbn-xsrf', 'true') + .send({}) + .expect(200); + + expect(body.comments[0].pushed_by).to.eql(defaultUser); + }); + + it('should pushes a case and closes when closure_type: close-by-pushing', async () => { + const { body: connector } = await supertest + .post('/api/actions/action') .set('kbn-xsrf', 'true') .send({ - connector_id: configure.connector.id, - connector_name: configure.connector.name, - external_id: 'external_id', - external_title: 'external_title', - external_url: 'external_url', + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, }) .expect(200); + actionsRemover.add('default', connector.id, 'action', 'actions'); await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) + .post(CASE_CONFIGURE_URL) .set('kbn-xsrf', 'true') - .send(postCommentUserReq) + .send({ + ...getConfiguration({ + id: connector.id, + name: connector.name, + type: connector.actionTypeId, + }), + closure_type: 'close-by-pushing', + }) .expect(200); - const { body } = await supertest - .post(`${CASES_URL}/${postedCase.id}/_push`) + const { body: postedCase } = await supertest + .post(CASES_URL) .set('kbn-xsrf', 'true') .send({ - connector_id: configure.connector.id, - connector_name: configure.connector.name, - external_id: 'external_id', - external_title: 'external_title', - external_url: 'external_url', + ...postCaseReq, + connector: getConfiguration({ + id: connector.id, + name: connector.name, + type: connector.actionTypeId, + fields: { urgency: '2', impact: '2', severity: '2' }, + }).connector, }) .expect(200); - expect(body.comments[0].pushed_by).to.eql(defaultUser); + const { body } = await supertest + .post(`${CASES_URL}/${postedCase.id}/connector/${connector.id}/_push`) + .set('kbn-xsrf', 'true') + .send({}) + .expect(200); + + expect(body.status).to.eql('closed'); }); it('unhappy path - 404s when case does not exist', async () => { await supertest - .post(`${CASES_URL}/fake-id/_push`) + .post(`${CASES_URL}/fake-id/connector/fake-connector/_push`) .set('kbn-xsrf', 'true') - .send({ - connector_id: 'connector_id', - connector_name: 'connector_name', - external_id: 'external_id', - external_title: 'external_title', - external_url: 'external_url', - }) + .send({}) .expect(404); }); - it('unhappy path - 400s when bad data supplied', async () => { - await supertest - .post(`${CASES_URL}/fake-id/_push`) + it('unhappy path - 404s when connector does not exist', async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) .set('kbn-xsrf', 'true') .send({ - badKey: 'connector_id', + ...postCaseReq, + connector: getConfiguration().connector, }) - .expect(400); + .expect(200); + + await supertest + .post(`${CASES_URL}/${postedCase.id}/connector/fake-connector/_push`) + .set('kbn-xsrf', 'true') + .send({}) + .expect(404); }); it('unhappy path = 409s when case is closed', async () => { - const { body: configure } = await supertest + const { body: connector } = await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'true') + .send({ + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }) + .expect(200); + + actionsRemover.add('default', connector.id, 'action', 'actions'); + + await supertest .post(CASE_CONFIGURE_URL) .set('kbn-xsrf', 'true') - .send(getConfiguration()) + .send( + getConfiguration({ + id: connector.id, + name: connector.name, + type: connector.actionTypeId, + }) + ) .expect(200); const { body: postedCase } = await supertest .post(CASES_URL) .set('kbn-xsrf', 'true') - .send(postCaseReq) + .send({ + ...postCaseReq, + connector: getConfiguration({ + id: connector.id, + name: connector.name, + type: connector.actionTypeId, + fields: { urgency: '2', impact: '2', severity: '2' }, + }).connector, + }) .expect(200); await supertest @@ -223,15 +288,9 @@ export default ({ getService }: FtrProviderContext): void => { .expect(200); await supertest - .post(`${CASES_URL}/${postedCase.id}/_push`) + .post(`${CASES_URL}/${postedCase.id}/connector/${connector.id}/_push`) .set('kbn-xsrf', 'true') - .send({ - connector_id: configure.connector.id, - connector_name: configure.connector.name, - external_id: 'external_id', - external_title: 'external_title', - external_url: 'external_url', - }) + .send({}) .expect(409); }); }); diff --git a/x-pack/test/case_api_integration/basic/tests/cases/user_actions/get_all_user_actions.ts b/x-pack/test/case_api_integration/basic/tests/cases/user_actions/get_all_user_actions.ts index d0b6ae53cbcd0..d83d87da1e7af 100644 --- a/x-pack/test/case_api_integration/basic/tests/cases/user_actions/get_all_user_actions.ts +++ b/x-pack/test/case_api_integration/basic/tests/cases/user_actions/get_all_user_actions.ts @@ -359,21 +359,15 @@ export default ({ getService }: FtrProviderContext): void => { id: connector.id, name: connector.name, type: connector.actionTypeId, - fields: { urgency: null, impact: null, severity: null }, + fields: { urgency: '2', impact: '2', severity: '2' }, }).connector, }) .expect(200); await supertest - .post(`${CASES_URL}/${postedCase.id}/_push`) + .post(`${CASES_URL}/${postedCase.id}/connector/${connector.id}/_push`) .set('kbn-xsrf', 'true') - .send({ - connector_id: configure.connector.id, - connector_name: configure.connector.name, - external_id: 'external_id', - external_title: 'external_title', - external_url: 'external_url', - }) + .send({}) .expect(200); const { body } = await supertest diff --git a/x-pack/test/case_api_integration/common/lib/utils.ts b/x-pack/test/case_api_integration/common/lib/utils.ts index 7115576ccccbd..27a49c3f05869 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -14,8 +14,8 @@ import { } from '../../../../plugins/case/common/api'; export const getConfiguration = ({ - id = 'connector-1', - name = 'Connector 1', + id = 'none', + name = 'none', type = ConnectorTypes.none, fields = null, }: Partial = {}): CasesConfigureRequest => { From 19543d8d3c28108270e65d58df97f2dd47b6c986 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Tue, 9 Feb 2021 13:51:12 +0300 Subject: [PATCH 05/28] [Vega] user should be able to set a specific tilemap service using the mapStyle property (#88440) * [Vega] user should be able to set a specific tilemap service using the mapStyle property * Update vega-reference.asciidoc * fix PR comments * rename mapStyle -> emsTileServiceId Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/user/dashboard/vega-reference.asciidoc | 9 +++-- .../public/data_model/vega_parser.test.js | 33 ++++++++----------- .../public/data_model/vega_parser.ts | 19 +++-------- .../public/vega_view/vega_map_view/view.ts | 18 +++++----- .../translations/translations/ja-JP.json | 2 -- .../translations/translations/zh-CN.json | 2 -- 6 files changed, 35 insertions(+), 48 deletions(-) diff --git a/docs/user/dashboard/vega-reference.asciidoc b/docs/user/dashboard/vega-reference.asciidoc index 2c961dca44474..88fd870fefa74 100644 --- a/docs/user/dashboard/vega-reference.asciidoc +++ b/docs/user/dashboard/vega-reference.asciidoc @@ -251,9 +251,14 @@ experimental[] To enable *Maps*, the graph must specify `type=map` in the host c "longitude": -74, // default 0 "zoom": 7, // default 2 - // defaults to "default". Use false to disable base layer. + // Defaults to 'true', disables the base map layer. "mapStyle": false, + // When 'mapStyle' is 'undefined' or 'true', sets the EMS-layer for the map. + // May either be: "road_map", "road_map_desaturated", "dark_map". + // If 'emsTileServiceId' is 'undefined', it falls back to the auto-switch-dark-light behavior. + "emsTileServiceId": "road_map", + // default 0 "minZoom": 5, @@ -261,7 +266,7 @@ experimental[] To enable *Maps*, the graph must specify `type=map` in the host c // or 25 when base is disabled "maxZoom": 13, - // defaults to true, shows +/- buttons to zoom in/out + // Defaults to 'true', shows +/- buttons to zoom in/out "zoomControl": false, // Defaults to 'false', disables mouse wheel zoom. If set to diff --git a/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js b/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js index 1948792d55a83..f33c2bfc27630 100644 --- a/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js +++ b/src/plugins/vis_type_vega/public/data_model/vega_parser.test.js @@ -279,7 +279,7 @@ describe('VegaParser._parseMapConfig', () => { delayRepaint: true, latitude: 0, longitude: 0, - mapStyle: 'default', + mapStyle: true, zoomControl: true, scrollWheelZoom: false, }, @@ -288,52 +288,47 @@ describe('VegaParser._parseMapConfig', () => { ); test( - 'filled', + 'emsTileServiceId', check( { - delayRepaint: true, - latitude: 0, - longitude: 0, - mapStyle: 'default', - zoomControl: true, - scrollWheelZoom: false, - maxBounds: [1, 2, 3, 4], + mapStyle: true, + emsTileServiceId: 'dark_map', }, { delayRepaint: true, latitude: 0, longitude: 0, - mapStyle: 'default', + mapStyle: true, + emsTileServiceId: 'dark_map', zoomControl: true, scrollWheelZoom: false, - maxBounds: [1, 2, 3, 4], }, 0 ) ); test( - 'warnings', + 'filled', check( { delayRepaint: true, latitude: 0, longitude: 0, - zoom: 'abc', // ignored - mapStyle: 'abc', - zoomControl: 'abc', - scrollWheelZoom: 'abc', - maxBounds: [2, 3, 4], + mapStyle: true, + zoomControl: true, + scrollWheelZoom: false, + maxBounds: [1, 2, 3, 4], }, { delayRepaint: true, latitude: 0, longitude: 0, - mapStyle: 'default', + mapStyle: true, zoomControl: true, scrollWheelZoom: false, + maxBounds: [1, 2, 3, 4], }, - 5 + 0 ) ); }); diff --git a/src/plugins/vis_type_vega/public/data_model/vega_parser.ts b/src/plugins/vis_type_vega/public/data_model/vega_parser.ts index e97418581a42f..d3647b35a5b94 100644 --- a/src/plugins/vis_type_vega/public/data_model/vega_parser.ts +++ b/src/plugins/vis_type_vega/public/data_model/vega_parser.ts @@ -465,21 +465,10 @@ The URL is an identifier only. Kibana and your browser will never access this UR validate(`minZoom`, true); validate(`maxZoom`, true); - // `false` is a valid value - res.mapStyle = this._config?.mapStyle === undefined ? `default` : this._config.mapStyle; - if (res.mapStyle !== `default` && res.mapStyle !== false) { - this._onWarning( - i18n.translate('visTypeVega.vegaParser.mapStyleValueTypeWarningMessage', { - defaultMessage: - '{mapStyleConfigName} may either be {mapStyleConfigFirstAllowedValue} or {mapStyleConfigSecondAllowedValue}', - values: { - mapStyleConfigName: 'config.kibana.mapStyle', - mapStyleConfigFirstAllowedValue: 'false', - mapStyleConfigSecondAllowedValue: '"default"', - }, - }) - ); - res.mapStyle = `default`; + this._parseBool('mapStyle', res, true); + + if (res.mapStyle) { + res.emsTileServiceId = this._config?.emsTileServiceId; } this._parseBool('zoomControl', res, true); diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts index 4c155d6b5ea88..c2112659a50ae 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts +++ b/src/plugins/vis_type_vega/public/vega_view/vega_map_view/view.ts @@ -52,12 +52,14 @@ async function updateVegaView(mapBoxInstance: Map, vegaView: View) { export class VegaMapView extends VegaBaseView { private mapServiceSettings: MapServiceSettings = getMapServiceSettings(); - private mapStyle = this.getMapStyle(); + private emsTileLayer = this.getEmsTileLayer(); - private getMapStyle() { - const { mapStyle } = this._parser.mapConfig; + private getEmsTileLayer() { + const { mapStyle, emsTileServiceId } = this._parser.mapConfig; - return mapStyle === 'default' ? this.mapServiceSettings.defaultTmsLayer() : mapStyle; + if (mapStyle) { + return emsTileServiceId ?? this.mapServiceSettings.defaultTmsLayer(); + } } private get shouldShowZoomControl() { @@ -83,14 +85,14 @@ export class VegaMapView extends VegaBaseView { maxZoom: defaultMapConfig.maxZoom, }; - if (this.mapStyle && this.mapStyle !== userConfiguredLayerId) { - const tmsService = await this.mapServiceSettings.getTmsService(this.mapStyle); + if (this.emsTileLayer && this.emsTileLayer !== userConfiguredLayerId) { + const tmsService = await this.mapServiceSettings.getTmsService(this.emsTileLayer); if (!tmsService) { this.onWarn( i18n.translate('visTypeVega.mapView.mapStyleNotFoundWarningMessage', { defaultMessage: '{mapStyleParam} was not found', - values: { mapStyleParam: `"mapStyle":${this.mapStyle}` }, + values: { mapStyleParam: `"emsTileServiceId":${this.emsTileLayer}` }, }) ); return; @@ -138,7 +140,7 @@ export class VegaMapView extends VegaBaseView { } private initLayers(mapBoxInstance: Map, vegaView: View) { - const shouldShowUserConfiguredLayer = this.mapStyle === userConfiguredLayerId; + const shouldShowUserConfiguredLayer = this.emsTileLayer === userConfiguredLayerId; if (shouldShowUserConfiguredLayer) { const { url, options } = this.mapServiceSettings.config.tilemap; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 5e61f58e7afac..b8a67d9c3388e 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -4553,7 +4553,6 @@ "visTypeVega.inspector.vegaAdapter.value": "値", "visTypeVega.inspector.vegaDebugLabel": "Vegaデバッグ", "visTypeVega.mapView.experimentalMapLayerInfo": "マップレイヤーはまだ実験段階であり、オフィシャルGA機能のサポートSLAが適用されません。フィードバックがある場合は、{githubLink}で問題を報告してください。", - "visTypeVega.mapView.mapStyleNotFoundWarningMessage": "{mapStyleParam} が見つかりませんでした", "visTypeVega.mapView.minZoomAndMaxZoomHaveBeenSwappedWarningMessage": "{minZoomPropertyName} と {maxZoomPropertyName} が交換されました", "visTypeVega.mapView.resettingPropertyToMaxValueWarningMessage": "{name} を {max} にリセットしています", "visTypeVega.mapView.resettingPropertyToMinValueWarningMessage": "{name} を {min} にリセットしています", @@ -4575,7 +4574,6 @@ "visTypeVega.vegaParser.inputSpecDoesNotSpecifySchemaErrorMessage": "仕様に基づき、{schemaParam}フィールドには、\nVega({vegaSchemaUrl}を参照)または\nVega-Lite({vegaLiteSchemaUrl}を参照)の有効なURLを入力する必要があります。\nURLは識別子にすぎません。Kibanaやご使用のブラウザーがこのURLにアクセスすることはありません。", "visTypeVega.vegaParser.invalidVegaSpecErrorMessage": "無効な Vega 仕様", "visTypeVega.vegaParser.kibanaConfigValueTypeErrorMessage": "{configName} が含まれている場合、オブジェクトでなければなりません", - "visTypeVega.vegaParser.mapStyleValueTypeWarningMessage": "{mapStyleConfigName} は {mapStyleConfigFirstAllowedValue} か {mapStyleConfigSecondAllowedValue} のどちらかです", "visTypeVega.vegaParser.maxBoundsValueTypeWarningMessage": "{maxBoundsConfigName} は 4 つの数字の配列でなければなりません", "visTypeVega.vegaParser.notSupportedUrlTypeErrorMessage": "{urlObject} はサポートされていません", "visTypeVega.vegaParser.notValidLibraryVersionForInputSpecWarningMessage": "インプット仕様に {schemaLibrary} {schemaVersion} が使用されていますが、現在のバージョンの {schemaLibrary} は {libraryVersion} です。’", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 14e26395ad3ce..229265fe62252 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -4558,7 +4558,6 @@ "visTypeVega.inspector.vegaAdapter.value": "值", "visTypeVega.inspector.vegaDebugLabel": "Vega 调试", "visTypeVega.mapView.experimentalMapLayerInfo": "地图图层处于试验状态,不受正式发行版功能的支持 SLA 的约束。如欲提供反馈,请在 {githubLink} 中创建问题。", - "visTypeVega.mapView.mapStyleNotFoundWarningMessage": "找不到 {mapStyleParam}", "visTypeVega.mapView.minZoomAndMaxZoomHaveBeenSwappedWarningMessage": "已互换 {minZoomPropertyName} 和 {maxZoomPropertyName}", "visTypeVega.mapView.resettingPropertyToMaxValueWarningMessage": "将 {name} 重置为 {max}", "visTypeVega.mapView.resettingPropertyToMinValueWarningMessage": "将 {name} 重置为 {min}", @@ -4580,7 +4579,6 @@ "visTypeVega.vegaParser.inputSpecDoesNotSpecifySchemaErrorMessage": "您的规范要求 {schemaParam} 字段包含\nVega(请参见 {vegaSchemaUrl})或\nVega-Lite(请参见 {vegaLiteSchemaUrl})的有效 URL。\n该 URL 仅限标识符。Kibana 和您的浏览器将不访问此 URL。", "visTypeVega.vegaParser.invalidVegaSpecErrorMessage": "Vega 规范无效", "visTypeVega.vegaParser.kibanaConfigValueTypeErrorMessage": "如果存在,{configName} 必须为对象", - "visTypeVega.vegaParser.mapStyleValueTypeWarningMessage": "{mapStyleConfigName} 可能为 {mapStyleConfigFirstAllowedValue} 或 {mapStyleConfigSecondAllowedValue}", "visTypeVega.vegaParser.maxBoundsValueTypeWarningMessage": "{maxBoundsConfigName} 必须为具有四个数字的数组", "visTypeVega.vegaParser.notSupportedUrlTypeErrorMessage": "不支持 {urlObject}", "visTypeVega.vegaParser.notValidLibraryVersionForInputSpecWarningMessage": "输入规范使用 {schemaLibrary} {schemaVersion},但 {schemaLibrary} 的当前版本为 {libraryVersion}。", From 4685e8c06ca4d4ad4369a4a41ee1488c35dc2dde Mon Sep 17 00:00:00 2001 From: MadameSheema Date: Tue, 9 Feb 2021 11:53:53 +0100 Subject: [PATCH 06/28] before/beforeEach clean up (#90663) --- .../indicator_match_rule.spec.ts | 32 ++++++++++++------- .../cypress/urls/navigation.ts | 1 + 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts index 98e6dad350ea7..a69f808001800 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts @@ -96,7 +96,7 @@ import { import { esArchiverLoad, esArchiverUnload } from '../../tasks/es_archiver'; import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; -import { DETECTIONS_URL } from '../../urls/navigation'; +import { DETECTIONS_URL, RULE_CREATION } from '../../urls/navigation'; describe('Detection rules, Indicator Match', () => { const expectedUrls = newThreatIndicatorRule.referenceUrls.join(''); @@ -106,25 +106,22 @@ describe('Detection rules, Indicator Match', () => { const expectedNumberOfRules = 1; const expectedNumberOfAlerts = 1; - beforeEach(() => { + before(() => { cleanKibana(); esArchiverLoad('threat_indicator'); esArchiverLoad('threat_data'); - loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); - waitForAlertsPanelToBeLoaded(); - waitForAlertsIndexToBeCreated(); - goToManageAlertsDetectionRules(); - waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); - goToCreateNewRule(); - selectIndicatorMatchType(); }); - - afterEach(() => { + after(() => { esArchiverUnload('threat_indicator'); esArchiverUnload('threat_data'); }); describe('Creating new indicator match rules', () => { + beforeEach(() => { + loginAndWaitForPageWithoutDateRange(RULE_CREATION); + selectIndicatorMatchType(); + }); + describe('Index patterns', () => { it('Contains a predefined index pattern', () => { getIndicatorIndex().should('have.text', indexPatterns.join('')); @@ -355,6 +352,19 @@ describe('Detection rules, Indicator Match', () => { getIndicatorMappingComboField(2).should('not.exist'); }); }); + }); + + describe('Generating signals', () => { + beforeEach(() => { + cleanKibana(); + loginAndWaitForPageWithoutDateRange(DETECTIONS_URL); + waitForAlertsPanelToBeLoaded(); + waitForAlertsIndexToBeCreated(); + goToManageAlertsDetectionRules(); + waitForLoadElasticPrebuiltDetectionRulesTableToBeLoaded(); + goToCreateNewRule(); + selectIndicatorMatchType(); + }); it('Creates and activates a new Indicator Match rule', () => { fillDefineIndicatorMatchRuleAndContinue(newThreatIndicatorRule); diff --git a/x-pack/plugins/security_solution/cypress/urls/navigation.ts b/x-pack/plugins/security_solution/cypress/urls/navigation.ts index f3881ab624f7b..2beed9e8ec0b7 100644 --- a/x-pack/plugins/security_solution/cypress/urls/navigation.ts +++ b/x-pack/plugins/security_solution/cypress/urls/navigation.ts @@ -24,5 +24,6 @@ export const KIBANA_HOME = '/app/home#/'; export const ADMINISTRATION_URL = '/app/security/administration'; export const NETWORK_URL = '/app/security/network'; export const OVERVIEW_URL = '/app/security/overview'; +export const RULE_CREATION = 'app/security/detections/rules/create'; export const TIMELINES_URL = '/app/security/timelines'; export const TIMELINE_TEMPLATES_URL = '/app/security/timelines/template'; From af75079a31e98c8a6c0141393617b39eeb39d9bf Mon Sep 17 00:00:00 2001 From: John Schulz Date: Tue, 9 Feb 2021 05:54:47 -0500 Subject: [PATCH 07/28] [Fleet] Use TS project references (#87574) ## Summary * Added references to all dependencies https://github.com/elastic/kibana/blob/6bc6f3459a120eddfae70ad2fc7e4669e3a996b0/x-pack/plugins/fleet/tsconfig.json#L17-L38 * `node scripts/check_ts_projects` is successful * `node scripts/build_ts_refs` is successful
node --max-old-space-size=4096 ./node_modules/.bin/tsc -p tsconfig.json --extendedDiagnostics --noEmit ``` Files: 1436 Lines: 267372 Nodes: 1016769 Identifiers: 361835 Symbols: 250405 Types: 31105 Instantiations: 57570 Memory used: 347817K Assignability cache size: 5597 Identity cache size: 3073 Subtype cache size: 2140 Strict subtype cache size: 1012 I/O Read time: 0.49s Parse time: 3.84s ResolveModule time: 1.59s ResolveTypeReference time: 0.16s Program time: 7.46s Bind time: 1.87s Check time: 4.02s printTime time: 0.00s Emit time: 0.00s Total time: 13.35s ```
--- x-pack/plugins/fleet/tsconfig.json | 39 ++++++++++++++++++++++++++++++ x-pack/test/tsconfig.json | 5 ++++ x-pack/tsconfig.json | 1 + x-pack/tsconfig.refs.json | 1 + 4 files changed, 46 insertions(+) create mode 100644 x-pack/plugins/fleet/tsconfig.json diff --git a/x-pack/plugins/fleet/tsconfig.json b/x-pack/plugins/fleet/tsconfig.json new file mode 100644 index 0000000000000..3a37b14410424 --- /dev/null +++ b/x-pack/plugins/fleet/tsconfig.json @@ -0,0 +1,39 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "composite": true, + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + // add all the folders containg files to be compiled + "common/**/*", + "public/**/*", + "server/**/*", + "scripts/**/*" + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + // add references to other TypeScript projects the plugin depends on + + // requiredPlugins from ./kibana.json + { "path": "../licensing/tsconfig.json" }, + { "path": "../../../src/plugins/data/tsconfig.json" }, + { "path": "../encrypted_saved_objects/tsconfig.json" }, + + // optionalPlugins from ./kibana.json + { "path": "../security/tsconfig.json" }, + { "path": "../features/tsconfig.json" }, + { "path": "../cloud/tsconfig.json" }, + { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, + { "path": "../../../src/plugins/home/tsconfig.json" }, + + // requiredBundles from ./kibana.json + { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, + { "path": "../../../src/plugins/es_ui_shared/tsconfig.json" }, + { "path": "../infra/tsconfig.json" }, + { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, + ] +} diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index 2981346e80e1d..7ba5c00a71b37 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -45,6 +45,11 @@ { "path": "../plugins/code/tsconfig.json" }, { "path": "../plugins/console_extensions/tsconfig.json" }, { "path": "../plugins/dashboard_mode/tsconfig.json" }, + { "path": "../plugins/enterprise_search/tsconfig.json" }, + { "path": "../plugins/fleet/tsconfig.json" }, + { "path": "../plugins/global_search/tsconfig.json" }, + { "path": "../plugins/global_search_providers/tsconfig.json" }, + { "path": "../plugins/features/tsconfig.json" }, { "path": "../plugins/data_enhanced/tsconfig.json" }, { "path": "../plugins/embeddable_enhanced/tsconfig.json" }, { "path": "../plugins/encrypted_saved_objects/tsconfig.json" }, diff --git a/x-pack/tsconfig.json b/x-pack/tsconfig.json index 740bac3f1b0de..3afbb027e7fde 100644 --- a/x-pack/tsconfig.json +++ b/x-pack/tsconfig.json @@ -14,6 +14,7 @@ "plugins/discover_enhanced/**/*", "plugins/dashboard_mode/**/*", "plugins/dashboard_enhanced/**/*", + "plugins/fleet/**/*", "plugins/global_search/**/*", "plugins/global_search_providers/**/*", "plugins/graph/**/*", diff --git a/x-pack/tsconfig.refs.json b/x-pack/tsconfig.refs.json index 7a2eebc78b69b..54cee9b124237 100644 --- a/x-pack/tsconfig.refs.json +++ b/x-pack/tsconfig.refs.json @@ -18,6 +18,7 @@ { "path": "./plugins/event_log/tsconfig.json" }, { "path": "./plugins/features/tsconfig.json" }, { "path": "./plugins/file_upload/tsconfig.json" }, + { "path": "./plugins/fleet/tsconfig.json" }, { "path": "./plugins/global_search_bar/tsconfig.json" }, { "path": "./plugins/global_search_providers/tsconfig.json" }, { "path": "./plugins/global_search/tsconfig.json" }, From 523110d4bda9136780e7299d737f83c16d0af30f Mon Sep 17 00:00:00 2001 From: Robert Oskamp Date: Tue, 9 Feb 2021 12:45:19 +0100 Subject: [PATCH 08/28] Functional tests - Add esSupertest support for SSL (#90425) This PR allows the functional test service esSupertest to work correctly in environments that have ES SSL enabled in the Kibana server configuration. --- test/api_integration/services/supertest.ts | 12 ++++++++++-- x-pack/test/functional_with_es_ssl/config.ts | 2 ++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/test/api_integration/services/supertest.ts b/test/api_integration/services/supertest.ts index 1257a934da8be..a0268b78cb151 100644 --- a/test/api_integration/services/supertest.ts +++ b/test/api_integration/services/supertest.ts @@ -19,6 +19,14 @@ export function KibanaSupertestProvider({ getService }: FtrProviderContext) { export function ElasticsearchSupertestProvider({ getService }: FtrProviderContext) { const config = getService('config'); - const elasticSearchServerUrl = formatUrl(config.get('servers.elasticsearch')); - return supertestAsPromised(elasticSearchServerUrl); + const esServerConfig = config.get('servers.elasticsearch'); + const elasticSearchServerUrl = formatUrl(esServerConfig); + + let agentOptions = {}; + if ('certificateAuthorities' in esServerConfig) { + agentOptions = { ca: esServerConfig!.certificateAuthorities }; + } + + // @ts-ignore - supertestAsPromised doesn't like the agentOptions, but still passes it correctly to supertest + return supertestAsPromised.agent(elasticSearchServerUrl, agentOptions); } diff --git a/x-pack/test/functional_with_es_ssl/config.ts b/x-pack/test/functional_with_es_ssl/config.ts index 5be8eee3155b9..a7259f2410d6b 100644 --- a/x-pack/test/functional_with_es_ssl/config.ts +++ b/x-pack/test/functional_with_es_ssl/config.ts @@ -5,6 +5,7 @@ * 2.0. */ +import Fs from 'fs'; import { resolve, join } from 'path'; import { CA_CERT_PATH } from '@kbn/dev-utils'; import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; @@ -33,6 +34,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { elasticsearch: { ...xpackFunctionalConfig.get('servers.elasticsearch'), protocol: 'https', + certificateAuthorities: [Fs.readFileSync(CA_CERT_PATH)], }, }; From 6bd7f7df20885d1f75ce6fa618b9fdd08e4104ba Mon Sep 17 00:00:00 2001 From: ymao1 Date: Tue, 9 Feb 2021 07:41:15 -0500 Subject: [PATCH 09/28] [Alerting] Unit tests for Index Threshold Alert Components (#90285) * Added unit test for index threshold expression * Fixing warnings in index select popover test * Added test for index select popover * Added initial test for threshold visualization * Unit tests * License * Visualization unit tests * Fixing types check Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/index_select_popover.test.tsx | 154 +++++++++---- .../alert_types/es_query/expression.test.tsx | 3 - .../alert_types/threshold/expression.test.tsx | 218 ++++++++++++++++++ .../alert_types/threshold/expression.tsx | 18 +- .../threshold/visualization.test.tsx | 187 +++++++++++++++ .../alert_types/threshold/visualization.tsx | 3 + .../common/expression_items/for_the_last.tsx | 1 + .../common/expression_items/group_by_over.tsx | 1 + 8 files changed, 528 insertions(+), 57 deletions(-) create mode 100644 x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.test.tsx create mode 100644 x-pack/plugins/stack_alerts/public/alert_types/threshold/visualization.test.tsx diff --git a/x-pack/plugins/stack_alerts/public/alert_types/components/index_select_popover.test.tsx b/x-pack/plugins/stack_alerts/public/alert_types/components/index_select_popover.test.tsx index 64c085a823478..3b7baac9b80e6 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/components/index_select_popover.test.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/components/index_select_popover.test.tsx @@ -9,54 +9,58 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { mountWithIntl, nextTick } from '@kbn/test/jest'; import { IndexSelectPopover } from './index_select_popover'; +import { EuiComboBox } from '@elastic/eui'; -jest.mock('../../../../triggers_actions_ui/public', () => ({ - getIndexPatterns: () => { - return ['index1', 'index2']; - }, - firstFieldOption: () => { - return { text: 'Select a field', value: '' }; - }, - getTimeFieldOptions: () => { - return [ - { - text: '@timestamp', - value: '@timestamp', - }, - ]; - }, - getFields: () => { - return Promise.resolve([ - { - name: '@timestamp', - type: 'date', - }, - { - name: 'field', - type: 'text', - }, - ]); - }, - getIndexOptions: () => { - return Promise.resolve([ - { - label: 'indexOption', - options: [ - { - label: 'index1', - value: 'index1', - }, - { - label: 'index2', - value: 'index2', - }, - ], - }, - ]); - }, -})); +jest.mock('../../../../triggers_actions_ui/public', () => { + const original = jest.requireActual('../../../../triggers_actions_ui/public'); + return { + ...original, + getIndexPatterns: () => { + return ['index1', 'index2']; + }, + getTimeFieldOptions: () => { + return [ + { + text: '@timestamp', + value: '@timestamp', + }, + ]; + }, + getFields: () => { + return Promise.resolve([ + { + name: '@timestamp', + type: 'date', + }, + { + name: 'field', + type: 'text', + }, + ]); + }, + getIndexOptions: () => { + return Promise.resolve([ + { + label: 'indexOption', + options: [ + { + label: 'index1', + value: 'index1', + }, + { + label: 'index2', + value: 'index2', + }, + ], + }, + ]); + }, + }; +}); describe('IndexSelectPopover', () => { + const onIndexChange = jest.fn(); + const onTimeFieldChange = jest.fn(); const props = { index: [], esFields: [], @@ -65,8 +69,8 @@ describe('IndexSelectPopover', () => { index: [], timeField: [], }, - onIndexChange: jest.fn(), - onTimeFieldChange: jest.fn(), + onIndexChange, + onTimeFieldChange, }; beforeEach(() => { @@ -106,10 +110,62 @@ describe('IndexSelectPopover', () => { const indexComboBox = wrapper.find('#indexSelectSearchBox'); indexComboBox.first().simulate('click'); - const event = { target: { value: 'indexPattern1' } }; - indexComboBox.find('input').first().simulate('change', event); + + await act(async () => { + const event = { target: { value: 'indexPattern1' } }; + indexComboBox.find('input').first().simulate('change', event); + await nextTick(); + wrapper.update(); + }); const updatedIndexSearchValue = wrapper.find('[data-test-subj="comboBoxSearchInput"]'); expect(updatedIndexSearchValue.first().props().value).toEqual('indexPattern1'); + + const thresholdComboBox = wrapper + .find(EuiComboBox) + .filter('[data-test-subj="thresholdIndexesComboBox"]'); + const thresholdOptions = thresholdComboBox.prop('options'); + expect(thresholdOptions.length > 0).toBeTruthy(); + + await act(async () => { + thresholdComboBox.prop('onChange')!([thresholdOptions[0].options![0]]); + await nextTick(); + wrapper.update(); + }); + expect(onIndexChange).toHaveBeenCalledWith( + [thresholdOptions[0].options![0]].map((opt) => opt.value) + ); + + const timeFieldSelect = wrapper.find('select[data-test-subj="thresholdAlertTimeFieldSelect"]'); + await act(async () => { + timeFieldSelect.simulate('change', { target: { value: '@timestamp' } }); + await nextTick(); + wrapper.update(); + }); + expect(onTimeFieldChange).toHaveBeenCalledWith('@timestamp'); + }); + + test('renders index and timeField if defined', async () => { + const index = 'test-index'; + const timeField = '@timestamp'; + const indexSelectProps = { + ...props, + index: [index], + timeField, + }; + const wrapper = mountWithIntl(); + expect(wrapper.find('button[data-test-subj="selectIndexExpression"]').text()).toEqual( + `index ${index}` + ); + + wrapper.find('[data-test-subj="selectIndexExpression"]').first().simulate('click'); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect( + wrapper.find('EuiSelect[data-test-subj="thresholdAlertTimeFieldSelect"]').text() + ).toEqual(`Select a field${timeField}`); }); }); diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.test.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.test.tsx index 3349de086d982..0a9f94f8efae2 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.test.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.test.tsx @@ -58,9 +58,6 @@ jest.mock('../../../../triggers_actions_ui/public', () => { getIndexPatterns: () => { return ['index1', 'index2']; }, - firstFieldOption: () => { - return { text: 'Select a field', value: '' }; - }, getTimeFieldOptions: () => { return [ { diff --git a/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.test.tsx b/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.test.tsx new file mode 100644 index 0000000000000..01c2bc18f35e8 --- /dev/null +++ b/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.test.tsx @@ -0,0 +1,218 @@ +/* + * 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 { mountWithIntl, nextTick } from '@kbn/test/jest'; +import { act } from 'react-dom/test-utils'; +import IndexThresholdAlertTypeExpression, { DEFAULT_VALUES } from './expression'; +import { dataPluginMock } from 'src/plugins/data/public/mocks'; +import { chartPluginMock } from 'src/plugins/charts/public/mocks'; +import { IndexThresholdAlertParams } from './types'; +import { validateExpression } from './validation'; +import { + builtInAggregationTypes, + builtInComparators, + getTimeUnitLabel, + TIME_UNITS, +} from '../../../../triggers_actions_ui/public'; + +jest.mock('../../../../triggers_actions_ui/public', () => { + const original = jest.requireActual('../../../../triggers_actions_ui/public'); + return { + ...original, + getIndexPatterns: () => { + return ['index1', 'index2']; + }, + getTimeFieldOptions: () => { + return [ + { + text: '@timestamp', + value: '@timestamp', + }, + ]; + }, + getFields: () => { + return Promise.resolve([ + { + name: '@timestamp', + type: 'date', + }, + { + name: 'field', + type: 'text', + }, + ]); + }, + getIndexOptions: () => { + return Promise.resolve([ + { + label: 'indexOption', + options: [ + { + label: 'index1', + value: 'index1', + }, + { + label: 'index2', + value: 'index2', + }, + ], + }, + ]); + }, + }; +}); + +const dataMock = dataPluginMock.createStartContract(); +const chartsStartMock = chartPluginMock.createStartContract(); + +describe('IndexThresholdAlertTypeExpression', () => { + function getAlertParams(overrides = {}) { + return { + index: 'test-index', + aggType: 'count', + thresholdComparator: '>', + threshold: [0], + timeWindowSize: 15, + timeWindowUnit: 's', + ...overrides, + }; + } + async function setup(alertParams: IndexThresholdAlertParams) { + const { errors } = validateExpression(alertParams); + + const wrapper = mountWithIntl( + {}} + setAlertProperty={() => {}} + errors={errors} + data={dataMock} + defaultActionGroupId="" + actionGroups={[]} + charts={chartsStartMock} + /> + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + return wrapper; + } + + test(`should render IndexThresholdAlertTypeExpression with expected components when aggType doesn't require field`, async () => { + const wrapper = await setup(getAlertParams()); + expect(wrapper.find('[data-test-subj="indexSelectPopover"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="whenExpression"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="groupByExpression"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="aggTypeExpression"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="thresholdExpression"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="forLastExpression"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="visualizationPlaceholder"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="thresholdVisualization"]').exists()).toBeFalsy(); + }); + + test(`should render IndexThresholdAlertTypeExpression with expected components when aggType does require field`, async () => { + const wrapper = await setup(getAlertParams({ aggType: 'avg' })); + expect(wrapper.find('[data-test-subj="indexSelectPopover"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="whenExpression"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="groupByExpression"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="aggTypeExpression"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="thresholdExpression"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="forLastExpression"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="visualizationPlaceholder"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="thresholdVisualization"]').exists()).toBeFalsy(); + }); + + test(`should render IndexThresholdAlertTypeExpression with visualization when there are no expression errors`, async () => { + const wrapper = await setup(getAlertParams({ timeField: '@timestamp' })); + expect(wrapper.find('[data-test-subj="visualizationPlaceholder"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="thresholdVisualization"]').exists()).toBeTruthy(); + }); + + test(`should set default alert params when params are undefined`, async () => { + const wrapper = await setup( + getAlertParams({ + aggType: undefined, + thresholdComparator: undefined, + timeWindowSize: undefined, + timeWindowUnit: undefined, + groupBy: undefined, + threshold: undefined, + }) + ); + + expect(wrapper.find('button[data-test-subj="selectIndexExpression"]').text()).toEqual( + 'index test-index' + ); + expect(wrapper.find('button[data-test-subj="whenExpression"]').text()).toEqual( + `when ${builtInAggregationTypes[DEFAULT_VALUES.AGGREGATION_TYPE].text}` + ); + expect(wrapper.find('button[data-test-subj="groupByExpression"]').text()).toEqual( + `over ${DEFAULT_VALUES.GROUP_BY} documents ` + ); + expect(wrapper.find('[data-test-subj="aggTypeExpression"]').exists()).toBeFalsy(); + expect(wrapper.find('button[data-test-subj="thresholdPopover"]').text()).toEqual( + `${builtInComparators[DEFAULT_VALUES.THRESHOLD_COMPARATOR].text} ` + ); + expect(wrapper.find('button[data-test-subj="forLastExpression"]').text()).toEqual( + `for the last ${DEFAULT_VALUES.TIME_WINDOW_SIZE} ${getTimeUnitLabel( + DEFAULT_VALUES.TIME_WINDOW_UNIT as TIME_UNITS, + DEFAULT_VALUES.TIME_WINDOW_SIZE.toString() + )}` + ); + expect( + wrapper.find('EuiEmptyPrompt[data-test-subj="visualizationPlaceholder"]').text() + ).toEqual(`Complete the expression to generate a preview.`); + }); + + test(`should use alert params when params are defined`, async () => { + const aggType = 'avg'; + const thresholdComparator = 'between'; + const timeWindowSize = 987; + const timeWindowUnit = 's'; + const threshold = [3, 1003]; + const groupBy = 'top'; + const termSize = '27'; + const termField = 'host.name'; + const wrapper = await setup( + getAlertParams({ + aggType, + thresholdComparator, + timeWindowSize, + timeWindowUnit, + termSize, + termField, + groupBy, + threshold, + }) + ); + + expect(wrapper.find('button[data-test-subj="whenExpression"]').text()).toEqual( + `when ${builtInAggregationTypes[aggType].text}` + ); + expect(wrapper.find('button[data-test-subj="groupByExpression"]').text()).toEqual( + `grouped over ${groupBy} ${termSize} '${termField}'` + ); + + expect(wrapper.find('button[data-test-subj="thresholdPopover"]').text()).toEqual( + `${builtInComparators[thresholdComparator].text} ${threshold[0]} AND ${threshold[1]}` + ); + expect(wrapper.find('button[data-test-subj="forLastExpression"]').text()).toEqual( + `for the last ${timeWindowSize} ${getTimeUnitLabel( + timeWindowUnit as TIME_UNITS, + timeWindowSize.toString() + )}` + ); + expect( + wrapper.find('EuiEmptyPrompt[data-test-subj="visualizationPlaceholder"]').text() + ).toEqual(`Complete the expression to generate a preview.`); + }); +}); diff --git a/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.tsx index aed115a53fa26..380e2793043f8 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/threshold/expression.tsx @@ -28,7 +28,7 @@ import { IndexThresholdAlertParams } from './types'; import './expression.scss'; import { IndexSelectPopover } from '../components/index_select_popover'; -const DEFAULT_VALUES = { +export const DEFAULT_VALUES = { AGGREGATION_TYPE: 'count', TERM_SIZE: 5, THRESHOLD_COMPARATOR: COMPARATORS.GREATER_THAN, @@ -100,7 +100,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent< alertParams[errorKey as keyof IndexThresholdAlertParams] !== undefined ); - const canShowVizualization = !!Object.keys(errors).find( + const cannotShowVisualization = !!Object.keys(errors).find( (errorKey) => expressionFieldsWithValidation.includes(errorKey) && errors[errorKey].length >= 1 ); @@ -158,6 +158,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent< setAlertParams('aggType', selectedAggType) @@ -196,6 +198,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent< {aggType && builtInAggregationTypes[aggType].fieldRequired ? ( @@ -258,9 +264,10 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent< />
- {canShowVizualization ? ( + {cannotShowVisualization ? ( @@ -275,6 +282,7 @@ export const IndexThresholdAlertTypeExpression: React.FunctionComponent< ) : ( ({ + getThresholdAlertVisualizationData: jest.fn(() => + Promise.resolve({ + results: [ + { group: 'a', metrics: [['b', 2]] }, + { group: 'a', metrics: [['b', 10]] }, + ], + }) + ), +})); + +const { getThresholdAlertVisualizationData } = jest.requireMock('./index_threshold_api'); + +const dataMock = dataPluginMock.createStartContract(); +const chartsStartMock = chartPluginMock.createStartContract(); +dataMock.fieldFormats = ({ + getDefaultInstance: jest.fn(() => ({ + convert: jest.fn((s: unknown) => JSON.stringify(s)), + })), +} as unknown) as DataPublicPluginStart['fieldFormats']; + +describe('ThresholdVisualization', () => { + beforeAll(() => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + uiSettings: uiSettingsServiceMock.createSetupContract(), + }, + }); + }); + + const alertParams = { + index: 'test-index', + aggType: 'count', + thresholdComparator: '>', + threshold: [0], + timeWindowSize: 15, + timeWindowUnit: 's', + }; + + async function setup() { + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + return wrapper; + } + + test('periodically requests visualization data', async () => { + const refreshRate = 10; + jest.useFakeTimers(); + + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + expect(getThresholdAlertVisualizationData).toHaveBeenCalledTimes(1); + + for (let i = 1; i <= 5; i++) { + await act(async () => { + jest.advanceTimersByTime(refreshRate); + await nextTick(); + wrapper.update(); + }); + expect(getThresholdAlertVisualizationData).toHaveBeenCalledTimes(i + 1); + } + }); + + test('renders loading message on initial load', async () => { + const wrapper = mountWithIntl( + + ); + expect(wrapper.find('[data-test-subj="firstLoad"]').exists()).toBeTruthy(); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="firstLoad"]').exists()).toBeFalsy(); + expect(getThresholdAlertVisualizationData).toHaveBeenCalled(); + }); + + test('renders chart when visualization results are available', async () => { + const wrapper = await setup(); + + expect(wrapper.find('[data-test-subj="alertVisualizationChart"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="noDataCallout"]').exists()).toBeFalsy(); + expect(wrapper.find(Chart)).toHaveLength(1); + expect(wrapper.find(LineSeries)).toHaveLength(1); + expect(wrapper.find(LineAnnotation)).toHaveLength(1); + }); + + test('renders multiple line series chart when visualization results contain multiple groups', async () => { + getThresholdAlertVisualizationData.mockImplementation(() => + Promise.resolve({ + results: [ + { group: 'a', metrics: [['b', 2]] }, + { group: 'a', metrics: [['b', 10]] }, + { group: 'c', metrics: [['d', 1]] }, + ], + }) + ); + + const wrapper = await setup(); + + expect(wrapper.find('[data-test-subj="alertVisualizationChart"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="noDataCallout"]').exists()).toBeFalsy(); + expect(wrapper.find(Chart)).toHaveLength(1); + expect(wrapper.find(LineSeries)).toHaveLength(2); + expect(wrapper.find(LineAnnotation)).toHaveLength(1); + }); + + test('renders error message when getting visualization fails', async () => { + const errorMessage = 'oh no'; + getThresholdAlertVisualizationData.mockImplementation(() => Promise.reject(errorMessage)); + const wrapper = await setup(); + + expect(wrapper.find('[data-test-subj="errorCallout"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="errorCallout"]').first().text()).toBe( + `Cannot load alert visualization${errorMessage}` + ); + }); + + test('renders no data message when visualization results are empty', async () => { + getThresholdAlertVisualizationData.mockImplementation(() => Promise.resolve({ results: [] })); + const wrapper = await setup(); + + expect(wrapper.find('[data-test-subj="alertVisualizationChart"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="noDataCallout"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="noDataCallout"]').first().text()).toBe( + `No data matches this queryCheck that your time range and filters are correct.` + ); + }); +}); diff --git a/x-pack/plugins/stack_alerts/public/alert_types/threshold/visualization.tsx b/x-pack/plugins/stack_alerts/public/alert_types/threshold/visualization.tsx index 7401d0e26be68..40736f7350b1b 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/threshold/visualization.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/threshold/visualization.tsx @@ -202,6 +202,7 @@ export const ThresholdVisualization: React.FunctionComponent = ({ if (loadingState === LoadingStateType.FirstLoad) { return ( } body={ @@ -220,6 +221,7 @@ export const ThresholdVisualization: React.FunctionComponent = ({ = ({ ) : ( Date: Tue, 9 Feb 2021 13:27:49 +0000 Subject: [PATCH 10/28] [Security Solution] Update open timeline filters and add unit tests (#89852) * update filter and add unit tests * styling * fix i18n error * fix unit test * fix lint error Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../open_timeline_modal_body.tsx | 12 +- .../open_timeline/title_row/index.tsx | 2 +- .../components/open_timeline/translations.ts | 8 +- .../components/open_timeline/types.ts | 2 - .../open_timeline/use_timeline_types.test.tsx | 193 ++++++++++++++++++ .../open_timeline/use_timeline_types.tsx | 71 +++---- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 8 files changed, 228 insertions(+), 62 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.test.tsx diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx index 13b9c9ef4f519..1616c5e84247f 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { EuiModalBody, EuiModalHeader } from '@elastic/eui'; +import { EuiModalBody, EuiModalHeader, EuiSpacer } from '@elastic/eui'; import React, { Fragment, memo, useMemo } from 'react'; import styled from 'styled-components'; @@ -62,11 +62,10 @@ export const OpenTimelineModalBody = memo( const SearchRowContent = useMemo( () => ( - {!!timelineFilter && timelineFilter} {!!templateTimelineFilter && templateTimelineFilter} ), - [timelineFilter, templateTimelineFilter] + [templateTimelineFilter] ); return ( @@ -84,9 +83,14 @@ export const OpenTimelineModalBody = memo( <> + {!!timelineFilter && ( + <> + {timelineFilter} + + + )} & { */ export const TitleRow = React.memo( ({ children, onAddTimelinesToFavorites, selectedTimelinesCount, title }) => ( - + {onAddTimelinesToFavorites && ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/translations.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/translations.ts index 84907c74cdace..ae743ad30eef1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/translations.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/translations.ts @@ -146,7 +146,7 @@ export const OPEN_TIMELINE = i18n.translate( export const OPEN_TIMELINE_TITLE = i18n.translate( 'xpack.securitySolution.open.timeline.openTimelineTitle', { - defaultMessage: 'Open Timeline', + defaultMessage: 'Open', } ); @@ -274,12 +274,6 @@ export const SUCCESSFULLY_EXPORTED_TIMELINE_TEMPLATES = (totalTimelineTemplates: } ); -export const FILTER_TIMELINES = (timelineType: string) => - i18n.translate('xpack.securitySolution.open.timeline.filterByTimelineTypesTitle', { - values: { timelineType }, - defaultMessage: 'Only {timelineType}', - }); - export const TAB_TIMELINES = i18n.translate( 'xpack.securitySolution.timelines.components.tabs.timelinesTitle', { diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts index ddf567edafe13..ad62bda4c9783 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/types.ts @@ -221,13 +221,11 @@ export enum TimelineTabsStyle { } export interface TimelineTab { - count: number | undefined; disabled: boolean; href: string; id: TimelineTypeLiteral; name: string; onClick: (ev: { preventDefault: () => void }) => void; - withNext: boolean; } export interface TemplateTimelineFilter { diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.test.tsx new file mode 100644 index 0000000000000..1d39dd169ffaa --- /dev/null +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.test.tsx @@ -0,0 +1,193 @@ +/* + * 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 { fireEvent, render } from '@testing-library/react'; +import { renderHook, act } from '@testing-library/react-hooks'; +import { + useTimelineTypes, + UseTimelineTypesArgs, + UseTimelineTypesResult, +} from './use_timeline_types'; + +jest.mock('react-router-dom', () => { + return { + useParams: jest.fn().mockReturnValue('default'), + useHistory: jest.fn().mockReturnValue([]), + }; +}); + +jest.mock('../../../common/components/link_to', () => { + return { + getTimelineTabsUrl: jest.fn(), + useFormatUrl: jest.fn().mockReturnValue({ + formatUrl: jest.fn(), + search: '', + }), + }; +}); + +describe('useTimelineTypes', () => { + it('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + UseTimelineTypesArgs, + UseTimelineTypesResult + >(() => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 3 })); + await waitForNextUpdate(); + expect(result.current).toEqual({ + timelineType: 'default', + timelineTabs: result.current.timelineTabs, + timelineFilters: result.current.timelineFilters, + }); + }); + }); + + describe('timelineTabs', () => { + it('render timelineTabs', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + UseTimelineTypesArgs, + UseTimelineTypesResult + >(() => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 3 })); + await waitForNextUpdate(); + + const { container } = render(result.current.timelineTabs); + expect( + container.querySelector('[data-test-subj="timeline-tab-default"]') + ).toHaveTextContent('Timelines'); + expect( + container.querySelector('[data-test-subj="timeline-tab-template"]') + ).toHaveTextContent('Templates'); + }); + }); + + it('set timelineTypes correctly', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + UseTimelineTypesArgs, + UseTimelineTypesResult + >(() => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 3 })); + await waitForNextUpdate(); + + const { container } = render(result.current.timelineTabs); + + fireEvent( + container.querySelector('[data-test-subj="timeline-tab-template"]')!, + new MouseEvent('click', { + bubbles: true, + cancelable: true, + }) + ); + + expect(result.current).toEqual({ + timelineType: 'template', + timelineTabs: result.current.timelineTabs, + timelineFilters: result.current.timelineFilters, + }); + }); + }); + + it('stays in the same tab if clicking again on current tab', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + UseTimelineTypesArgs, + UseTimelineTypesResult + >(() => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 3 })); + await waitForNextUpdate(); + + const { container } = render(result.current.timelineTabs); + + fireEvent( + container.querySelector('[data-test-subj="timeline-tab-default"]')!, + new MouseEvent('click', { + bubbles: true, + cancelable: true, + }) + ); + + expect(result.current).toEqual({ + timelineType: 'default', + timelineTabs: result.current.timelineTabs, + timelineFilters: result.current.timelineFilters, + }); + }); + }); + }); + + describe('timelineFilters', () => { + it('render timelineFilters', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + UseTimelineTypesArgs, + UseTimelineTypesResult + >(() => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 3 })); + await waitForNextUpdate(); + + const { container } = render(<>{result.current.timelineFilters}); + expect( + container.querySelector('[data-test-subj="open-timeline-modal-body-filter-default"]') + ).toHaveTextContent('Timelines'); + expect( + container.querySelector('[data-test-subj="open-timeline-modal-body-filter-template"]') + ).toHaveTextContent('Templates'); + }); + }); + + it('set timelineTypes correctly', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + UseTimelineTypesArgs, + UseTimelineTypesResult + >(() => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 3 })); + await waitForNextUpdate(); + + const { container } = render(<>{result.current.timelineFilters}); + + fireEvent( + container.querySelector('[data-test-subj="open-timeline-modal-body-filter-template"]')!, + new MouseEvent('click', { + bubbles: true, + cancelable: true, + }) + ); + + expect(result.current).toEqual({ + timelineType: 'template', + timelineTabs: result.current.timelineTabs, + timelineFilters: result.current.timelineFilters, + }); + }); + }); + + it('stays in the same tab if clicking again on current tab', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook< + UseTimelineTypesArgs, + UseTimelineTypesResult + >(() => useTimelineTypes({ defaultTimelineCount: 0, templateTimelineCount: 3 })); + await waitForNextUpdate(); + + const { container } = render(<>{result.current.timelineFilters}); + + fireEvent( + container.querySelector('[data-test-subj="open-timeline-modal-body-filter-default"]')!, + new MouseEvent('click', { + bubbles: true, + cancelable: true, + }) + ); + + expect(result.current).toEqual({ + timelineType: 'default', + timelineTabs: result.current.timelineTabs, + timelineFilters: result.current.timelineFilters, + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx index 728d8b6eeb488..a66fe43d305f1 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/use_timeline_types.tsx @@ -7,7 +7,7 @@ import React, { useState, useCallback, useMemo } from 'react'; import { useParams, useHistory } from 'react-router-dom'; -import { EuiTabs, EuiTab, EuiSpacer, EuiFilterButton } from '@elastic/eui'; +import { EuiTabs, EuiTab, EuiSpacer } from '@elastic/eui'; import { noop } from 'lodash/fp'; import { TimelineTypeLiteralWithNull, TimelineType } from '../../../../common/types/timeline'; @@ -24,7 +24,7 @@ export interface UseTimelineTypesArgs { export interface UseTimelineTypesResult { timelineType: TimelineTypeLiteralWithNull; timelineTabs: JSX.Element; - timelineFilters: JSX.Element[]; + timelineFilters: JSX.Element; } export const useTimelineTypes = ({ @@ -59,51 +59,28 @@ export const useTimelineTypes = ({ (timelineTabsStyle: TimelineTabsStyle) => [ { id: TimelineType.default, - name: - timelineTabsStyle === TimelineTabsStyle.filter - ? i18n.FILTER_TIMELINES(i18n.TAB_TIMELINES) - : i18n.TAB_TIMELINES, + name: i18n.TAB_TIMELINES, href: formatUrl(getTimelineTabsUrl(TimelineType.default, urlSearch)), disabled: false, - withNext: true, - count: - timelineTabsStyle === TimelineTabsStyle.filter - ? defaultTimelineCount ?? undefined - : undefined, + onClick: timelineTabsStyle === TimelineTabsStyle.tab ? goToTimeline : noop, }, { id: TimelineType.template, - name: - timelineTabsStyle === TimelineTabsStyle.filter - ? i18n.FILTER_TIMELINES(i18n.TAB_TEMPLATES) - : i18n.TAB_TEMPLATES, + name: i18n.TAB_TEMPLATES, href: formatUrl(getTimelineTabsUrl(TimelineType.template, urlSearch)), disabled: false, - withNext: false, - count: - timelineTabsStyle === TimelineTabsStyle.filter - ? templateTimelineCount ?? undefined - : undefined, + onClick: timelineTabsStyle === TimelineTabsStyle.tab ? goToTemplateTimeline : noop, }, ], - [ - defaultTimelineCount, - templateTimelineCount, - urlSearch, - formatUrl, - goToTimeline, - goToTemplateTimeline, - ] + [urlSearch, formatUrl, goToTimeline, goToTemplateTimeline] ); const onFilterClicked = useCallback( (tabId, tabStyle: TimelineTabsStyle) => { setTimelineTypes((prevTimelineTypes) => { - if (tabId === prevTimelineTypes && tabStyle === TimelineTabsStyle.filter) { - return tabId === TimelineType.default ? TimelineType.template : TimelineType.default; - } else if (prevTimelineTypes !== tabId) { + if (prevTimelineTypes !== tabId) { setTimelineTypes(tabId); } return prevTimelineTypes; @@ -139,21 +116,23 @@ export const useTimelineTypes = ({ }, [tabName]); const timelineFilters = useMemo(() => { - return getFilterOrTabs(TimelineTabsStyle.filter).map((tab: TimelineTab) => ( - void }) => { - tab.onClick(ev); - onFilterClicked(tab.id, TimelineTabsStyle.filter); - }} - withNext={tab.withNext} - > - {tab.name} - - )); + return ( + + {getFilterOrTabs(TimelineTabsStyle.filter).map((tab: TimelineTab) => ( + void }) => { + tab.onClick(ev); + onFilterClicked(tab.id, TimelineTabsStyle.filter); + }} + > + {tab.name} + + ))} + + ); }, [timelineType, getFilterOrTabs, onFilterClicked]); return { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index b8a67d9c3388e..439b9a93d0d97 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -19247,7 +19247,6 @@ "xpack.securitySolution.open.timeline.exportSelectedButton": "選択した項目のエクスポート", "xpack.securitySolution.open.timeline.favoriteSelectedButton": "選択中のお気に入り", "xpack.securitySolution.open.timeline.favoritesTooltip": "お気に入り", - "xpack.securitySolution.open.timeline.filterByTimelineTypesTitle": "{timelineType}のみ", "xpack.securitySolution.open.timeline.lastModifiedTableHeader": "最終更新:", "xpack.securitySolution.open.timeline.missingSavedObjectIdTooltip": "savedObjectId がありません", "xpack.securitySolution.open.timeline.modifiedByTableHeader": "変更者:", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 229265fe62252..643192df99309 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -19294,7 +19294,6 @@ "xpack.securitySolution.open.timeline.exportSelectedButton": "导出所选", "xpack.securitySolution.open.timeline.favoriteSelectedButton": "收藏所选", "xpack.securitySolution.open.timeline.favoritesTooltip": "收藏夹", - "xpack.securitySolution.open.timeline.filterByTimelineTypesTitle": "仅 {timelineType}", "xpack.securitySolution.open.timeline.lastModifiedTableHeader": "最后修改时间", "xpack.securitySolution.open.timeline.missingSavedObjectIdTooltip": "缺失 savedObjectId", "xpack.securitySolution.open.timeline.modifiedByTableHeader": "修改者", From ac18273df58305706cb0d770d038f399ab0479ea Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Tue, 9 Feb 2021 14:33:46 +0100 Subject: [PATCH 11/28] [APM] Higher timeout for flaky abort test (#90728) --- .../create_es_client/create_apm_event_client/index.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.test.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.test.ts index 741f282b169ed..addd7391d782d 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.test.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.test.ts @@ -69,10 +69,10 @@ describe('createApmEventClient', () => { incomingRequest.on('abort', () => { setTimeout(() => { resolve(undefined); - }, 0); + }, 100); }); incomingRequest.abort(); - }, 50); + }, 100); }); expect(abort).toHaveBeenCalled(); From 3456766c394ba88037923223ba74193f0902ba28 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 9 Feb 2021 13:40:12 +0000 Subject: [PATCH 12/28] skip flaky suite (#90576) --- .../apps/transform/feature_controls/transform_security.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/transform/feature_controls/transform_security.ts b/x-pack/test/functional/apps/transform/feature_controls/transform_security.ts index 46e0c01afcc38..b8d6b88e4ed9a 100644 --- a/x-pack/test/functional/apps/transform/feature_controls/transform_security.ts +++ b/x-pack/test/functional/apps/transform/feature_controls/transform_security.ts @@ -15,7 +15,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const appsMenu = getService('appsMenu'); const managementMenu = getService('managementMenu'); - describe('security', () => { + // FLAKY: https://github.com/elastic/kibana/issues/90576 + describe.skip('security', () => { before(async () => { await esArchiver.load('empty_kibana'); await PageObjects.security.forceLogout(); From b78f9f9c460cb8a34aad3e6d3931af3acd499fea Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 9 Feb 2021 13:42:48 +0000 Subject: [PATCH 13/28] skip flaky suite (#50448) --- x-pack/test/functional/apps/status_page/status_page.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/status_page/status_page.ts b/x-pack/test/functional/apps/status_page/status_page.ts index d2d6a24bdccd1..55a54245cf832 100644 --- a/x-pack/test/functional/apps/status_page/status_page.ts +++ b/x-pack/test/functional/apps/status_page/status_page.ts @@ -14,7 +14,8 @@ export default function statusPageFunctonalTests({ const esArchiver = getService('esArchiver'); const PageObjects = getPageObjects(['security', 'statusPage', 'home']); - describe('Status Page', function () { + // FLAKY: https://github.com/elastic/kibana/issues/50448 + describe.skip('Status Page', function () { this.tags(['skipCloud', 'includeFirefox']); before(async () => await esArchiver.load('empty_kibana')); after(async () => await esArchiver.unload('empty_kibana')); From 731e333078d31b00e71c09cc5ac2bf1a6457ad96 Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Tue, 9 Feb 2021 15:43:40 +0200 Subject: [PATCH 14/28] [Search Sessions] Apply awaits to avoid unhandled errors (#90593) * Apply awaits to avoid unhandled errors * catch and ignore tracking error * added reject test for search service * Improve search service api test coverage Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../data/server/search/search_service.test.ts | 287 +++++++++++++++++- .../data/server/search/search_service.ts | 56 ++-- 2 files changed, 315 insertions(+), 28 deletions(-) diff --git a/src/plugins/data/server/search/search_service.test.ts b/src/plugins/data/server/search/search_service.test.ts index d6589e88085a0..192c133c94a04 100644 --- a/src/plugins/data/server/search/search_service.test.ts +++ b/src/plugins/data/server/search/search_service.test.ts @@ -7,7 +7,7 @@ */ import type { MockedKeys } from '@kbn/utility-types/jest'; -import { CoreSetup, CoreStart } from '../../../../core/server'; +import { CoreSetup, CoreStart, SavedObject } from '../../../../core/server'; import { coreMock } from '../../../../core/server/mocks'; import { DataPluginStart } from '../plugin'; @@ -86,13 +86,22 @@ describe('Search service', () => { describe('asScopedProvider', () => { let mockScopedClient: IScopedSearchClient; let searcPluginStart: ISearchStart>; - let mockStrategy: jest.Mocked; + let mockStrategy: any; + let mockStrategyNoCancel: jest.Mocked; let mockSessionService: ISearchSessionService; let mockSessionClient: jest.Mocked; const sessionId = '1234'; beforeEach(() => { - mockStrategy = { search: jest.fn().mockReturnValue(of({})) }; + mockStrategy = { + search: jest.fn().mockReturnValue(of({})), + cancel: jest.fn(), + extend: jest.fn(), + }; + + mockStrategyNoCancel = { + search: jest.fn().mockReturnValue(of({})), + }; mockSessionClient = createSearchSessionsClientMock(); mockSessionService = { @@ -104,6 +113,7 @@ describe('Search service', () => { expressions: expressionsPluginMock.createSetupContract(), }); pluginSetup.registerSearchStrategy('es', mockStrategy); + pluginSetup.registerSearchStrategy('nocancel', mockStrategyNoCancel); pluginSetup.__enhance({ defaultStrategy: 'es', sessionService: mockSessionService, @@ -123,7 +133,7 @@ describe('Search service', () => { it('searches using the original request if not restoring, trackId is not called if there is no id in the response', async () => { const searchRequest = { params: {} }; const options = { sessionId, isStored: false, isRestore: false }; - mockSessionClient.trackId = jest.fn(); + mockSessionClient.trackId = jest.fn().mockResolvedValue(undefined); mockStrategy.search.mockReturnValue( of({ @@ -165,10 +175,27 @@ describe('Search service', () => { expect(request).toStrictEqual({ ...searchRequest, id: 'my_id' }); }); + it('does not fail if `trackId` throws', async () => { + const searchRequest = { params: {} }; + const options = { sessionId, isStored: false, isRestore: false }; + mockSessionClient.trackId = jest.fn().mockRejectedValue(undefined); + + mockStrategy.search.mockReturnValue( + of({ + id: 'my_id', + rawResponse: {} as any, + }) + ); + + await mockScopedClient.search(searchRequest, options).toPromise(); + + expect(mockSessionClient.trackId).toBeCalledTimes(1); + }); + it('calls `trackId` for every response, if the response contains an `id` and not restoring', async () => { const searchRequest = { params: {} }; const options = { sessionId, isStored: false, isRestore: false }; - mockSessionClient.trackId = jest.fn(); + mockSessionClient.trackId = jest.fn().mockResolvedValue(undefined); mockStrategy.search.mockReturnValue( of( @@ -195,7 +222,7 @@ describe('Search service', () => { const searchRequest = { params: {} }; const options = { sessionId, isStored: true, isRestore: true }; mockSessionClient.getId = jest.fn().mockResolvedValueOnce('my_id'); - mockSessionClient.trackId = jest.fn(); + mockSessionClient.trackId = jest.fn().mockResolvedValue(undefined); await mockScopedClient.search(searchRequest, options).toPromise(); @@ -206,12 +233,258 @@ describe('Search service', () => { const searchRequest = { params: {} }; const options = {}; mockSessionClient.getId = jest.fn().mockResolvedValueOnce('my_id'); - mockSessionClient.trackId = jest.fn(); + mockSessionClient.trackId = jest.fn().mockResolvedValue(undefined); await mockScopedClient.search(searchRequest, options).toPromise(); expect(mockSessionClient.trackId).not.toBeCalled(); }); }); + + describe('cancelSession', () => { + const mockSavedObject: SavedObject = { + id: 'd7170a35-7e2c-48d6-8dec-9a056721b489', + type: 'search-session', + attributes: { + name: 'my_name', + appId: 'my_app_id', + urlGeneratorId: 'my_url_generator_id', + idMapping: {}, + }, + references: [], + }; + + it('cancels a saved object with no search ids', async () => { + mockSessionClient.getSearchIdMapping = jest + .fn() + .mockResolvedValue(new Map()); + mockSessionClient.cancel = jest.fn().mockResolvedValue(mockSavedObject); + const cancelSpy = jest.spyOn(mockScopedClient, 'cancel'); + + await mockScopedClient.cancelSession('123'); + + expect(mockSessionClient.cancel).toHaveBeenCalledTimes(1); + expect(cancelSpy).not.toHaveBeenCalled(); + }); + + it('cancels a saved object and search ids', async () => { + const mockMap = new Map(); + mockMap.set('abc', 'es'); + mockSessionClient.getSearchIdMapping = jest.fn().mockResolvedValue(mockMap); + mockStrategy.cancel = jest.fn(); + mockSessionClient.cancel = jest.fn().mockResolvedValue(mockSavedObject); + + await mockScopedClient.cancelSession('123'); + + expect(mockSessionClient.cancel).toHaveBeenCalledTimes(1); + + const [searchId, options] = mockStrategy.cancel.mock.calls[0]; + expect(mockStrategy.cancel).toHaveBeenCalledTimes(1); + expect(searchId).toBe('abc'); + expect(options).toHaveProperty('strategy', 'es'); + }); + + it('cancels a saved object with some strategies that dont support cancellation, dont throw an error', async () => { + const mockMap = new Map(); + mockMap.set('abc', 'nocancel'); + mockMap.set('def', 'es'); + mockSessionClient.getSearchIdMapping = jest.fn().mockResolvedValue(mockMap); + mockStrategy.cancel = jest.fn(); + mockSessionClient.cancel = jest.fn().mockResolvedValue(mockSavedObject); + + await mockScopedClient.cancelSession('123'); + + expect(mockSessionClient.cancel).toHaveBeenCalledTimes(1); + + const [searchId, options] = mockStrategy.cancel.mock.calls[0]; + expect(mockStrategy.cancel).toHaveBeenCalledTimes(1); + expect(searchId).toBe('def'); + expect(options).toHaveProperty('strategy', 'es'); + }); + + it('cancels a saved object with some strategies that dont exist, dont throw an error', async () => { + const mockMap = new Map(); + mockMap.set('abc', 'notsupported'); + mockMap.set('def', 'es'); + mockSessionClient.getSearchIdMapping = jest.fn().mockResolvedValue(mockMap); + mockStrategy.cancel = jest.fn(); + mockSessionClient.cancel = jest.fn().mockResolvedValue(mockSavedObject); + + await mockScopedClient.cancelSession('123'); + + expect(mockSessionClient.cancel).toHaveBeenCalledTimes(1); + + const [searchId, options] = mockStrategy.cancel.mock.calls[0]; + expect(mockStrategy.cancel).toHaveBeenCalledTimes(1); + expect(searchId).toBe('def'); + expect(options).toHaveProperty('strategy', 'es'); + }); + }); + + describe('deleteSession', () => { + const mockSavedObject: SavedObject = { + id: 'd7170a35-7e2c-48d6-8dec-9a056721b489', + type: 'search-session', + attributes: { + name: 'my_name', + appId: 'my_app_id', + urlGeneratorId: 'my_url_generator_id', + idMapping: {}, + }, + references: [], + }; + + it('deletes a saved object with no search ids', async () => { + mockSessionClient.getSearchIdMapping = jest + .fn() + .mockResolvedValue(new Map()); + mockSessionClient.delete = jest.fn().mockResolvedValue(mockSavedObject); + const cancelSpy = jest.spyOn(mockScopedClient, 'cancel'); + + await mockScopedClient.deleteSession('123'); + + expect(mockSessionClient.delete).toHaveBeenCalledTimes(1); + expect(cancelSpy).not.toHaveBeenCalled(); + }); + + it('deletes a saved object and search ids', async () => { + const mockMap = new Map(); + mockMap.set('abc', 'es'); + mockSessionClient.getSearchIdMapping = jest.fn().mockResolvedValue(mockMap); + mockSessionClient.delete = jest.fn().mockResolvedValue(mockSavedObject); + mockStrategy.cancel = jest.fn(); + + await mockScopedClient.deleteSession('123'); + + expect(mockSessionClient.delete).toHaveBeenCalledTimes(1); + + const [searchId, options] = mockStrategy.cancel.mock.calls[0]; + expect(mockStrategy.cancel).toHaveBeenCalledTimes(1); + expect(searchId).toBe('abc'); + expect(options).toHaveProperty('strategy', 'es'); + }); + + it('deletes a saved object with some strategies that dont support cancellation, dont throw an error', async () => { + const mockMap = new Map(); + mockMap.set('abc', 'nocancel'); + mockMap.set('def', 'es'); + mockSessionClient.getSearchIdMapping = jest.fn().mockResolvedValue(mockMap); + mockSessionClient.delete = jest.fn().mockResolvedValue(mockSavedObject); + mockStrategy.cancel = jest.fn(); + + await mockScopedClient.deleteSession('123'); + + expect(mockSessionClient.delete).toHaveBeenCalledTimes(1); + + const [searchId, options] = mockStrategy.cancel.mock.calls[0]; + expect(mockStrategy.cancel).toHaveBeenCalledTimes(1); + expect(searchId).toBe('def'); + expect(options).toHaveProperty('strategy', 'es'); + }); + + it('deletes a saved object with some strategies that dont exist, dont throw an error', async () => { + const mockMap = new Map(); + mockMap.set('abc', 'notsupported'); + mockMap.set('def', 'es'); + mockSessionClient.getSearchIdMapping = jest.fn().mockResolvedValue(mockMap); + mockStrategy.cancel = jest.fn(); + mockSessionClient.delete = jest.fn().mockResolvedValue(mockSavedObject); + + await mockScopedClient.deleteSession('123'); + + expect(mockSessionClient.delete).toHaveBeenCalledTimes(1); + + const [searchId, options] = mockStrategy.cancel.mock.calls[0]; + expect(mockStrategy.cancel).toHaveBeenCalledTimes(1); + expect(searchId).toBe('def'); + expect(options).toHaveProperty('strategy', 'es'); + }); + }); + + describe('extendSession', () => { + const mockSavedObject: SavedObject = { + id: 'd7170a35-7e2c-48d6-8dec-9a056721b489', + type: 'search-session', + attributes: { + name: 'my_name', + appId: 'my_app_id', + urlGeneratorId: 'my_url_generator_id', + idMapping: {}, + }, + references: [], + }; + + it('extends a saved object with no search ids', async () => { + mockSessionClient.getSearchIdMapping = jest + .fn() + .mockResolvedValue(new Map()); + mockSessionClient.extend = jest.fn().mockResolvedValue(mockSavedObject); + mockStrategy.extend = jest.fn(); + + await mockScopedClient.extendSession('123', new Date('2020-01-01')); + + expect(mockSessionClient.extend).toHaveBeenCalledTimes(1); + expect(mockStrategy.extend).not.toHaveBeenCalled(); + }); + + it('extends a saved object and search ids', async () => { + const mockMap = new Map(); + mockMap.set('abc', 'es'); + mockSessionClient.getSearchIdMapping = jest.fn().mockResolvedValue(mockMap); + mockSessionClient.extend = jest.fn().mockResolvedValue(mockSavedObject); + mockStrategy.extend = jest.fn(); + + await mockScopedClient.extendSession('123', new Date('2020-01-01')); + + expect(mockSessionClient.extend).toHaveBeenCalledTimes(1); + expect(mockStrategy.extend).toHaveBeenCalledTimes(1); + const [searchId, keepAlive, options] = mockStrategy.extend.mock.calls[0]; + expect(searchId).toBe('abc'); + expect(keepAlive).toContain('ms'); + expect(options).toHaveProperty('strategy', 'es'); + }); + + it('doesnt extend the saved object with some strategies that dont support cancellation, throws an error', async () => { + const mockMap = new Map(); + mockMap.set('abc', 'nocancel'); + mockMap.set('def', 'es'); + mockSessionClient.getSearchIdMapping = jest.fn().mockResolvedValue(mockMap); + mockSessionClient.extend = jest.fn().mockResolvedValue(mockSavedObject); + mockStrategy.extend = jest.fn().mockResolvedValue({}); + + const extendRes = mockScopedClient.extendSession('123', new Date('2020-01-01')); + + await expect(extendRes).rejects.toThrowError( + 'Failed to extend the expiration of some searches' + ); + + expect(mockSessionClient.extend).not.toHaveBeenCalled(); + const [searchId, keepAlive, options] = mockStrategy.extend.mock.calls[0]; + expect(searchId).toBe('def'); + expect(keepAlive).toContain('ms'); + expect(options).toHaveProperty('strategy', 'es'); + }); + + it('doesnt extend the saved object with some strategies that dont exist, throws an error', async () => { + const mockMap = new Map(); + mockMap.set('abc', 'notsupported'); + mockMap.set('def', 'es'); + mockSessionClient.getSearchIdMapping = jest.fn().mockResolvedValue(mockMap); + mockSessionClient.extend = jest.fn().mockResolvedValue(mockSavedObject); + mockStrategy.extend = jest.fn().mockResolvedValue({}); + + const extendRes = mockScopedClient.extendSession('123', new Date('2020-01-01')); + + await expect(extendRes).rejects.toThrowError( + 'Failed to extend the expiration of some searches' + ); + + expect(mockSessionClient.extend).not.toHaveBeenCalled(); + const [searchId, keepAlive, options] = mockStrategy.extend.mock.calls[0]; + expect(searchId).toBe('def'); + expect(keepAlive).toContain('ms'); + expect(options).toHaveProperty('strategy', 'es'); + }); + }); }); }); diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index ce0771a1e9df8..6ece8ff945468 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -275,7 +275,10 @@ export class SearchService implements Plugin { switchMap((searchRequest) => strategy.search(searchRequest, options, deps)), tap((response) => { if (!options.sessionId || !response.id || options.isRestore) return; - deps.searchSessionsClient.trackId(request, response.id, options); + // intentionally swallow tracking error, as it shouldn't fail the search + deps.searchSessionsClient.trackId(request, response.id, options).catch((trackErr) => { + this.logger.error(trackErr); + }); }) ); } catch (e) { @@ -283,7 +286,11 @@ export class SearchService implements Plugin { } }; - private cancel = (deps: SearchStrategyDependencies, id: string, options: ISearchOptions = {}) => { + private cancel = async ( + deps: SearchStrategyDependencies, + id: string, + options: ISearchOptions = {} + ) => { const strategy = this.getSearchStrategy(options.strategy); if (!strategy.cancel) { throw new KbnServerError( @@ -294,7 +301,7 @@ export class SearchService implements Plugin { return strategy.cancel(id, options, deps); }; - private extend = ( + private extend = async ( deps: SearchStrategyDependencies, id: string, keepAlive: string, @@ -309,25 +316,26 @@ export class SearchService implements Plugin { private cancelSessionSearches = async (deps: SearchStrategyDependencies, sessionId: string) => { const searchIdMapping = await deps.searchSessionsClient.getSearchIdMapping(sessionId); - - for (const [searchId, strategyName] of searchIdMapping.entries()) { - const searchOptions = { - sessionId, - strategy: strategyName, - isStored: true, - }; - this.cancel(deps, searchId, searchOptions); - } + await Promise.allSettled( + Array.from(searchIdMapping).map(([searchId, strategyName]) => { + const searchOptions = { + sessionId, + strategy: strategyName, + isStored: true, + }; + return this.cancel(deps, searchId, searchOptions); + }) + ); }; private cancelSession = async (deps: SearchStrategyDependencies, sessionId: string) => { const response = await deps.searchSessionsClient.cancel(sessionId); - this.cancelSessionSearches(deps, sessionId); + await this.cancelSessionSearches(deps, sessionId); return response; }; private deleteSession = async (deps: SearchStrategyDependencies, sessionId: string) => { - this.cancelSessionSearches(deps, sessionId); + await this.cancelSessionSearches(deps, sessionId); return deps.searchSessionsClient.delete(sessionId); }; @@ -339,13 +347,19 @@ export class SearchService implements Plugin { const searchIdMapping = await deps.searchSessionsClient.getSearchIdMapping(sessionId); const keepAlive = `${moment(expires).diff(moment())}ms`; - for (const [searchId, strategyName] of searchIdMapping.entries()) { - const searchOptions = { - sessionId, - strategy: strategyName, - isStored: true, - }; - await this.extend(deps, searchId, keepAlive, searchOptions); + const result = await Promise.allSettled( + Array.from(searchIdMapping).map(([searchId, strategyName]) => { + const searchOptions = { + sessionId, + strategy: strategyName, + isStored: true, + }; + return this.extend(deps, searchId, keepAlive, searchOptions); + }) + ); + + if (result.some((extRes) => extRes.status === 'rejected')) { + throw new Error('Failed to extend the expiration of some searches'); } return deps.searchSessionsClient.extend(sessionId, expires); From 810e4ab8e8206949965c89c889aac2fc396c4111 Mon Sep 17 00:00:00 2001 From: John Schulz Date: Tue, 9 Feb 2021 08:54:51 -0500 Subject: [PATCH 15/28] [Fleet] Prevent agents from enrolling in a managed policy (#90458) ## Summary Add guard to `/agents/enroll` API preventing agents from enrolling in managed policies closes #90435 - [x] No Agents can be enrolled into this policy by the user. - [x] The install & enroll commands should print an error to the console if the enroll command fails (due to being a managed policy or any other reason) #### So how do you associate an agent with a managed policy? Enroll in an unmanaged policy then set that policy to managed. We don't restrict the agent policy, only what other things (agents, integrations) can do if they're associated with a managed policy. A _force flag_ has been mentioned for some other actions. It might work here as well, but I'd like to handle discussion & implementation of those later. ### Manual testing
Prevent enroll for managed policies 1. Created a managed agent policy ``` curl --user elastic:changeme -X POST localhost:5601/api/fleet/agent_policies -H 'Content-Type: application/json' -d'{ "name": "User created MANAGED", "namespace": "default", "is_managed": true}' -H 'kbn-xsrf: true' {"item":{"id":"3bd07db0-67d0-11eb-b656-21ad68ebfa8a","name":"User created MANAGED","namespace":"default","is_managed":true,"revision":1,"updated_at":"2021-02-05T16:36:01.931Z","updated_by":"elastic"}} ``` 2. Try `install` command show in the UI ``` sudo ./elastic-agent install -f --kibana-url=http://localhost:5601 --enrollment-token=WmcwTWMzY0IzWlBUUWJJUjZqRDA6UGRZelVlaS1STml1cVdjSUVwSkJRQQ== --insecure Password: The Elastic Agent is currently in BETA and should not be used in production Error: fail to enroll: fail to execute request to Kibana: Status code: 400, Kibana returned an error: Bad Request, message: Cannot enroll in managed policy 3bd07db0-67d0-11eb-b656-21ad68ebfa8a Error: enroll command failed with exit code: 1 ``` 3. Observe `Cannot enroll in managed policy 3bd07db0-67d0-11eb-b656-21ad68ebfa8a` error 4. Try `enroll` instead: ``` sudo ./elastic-agent enroll http://localhost:5601 WmcwTWMzY0IzWlBUUWJJUjZqRDA6UGRZelVlaS1STml1cVdjSUVwSkJRQQ== --insecure The Elastic Agent is currently in BETA and should not be used in production This will replace your current settings. Do you want to continue? [Y/n]: Error: fail to enroll: fail to execute request to Kibana: Status code: 400, Kibana returned an error: Bad Request, message: Cannot enroll in managed policy 3bd07db0-67d0-11eb-b656-21ad68ebfa8a ``` 5. Observe same `Cannot enroll in managed policy 3bd07db0-67d0-11eb-b656-21ad68ebfa8a` error
Enroll in unmanaged policy, then update it to managed Agent policies are `is_managed: false` by default, or we can update the policy to `is_managed: false`, like: ``` curl --user elastic:changeme -X PUT localhost:5601/api/fleet/agent_policies/3bd07db0-67d0-11eb-b656-21ad68ebfa8a -H 'Content-Type: application/json' -d'{ "is_managed": false, "name": "xyz", "namespace": "default" }' -H 'kbn-xsrf: true' {"item":{"id":"3bd07db0-67d0-11eb-b656-21ad68ebfa8a","name":"xyz","namespace":"default","is_managed":false,"revision":4,"updated_at":"2021-02-05T17:42:05.610Z","updated_by":"elastic","package_policies":[]}} ``` then enroll ``` sudo ./elastic-agent install -f --kibana-url=http://localhost:5601 --enrollment-token=WmcwTWMzY0IzWlBUUWJJUjZqRDA6UGRZelVlaS1STml1cVdjSUVwSkJRQQ== --insecure The Elastic Agent is currently in BETA and should not be used in production Successfully enrolled the Elastic Agent. Installation was successful and Elastic Agent is running. ``` and set the policy back to managed ``` curl --user elastic:changeme -X PUT localhost:5601/api/fleet/agent_policies/3bd07db0-67d0-11eb-b656-21ad68ebfa8a -H 'Content-Type: application/json' -d'{ "is_managed": true, "name": "xyz", "namespace": "default" }' -H 'kbn-xsrf: true' {"item":{"id":"3bd07db0-67d0-11eb-b656-21ad68ebfa8a","name":"xyz","namespace":"default","is_managed":true,"revision":5,"updated_at":"2021-02-05T17:44:18.757Z","updated_by":"elastic","package_policies":[]}} ``` with all the restrictions that entails (cannot unenroll, reassign, etc) ``` curl --user elastic:changeme -X PUT 'http://localhost:5601/api/fleet/agents/8169f0a0-67d9-11eb-80f2-73dd45e7318e/reassign' -X 'PUT' -H 'kbn-xsrf: abc' -H 'Content-Type: application/json' --data-raw '{"policy_id":"729f8440-67cf-11eb-b656-21ad68ebfa8a"}' { "statusCode": 400, "error": "Bad Request", "message": "Cannot reassign an agent from managed agent policy 3bd07db0-67d0-11eb-b656-21ad68ebfa8a" } ```
### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../fleet/server/services/agents/enroll.ts | 11 ++++- .../apis/agents/enroll.ts | 48 ++++++++++++++++++- 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/fleet/server/services/agents/enroll.ts b/x-pack/plugins/fleet/server/services/agents/enroll.ts index c984a84ceea01..6ca19bf884cca 100644 --- a/x-pack/plugins/fleet/server/services/agents/enroll.ts +++ b/x-pack/plugins/fleet/server/services/agents/enroll.ts @@ -11,11 +11,13 @@ import semverParse from 'semver/functions/parse'; import semverDiff from 'semver/functions/diff'; import semverLte from 'semver/functions/lte'; -import { SavedObjectsClientContract } from 'src/core/server'; -import { AgentType, Agent, AgentSOAttributes, FleetServerAgent } from '../../types'; +import type { SavedObjectsClientContract } from 'src/core/server'; +import type { AgentType, Agent, AgentSOAttributes, FleetServerAgent } from '../../types'; import { savedObjectToAgent } from './saved_objects'; import { AGENT_SAVED_OBJECT_TYPE, AGENTS_INDEX } from '../../constants'; +import { IngestManagerError } from '../../errors'; import * as APIKeyService from '../api_keys'; +import { agentPolicyService } from '../../services'; import { appContextService } from '../app_context'; export async function enroll( @@ -27,6 +29,11 @@ export async function enroll( const agentVersion = metadata?.local?.elastic?.agent?.version; validateAgentVersion(agentVersion); + const agentPolicy = await agentPolicyService.get(soClient, agentPolicyId, false); + if (agentPolicy?.is_managed) { + throw new IngestManagerError(`Cannot enroll in managed policy ${agentPolicyId}`); + } + if (appContextService.getConfig()?.agents?.fleetServerEnabled) { const esClient = appContextService.getInternalUserESClient(); diff --git a/x-pack/test/fleet_api_integration/apis/agents/enroll.ts b/x-pack/test/fleet_api_integration/apis/agents/enroll.ts index 96c472697801e..3358d045fe69b 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/enroll.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/enroll.ts @@ -18,8 +18,9 @@ export default function (providerContext: FtrProviderContext) { const esArchiver = getService('esArchiver'); const esClient = getService('es'); const kibanaServer = getService('kibanaServer'); - + const supertestWithAuth = getService('supertest'); const supertest = getSupertestWithoutAuth(providerContext); + let apiKey: { id: string; api_key: string }; let kibanaVersion: string; @@ -58,6 +59,51 @@ export default function (providerContext: FtrProviderContext) { await esArchiver.unload('fleet/agents'); }); + it('should not allow enrolling in a managed policy', async () => { + // update existing policy to managed + await supertestWithAuth + .put(`/api/fleet/agent_policies/policy1`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'Test policy', + namespace: 'default', + is_managed: true, + }) + .expect(200); + + // try to enroll in managed policy + const { body } = await supertest + .post(`/api/fleet/agents/enroll`) + .set('kbn-xsrf', 'xxx') + .set( + 'Authorization', + `ApiKey ${Buffer.from(`${apiKey.id}:${apiKey.api_key}`).toString('base64')}` + ) + .send({ + type: 'PERMANENT', + metadata: { + local: { + elastic: { agent: { version: kibanaVersion } }, + }, + user_provided: {}, + }, + }) + .expect(400); + + expect(body.message).to.contain('Cannot enroll in managed policy'); + + // restore to original (unmanaged) + await supertestWithAuth + .put(`/api/fleet/agent_policies/policy1`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'Test policy', + namespace: 'default', + is_managed: false, + }) + .expect(200); + }); + it('should not allow to enroll an agent with a invalid enrollment', async () => { await supertest .post(`/api/fleet/agents/enroll`) From 82d1672e79c4a90dbabf11be605de3ed910592db Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Tue, 9 Feb 2021 15:38:35 +0100 Subject: [PATCH 16/28] [Core] add timeout for "stop" lifecycle (#90432) * add timeout for stop lifecycle * add timeout for stop lifecycle * update message * cleanup timeout to remove async tasks --- packages/kbn-std/src/promise.test.ts | 22 ++++------ packages/kbn-std/src/promise.ts | 27 +++++++----- src/core/public/plugins/plugins_service.ts | 26 +++++++++--- .../server/plugins/plugins_system.test.ts | 42 +++++++++++++++++++ src/core/server/plugins/plugins_system.ts | 36 ++++++++++++---- 5 files changed, 117 insertions(+), 36 deletions(-) diff --git a/packages/kbn-std/src/promise.test.ts b/packages/kbn-std/src/promise.test.ts index f7c119acd0c7a..bf4f3951d5850 100644 --- a/packages/kbn-std/src/promise.test.ts +++ b/packages/kbn-std/src/promise.test.ts @@ -12,40 +12,36 @@ const delay = (ms: number, resolveValue?: any) => new Promise((resolve) => setTimeout(resolve, ms, resolveValue)); describe('withTimeout', () => { - it('resolves with a promise value if resolved in given timeout', async () => { + it('resolves with a promise value and "timedout: false" if resolved in given timeout', async () => { await expect( withTimeout({ promise: delay(10, 'value'), - timeout: 200, - errorMessage: 'error-message', + timeoutMs: 200, }) - ).resolves.toBe('value'); + ).resolves.toStrictEqual({ value: 'value', timedout: false }); }); - it('rejects with errorMessage if not resolved in given time', async () => { + it('resolves with "timedout: false" if not resolved in given time', async () => { await expect( withTimeout({ promise: delay(200, 'value'), - timeout: 10, - errorMessage: 'error-message', + timeoutMs: 10, }) - ).rejects.toMatchInlineSnapshot(`[Error: error-message]`); + ).resolves.toStrictEqual({ timedout: true }); await expect( withTimeout({ promise: new Promise((i) => i), - timeout: 10, - errorMessage: 'error-message', + timeoutMs: 10, }) - ).rejects.toMatchInlineSnapshot(`[Error: error-message]`); + ).resolves.toStrictEqual({ timedout: true }); }); it('does not swallow promise error', async () => { await expect( withTimeout({ promise: Promise.reject(new Error('from-promise')), - timeout: 10, - errorMessage: 'error-message', + timeoutMs: 10, }) ).rejects.toMatchInlineSnapshot(`[Error: from-promise]`); }); diff --git a/packages/kbn-std/src/promise.ts b/packages/kbn-std/src/promise.ts index 9d8f7703c026d..9209c2ce372c6 100644 --- a/packages/kbn-std/src/promise.ts +++ b/packages/kbn-std/src/promise.ts @@ -6,19 +6,26 @@ * Side Public License, v 1. */ -export function withTimeout({ +export async function withTimeout({ promise, - timeout, - errorMessage, + timeoutMs, }: { promise: Promise; - timeout: number; - errorMessage: string; -}) { - return Promise.race([ - promise, - new Promise((resolve, reject) => setTimeout(() => reject(new Error(errorMessage)), timeout)), - ]) as Promise; + timeoutMs: number; +}): Promise<{ timedout: true } | { timedout: false; value: T }> { + let timeout: NodeJS.Timeout | undefined; + try { + return (await Promise.race([ + promise.then((v) => ({ value: v, timedout: false })), + new Promise((resolve) => { + timeout = setTimeout(() => resolve({ timedout: true }), timeoutMs); + }), + ])) as Promise<{ timedout: true } | { timedout: false; value: T }>; + } finally { + if (timeout !== undefined) { + clearTimeout(timeout); + } + } } export function isPromise(maybePromise: T | Promise): maybePromise is Promise { diff --git a/src/core/public/plugins/plugins_service.ts b/src/core/public/plugins/plugins_service.ts index 57fbe4cbecd12..230a675b4cda6 100644 --- a/src/core/public/plugins/plugins_service.ts +++ b/src/core/public/plugins/plugins_service.ts @@ -111,11 +111,18 @@ export class PluginsService implements CoreService { `); }); }); + +describe('stop', () => { + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + it('waits for 30 sec to finish "stop" and move on to the next plugin.', async () => { + const [plugin1, plugin2] = [createPlugin('timeout-stop-1'), createPlugin('timeout-stop-2')].map( + (plugin, index) => { + jest.spyOn(plugin, 'setup').mockResolvedValue(`setup-as-${index}`); + jest.spyOn(plugin, 'start').mockResolvedValue(`started-as-${index}`); + pluginsSystem.addPlugin(plugin); + return plugin; + } + ); + + const stopSpy1 = jest + .spyOn(plugin1, 'stop') + .mockImplementationOnce(() => new Promise((resolve) => resolve)); + const stopSpy2 = jest.spyOn(plugin2, 'stop').mockImplementationOnce(() => Promise.resolve()); + + mockCreatePluginSetupContext.mockImplementation(() => ({})); + + await pluginsSystem.setupPlugins(setupDeps); + const stopPromise = pluginsSystem.stopPlugins(); + + jest.runAllTimers(); + await stopPromise; + expect(stopSpy1).toHaveBeenCalledTimes(1); + expect(stopSpy2).toHaveBeenCalledTimes(1); + + expect(loggingSystemMock.collect(logger).warn.flat()).toEqual( + expect.arrayContaining([ + `"timeout-stop-1" plugin didn't stop in 30sec., move on to the next.`, + ]) + ); + }); +}); diff --git a/src/core/server/plugins/plugins_system.ts b/src/core/server/plugins/plugins_system.ts index b7b8c297ea571..0244254838fab 100644 --- a/src/core/server/plugins/plugins_system.ts +++ b/src/core/server/plugins/plugins_system.ts @@ -105,11 +105,18 @@ export class PluginsSystem { `Plugin ${pluginName} is using asynchronous setup lifecycle. Asynchronous plugins support will be removed in a later version.` ); } - contract = await withTimeout({ + const contractMaybe = await withTimeout({ promise: contractOrPromise, - timeout: 10 * Sec, - errorMessage: `Setup lifecycle of "${pluginName}" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.`, + timeoutMs: 10 * Sec, }); + + if (contractMaybe.timedout) { + throw new Error( + `Setup lifecycle of "${pluginName}" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.` + ); + } else { + contract = contractMaybe.value; + } } else { contract = contractOrPromise; } @@ -154,11 +161,18 @@ export class PluginsSystem { `Plugin ${pluginName} is using asynchronous start lifecycle. Asynchronous plugins support will be removed in a later version.` ); } - contract = await withTimeout({ + const contractMaybe = await withTimeout({ promise: contractOrPromise, - timeout: 10 * Sec, - errorMessage: `Start lifecycle of "${pluginName}" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.`, + timeoutMs: 10 * Sec, }); + + if (contractMaybe.timedout) { + throw new Error( + `Start lifecycle of "${pluginName}" plugin wasn't completed in 10sec. Consider disabling the plugin and re-start.` + ); + } else { + contract = contractMaybe.value; + } } else { contract = contractOrPromise; } @@ -181,7 +195,15 @@ export class PluginsSystem { const pluginName = this.satupPlugins.pop()!; this.log.debug(`Stopping plugin "${pluginName}"...`); - await this.plugins.get(pluginName)!.stop(); + + const resultMaybe = await withTimeout({ + promise: this.plugins.get(pluginName)!.stop(), + timeoutMs: 30 * Sec, + }); + + if (resultMaybe?.timedout) { + this.log.warn(`"${pluginName}" plugin didn't stop in 30sec., move on to the next.`); + } } } From 9314b8e2fadf78b99ecbcd8588d2507076b42563 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 9 Feb 2021 07:57:00 -0700 Subject: [PATCH 17/28] [Maps] use chart pallete registry to support sync colors in dashboard (#88099) * [Maps] use chart pallete registry to support sync colors in dashboard * pass getColor to createLayerInstance * use chartsPaletteServiceGetColor to get categorical color * revert changes to layer_actions * tslint and jest test updates Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/maps/kibana.json | 3 +- .../blended_vector_layer.ts | 8 ++++- .../layers/vector_layer/vector_layer.tsx | 15 ++++++-- .../properties/dynamic_color_property.tsx | 35 ++++++++++++++++--- .../classes/styles/vector/vector_style.tsx | 21 +++++++---- .../maps/public/embeddable/map_embeddable.tsx | 28 ++++++++++++++- x-pack/plugins/maps/public/kibana_services.ts | 20 +++++++++++ x-pack/plugins/maps/public/plugin.ts | 2 ++ .../reducers/non_serializable_instances.d.ts | 9 +++++ .../reducers/non_serializable_instances.js | 20 +++++++++++ x-pack/plugins/maps/public/reducers/store.js | 1 + .../public/selectors/map_selectors.test.ts | 5 --- .../maps/public/selectors/map_selectors.ts | 15 +++++--- 13 files changed, 156 insertions(+), 26 deletions(-) diff --git a/x-pack/plugins/maps/kibana.json b/x-pack/plugins/maps/kibana.json index 744cc18c36f3e..3966af9e28742 100644 --- a/x-pack/plugins/maps/kibana.json +++ b/x-pack/plugins/maps/kibana.json @@ -25,7 +25,8 @@ ], "optionalPlugins": [ "home", - "savedObjectsTagging" + "savedObjectsTagging", + "charts" ], "ui": true, "server": true, diff --git a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts index efd022292f90b..5d4b915c4e971 100644 --- a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts +++ b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts @@ -169,6 +169,7 @@ function getClusterStyleDescriptor( } export interface BlendedVectorLayerArguments { + chartsPaletteServiceGetColor?: (value: string) => string | null; source: IVectorSource; layerDescriptor: VectorLayerDescriptor; } @@ -205,7 +206,12 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer { this._documentStyle, this._clusterSource ); - this._clusterStyle = new VectorStyle(clusterStyleDescriptor, this._clusterSource, this); + this._clusterStyle = new VectorStyle( + clusterStyleDescriptor, + this._clusterSource, + this, + options.chartsPaletteServiceGetColor + ); let isClustered = false; const countDataRequest = this.getDataRequest(ACTIVE_COUNT_DATA_ID); diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx index ee1cda6eaee43..e9c0cb29c7c17 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx @@ -81,6 +81,7 @@ export interface VectorLayerArguments { source: IVectorSource; joins?: InnerJoin[]; layerDescriptor: VectorLayerDescriptor; + chartsPaletteServiceGetColor?: (value: string) => string | null; } export interface IVectorLayer extends ILayer { @@ -119,13 +120,23 @@ export class VectorLayer extends AbstractLayer { return layerDescriptor as VectorLayerDescriptor; } - constructor({ layerDescriptor, source, joins = [] }: VectorLayerArguments) { + constructor({ + layerDescriptor, + source, + joins = [], + chartsPaletteServiceGetColor, + }: VectorLayerArguments) { super({ layerDescriptor, source, }); this._joins = joins; - this._style = new VectorStyle(layerDescriptor.style, source, this); + this._style = new VectorStyle( + layerDescriptor.style, + source, + this, + chartsPaletteServiceGetColor + ); } getSource(): IVectorSource { diff --git a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.tsx b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.tsx index cac56ad1c8a57..d654cdc6bff51 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.tsx @@ -16,7 +16,12 @@ import { getPercentilesMbColorRampStops, getColorPalette, } from '../../color_palettes'; -import { COLOR_MAP_TYPE, DATA_MAPPING_FUNCTION } from '../../../../../common/constants'; +import { + COLOR_MAP_TYPE, + DATA_MAPPING_FUNCTION, + FieldFormatter, + VECTOR_STYLES, +} from '../../../../../common/constants'; import { isCategoricalStopsInvalid, getOtherCategoryLabel, @@ -26,6 +31,8 @@ import { Break, BreakedLegend } from '../components/legend/breaked_legend'; import { ColorDynamicOptions, OrdinalColorStop } from '../../../../../common/descriptor_types'; import { LegendProps } from './style_property'; import { getOrdinalSuffix } from '../../../util/ordinal_suffix'; +import { IField } from '../../../fields/field'; +import { IVectorLayer } from '../../../layers/vector_layer/vector_layer'; const UP_TO = i18n.translate('xpack.maps.legend.upto', { defaultMessage: 'up to', @@ -34,6 +41,20 @@ const EMPTY_STOPS = { stops: [], defaultColor: null }; const RGBA_0000 = 'rgba(0,0,0,0)'; export class DynamicColorProperty extends DynamicStyleProperty { + private readonly _chartsPaletteServiceGetColor?: (value: string) => string | null; + + constructor( + options: ColorDynamicOptions, + styleName: VECTOR_STYLES, + field: IField | null, + vectorLayer: IVectorLayer, + getFieldFormatter: (fieldName: string) => null | FieldFormatter, + chartsPaletteServiceGetColor?: (value: string) => string | null + ) { + super(options, styleName, field, vectorLayer, getFieldFormatter); + this._chartsPaletteServiceGetColor = chartsPaletteServiceGetColor; + } + syncCircleColorWithMb(mbLayerId: string, mbMap: MbMap, alpha: number) { const color = this._getMbColor(); mbMap.setPaintProperty(mbLayerId, 'circle-color', color); @@ -260,12 +281,16 @@ export class DynamicColorProperty extends DynamicStyleProperty { - if (stop !== null) { + stops.forEach(({ stop, color }: { stop: string | number | null; color: string | null }) => { + if (stop !== null && color != null) { breaks.push({ color, symbolId, diff --git a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx index cef5f5048e9af..c61e72807224a 100644 --- a/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx +++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.tsx @@ -178,7 +178,8 @@ export class VectorStyle implements IVectorStyle { constructor( descriptor: VectorStyleDescriptor | null, source: IVectorSource, - layer: IVectorLayer + layer: IVectorLayer, + chartsPaletteServiceGetColor?: (value: string) => string | null ) { this._source = source; this._layer = layer; @@ -197,11 +198,13 @@ export class VectorStyle implements IVectorStyle { ); this._lineColorStyleProperty = this._makeColorProperty( this._descriptor.properties[VECTOR_STYLES.LINE_COLOR], - VECTOR_STYLES.LINE_COLOR + VECTOR_STYLES.LINE_COLOR, + chartsPaletteServiceGetColor ); this._fillColorStyleProperty = this._makeColorProperty( this._descriptor.properties[VECTOR_STYLES.FILL_COLOR], - VECTOR_STYLES.FILL_COLOR + VECTOR_STYLES.FILL_COLOR, + chartsPaletteServiceGetColor ); this._lineWidthStyleProperty = this._makeSizeProperty( this._descriptor.properties[VECTOR_STYLES.LINE_WIDTH], @@ -230,11 +233,13 @@ export class VectorStyle implements IVectorStyle { ); this._labelColorStyleProperty = this._makeColorProperty( this._descriptor.properties[VECTOR_STYLES.LABEL_COLOR], - VECTOR_STYLES.LABEL_COLOR + VECTOR_STYLES.LABEL_COLOR, + chartsPaletteServiceGetColor ); this._labelBorderColorStyleProperty = this._makeColorProperty( this._descriptor.properties[VECTOR_STYLES.LABEL_BORDER_COLOR], - VECTOR_STYLES.LABEL_BORDER_COLOR + VECTOR_STYLES.LABEL_BORDER_COLOR, + chartsPaletteServiceGetColor ); this._labelBorderSizeStyleProperty = new LabelBorderSizeProperty( this._descriptor.properties[VECTOR_STYLES.LABEL_BORDER_SIZE].options, @@ -890,7 +895,8 @@ export class VectorStyle implements IVectorStyle { _makeColorProperty( descriptor: ColorStylePropertyDescriptor | undefined, - styleName: VECTOR_STYLES + styleName: VECTOR_STYLES, + chartsPaletteServiceGetColor?: (value: string) => string | null ) { if (!descriptor || !descriptor.options) { return new StaticColorProperty({ color: '' }, styleName); @@ -904,7 +910,8 @@ export class VectorStyle implements IVectorStyle { styleName, field, this._layer, - this._getFieldFormatter + this._getFieldFormatter, + chartsPaletteServiceGetColor ); } else { throw new Error(`${descriptor} not implemented`); diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx index a1d65bf08c458..b769ac489f565 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx @@ -37,6 +37,7 @@ import { import { getIsLayerTOCOpen, getOpenTOCDetails } from '../selectors/ui_selectors'; import { getInspectorAdapters, + setChartsPaletteServiceGetColor, setEventHandlers, EventHandlers, } from '../reducers/non_serializable_instances'; @@ -54,7 +55,12 @@ import { RawValue, } from '../../common/constants'; import { RenderToolTipContent } from '../classes/tooltips/tooltip_property'; -import { getUiActions, getCoreI18n, getHttp } from '../kibana_services'; +import { + getUiActions, + getCoreI18n, + getHttp, + getChartsPaletteServiceGetColor, +} from '../kibana_services'; import { LayerDescriptor } from '../../common/descriptor_types'; import { MapContainer } from '../connected_components/map_container'; import { SavedMap } from '../routes/map_page'; @@ -83,6 +89,7 @@ export class MapEmbeddable private _prevQuery?: Query; private _prevRefreshConfig?: RefreshInterval; private _prevFilters?: Filter[]; + private _prevSyncColors?: boolean; private _prevSearchSessionId?: string; private _domNode?: HTMLElement; private _unsubscribeFromStore?: Unsubscribe; @@ -126,6 +133,8 @@ export class MapEmbeddable } private _initializeStore() { + this._dispatchSetChartsPaletteServiceGetColor(this.input.syncColors); + const store = this._savedMap.getStore(); store.dispatch(setReadOnly(true)); store.dispatch(disableScrollZoom()); @@ -221,6 +230,10 @@ export class MapEmbeddable if (this.input.refreshConfig && !_.isEqual(this.input.refreshConfig, this._prevRefreshConfig)) { this._dispatchSetRefreshConfig(this.input.refreshConfig); } + + if (this.input.syncColors !== this._prevSyncColors) { + this._dispatchSetChartsPaletteServiceGetColor(this.input.syncColors); + } } _dispatchSetQuery({ @@ -261,6 +274,19 @@ export class MapEmbeddable ); } + async _dispatchSetChartsPaletteServiceGetColor(syncColors?: boolean) { + this._prevSyncColors = syncColors; + const chartsPaletteServiceGetColor = syncColors + ? await getChartsPaletteServiceGetColor() + : null; + if (syncColors !== this._prevSyncColors) { + return; + } + this._savedMap + .getStore() + .dispatch(setChartsPaletteServiceGetColor(chartsPaletteServiceGetColor)); + } + /** * * @param {HTMLElement} domNode diff --git a/x-pack/plugins/maps/public/kibana_services.ts b/x-pack/plugins/maps/public/kibana_services.ts index 632a5f5382f73..4a7bccb31380d 100644 --- a/x-pack/plugins/maps/public/kibana_services.ts +++ b/x-pack/plugins/maps/public/kibana_services.ts @@ -11,6 +11,7 @@ import { MapsLegacyConfig } from '../../../../src/plugins/maps_legacy/config'; import { MapsConfigType } from '../config'; import { MapsPluginStartDependencies } from './plugin'; import { EMSSettings } from '../common/ems_settings'; +import { PaletteRegistry } from '../../../../src/plugins/charts/public'; let kibanaVersion: string; export const setKibanaVersion = (version: string) => (kibanaVersion = version); @@ -83,3 +84,22 @@ export const getShareService = () => pluginsStart.share; export const getIsAllowByValueEmbeddables = () => pluginsStart.dashboard.dashboardFeatureFlagConfig.allowByValueEmbeddables; + +export async function getChartsPaletteServiceGetColor(): Promise< + ((value: string) => string) | null +> { + const paletteRegistry: PaletteRegistry | null = pluginsStart.charts + ? await pluginsStart.charts.palettes.getPalettes() + : null; + if (!paletteRegistry) { + return null; + } + + const paletteDefinition = paletteRegistry.get('default'); + const chartConfiguration = { syncColors: true }; + return (value: string) => { + const series = [{ name: value, rankAtDepth: 0, totalSeriesAtDepth: 1 }]; + const color = paletteDefinition.getColor(series, chartConfiguration); + return color ? color : '#3d3d3d'; + }; +} diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index 8889d1d44f10f..4c668e0a2276b 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -64,6 +64,7 @@ import { } from './licensed_features'; import { EMSSettings } from '../common/ems_settings'; import { SavedObjectTaggingPluginStart } from '../../saved_objects_tagging/public'; +import { ChartsPluginStart } from '../../../../src/plugins/charts/public'; export interface MapsPluginSetupDependencies { inspector: InspectorSetupContract; @@ -76,6 +77,7 @@ export interface MapsPluginSetupDependencies { } export interface MapsPluginStartDependencies { + charts: ChartsPluginStart; data: DataPublicPluginStart; embeddable: EmbeddableStart; mapsFileUpload: FileUploadStartContract; diff --git a/x-pack/plugins/maps/public/reducers/non_serializable_instances.d.ts b/x-pack/plugins/maps/public/reducers/non_serializable_instances.d.ts index 54a90946a5a89..9808a5e09b8ab 100644 --- a/x-pack/plugins/maps/public/reducers/non_serializable_instances.d.ts +++ b/x-pack/plugins/maps/public/reducers/non_serializable_instances.d.ts @@ -15,6 +15,7 @@ export type NonSerializableState = { inspectorAdapters: Adapters; cancelRequestCallbacks: Map {}>; // key is request token, value is cancel callback eventHandlers: Partial; + chartsPaletteServiceGetColor: (value: string) => string | null; }; export interface ResultMeta { @@ -58,6 +59,14 @@ export function getInspectorAdapters(state: MapStoreState): Adapters; export function getEventHandlers(state: MapStoreState): Partial; +export function getChartsPaletteServiceGetColor( + state: MapStoreState +): (value: string) => string | null; + +export function setChartsPaletteServiceGetColor( + chartsPaletteServiceGetColor: ((value: string) => string) | null +): AnyAction; + export function cancelRequest(requestToken?: symbol): AnyAction; export function registerCancelCallback(requestToken: symbol, callback: () => void): AnyAction; diff --git a/x-pack/plugins/maps/public/reducers/non_serializable_instances.js b/x-pack/plugins/maps/public/reducers/non_serializable_instances.js index 46846a8df3f23..4cc4e91a308a5 100644 --- a/x-pack/plugins/maps/public/reducers/non_serializable_instances.js +++ b/x-pack/plugins/maps/public/reducers/non_serializable_instances.js @@ -12,6 +12,7 @@ import { getShowMapsInspectorAdapter } from '../kibana_services'; const REGISTER_CANCEL_CALLBACK = 'REGISTER_CANCEL_CALLBACK'; const UNREGISTER_CANCEL_CALLBACK = 'UNREGISTER_CANCEL_CALLBACK'; const SET_EVENT_HANDLERS = 'SET_EVENT_HANDLERS'; +const SET_CHARTS_PALETTE_SERVICE_GET_COLOR = 'SET_CHARTS_PALETTE_SERVICE_GET_COLOR'; function createInspectorAdapters() { const inspectorAdapters = { @@ -30,6 +31,7 @@ export function nonSerializableInstances(state, action = {}) { inspectorAdapters: createInspectorAdapters(), cancelRequestCallbacks: new Map(), // key is request token, value is cancel callback eventHandlers: {}, + chartsPaletteServiceGetColor: null, }; } @@ -50,6 +52,12 @@ export function nonSerializableInstances(state, action = {}) { eventHandlers: action.eventHandlers, }; } + case SET_CHARTS_PALETTE_SERVICE_GET_COLOR: { + return { + ...state, + chartsPaletteServiceGetColor: action.chartsPaletteServiceGetColor, + }; + } default: return state; } @@ -68,6 +76,11 @@ export const getEventHandlers = ({ nonSerializableInstances }) => { return nonSerializableInstances.eventHandlers; }; +export function getChartsPaletteServiceGetColor({ nonSerializableInstances }) { + console.log('getChartsPaletteServiceGetColor', nonSerializableInstances); + return nonSerializableInstances.chartsPaletteServiceGetColor; +} + // Actions export const registerCancelCallback = (requestToken, callback) => { return { @@ -104,3 +117,10 @@ export const setEventHandlers = (eventHandlers = {}) => { eventHandlers, }; }; + +export function setChartsPaletteServiceGetColor(chartsPaletteServiceGetColor) { + return { + type: SET_CHARTS_PALETTE_SERVICE_GET_COLOR, + chartsPaletteServiceGetColor, + }; +} diff --git a/x-pack/plugins/maps/public/reducers/store.js b/x-pack/plugins/maps/public/reducers/store.js index 3c9b5d1b98e29..4e355add59fee 100644 --- a/x-pack/plugins/maps/public/reducers/store.js +++ b/x-pack/plugins/maps/public/reducers/store.js @@ -15,6 +15,7 @@ import { MAP_DESTROYED } from '../actions'; export const DEFAULT_MAP_STORE_STATE = { ui: { ...DEFAULT_MAP_UI_STATE }, map: { ...DEFAULT_MAP_STATE }, + nonSerializableInstances: {}, }; export function createMapStore() { diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.test.ts b/x-pack/plugins/maps/public/selectors/map_selectors.test.ts index eb11ee61d9deb..dd6a9fc377e5b 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.test.ts +++ b/x-pack/plugins/maps/public/selectors/map_selectors.test.ts @@ -11,11 +11,6 @@ jest.mock('../classes/layers/blended_vector_layer/blended_vector_layer', () => { jest.mock('../classes/layers/heatmap_layer/heatmap_layer', () => {}); jest.mock('../classes/layers/vector_tile_layer/vector_tile_layer', () => {}); jest.mock('../classes/joins/inner_join', () => {}); -jest.mock('../reducers/non_serializable_instances', () => ({ - getInspectorAdapters: () => { - return {}; - }, -})); jest.mock('../kibana_services', () => ({ getTimeFilter: () => ({ getTime: () => { diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.ts b/x-pack/plugins/maps/public/selectors/map_selectors.ts index 34af789f6834f..27281fe17f0fa 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.ts +++ b/x-pack/plugins/maps/public/selectors/map_selectors.ts @@ -18,7 +18,10 @@ import { VectorStyle } from '../classes/styles/vector/vector_style'; import { HeatmapLayer } from '../classes/layers/heatmap_layer/heatmap_layer'; import { BlendedVectorLayer } from '../classes/layers/blended_vector_layer/blended_vector_layer'; import { getTimeFilter } from '../kibana_services'; -import { getInspectorAdapters } from '../reducers/non_serializable_instances'; +import { + getChartsPaletteServiceGetColor, + getInspectorAdapters, +} from '../reducers/non_serializable_instances'; import { TiledVectorLayer } from '../classes/layers/tiled_vector_layer/tiled_vector_layer'; import { copyPersistentState, TRACKED_LAYER_DESCRIPTOR } from '../reducers/util'; import { InnerJoin } from '../classes/joins/inner_join'; @@ -55,7 +58,8 @@ import { ILayer } from '../classes/layers/layer'; export function createLayerInstance( layerDescriptor: LayerDescriptor, - inspectorAdapters?: Adapters + inspectorAdapters?: Adapters, + chartsPaletteServiceGetColor?: (value: string) => string | null ): ILayer { const source: ISource = createSourceInstance(layerDescriptor.sourceDescriptor, inspectorAdapters); @@ -75,6 +79,7 @@ export function createLayerInstance( layerDescriptor: vectorLayerDescriptor, source: source as IVectorSource, joins, + chartsPaletteServiceGetColor, }); case VectorTileLayer.type: return new VectorTileLayer({ layerDescriptor, source: source as ITMSSource }); @@ -84,6 +89,7 @@ export function createLayerInstance( return new BlendedVectorLayer({ layerDescriptor: layerDescriptor as VectorLayerDescriptor, source: source as IVectorSource, + chartsPaletteServiceGetColor, }); case TiledVectorLayer.type: return new TiledVectorLayer({ @@ -295,9 +301,10 @@ export const getSpatialFiltersLayer = createSelector( export const getLayerList = createSelector( getLayerListRaw, getInspectorAdapters, - (layerDescriptorList, inspectorAdapters) => { + getChartsPaletteServiceGetColor, + (layerDescriptorList, inspectorAdapters, chartsPaletteServiceGetColor) => { return layerDescriptorList.map((layerDescriptor) => - createLayerInstance(layerDescriptor, inspectorAdapters) + createLayerInstance(layerDescriptor, inspectorAdapters, chartsPaletteServiceGetColor) ); } ); From 3d068c5db947799b958cef3bc190d3b102aca0b9 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Tue, 9 Feb 2021 15:03:23 +0000 Subject: [PATCH 18/28] [ML] Lazy ml node UI improvements (#90455) * [ML] Lazy ml node UI improvements * fixing test * adding awaitingMlNodeAllocation to default datafeed response * changing datafeed icon when node is not assigned * updating text Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/ml/common/types/modules.ts | 1 + .../jobs_awaiting_node_warning.tsx | 8 +++--- .../new_job_awaiting_node.tsx | 9 ++++-- .../new_job/recognize/components/job_item.tsx | 17 +++++++++-- .../jobs/new_job/recognize/page.tsx | 20 +++++++++++-- .../ml_nodes_check/check_ml_nodes.ts | 8 ++++++ .../application/ml_nodes_check/index.ts | 1 + .../forecasting_modal/run_controls.js | 4 +-- .../models/data_recognizer/data_recognizer.ts | 28 ++++++++++++++++--- .../apis/ml/modules/setup_module.ts | 1 + 10 files changed, 79 insertions(+), 18 deletions(-) diff --git a/x-pack/plugins/ml/common/types/modules.ts b/x-pack/plugins/ml/common/types/modules.ts index faa9c700f95a4..7c9623d3e68ec 100644 --- a/x-pack/plugins/ml/common/types/modules.ts +++ b/x-pack/plugins/ml/common/types/modules.ts @@ -68,6 +68,7 @@ export interface KibanaObjectResponse extends ResultItem { export interface DatafeedResponse extends ResultItem { started: boolean; + awaitingMlNodeAllocation?: boolean; error?: ErrorType; } diff --git a/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/jobs_awaiting_node_warning.tsx b/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/jobs_awaiting_node_warning.tsx index bc216ce62a57c..2cc36b7a2adf7 100644 --- a/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/jobs_awaiting_node_warning.tsx +++ b/x-pack/plugins/ml/public/application/components/jobs_awaiting_node_warning/jobs_awaiting_node_warning.tsx @@ -9,14 +9,14 @@ import React, { Fragment, FC } from 'react'; import { EuiCallOut, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { isCloud } from '../../services/ml_server_info'; +import { lazyMlNodesAvailable } from '../../ml_nodes_check'; interface Props { jobCount: number; } export const JobsAwaitingNodeWarning: FC = ({ jobCount }) => { - if (isCloud() === false || jobCount === 0) { + if (lazyMlNodesAvailable() === false || jobCount === 0) { return null; } @@ -26,7 +26,7 @@ export const JobsAwaitingNodeWarning: FC = ({ jobCount }) => { title={ } color="primary" @@ -35,7 +35,7 @@ export const JobsAwaitingNodeWarning: FC = ({ jobCount }) => {
= () => { + if (lazyMlNodesAvailable() === false) { + return null; + } + return ( } color="primary" @@ -31,7 +36,7 @@ export const NewJobAwaitingNodeWarning: FC = () => {
diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/job_item.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/job_item.tsx index 760ff67d97b9d..311e291cf2519 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/job_item.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/components/job_item.tsx @@ -21,7 +21,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { ModuleJobUI } from '../page'; import { SETUP_RESULTS_WIDTH } from './module_jobs'; import { tabColor } from '../../../../../../common/util/group_color_utils'; -import { JobOverride } from '../../../../../../common/types/modules'; +import { JobOverride, DatafeedResponse } from '../../../../../../common/types/modules'; import { extractErrorMessage } from '../../../../../../common/util/errors'; interface JobItemProps { @@ -151,8 +151,8 @@ export const JobItem: FC = memo( = memo( ); } ); + +function getDatafeedStartedIcon({ + awaitingMlNodeAllocation, + success, +}: DatafeedResponse): { type: string; color: string } { + if (awaitingMlNodeAllocation === true) { + return { type: 'alert', color: 'warning' }; + } + + return success ? { type: 'check', color: 'secondary' } : { type: 'cross', color: 'danger' }; +} diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/page.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/page.tsx index 14018d485e04c..271898654ca49 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/recognize/page.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/recognize/page.tsx @@ -43,6 +43,7 @@ import { TimeRange } from '../common/components'; import { JobId } from '../../../../../common/types/anomaly_detection_jobs'; import { ML_PAGES } from '../../../../../common/constants/ml_url_generator'; import { TIME_FORMAT } from '../../../../../common/constants/time_format'; +import { JobsAwaitingNodeWarning } from '../../../components/jobs_awaiting_node_warning'; export interface ModuleJobUI extends ModuleJob { datafeedResult?: DatafeedResponse; @@ -84,6 +85,7 @@ export const Page: FC = ({ moduleId, existingGroupIds }) => { const [saveState, setSaveState] = useState(SAVE_STATE.NOT_SAVED); const [resultsUrl, setResultsUrl] = useState(''); const [existingGroups, setExistingGroups] = useState(existingGroupIds); + const [jobsAwaitingNodeCount, setJobsAwaitingNodeCount] = useState(0); // #endregion const { @@ -204,9 +206,19 @@ export const Page: FC = ({ moduleId, existingGroupIds }) => { }); setResultsUrl(url); - const failedJobsCount = jobsResponse.reduce((count, { success }) => { - return success ? count : count + 1; - }, 0); + const failedJobsCount = jobsResponse.reduce( + (count, { success }) => (success ? count : count + 1), + 0 + ); + + const lazyJobsCount = datafeedsResponse.reduce( + (count, { awaitingMlNodeAllocation }) => + awaitingMlNodeAllocation === true ? count + 1 : count, + 0 + ); + + setJobsAwaitingNodeCount(lazyJobsCount); + setSaveState( failedJobsCount === 0 ? SAVE_STATE.SAVED @@ -291,6 +303,8 @@ export const Page: FC = ({ moduleId, existingGroupIds }) => { )} + {jobsAwaitingNodeCount > 0 && } + diff --git a/x-pack/plugins/ml/public/application/ml_nodes_check/check_ml_nodes.ts b/x-pack/plugins/ml/public/application/ml_nodes_check/check_ml_nodes.ts index 71aef2da312a6..551a5823c1f41 100644 --- a/x-pack/plugins/ml/public/application/ml_nodes_check/check_ml_nodes.ts +++ b/x-pack/plugins/ml/public/application/ml_nodes_check/check_ml_nodes.ts @@ -48,6 +48,14 @@ export function mlNodesAvailable() { return mlNodeCount !== 0 || lazyMlNodeCount !== 0; } +export function currentMlNodesAvailable() { + return mlNodeCount !== 0; +} + +export function lazyMlNodesAvailable() { + return lazyMlNodeCount !== 0; +} + export function permissionToViewMlNodeCount() { return userHasPermissionToViewMlNodeCount; } diff --git a/x-pack/plugins/ml/public/application/ml_nodes_check/index.ts b/x-pack/plugins/ml/public/application/ml_nodes_check/index.ts index 295ff1aca2ec7..8102f95c035b0 100644 --- a/x-pack/plugins/ml/public/application/ml_nodes_check/index.ts +++ b/x-pack/plugins/ml/public/application/ml_nodes_check/index.ts @@ -9,5 +9,6 @@ export { checkMlNodesAvailable, getMlNodeCount, mlNodesAvailable, + lazyMlNodesAvailable, permissionToViewMlNodeCount, } from './check_ml_nodes'; diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/run_controls.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/run_controls.js index a37ad5fd30517..b36acba8b4ba4 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/run_controls.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/components/forecasting_modal/run_controls.js @@ -27,7 +27,7 @@ import { import { JOB_STATE } from '../../../../../common/constants/states'; import { FORECAST_DURATION_MAX_DAYS } from './forecasting_modal'; import { ForecastProgress } from './forecast_progress'; -import { mlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes'; +import { currentMlNodesAvailable } from '../../../ml_nodes_check/check_ml_nodes'; import { checkPermission, createPermissionFailureMessage, @@ -41,7 +41,7 @@ function getRunInputDisabledState(job, isForecastRequested) { // - No canForecastJob permission // - Job is not in an OPENED or CLOSED state // - A new forecast has been requested - if (mlNodesAvailable() === false) { + if (currentMlNodesAvailable() === false) { return { isDisabled: true, isDisabledToolTipText: i18n.translate( diff --git a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts index 92dfe3aa0fbf9..a1fac92d45b4e 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts +++ b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts @@ -491,6 +491,7 @@ export class DataRecognizer { const startedDatafeed = startResults[df.id]; if (startedDatafeed !== undefined) { df.started = startedDatafeed.started; + df.awaitingMlNodeAllocation = startedDatafeed.awaitingMlNodeAllocation; if (startedDatafeed.error !== undefined) { df.error = startedDatafeed.error; } @@ -749,9 +750,20 @@ export class DataRecognizer { datafeeds.map(async (datafeed) => { try { await this.saveDatafeed(datafeed); - return { id: datafeed.id, success: true, started: false }; + return { + id: datafeed.id, + success: true, + started: false, + awaitingMlNodeAllocation: false, + }; } catch ({ body }) { - return { id: datafeed.id, success: false, started: false, error: body }; + return { + id: datafeed.id, + success: false, + started: false, + awaitingMlNodeAllocation: false, + error: body, + }; } }) ); @@ -811,11 +823,18 @@ export class DataRecognizer { duration.end = (end as unknown) as string; } - await this._mlClient.startDatafeed({ + const { + body: { started, node }, + } = await this._mlClient.startDatafeed<{ + started: boolean; + node: string; + }>({ datafeed_id: datafeed.id, ...duration, }); - result.started = true; + + result.started = started; + result.awaitingMlNodeAllocation = node?.length === 0; } catch ({ body }) { result.started = false; result.error = body; @@ -845,6 +864,7 @@ export class DataRecognizer { if (d.id === d2.id) { d.success = d2.success; d.started = d2.started; + d.awaitingMlNodeAllocation = d2.awaitingMlNodeAllocation; if (d2.error !== undefined) { d.error = d2.error; } 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 be1ac7fbb0965..8064d498774a3 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 @@ -772,6 +772,7 @@ export default ({ getService }: FtrProviderContext) => { const expectedRspDatafeeds = sortBy( testData.expected.jobs.map((job) => { return { + awaitingMlNodeAllocation: false, id: `datafeed-${job.jobId}`, success: true, started: testData.requestBody.startDatafeed, From 32ddd5e795b20d4b6d452412925d1921613d7336 Mon Sep 17 00:00:00 2001 From: Kerry Gallagher Date: Tue, 9 Feb 2021 15:28:59 +0000 Subject: [PATCH 19/28] [Logs UI] Show anomalies across both the log rate and categorization ML jobs in a swimlane visualization. (#89589) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Allow anomalies visualisation to represent anomalies from all jobs Co-authored-by: Felix Stürmer --- .../http_api/log_analysis/results/index.ts | 1 - .../log_analysis/results/log_entry_rate.ts | 86 --------- .../log_analysis/log_analysis_results.ts | 4 - x-pack/plugins/infra/kibana.json | 12 +- .../anomaly_severity_indicator.tsx | 9 +- .../missing_embeddable_factory_callout.tsx | 26 +++ .../hooks/use_kibana_timefilter_time.tsx | 39 +++- .../log_entry_rate/page_results_content.tsx | 173 +++++------------ .../anomalies_swimlane_visualisation.tsx | 70 +++++++ .../sections/anomalies/chart.tsx | 181 ----------------- .../sections/anomalies/index.tsx | 168 +++++----------- .../sections/anomalies/table.tsx | 10 +- .../sections/helpers/data_formatters.tsx | 182 ------------------ .../service_calls/get_log_entry_rate.ts | 43 ----- .../log_entry_rate/use_dataset_filtering.ts | 99 ++++++++++ .../use_log_entry_rate_results.ts | 160 --------------- .../use_log_entry_rate_results_url_state.tsx | 125 +++++++++--- x-pack/plugins/infra/public/types.ts | 2 + .../infra/public/utils/use_url_state.ts | 16 +- x-pack/plugins/infra/server/infra_server.ts | 2 - .../lib/log_analysis/log_entry_anomalies.ts | 1 - .../routes/log_analysis/results/index.ts | 1 - .../log_analysis/results/log_entry_rate.ts | 90 --------- .../swimlane_input_resolver.ts | 2 +- x-pack/plugins/ml/public/index.ts | 12 +- .../apply_influencer_filters_action.tsx | 3 +- .../plugins/ml/public/ui_actions/constants.ts | 8 + .../translations/translations/ja-JP.json | 4 - .../translations/translations/zh-CN.json | 4 - .../api_integration/apis/metrics_ui/index.js | 1 - .../apis/metrics_ui/log_analysis.ts | 137 ------------- 31 files changed, 469 insertions(+), 1202 deletions(-) delete mode 100644 x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_rate.ts create mode 100644 x-pack/plugins/infra/public/components/missing_embeddable_factory_callout.tsx create mode 100644 x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/anomalies_swimlane_visualisation.tsx delete mode 100644 x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/chart.tsx delete mode 100644 x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/helpers/data_formatters.tsx delete mode 100644 x-pack/plugins/infra/public/pages/logs/log_entry_rate/service_calls/get_log_entry_rate.ts create mode 100644 x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_dataset_filtering.ts delete mode 100644 x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results.ts delete mode 100644 x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_rate.ts create mode 100644 x-pack/plugins/ml/public/ui_actions/constants.ts delete mode 100644 x-pack/test/api_integration/apis/metrics_ui/log_analysis.ts diff --git a/x-pack/plugins/infra/common/http_api/log_analysis/results/index.ts b/x-pack/plugins/infra/common/http_api/log_analysis/results/index.ts index d50495689e9d8..23c2ce5f0c21f 100644 --- a/x-pack/plugins/infra/common/http_api/log_analysis/results/index.ts +++ b/x-pack/plugins/infra/common/http_api/log_analysis/results/index.ts @@ -9,7 +9,6 @@ export * from './log_entry_categories'; export * from './log_entry_category_datasets'; export * from './log_entry_category_datasets_stats'; export * from './log_entry_category_examples'; -export * from './log_entry_rate'; export * from './log_entry_examples'; export * from './log_entry_anomalies'; export * from './log_entry_anomalies_datasets'; diff --git a/x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_rate.ts b/x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_rate.ts deleted file mode 100644 index 943e1df70c0ba..0000000000000 --- a/x-pack/plugins/infra/common/http_api/log_analysis/results/log_entry_rate.ts +++ /dev/null @@ -1,86 +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 * as rt from 'io-ts'; - -import { badRequestErrorRT, conflictErrorRT, forbiddenErrorRT, timeRangeRT } from '../../shared'; - -export const LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH = - '/api/infra/log_analysis/results/log_entry_rate'; - -/** - * request - */ - -export const getLogEntryRateRequestPayloadRT = rt.type({ - data: rt.intersection([ - rt.type({ - bucketDuration: rt.number, - sourceId: rt.string, - timeRange: timeRangeRT, - }), - rt.partial({ - datasets: rt.array(rt.string), - }), - ]), -}); - -export type GetLogEntryRateRequestPayload = rt.TypeOf; - -/** - * response - */ - -export const logEntryRateAnomalyRT = rt.type({ - id: rt.string, - actualLogEntryRate: rt.number, - anomalyScore: rt.number, - duration: rt.number, - startTime: rt.number, - typicalLogEntryRate: rt.number, -}); - -export type LogEntryRateAnomaly = rt.TypeOf; - -export const logEntryRatePartitionRT = rt.type({ - analysisBucketCount: rt.number, - anomalies: rt.array(logEntryRateAnomalyRT), - averageActualLogEntryRate: rt.number, - maximumAnomalyScore: rt.number, - numberOfLogEntries: rt.number, - partitionId: rt.string, -}); - -export type LogEntryRatePartition = rt.TypeOf; - -export const logEntryRateHistogramBucketRT = rt.type({ - partitions: rt.array(logEntryRatePartitionRT), - startTime: rt.number, -}); - -export type LogEntryRateHistogramBucket = rt.TypeOf; - -export const getLogEntryRateSuccessReponsePayloadRT = rt.type({ - data: rt.type({ - bucketDuration: rt.number, - histogramBuckets: rt.array(logEntryRateHistogramBucketRT), - totalNumberOfLogEntries: rt.number, - }), -}); - -export type GetLogEntryRateSuccessResponsePayload = rt.TypeOf< - typeof getLogEntryRateSuccessReponsePayloadRT ->; - -export const getLogEntryRateResponsePayloadRT = rt.union([ - getLogEntryRateSuccessReponsePayloadRT, - badRequestErrorRT, - conflictErrorRT, - forbiddenErrorRT, -]); - -export type GetLogEntryRateReponsePayload = rt.TypeOf; diff --git a/x-pack/plugins/infra/common/log_analysis/log_analysis_results.ts b/x-pack/plugins/infra/common/log_analysis/log_analysis_results.ts index f460747f8b142..113e8ff8c34e6 100644 --- a/x-pack/plugins/infra/common/log_analysis/log_analysis_results.ts +++ b/x-pack/plugins/infra/common/log_analysis/log_analysis_results.ts @@ -40,10 +40,6 @@ export const getSeverityCategoryForScore = ( } }; -export const formatAnomalyScore = (score: number) => { - return Math.round(score); -}; - export const formatOneDecimalPlace = (number: number) => { return Math.round(number * 10) / 10; }; diff --git a/x-pack/plugins/infra/kibana.json b/x-pack/plugins/infra/kibana.json index 327cb674de00b..c892f7017da33 100644 --- a/x-pack/plugins/infra/kibana.json +++ b/x-pack/plugins/infra/kibana.json @@ -13,9 +13,17 @@ "alerts", "triggersActionsUi" ], - "optionalPlugins": ["ml", "observability", "home"], + "optionalPlugins": ["ml", "observability", "home", "embeddable"], "server": true, "ui": true, "configPath": ["xpack", "infra"], - "requiredBundles": ["observability", "licenseManagement", "kibanaUtils", "kibanaReact", "home"] + "requiredBundles": [ + "observability", + "licenseManagement", + "kibanaUtils", + "kibanaReact", + "home", + "ml", + "embeddable" + ] } diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_results/anomaly_severity_indicator.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_results/anomaly_severity_indicator.tsx index 6fcbb0f6ffd4c..20fe816d1dab2 100644 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_results/anomaly_severity_indicator.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_results/anomaly_severity_indicator.tsx @@ -7,18 +7,15 @@ import { EuiHealth } from '@elastic/eui'; import React, { useMemo } from 'react'; -import { - formatAnomalyScore, - getSeverityCategoryForScore, - ML_SEVERITY_COLORS, -} from '../../../../common/log_analysis'; +import { getFormattedSeverityScore } from '../../../../../ml/public'; +import { getSeverityCategoryForScore, ML_SEVERITY_COLORS } from '../../../../common/log_analysis'; export const AnomalySeverityIndicator: React.FunctionComponent<{ anomalyScore: number; }> = ({ anomalyScore }) => { const severityColor = useMemo(() => getColorForAnomalyScore(anomalyScore), [anomalyScore]); - return {formatAnomalyScore(anomalyScore)}; + return {getFormattedSeverityScore(anomalyScore)}; }; const getColorForAnomalyScore = (anomalyScore: number) => { diff --git a/x-pack/plugins/infra/public/components/missing_embeddable_factory_callout.tsx b/x-pack/plugins/infra/public/components/missing_embeddable_factory_callout.tsx new file mode 100644 index 0000000000000..8afd8cde32ef3 --- /dev/null +++ b/x-pack/plugins/infra/public/components/missing_embeddable_factory_callout.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiCallOut } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +export const MissingEmbeddableFactoryCallout: React.FC<{ embeddableType: string }> = ({ + embeddableType, +}) => { + return ( + + ); +}; diff --git a/x-pack/plugins/infra/public/hooks/use_kibana_timefilter_time.tsx b/x-pack/plugins/infra/public/hooks/use_kibana_timefilter_time.tsx index 12b0cb06d8682..15eb525dca734 100644 --- a/x-pack/plugins/infra/public/hooks/use_kibana_timefilter_time.tsx +++ b/x-pack/plugins/infra/public/hooks/use_kibana_timefilter_time.tsx @@ -5,11 +5,10 @@ * 2.0. */ -import { useCallback } from 'react'; +import { useCallback, useEffect } from 'react'; import useUpdateEffect from 'react-use/lib/useUpdateEffect'; import useMount from 'react-use/lib/useMount'; - import { useKibanaContextForPlugin } from './use_kibana'; import { TimeRange, TimefilterContract } from '../../../../../src/plugins/data/public'; @@ -29,8 +28,24 @@ export const useKibanaTimefilterTime = ({ return [getTime, services.data.query.timefilter.timefilter.setTime]; }; -export const useSyncKibanaTimeFilterTime = (defaults: TimeRange, currentTimeRange: TimeRange) => { - const [, setTime] = useKibanaTimefilterTime(defaults); +/** + * Handles one or two way syncing with the Kibana time filter service. + * + * For one way syncing the time range will be synced back to the time filter service + * on mount *if* it differs from the defaults, e.g. a URL param. + * Future updates, after mount, will also be synced back to the time filter service. + * + * For two way syncing, in addition to the above, changes *from* the time filter service + * will be sycned to the solution, e.g. there might be an embeddable on the page that + * fires an action that hooks into the time filter service. + */ +export const useSyncKibanaTimeFilterTime = ( + defaults: TimeRange, + currentTimeRange: TimeRange, + setTimeRange?: (timeRange: TimeRange) => void +) => { + const { services } = useKibanaContextForPlugin(); + const [getTime, setTime] = useKibanaTimefilterTime(defaults); // On first mount we only want to sync time with Kibana if the derived currentTimeRange (e.g. from URL params) // differs from our defaults. @@ -40,8 +55,22 @@ export const useSyncKibanaTimeFilterTime = (defaults: TimeRange, currentTimeRang } }); - // Sync explicit changes *after* mount back to Kibana + // Sync explicit changes *after* mount from the solution back to Kibana useUpdateEffect(() => { setTime({ from: currentTimeRange.from, to: currentTimeRange.to }); }, [currentTimeRange.from, currentTimeRange.to, setTime]); + + // *Optionally* sync time filter service changes back to the solution. + // For example, an embeddable might have a time range action that hooks into + // the time filter service. + useEffect(() => { + const sub = services.data.query.timefilter.timefilter.getTimeUpdate$().subscribe(() => { + if (setTimeRange) { + const timeRange = getTime(); + setTimeRange(timeRange); + } + }); + + return () => sub.unsubscribe(); + }, [getTime, setTimeRange, services.data.query.timefilter.timefilter]); }; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx index a8660e1ce8013..54617d025652b 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/page_results_content.tsx @@ -5,17 +5,14 @@ * 2.0. */ -import datemath from '@elastic/datemath'; import { EuiFlexGroup, EuiFlexItem, EuiPage, EuiPanel, EuiSuperDatePicker } from '@elastic/eui'; import moment from 'moment'; import { stringify } from 'query-string'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { encode, RisonValue } from 'rison-node'; -import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { useTrackPageview } from '../../../../../observability/public'; -import { TimeRange } from '../../../../common/time/time_range'; -import { bucketSpan } from '../../../../common/log_analysis'; import { TimeKey } from '../../../../common/time'; import { CategoryJobNoticesSection, @@ -29,14 +26,11 @@ import { useLogEntryCategoriesModuleContext } from '../../../containers/logs/log import { useLogEntryRateModuleContext } from '../../../containers/logs/log_analysis/modules/log_entry_rate'; import { useLogEntryFlyoutContext } from '../../../containers/logs/log_flyout'; import { useLogSourceContext } from '../../../containers/logs/log_source'; -import { useInterval } from '../../../hooks/use_interval'; import { AnomaliesResults } from './sections/anomalies'; import { useLogEntryAnomaliesResults } from './use_log_entry_anomalies_results'; -import { useLogEntryRateResults } from './use_log_entry_rate_results'; -import { - StringTimeRange, - useLogAnalysisResultsUrlState, -} from './use_log_entry_rate_results_url_state'; +import { useDatasetFiltering } from './use_dataset_filtering'; +import { useLogAnalysisResultsUrlState } from './use_log_entry_rate_results_url_state'; +import { isJobStatusWithResults } from '../../../../common/log_analysis'; export const SORT_DEFAULTS = { direction: 'desc' as const, @@ -62,6 +56,8 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => { hasStoppedJobs: hasStoppedLogEntryRateJobs, moduleDescriptor: logEntryRateModuleDescriptor, setupStatus: logEntryRateSetupStatus, + jobStatus: logEntryRateJobStatus, + jobIds: logEntryRateJobIds, } = useLogEntryRateModuleContext(); const { @@ -71,10 +67,29 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => { hasStoppedJobs: hasStoppedLogEntryCategoriesJobs, moduleDescriptor: logEntryCategoriesModuleDescriptor, setupStatus: logEntryCategoriesSetupStatus, + jobStatus: logEntryCategoriesJobStatus, + jobIds: logEntryCategoriesJobIds, } = useLogEntryCategoriesModuleContext(); + const jobIds = useMemo(() => { + return [ + ...(isJobStatusWithResults(logEntryRateJobStatus['log-entry-rate']) + ? [logEntryRateJobIds['log-entry-rate']] + : []), + ...(isJobStatusWithResults(logEntryCategoriesJobStatus['log-entry-categories-count']) + ? [logEntryCategoriesJobIds['log-entry-categories-count']] + : []), + ]; + }, [ + logEntryRateJobIds, + logEntryCategoriesJobIds, + logEntryRateJobStatus, + logEntryCategoriesJobStatus, + ]); + const { - timeRange: selectedTimeRange, + timeRange, + friendlyTimeRange, setTimeRange: setSelectedTimeRange, autoRefresh, setAutoRefresh, @@ -86,21 +101,13 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => { logEntryId: flyoutLogEntryId, } = useLogEntryFlyoutContext(); - const [queryTimeRange, setQueryTimeRange] = useState<{ - value: TimeRange; - lastChangedTime: number; - }>(() => ({ - value: stringToNumericTimeRange(selectedTimeRange), - lastChangedTime: Date.now(), - })); - const linkToLogStream = useCallback( (filter: string, id: string, timeKey?: TimeKey) => { const params = { logPosition: encode({ - end: moment(queryTimeRange.value.endTime).format('YYYY-MM-DDTHH:mm:ss.SSSZ'), + end: moment(timeRange.value.endTime).format('YYYY-MM-DDTHH:mm:ss.SSSZ'), position: timeKey as RisonValue, - start: moment(queryTimeRange.value.startTime).format('YYYY-MM-DDTHH:mm:ss.SSSZ'), + start: moment(timeRange.value.startTime).format('YYYY-MM-DDTHH:mm:ss.SSSZ'), streamLive: false, }), flyoutOptions: encode({ @@ -114,23 +121,10 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => { navigateToApp?.('logs', { path: `/stream?${stringify(params)}` }); }, - [queryTimeRange, navigateToApp] - ); - - const bucketDuration = useMemo( - () => getBucketDuration(queryTimeRange.value.startTime, queryTimeRange.value.endTime), - [queryTimeRange.value.endTime, queryTimeRange.value.startTime] + [timeRange, navigateToApp] ); - const [selectedDatasets, setSelectedDatasets] = useState([]); - - const { getLogEntryRate, isLoading, logEntryRate } = useLogEntryRateResults({ - sourceId, - startTime: queryTimeRange.value.startTime, - endTime: queryTimeRange.value.endTime, - bucketDuration, - filteredDatasets: selectedDatasets, - }); + const { selectedDatasets, setSelectedDatasets } = useDatasetFiltering(); const { isLoadingLogEntryAnomalies, @@ -146,48 +140,13 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => { isLoadingDatasets, } = useLogEntryAnomaliesResults({ sourceId, - startTime: queryTimeRange.value.startTime, - endTime: queryTimeRange.value.endTime, + startTime: timeRange.value.startTime, + endTime: timeRange.value.endTime, defaultSortOptions: SORT_DEFAULTS, defaultPaginationOptions: PAGINATION_DEFAULTS, filteredDatasets: selectedDatasets, }); - const handleQueryTimeRangeChange = useCallback( - ({ start: startTime, end: endTime }: { start: string; end: string }) => { - setQueryTimeRange({ - value: stringToNumericTimeRange({ startTime, endTime }), - lastChangedTime: Date.now(), - }); - }, - [setQueryTimeRange] - ); - - const handleSelectedTimeRangeChange = useCallback( - (selectedTime: { start: string; end: string; isInvalid: boolean }) => { - if (selectedTime.isInvalid) { - return; - } - setSelectedTimeRange({ - startTime: selectedTime.start, - endTime: selectedTime.end, - }); - handleQueryTimeRangeChange(selectedTime); - }, - [setSelectedTimeRange, handleQueryTimeRangeChange] - ); - - const handleChartTimeRangeChange = useCallback( - ({ startTime, endTime }: TimeRange) => { - handleSelectedTimeRangeChange({ - end: new Date(endTime).toISOString(), - isInvalid: false, - start: new Date(startTime).toISOString(), - }); - }, - [handleSelectedTimeRangeChange] - ); - const handleAutoRefreshChange = useCallback( ({ isPaused, refreshInterval: interval }: { isPaused: boolean; refreshInterval: number }) => { setAutoRefresh({ @@ -207,7 +166,6 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => { showModuleSetup, ]); - const hasLogRateResults = (logEntryRate?.histogramBuckets?.length ?? 0) > 0; const hasAnomalyResults = logEntryAnomalies.length > 0; const isFirstUse = useMemo( @@ -217,22 +175,18 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => { logEntryCategoriesSetupStatus.type === 'succeeded' || (logEntryRateSetupStatus.type === 'skipped' && !!logEntryRateSetupStatus.newlyCreated) || logEntryRateSetupStatus.type === 'succeeded') && - !(hasLogRateResults || hasAnomalyResults), - [hasAnomalyResults, hasLogRateResults, logEntryCategoriesSetupStatus, logEntryRateSetupStatus] + !hasAnomalyResults, + [hasAnomalyResults, logEntryCategoriesSetupStatus, logEntryRateSetupStatus] ); - useEffect(() => { - getLogEntryRate(); - }, [getLogEntryRate, selectedDatasets, queryTimeRange.lastChangedTime]); - - useInterval( - () => { - handleQueryTimeRangeChange({ - start: selectedTimeRange.startTime, - end: selectedTimeRange.endTime, - }); + const handleSelectedTimeRangeChange = useCallback( + (selectedTime: { start: string; end: string; isInvalid: boolean }) => { + if (selectedTime.isInvalid) { + return; + } + setSelectedTimeRange(selectedTime); }, - autoRefresh.isPaused ? null : autoRefresh.interval + [setSelectedTimeRange] ); return ( @@ -251,8 +205,8 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => { { { changePaginationOptions={changePaginationOptions} sortOptions={sortOptions} paginationOptions={paginationOptions} + selectedDatasets={selectedDatasets} + jobIds={jobIds} + autoRefresh={autoRefresh} /> @@ -318,37 +272,6 @@ export const LogEntryRateResultsContent: React.FunctionComponent = () => { ); }; -const stringToNumericTimeRange = (timeRange: StringTimeRange): TimeRange => ({ - startTime: moment( - datemath.parse(timeRange.startTime, { - momentInstance: moment, - }) - ).valueOf(), - endTime: moment( - datemath.parse(timeRange.endTime, { - momentInstance: moment, - roundUp: true, - }) - ).valueOf(), -}); - -/** - * This function takes the current time range in ms, - * works out the bucket interval we'd need to always - * display 100 data points, and then takes that new - * value and works out the nearest multiple of - * 900000 (15 minutes) to it, so that we don't end up with - * jaggy bucket boundaries between the ML buckets and our - * aggregation buckets. - */ -const getBucketDuration = (startTime: number, endTime: number) => { - const msRange = moment(endTime).diff(moment(startTime)); - const bucketIntervalInMs = msRange / 100; - const result = bucketSpan * Math.round(bucketIntervalInMs / bucketSpan); - const roundedResult = parseInt(Number(result).toFixed(0), 10); - return roundedResult < bucketSpan ? bucketSpan : roundedResult; -}; - // This is needed due to the flex-basis: 100% !important; rule that // kicks in on small screens via media queries breaking when using direction="column" export const ResultsContentPage = euiStyled(EuiPage)` diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/anomalies_swimlane_visualisation.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/anomalies_swimlane_visualisation.tsx new file mode 100644 index 0000000000000..b0e85a4648d6e --- /dev/null +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/anomalies_swimlane_visualisation.tsx @@ -0,0 +1,70 @@ +/* + * 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, { useMemo } from 'react'; +import moment from 'moment'; +import { AutoRefresh } from '../../use_log_entry_rate_results_url_state'; +import { useKibanaContextForPlugin } from '../../../../../hooks/use_kibana'; +import { + ANOMALY_SWIMLANE_EMBEDDABLE_TYPE, + AnomalySwimlaneEmbeddableInput, +} from '../../../../../../../ml/public'; +import { EmbeddableRenderer } from '../../../../../../../../../src/plugins/embeddable/public'; +import { partitionField } from '../../../../../../common/infra_ml'; +import { MissingEmbeddableFactoryCallout } from '../../../../../components/missing_embeddable_factory_callout'; +import { TimeRange } from '../../../../../../common/time/time_range'; + +interface Props { + timeRange: TimeRange; + jobIds: string[]; + selectedDatasets: string[]; + autoRefresh: AutoRefresh; +} + +// Disable refresh, allow our timerange changes to refresh the embeddable. +const REFRESH_CONFIG = { + pause: true, + value: 0, +}; + +export const AnomaliesSwimlaneVisualisation: React.FC = (props) => { + const { embeddable: embeddablePlugin } = useKibanaContextForPlugin().services; + if (!embeddablePlugin) return null; + return ; +}; + +export const VisualisationContent: React.FC = ({ timeRange, jobIds, selectedDatasets }) => { + const { embeddable: embeddablePlugin } = useKibanaContextForPlugin().services; + const factory = embeddablePlugin?.getEmbeddableFactory(ANOMALY_SWIMLANE_EMBEDDABLE_TYPE); + + const embeddableInput: AnomalySwimlaneEmbeddableInput = useMemo(() => { + return { + id: 'LOG_ENTRY_ANOMALIES_EMBEDDABLE_INSTANCE', // NOTE: This is the only embeddable on the anomalies page, a static string will do. + jobIds, + swimlaneType: 'viewBy', + timeRange: { + from: moment(timeRange.startTime).format('YYYY-MM-DDTHH:mm:ss.SSSZ'), + to: moment(timeRange.endTime).format('YYYY-MM-DDTHH:mm:ss.SSSZ'), + }, + refreshConfig: REFRESH_CONFIG, + viewBy: partitionField, + filters: [], + query: { + language: 'kuery', + query: selectedDatasets + .map((dataset) => `${partitionField} : ${dataset !== '' ? dataset : '""'}`) + .join(' or '), // Ensure unknown (those with an empty "" string) datasets are handled correctly. + }, + }; + }, [jobIds, timeRange.startTime, timeRange.endTime, selectedDatasets]); + + if (!factory) { + return ; + } + + return ; +}; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/chart.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/chart.tsx deleted file mode 100644 index dd9c2dd707044..0000000000000 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/chart.tsx +++ /dev/null @@ -1,181 +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 { EuiEmptyPrompt } from '@elastic/eui'; -import { RectAnnotationDatum, AnnotationId } from '@elastic/charts'; -import { - Axis, - BarSeries, - Chart, - niceTimeFormatter, - Settings, - TooltipValue, - LIGHT_THEME, - DARK_THEME, - RectAnnotation, - BrushEndListener, -} from '@elastic/charts'; -import numeral from '@elastic/numeral'; -import { i18n } from '@kbn/i18n'; -import moment from 'moment'; -import React, { useCallback, useMemo } from 'react'; -import { LoadingOverlayWrapper } from '../../../../../components/loading_overlay_wrapper'; - -import { TimeRange } from '../../../../../../common/time/time_range'; -import { - MLSeverityScoreCategories, - ML_SEVERITY_COLORS, -} from '../../../../../../common/log_analysis'; -import { useKibanaUiSetting } from '../../../../../utils/use_kibana_ui_setting'; - -export const AnomaliesChart: React.FunctionComponent<{ - chartId: string; - setTimeRange: (timeRange: TimeRange) => void; - timeRange: TimeRange; - series: Array<{ time: number; value: number }>; - annotations: Record; - renderAnnotationTooltip?: (details?: string) => JSX.Element; - isLoading: boolean; -}> = ({ - chartId, - series, - annotations, - setTimeRange, - timeRange, - renderAnnotationTooltip, - isLoading, -}) => { - const [dateFormat] = useKibanaUiSetting('dateFormat', 'Y-MM-DD HH:mm:ss.SSS'); - const [isDarkMode] = useKibanaUiSetting('theme:darkMode'); - - const chartDateFormatter = useMemo( - () => niceTimeFormatter([timeRange.startTime, timeRange.endTime]), - [timeRange] - ); - - const logEntryRateSpecId = 'averageValues'; - - const tooltipProps = useMemo( - () => ({ - headerFormatter: (tooltipData: TooltipValue) => moment(tooltipData.value).format(dateFormat), - }), - [dateFormat] - ); - - const handleBrushEnd = useCallback( - ({ x }) => { - if (!x) { - return; - } - const [startTime, endTime] = x; - setTimeRange({ - endTime, - startTime, - }); - }, - [setTimeRange] - ); - - return !isLoading && !series.length ? ( - - {i18n.translate('xpack.infra.logs.analysis.anomalySectionLogRateChartNoData', { - defaultMessage: 'There is no log rate data to display.', - })} - - } - titleSize="m" - /> - ) : ( - -
- {series.length ? ( - - - numeral(value.toPrecision(3)).format('0[.][00]a')} // https://github.com/adamwdraper/Numeral-js/issues/194 - /> - - {renderAnnotations(annotations, chartId, renderAnnotationTooltip)} - - - ) : null} -
-
- ); -}; - -interface SeverityConfig { - id: AnnotationId; - style: { - fill: string; - opacity: number; - }; -} - -const severityConfigs: Record = { - warning: { - id: `anomalies-warning`, - style: { fill: ML_SEVERITY_COLORS.warning, opacity: 0.7 }, - }, - minor: { - id: `anomalies-minor`, - style: { fill: ML_SEVERITY_COLORS.minor, opacity: 0.7 }, - }, - major: { - id: `anomalies-major`, - style: { fill: ML_SEVERITY_COLORS.major, opacity: 0.7 }, - }, - critical: { - id: `anomalies-critical`, - style: { fill: ML_SEVERITY_COLORS.critical, opacity: 0.7 }, - }, -}; - -const renderAnnotations = ( - annotations: Record, - chartId: string, - renderAnnotationTooltip?: (details?: string) => JSX.Element -) => { - return Object.entries(annotations).map((entry, index) => { - return ( - - ); - }); -}; - -const barSeriesStyle = { rect: { fill: '#D3DAE6', opacity: 0.6 } }; // TODO: Acquire this from "theme" as euiColorLightShade diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/index.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/index.tsx index 75d7c4212bbc3..3bc206e9ad7bb 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/index.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/index.tsx @@ -14,12 +14,9 @@ import { EuiLoadingSpinner, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { useMemo } from 'react'; -import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; -import { LogEntryRateResults } from '../../use_log_entry_rate_results'; +import React from 'react'; import { TimeRange } from '../../../../../../common/time/time_range'; -import { getAnnotationsForAll, getLogEntryRateCombinedSeries } from '../helpers/data_formatters'; -import { AnomaliesChart } from './chart'; +import { AnomaliesSwimlaneVisualisation } from './anomalies_swimlane_visualisation'; import { AnomaliesTable } from './table'; import { ManageJobsButton } from '../../../../../components/logging/log_analysis_setup/manage_jobs_button'; import { @@ -33,13 +30,11 @@ import { SortOptions, } from '../../use_log_entry_anomalies_results'; import { LoadingOverlayWrapper } from '../../../../../components/loading_overlay_wrapper'; +import { AutoRefresh } from '../../use_log_entry_rate_results_url_state'; export const AnomaliesResults: React.FunctionComponent<{ - isLoadingLogRateResults: boolean; isLoadingAnomaliesResults: boolean; - logEntryRateResults: LogEntryRateResults | null; anomalies: LogEntryAnomalies; - setTimeRange: (timeRange: TimeRange) => void; timeRange: TimeRange; onViewModuleList: () => void; page: Page; @@ -49,11 +44,11 @@ export const AnomaliesResults: React.FunctionComponent<{ changePaginationOptions: ChangePaginationOptions; sortOptions: SortOptions; paginationOptions: PaginationOptions; + selectedDatasets: string[]; + jobIds: string[]; + autoRefresh: AutoRefresh; }> = ({ - isLoadingLogRateResults, isLoadingAnomaliesResults, - logEntryRateResults, - setTimeRange, timeRange, onViewModuleList, anomalies, @@ -64,27 +59,10 @@ export const AnomaliesResults: React.FunctionComponent<{ fetchNextPage, fetchPreviousPage, page, + selectedDatasets, + jobIds, + autoRefresh, }) => { - const logEntryRateSeries = useMemo( - () => - logEntryRateResults && logEntryRateResults.histogramBuckets - ? getLogEntryRateCombinedSeries(logEntryRateResults) - : [], - [logEntryRateResults] - ); - const anomalyAnnotations = useMemo( - () => - logEntryRateResults && logEntryRateResults.histogramBuckets - ? getAnnotationsForAll(logEntryRateResults) - : { - warning: [], - minor: [], - major: [], - critical: [], - }, - [logEntryRateResults] - ); - return ( <> @@ -98,52 +76,44 @@ export const AnomaliesResults: React.FunctionComponent<{
- {(!logEntryRateResults || - (logEntryRateResults && - logEntryRateResults.histogramBuckets && - !logEntryRateResults.histogramBuckets.length)) && - (!anomalies || anomalies.length === 0) ? ( - } - > - - {i18n.translate('xpack.infra.logs.analysis.anomalySectionNoDataTitle', { - defaultMessage: 'There is no data to display.', - })} - - } - titleSize="m" - body={ -

- {i18n.translate('xpack.infra.logs.analysis.anomalySectionNoDataBody', { - defaultMessage: 'You may want to adjust your time range.', - })} -

- } + + + -
- ) : ( - <> - - - - - - +
+ + + <> + {!anomalies || anomalies.length === 0 ? ( + } + > + + {i18n.translate('xpack.infra.logs.analysis.anomalySectionNoDataTitle', { + defaultMessage: 'There is no data to display.', + })} + + } + titleSize="m" + body={ +

+ {i18n.translate('xpack.infra.logs.analysis.anomalySectionNoDataBody', { + defaultMessage: 'You may want to adjust your time range.', + })} +

+ } + /> +
+ ) : ( - - )} + )} + ); }; @@ -164,52 +134,6 @@ const title = i18n.translate('xpack.infra.logs.analysis.anomaliesSectionTitle', defaultMessage: 'Anomalies', }); -interface ParsedAnnotationDetails { - anomalyScoresByPartition: Array<{ partitionName: string; maximumAnomalyScore: number }>; -} - -const overallAnomalyScoreLabel = i18n.translate( - 'xpack.infra.logs.analysis.overallAnomalyChartMaxScoresLabel', - { - defaultMessage: 'Max anomaly scores:', - } -); - -const AnnotationTooltip: React.FunctionComponent<{ details: string }> = ({ details }) => { - const parsedDetails: ParsedAnnotationDetails = JSON.parse(details); - return ( - - - {overallAnomalyScoreLabel} - -
    - {parsedDetails.anomalyScoresByPartition.map(({ partitionName, maximumAnomalyScore }) => { - return ( -
  • - - {`${partitionName}: `} - {maximumAnomalyScore} - -
  • - ); - })} -
-
- ); -}; - -const renderAnnotationTooltip = (details?: string) => { - // Note: Seems to be necessary to get things typed correctly all the way through to elastic-charts components - if (!details) { - return
; - } - return ; -}; - -const TooltipWrapper = euiStyled('div')` - white-space: nowrap; -`; - const loadingAriaLabel = i18n.translate( 'xpack.infra.logs.analysis.anomaliesSectionLoadingAriaLabel', { defaultMessage: 'Loading anomalies' } diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx index d80f9d04e72a8..c208c72558362 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/anomalies/table.tsx @@ -22,7 +22,6 @@ import useSet from 'react-use/lib/useSet'; import { TimeRange } from '../../../../../../common/time/time_range'; import { AnomalyType, - formatAnomalyScore, getFriendlyNameForPartitionId, formatOneDecimalPlace, isCategoryAnomaly, @@ -47,7 +46,6 @@ import { LoadingOverlayWrapper } from '../../../../../components/loading_overlay interface TableItem { id: string; dataset: string; - datasetName: string; anomalyScore: number; startTime: number; typical: number; @@ -86,7 +84,6 @@ const datasetColumnName = i18n.translate( export const AnomaliesTable: React.FunctionComponent<{ results: LogEntryAnomalies; - setTimeRange: (timeRange: TimeRange) => void; timeRange: TimeRange; changeSortOptions: ChangeSortOptions; changePaginationOptions: ChangePaginationOptions; @@ -99,7 +96,6 @@ export const AnomaliesTable: React.FunctionComponent<{ }> = ({ results, timeRange, - setTimeRange, changeSortOptions, sortOptions, changePaginationOptions, @@ -122,8 +118,7 @@ export const AnomaliesTable: React.FunctionComponent<{ return { id: anomaly.id, dataset: anomaly.dataset, - datasetName: getFriendlyNameForPartitionId(anomaly.dataset), - anomalyScore: formatAnomalyScore(anomaly.anomalyScore), + anomalyScore: anomaly.anomalyScore, startTime: anomaly.startTime, type: anomaly.type, typical: anomaly.typical, @@ -182,11 +177,12 @@ export const AnomaliesTable: React.FunctionComponent<{ render: (startTime: number) => moment(startTime).format(dateFormat), }, { - field: 'datasetName', + field: 'dataset', name: datasetColumnName, sortable: true, truncateText: true, width: '200px', + render: (dataset: string) => getFriendlyNameForPartitionId(dataset), }, { align: RIGHT_ALIGNMENT, diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/helpers/data_formatters.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/helpers/data_formatters.tsx deleted file mode 100644 index 8041ad1458546..0000000000000 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/sections/helpers/data_formatters.tsx +++ /dev/null @@ -1,182 +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 { RectAnnotationDatum } from '@elastic/charts'; -import { i18n } from '@kbn/i18n'; - -import { - formatAnomalyScore, - getFriendlyNameForPartitionId, - getSeverityCategoryForScore, - MLSeverityScoreCategories, -} from '../../../../../../common/log_analysis'; -import { LogEntryRateResults } from '../../use_log_entry_rate_results'; - -export const getLogEntryRatePartitionedSeries = (results: LogEntryRateResults) => { - return results.histogramBuckets.reduce>( - (buckets, bucket) => { - return [ - ...buckets, - ...bucket.partitions.map((partition) => ({ - group: getFriendlyNameForPartitionId(partition.partitionId), - time: bucket.startTime, - value: partition.averageActualLogEntryRate, - })), - ]; - }, - [] - ); -}; - -export const getLogEntryRateCombinedSeries = (results: LogEntryRateResults) => { - return results.histogramBuckets.reduce>( - (buckets, bucket) => { - return [ - ...buckets, - { - time: bucket.startTime, - value: bucket.partitions.reduce((accumulatedValue, partition) => { - return accumulatedValue + partition.averageActualLogEntryRate; - }, 0), - }, - ]; - }, - [] - ); -}; - -export const getLogEntryRateSeriesForPartition = ( - results: LogEntryRateResults, - partitionId: string -) => { - return results.partitionBuckets[partitionId].buckets.reduce< - Array<{ time: number; value: number }> - >((buckets, bucket) => { - return [ - ...buckets, - { - time: bucket.startTime, - value: bucket.averageActualLogEntryRate, - }, - ]; - }, []); -}; - -export const getAnnotationsForPartition = (results: LogEntryRateResults, partitionId: string) => { - return results.partitionBuckets[partitionId].buckets.reduce< - Record - >( - (annotatedBucketsBySeverity, bucket) => { - const severityCategory = getSeverityCategoryForScore(bucket.maximumAnomalyScore); - if (!severityCategory) { - return annotatedBucketsBySeverity; - } - - return { - ...annotatedBucketsBySeverity, - [severityCategory]: [ - ...annotatedBucketsBySeverity[severityCategory], - { - coordinates: { - x0: bucket.startTime, - x1: bucket.startTime + results.bucketDuration, - }, - details: i18n.translate( - 'xpack.infra.logs.analysis.partitionMaxAnomalyScoreAnnotationLabel', - { - defaultMessage: 'Max anomaly score: {maxAnomalyScore}', - values: { - maxAnomalyScore: formatAnomalyScore(bucket.maximumAnomalyScore), - }, - } - ), - }, - ], - }; - }, - { - warning: [], - minor: [], - major: [], - critical: [], - } - ); -}; - -export const getTotalNumberOfLogEntriesForPartition = ( - results: LogEntryRateResults, - partitionId: string -) => { - return results.partitionBuckets[partitionId].totalNumberOfLogEntries; -}; - -export const getAnnotationsForAll = (results: LogEntryRateResults) => { - return results.histogramBuckets.reduce>( - (annotatedBucketsBySeverity, bucket) => { - const maxAnomalyScoresByPartition = bucket.partitions.reduce< - Array<{ partitionName: string; maximumAnomalyScore: number }> - >((bucketMaxAnomalyScoresByPartition, partition) => { - if (!getSeverityCategoryForScore(partition.maximumAnomalyScore)) { - return bucketMaxAnomalyScoresByPartition; - } - return [ - ...bucketMaxAnomalyScoresByPartition, - { - partitionName: getFriendlyNameForPartitionId(partition.partitionId), - maximumAnomalyScore: formatAnomalyScore(partition.maximumAnomalyScore), - }, - ]; - }, []); - - if (maxAnomalyScoresByPartition.length === 0) { - return annotatedBucketsBySeverity; - } - const severityCategory = getSeverityCategoryForScore( - Math.max( - ...maxAnomalyScoresByPartition.map((partitionScore) => partitionScore.maximumAnomalyScore) - ) - ); - if (!severityCategory) { - return annotatedBucketsBySeverity; - } - const sortedMaxAnomalyScoresByPartition = maxAnomalyScoresByPartition.sort( - (a, b) => b.maximumAnomalyScore - a.maximumAnomalyScore - ); - return { - ...annotatedBucketsBySeverity, - [severityCategory]: [ - ...annotatedBucketsBySeverity[severityCategory], - { - coordinates: { - x0: bucket.startTime, - x1: bucket.startTime + results.bucketDuration, - }, - details: JSON.stringify({ - anomalyScoresByPartition: sortedMaxAnomalyScoresByPartition, - }), - }, - ], - }; - }, - { - warning: [], - minor: [], - major: [], - critical: [], - } - ); -}; - -export const getTopAnomalyScoreAcrossAllPartitions = (results: LogEntryRateResults) => { - const allTopScores = Object.values(results.partitionBuckets).reduce( - (scores: number[], partition) => { - return [...scores, partition.topAnomalyScore]; - }, - [] - ); - return Math.max(...allTopScores); -}; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/service_calls/get_log_entry_rate.ts b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/service_calls/get_log_entry_rate.ts deleted file mode 100644 index 4b677140e2a7a..0000000000000 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/service_calls/get_log_entry_rate.ts +++ /dev/null @@ -1,43 +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 type { HttpHandler } from 'src/core/public'; -import { - getLogEntryRateRequestPayloadRT, - getLogEntryRateSuccessReponsePayloadRT, - LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH, -} from '../../../../../common/http_api/log_analysis'; -import { decodeOrThrow } from '../../../../../common/runtime_types'; - -interface RequestArgs { - sourceId: string; - startTime: number; - endTime: number; - bucketDuration: number; - datasets?: string[]; -} - -export const callGetLogEntryRateAPI = async (requestArgs: RequestArgs, fetch: HttpHandler) => { - const { sourceId, startTime, endTime, bucketDuration, datasets } = requestArgs; - const response = await fetch(LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH, { - method: 'POST', - body: JSON.stringify( - getLogEntryRateRequestPayloadRT.encode({ - data: { - sourceId, - timeRange: { - startTime, - endTime, - }, - bucketDuration, - datasets, - }, - }) - ), - }); - return decodeOrThrow(getLogEntryRateSuccessReponsePayloadRT)(response); -}; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_dataset_filtering.ts b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_dataset_filtering.ts new file mode 100644 index 0000000000000..9bd1e42779a36 --- /dev/null +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_dataset_filtering.ts @@ -0,0 +1,99 @@ +/* + * 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, useReducer, useCallback } from 'react'; +import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; +import { Filter } from '../../../../../../../src/plugins/data/common'; +import { CONTROLLED_BY_SWIM_LANE_FILTER } from '../../../../../ml/public'; + +interface ReducerState { + selectedDatasets: string[]; + selectedDatasetsFilters: Filter[]; +} + +type ReducerAction = + | { type: 'changeSelectedDatasets'; payload: { datasets: string[] } } + | { type: 'updateDatasetsFilters'; payload: { filters: Filter[] } }; + +const initialState: ReducerState = { + selectedDatasets: [], + selectedDatasetsFilters: [], +}; + +function reducer(state: ReducerState, action: ReducerAction) { + switch (action.type) { + case 'changeSelectedDatasets': + return { + ...state, + selectedDatasets: action.payload.datasets, + }; + case 'updateDatasetsFilters': + const datasetsToAdd = action.payload.filters + .filter((filter) => !state.selectedDatasets.includes(filter.meta.params.query)) + .map((filter) => filter.meta.params.query); + return { + ...state, + selectedDatasets: [...state.selectedDatasets, ...datasetsToAdd], + selectedDatasetsFilters: action.payload.filters, + }; + default: + throw new Error('Unknown action'); + } +} + +export const useDatasetFiltering = () => { + const { services } = useKibanaContextForPlugin(); + const [reducerState, dispatch] = useReducer(reducer, initialState); + + const handleSetSelectedDatasets = useCallback( + (datasets: string[]) => { + dispatch({ type: 'changeSelectedDatasets', payload: { datasets } }); + }, + [dispatch] + ); + + // NOTE: The anomaly swimlane embeddable will communicate it's filter action + // changes via the filterManager service. + useEffect(() => { + const sub = services.data.query.filterManager.getUpdates$().subscribe(() => { + const filters = services.data.query.filterManager + .getFilters() + .filter( + (filter) => + filter.meta.controlledBy && filter.meta.controlledBy === CONTROLLED_BY_SWIM_LANE_FILTER + ); + dispatch({ type: 'updateDatasetsFilters', payload: { filters } }); + }); + + return () => sub.unsubscribe(); + }, [services.data.query.filterManager, dispatch]); + + // NOTE: When filters are removed via the UI we need to make sure these are also tidied up + // within the FilterManager service, otherwise a scenario can occur where that filter can't + // be re-added via the embeddable as it will be seen as a duplicate to the FilterManager, + // and no update will be emitted. + useEffect(() => { + const filtersToRemove = reducerState.selectedDatasetsFilters.filter( + (filter) => !reducerState.selectedDatasets.includes(filter.meta.params.query) + ); + if (filtersToRemove.length > 0) { + filtersToRemove.forEach((filter) => { + services.data.query.filterManager.removeFilter(filter); + }); + } + }, [ + reducerState.selectedDatasets, + reducerState.selectedDatasetsFilters, + services.data.query.filterManager, + ]); + + return { + selectedDatasets: reducerState.selectedDatasets, + setSelectedDatasets: handleSetSelectedDatasets, + selectedDatasetsFilters: reducerState.selectedDatasetsFilters, + }; +}; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results.ts b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results.ts deleted file mode 100644 index a226977a30c57..0000000000000 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results.ts +++ /dev/null @@ -1,160 +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 { useMemo, useState } from 'react'; - -import { - GetLogEntryRateSuccessResponsePayload, - LogEntryRateHistogramBucket, - LogEntryRatePartition, - LogEntryRateAnomaly, -} from '../../../../common/http_api/log_analysis'; -import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; -import { useTrackedPromise } from '../../../utils/use_tracked_promise'; -import { callGetLogEntryRateAPI } from './service_calls/get_log_entry_rate'; - -type PartitionBucket = LogEntryRatePartition & { - startTime: number; -}; - -type PartitionRecord = Record< - string, - { buckets: PartitionBucket[]; topAnomalyScore: number; totalNumberOfLogEntries: number } ->; - -export type AnomalyRecord = LogEntryRateAnomaly & { - partitionId: string; -}; - -export interface LogEntryRateResults { - bucketDuration: number; - totalNumberOfLogEntries: number; - histogramBuckets: LogEntryRateHistogramBucket[]; - partitionBuckets: PartitionRecord; - anomalies: AnomalyRecord[]; -} - -export const useLogEntryRateResults = ({ - sourceId, - startTime, - endTime, - bucketDuration = 15 * 60 * 1000, - filteredDatasets, -}: { - sourceId: string; - startTime: number; - endTime: number; - bucketDuration: number; - filteredDatasets?: string[]; -}) => { - const { services } = useKibanaContextForPlugin(); - const [logEntryRate, setLogEntryRate] = useState(null); - - const [getLogEntryRateRequest, getLogEntryRate] = useTrackedPromise( - { - cancelPreviousOn: 'resolution', - createPromise: async () => { - return await callGetLogEntryRateAPI( - { - sourceId, - startTime, - endTime, - bucketDuration, - datasets: filteredDatasets, - }, - services.http.fetch - ); - }, - onResolve: ({ data }) => { - setLogEntryRate({ - bucketDuration: data.bucketDuration, - totalNumberOfLogEntries: data.totalNumberOfLogEntries, - histogramBuckets: data.histogramBuckets, - partitionBuckets: formatLogEntryRateResultsByPartition(data), - anomalies: formatLogEntryRateResultsByAllAnomalies(data), - }); - }, - onReject: () => { - setLogEntryRate(null); - }, - }, - [sourceId, startTime, endTime, bucketDuration, filteredDatasets] - ); - - const isLoading = useMemo(() => getLogEntryRateRequest.state === 'pending', [ - getLogEntryRateRequest.state, - ]); - - return { - getLogEntryRate, - isLoading, - logEntryRate, - }; -}; - -const formatLogEntryRateResultsByPartition = ( - results: GetLogEntryRateSuccessResponsePayload['data'] -): PartitionRecord => { - const partitionedBuckets = results.histogramBuckets.reduce< - Record - >((partitionResults, bucket) => { - return bucket.partitions.reduce>( - (_partitionResults, partition) => { - return { - ..._partitionResults, - [partition.partitionId]: { - buckets: _partitionResults[partition.partitionId] - ? [ - ..._partitionResults[partition.partitionId].buckets, - { startTime: bucket.startTime, ...partition }, - ] - : [{ startTime: bucket.startTime, ...partition }], - }, - }; - }, - partitionResults - ); - }, {}); - - const resultsByPartition: PartitionRecord = {}; - - Object.entries(partitionedBuckets).map(([key, value]) => { - const anomalyScores = value.buckets.reduce((scores: number[], bucket) => { - return [...scores, bucket.maximumAnomalyScore]; - }, []); - const totalNumberOfLogEntries = value.buckets.reduce((total, bucket) => { - return (total += bucket.numberOfLogEntries); - }, 0); - resultsByPartition[key] = { - topAnomalyScore: Math.max(...anomalyScores), - totalNumberOfLogEntries, - buckets: value.buckets, - }; - }); - - return resultsByPartition; -}; - -const formatLogEntryRateResultsByAllAnomalies = ( - results: GetLogEntryRateSuccessResponsePayload['data'] -): AnomalyRecord[] => { - return results.histogramBuckets.reduce((anomalies, bucket) => { - return bucket.partitions.reduce((_anomalies, partition) => { - if (partition.anomalies.length > 0) { - partition.anomalies.forEach((anomaly) => { - _anomalies.push({ - partitionId: partition.partitionId, - ...anomaly, - }); - }); - return _anomalies; - } else { - return _anomalies; - } - }, anomalies); - }, []); -}; diff --git a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results_url_state.tsx b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results_url_state.tsx index fdbde1acb83ad..ccfae14fd4a59 100644 --- a/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results_url_state.tsx +++ b/x-pack/plugins/infra/public/pages/logs/log_entry_rate/use_log_entry_rate_results_url_state.tsx @@ -5,24 +5,32 @@ * 2.0. */ -import { fold } from 'fp-ts/lib/Either'; -import { constant, identity } from 'fp-ts/lib/function'; -import { pipe } from 'fp-ts/lib/pipeable'; +import { useCallback, useMemo, useState } from 'react'; +import datemath from '@elastic/datemath'; +import moment from 'moment'; import * as rt from 'io-ts'; - +import { TimeRange as KibanaTimeRange } from '../../../../../../../src/plugins/data/public'; +import { TimeRange } from '../../../../common/time/time_range'; import { useUrlState } from '../../../utils/use_url_state'; +import { useInterval } from '../../../hooks/use_interval'; import { useKibanaTimefilterTime, useSyncKibanaTimeFilterTime, } from '../../../hooks/use_kibana_timefilter_time'; +import { decodeOrThrow } from '../../../../common/runtime_types'; -const autoRefreshRT = rt.union([ - rt.type({ - interval: rt.number, - isPaused: rt.boolean, - }), - rt.undefined, -]); +const autoRefreshRT = rt.type({ + interval: rt.number, + isPaused: rt.boolean, +}); + +export type AutoRefresh = rt.TypeOf; +const urlAutoRefreshRT = rt.union([autoRefreshRT, rt.undefined]); +const decodeAutoRefreshUrlState = decodeOrThrow(urlAutoRefreshRT); +const defaultAutoRefreshState = { + isPaused: false, + interval: 30000, +}; export const stringTimeRangeRT = rt.type({ startTime: rt.string, @@ -31,6 +39,7 @@ export const stringTimeRangeRT = rt.type({ export type StringTimeRange = rt.TypeOf; const urlTimeRangeRT = rt.union([stringTimeRangeRT, rt.undefined]); +const decodeTimeRangeUrlState = decodeOrThrow(urlTimeRangeRT); const TIME_RANGE_URL_STATE_KEY = 'timeRange'; const AUTOREFRESH_URL_STATE_KEY = 'autoRefresh'; @@ -40,36 +49,102 @@ export const useLogAnalysisResultsUrlState = () => { const [getTime] = useKibanaTimefilterTime(TIME_DEFAULTS); const { from: start, to: end } = getTime(); - const [timeRange, setTimeRange] = useUrlState({ - defaultState: { + const defaultTimeRangeState = useMemo(() => { + return { startTime: start, endTime: end, - }, - decodeUrlState: (value: unknown) => - pipe(urlTimeRangeRT.decode(value), fold(constant(undefined), identity)), + }; + }, [start, end]); + + const [urlTimeRange, setUrlTimeRange] = useUrlState({ + defaultState: defaultTimeRangeState, + decodeUrlState: decodeTimeRangeUrlState, encodeUrlState: urlTimeRangeRT.encode, urlStateKey: TIME_RANGE_URL_STATE_KEY, writeDefaultState: true, }); - useSyncKibanaTimeFilterTime(TIME_DEFAULTS, { from: timeRange.startTime, to: timeRange.endTime }); + // Numeric time range for querying APIs + const [queryTimeRange, setQueryTimeRange] = useState<{ + value: TimeRange; + lastChangedTime: number; + }>(() => ({ + value: stringToNumericTimeRange({ start: urlTimeRange.startTime, end: urlTimeRange.endTime }), + lastChangedTime: Date.now(), + })); - const [autoRefresh, setAutoRefresh] = useUrlState({ - defaultState: { - isPaused: false, - interval: 30000, + const handleQueryTimeRangeChange = useCallback( + ({ start: startTime, end: endTime }: { start: string; end: string }) => { + setQueryTimeRange({ + value: stringToNumericTimeRange({ start: startTime, end: endTime }), + lastChangedTime: Date.now(), + }); + }, + [setQueryTimeRange] + ); + + const setTimeRange = useCallback( + (selectedTime: { start: string; end: string }) => { + setUrlTimeRange({ + startTime: selectedTime.start, + endTime: selectedTime.end, + }); + handleQueryTimeRangeChange(selectedTime); }, - decodeUrlState: (value: unknown) => - pipe(autoRefreshRT.decode(value), fold(constant(undefined), identity)), - encodeUrlState: autoRefreshRT.encode, + [setUrlTimeRange, handleQueryTimeRangeChange] + ); + + const handleTimeFilterChange = useCallback( + (newTimeRange: KibanaTimeRange) => { + const { from, to } = newTimeRange; + setTimeRange({ start: from, end: to }); + }, + [setTimeRange] + ); + + useSyncKibanaTimeFilterTime( + TIME_DEFAULTS, + { from: urlTimeRange.startTime, to: urlTimeRange.endTime }, + handleTimeFilterChange + ); + + const [autoRefresh, setAutoRefresh] = useUrlState({ + defaultState: defaultAutoRefreshState, + decodeUrlState: decodeAutoRefreshUrlState, + encodeUrlState: urlAutoRefreshRT.encode, urlStateKey: AUTOREFRESH_URL_STATE_KEY, writeDefaultState: true, }); + useInterval( + () => { + handleQueryTimeRangeChange({ + start: urlTimeRange.startTime, + end: urlTimeRange.endTime, + }); + }, + autoRefresh.isPaused ? null : autoRefresh.interval + ); + return { - timeRange, + timeRange: queryTimeRange, + friendlyTimeRange: urlTimeRange, setTimeRange, autoRefresh, setAutoRefresh, }; }; + +const stringToNumericTimeRange = (timeRange: { start: string; end: string }): TimeRange => ({ + startTime: moment( + datemath.parse(timeRange.start, { + momentInstance: moment, + }) + ).valueOf(), + endTime: moment( + datemath.parse(timeRange.end, { + momentInstance: moment, + roundUp: true, + }) + ).valueOf(), +}); diff --git a/x-pack/plugins/infra/public/types.ts b/x-pack/plugins/infra/public/types.ts index b78912bfba3ac..b18b6e8a6eba6 100644 --- a/x-pack/plugins/infra/public/types.ts +++ b/x-pack/plugins/infra/public/types.ts @@ -24,6 +24,7 @@ import type { } from '../../observability/public'; import type { SpacesPluginStart } from '../../spaces/public'; import { MlPluginStart } from '../../ml/public'; +import type { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; // Our own setup and start contract values export type InfraClientSetupExports = void; @@ -46,6 +47,7 @@ export interface InfraClientStartDeps { triggersActionsUi: TriggersAndActionsUIPublicPluginStart; usageCollection: UsageCollectionStart; ml: MlPluginStart; + embeddable?: EmbeddableStart; } export type InfraClientCoreSetup = CoreSetup; diff --git a/x-pack/plugins/infra/public/utils/use_url_state.ts b/x-pack/plugins/infra/public/utils/use_url_state.ts index fd927bb5ef662..970b3a20b2951 100644 --- a/x-pack/plugins/infra/public/utils/use_url_state.ts +++ b/x-pack/plugins/infra/public/utils/use_url_state.ts @@ -38,15 +38,13 @@ export const useUrlState = ({ return getParamFromQueryString(queryString, urlStateKey); }, [queryString, urlStateKey]); - const decodedState = useMemo(() => decodeUrlState(decodeRisonUrlState(urlStateString)), [ - decodeUrlState, - urlStateString, - ]); - - const state = useMemo(() => (typeof decodedState !== 'undefined' ? decodedState : defaultState), [ - defaultState, - decodedState, - ]); + const decodedState = useMemo(() => { + return decodeUrlState(decodeRisonUrlState(urlStateString)); + }, [decodeUrlState, urlStateString]); + + const state = useMemo(() => { + return typeof decodedState !== 'undefined' ? decodedState : defaultState; + }, [defaultState, decodedState]); const setState = useCallback( (newState: State | undefined) => { diff --git a/x-pack/plugins/infra/server/infra_server.ts b/x-pack/plugins/infra/server/infra_server.ts index 00ec36d866624..8a6f22d55750e 100644 --- a/x-pack/plugins/infra/server/infra_server.ts +++ b/x-pack/plugins/infra/server/infra_server.ts @@ -12,7 +12,6 @@ import { initGetLogEntryCategoryDatasetsRoute, initGetLogEntryCategoryDatasetsStatsRoute, initGetLogEntryCategoryExamplesRoute, - initGetLogEntryRateRoute, initGetLogEntryExamplesRoute, initValidateLogAnalysisDatasetsRoute, initValidateLogAnalysisIndicesRoute, @@ -46,7 +45,6 @@ export const initInfraServer = (libs: InfraBackendLibs) => { initGetLogEntryCategoryDatasetsRoute(libs); initGetLogEntryCategoryDatasetsStatsRoute(libs); initGetLogEntryCategoryExamplesRoute(libs); - initGetLogEntryRateRoute(libs); initGetLogEntryAnomaliesRoute(libs); initGetLogEntryAnomaliesDatasetsRoute(libs); initGetK8sAnomaliesRoute(libs); diff --git a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_anomalies.ts b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_anomalies.ts index 3fc098bcf8846..f5465a967f2a5 100644 --- a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_anomalies.ts +++ b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_anomalies.ts @@ -281,7 +281,6 @@ async function fetchLogEntryAnomalies( nextPageCursor: hits[hits.length - 1].sort, } : undefined; - const anomalies = hits.map((result) => { const { // eslint-disable-next-line @typescript-eslint/naming-convention diff --git a/x-pack/plugins/infra/server/routes/log_analysis/results/index.ts b/x-pack/plugins/infra/server/routes/log_analysis/results/index.ts index d50495689e9d8..23c2ce5f0c21f 100644 --- a/x-pack/plugins/infra/server/routes/log_analysis/results/index.ts +++ b/x-pack/plugins/infra/server/routes/log_analysis/results/index.ts @@ -9,7 +9,6 @@ export * from './log_entry_categories'; export * from './log_entry_category_datasets'; export * from './log_entry_category_datasets_stats'; export * from './log_entry_category_examples'; -export * from './log_entry_rate'; export * from './log_entry_examples'; export * from './log_entry_anomalies'; export * from './log_entry_anomalies_datasets'; diff --git a/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_rate.ts b/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_rate.ts deleted file mode 100644 index c1762f88a6cdd..0000000000000 --- a/x-pack/plugins/infra/server/routes/log_analysis/results/log_entry_rate.ts +++ /dev/null @@ -1,90 +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 Boom from '@hapi/boom'; -import { InfraBackendLibs } from '../../../lib/infra_types'; -import { - LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH, - getLogEntryRateRequestPayloadRT, - getLogEntryRateSuccessReponsePayloadRT, - GetLogEntryRateSuccessResponsePayload, -} from '../../../../common/http_api/log_analysis'; -import { createValidationFunction } from '../../../../common/runtime_types'; -import { getLogEntryRateBuckets } from '../../../lib/log_analysis'; -import { assertHasInfraMlPlugins } from '../../../utils/request_context'; -import { isMlPrivilegesError } from '../../../lib/log_analysis/errors'; - -export const initGetLogEntryRateRoute = ({ framework }: InfraBackendLibs) => { - framework.registerRoute( - { - method: 'post', - path: LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH, - validate: { - body: createValidationFunction(getLogEntryRateRequestPayloadRT), - }, - }, - framework.router.handleLegacyErrors(async (requestContext, request, response) => { - const { - data: { sourceId, timeRange, bucketDuration, datasets }, - } = request.body; - - try { - assertHasInfraMlPlugins(requestContext); - - const logEntryRateBuckets = await getLogEntryRateBuckets( - requestContext, - sourceId, - timeRange.startTime, - timeRange.endTime, - bucketDuration, - datasets - ); - - return response.ok({ - body: getLogEntryRateSuccessReponsePayloadRT.encode({ - data: { - bucketDuration, - histogramBuckets: logEntryRateBuckets, - totalNumberOfLogEntries: getTotalNumberOfLogEntries(logEntryRateBuckets), - }, - }), - }); - } catch (error) { - if (Boom.isBoom(error)) { - throw error; - } - - if (isMlPrivilegesError(error)) { - return response.customError({ - statusCode: 403, - body: { - message: error.message, - }, - }); - } - - return response.customError({ - statusCode: error.statusCode ?? 500, - body: { - message: error.message ?? 'An unexpected error occurred', - }, - }); - } - }) - ); -}; - -const getTotalNumberOfLogEntries = ( - logEntryRateBuckets: GetLogEntryRateSuccessResponsePayload['data']['histogramBuckets'] -) => { - return logEntryRateBuckets.reduce((sumNumberOfLogEntries, bucket) => { - const sumPartitions = bucket.partitions.reduce((partitionsTotal, partition) => { - return (partitionsTotal += partition.numberOfLogEntries); - }, 0); - return (sumNumberOfLogEntries += sumPartitions); - }, 0); -}; diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts index 0d75db64a01b9..fa0cccda99d22 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/swimlane_input_resolver.ts @@ -36,7 +36,7 @@ import { parseInterval } from '../../../common/util/parse_interval'; import { AnomalyDetectorService } from '../../application/services/anomaly_detector_service'; import { isViewBySwimLaneData } from '../../application/explorer/swimlane_container'; import { ViewMode } from '../../../../../../src/plugins/embeddable/public'; -import { CONTROLLED_BY_SWIM_LANE_FILTER } from '../../ui_actions/apply_influencer_filters_action'; +import { CONTROLLED_BY_SWIM_LANE_FILTER } from '../../ui_actions/constants'; import { AnomalySwimlaneEmbeddableInput, AnomalySwimlaneEmbeddableOutput, diff --git a/x-pack/plugins/ml/public/index.ts b/x-pack/plugins/ml/public/index.ts index 1c4aa4031171d..c88ce2d7f95d2 100755 --- a/x-pack/plugins/ml/public/index.ts +++ b/x-pack/plugins/ml/public/index.ts @@ -39,8 +39,18 @@ export type { RenderCellValue, } from './shared'; +export type { AnomalySwimlaneEmbeddableInput } from './embeddables'; + +export { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE } from './embeddables/constants'; +export { CONTROLLED_BY_SWIM_LANE_FILTER } from './ui_actions/constants'; + // Static exports -export { getSeverityColor, getSeverityType } from '../common/util/anomaly_utils'; +export { + getSeverityColor, + getSeverityType, + getFormattedSeverityScore, +} from '../common/util/anomaly_utils'; + export { ANOMALY_SEVERITY } from '../common'; export { useMlHref, ML_PAGES, MlUrlGenerator } from './ml_url_generator'; diff --git a/x-pack/plugins/ml/public/ui_actions/apply_influencer_filters_action.tsx b/x-pack/plugins/ml/public/ui_actions/apply_influencer_filters_action.tsx index e9b70ee14aae6..e3d2ca4ce0de1 100644 --- a/x-pack/plugins/ml/public/ui_actions/apply_influencer_filters_action.tsx +++ b/x-pack/plugins/ml/public/ui_actions/apply_influencer_filters_action.tsx @@ -11,11 +11,10 @@ import { MlCoreSetup } from '../plugin'; import { SWIMLANE_TYPE, VIEW_BY_JOB_LABEL } from '../application/explorer/explorer_constants'; import { Filter, FilterStateStore } from '../../../../../src/plugins/data/common'; import { ANOMALY_SWIMLANE_EMBEDDABLE_TYPE, SwimLaneDrilldownContext } from '../embeddables'; +import { CONTROLLED_BY_SWIM_LANE_FILTER } from './constants'; export const APPLY_INFLUENCER_FILTERS_ACTION = 'applyInfluencerFiltersAction'; -export const CONTROLLED_BY_SWIM_LANE_FILTER = 'anomaly-swim-lane'; - export function createApplyInfluencerFiltersAction( getStartServices: MlCoreSetup['getStartServices'] ) { diff --git a/x-pack/plugins/ml/public/ui_actions/constants.ts b/x-pack/plugins/ml/public/ui_actions/constants.ts new file mode 100644 index 0000000000000..6dc3f03d10fd9 --- /dev/null +++ b/x-pack/plugins/ml/public/ui_actions/constants.ts @@ -0,0 +1,8 @@ +/* + * 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 CONTROLLED_BY_SWIM_LANE_FILTER = 'anomaly-swim-lane'; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 439b9a93d0d97..278294dea9449 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -9893,7 +9893,6 @@ "xpack.infra.logs.analysis.anomaliesExpandedRowActualRateTitle": "{actualCount, plural, other {件のメッセージ}}", "xpack.infra.logs.analysis.anomaliesExpandedRowTypicalRateDescription": "通常", "xpack.infra.logs.analysis.anomaliesExpandedRowTypicalRateTitle": "{typicalCount, plural, other {件のメッセージ}}", - "xpack.infra.logs.analysis.anomaliesSectionLineSeriesName": "15 分ごとのログエントリー (平均)", "xpack.infra.logs.analysis.anomaliesSectionLoadingAriaLabel": "異常を読み込み中", "xpack.infra.logs.analysis.anomaliesSectionTitle": "異常", "xpack.infra.logs.analysis.anomaliesTableAnomalyDatasetName": "データセット", @@ -9905,7 +9904,6 @@ "xpack.infra.logs.analysis.anomaliesTableMoreThanExpectedAnomalyMessage": "この{type, select, logRate {データセット} logCategory {カテゴリ}}のログメッセージ数が想定よりも多くなっています", "xpack.infra.logs.analysis.anomaliesTableNextPageLabel": "次のページ", "xpack.infra.logs.analysis.anomaliesTablePreviousPageLabel": "前のページ", - "xpack.infra.logs.analysis.anomalySectionLogRateChartNoData": "表示するログレートデータがありません。", "xpack.infra.logs.analysis.anomalySectionNoDataBody": "時間範囲を調整する必要があるかもしれません。", "xpack.infra.logs.analysis.anomalySectionNoDataTitle": "表示するデータがありません。", "xpack.infra.logs.analysis.createJobButtonLabel": "MLジョブを作成", @@ -9934,8 +9932,6 @@ "xpack.infra.logs.analysis.mlUnavailableTitle": "この機能には機械学習が必要です", "xpack.infra.logs.analysis.onboardingSuccessContent": "機械学習ロボットがデータの収集を開始するまでしばらくお待ちください。", "xpack.infra.logs.analysis.onboardingSuccessTitle": "成功!", - "xpack.infra.logs.analysis.overallAnomalyChartMaxScoresLabel": "最高異常スコア", - "xpack.infra.logs.analysis.partitionMaxAnomalyScoreAnnotationLabel": "最大異常スコア:{maxAnomalyScore}", "xpack.infra.logs.analysis.recreateJobButtonLabel": "ML ジョブを再作成", "xpack.infra.logs.analysis.setupFlyoutGotoListButtonLabel": "すべての機械学習ジョブ", "xpack.infra.logs.analysis.setupFlyoutTitle": "機械学習を使用した異常検知", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 643192df99309..4704cb07d27b0 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -9919,7 +9919,6 @@ "xpack.infra.logs.analysis.anomaliesExpandedRowActualRateTitle": "{actualCount, plural, other {消息}}", "xpack.infra.logs.analysis.anomaliesExpandedRowTypicalRateDescription": "典型", "xpack.infra.logs.analysis.anomaliesExpandedRowTypicalRateTitle": "{typicalCount, plural, other {消息}}", - "xpack.infra.logs.analysis.anomaliesSectionLineSeriesName": "每 15 分钟日志条目数(平均值)", "xpack.infra.logs.analysis.anomaliesSectionLoadingAriaLabel": "正在加载异常", "xpack.infra.logs.analysis.anomaliesSectionTitle": "异常", "xpack.infra.logs.analysis.anomaliesTableAnomalyDatasetName": "数据集", @@ -9931,7 +9930,6 @@ "xpack.infra.logs.analysis.anomaliesTableMoreThanExpectedAnomalyMessage": "此{type, select, logRate {数据集} logCategory {类别}}中的日志消息多于预期", "xpack.infra.logs.analysis.anomaliesTableNextPageLabel": "下一页", "xpack.infra.logs.analysis.anomaliesTablePreviousPageLabel": "上一页", - "xpack.infra.logs.analysis.anomalySectionLogRateChartNoData": "没有要显示的日志速率数据。", "xpack.infra.logs.analysis.anomalySectionNoDataBody": "您可能想调整时间范围。", "xpack.infra.logs.analysis.anomalySectionNoDataTitle": "没有可显示的数据。", "xpack.infra.logs.analysis.createJobButtonLabel": "创建 ML 作业", @@ -9960,8 +9958,6 @@ "xpack.infra.logs.analysis.mlUnavailableTitle": "此功能需要 Machine Learning", "xpack.infra.logs.analysis.onboardingSuccessContent": "请注意,我们的 Machine Learning 机器人若干分钟后才会开始收集数据。", "xpack.infra.logs.analysis.onboardingSuccessTitle": "成功!", - "xpack.infra.logs.analysis.overallAnomalyChartMaxScoresLabel": "最大异常分数:", - "xpack.infra.logs.analysis.partitionMaxAnomalyScoreAnnotationLabel": "最大异常分数:{maxAnomalyScore}", "xpack.infra.logs.analysis.recreateJobButtonLabel": "重新创建 ML 作业", "xpack.infra.logs.analysis.setupFlyoutGotoListButtonLabel": "所有 Machine Learning 作业", "xpack.infra.logs.analysis.setupFlyoutTitle": "通过 Machine Learning 检测异常", diff --git a/x-pack/test/api_integration/apis/metrics_ui/index.js b/x-pack/test/api_integration/apis/metrics_ui/index.js index 254360ce64922..34ad92e6b89a6 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/index.js +++ b/x-pack/test/api_integration/apis/metrics_ui/index.js @@ -8,7 +8,6 @@ export default function ({ loadTestFile }) { describe('MetricsUI Endpoints', () => { loadTestFile(require.resolve('./metadata')); - loadTestFile(require.resolve('./log_analysis')); loadTestFile(require.resolve('./log_entries')); loadTestFile(require.resolve('./log_entry_highlights')); loadTestFile(require.resolve('./logs_without_millis')); diff --git a/x-pack/test/api_integration/apis/metrics_ui/log_analysis.ts b/x-pack/test/api_integration/apis/metrics_ui/log_analysis.ts deleted file mode 100644 index ecfa0cc6f2438..0000000000000 --- a/x-pack/test/api_integration/apis/metrics_ui/log_analysis.ts +++ /dev/null @@ -1,137 +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 expect from '@kbn/expect'; - -import { pipe } from 'fp-ts/lib/pipeable'; -import { identity } from 'fp-ts/lib/function'; -import { fold } from 'fp-ts/lib/Either'; -import { - LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH, - getLogEntryRateRequestPayloadRT, - getLogEntryRateSuccessReponsePayloadRT, -} from '../../../../plugins/infra/common/http_api/log_analysis'; -import { createPlainError, throwErrors } from '../../../../plugins/infra/common/runtime_types'; -import { FtrProviderContext } from '../../ftr_provider_context'; - -const TIME_BEFORE_START = 1569934800000; -const TIME_AFTER_END = 1570016700000; -const COMMON_HEADERS = { - 'kbn-xsrf': 'some-xsrf-token', -}; -const ML_JOB_ID = 'kibana-logs-ui-default-default-log-entry-rate'; - -export default ({ getService }: FtrProviderContext) => { - const esArchiver = getService('esArchiver'); - const supertest = getService('supertest'); - const retry = getService('retry'); - - async function createDummyJob(jobId: string) { - await supertest - .put(`/api/ml/anomaly_detectors/${jobId}`) - .set(COMMON_HEADERS) - .send({ - job_id: jobId, - groups: [], - analysis_config: { - bucket_span: '15m', - detectors: [{ function: 'count' }], - influencers: [], - }, - data_description: { time_field: '@timestamp' }, - analysis_limits: { model_memory_limit: '11MB' }, - model_plot_config: { enabled: false, annotations_enabled: false }, - }) - .expect(200); - } - - async function deleteDummyJob(jobId: string) { - await supertest.delete(`/api/ml/anomaly_detectors/${jobId}`).set(COMMON_HEADERS).expect(200); - - await retry.waitForWithTimeout(`'${jobId}' to not exist`, 5 * 1000, async () => { - if (await supertest.get(`/api/ml/anomaly_detectors/${jobId}`).expect(404)) { - return true; - } else { - throw new Error(`expected anomaly detection job '${jobId}' not to exist`); - } - }); - } - - describe('log analysis apis', () => { - before(async () => { - // a real ML job must exist when searching for the results - await createDummyJob(ML_JOB_ID); - await esArchiver.load('infra/8.0.0/ml_anomalies_partitioned_log_rate'); - }); - after(async () => { - await deleteDummyJob(ML_JOB_ID); - await esArchiver.unload('infra/8.0.0/ml_anomalies_partitioned_log_rate'); - }); - - describe('log rate results', () => { - describe('with the default source', () => { - it('should return buckets when there are matching ml result documents', async () => { - const { body } = await supertest - .post(LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH) - .set(COMMON_HEADERS) - .send( - getLogEntryRateRequestPayloadRT.encode({ - data: { - sourceId: 'default', - timeRange: { - startTime: TIME_BEFORE_START, - endTime: TIME_AFTER_END, - }, - bucketDuration: 15 * 60 * 1000, - }, - }) - ) - .expect(200); - - const logEntryRateBuckets = pipe( - getLogEntryRateSuccessReponsePayloadRT.decode(body), - fold(throwErrors(createPlainError), identity) - ); - expect(logEntryRateBuckets.data.bucketDuration).to.be(15 * 60 * 1000); - expect(logEntryRateBuckets.data.histogramBuckets).to.not.be.empty(); - expect( - logEntryRateBuckets.data.histogramBuckets.some((bucket) => { - return bucket.partitions.some((partition) => partition.anomalies.length > 0); - }) - ).to.be(true); - }); - - it('should return no buckets when there are no matching ml result documents', async () => { - const { body } = await supertest - .post(LOG_ANALYSIS_GET_LOG_ENTRY_RATE_PATH) - .set(COMMON_HEADERS) - .send( - getLogEntryRateRequestPayloadRT.encode({ - data: { - sourceId: 'default', - timeRange: { - startTime: TIME_BEFORE_START - 10 * 15 * 60 * 1000, - endTime: TIME_BEFORE_START - 1, - }, - bucketDuration: 15 * 60 * 1000, - }, - }) - ) - .expect(200); - - const logEntryRateBuckets = pipe( - getLogEntryRateSuccessReponsePayloadRT.decode(body), - fold(throwErrors(createPlainError), identity) - ); - - expect(logEntryRateBuckets.data.bucketDuration).to.be(15 * 60 * 1000); - expect(logEntryRateBuckets.data.histogramBuckets).to.be.empty(); - }); - }); - }); - }); -}; From 5099eab19f12271b935efe47b8ed6ea508f8e00c Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Tue, 9 Feb 2021 16:29:16 +0100 Subject: [PATCH 20/28] [ILM] Simplify timeline and rollover info (#90004) * remove rollover indicator on timeline and relative timing text * simplify rollover description text * remove non-existent export * incorporate copy recommendations * slight visual adjustment to timeline, infinity icon more subdued, moved icon to left * update visual appearance of delete icon on timeline, grey circle and trash can * remove tooltip next to recommended defaults Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../edit_policy/edit_policy.helpers.tsx | 1 - .../edit_policy/edit_policy.test.ts | 8 - .../components/phases/hot_phase/hot_phase.tsx | 49 ++-- .../phases/shared_fields/forcemerge_field.tsx | 2 +- .../searchable_snapshot_field.tsx | 2 +- .../phases/shared_fields/shrink_field.tsx | 2 +- .../components/timeline_phase_text.tsx | 4 +- .../components/timeline/timeline.scss | 18 +- .../components/timeline/timeline.tsx | 48 ++-- .../sections/edit_policy/i18n_texts.ts | 4 +- ...absolute_timing_to_relative_timing.test.ts | 240 ------------------ .../lib/absolute_timing_to_relative_timing.ts | 52 ---- .../sections/edit_policy/lib/index.ts | 2 - 13 files changed, 58 insertions(+), 374 deletions(-) diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx index dc375f6370048..38049dd7c6cfa 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.helpers.tsx @@ -278,7 +278,6 @@ export const setup = async (arg?: { appServicesContext: Partial exists('policyFormErrorsCallout'), timeline: { - hasRolloverIndicator: () => exists('timelineHotPhaseRolloverToolTip'), hasHotPhase: () => exists('ilmTimelineHotPhase'), hasWarmPhase: () => exists('ilmTimelineWarmPhase'), hasColdPhase: () => exists('ilmTimelineColdPhase'), diff --git a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts index f2266741ec7d1..282daf780b86c 100644 --- a/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts +++ b/x-pack/plugins/index_lifecycle_management/__jest__/client_integration/edit_policy/edit_policy.test.ts @@ -845,14 +845,6 @@ describe('', () => { expect(actions.timeline.hasColdPhase()).toBe(true); expect(actions.timeline.hasDeletePhase()).toBe(true); }); - - test('show and hide rollover indicator on timeline', async () => { - const { actions } = testBed; - expect(actions.timeline.hasRolloverIndicator()).toBe(true); - await actions.hot.toggleDefaultRollover(false); - await actions.hot.toggleRollover(false); - expect(actions.timeline.hasRolloverIndicator()).toBe(false); - }); }); describe('policy error notifications', () => { diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx index 74809965a52d9..c77493476b929 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/hot_phase/hot_phase.tsx @@ -17,7 +17,7 @@ import { EuiTextColor, EuiSwitch, EuiIconTip, - EuiIcon, + EuiText, } from '@elastic/eui'; import { useFormData, SelectField, NumericField } from '../../../../../../shared_imports'; @@ -68,8 +68,20 @@ export const HotPhase: FunctionComponent = () => {

{' '} + defaultMessage="Start writing to a new index when the current index reaches a certain size, document count, or age. Enables you to optimize performance and manage resource usage when working with time series data." + /> +

+ + + +

+ + {i18n.translate( + 'xpack.indexLifecycleMgmt.rollover.rolloverOffsetsPhaseTimingDescriptionNote', + { defaultMessage: 'Note: ' } + )} + + {i18nTexts.editPolicy.rolloverOffsetsHotPhaseTiming}{' '} {

- -   - {i18nTexts.editPolicy.rolloverOffsetsHotPhaseTiming} - path={isUsingDefaultRolloverPath}> {(field) => ( <> - field.setValue(e.target.checked)} - data-test-subj="useDefaultRolloverSwitch" - /> -   - - } + + field.setValue(e.target.checked)} + data-test-subj="useDefaultRolloverSwitch" + /> + + + )} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx index bbdcbbf4759ef..8cb566ceae25a 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/forcemerge_field.tsx @@ -45,7 +45,7 @@ export const ForcemergeField: React.FunctionComponent = ({ phase }) => { <> {' '} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx index 9251b08742476..c85201f708a2b 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/searchable_snapshot_field/searchable_snapshot_field.tsx @@ -342,7 +342,7 @@ export const SearchableSnapshotField: FunctionComponent = ({ phase }) => , }} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/shrink_field.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/shrink_field.tsx index b5fb79811ee2d..8ac387ba106b7 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/shrink_field.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/phases/shared_fields/shrink_field.tsx @@ -38,7 +38,7 @@ export const ShrinkField: FunctionComponent = ({ phase }) => { {' '} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/components/timeline_phase_text.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/components/timeline_phase_text.tsx index 3a9f33fa3d169..62b100b85cbe2 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/components/timeline_phase_text.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/components/timeline_phase_text.tsx @@ -12,8 +12,8 @@ export const TimelinePhaseText: FunctionComponent<{ phaseName: ReactNode | string; durationInPhase?: ReactNode | string; }> = ({ phaseName, durationInPhase }) => ( - - + + {phaseName} diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.scss b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.scss index 7d65d2cd6b212..de49e665ed933 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.scss +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.scss @@ -1,11 +1,5 @@ $ilmTimelineBarHeight: $euiSizeS; -/* -* For theming we need to shade or tint to get the right color from the base EUI color -*/ -$ilmDeletePhaseBackgroundColor: tintOrShade($euiColorVis5_behindText, 80%,80%); -$ilmDeletePhaseColor: shadeOrTint($euiColorVis5, 40%, 40%); - .ilmTimeline { overflow: hidden; width: 100%; @@ -49,14 +43,16 @@ $ilmDeletePhaseColor: shadeOrTint($euiColorVis5, 40%, 40%); */ padding: $euiSizeM; margin-left: $euiSizeM; - background-color: $ilmDeletePhaseBackgroundColor; - color: $ilmDeletePhaseColor; - border-radius: calc(#{$euiSizeS} / 2); + background-color: $euiColorLightestShade; + color: $euiColorDarkShade; + border-radius: 50%; } &__colorBar { display: inline-block; height: $ilmTimelineBarHeight; + margin-top: $euiSizeS; + margin-bottom: $euiSizeXS; border-radius: calc(#{$ilmTimelineBarHeight} / 2); width: 100%; } @@ -84,8 +80,4 @@ $ilmDeletePhaseColor: shadeOrTint($euiColorVis5, 40%, 40%); background-color: $euiColorVis1; } } - - &__rolloverIcon { - display: inline-block; - } } diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx index 2d83009bd4df4..8097ab51eb59e 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/timeline/timeline.tsx @@ -8,14 +8,12 @@ import { i18n } from '@kbn/i18n'; import React, { FunctionComponent, memo } from 'react'; - -import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiIconTip } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiText, EuiIconTip } from '@elastic/eui'; import { PhasesExceptDelete } from '../../../../../../common/types'; import { calculateRelativeFromAbsoluteMilliseconds, - normalizeTimingsToHumanReadable, PhaseAgeInMilliseconds, AbsoluteTimings, } from '../../lib'; @@ -48,6 +46,12 @@ const msTimeToOverallPercent = (ms: number, totalMs: number) => { const SCORE_BUFFER_AMOUNT = 50; const i18nTexts = { + title: i18n.translate('xpack.indexLifecycleMgmt.timeline.title', { + defaultMessage: 'Policy Summary', + }), + description: i18n.translate('xpack.indexLifecycleMgmt.timeline.description', { + defaultMessage: 'This policy moves data through the following phases.', + }), hotPhase: i18n.translate('xpack.indexLifecycleMgmt.timeline.hotPhaseSectionTitle', { defaultMessage: 'Hot phase', }), @@ -69,6 +73,11 @@ const i18nTexts = { defaultMessage: 'Policy deletes the index after lifecycle phases complete.', }), }, + foreverIcon: { + ariaLabel: i18n.translate('xpack.indexLifecycleMgmt.timeline.foreverIconToolTipContent', { + defaultMessage: 'Forever', + }), + }, }; const calculateWidths = (inputs: PhaseAgeInMilliseconds) => { @@ -118,27 +127,23 @@ export const Timeline: FunctionComponent = memo( }; const phaseAgeInMilliseconds = calculateRelativeFromAbsoluteMilliseconds(absoluteTimings); - const humanReadableTimings = normalizeTimingsToHumanReadable(phaseAgeInMilliseconds); const widths = calculateWidths(phaseAgeInMilliseconds); const getDurationInPhaseContent = (phase: PhasesExceptDelete): string | React.ReactNode => phaseAgeInMilliseconds.phases[phase] === Infinity ? ( - - ) : ( - humanReadableTimings[phase] - ); + + ) : null; return ( -

- {i18n.translate('xpack.indexLifecycleMgmt.timeline.title', { - defaultMessage: 'Policy Timeline', - })} -

+

{i18nTexts.title}

+ + {i18nTexts.description} +
= memo( >
- {i18nTexts.hotPhase} -   -
- -
- - ) : ( - i18nTexts.hotPhase - ) - } + phaseName={i18nTexts.hotPhase} durationInPhase={getDurationInPhaseContent('hot')} />
diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts index 5deba8607cd52..3923cf93cd0d3 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/i18n_texts.ts @@ -16,7 +16,7 @@ export const i18nTexts = { 'xpack.indexLifecycleMgmt.rollover.rolloverOffsetsPhaseTimingDescription', { defaultMessage: - 'How long it takes to reach the rollover criteria in the hot phase can vary. Data moves to the next phase when the time since rollover reaches the minimum age.', + 'How long it takes to reach the rollover criteria in the hot phase can vary.', } ), searchableSnapshotInHotPhase: { @@ -195,7 +195,7 @@ export const i18nTexts = { descriptions: { hot: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.hotPhase.hotPhaseDescription', { defaultMessage: - 'This phase is required. You are actively querying and writing to your index. For faster updates, you can roll over the index when it gets too big or too old.', + 'You actively store and query data in the hot phase. All policies have a hot phase.', }), warm: i18n.translate('xpack.indexLifecycleMgmt.editPolicy.warmPhase.warmPhaseDescription', { defaultMessage: diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.test.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.test.ts index 7ec20cc2a5966..8a9635e2db219 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.test.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.test.ts @@ -11,7 +11,6 @@ import { deserializer } from '../form'; import { formDataToAbsoluteTimings, calculateRelativeFromAbsoluteMilliseconds, - absoluteTimingToRelativeTiming, } from './absolute_timing_to_relative_timing'; export const calculateRelativeTimingMs = flow( @@ -273,243 +272,4 @@ describe('Conversion of absolute policy timing to relative timing', () => { }); }); }); - - describe('absoluteTimingToRelativeTiming', () => { - describe('policy that never deletes data (keep forever)', () => { - test('always hot', () => { - expect( - absoluteTimingToRelativeTiming( - deserializer({ - name: 'test', - phases: { - hot: { - min_age: '0ms', - actions: {}, - }, - }, - }) - ) - ).toEqual({ total: 'forever', hot: 'forever', warm: undefined, cold: undefined }); - }); - - test('hot, then always warm', () => { - expect( - absoluteTimingToRelativeTiming( - deserializer({ - name: 'test', - phases: { - hot: { - min_age: '0ms', - actions: {}, - }, - warm: { - actions: {}, - }, - }, - }) - ) - ).toEqual({ total: 'forever', hot: 'less than a day', warm: 'forever', cold: undefined }); - }); - - test('hot, then warm, then always cold', () => { - expect( - absoluteTimingToRelativeTiming( - deserializer({ - name: 'test', - phases: { - hot: { - min_age: '0ms', - actions: {}, - }, - warm: { - min_age: '1M', - actions: {}, - }, - cold: { - min_age: '34d', - actions: {}, - }, - }, - }) - ) - ).toEqual({ - total: 'forever', - hot: '30 days', - warm: '4 days', - cold: 'forever', - }); - }); - - test('hot, then always cold', () => { - expect( - absoluteTimingToRelativeTiming( - deserializer({ - name: 'test', - phases: { - hot: { - min_age: '0ms', - actions: {}, - }, - cold: { - min_age: '34d', - actions: {}, - }, - }, - }) - ) - ).toEqual({ total: 'forever', hot: '34 days', warm: undefined, cold: 'forever' }); - }); - }); - - describe('policy that deletes data', () => { - test('hot, then delete', () => { - expect( - absoluteTimingToRelativeTiming( - deserializer({ - name: 'test', - phases: { - hot: { - min_age: '0ms', - actions: {}, - }, - delete: { - min_age: '1M', - actions: {}, - }, - }, - }) - ) - ).toEqual({ - total: '30 days', - hot: '30 days', - warm: undefined, - cold: undefined, - }); - }); - - test('hot, then warm, then delete', () => { - expect( - absoluteTimingToRelativeTiming( - deserializer({ - name: 'test', - phases: { - hot: { - min_age: '0ms', - actions: {}, - }, - warm: { - min_age: '24d', - actions: {}, - }, - delete: { - min_age: '1M', - actions: {}, - }, - }, - }) - ) - ).toEqual({ - total: '30 days', - hot: '24 days', - warm: '6 days', - cold: undefined, - }); - }); - - test('hot, then warm, then cold, then delete', () => { - expect( - absoluteTimingToRelativeTiming( - deserializer({ - name: 'test', - phases: { - hot: { - min_age: '0ms', - actions: {}, - }, - warm: { - min_age: '24d', - actions: {}, - }, - cold: { - min_age: '2M', - actions: {}, - }, - delete: { - min_age: '2d', - actions: {}, - }, - }, - }) - ) - ).toEqual({ - total: '61 days', - hot: '24 days', - warm: '37 days', - cold: 'less than a day', - }); - }); - - test('hot, then cold, then delete', () => { - expect( - absoluteTimingToRelativeTiming( - deserializer({ - name: 'test', - phases: { - hot: { - min_age: '0ms', - actions: {}, - }, - cold: { - min_age: '2M', - actions: {}, - }, - delete: { - min_age: '2d', - actions: {}, - }, - }, - }) - ) - ).toEqual({ - total: '61 days', - hot: '61 days', - warm: undefined, - cold: 'less than a day', - }); - }); - - test('hot, then long warm, then short cold, then delete', () => { - expect( - absoluteTimingToRelativeTiming( - deserializer({ - name: 'test', - phases: { - hot: { - min_age: '0ms', - actions: {}, - }, - warm: { - min_age: '2M', - actions: {}, - }, - cold: { - min_age: '1d', - actions: {}, - }, - delete: { - min_age: '2d', - actions: {}, - }, - }, - }) - ) - ).toEqual({ - total: '61 days', - hot: '61 days', - warm: 'less than a day', - cold: 'less than a day', - }); - }); - }); - }); }); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts index 73ff8c76b9233..2974a88c22343 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/absolute_timing_to_relative_timing.ts @@ -21,8 +21,6 @@ */ import moment from 'moment'; -import { i18n } from '@kbn/i18n'; -import { flow } from 'fp-ts/function'; import { splitSizeAndUnits } from '../../../lib/policies'; @@ -34,21 +32,6 @@ type MinAgePhase = 'warm' | 'cold' | 'delete'; type Phase = 'hot' | MinAgePhase; -const i18nTexts = { - forever: i18n.translate('xpack.indexLifecycleMgmt.relativeTiming.forever', { - defaultMessage: 'forever', - }), - lessThanADay: i18n.translate('xpack.indexLifecycleMgmt.relativeTiming.lessThanADay', { - defaultMessage: 'less than a day', - }), - day: i18n.translate('xpack.indexLifecycleMgmt.relativeTiming.day', { - defaultMessage: 'day', - }), - days: i18n.translate('xpack.indexLifecycleMgmt.relativeTiming.days', { - defaultMessage: 'days', - }), -}; - const phaseOrder: Phase[] = ['hot', 'warm', 'cold', 'delete']; const getMinAge = (phase: MinAgePhase, formData: FormInternal) => ({ @@ -162,38 +145,3 @@ export const calculateRelativeFromAbsoluteMilliseconds = ( }; export type RelativePhaseTimingInMs = ReturnType; - -const millisecondsToDays = (milliseconds?: number): string | undefined => { - if (milliseconds == null) { - return; - } - if (!isFinite(milliseconds)) { - return i18nTexts.forever; - } - const days = milliseconds / 8.64e7; - return days < 1 - ? i18nTexts.lessThanADay - : `${Math.floor(days)} ${days === 1 ? i18nTexts.day : i18nTexts.days}`; -}; - -export const normalizeTimingsToHumanReadable = ({ - total, - phases, -}: PhaseAgeInMilliseconds): { total?: string; hot?: string; warm?: string; cold?: string } => { - return { - total: millisecondsToDays(total), - hot: millisecondsToDays(phases.hot), - warm: millisecondsToDays(phases.warm), - cold: millisecondsToDays(phases.cold), - }; -}; - -/** - * Given {@link FormInternal}, extract the min_age values for each phase and calculate - * human readable strings for communicating how long data will remain in a phase. - */ -export const absoluteTimingToRelativeTiming = flow( - formDataToAbsoluteTimings, - calculateRelativeFromAbsoluteMilliseconds, - normalizeTimingsToHumanReadable -); diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/index.ts b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/index.ts index 396318a1d78cf..af4757a7b7105 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/index.ts +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/lib/index.ts @@ -6,9 +6,7 @@ */ export { - absoluteTimingToRelativeTiming, calculateRelativeFromAbsoluteMilliseconds, - normalizeTimingsToHumanReadable, formDataToAbsoluteTimings, AbsoluteTimings, PhaseAgeInMilliseconds, From 3cb04fc6d080c9158026885c2fab6a6062fe48b3 Mon Sep 17 00:00:00 2001 From: Wylie Conlon Date: Tue, 9 Feb 2021 10:46:07 -0500 Subject: [PATCH 21/28] Expand Tinymath grammar, including named arguments (#89795) * Expand Tinymath grammar, including named arguments * Add tsconfig project * Fix tests * Allow named arguments with numeric types * Remove dashes from named argument validation * Fix license header Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .github/CODEOWNERS | 1 + packages/kbn-tinymath/package.json | 1 + packages/kbn-tinymath/src/grammar.js | 602 +++++++++++------- packages/kbn-tinymath/src/grammar.pegjs | 83 ++- packages/kbn-tinymath/src/index.js | 28 +- packages/kbn-tinymath/test/library.test.js | 192 ++++-- packages/kbn-tinymath/tinymath.d.ts | 45 ++ packages/kbn-tinymath/tsconfig.json | 7 + .../response_processors/series/math.test.js | 4 +- .../functions/common/math.ts | 1 - ...pe.test.js => get_expression_type.test.ts} | 2 +- .../functions/server/get_field_names.test.ts | 1 - .../functions/server/pointseries/index.ts | 15 +- ...ression_type.js => get_expression_type.ts} | 7 +- .../server/pointseries/lib/get_field_names.ts | 22 +- .../pointseries/lib/is_column_reference.ts | 3 +- ...object.test.js => get_form_object.test.ts} | 0 ...{get_form_object.js => get_form_object.ts} | 17 +- 18 files changed, 683 insertions(+), 348 deletions(-) create mode 100644 packages/kbn-tinymath/tinymath.d.ts create mode 100644 packages/kbn-tinymath/tsconfig.json rename x-pack/plugins/canvas/canvas_plugin_src/functions/server/{get_expression_type.test.js => get_expression_type.test.ts} (96%) rename x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/lib/{get_expression_type.js => get_expression_type.ts} (82%) rename x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/{get_form_object.test.js => get_form_object.test.ts} (100%) rename x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/{get_form_object.js => get_form_object.ts} (71%) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 34b449346ddf7..87dc99fa33749 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -26,6 +26,7 @@ /src/plugins/vis_type_xy/ @elastic/kibana-app /src/plugins/visualize/ @elastic/kibana-app /src/plugins/visualizations/ @elastic/kibana-app +/packages/kbn-tinymath/ @elastic/kibana-app # Application Services /examples/bfetch_explorer/ @elastic/kibana-app-services diff --git a/packages/kbn-tinymath/package.json b/packages/kbn-tinymath/package.json index 13b77b1482af9..cc4fa0a64d9c3 100644 --- a/packages/kbn-tinymath/package.json +++ b/packages/kbn-tinymath/package.json @@ -4,6 +4,7 @@ "license": "SSPL-1.0 OR Elastic License 2.0", "private": true, "main": "src/index.js", + "types": "tinymath.d.ts", "scripts": { "kbn:bootstrap": "yarn build", "build": "../../node_modules/.bin/pegjs -o src/grammar.js src/grammar.pegjs" diff --git a/packages/kbn-tinymath/src/grammar.js b/packages/kbn-tinymath/src/grammar.js index 60dfcf4800631..5454143530c39 100644 --- a/packages/kbn-tinymath/src/grammar.js +++ b/packages/kbn-tinymath/src/grammar.js @@ -156,11 +156,21 @@ function peg$parse(input, options) { peg$c12 = function(literal) { return literal; }, - peg$c13 = function(first, rest) { // We can open this up later. Strict for now. - return first + rest.join(''); + peg$c13 = function(chars) { + return { + type: 'variable', + value: chars.join(''), + location: simpleLocation(location()), + text: text() + }; }, - peg$c14 = function(first, mid) { - return first + mid.map(m => m[0].join('') + m[1].join('')).join('') + peg$c14 = function(rest) { + return { + type: 'variable', + value: rest.join(''), + location: simpleLocation(location()), + text: text() + }; }, peg$c15 = "+", peg$c16 = peg$literalExpectation("+", false), @@ -168,8 +178,11 @@ function peg$parse(input, options) { peg$c18 = peg$literalExpectation("-", false), peg$c19 = function(left, rest) { return rest.reduce((acc, curr) => ({ + type: 'function', name: curr[0] === '+' ? 'add' : 'subtract', - args: [acc, curr[1]] + args: [acc, curr[1]], + location: simpleLocation(location()), + text: text() }), left) }, peg$c20 = "*", @@ -178,8 +191,11 @@ function peg$parse(input, options) { peg$c23 = peg$literalExpectation("/", false), peg$c24 = function(left, rest) { return rest.reduce((acc, curr) => ({ + type: 'function', name: curr[0] === '*' ? 'multiply' : 'divide', - args: [acc, curr[1]] + args: [acc, curr[1]], + location: simpleLocation(location()), + text: text() }), left) }, peg$c25 = "(", @@ -196,25 +212,51 @@ function peg$parse(input, options) { peg$c34 = function(first, rest) { return [first].concat(rest); }, - peg$c35 = peg$otherExpectation("function"), - peg$c36 = /^[a-z]/, - peg$c37 = peg$classExpectation([["a", "z"]], false, false), - peg$c38 = function(name, args) { - return {name: name.join(''), args: args || []}; + peg$c35 = /^["]/, + peg$c36 = peg$classExpectation(["\""], false, false), + peg$c37 = function(value) { return value.join(''); }, + peg$c38 = /^[']/, + peg$c39 = peg$classExpectation(["'"], false, false), + peg$c40 = /^[a-zA-Z_]/, + peg$c41 = peg$classExpectation([["a", "z"], ["A", "Z"], "_"], false, false), + peg$c42 = "=", + peg$c43 = peg$literalExpectation("=", false), + peg$c44 = function(name, value) { + return { + type: 'namedArgument', + name: name.join(''), + value: value, + location: simpleLocation(location()), + text: text() + }; + }, + peg$c45 = peg$otherExpectation("function"), + peg$c46 = /^[a-zA-Z_\-]/, + peg$c47 = peg$classExpectation([["a", "z"], ["A", "Z"], "_", "-"], false, false), + peg$c48 = function(name, args) { + return { + type: 'function', + name: name.join(''), + args: args || [], + location: simpleLocation(location()), + text: text() + }; }, - peg$c39 = peg$otherExpectation("number"), - peg$c40 = function() { return parseFloat(text()); }, - peg$c41 = /^[eE]/, - peg$c42 = peg$classExpectation(["e", "E"], false, false), - peg$c43 = peg$otherExpectation("exponent"), - peg$c44 = ".", - peg$c45 = peg$literalExpectation(".", false), - peg$c46 = "0", - peg$c47 = peg$literalExpectation("0", false), - peg$c48 = /^[1-9]/, - peg$c49 = peg$classExpectation([["1", "9"]], false, false), - peg$c50 = /^[0-9]/, - peg$c51 = peg$classExpectation([["0", "9"]], false, false), + peg$c49 = peg$otherExpectation("number"), + peg$c50 = function() { + return parseFloat(text()); + }, + peg$c51 = /^[eE]/, + peg$c52 = peg$classExpectation(["e", "E"], false, false), + peg$c53 = peg$otherExpectation("exponent"), + peg$c54 = ".", + peg$c55 = peg$literalExpectation(".", false), + peg$c56 = "0", + peg$c57 = peg$literalExpectation("0", false), + peg$c58 = /^[1-9]/, + peg$c59 = peg$classExpectation([["1", "9"]], false, false), + peg$c60 = /^[0-9]/, + peg$c61 = peg$classExpectation([["0", "9"]], false, false), peg$currPos = 0, peg$savedPos = 0, @@ -456,10 +498,7 @@ function peg$parse(input, options) { if (s1 !== peg$FAILED) { s2 = peg$parseNumber(); if (s2 === peg$FAILED) { - s2 = peg$parseVariableWithQuote(); - if (s2 === peg$FAILED) { - s2 = peg$parseVariable(); - } + s2 = peg$parseVariable(); } if (s2 !== peg$FAILED) { s3 = peg$parse_(); @@ -489,25 +528,37 @@ function peg$parse(input, options) { } function peg$parseVariable() { - var s0, s1, s2, s3, s4; + var s0, s1, s2, s3, s4, s5; s0 = peg$currPos; s1 = peg$parse_(); if (s1 !== peg$FAILED) { - s2 = peg$parseStartChar(); + s2 = peg$parseQuote(); if (s2 !== peg$FAILED) { s3 = []; s4 = peg$parseValidChar(); + if (s4 === peg$FAILED) { + s4 = peg$parseSpace(); + } while (s4 !== peg$FAILED) { s3.push(s4); s4 = peg$parseValidChar(); + if (s4 === peg$FAILED) { + s4 = peg$parseSpace(); + } } if (s3 !== peg$FAILED) { - s4 = peg$parse_(); + s4 = peg$parseQuote(); if (s4 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c13(s2, s3); - s0 = s1; + s5 = peg$parse_(); + if (s5 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c13(s3); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } } else { peg$currPos = s0; s0 = peg$FAILED; @@ -524,98 +575,26 @@ function peg$parse(input, options) { peg$currPos = s0; s0 = peg$FAILED; } - - return s0; - } - - function peg$parseVariableWithQuote() { - var s0, s1, s2, s3, s4, s5, s6, s7, s8; - - s0 = peg$currPos; - s1 = peg$parse_(); - if (s1 !== peg$FAILED) { - s2 = peg$parseQuote(); - if (s2 !== peg$FAILED) { - s3 = peg$parseStartChar(); + if (s0 === peg$FAILED) { + s0 = peg$currPos; + s1 = peg$parse_(); + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parseValidChar(); if (s3 !== peg$FAILED) { - s4 = []; - s5 = peg$currPos; - s6 = []; - s7 = peg$parseSpace(); - while (s7 !== peg$FAILED) { - s6.push(s7); - s7 = peg$parseSpace(); + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parseValidChar(); } - if (s6 !== peg$FAILED) { - s7 = []; - s8 = peg$parseValidChar(); - if (s8 !== peg$FAILED) { - while (s8 !== peg$FAILED) { - s7.push(s8); - s8 = peg$parseValidChar(); - } - } else { - s7 = peg$FAILED; - } - if (s7 !== peg$FAILED) { - s6 = [s6, s7]; - s5 = s6; - } else { - peg$currPos = s5; - s5 = peg$FAILED; - } - } else { - peg$currPos = s5; - s5 = peg$FAILED; - } - while (s5 !== peg$FAILED) { - s4.push(s5); - s5 = peg$currPos; - s6 = []; - s7 = peg$parseSpace(); - while (s7 !== peg$FAILED) { - s6.push(s7); - s7 = peg$parseSpace(); - } - if (s6 !== peg$FAILED) { - s7 = []; - s8 = peg$parseValidChar(); - if (s8 !== peg$FAILED) { - while (s8 !== peg$FAILED) { - s7.push(s8); - s8 = peg$parseValidChar(); - } - } else { - s7 = peg$FAILED; - } - if (s7 !== peg$FAILED) { - s6 = [s6, s7]; - s5 = s6; - } else { - peg$currPos = s5; - s5 = peg$FAILED; - } - } else { - peg$currPos = s5; - s5 = peg$FAILED; - } - } - if (s4 !== peg$FAILED) { - s5 = peg$parseQuote(); - if (s5 !== peg$FAILED) { - s6 = peg$parse_(); - if (s6 !== peg$FAILED) { - peg$savedPos = s0; - s1 = peg$c14(s3, s4); - s0 = s1; - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } - } else { - peg$currPos = s0; - s0 = peg$FAILED; - } + } else { + s2 = peg$FAILED; + } + if (s2 !== peg$FAILED) { + s3 = peg$parse_(); + if (s3 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c14(s2); + s0 = s1; } else { peg$currPos = s0; s0 = peg$FAILED; @@ -628,9 +607,6 @@ function peg$parse(input, options) { peg$currPos = s0; s0 = peg$FAILED; } - } else { - peg$currPos = s0; - s0 = peg$FAILED; } return s0; @@ -911,105 +887,288 @@ function peg$parse(input, options) { return s0; } - function peg$parseArguments() { - var s0, s1, s2, s3, s4, s5, s6, s7, s8; + function peg$parseArgument_List() { + var s0, s1, s2, s3, s4, s5, s6, s7; peg$silentFails++; s0 = peg$currPos; - s1 = peg$parse_(); + s1 = peg$parseArgument(); if (s1 !== peg$FAILED) { - s2 = peg$parseAddSubtract(); - if (s2 !== peg$FAILED) { - s3 = []; - s4 = peg$currPos; - s5 = peg$parse_(); + s2 = []; + s3 = peg$currPos; + s4 = peg$parse_(); + if (s4 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 44) { + s5 = peg$c31; + peg$currPos++; + } else { + s5 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c32); } + } if (s5 !== peg$FAILED) { - if (input.charCodeAt(peg$currPos) === 44) { - s6 = peg$c31; - peg$currPos++; - } else { - s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c32); } - } + s6 = peg$parse_(); if (s6 !== peg$FAILED) { - s7 = peg$parse_(); + s7 = peg$parseArgument(); if (s7 !== peg$FAILED) { - s8 = peg$parseAddSubtract(); - if (s8 !== peg$FAILED) { - peg$savedPos = s4; - s5 = peg$c33(s2, s8); - s4 = s5; - } else { - peg$currPos = s4; - s4 = peg$FAILED; - } + peg$savedPos = s3; + s4 = peg$c33(s1, s7); + s3 = s4; } else { - peg$currPos = s4; - s4 = peg$FAILED; + peg$currPos = s3; + s3 = peg$FAILED; } } else { - peg$currPos = s4; - s4 = peg$FAILED; + peg$currPos = s3; + s3 = peg$FAILED; } } else { - peg$currPos = s4; - s4 = peg$FAILED; + peg$currPos = s3; + s3 = peg$FAILED; } - while (s4 !== peg$FAILED) { - s3.push(s4); - s4 = peg$currPos; - s5 = peg$parse_(); + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$currPos; + s4 = peg$parse_(); + if (s4 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 44) { + s5 = peg$c31; + peg$currPos++; + } else { + s5 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c32); } + } if (s5 !== peg$FAILED) { - if (input.charCodeAt(peg$currPos) === 44) { - s6 = peg$c31; - peg$currPos++; - } else { - s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c32); } - } + s6 = peg$parse_(); if (s6 !== peg$FAILED) { - s7 = peg$parse_(); + s7 = peg$parseArgument(); if (s7 !== peg$FAILED) { - s8 = peg$parseAddSubtract(); - if (s8 !== peg$FAILED) { - peg$savedPos = s4; - s5 = peg$c33(s2, s8); - s4 = s5; - } else { - peg$currPos = s4; - s4 = peg$FAILED; - } + peg$savedPos = s3; + s4 = peg$c33(s1, s7); + s3 = s4; } else { - peg$currPos = s4; - s4 = peg$FAILED; + peg$currPos = s3; + s3 = peg$FAILED; } } else { - peg$currPos = s4; - s4 = peg$FAILED; + peg$currPos = s3; + s3 = peg$FAILED; } } else { - peg$currPos = s4; + peg$currPos = s3; + s3 = peg$FAILED; + } + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } + if (s2 !== peg$FAILED) { + s3 = peg$parse_(); + if (s3 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 44) { + s4 = peg$c31; + peg$currPos++; + } else { s4 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c32); } + } + if (s4 === peg$FAILED) { + s4 = null; + } + if (s4 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c34(s1, s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + peg$silentFails--; + if (s0 === peg$FAILED) { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c30); } + } + + return s0; + } + + function peg$parseString() { + var s0, s1, s2, s3; + + s0 = peg$currPos; + if (peg$c35.test(input.charAt(peg$currPos))) { + s1 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c36); } + } + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parseValidChar(); + if (s3 !== peg$FAILED) { + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parseValidChar(); + } + } else { + s2 = peg$FAILED; + } + if (s2 !== peg$FAILED) { + if (peg$c35.test(input.charAt(peg$currPos))) { + s3 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c36); } + } + if (s3 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c37(s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (peg$c38.test(input.charAt(peg$currPos))) { + s1 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c39); } + } + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parseValidChar(); + if (s3 !== peg$FAILED) { + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parseValidChar(); + } + } else { + s2 = peg$FAILED; + } + if (s2 !== peg$FAILED) { + if (peg$c38.test(input.charAt(peg$currPos))) { + s3 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c39); } + } + if (s3 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c37(s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + s1 = []; + s2 = peg$parseValidChar(); + if (s2 !== peg$FAILED) { + while (s2 !== peg$FAILED) { + s1.push(s2); + s2 = peg$parseValidChar(); } + } else { + s1 = peg$FAILED; + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c37(s1); + } + s0 = s1; + } + } + + return s0; + } + + function peg$parseArgument() { + var s0, s1, s2, s3, s4, s5, s6; + + s0 = peg$currPos; + s1 = []; + if (peg$c40.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c41); } + } + if (s2 !== peg$FAILED) { + while (s2 !== peg$FAILED) { + s1.push(s2); + if (peg$c40.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c41); } + } + } + } else { + s1 = peg$FAILED; + } + if (s1 !== peg$FAILED) { + s2 = peg$parse_(); + if (s2 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 61) { + s3 = peg$c42; + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c43); } } if (s3 !== peg$FAILED) { s4 = peg$parse_(); if (s4 !== peg$FAILED) { - if (input.charCodeAt(peg$currPos) === 44) { - s5 = peg$c31; - peg$currPos++; - } else { - s5 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c32); } - } + s5 = peg$parseNumber(); if (s5 === peg$FAILED) { - s5 = null; + s5 = peg$parseString(); } if (s5 !== peg$FAILED) { s6 = peg$parse_(); if (s6 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$c34(s2, s3); + s1 = peg$c44(s1, s5); s0 = s1; } else { peg$currPos = s0; @@ -1035,10 +1194,8 @@ function peg$parse(input, options) { peg$currPos = s0; s0 = peg$FAILED; } - peg$silentFails--; if (s0 === peg$FAILED) { - s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c30); } + s0 = peg$parseAddSubtract(); } return s0; @@ -1052,22 +1209,22 @@ function peg$parse(input, options) { s1 = peg$parse_(); if (s1 !== peg$FAILED) { s2 = []; - if (peg$c36.test(input.charAt(peg$currPos))) { + if (peg$c46.test(input.charAt(peg$currPos))) { s3 = input.charAt(peg$currPos); peg$currPos++; } else { s3 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c37); } + if (peg$silentFails === 0) { peg$fail(peg$c47); } } if (s3 !== peg$FAILED) { while (s3 !== peg$FAILED) { s2.push(s3); - if (peg$c36.test(input.charAt(peg$currPos))) { + if (peg$c46.test(input.charAt(peg$currPos))) { s3 = input.charAt(peg$currPos); peg$currPos++; } else { s3 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c37); } + if (peg$silentFails === 0) { peg$fail(peg$c47); } } } } else { @@ -1084,7 +1241,7 @@ function peg$parse(input, options) { if (s3 !== peg$FAILED) { s4 = peg$parse_(); if (s4 !== peg$FAILED) { - s5 = peg$parseArguments(); + s5 = peg$parseArgument_List(); if (s5 === peg$FAILED) { s5 = null; } @@ -1102,7 +1259,7 @@ function peg$parse(input, options) { s8 = peg$parse_(); if (s8 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$c38(s2, s5); + s1 = peg$c48(s2, s5); s0 = s1; } else { peg$currPos = s0; @@ -1139,7 +1296,7 @@ function peg$parse(input, options) { peg$silentFails--; if (s0 === peg$FAILED) { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c35); } + if (peg$silentFails === 0) { peg$fail(peg$c45); } } return s0; @@ -1174,7 +1331,7 @@ function peg$parse(input, options) { } if (s4 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$c40(); + s1 = peg$c50(); s0 = s1; } else { peg$currPos = s0; @@ -1195,7 +1352,7 @@ function peg$parse(input, options) { peg$silentFails--; if (s0 === peg$FAILED) { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c39); } + if (peg$silentFails === 0) { peg$fail(peg$c49); } } return s0; @@ -1204,12 +1361,12 @@ function peg$parse(input, options) { function peg$parseE() { var s0; - if (peg$c41.test(input.charAt(peg$currPos))) { + if (peg$c51.test(input.charAt(peg$currPos))) { s0 = input.charAt(peg$currPos); peg$currPos++; } else { s0 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c42); } + if (peg$silentFails === 0) { peg$fail(peg$c52); } } return s0; @@ -1261,7 +1418,7 @@ function peg$parse(input, options) { peg$silentFails--; if (s0 === peg$FAILED) { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c43); } + if (peg$silentFails === 0) { peg$fail(peg$c53); } } return s0; @@ -1272,11 +1429,11 @@ function peg$parse(input, options) { s0 = peg$currPos; if (input.charCodeAt(peg$currPos) === 46) { - s1 = peg$c44; + s1 = peg$c54; peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c45); } + if (peg$silentFails === 0) { peg$fail(peg$c55); } } if (s1 !== peg$FAILED) { s2 = []; @@ -1308,20 +1465,20 @@ function peg$parse(input, options) { var s0, s1, s2, s3; if (input.charCodeAt(peg$currPos) === 48) { - s0 = peg$c46; + s0 = peg$c56; peg$currPos++; } else { s0 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c47); } + if (peg$silentFails === 0) { peg$fail(peg$c57); } } if (s0 === peg$FAILED) { s0 = peg$currPos; - if (peg$c48.test(input.charAt(peg$currPos))) { + if (peg$c58.test(input.charAt(peg$currPos))) { s1 = input.charAt(peg$currPos); peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c49); } + if (peg$silentFails === 0) { peg$fail(peg$c59); } } if (s1 !== peg$FAILED) { s2 = []; @@ -1349,17 +1506,30 @@ function peg$parse(input, options) { function peg$parseDigit() { var s0; - if (peg$c50.test(input.charAt(peg$currPos))) { + if (peg$c60.test(input.charAt(peg$currPos))) { s0 = input.charAt(peg$currPos); peg$currPos++; } else { s0 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$c51); } + if (peg$silentFails === 0) { peg$fail(peg$c61); } } return s0; } + + function simpleLocation (location) { + // Returns an object representing the position of the function within the expression, + // demarcated by the position of its first character and last character. We calculate these values + // using the offset because the expression could span multiple lines, and we don't want to deal + // with column and line values. + return { + min: location.start.offset, + max: location.end.offset + } + } + + peg$result = peg$startRuleFunction(); if (peg$result !== peg$FAILED && peg$currPos === input.length) { diff --git a/packages/kbn-tinymath/src/grammar.pegjs b/packages/kbn-tinymath/src/grammar.pegjs index cab8e024e60b3..9cb92fa9374a2 100644 --- a/packages/kbn-tinymath/src/grammar.pegjs +++ b/packages/kbn-tinymath/src/grammar.pegjs @@ -1,5 +1,18 @@ // tinymath parsing grammar +{ + function simpleLocation (location) { + // Returns an object representing the position of the function within the expression, + // demarcated by the position of its first character and last character. We calculate these values + // using the offset because the expression could span multiple lines, and we don't want to deal + // with column and line values. + return { + min: location.start.offset, + max: location.end.offset + } + } +} + start = Expression @@ -23,18 +36,28 @@ ValidChar // literals and variables Literal "literal" - = _ literal:(Number / VariableWithQuote / Variable) _ { + = _ literal:(Number / Variable) _ { return literal; } +// Quoted variables are interpreted as strings +// but unquoted variables are more restrictive Variable - = _ first:StartChar rest:ValidChar* _ { // We can open this up later. Strict for now. - return first + rest.join(''); + = _ Quote chars:(ValidChar / Space)* Quote _ { + return { + type: 'variable', + value: chars.join(''), + location: simpleLocation(location()), + text: text() + }; } - -VariableWithQuote - = _ Quote first:StartChar mid:(Space* ValidChar+)* Quote _ { - return first + mid.map(m => m[0].join('') + m[1].join('')).join('') + / _ rest:ValidChar+ _ { + return { + type: 'variable', + value: rest.join(''), + location: simpleLocation(location()), + text: text() + }; } // expressions @@ -45,16 +68,22 @@ Expression AddSubtract = _ left:MultiplyDivide rest:(('+' / '-') MultiplyDivide)* _ { return rest.reduce((acc, curr) => ({ + type: 'function', name: curr[0] === '+' ? 'add' : 'subtract', - args: [acc, curr[1]] + args: [acc, curr[1]], + location: simpleLocation(location()), + text: text() }), left) } MultiplyDivide = _ left:Factor rest:(('*' / '/') Factor)* _ { return rest.reduce((acc, curr) => ({ + type: 'function', name: curr[0] === '*' ? 'multiply' : 'divide', - args: [acc, curr[1]] + args: [acc, curr[1]], + location: simpleLocation(location()), + text: text() }), left) } @@ -68,20 +97,46 @@ Group return expr } -Arguments "arguments" - = _ first:Expression rest:(_ ',' _ arg:Expression {return arg})* _ ','? _ { +Argument_List "arguments" + = first:Argument rest:(_ ',' _ arg:Argument {return arg})* _ ','? { return [first].concat(rest); } +String + = [\"] value:(ValidChar)+ [\"] { return value.join(''); } + / [\'] value:(ValidChar)+ [\'] { return value.join(''); } + / value:(ValidChar)+ { return value.join(''); } + + +Argument + = name:[a-zA-Z_]+ _ '=' _ value:(Number / String) _ { + return { + type: 'namedArgument', + name: name.join(''), + value: value, + location: simpleLocation(location()), + text: text() + }; + } + / arg:Expression + Function "function" - = _ name:[a-z]+ '(' _ args:Arguments? _ ')' _ { - return {name: name.join(''), args: args || []}; + = _ name:[a-zA-Z_-]+ '(' _ args:Argument_List? _ ')' _ { + return { + type: 'function', + name: name.join(''), + args: args || [], + location: simpleLocation(location()), + text: text() + }; } // Numbers. Lol. Number "number" - = '-'? Integer Fraction? Exp? { return parseFloat(text()); } + = '-'? Integer Fraction? Exp? { + return parseFloat(text()); + } E = [eE] diff --git a/packages/kbn-tinymath/src/index.js b/packages/kbn-tinymath/src/index.js index fd4d167bf04dc..4db7df9c57315 100644 --- a/packages/kbn-tinymath/src/index.js +++ b/packages/kbn-tinymath/src/index.js @@ -38,17 +38,22 @@ function interpret(node, scope, injectedFunctions) { return exec(node); function exec(node) { - const type = getType(node); + if (typeof node === 'number') { + return node; + } - if (type === 'function') return invoke(node); + if (node.type === 'function') return invoke(node); - if (type === 'string') { - const val = getValue(scope, node); - if (typeof val === 'undefined') throw new Error(`Unknown variable: ${node}`); + if (node.type === 'variable') { + const val = getValue(scope, node.value); + if (typeof val === 'undefined') throw new Error(`Unknown variable: ${node.value}`); return val; } - return node; // Can only be a number at this point + if (node.type === 'namedArgument') { + // We are ignoring named arguments in the interpreter + throw new Error(`Named arguments are not supported in tinymath itself, at ${node.name}`); + } } function invoke(node) { @@ -67,17 +72,6 @@ function getValue(scope, node) { return typeof val !== 'undefined' ? val : scope[node]; } -function getType(x) { - const type = typeof x; - if (type === 'object') { - const keys = Object.keys(x); - if (keys.length !== 2 || !x.name || !x.args) throw new Error('Invalid AST object'); - return 'function'; - } - if (type === 'string' || type === 'number') return type; - throw new Error(`Unknown AST property type: ${type}`); -} - function isOperable(args) { return args.every((arg) => { if (Array.isArray(arg)) return isOperable(arg); diff --git a/packages/kbn-tinymath/test/library.test.js b/packages/kbn-tinymath/test/library.test.js index 01b4aa3fbf7ae..d11822625b98f 100644 --- a/packages/kbn-tinymath/test/library.test.js +++ b/packages/kbn-tinymath/test/library.test.js @@ -11,7 +11,19 @@ Need tests for spacing, etc */ -const { evaluate, parse } = require('..'); +import { evaluate, parse } from '..'; + +function variableEqual(value) { + return expect.objectContaining({ type: 'variable', value }); +} + +function functionEqual(name, args) { + return expect.objectContaining({ type: 'function', name, args }); +} + +function namedArgumentEqual(name, value) { + return expect.objectContaining({ type: 'namedArgument', name, value }); +} describe('Parser', () => { describe('Numbers', () => { @@ -31,96 +43,144 @@ describe('Parser', () => { describe('Variables', () => { it('strings', () => { - expect(parse('f')).toEqual('f'); - expect(parse('foo')).toEqual('foo'); + expect(parse('f')).toEqual(variableEqual('f')); + expect(parse('foo')).toEqual(variableEqual('foo')); + expect(parse('foo1')).toEqual(variableEqual('foo1')); + expect(() => parse('1foo1')).toThrow('but "f" found'); + }); + + it('strings with spaces', () => { + expect(parse(' foo ')).toEqual(variableEqual('foo')); + expect(() => parse(' foo bar ')).toThrow('but "b" found'); }); it('allowed characters', () => { - expect(parse('_foo')).toEqual('_foo'); - expect(parse('@foo')).toEqual('@foo'); - expect(parse('.foo')).toEqual('.foo'); - expect(parse('-foo')).toEqual('-foo'); - expect(parse('_foo0')).toEqual('_foo0'); - expect(parse('@foo0')).toEqual('@foo0'); - expect(parse('.foo0')).toEqual('.foo0'); - expect(parse('-foo0')).toEqual('-foo0'); + expect(parse('_foo')).toEqual(variableEqual('_foo')); + expect(parse('@foo')).toEqual(variableEqual('@foo')); + expect(parse('.foo')).toEqual(variableEqual('.foo')); + expect(parse('-foo')).toEqual(variableEqual('-foo')); + expect(parse('_foo0')).toEqual(variableEqual('_foo0')); + expect(parse('@foo0')).toEqual(variableEqual('@foo0')); + expect(parse('.foo0')).toEqual(variableEqual('.foo0')); + expect(parse('-foo0')).toEqual(variableEqual('-foo0')); }); }); describe('quoted variables', () => { it('strings with double quotes', () => { - expect(parse('"foo"')).toEqual('foo'); - expect(parse('"f b"')).toEqual('f b'); - expect(parse('"foo bar"')).toEqual('foo bar'); - expect(parse('"foo bar fizz buzz"')).toEqual('foo bar fizz buzz'); - expect(parse('"foo bar baby"')).toEqual('foo bar baby'); + expect(parse('"foo"')).toEqual(variableEqual('foo')); + expect(parse('"f b"')).toEqual(variableEqual('f b')); + expect(parse('"foo bar"')).toEqual(variableEqual('foo bar')); + expect(parse('"foo bar fizz buzz"')).toEqual(variableEqual('foo bar fizz buzz')); + expect(parse('"foo bar baby"')).toEqual(variableEqual('foo bar baby')); }); it('strings with single quotes', () => { /* eslint-disable prettier/prettier */ - expect(parse("'foo'")).toEqual('foo'); - expect(parse("'f b'")).toEqual('f b'); - expect(parse("'foo bar'")).toEqual('foo bar'); - expect(parse("'foo bar fizz buzz'")).toEqual('foo bar fizz buzz'); - expect(parse("'foo bar baby'")).toEqual('foo bar baby'); + expect(parse("'foo'")).toEqual(variableEqual('foo')); + expect(parse("'f b'")).toEqual(variableEqual('f b')); + expect(parse("'foo bar'")).toEqual(variableEqual('foo bar')); + expect(parse("'foo bar fizz buzz'")).toEqual(variableEqual('foo bar fizz buzz')); + expect(parse("'foo bar baby'")).toEqual(variableEqual('foo bar baby')); + expect(parse("' foo bar'")).toEqual(variableEqual(" foo bar")); + expect(parse("'foo bar '")).toEqual(variableEqual("foo bar ")); + expect(parse("'0foo'")).toEqual(variableEqual("0foo")); + expect(parse("' foo bar'")).toEqual(variableEqual(" foo bar")); + expect(parse("'foo bar '")).toEqual(variableEqual("foo bar ")); + expect(parse("'0foo'")).toEqual(variableEqual("0foo")); /* eslint-enable prettier/prettier */ }); it('allowed characters', () => { - expect(parse('"_foo bar"')).toEqual('_foo bar'); - expect(parse('"@foo bar"')).toEqual('@foo bar'); - expect(parse('".foo bar"')).toEqual('.foo bar'); - expect(parse('"-foo bar"')).toEqual('-foo bar'); - expect(parse('"_foo0 bar1"')).toEqual('_foo0 bar1'); - expect(parse('"@foo0 bar1"')).toEqual('@foo0 bar1'); - expect(parse('".foo0 bar1"')).toEqual('.foo0 bar1'); - expect(parse('"-foo0 bar1"')).toEqual('-foo0 bar1'); - }); - - it('invalid characters in double quotes', () => { - const check = (str) => () => parse(str); - expect(check('" foo bar"')).toThrow('but "\\"" found'); - expect(check('"foo bar "')).toThrow('but "\\"" found'); - expect(check('"0foo"')).toThrow('but "\\"" found'); - expect(check('" foo bar"')).toThrow('but "\\"" found'); - expect(check('"foo bar "')).toThrow('but "\\"" found'); - expect(check('"0foo"')).toThrow('but "\\"" found'); - }); - - it('invalid characters in single quotes', () => { - const check = (str) => () => parse(str); - /* eslint-disable prettier/prettier */ - expect(check("' foo bar'")).toThrow('but "\'" found'); - expect(check("'foo bar '")).toThrow('but "\'" found'); - expect(check("'0foo'")).toThrow('but "\'" found'); - expect(check("' foo bar'")).toThrow('but "\'" found'); - expect(check("'foo bar '")).toThrow('but "\'" found'); - expect(check("'0foo'")).toThrow('but "\'" found'); - /* eslint-enable prettier/prettier */ + expect(parse('"_foo bar"')).toEqual(variableEqual('_foo bar')); + expect(parse('"@foo bar"')).toEqual(variableEqual('@foo bar')); + expect(parse('".foo bar"')).toEqual(variableEqual('.foo bar')); + expect(parse('"-foo bar"')).toEqual(variableEqual('-foo bar')); + expect(parse('"_foo0 bar1"')).toEqual(variableEqual('_foo0 bar1')); + expect(parse('"@foo0 bar1"')).toEqual(variableEqual('@foo0 bar1')); + expect(parse('".foo0 bar1"')).toEqual(variableEqual('.foo0 bar1')); + expect(parse('"-foo0 bar1"')).toEqual(variableEqual('-foo0 bar1')); + expect(parse('" foo bar"')).toEqual(variableEqual(' foo bar')); + expect(parse('"foo bar "')).toEqual(variableEqual('foo bar ')); + expect(parse('"0foo"')).toEqual(variableEqual('0foo')); + expect(parse('" foo bar"')).toEqual(variableEqual(' foo bar')); + expect(parse('"foo bar "')).toEqual(variableEqual('foo bar ')); + expect(parse('"0foo"')).toEqual(variableEqual('0foo')); }); }); describe('Functions', () => { it('no arguments', () => { - expect(parse('foo()')).toEqual({ name: 'foo', args: [] }); + expect(parse('foo()')).toEqual(functionEqual('foo', [])); }); it('arguments', () => { - expect(parse('foo(5,10)')).toEqual({ name: 'foo', args: [5, 10] }); + expect(parse('foo(5,10)')).toEqual(functionEqual('foo', [5, 10])); }); it('arguments with strings', () => { - expect(parse('foo("string with spaces")')).toEqual({ - name: 'foo', - args: ['string with spaces'], - }); + expect(parse('foo("string with spaces")')).toEqual( + functionEqual('foo', [variableEqual('string with spaces')]) + ); - /* eslint-disable prettier/prettier */ - expect(parse("foo('string with spaces')")).toEqual({ - name: 'foo', - args: ['string with spaces'], - }); - /* eslint-enable prettier/prettier */ + expect(parse("foo('string with spaces')")).toEqual( + functionEqual('foo', [variableEqual('string with spaces')]) + ); + }); + + it('named only', () => { + expect(parse('foo(q=10)')).toEqual(functionEqual('foo', [namedArgumentEqual('q', 10)])); + }); + + it('named argument is numeric', () => { + expect(parse('foo(q=10.1234e5)')).toEqual( + functionEqual('foo', [namedArgumentEqual('q', 10.1234e5)]) + ); + }); + + it('named and positional', () => { + expect(parse('foo(ref, q="bar")')).toEqual( + functionEqual('foo', [variableEqual('ref'), namedArgumentEqual('q', 'bar')]) + ); + }); + + it('numerically named', () => { + expect(() => parse('foo(1=2)')).toThrow('but "(" found'); + }); + + it('multiple named', () => { + expect(parse('foo(q_param="bar", offset="1d")')).toEqual( + functionEqual('foo', [ + namedArgumentEqual('q_param', 'bar'), + namedArgumentEqual('offset', '1d'), + ]) + ); + }); + + it('multiple named and positional', () => { + expect(parse('foo(q="bar", ref, offset="1d", 100)')).toEqual( + functionEqual('foo', [ + namedArgumentEqual('q', 'bar'), + variableEqual('ref'), + namedArgumentEqual('offset', '1d'), + 100, + ]) + ); + }); + + it('duplicate named', () => { + expect(parse('foo(q="bar", q="test")')).toEqual( + functionEqual('foo', [namedArgumentEqual('q', 'bar'), namedArgumentEqual('q', 'test')]) + ); + }); + + it('incomplete named', () => { + expect(() => parse('foo(a=)')).toThrow('but "(" found'); + expect(() => parse('foo(=a)')).toThrow('but "(" found'); + }); + + it('invalid named', () => { + expect(() => parse('foo(offset-type="1d")')).toThrow('but "(" found'); }); }); @@ -155,7 +215,7 @@ describe('Evaluate', () => { ); }); - it('valiables with dots', () => { + it('variables with dots', () => { expect(evaluate('foo.bar', { 'foo.bar': 20 })).toEqual(20); expect(evaluate('"is.null"', { 'is.null': null })).toEqual(null); expect(evaluate('"is.false"', { 'is.null': null, 'is.false': false })).toEqual(false); @@ -210,6 +270,10 @@ describe('Evaluate', () => { expect(evaluate('sum("space name")', { 'space name': [1, 2, 21] })).toEqual(24); }); + it('throws on named arguments', () => { + expect(() => evaluate('sum(invalid=a)')).toThrow('Named arguments are not supported'); + }); + it('equations with injected functions', () => { expect( evaluate( diff --git a/packages/kbn-tinymath/tinymath.d.ts b/packages/kbn-tinymath/tinymath.d.ts new file mode 100644 index 0000000000000..c3c32a59fa15a --- /dev/null +++ b/packages/kbn-tinymath/tinymath.d.ts @@ -0,0 +1,45 @@ +/* + * 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. + */ + +export function parse(expression: string): TinymathAST; +export function evaluate( + expression: string | null, + context: Record +): number | number[]; + +// Named arguments are not top-level parts of the grammar, but can be nested +export type TinymathAST = number | TinymathVariable | TinymathFunction | TinymathNamedArgument; + +// Zero-indexed location +export interface TinymathLocation { + min: number; + max: number; +} + +export interface TinymathFunction { + type: 'function'; + name: string; + text: string; + args: TinymathAST[]; + location: TinymathLocation; +} + +export interface TinymathVariable { + type: 'variable'; + value: string; + text: string; + location: TinymathLocation; +} + +export interface TinymathNamedArgument { + type: 'namedArgument'; + name: string; + value: string; + text: string; + location: TinymathLocation; +} diff --git a/packages/kbn-tinymath/tsconfig.json b/packages/kbn-tinymath/tsconfig.json new file mode 100644 index 0000000000000..62a7376efdfa6 --- /dev/null +++ b/packages/kbn-tinymath/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "tsBuildInfoFile": "../../build/tsbuildinfo/packages/kbn-tinymath" + }, + "include": ["tinymath.d.ts"] +} diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/math.test.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/math.test.js index 5376abbc1a088..1e30720d6e5b2 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/math.test.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/response_processors/series/math.test.js @@ -134,9 +134,7 @@ describe('math(resp, panel, series)', () => { series )(await mathAgg(resp, panel, series)((results) => results))([]); } catch (e) { - expect(e.message).toEqual( - 'Failed to parse expression. Expected "*", "+", "-", "/", or end of input but "(" found.' - ); + expect(e.message).toEqual('No such function: notExistingFn'); } }); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/math.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/math.ts index 15ca11c902280..af70fa729b7da 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/common/math.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/common/math.ts @@ -5,7 +5,6 @@ * 2.0. */ -// @ts-expect-error no @typed def; Elastic library import { evaluate } from '@kbn/tinymath'; import { pivotObjectArray } from '../../../common/lib/pivot_object_array'; import { Datatable, isDatatable, ExpressionFunctionDefinition } from '../../../types'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/get_expression_type.test.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/get_expression_type.test.ts similarity index 96% rename from x-pack/plugins/canvas/canvas_plugin_src/functions/server/get_expression_type.test.js rename to x-pack/plugins/canvas/canvas_plugin_src/functions/server/get_expression_type.test.ts index 0345f05efa8ff..81b7517686b1c 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/get_expression_type.test.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/get_expression_type.test.ts @@ -11,7 +11,7 @@ import { getExpressionType } from './pointseries/lib/get_expression_type'; describe('getExpressionType', () => { it('returns the result type of an evaluated math expression', () => { expect(getExpressionType(testTable.columns, '2')).toBe('number'); - expect(getExpressionType(testTable.colunns, '2 + 3')).toBe('number'); + expect(getExpressionType(testTable.columns, '2 + 3')).toBe('number'); expect(getExpressionType(testTable.columns, 'name')).toBe('string'); expect(getExpressionType(testTable.columns, 'time')).toBe('date'); expect(getExpressionType(testTable.columns, 'price')).toBe('number'); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/get_field_names.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/get_field_names.test.ts index 136fbf2ac5d13..dc2b85c7393b4 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/get_field_names.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/get_field_names.test.ts @@ -5,7 +5,6 @@ * 2.0. */ -// @ts-expect-error untyped library import { parse } from '@kbn/tinymath'; import { getFieldNames } from './pointseries/lib/get_field_names'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/index.ts index a88a31388eeeb..38438ffb4ad66 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/index.ts @@ -5,7 +5,6 @@ * 2.0. */ -// @ts-expect-error Untyped Elastic library import { evaluate } from '@kbn/tinymath'; import { groupBy, zipObject, omit, uniqBy } from 'lodash'; import moment from 'moment'; @@ -20,7 +19,6 @@ import { import { pivotObjectArray } from '../../../../common/lib/pivot_object_array'; import { unquoteString } from '../../../../common/lib/unquote_string'; import { isColumnReference } from './lib/is_column_reference'; -// @ts-expect-error untyped local import { getExpressionType } from './lib/get_expression_type'; import { getFunctionHelp, getFunctionErrors } from '../../../../i18n'; @@ -132,16 +130,17 @@ export function pointseries(): ExpressionFunctionDefinition< [PRIMARY_KEY]: i, })); - function normalizeValue(expression: string, value: string) { + function normalizeValue(expression: string, value: number | number[], index: number) { + const numberValue = Array.isArray(value) ? value[index] : value; switch (getExpressionType(input.columns, expression)) { case 'string': - return String(value); + return String(numberValue); case 'number': - return Number(value); + return Number(numberValue); case 'date': - return moment(value).valueOf(); + return moment(numberValue).valueOf(); default: - return value; + return numberValue; } } @@ -153,7 +152,7 @@ export function pointseries(): ExpressionFunctionDefinition< (acc: Record, { name, value }) => { try { acc[name] = args[name] - ? normalizeValue(value, evaluate(value, mathScope)[i]) + ? normalizeValue(value, evaluate(value, mathScope), i) : '_all'; } catch (e) { // TODO: handle invalid column names... diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/lib/get_expression_type.js b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/lib/get_expression_type.ts similarity index 82% rename from x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/lib/get_expression_type.js rename to x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/lib/get_expression_type.ts index 5ed10a084e34f..80ac627747318 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/lib/get_expression_type.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/lib/get_expression_type.ts @@ -6,11 +6,12 @@ */ import { parse } from '@kbn/tinymath'; +import { DatatableColumn } from 'src/plugins/expressions/common'; import { getFieldType } from '../../../../../common/lib/get_field_type'; import { isColumnReference } from './is_column_reference'; import { getFieldNames } from './get_field_names'; -export function getExpressionType(columns, mathExpression) { +export function getExpressionType(columns: DatatableColumn[], mathExpression: string) { // if isColumnReference returns true, then mathExpression is just a string // referencing a column in a datatable if (isColumnReference(mathExpression)) { @@ -19,7 +20,7 @@ export function getExpressionType(columns, mathExpression) { const parsedMath = parse(mathExpression); - if (parsedMath.args) { + if (typeof parsedMath !== 'number' && parsedMath.type === 'function') { const fieldNames = parsedMath.args.reduce(getFieldNames, []); if (fieldNames.length > 0) { @@ -30,7 +31,7 @@ export function getExpressionType(columns, mathExpression) { } return types; - }, []); + }, [] as string[]); return fieldTypes.length === 1 ? fieldTypes[0] : 'string'; } diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/lib/get_field_names.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/lib/get_field_names.ts index 5ae27b27c66f2..550705fdddd7f 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/lib/get_field_names.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/lib/get_field_names.ts @@ -5,21 +5,19 @@ * 2.0. */ -type Arg = - | string - | number - | { - name: string; - args: Arg[]; - }; +import { TinymathAST } from '@kbn/tinymath'; -export function getFieldNames(names: string[], arg: Arg): string[] { - if (typeof arg === 'object' && arg.args !== undefined) { - return names.concat(arg.args.reduce(getFieldNames, [])); +export function getFieldNames(names: string[], ast: TinymathAST): string[] { + if (typeof ast === 'number') { + return names; } - if (typeof arg === 'string') { - return names.concat(arg); + if (ast.type === 'function') { + return names.concat(ast.args.reduce(getFieldNames, [])); + } + + if (ast.type === 'variable') { + return names.concat(ast.value); } return names; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/lib/is_column_reference.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/lib/is_column_reference.ts index abcd953a4e123..4b9de8b90cb20 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/lib/is_column_reference.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/server/pointseries/lib/is_column_reference.ts @@ -5,7 +5,6 @@ * 2.0. */ -// @ts-expect-error untyped library import { parse } from '@kbn/tinymath'; export function isColumnReference(mathExpression: string | null): boolean { @@ -13,5 +12,5 @@ export function isColumnReference(mathExpression: string | null): boolean { mathExpression = 'null'; } const parsedMath = parse(mathExpression); - return typeof parsedMath === 'string'; + return typeof parsedMath !== 'number' && parsedMath.type === 'variable'; } diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/get_form_object.test.js b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/get_form_object.test.ts similarity index 100% rename from x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/get_form_object.test.js rename to x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/get_form_object.test.ts diff --git a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/get_form_object.js b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/get_form_object.ts similarity index 71% rename from x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/get_form_object.js rename to x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/get_form_object.ts index 7e7930f39c9bd..015dca39402b5 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/get_form_object.js +++ b/x-pack/plugins/canvas/canvas_plugin_src/uis/arguments/datacolumn/get_form_object.ts @@ -9,7 +9,7 @@ import { parse } from '@kbn/tinymath'; import { unquoteString } from '../../../../common/lib/unquote_string'; // break out into separate function, write unit tests first -export function getFormObject(argValue) { +export function getFormObject(argValue: string) { if (argValue === '') { return { fn: '', @@ -20,23 +20,28 @@ export function getFormObject(argValue) { // check if the value is a math expression, and set its type if it is const mathObj = parse(argValue); // A symbol node is a plain string, so we guess that they're looking for a column. - if (typeof mathObj === 'string') { + if (typeof mathObj === 'number') { + throw new Error(`Cannot render scalar values or complex math expressions`); + } + + if (mathObj.type === 'variable') { return { fn: '', - column: unquoteString(argValue), + column: unquoteString(mathObj.value), }; } // Check if its a simple function, eg a function wrapping a symbol node // check for only one arg of type string if ( - typeof mathObj === 'object' && + mathObj.type === 'function' && mathObj.args.length === 1 && - typeof mathObj.args[0] === 'string' + typeof mathObj.args[0] !== 'number' && + mathObj.args[0].type === 'variable' ) { return { fn: mathObj.name, - column: unquoteString(mathObj.args[0]), + column: unquoteString(mathObj.args[0].value), }; } From 3ad86f13281cb7bbca55123faddae43b15263375 Mon Sep 17 00:00:00 2001 From: Kerry Gallagher Date: Tue, 9 Feb 2021 15:50:00 +0000 Subject: [PATCH 22/28] [Logs UI] Add setup error telemetry for ML functionality (#90298) * Add logs ML setup error telemetry --- .../process_step/process_step.tsx | 2 +- .../log_analysis/api/ml_setup_module_api.ts | 13 ++++++++- .../logs/log_analysis/log_analysis_module.tsx | 27 ++++++++++++++++++- .../log_analysis_module_status.tsx | 4 +-- 4 files changed, 41 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/process_step/process_step.tsx b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/process_step/process_step.tsx index ed26bd5b2077c..987ae87423fda 100644 --- a/x-pack/plugins/infra/public/components/logging/log_analysis_setup/process_step/process_step.tsx +++ b/x-pack/plugins/infra/public/components/logging/log_analysis_setup/process_step/process_step.tsx @@ -75,7 +75,7 @@ export const ProcessStep: React.FunctionComponent = ({ defaultMessage="Something went wrong creating the necessary ML jobs. Please ensure all selected log indices exist." /> - {errorMessages.map((errorMessage, i) => ( + {setupStatus.reasons.map((errorMessage, i) => ( {errorMessage} diff --git a/x-pack/plugins/infra/public/containers/logs/log_analysis/api/ml_setup_module_api.ts b/x-pack/plugins/infra/public/containers/logs/log_analysis/api/ml_setup_module_api.ts index 69846e1f51482..ea1567d6056f1 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_analysis/api/ml_setup_module_api.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_analysis/api/ml_setup_module_api.ts @@ -91,8 +91,19 @@ const setupMlModuleRequestPayloadRT = rt.intersection([ setupMlModuleRequestParamsRT, ]); +const setupErrorRT = rt.type({ + reason: rt.string, + type: rt.string, +}); + const setupErrorResponseRT = rt.type({ - msg: rt.string, + status: rt.number, + error: rt.intersection([ + setupErrorRT, + rt.type({ + root_cause: rt.array(setupErrorRT), + }), + ]), }); const datafeedSetupResponseRT = rt.intersection([ diff --git a/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module.tsx b/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module.tsx index 72b74d5f99719..00a6c3c2a72fb 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module.tsx +++ b/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module.tsx @@ -11,6 +11,7 @@ import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; import { useTrackedPromise } from '../../../utils/use_tracked_promise'; import { useModuleStatus } from './log_analysis_module_status'; import { ModuleDescriptor, ModuleSourceConfiguration } from './log_analysis_module_types'; +import { useUiTracker } from '../../../../../observability/public'; export const useLogAnalysisModule = ({ sourceConfiguration, @@ -23,6 +24,8 @@ export const useLogAnalysisModule = ({ const { spaceId, sourceId, timestampField } = sourceConfiguration; const [moduleStatus, dispatchModuleStatus] = useModuleStatus(moduleDescriptor.jobTypes); + const trackMetric = useUiTracker({ app: 'infra_logs' }); + const [, fetchJobStatus] = useTrackedPromise( { cancelPreviousOn: 'resolution', @@ -75,6 +78,25 @@ export const useLogAnalysisModule = ({ return { setupResult, jobSummaries }; }, onResolve: ({ setupResult: { datafeeds, jobs }, jobSummaries }) => { + // Track failures + if ( + [...datafeeds, ...jobs] + .reduce((acc, resource) => [...acc, ...Object.keys(resource)], []) + .some((key) => key === 'error') + ) { + const reasons = [...datafeeds, ...jobs] + .filter((resource) => resource.error !== undefined) + .map((resource) => resource.error?.error?.reason ?? ''); + // NOTE: Lack of indices and a missing field mapping have the same error + if ( + reasons.filter((reason) => reason.includes('because it has no mappings')).length > 0 + ) { + trackMetric({ metric: 'logs_ml_setup_error_bad_indices_or_mappings' }); + } else { + trackMetric({ metric: 'logs_ml_setup_error_unknown_cause' }); + } + } + dispatchModuleStatus({ type: 'finishedSetup', datafeedSetupResults: datafeeds, @@ -84,8 +106,11 @@ export const useLogAnalysisModule = ({ sourceId, }); }, - onReject: () => { + onReject: (e: any) => { dispatchModuleStatus({ type: 'failedSetup' }); + if (e?.body?.statusCode === 403) { + trackMetric({ metric: 'logs_ml_setup_error_lack_of_privileges' }); + } }, }, [moduleDescriptor.setUpModule, spaceId, sourceId, timestampField] diff --git a/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_status.tsx b/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_status.tsx index 1fec67228aa22..c3117c9326d1e 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_status.tsx +++ b/x-pack/plugins/infra/public/containers/logs/log_analysis/log_analysis_module_status.tsx @@ -105,10 +105,10 @@ const createStatusReducer = (jobTypes: JobType[]) => ( reasons: [ ...Object.values(datafeedSetupResults) .filter(hasError) - .map((datafeed) => datafeed.error.msg), + .map((datafeed) => datafeed.error.error?.reason), ...Object.values(jobSetupResults) .filter(hasError) - .map((job) => job.error.msg), + .map((job) => job.error.error?.reason), ], }; From d43547af82426f113c29867a7716fe895c318324 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 9 Feb 2021 07:53:00 -0800 Subject: [PATCH 23/28] skip flaky suite (#86546) --- .../functional/apps/upgrade_assistant/upgrade_assistant.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/upgrade_assistant/upgrade_assistant.ts b/x-pack/test/functional/apps/upgrade_assistant/upgrade_assistant.ts index 711c9b7683678..93955fb741044 100644 --- a/x-pack/test/functional/apps/upgrade_assistant/upgrade_assistant.ts +++ b/x-pack/test/functional/apps/upgrade_assistant/upgrade_assistant.ts @@ -18,7 +18,8 @@ export default function upgradeAssistantFunctionalTests({ const log = getService('log'); const retry = getService('retry'); - describe('Upgrade Checkup', function () { + // Failing: See https://github.com/elastic/kibana/issues/86546 + describe.skip('Upgrade Checkup', function () { this.tags('includeFirefox'); before(async () => { From 4b3d1bf83c07d0cc1618b26729b8008778a97cb0 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Tue, 9 Feb 2021 10:01:40 -0700 Subject: [PATCH 24/28] [Tabify] Add meta option to include top-level underscored field values (#90535) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../__snapshots__/tabify_docs.test.ts.snap | 284 ++++++++++++++++++ .../common/search/tabify/tabify_docs.test.ts | 9 + .../data/common/search/tabify/tabify_docs.ts | 17 +- 3 files changed, 305 insertions(+), 5 deletions(-) diff --git a/src/plugins/data/common/search/tabify/__snapshots__/tabify_docs.test.ts.snap b/src/plugins/data/common/search/tabify/__snapshots__/tabify_docs.test.ts.snap index 6cc191a67633c..22276335a0599 100644 --- a/src/plugins/data/common/search/tabify/__snapshots__/tabify_docs.test.ts.snap +++ b/src/plugins/data/common/search/tabify/__snapshots__/tabify_docs.test.ts.snap @@ -1,5 +1,113 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`tabifyDocs combines meta fields if meta option is set 1`] = ` +Object { + "columns": Array [ + Object { + "id": "fieldTest", + "meta": Object { + "field": "fieldTest", + "index": "test-index", + "params": Object { + "id": "number", + }, + "type": "number", + }, + "name": "fieldTest", + }, + Object { + "id": "invalidMapping", + "meta": Object { + "field": "invalidMapping", + "index": "test-index", + "params": undefined, + "type": "number", + }, + "name": "invalidMapping", + }, + Object { + "id": "nested", + "meta": Object { + "field": "nested", + "index": "test-index", + "params": undefined, + "type": "object", + }, + "name": "nested", + }, + Object { + "id": "sourceTest", + "meta": Object { + "field": "sourceTest", + "index": "test-index", + "params": Object { + "id": "number", + }, + "type": "number", + }, + "name": "sourceTest", + }, + Object { + "id": "_id", + "meta": Object { + "field": "_id", + "index": "test-index", + "params": undefined, + "type": "string", + }, + "name": "_id", + }, + Object { + "id": "_index", + "meta": Object { + "field": "_index", + "index": "test-index", + "params": undefined, + "type": "string", + }, + "name": "_index", + }, + Object { + "id": "_score", + "meta": Object { + "field": "_score", + "index": "test-index", + "params": undefined, + "type": "number", + }, + "name": "_score", + }, + Object { + "id": "_type", + "meta": Object { + "field": "_type", + "index": "test-index", + "params": undefined, + "type": "string", + }, + "name": "_type", + }, + ], + "rows": Array [ + Object { + "_id": "hit-id-value", + "_index": "hit-index-value", + "_score": 77, + "_type": "hit-type-value", + "fieldTest": 123, + "invalidMapping": 345, + "nested": Array [ + Object { + "field": 123, + }, + ], + "sourceTest": 123, + }, + ], + "type": "datatable", +} +`; + exports[`tabifyDocs converts fields by default 1`] = ` Object { "columns": Array [ @@ -47,9 +155,53 @@ Object { }, "name": "sourceTest", }, + Object { + "id": "_id", + "meta": Object { + "field": "_id", + "index": "test-index", + "params": undefined, + "type": "string", + }, + "name": "_id", + }, + Object { + "id": "_index", + "meta": Object { + "field": "_index", + "index": "test-index", + "params": undefined, + "type": "string", + }, + "name": "_index", + }, + Object { + "id": "_score", + "meta": Object { + "field": "_score", + "index": "test-index", + "params": undefined, + "type": "number", + }, + "name": "_score", + }, + Object { + "id": "_type", + "meta": Object { + "field": "_type", + "index": "test-index", + "params": undefined, + "type": "string", + }, + "name": "_type", + }, ], "rows": Array [ Object { + "_id": "hit-id-value", + "_index": "hit-index-value", + "_score": 77, + "_type": "hit-type-value", "fieldTest": 123, "invalidMapping": 345, "nested": Array [ @@ -111,9 +263,53 @@ Object { }, "name": "sourceTest", }, + Object { + "id": "_id", + "meta": Object { + "field": "_id", + "index": "test-index", + "params": undefined, + "type": "string", + }, + "name": "_id", + }, + Object { + "id": "_index", + "meta": Object { + "field": "_index", + "index": "test-index", + "params": undefined, + "type": "string", + }, + "name": "_index", + }, + Object { + "id": "_score", + "meta": Object { + "field": "_score", + "index": "test-index", + "params": undefined, + "type": "number", + }, + "name": "_score", + }, + Object { + "id": "_type", + "meta": Object { + "field": "_type", + "index": "test-index", + "params": undefined, + "type": "string", + }, + "name": "_type", + }, ], "rows": Array [ Object { + "_id": "hit-id-value", + "_index": "hit-index-value", + "_score": 77, + "_type": "hit-type-value", "fieldTest": 123, "invalidMapping": 345, "nested": Array [ @@ -175,9 +371,53 @@ Object { }, "name": "sourceTest", }, + Object { + "id": "_id", + "meta": Object { + "field": "_id", + "index": "test-index", + "params": undefined, + "type": "string", + }, + "name": "_id", + }, + Object { + "id": "_index", + "meta": Object { + "field": "_index", + "index": "test-index", + "params": undefined, + "type": "string", + }, + "name": "_index", + }, + Object { + "id": "_score", + "meta": Object { + "field": "_score", + "index": "test-index", + "params": undefined, + "type": "number", + }, + "name": "_score", + }, + Object { + "id": "_type", + "meta": Object { + "field": "_type", + "index": "test-index", + "params": undefined, + "type": "string", + }, + "name": "_type", + }, ], "rows": Array [ Object { + "_id": "hit-id-value", + "_index": "hit-index-value", + "_score": 77, + "_type": "hit-type-value", "fieldTest": 123, "invalidMapping": 345, "nested": Array [ @@ -235,9 +475,53 @@ Object { }, "name": "sourceTest", }, + Object { + "id": "_id", + "meta": Object { + "field": "_id", + "index": undefined, + "params": undefined, + "type": "string", + }, + "name": "_id", + }, + Object { + "id": "_index", + "meta": Object { + "field": "_index", + "index": undefined, + "params": undefined, + "type": "string", + }, + "name": "_index", + }, + Object { + "id": "_score", + "meta": Object { + "field": "_score", + "index": undefined, + "params": undefined, + "type": "number", + }, + "name": "_score", + }, + Object { + "id": "_type", + "meta": Object { + "field": "_type", + "index": undefined, + "params": undefined, + "type": "string", + }, + "name": "_type", + }, ], "rows": Array [ Object { + "_id": "hit-id-value", + "_index": "hit-index-value", + "_score": 77, + "_type": "hit-type-value", "fieldTest": 123, "invalidMapping": 345, "nested": Array [ diff --git a/src/plugins/data/common/search/tabify/tabify_docs.test.ts b/src/plugins/data/common/search/tabify/tabify_docs.test.ts index c81e39f4c156a..52e12aeee1ae6 100644 --- a/src/plugins/data/common/search/tabify/tabify_docs.test.ts +++ b/src/plugins/data/common/search/tabify/tabify_docs.test.ts @@ -37,6 +37,10 @@ describe('tabifyDocs', () => { hits: { hits: [ { + _id: 'hit-id-value', + _index: 'hit-index-value', + _type: 'hit-type-value', + _score: 77, _source: { sourceTest: 123 }, fields: { fieldTest: 123, invalidMapping: 345, nested: [{ field: 123 }] }, }, @@ -59,6 +63,11 @@ describe('tabifyDocs', () => { expect(table).toMatchSnapshot(); }); + it('combines meta fields if meta option is set', () => { + const table = tabifyDocs(response, index, { meta: true }); + expect(table).toMatchSnapshot(); + }); + it('works without provided index pattern', () => { const table = tabifyDocs(response); expect(table).toMatchSnapshot(); diff --git a/src/plugins/data/common/search/tabify/tabify_docs.ts b/src/plugins/data/common/search/tabify/tabify_docs.ts index eaf43d9fd6ff6..b4806283e63f2 100644 --- a/src/plugins/data/common/search/tabify/tabify_docs.ts +++ b/src/plugins/data/common/search/tabify/tabify_docs.ts @@ -11,6 +11,12 @@ import { isPlainObject } from 'lodash'; import { IndexPattern } from '../../index_patterns/index_patterns'; import { Datatable, DatatableColumn, DatatableColumnType } from '../../../../expressions/common'; +export interface TabifyDocsOptions { + shallow?: boolean; + source?: boolean; + meta?: boolean; +} + export function flattenHit( hit: SearchResponse['hits']['hits'][0], indexPattern?: IndexPattern, @@ -56,12 +62,13 @@ export function flattenHit( if (params?.source !== false && hit._source) { flatten(hit._source as Record); } - return flat; -} + if (params?.meta !== false) { + // combine the fields that Discover allows to add as columns + const { _id, _index, _type, _score } = hit; + flatten({ _id, _index, _score, _type }); + } -export interface TabifyDocsOptions { - shallow?: boolean; - source?: boolean; + return flat; } export const tabifyDocs = ( From f3399620cf558efe1e3a862e32289839f88ed815 Mon Sep 17 00:00:00 2001 From: Constance Date: Tue, 9 Feb 2021 09:33:39 -0800 Subject: [PATCH 25/28] [Enterprise Search] Add eslint import/order rules (#90530) * Add import rules - newlines between each group - mocks in test files before everything else - React before all other externals * Run --fix on public/applications/workplace_search * Manually fix errors still present in WS public files - these appear to be mostly due to jest.mock() or const mixed in with imports, which confuses the autofixer * Run --fix on public/applications/app_search + some manual fixes/tweaks along the way * Run --fix on public/applications/shared - some opinionated changes, particularly around IFlashMessages and grouping together types coming from Kibana (src->../../src) * Run --fix on public/applications/enterprise_search - mostly straightforward * Run --fix on public/applications/__mocks__ * Fix remaining top-level public files - Some opinionated changes (src/core -> ../../src/core) to keep types/mocks together * Run --fix on server/ files - same opinionated src->../src changes to keep deps grouped together - opinionated require->import fetch change in enterprise_esarch_config_api.test.ts - opinionated [] inclusion of builtins & external imports together (mostly for enterprise_search_request_handler.ts) --- .eslintrc.js | 33 +++++++++++++++++++ .../public/applications/__mocks__/kea.mock.ts | 4 +-- .../__mocks__/kibana_logic.mock.ts | 1 + .../__mocks__/mount_async.mock.tsx | 3 +- .../__mocks__/mount_with_i18n.mock.tsx | 2 ++ .../__mocks__/shallow_with_i18n.mock.tsx | 1 + .../applications/app_search/app_logic.test.ts | 2 +- .../applications/app_search/app_logic.ts | 1 + .../analytics/analytics_layout.test.tsx | 5 +-- .../components/analytics/analytics_layout.tsx | 7 ++-- .../analytics/analytics_logic.test.ts | 1 + .../components/analytics/analytics_logic.ts | 4 +-- .../analytics/analytics_router.test.tsx | 4 ++- .../components/analytics_cards.test.tsx | 2 ++ .../analytics/components/analytics_cards.tsx | 1 + .../components/analytics_chart.test.tsx | 2 ++ .../analytics/components/analytics_chart.tsx | 2 ++ .../components/analytics_header.test.tsx | 3 ++ .../analytics/components/analytics_header.tsx | 8 ++--- .../components/analytics_search.test.tsx | 2 ++ .../analytics/components/analytics_search.tsx | 3 +- .../components/analytics_section.test.tsx | 1 + .../analytics_tables/analytics_table.test.tsx | 1 + .../analytics_tables/analytics_table.tsx | 4 ++- .../inline_tags_list.test.tsx | 2 ++ .../analytics_tables/inline_tags_list.tsx | 3 +- .../query_clicks_table.test.tsx | 1 + .../analytics_tables/query_clicks_table.tsx | 5 +-- .../recent_queries_table.test.tsx | 1 + .../analytics_tables/recent_queries_table.tsx | 3 +- .../analytics_tables/shared_columns.tsx | 5 +-- .../components/analytics_unavailable.test.tsx | 2 ++ .../components/analytics_unavailable.tsx | 3 +- .../components/analytics/constants.ts | 1 + .../app_search/components/analytics/utils.ts | 5 +-- .../analytics/views/analytics.test.tsx | 2 ++ .../components/analytics/views/analytics.tsx | 9 ++--- .../analytics/views/query_detail.test.tsx | 2 ++ .../analytics/views/query_detail.tsx | 5 +-- .../analytics/views/recent_queries.test.tsx | 2 ++ .../analytics/views/recent_queries.tsx | 5 +-- .../analytics/views/top_queries.test.tsx | 2 ++ .../analytics/views/top_queries.tsx | 5 +-- .../views/top_queries_no_clicks.test.tsx | 2 ++ .../analytics/views/top_queries_no_clicks.tsx | 5 +-- .../views/top_queries_no_results.test.tsx | 2 ++ .../views/top_queries_no_results.tsx | 5 +-- .../views/top_queries_with_clicks.test.tsx | 2 ++ .../views/top_queries_with_clicks.tsx | 5 +-- .../components/credentials/constants.ts | 1 + .../credentials/credentials.test.tsx | 5 ++- .../components/credentials/credentials.tsx | 9 ++--- .../credentials_flyout/body.test.tsx | 4 ++- .../credentials/credentials_flyout/body.tsx | 4 ++- .../credentials_flyout/footer.test.tsx | 2 ++ .../credentials/credentials_flyout/footer.tsx | 2 ++ .../key_engine_access.test.tsx | 2 ++ .../form_components/key_engine_access.tsx | 2 ++ .../form_components/key_name.test.tsx | 2 ++ .../form_components/key_name.tsx | 2 ++ .../key_read_write_access.test.tsx | 2 ++ .../form_components/key_read_write_access.tsx | 2 ++ .../form_components/key_type.test.tsx | 3 ++ .../form_components/key_type.tsx | 4 ++- .../key_update_warning.test.tsx | 2 ++ .../form_components/key_update_warning.tsx | 1 + .../credentials_flyout/header.test.tsx | 2 ++ .../credentials/credentials_flyout/header.tsx | 4 ++- .../credentials_flyout/index.test.tsx | 2 ++ .../credentials/credentials_flyout/index.tsx | 7 ++-- .../credentials_list.test.tsx | 9 +++-- .../credentials_list/credentials_list.tsx | 14 ++++---- .../credentials/credentials_list/key.test.tsx | 2 ++ .../credentials/credentials_list/key.tsx | 1 + .../credentials/credentials_logic.test.ts | 1 + .../credentials/credentials_logic.ts | 12 +++---- .../components/credentials/types.ts | 1 + .../credentials/utils/api_token_sort.test.ts | 3 +- .../utils/get_engines_display_text.test.tsx | 22 +++++++------ .../utils/get_engines_display_text.tsx | 1 + .../api_code_example.test.tsx | 2 ++ .../api_code_example.tsx | 13 ++++---- .../paste_json_text.test.tsx | 3 ++ .../paste_json_text.tsx | 5 +-- .../show_creation_modes.test.tsx | 3 ++ .../show_creation_modes.tsx | 5 +-- .../upload_json_file.test.tsx | 3 ++ .../upload_json_file.tsx | 5 +-- .../errors.test.tsx | 2 ++ .../creation_response_components/errors.tsx | 3 +- .../summary.test.tsx | 6 +++- .../creation_response_components/summary.tsx | 5 +-- .../summary_documents.test.tsx | 2 ++ .../summary_documents.tsx | 2 +- .../summary_section.test.tsx | 2 ++ .../summary_sections.test.tsx | 4 ++- .../summary_sections.tsx | 5 +-- .../document_creation_buttons.test.tsx | 3 ++ .../document_creation_buttons.tsx | 5 +-- .../document_creation_flyout.test.tsx | 5 +-- .../document_creation_flyout.tsx | 7 ++-- .../document_creation_logic.test.ts | 12 ++++--- .../document_creation_logic.ts | 2 +- .../document_creation_button.test.tsx | 3 ++ .../documents/document_creation_button.tsx | 3 +- .../documents/document_detail.test.tsx | 7 ++-- .../components/documents/document_detail.tsx | 9 ++--- .../documents/document_detail_logic.test.ts | 3 +- .../documents/document_detail_logic.ts | 3 +- .../components/documents/documents.test.tsx | 2 ++ .../components/documents/documents.tsx | 13 +++++--- .../build_search_ui_config.ts | 1 + .../search_experience/build_sort_options.ts | 2 +- .../customization_callout.test.tsx | 1 + .../customization_callout.tsx | 2 +- .../customization_modal.test.tsx | 2 ++ .../search_experience/customization_modal.tsx | 3 +- .../search_experience/hooks.test.tsx | 3 +- .../search_experience/pagination.test.tsx | 1 + .../search_experience/pagination.tsx | 1 + .../search_experience.test.tsx | 15 +++++---- .../search_experience/search_experience.tsx | 13 ++++---- .../search_experience_content.test.tsx | 8 +++-- .../search_experience_content.tsx | 16 +++++---- .../views/paging_view.test.tsx | 1 + .../views/result_view.test.tsx | 3 +- .../search_experience/views/result_view.tsx | 2 +- .../views/results_per_page_view.test.tsx | 1 + .../views/results_per_page_view.tsx | 2 +- .../views/search_box_view.test.tsx | 1 + .../views/sorting_view.test.tsx | 1 + .../search_experience/views/sorting_view.tsx | 2 +- .../components/engine/engine_logic.ts | 1 + .../components/engine/engine_nav.test.tsx | 2 ++ .../components/engine/engine_nav.tsx | 20 ++++++----- .../components/engine/engine_router.test.tsx | 7 ++-- .../components/engine/engine_router.tsx | 12 +++---- .../app_search/components/engine/types.ts | 2 +- .../components/recent_api_logs.test.tsx | 1 + .../components/recent_api_logs.tsx | 2 +- .../components/total_charts.test.tsx | 1 + .../components/total_charts.tsx | 7 ++-- .../components/total_stats.test.tsx | 2 ++ .../components/total_stats.tsx | 6 ++-- .../components/unavailable_prompt.test.tsx | 2 ++ .../components/unavailable_prompt.tsx | 2 +- .../engine_overview/engine_overview.test.tsx | 3 ++ .../engine_overview/engine_overview.tsx | 7 ++-- .../engine_overview_empty.test.tsx | 3 ++ .../engine_overview/engine_overview_empty.tsx | 2 +- .../engine_overview_metrics.test.tsx | 1 + .../engine_overview_metrics.tsx | 7 ++-- .../components/engines/assets/icons.test.tsx | 1 + .../engines/components/empty_state.test.tsx | 2 ++ .../engines/components/empty_state.tsx | 4 ++- .../engines/components/header.test.tsx | 1 + .../components/engines/components/header.tsx | 4 ++- .../engines/components/loading_state.test.tsx | 2 ++ .../engines/components/loading_state.tsx | 2 ++ .../components/engines/engines_logic.test.ts | 1 + .../engines/engines_overview.test.tsx | 1 + .../components/engines/engines_overview.tsx | 10 +++--- .../components/engines/engines_table.test.tsx | 3 ++ .../components/engines/engines_table.tsx | 13 ++++---- .../error_connecting.test.tsx | 2 ++ .../error_connecting/error_connecting.tsx | 1 + .../app_search/components/library/library.tsx | 3 +- .../components/log_retention_callout.test.tsx | 6 ++-- .../components/log_retention_callout.tsx | 5 +-- .../components/log_retention_tooltip.test.tsx | 3 ++ .../components/log_retention_tooltip.tsx | 3 +- .../log_retention/log_retention_logic.test.ts | 3 +- .../log_retention/log_retention_logic.ts | 2 +- .../log_retention/messaging/constants.tsx | 3 +- .../messaging/log_retention_message.test.tsx | 5 +-- .../messaging/log_retention_message.tsx | 1 + .../relevance_tuning/relevance_tuning.tsx | 3 +- .../relevance_tuning_logic.test.ts | 2 +- .../components/result/result.test.tsx | 6 ++-- .../app_search/components/result/result.tsx | 8 +++-- .../components/result/result_field.test.tsx | 1 + .../components/result/result_field.tsx | 4 ++- .../result/result_field_value.test.tsx | 1 + .../components/result/result_header.test.tsx | 1 + .../result/result_header_item.test.tsx | 1 + .../generic_confirmation_modal.test.tsx | 1 + .../generic_confirmation_modal.tsx | 2 +- .../log_retention_confirmation_modal.test.tsx | 2 ++ .../log_retention_confirmation_modal.tsx | 6 ++-- .../log_retention_panel.test.tsx | 2 ++ .../log_retention/log_retention_panel.tsx | 5 +-- .../components/settings/settings.test.tsx | 2 ++ .../components/settings/settings.tsx | 3 +- .../setup_guide/setup_guide.test.tsx | 2 ++ .../components/setup_guide/setup_guide.tsx | 6 ++-- .../applications/app_search/index.test.tsx | 9 +++-- .../public/applications/app_search/index.tsx | 29 ++++++++-------- .../error_connecting.test.tsx | 2 ++ .../error_connecting/error_connecting.tsx | 3 +- .../product_card/product_card.test.tsx | 4 ++- .../components/product_card/product_card.tsx | 6 ++-- .../product_selector.test.tsx | 4 ++- .../product_selector/product_selector.tsx | 7 ++-- .../setup_guide/setup_guide.test.tsx | 2 ++ .../components/setup_guide/setup_guide.tsx | 6 ++-- .../setup_guide/setup_guide_cta.test.tsx | 1 + .../setup_guide/setup_guide_cta.tsx | 4 ++- .../enterprise_search/index.test.tsx | 10 +++--- .../applications/enterprise_search/index.tsx | 7 ++-- .../public/applications/index.test.tsx | 12 ++++--- .../public/applications/index.tsx | 16 +++++---- .../error_state/error_state_prompt.test.tsx | 2 ++ .../shared/error_state/error_state_prompt.tsx | 4 ++- .../flash_messages/flash_messages.test.tsx | 2 ++ .../shared/flash_messages/flash_messages.tsx | 2 ++ .../flash_messages_logic.test.ts | 6 ++-- .../flash_messages/flash_messages_logic.ts | 7 +--- .../flash_messages/handle_api_errors.test.ts | 1 + .../flash_messages/handle_api_errors.ts | 3 +- .../shared/flash_messages/index.ts | 3 +- .../shared/flash_messages/types.ts | 14 ++++++++ .../shared/hidden_text/hidden_text.test.tsx | 1 + .../shared/hidden_text/hidden_text.tsx | 1 + .../indexing_status/indexing_status.test.tsx | 3 +- .../indexing_status/indexing_status.tsx | 4 +-- .../indexing_status_content.test.tsx | 1 + .../indexing_status_errors.test.tsx | 1 + .../indexing_status/indexing_status_logic.ts | 2 +- .../shared/kibana/kibana_logic.test.ts | 4 +-- .../shared/kibana/kibana_logic.ts | 9 ++--- .../kibana_chrome/generate_breadcrumbs.ts | 6 ++-- .../shared/kibana_chrome/set_chrome.test.tsx | 1 + .../shared/kibana_chrome/set_chrome.tsx | 1 + .../shared/layout/layout.test.tsx | 2 ++ .../applications/shared/layout/layout.tsx | 1 + .../shared/layout/side_nav.test.tsx | 4 ++- .../applications/shared/layout/side_nav.tsx | 5 +-- .../shared/loading/loading.test.tsx | 1 + .../applications/shared/loading/loading.tsx | 1 + .../shared/not_found/not_found.test.tsx | 2 ++ .../shared/not_found/not_found.tsx | 10 +++--- .../react_router_helpers/create_href.test.ts | 3 +- .../react_router_helpers/create_href.ts | 1 + .../eui_components.test.tsx | 6 ++-- .../react_router_helpers/eui_components.tsx | 5 ++- .../schema/schema_add_field_modal.test.tsx | 5 +-- .../schema/schema_errors_accordion.test.tsx | 2 ++ .../schema/schema_existing_field.test.tsx | 1 + .../setup_guide/cloud/instructions.test.tsx | 6 ++-- .../shared/setup_guide/cloud/instructions.tsx | 5 +-- .../shared/setup_guide/instructions.test.tsx | 6 ++-- .../shared/setup_guide/instructions.tsx | 5 +-- .../shared/setup_guide/setup_guide.test.tsx | 4 ++- .../shared/setup_guide/setup_guide.tsx | 3 +- .../shared/table_header/table_header.test.tsx | 2 ++ .../shared/telemetry/send_telemetry.test.tsx | 1 + .../shared/telemetry/send_telemetry.tsx | 1 + .../shared/telemetry/telemetry_logic.test.ts | 3 +- .../shared/truncate/truncate.test.tsx | 1 + .../__mocks__/content_sources.mock.ts | 5 +-- .../workplace_search/app_logic.test.ts | 2 +- .../layout/kibana_header_actions.test.tsx | 6 ++-- .../layout/kibana_header_actions.tsx | 2 +- .../components/layout/nav.test.tsx | 2 ++ .../components/layout/nav.tsx | 2 -- .../shared/api_key/api_key.test.tsx | 1 + .../component_loader.test.tsx | 1 + .../content_section/content_section.test.tsx | 5 ++- .../content_section/content_section.tsx | 1 - .../credential_item/credential_item.test.tsx | 1 + .../license_badge/license_badge.test.tsx | 1 + .../license_callout/license_callout.test.tsx | 1 + .../product_button/product_button.test.tsx | 2 ++ .../shared/product_button/product_button.tsx | 3 +- .../source_config_fields.test.tsx | 1 + .../source_config_fields.tsx | 1 - .../shared/source_icon/source_icon.test.tsx | 1 + .../shared/source_row/source_row.test.tsx | 4 ++- .../shared/source_row/source_row.tsx | 3 +- .../sources_table/sources_table.test.tsx | 6 ++-- .../shared/sources_table/sources_table.tsx | 2 +- .../table_pagination_bar.test.tsx | 1 + .../shared/user_icon/user_icon.test.tsx | 5 +-- .../shared/user_row/user_row.test.tsx | 5 +-- .../view_content_header.test.tsx | 2 ++ .../view_content_header.tsx | 1 - .../workplace_search/index.test.tsx | 4 ++- .../applications/workplace_search/index.tsx | 21 ++++++------ .../workplace_search/routes.test.tsx | 1 + .../components/add_source/add_source.test.tsx | 2 +- .../components/add_source/add_source.tsx | 8 ++--- .../add_source/add_source_header.test.tsx | 1 + .../add_source/add_source_list.test.tsx | 8 ++--- .../components/add_source/add_source_list.tsx | 11 +++---- .../add_source/add_source_logic.test.ts | 14 ++++---- .../components/add_source/add_source_logic.ts | 24 +++++--------- .../available_sources_list.test.tsx | 2 +- .../add_source/available_sources_list.tsx | 8 ++--- .../add_source/config_completed.test.tsx | 1 + .../add_source/config_completed.tsx | 8 ++--- .../add_source/config_docs_links.test.tsx | 1 + .../add_source/config_docs_links.tsx | 3 +- .../add_source/configuration_intro.test.tsx | 1 + .../add_source/configuration_intro.tsx | 9 +++-- .../add_source/configure_custom.test.tsx | 1 + .../add_source/configure_custom.tsx | 4 +-- .../add_source/configure_oauth.test.tsx | 1 + .../components/add_source/configure_oauth.tsx | 7 ++-- .../configured_sources_list.test.tsx | 5 +-- .../add_source/configured_sources_list.tsx | 2 +- .../add_source/connect_instance.test.tsx | 2 ++ .../add_source/connect_instance.tsx | 13 ++++---- .../add_source/re_authenticate.test.tsx | 1 + .../components/add_source/re_authenticate.tsx | 5 +-- .../add_source/save_config.test.tsx | 6 ++-- .../components/add_source/save_config.tsx | 13 +++----- .../add_source/save_custom.test.tsx | 1 + .../components/add_source/save_custom.tsx | 9 ++--- .../add_source/source_features.test.tsx | 4 +-- .../components/add_source/source_features.tsx | 5 ++- .../custom_source_icon.test.tsx | 1 + .../display_settings.test.tsx | 11 +++---- .../display_settings/display_settings.tsx | 17 ++++------ .../display_settings_logic.test.ts | 11 +++---- .../display_settings_logic.ts | 9 +++-- .../display_settings_router.test.tsx | 5 ++- .../display_settings_router.tsx | 3 +- .../example_result_detail_card.test.tsx | 4 +-- .../example_result_detail_card.tsx | 3 +- .../example_search_result_group.test.tsx | 5 ++- .../example_search_result_group.tsx | 6 ++-- .../example_standout_result.test.tsx | 5 ++- .../example_standout_result.tsx | 3 +- .../field_editor_modal.test.tsx | 6 ++-- .../display_settings/result_detail.test.tsx | 8 ++--- .../display_settings/result_detail.tsx | 3 +- .../display_settings/search_results.test.tsx | 7 ++-- .../display_settings/search_results.tsx | 5 ++- .../display_settings/subtitle_field.test.tsx | 1 + .../display_settings/title_field.test.tsx | 1 + .../components/overview.test.tsx | 4 +-- .../content_sources/components/overview.tsx | 33 ++++++++----------- .../components/schema/schema.test.tsx | 7 ++-- .../components/schema/schema.tsx | 15 ++++----- .../schema/schema_change_errors.test.tsx | 3 +- .../schema/schema_change_errors.tsx | 3 +- .../schema/schema_fields_table.test.tsx | 1 + .../components/schema/schema_fields_table.tsx | 6 ++-- .../components/schema/schema_logic.test.ts | 5 ++- .../components/schema/schema_logic.ts | 11 +++---- .../components/source_added.test.tsx | 4 +-- .../components/source_added.tsx | 2 +- .../components/source_content.test.tsx | 8 ++--- .../components/source_content.tsx | 19 ++++------- .../components/source_info_card.test.tsx | 1 + .../components/source_info_card.tsx | 1 - .../components/source_settings.test.tsx | 4 +-- .../components/source_settings.tsx | 20 +++++------ .../components/source_sub_nav.test.tsx | 5 +-- .../components/source_sub_nav.tsx | 8 ++--- .../organization_sources.test.tsx | 6 ++-- .../content_sources/organization_sources.tsx | 14 ++++---- .../views/content_sources/private_sources.tsx | 20 +++++------ .../views/content_sources/source_data.tsx | 4 +-- .../content_sources/source_logic.test.ts | 10 +++--- .../views/content_sources/source_logic.ts | 8 ++--- .../content_sources/source_router.test.tsx | 10 ++---- .../views/content_sources/source_router.tsx | 26 ++++++--------- .../content_sources/sources_logic.test.ts | 5 ++- .../views/content_sources/sources_logic.ts | 10 ++---- .../content_sources/sources_router.test.tsx | 4 +-- .../views/content_sources/sources_router.tsx | 16 ++++----- .../content_sources/sources_view.test.tsx | 4 +-- .../views/content_sources/sources_view.tsx | 7 ++-- .../views/error_state/error_state.test.tsx | 2 ++ .../views/error_state/error_state.tsx | 1 + .../groups/__mocks__/groups_logic.mock.ts | 3 +- .../components/add_group_modal.test.tsx | 5 +-- .../groups/components/add_group_modal.tsx | 3 +- .../components/clear_filters_link.test.tsx | 5 +-- .../groups/components/clear_filters_link.tsx | 2 +- .../components/filterable_users_list.test.tsx | 5 +-- .../components/filterable_users_list.tsx | 3 +- .../filterable_users_popover.test.tsx | 7 ++-- .../components/filterable_users_popover.tsx | 2 +- .../components/group_manager_modal.test.tsx | 7 ++-- .../groups/components/group_manager_modal.tsx | 10 ++---- .../groups/components/group_overview.test.tsx | 15 +++++---- .../groups/components/group_overview.tsx | 15 ++++----- .../groups/components/group_row.test.tsx | 5 +-- .../views/groups/components/group_row.tsx | 13 ++++---- .../group_row_sources_dropdown.test.tsx | 5 +-- .../components/group_row_sources_dropdown.tsx | 3 +- .../group_row_users_dropdown.test.tsx | 3 +- .../components/group_row_users_dropdown.tsx | 1 + .../group_source_prioritization.test.tsx | 5 +-- .../group_source_prioritization.tsx | 9 ++--- .../groups/components/group_sources.test.tsx | 8 ++--- .../views/groups/components/group_sources.tsx | 1 - .../groups/components/group_sub_nav.test.tsx | 5 +-- .../views/groups/components/group_sub_nav.tsx | 7 ++-- .../groups/components/group_users.test.tsx | 6 ++-- .../views/groups/components/group_users.tsx | 1 - .../components/group_users_table.test.tsx | 7 ++-- .../groups/components/group_users_table.tsx | 9 ++--- .../groups/components/groups_table.test.tsx | 12 +++---- .../views/groups/components/groups_table.tsx | 8 ++--- .../components/manage_users_modal.test.tsx | 3 +- .../components/shared_sources_modal.test.tsx | 3 +- .../components/source_option_item.test.tsx | 6 ++-- .../groups/components/source_option_item.tsx | 1 - .../groups/components/sources_list.test.tsx | 5 +-- .../table_filter_sources_dropdown.test.tsx | 4 +-- .../table_filter_sources_dropdown.tsx | 4 +-- .../table_filter_users_dropdown.test.tsx | 3 +- .../table_filter_users_dropdown.tsx | 4 +-- .../groups/components/table_filters.test.tsx | 5 +-- .../views/groups/components/table_filters.tsx | 3 +- .../components/user_option_item.test.tsx | 6 ++-- .../views/groups/group_logic.test.ts | 6 ++-- .../views/groups/group_logic.ts | 7 ++-- .../views/groups/group_router.test.tsx | 12 +++---- .../views/groups/group_router.tsx | 12 +++---- .../views/groups/groups.test.tsx | 15 ++++----- .../workplace_search/views/groups/groups.tsx | 14 +++----- .../views/groups/groups_logic.test.ts | 8 ++--- .../views/groups/groups_logic.ts | 12 +++---- .../views/groups/groups_router.test.tsx | 5 ++- .../views/groups/groups_router.tsx | 9 ++--- .../overview/__mocks__/overview_logic.mock.ts | 2 +- .../views/overview/onboarding_card.test.tsx | 1 + .../views/overview/onboarding_card.tsx | 3 +- .../views/overview/onboarding_steps.test.tsx | 6 ++-- .../views/overview/onboarding_steps.tsx | 18 +++++----- .../overview/organization_stats.test.tsx | 4 ++- .../views/overview/organization_stats.tsx | 8 ++--- .../views/overview/overview.test.tsx | 5 +-- .../views/overview/overview.tsx | 10 +++--- .../views/overview/overview_logic.ts | 1 + .../views/overview/recent_activity.test.tsx | 4 ++- .../views/overview/recent_activity.tsx | 10 +++--- .../views/overview/statistic_card.test.tsx | 1 + .../views/overview/statistic_card.tsx | 1 + .../components/private_sources_table.test.tsx | 2 ++ .../components/private_sources_table.tsx | 4 +-- .../views/security/security.test.tsx | 5 ++- .../views/security/security.tsx | 11 +++---- .../views/security/security_logic.test.ts | 6 ++-- .../views/security/security_logic.ts | 4 +-- .../settings/components/connectors.test.tsx | 2 +- .../views/settings/components/connectors.tsx | 7 ++-- .../settings/components/customize.test.tsx | 1 + .../views/settings/components/customize.tsx | 6 ++-- .../components/oauth_application.test.tsx | 7 ++-- .../settings/components/oauth_application.tsx | 13 ++++---- .../components/settings_sub_nav.test.tsx | 1 + .../settings/components/settings_sub_nav.tsx | 4 +-- .../components/source_config.test.tsx | 5 +-- .../settings/components/source_config.tsx | 8 ++--- .../views/settings/settings_logic.test.ts | 7 ++-- .../views/settings/settings_logic.ts | 7 ++-- .../views/settings/settings_router.test.tsx | 6 ++-- .../views/settings/settings_router.tsx | 9 ++--- .../views/setup_guide/setup_guide.test.tsx | 2 ++ .../views/setup_guide/setup_guide.tsx | 7 ++-- .../plugins/enterprise_search/public/index.ts | 1 + .../enterprise_search/public/plugin.ts | 6 ++-- .../server/__mocks__/router.mock.ts | 2 +- .../__mocks__/routerDependencies.mock.ts | 1 + .../server/collectors/app_search/telemetry.ts | 1 + .../collectors/enterprise_search/telemetry.ts | 1 + .../collectors/workplace_search/telemetry.ts | 1 + .../plugins/enterprise_search/server/index.ts | 3 +- .../server/lib/check_access.test.ts | 7 ++-- .../server/lib/check_access.ts | 5 +-- .../lib/enterprise_search_config_api.test.ts | 23 ++++++------- .../lib/enterprise_search_config_api.ts | 7 ++-- .../enterprise_search_request_handler.test.ts | 2 ++ .../lib/enterprise_search_request_handler.ts | 4 ++- .../enterprise_search/server/plugin.ts | 30 +++++++++-------- .../server/routes/app_search/engines.ts | 2 +- .../server/routes/app_search/index.ts | 6 ++-- .../routes/enterprise_search/config_data.ts | 2 +- .../enterprise_search/telemetry.test.ts | 3 +- .../routes/enterprise_search/telemetry.ts | 9 ++--- .../server/routes/workplace_search/index.ts | 6 ++-- .../saved_objects/app_search/telemetry.ts | 1 + .../enterprise_search/telemetry.ts | 1 + .../workplace_search/telemetry.ts | 1 + 489 files changed, 1376 insertions(+), 1032 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/types.ts diff --git a/.eslintrc.js b/.eslintrc.js index 9430b9bf24466..7608bcb40a0b9 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1119,6 +1119,39 @@ module.exports = { // All files files: ['x-pack/plugins/enterprise_search/**/*.{ts,tsx}'], rules: { + 'import/order': [ + 'error', + { + groups: ['unknown', ['builtin', 'external'], 'internal', 'parent', 'sibling', 'index'], + pathGroups: [ + { + pattern: + '{../../../../../../,../../../../../,../../../../,../../../,../../,../}{common/,*}__mocks__{*,/**}', + group: 'unknown', + }, + { + pattern: '{**,.}/*.mock', + group: 'unknown', + }, + { + pattern: 'react*', + group: 'external', + position: 'before', + }, + { + pattern: '{@elastic/**,@kbn/**,src/**}', + group: 'internal', + }, + ], + pathGroupsExcludedImportTypes: [], + alphabetize: { + order: 'asc', + caseInsensitive: true, + }, + 'newlines-between': 'always-and-inside-groups', + }, + ], + 'import/newline-after-import': 'error', 'react-hooks/exhaustive-deps': 'off', 'react/jsx-boolean-value': ['error', 'never'], }, diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts index f284fef370f02..ecc7b991f0761 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kea.mock.ts @@ -11,11 +11,11 @@ * NOTE: These variable names MUST start with 'mock*' in order for * Jest to accept its use within a jest.mock() */ +import { mockFlashMessagesValues, mockFlashMessagesActions } from './flash_messages_logic.mock'; +import { mockHttpValues } from './http_logic.mock'; import { mockKibanaValues } from './kibana_logic.mock'; import { mockLicensingValues } from './licensing_logic.mock'; -import { mockHttpValues } from './http_logic.mock'; import { mockTelemetryActions } from './telemetry_logic.mock'; -import { mockFlashMessagesValues, mockFlashMessagesActions } from './flash_messages_logic.mock'; export const mockAllValues = { ...mockKibanaValues, diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_logic.mock.ts index a201a2b56c25c..d8d66e5ee1998 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/kibana_logic.mock.ts @@ -6,6 +6,7 @@ */ import { chartPluginMock } from '../../../../../../src/plugins/charts/public/mocks'; + import { mockHistory } from './'; export const mockKibanaValues = { diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_async.mock.tsx b/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_async.mock.tsx index 27e8a1421f462..2b5c06df37e8c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_async.mock.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_async.mock.tsx @@ -6,8 +6,9 @@ */ import React from 'react'; -import { act } from 'react-dom/test-utils'; + import { mount, ReactWrapper } from 'enzyme'; +import { act } from 'react-dom/test-utils'; import { mountWithIntl } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_i18n.mock.tsx b/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_i18n.mock.tsx index a5a2891d3699c..3a98616082412 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_i18n.mock.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/mount_with_i18n.mock.tsx @@ -6,7 +6,9 @@ */ import React from 'react'; + import { mount } from 'enzyme'; + import { I18nProvider } from '@kbn/i18n/react'; /** diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_with_i18n.mock.tsx b/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_with_i18n.mock.tsx index 224d71ac579a0..0127804374163 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_with_i18n.mock.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/shallow_with_i18n.mock.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { shallow, mount, ReactWrapper } from 'enzyme'; + import { I18nProvider, __IntlProvider } from '@kbn/i18n/react'; // Use fake component to extract `intl` property to use in tests. diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts index 86f3993728e06..e5b0a702897bf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts @@ -5,9 +5,9 @@ * 2.0. */ +import { DEFAULT_INITIAL_APP_DATA } from '../../../common/__mocks__'; import { LogicMounter } from '../__mocks__'; -import { DEFAULT_INITIAL_APP_DATA } from '../../../common/__mocks__'; import { AppLogic } from './app_logic'; describe('AppLogic', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts index 8a55b7c0add94..c33a0e89d2aee 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts @@ -8,6 +8,7 @@ import { kea, MakeLogicType } from 'kea'; import { InitialAppData } from '../../../common/types'; + import { ConfiguredLimits, Account, Role } from './types'; import { getRoleAbilities } from './utils/role'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_layout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_layout.test.tsx index 5248833d827b2..1a4e05c04f319 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_layout.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_layout.test.tsx @@ -11,14 +11,15 @@ import { mockKibanaValues, setMockValues, setMockActions, rerender } from '../.. import React from 'react'; import { useParams } from 'react-router-dom'; + import { shallow } from 'enzyme'; -import { Loading } from '../../../shared/loading'; import { FlashMessages } from '../../../shared/flash_messages'; +import { Loading } from '../../../shared/loading'; import { LogRetentionCallout } from '../log_retention'; -import { AnalyticsHeader, AnalyticsUnavailable } from './components'; import { AnalyticsLayout } from './analytics_layout'; +import { AnalyticsHeader, AnalyticsUnavailable } from './components'; describe('AnalyticsLayout', () => { const { history } = mockKibanaValues; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_layout.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_layout.tsx index 88d0f77541166..0c90267c1dbad 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_layout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_layout.tsx @@ -7,18 +7,21 @@ import React, { useEffect } from 'react'; import { useParams } from 'react-router-dom'; + import { useValues, useActions } from 'kea'; + import { EuiSpacer } from '@elastic/eui'; -import { KibanaLogic } from '../../../shared/kibana'; import { FlashMessages } from '../../../shared/flash_messages'; +import { KibanaLogic } from '../../../shared/kibana'; import { Loading } from '../../../shared/loading'; import { LogRetentionCallout, LogRetentionOptions } from '../log_retention'; -import { AnalyticsLogic } from './'; import { AnalyticsHeader, AnalyticsUnavailable } from './components'; +import { AnalyticsLogic } from './'; + interface Props { title: string; isQueryView?: boolean; 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 6ca9eb23c962b..ad612e48c770a 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 @@ -19,6 +19,7 @@ jest.mock('../engine', () => ({ import { nextTick } from '@kbn/test/jest'; import { DEFAULT_START_DATE, DEFAULT_END_DATE } from './constants'; + import { AnalyticsLogic } from './'; describe('AnalyticsLogic', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_logic.ts index e978d2c65398e..de0828f6d71ea 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_logic.ts @@ -8,9 +8,9 @@ import { kea, MakeLogicType } from 'kea'; import queryString from 'query-string'; -import { KibanaLogic } from '../../../shared/kibana'; -import { HttpLogic } from '../../../shared/http'; import { flashAPIErrors } from '../../../shared/flash_messages'; +import { HttpLogic } from '../../../shared/http'; +import { KibanaLogic } from '../../../shared/kibana'; import { EngineLogic } from '../engine'; import { DEFAULT_START_DATE, DEFAULT_END_DATE } from './constants'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_router.test.tsx index 3f6bf77024c1e..3940151d3b7cd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/analytics_router.test.tsx @@ -8,9 +8,11 @@ import '../../__mocks__/engine_logic.mock'; import React from 'react'; -import { shallow } from 'enzyme'; + import { Route, Switch } from 'react-router-dom'; +import { shallow } from 'enzyme'; + import { AnalyticsRouter } from './'; describe('AnalyticsRouter', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_cards.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_cards.test.tsx index 84ee392c2419e..8883d0d1ffcbd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_cards.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_cards.test.tsx @@ -6,7 +6,9 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiStat } from '@elastic/eui'; import { AnalyticsCards } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_cards.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_cards.tsx index 417fa0cc48f65..b08e391f845e6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_cards.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_cards.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiStat } from '@elastic/eui'; interface Props { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_chart.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_chart.test.tsx index dcea1f81e53eb..51238d62bdac7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_chart.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_chart.test.tsx @@ -8,7 +8,9 @@ import { mockKibanaValues } from '../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; + import { Chart, Settings, LineSeries, Axis } from '@elastic/charts'; import { AnalyticsChart } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_chart.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_chart.tsx index 686cadda02f63..fa33389503beb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_chart.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_chart.tsx @@ -6,9 +6,11 @@ */ import React from 'react'; + import { useValues } from 'kea'; import moment from 'moment'; + import { Chart, Settings, LineSeries, CurveType, Axis } from '@elastic/charts'; import { KibanaLogic } from '../../../../shared/kibana'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_header.test.tsx index 3faf2b03097f7..952c4c2517a0e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_header.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_header.test.tsx @@ -8,13 +8,16 @@ import { setMockValues, mockKibanaValues } from '../../../../__mocks__'; import React, { ReactElement } from 'react'; + import { shallow, ShallowWrapper } from 'enzyme'; import moment, { Moment } from 'moment'; + import { EuiPageHeader, EuiSelect, EuiDatePickerRange, EuiButton } from '@elastic/eui'; import { LogRetentionTooltip } from '../../log_retention'; import { DEFAULT_START_DATE, DEFAULT_END_DATE } from '../constants'; + import { AnalyticsHeader } from './'; describe('AnalyticsHeader', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_header.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_header.tsx index 3986f7859bfd2..8a87a5e8c211c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_header.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_header.tsx @@ -6,12 +6,11 @@ */ import React, { useState } from 'react'; -import { useValues } from 'kea'; -import queryString from 'query-string'; +import { useValues } from 'kea'; import moment from 'moment'; +import queryString from 'query-string'; -import { i18n } from '@kbn/i18n'; import { EuiPageHeader, EuiPageHeaderSection, @@ -23,11 +22,12 @@ import { EuiDatePicker, EuiButton, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { AnalyticsLogic } from '../'; import { KibanaLogic } from '../../../../shared/kibana'; import { LogRetentionTooltip, LogRetentionOptions } from '../../log_retention'; -import { AnalyticsLogic } from '../'; import { DEFAULT_START_DATE, DEFAULT_END_DATE, SERVER_DATE_FORMAT } from '../constants'; import { convertTagsToSelectOptions } from '../utils'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_search.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_search.test.tsx index 4a3bbda5120bc..89fa5b4cc4b73 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_search.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_search.test.tsx @@ -9,7 +9,9 @@ import { mockKibanaValues } from '../../../../__mocks__'; import '../../../__mocks__/engine_logic.mock'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiFieldSearch } from '@elastic/eui'; import { AnalyticsSearch } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_search.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_search.tsx index 922e096701e84..4f2b525aaa168 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_search.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_search.tsx @@ -6,10 +6,11 @@ */ import React, { useState } from 'react'; + import { useValues } from 'kea'; -import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem, EuiFieldSearch, EuiButton, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { KibanaLogic } from '../../../../shared/kibana'; import { ENGINE_ANALYTICS_QUERY_DETAIL_PATH } from '../../../routes'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_section.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_section.test.tsx index 981173e2a915b..56e30e6061173 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_section.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_section.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { AnalyticsSection } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/analytics_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/analytics_table.test.tsx index 0788edfdda427..2eac65fc21091 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/analytics_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/analytics_table.test.tsx @@ -9,6 +9,7 @@ import { mountWithIntl, mockKibanaValues } from '../../../../../__mocks__'; import '../../../../__mocks__/engine_logic.mock'; import React from 'react'; + import { EuiBasicTable, EuiBadge, EuiEmptyPrompt } from '@elastic/eui'; import { AnalyticsTable } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/analytics_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/analytics_table.tsx index 8e9853233cbed..a580047f1f635 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/analytics_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/analytics_table.tsx @@ -6,10 +6,12 @@ */ import React from 'react'; -import { i18n } from '@kbn/i18n'; + import { EuiBasicTable, EuiBasicTableColumn, EuiEmptyPrompt } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { Query } from '../../types'; + import { TERM_COLUMN_PROPS, TAGS_COLUMN, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/inline_tags_list.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/inline_tags_list.test.tsx index 9ad2cc32f99c5..9204fa6e75fa7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/inline_tags_list.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/inline_tags_list.test.tsx @@ -6,7 +6,9 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiBadge, EuiToolTip } from '@elastic/eui'; import { InlineTagsList } from './inline_tags_list'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/inline_tags_list.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/inline_tags_list.tsx index 421ff1eedf278..908b096c80a9e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/inline_tags_list.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/inline_tags_list.tsx @@ -6,8 +6,9 @@ */ import React from 'react'; -import { i18n } from '@kbn/i18n'; + import { EuiBadgeGroup, EuiBadge, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { Query } from '../../types'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/query_clicks_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/query_clicks_table.test.tsx index 4396f91136258..cc8f13299c57f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/query_clicks_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/query_clicks_table.test.tsx @@ -9,6 +9,7 @@ import { mountWithIntl } from '../../../../../__mocks__'; import '../../../../__mocks__/engine_logic.mock'; import React from 'react'; + import { EuiBasicTable, EuiLink, EuiBadge, EuiEmptyPrompt } from '@elastic/eui'; import { QueryClicksTable } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/query_clicks_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/query_clicks_table.tsx index 7c333623df6c0..4a93724ff5245 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/query_clicks_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/query_clicks_table.tsx @@ -7,15 +7,16 @@ import React from 'react'; -import { i18n } from '@kbn/i18n'; import { EuiBasicTable, EuiBasicTableColumn, EuiEmptyPrompt } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { EuiLinkTo } from '../../../../../shared/react_router_helpers'; import { ENGINE_DOCUMENT_DETAIL_PATH } from '../../../../routes'; -import { generateEnginePath } from '../../../engine'; import { DOCUMENTS_TITLE } from '../../../documents'; +import { generateEnginePath } from '../../../engine'; import { QueryClick } from '../../types'; + import { FIRST_COLUMN_PROPS, TAGS_COLUMN, COUNT_COLUMN_PROPS } from './shared_columns'; interface Props { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/recent_queries_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/recent_queries_table.test.tsx index fdbbd326c47a1..a5a582d3747bc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/recent_queries_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/recent_queries_table.test.tsx @@ -9,6 +9,7 @@ import { mountWithIntl, mockKibanaValues } from '../../../../../__mocks__'; import '../../../../__mocks__/engine_logic.mock'; import React from 'react'; + import { EuiBasicTable, EuiBadge, EuiEmptyPrompt } from '@elastic/eui'; import { RecentQueriesTable } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/recent_queries_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/recent_queries_table.tsx index 20e50e633b321..7724ac5c393ec 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/recent_queries_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/recent_queries_table.tsx @@ -7,11 +7,12 @@ import React from 'react'; +import { EuiBasicTable, EuiBasicTableColumn, EuiEmptyPrompt } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedDate, FormattedTime } from '@kbn/i18n/react'; -import { EuiBasicTable, EuiBasicTableColumn, EuiEmptyPrompt } from '@elastic/eui'; import { RecentQuery } from '../../types'; + import { TERM_COLUMN_PROPS, TAGS_COLUMN, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/shared_columns.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/shared_columns.tsx index 0612fac1c07ed..9d8365a2f7af1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/shared_columns.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_tables/shared_columns.tsx @@ -6,14 +6,15 @@ */ import React from 'react'; + import { i18n } from '@kbn/i18n'; -import { EuiLinkTo } from '../../../../../shared/react_router_helpers'; import { KibanaLogic } from '../../../../../shared/kibana'; +import { EuiLinkTo } from '../../../../../shared/react_router_helpers'; import { ENGINE_ANALYTICS_QUERY_DETAIL_PATH } from '../../../../routes'; import { generateEnginePath } from '../../../engine'; - import { Query, RecentQuery } from '../../types'; + import { InlineTagsList } from './inline_tags_list'; /** diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_unavailable.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_unavailable.test.tsx index ddc0e4636b3ad..e2ff440615dfc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_unavailable.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_unavailable.test.tsx @@ -6,7 +6,9 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiEmptyPrompt } from '@elastic/eui'; import { FlashMessages } from '../../../../shared/flash_messages'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_unavailable.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_unavailable.tsx index 2ef020d2f4992..388570b32b6d2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_unavailable.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/components/analytics_unavailable.tsx @@ -6,8 +6,9 @@ */ import React from 'react'; -import { i18n } from '@kbn/i18n'; + import { EuiEmptyPrompt } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { FlashMessages } from '../../../../shared/flash_messages'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/constants.ts index a04a9474ce5ae..75001f5bc86d6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/constants.ts @@ -6,6 +6,7 @@ */ import moment from 'moment'; + import { i18n } from '@kbn/i18n'; export const ANALYTICS_TITLE = i18n.translate( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/utils.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/utils.ts index 2d00c906b2aec..db679b0f387e8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/utils.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/utils.ts @@ -6,11 +6,12 @@ */ import moment from 'moment'; -import { i18n } from '@kbn/i18n'; + import { EuiSelectProps } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; -import { SERVER_DATE_FORMAT } from './constants'; import { ChartData } from './components/analytics_chart'; +import { SERVER_DATE_FORMAT } from './constants'; interface ConvertToChartData { data: number[]; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/analytics.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/analytics.test.tsx index 065b2208648bf..d8921ff0d3723 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/analytics.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/analytics.test.tsx @@ -9,6 +9,7 @@ import { setMockValues } from '../../../../__mocks__'; import '../../../__mocks__/engine_logic.mock'; import React from 'react'; + import { shallow } from 'enzyme'; import { @@ -18,6 +19,7 @@ import { AnalyticsTable, RecentQueriesTable, } from '../components'; + import { Analytics, ViewAllButton } from './analytics'; describe('Analytics overview', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/analytics.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/analytics.tsx index 09b1ff45c6122..a4f0bc356ac78 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/analytics.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/analytics.tsx @@ -6,10 +6,11 @@ */ import React from 'react'; + import { useValues } from 'kea'; -import { i18n } from '@kbn/i18n'; import { EuiSpacer, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { EuiButtonTo } from '../../../../shared/react_router_helpers'; import { @@ -21,6 +22,8 @@ import { } from '../../../routes'; import { generateEnginePath } from '../../engine'; +import { AnalyticsLayout } from '../analytics_layout'; +import { AnalyticsSection, AnalyticsTable, RecentQueriesTable } from '../components'; import { ANALYTICS_TITLE, TOTAL_QUERIES, @@ -32,9 +35,7 @@ import { TOP_QUERIES_NO_CLICKS, RECENT_QUERIES, } from '../constants'; -import { AnalyticsLayout } from '../analytics_layout'; -import { AnalyticsSection, AnalyticsTable, RecentQueriesTable } from '../components'; -import { AnalyticsLogic, AnalyticsCards, AnalyticsChart, convertToChartData } from '../'; +import { AnalyticsLogic, AnalyticsCards, AnalyticsChart, convertToChartData } from '../index'; export const Analytics: React.FC = () => { const { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/query_detail.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/query_detail.test.tsx index 050770944edcd..978f11ddfd5cd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/query_detail.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/query_detail.test.tsx @@ -10,12 +10,14 @@ import { setMockValues } from '../../../../__mocks__'; import React from 'react'; import { useParams } from 'react-router-dom'; + import { shallow } from 'enzyme'; import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome'; import { AnalyticsLayout } from '../analytics_layout'; import { AnalyticsCards, AnalyticsChart, QueryClicksTable } from '../components'; + import { QueryDetail } from './'; describe('QueryDetail', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/query_detail.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/query_detail.tsx index 96587eb528710..f00c4e29a7190 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/query_detail.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/query_detail.tsx @@ -6,10 +6,11 @@ */ import React from 'react'; + import { useValues } from 'kea'; -import { i18n } from '@kbn/i18n'; import { EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome'; import { BreadcrumbTrail } from '../../../../shared/kibana_chrome/generate_breadcrumbs'; @@ -17,7 +18,7 @@ import { useDecodedParams } from '../../../utils/encode_path_params'; import { AnalyticsLayout } from '../analytics_layout'; import { AnalyticsSection, QueryClicksTable } from '../components'; -import { AnalyticsLogic, AnalyticsCards, AnalyticsChart, convertToChartData } from '../'; +import { AnalyticsLogic, AnalyticsCards, AnalyticsChart, convertToChartData } from '../index'; const QUERY_DETAIL_TITLE = i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.analytics.queryDetail.title', diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/recent_queries.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/recent_queries.test.tsx index 40577fb2d4447..21d515a7b9795 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/recent_queries.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/recent_queries.test.tsx @@ -8,9 +8,11 @@ import { setMockValues } from '../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; import { RecentQueriesTable } from '../components'; + import { RecentQueries } from './'; describe('RecentQueries', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/recent_queries.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/recent_queries.tsx index e5380258894ae..bb0c3c4d32244 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/recent_queries.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/recent_queries.tsx @@ -6,12 +6,13 @@ */ import React from 'react'; + import { useValues } from 'kea'; -import { RECENT_QUERIES } from '../constants'; +import { AnalyticsLogic } from '../'; import { AnalyticsLayout } from '../analytics_layout'; import { AnalyticsSearch, RecentQueriesTable } from '../components'; -import { AnalyticsLogic } from '../'; +import { RECENT_QUERIES } from '../constants'; export const RecentQueries: React.FC = () => { const { recentQueries } = useValues(AnalyticsLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries.test.tsx index b037e6bf1d64e..46b2b37958435 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries.test.tsx @@ -8,9 +8,11 @@ import { setMockValues } from '../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; import { AnalyticsTable } from '../components'; + import { TopQueries } from './'; describe('TopQueries', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries.tsx index 76d523d16ee11..6459126560b3a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries.tsx @@ -6,12 +6,13 @@ */ import React from 'react'; + import { useValues } from 'kea'; -import { TOP_QUERIES } from '../constants'; +import { AnalyticsLogic } from '../'; import { AnalyticsLayout } from '../analytics_layout'; import { AnalyticsSearch, AnalyticsTable } from '../components'; -import { AnalyticsLogic } from '../'; +import { TOP_QUERIES } from '../constants'; export const TopQueries: React.FC = () => { const { topQueries } = useValues(AnalyticsLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_clicks.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_clicks.test.tsx index 1248a49fc5a9c..83212160d1350 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_clicks.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_clicks.test.tsx @@ -8,9 +8,11 @@ import { setMockValues } from '../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; import { AnalyticsTable } from '../components'; + import { TopQueriesNoClicks } from './'; describe('TopQueriesNoClicks', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_clicks.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_clicks.tsx index 604ab96b871e7..8e2591697feaa 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_clicks.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_clicks.tsx @@ -6,12 +6,13 @@ */ import React from 'react'; + import { useValues } from 'kea'; -import { TOP_QUERIES_NO_CLICKS } from '../constants'; +import { AnalyticsLogic } from '../'; import { AnalyticsLayout } from '../analytics_layout'; import { AnalyticsSearch, AnalyticsTable } from '../components'; -import { AnalyticsLogic } from '../'; +import { TOP_QUERIES_NO_CLICKS } from '../constants'; export const TopQueriesNoClicks: React.FC = () => { const { topQueriesNoClicks } = useValues(AnalyticsLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_results.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_results.test.tsx index 3cb77b3c7afbc..dfc5b9c93ab64 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_results.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_results.test.tsx @@ -8,9 +8,11 @@ import { setMockValues } from '../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; import { AnalyticsTable } from '../components'; + import { TopQueriesNoResults } from './'; describe('TopQueriesNoResults', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_results.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_results.tsx index 425fdf8e88559..e093a5130d204 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_results.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_no_results.tsx @@ -6,12 +6,13 @@ */ import React from 'react'; + import { useValues } from 'kea'; -import { TOP_QUERIES_NO_RESULTS } from '../constants'; +import { AnalyticsLogic } from '../'; import { AnalyticsLayout } from '../analytics_layout'; import { AnalyticsSearch, AnalyticsTable } from '../components'; -import { AnalyticsLogic } from '../'; +import { TOP_QUERIES_NO_RESULTS } from '../constants'; export const TopQueriesNoResults: React.FC = () => { const { topQueriesNoResults } = useValues(AnalyticsLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_with_clicks.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_with_clicks.test.tsx index 83be03e95d2cf..fb967ca06b387 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_with_clicks.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_with_clicks.test.tsx @@ -8,9 +8,11 @@ import { setMockValues } from '../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; import { AnalyticsTable } from '../components'; + import { TopQueriesWithClicks } from './'; describe('TopQueriesWithClicks', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_with_clicks.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_with_clicks.tsx index bec096019035b..87e276a8382c3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_with_clicks.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/analytics/views/top_queries_with_clicks.tsx @@ -6,12 +6,13 @@ */ import React from 'react'; + import { useValues } from 'kea'; -import { TOP_QUERIES_WITH_CLICKS } from '../constants'; +import { AnalyticsLogic } from '../'; import { AnalyticsLayout } from '../analytics_layout'; import { AnalyticsSearch, AnalyticsTable } from '../components'; -import { AnalyticsLogic } from '../'; +import { TOP_QUERIES_WITH_CLICKS } from '../constants'; export const TopQueriesWithClicks: React.FC = () => { const { topQueriesWithClicks } = useValues(AnalyticsLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts index 2e28e5a272643..0fb118548a67b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/constants.ts @@ -6,6 +6,7 @@ */ import { i18n } from '@kbn/i18n'; + import { DOCS_PREFIX } from '../../routes'; export const CREDENTIALS_TITLE = i18n.translate( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.test.tsx index cc783e7c056e2..48fcf4b8c5b66 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.test.tsx @@ -9,12 +9,15 @@ import { setMockValues, setMockActions } from '../../../__mocks__/kea.mock'; import { unmountHandler } from '../../../__mocks__/shallow_useeffect.mock'; import React from 'react'; + import { shallow } from 'enzyme'; -import { Credentials } from './credentials'; import { EuiCopy, EuiLoadingContent, EuiPageContentBody } from '@elastic/eui'; import { externalUrl } from '../../../shared/enterprise_search_url'; + +import { Credentials } from './credentials'; + import { CredentialsFlyout } from './credentials_flyout'; describe('Credentials', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx index 0266b64f97104..266e9467c300d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials.tsx @@ -6,6 +6,7 @@ */ import React, { useEffect } from 'react'; + import { useActions, useValues } from 'kea'; import { @@ -24,14 +25,14 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { externalUrl } from '../../../shared/enterprise_search_url/external_url'; import { FlashMessages } from '../../../shared/flash_messages'; +import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; -import { CredentialsLogic } from './credentials_logic'; -import { externalUrl } from '../../../shared/enterprise_search_url/external_url'; import { CREDENTIALS_TITLE } from './constants'; -import { CredentialsList } from './credentials_list'; import { CredentialsFlyout } from './credentials_flyout'; +import { CredentialsList } from './credentials_list'; +import { CredentialsLogic } from './credentials_logic'; export const Credentials: React.FC = () => { const { initializeCredentialsData, resetCredentials, showCredentialsForm } = useActions( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/body.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/body.test.tsx index 8b5a59b82c19b..595bc1bcbb828 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/body.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/body.test.tsx @@ -8,12 +8,15 @@ import { setMockValues, setMockActions } from '../../../../__mocks__/kea.mock'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiFlyoutBody, EuiForm } from '@elastic/eui'; import { ApiTokenTypes } from '../constants'; import { defaultApiToken } from '../credentials_logic'; +import { CredentialsFlyoutBody } from './body'; import { FormKeyName, FormKeyType, @@ -21,7 +24,6 @@ import { FormKeyEngineAccess, FormKeyUpdateWarning, } from './form_components'; -import { CredentialsFlyoutBody } from './body'; describe('CredentialsFlyoutBody', () => { const values = { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/body.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/body.tsx index f3de25fe0a25d..def165f3f82a2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/body.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/body.tsx @@ -6,12 +6,14 @@ */ import React from 'react'; + import { useValues, useActions } from 'kea'; + import { EuiFlyoutBody, EuiForm } from '@elastic/eui'; import { FlashMessages } from '../../../../shared/flash_messages'; -import { CredentialsLogic } from '../credentials_logic'; import { ApiTokenTypes } from '../constants'; +import { CredentialsLogic } from '../credentials_logic'; import { FormKeyName, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/footer.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/footer.test.tsx index 036fe881c7d0d..23e85b92bb8b4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/footer.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/footer.test.tsx @@ -8,7 +8,9 @@ import { setMockValues, setMockActions } from '../../../../__mocks__/kea.mock'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiFlyoutFooter, EuiButtonEmpty } from '@elastic/eui'; import { CredentialsFlyoutFooter } from './footer'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/footer.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/footer.tsx index dc2d52a073b36..c05bd82c6206e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/footer.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/footer.tsx @@ -6,7 +6,9 @@ */ import React from 'react'; + import { useValues, useActions } from 'kea'; + import { EuiFlyoutFooter, EuiFlexGroup, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_engine_access.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_engine_access.test.tsx index 51a737ce8c826..7247deb09f12b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_engine_access.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_engine_access.test.tsx @@ -8,7 +8,9 @@ import { setMockValues, setMockActions, rerender } from '../../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiRadio, EuiCheckbox } from '@elastic/eui'; import { FormKeyEngineAccess, EngineSelection } from './key_engine_access'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_engine_access.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_engine_access.tsx index 2a9e8cf153dca..0d6ebfe437927 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_engine_access.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_engine_access.tsx @@ -6,7 +6,9 @@ */ import React from 'react'; + import { useValues, useActions } from 'kea'; + import { EuiFormRow, EuiRadio, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_name.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_name.test.tsx index 27f95f2ba7cd8..d54d0c89c90cb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_name.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_name.test.tsx @@ -8,7 +8,9 @@ import { setMockValues, setMockActions } from '../../../../../__mocks__/kea.mock'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiFieldText, EuiFormRow } from '@elastic/eui'; import { FormKeyName } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_name.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_name.tsx index cb4dce76dfcc1..f4f4f5f0aaaaa 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_name.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_name.tsx @@ -6,7 +6,9 @@ */ import React from 'react'; + import { useValues, useActions } from 'kea'; + import { EuiFormRow, EuiFieldText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_read_write_access.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_read_write_access.test.tsx index 8cfa5b3c4571a..cf45576d691cf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_read_write_access.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_read_write_access.test.tsx @@ -8,7 +8,9 @@ import { setMockValues, setMockActions } from '../../../../../__mocks__/kea.mock'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiCheckbox } from '@elastic/eui'; import { FormKeyReadWriteAccess } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_read_write_access.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_read_write_access.tsx index f9653159b4403..0b631089c3984 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_read_write_access.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_read_write_access.tsx @@ -6,7 +6,9 @@ */ import React from 'react'; + import { useValues, useActions } from 'kea'; + import { EuiCheckbox, EuiText, EuiTitle, EuiSpacer, EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_type.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_type.test.tsx index 9cf6c82184579..5de2c7fda53ca 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_type.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_type.test.tsx @@ -8,10 +8,13 @@ import { setMockValues, setMockActions } from '../../../../../__mocks__/kea.mock'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiSelect } from '@elastic/eui'; import { ApiTokenTypes, TOKEN_TYPE_INFO } from '../../constants'; + import { FormKeyType } from './'; describe('FormKeyType', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_type.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_type.tsx index a8cc16b3b30e7..60308274fbb76 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_type.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_type.tsx @@ -6,13 +6,15 @@ */ import React from 'react'; + import { useValues, useActions } from 'kea'; + import { EuiFormRow, EuiSelect, EuiText, EuiLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { AppLogic } from '../../../../app_logic'; -import { CredentialsLogic } from '../../credentials_logic'; import { TOKEN_TYPE_DESCRIPTION, TOKEN_TYPE_INFO, DOCS_HREF } from '../../constants'; +import { CredentialsLogic } from '../../credentials_logic'; export const FormKeyType: React.FC = () => { const { myRole } = useValues(AppLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_update_warning.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_update_warning.test.tsx index 073c4ec1c92bf..38eec0b371576 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_update_warning.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_update_warning.test.tsx @@ -6,7 +6,9 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiCallOut } from '@elastic/eui'; import { FormKeyUpdateWarning } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_update_warning.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_update_warning.tsx index 87cda9590f5cb..c24eebea9178b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_update_warning.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/form_components/key_update_warning.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { EuiSpacer, EuiCallOut } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/header.test.tsx index 0772a395dbe71..8ee7f810c1fa5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/header.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/header.test.tsx @@ -8,7 +8,9 @@ import { setMockValues } from '../../../../__mocks__/kea.mock'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiFlyoutHeader } from '@elastic/eui'; import { ApiTokenTypes } from '../constants'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/header.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/header.tsx index a9efcbe371c4f..586ddc5c22b97 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/header.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/header.tsx @@ -6,12 +6,14 @@ */ import React from 'react'; + import { useValues } from 'kea'; + import { EuiFlyoutHeader, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { CredentialsLogic } from '../credentials_logic'; import { FLYOUT_ARIA_LABEL_ID } from '../constants'; +import { CredentialsLogic } from '../credentials_logic'; export const CredentialsFlyoutHeader: React.FC = () => { const { activeApiToken } = useValues(CredentialsLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/index.test.tsx index 1f7408857857a..9932b8ca227b0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/index.test.tsx @@ -8,7 +8,9 @@ import { setMockActions } from '../../../../__mocks__/kea.mock'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiFlyout } from '@elastic/eui'; import { CredentialsFlyout } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/index.tsx index 1335a3cdeea18..2ee73a6b80b5a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_flyout/index.tsx @@ -6,14 +6,17 @@ */ import React from 'react'; + import { useActions } from 'kea'; + import { EuiPortal, EuiFlyout } from '@elastic/eui'; -import { CredentialsLogic } from '../credentials_logic'; import { FLYOUT_ARIA_LABEL_ID } from '../constants'; -import { CredentialsFlyoutHeader } from './header'; +import { CredentialsLogic } from '../credentials_logic'; + import { CredentialsFlyoutBody } from './body'; import { CredentialsFlyoutFooter } from './footer'; +import { CredentialsFlyoutHeader } from './header'; export const CredentialsFlyout: React.FC = () => { const { hideCredentialsForm } = useActions(CredentialsLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.test.tsx index dd3d8ef8069ba..8c52df30bfc67 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.test.tsx @@ -8,15 +8,18 @@ import { setMockValues, setMockActions } from '../../../../__mocks__/kea.mock'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiBasicTable, EuiCopy, EuiEmptyPrompt } from '@elastic/eui'; -import { ApiToken } from '../types'; +import { HiddenText } from '../../../../shared/hidden_text'; import { ApiTokenTypes } from '../constants'; +import { ApiToken } from '../types'; -import { HiddenText } from '../../../../shared/hidden_text'; import { Key } from './key'; -import { CredentialsList } from './credentials_list'; + +import { CredentialsList } from './'; describe('Credentials', () => { const apiToken: ApiToken = { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.tsx index 9d220469347f2..f23479017a680 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/credentials_list.tsx @@ -6,19 +6,21 @@ */ import React, { useMemo } from 'react'; -import { EuiBasicTable, EuiBasicTableColumn, EuiCopy, EuiEmptyPrompt } from '@elastic/eui'; -import { CriteriaWithPagination } from '@elastic/eui/src/components/basic_table/basic_table'; + import { useActions, useValues } from 'kea'; +import { EuiBasicTable, EuiBasicTableColumn, EuiCopy, EuiEmptyPrompt } from '@elastic/eui'; +import { CriteriaWithPagination } from '@elastic/eui/src/components/basic_table/basic_table'; import { i18n } from '@kbn/i18n'; -import { CredentialsLogic } from '../credentials_logic'; -import { Key } from './key'; import { HiddenText } from '../../../../shared/hidden_text'; -import { ApiToken } from '../types'; import { TOKEN_TYPE_DISPLAY_NAMES } from '../constants'; -import { apiTokenSort } from '../utils/api_token_sort'; +import { CredentialsLogic } from '../credentials_logic'; +import { ApiToken } from '../types'; import { getModeDisplayText, getEnginesDisplayText } from '../utils'; +import { apiTokenSort } from '../utils/api_token_sort'; + +import { Key } from './key'; export const CredentialsList: React.FC = () => { const { deleteApiKey, fetchCredentials, showCredentialsForm } = useActions(CredentialsLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/key.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/key.test.tsx index c18302db9ddfd..5e042319ae613 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/key.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/key.test.tsx @@ -6,7 +6,9 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiButtonIcon } from '@elastic/eui'; import { Key } from './key'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/key.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/key.tsx index 940453c83a1fe..ff14379b9aecc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/key.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_list/key.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { EuiButtonIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; 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 005f487772d80..c9d6a43ebbbae 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 @@ -17,6 +17,7 @@ jest.mock('../../app_logic', () => ({ import { nextTick } from '@kbn/test/jest'; import { AppLogic } from '../../app_logic'; + import { ApiTokenTypes } from './constants'; import { CredentialsLogic } from './credentials_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.ts index 25cd1be93836d..ff4600872c589 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/credentials_logic.ts @@ -7,19 +7,19 @@ import { kea, MakeLogicType } from 'kea'; -import { formatApiName } from '../../utils/format_api_name'; -import { ApiTokenTypes, CREATE_MESSAGE, UPDATE_MESSAGE, DELETE_MESSAGE } from './constants'; - -import { HttpLogic } from '../../../shared/http'; +import { Meta } from '../../../../../common/types'; import { clearFlashMessages, setSuccessMessage, flashAPIErrors, } from '../../../shared/flash_messages'; +import { HttpLogic } from '../../../shared/http'; import { AppLogic } from '../../app_logic'; - -import { Meta } from '../../../../../common/types'; import { Engine } from '../../types'; +import { formatApiName } from '../../utils/format_api_name'; + +import { ApiTokenTypes, CREATE_MESSAGE, UPDATE_MESSAGE, DELETE_MESSAGE } from './constants'; + import { ApiToken, CredentialsDetails, TokenReadWrite } from './types'; export const defaultApiToken: ApiToken = { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/types.ts index ddc81658eed2c..0427d25add49b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/types.ts @@ -6,6 +6,7 @@ */ import { Engine } from '../../types'; + import { ApiTokenTypes } from './constants'; export interface CredentialsDetails { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/api_token_sort.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/api_token_sort.test.ts index 1f84caa7e1ef7..70277d6cb7f22 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/api_token_sort.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/api_token_sort.test.ts @@ -5,11 +5,12 @@ * 2.0. */ -import { apiTokenSort } from '.'; import { ApiTokenTypes } from '../constants'; import { ApiToken } from '../types'; +import { apiTokenSort } from '.'; + describe('apiTokenSort', () => { const apiToken: ApiToken = { name: '', diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/get_engines_display_text.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/get_engines_display_text.test.tsx index e92957405a524..71d00efa2a868 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/get_engines_display_text.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/get_engines_display_text.test.tsx @@ -6,22 +6,24 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; -import { getEnginesDisplayText } from './get_engines_display_text'; -import { ApiToken } from '../types'; import { ApiTokenTypes } from '../constants'; +import { ApiToken } from '../types'; -const apiToken: ApiToken = { - name: '', - type: ApiTokenTypes.Private, - read: true, - write: true, - access_all_engines: true, - engines: ['engine1', 'engine2', 'engine3'], -}; +import { getEnginesDisplayText } from './get_engines_display_text'; describe('getEnginesDisplayText', () => { + const apiToken: ApiToken = { + name: '', + type: ApiTokenTypes.Private, + read: true, + write: true, + access_all_engines: true, + engines: ['engine1', 'engine2', 'engine3'], + }; + it('returns "--" when the token is an admin token', () => { const wrapper = shallow(
{getEnginesDisplayText({ ...apiToken, type: ApiTokenTypes.Admin })}
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/get_engines_display_text.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/get_engines_display_text.tsx index 34089cacbf180..d3577ec14fec9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/get_engines_display_text.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/credentials/utils/get_engines_display_text.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { ApiTokenTypes, ALL } from '../constants'; import { ApiToken } from '../types'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/api_code_example.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/api_code_example.test.tsx index 7203cf6982086..34afa9d1e39ed 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/api_code_example.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/api_code_example.test.tsx @@ -9,7 +9,9 @@ import '../../../../__mocks__/enterprise_search_url.mock'; import { setMockValues, setMockActions } from '../../../../__mocks__/kea.mock'; import React from 'react'; + import { shallow, ShallowWrapper } from 'enzyme'; + import { EuiCode, EuiCodeBlock, EuiButtonEmpty } from '@elastic/eui'; import { ApiCodeExample, FlyoutHeader, FlyoutBody, FlyoutFooter } from './api_code_example'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/api_code_example.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/api_code_example.tsx index 9167df25f75b5..88e9df5c2bbf5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/api_code_example.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/api_code_example.tsx @@ -5,12 +5,12 @@ * 2.0. */ -import dedent from 'dedent'; import React from 'react'; + +import dedent from 'dedent'; + import { useValues, useActions } from 'kea'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlyoutHeader, EuiTitle, @@ -27,18 +27,19 @@ import { EuiFlexGroup, EuiFlexItem, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { getEnterpriseSearchUrl } from '../../../../shared/enterprise_search_url'; +import { DOCS_PREFIX } from '../../../routes'; import { EngineLogic } from '../../engine'; import { EngineDetails } from '../../engine/types'; - -import { DOCS_PREFIX } from '../../../routes'; import { DOCUMENTS_API_JSON_EXAMPLE, FLYOUT_ARIA_LABEL_ID, FLYOUT_CANCEL_BUTTON, } from '../constants'; -import { DocumentCreationLogic } from '../'; +import { DocumentCreationLogic } from '../index'; export const ApiCodeExample: React.FC = () => ( <> diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/paste_json_text.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/paste_json_text.test.tsx index c1c0a554b4794..8b5b36094fbc6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/paste_json_text.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/paste_json_text.test.tsx @@ -8,10 +8,13 @@ import { setMockValues, setMockActions, rerender } from '../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiTextArea, EuiButtonEmpty, EuiButton } from '@elastic/eui'; import { Errors } from '../creation_response_components'; + import { PasteJsonText, FlyoutHeader, FlyoutBody, FlyoutFooter } from './paste_json_text'; describe('PasteJsonText', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/paste_json_text.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/paste_json_text.tsx index 377d795413714..2d4a6de26333f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/paste_json_text.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/paste_json_text.tsx @@ -6,9 +6,9 @@ */ import React from 'react'; + import { useValues, useActions } from 'kea'; -import { i18n } from '@kbn/i18n'; import { EuiFlyoutHeader, EuiTitle, @@ -22,12 +22,13 @@ import { EuiSpacer, EuiText, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { AppLogic } from '../../../app_logic'; import { FLYOUT_ARIA_LABEL_ID, FLYOUT_CANCEL_BUTTON, FLYOUT_CONTINUE_BUTTON } from '../constants'; import { Errors } from '../creation_response_components'; -import { DocumentCreationLogic } from '../'; +import { DocumentCreationLogic } from '../index'; import './paste_json_text.scss'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/show_creation_modes.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/show_creation_modes.test.tsx index 2c66ae56dd3ce..739580d039a36 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/show_creation_modes.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/show_creation_modes.test.tsx @@ -8,10 +8,13 @@ import { setMockActions } from '../../../../__mocks__/kea.mock'; import React from 'react'; + import { shallow, ShallowWrapper } from 'enzyme'; + import { EuiButtonEmpty } from '@elastic/eui'; import { DocumentCreationButtons } from '../'; + import { ShowCreationModes } from './'; describe('ShowCreationModes', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/show_creation_modes.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/show_creation_modes.tsx index b67c7689d816f..d46b9acbb63d7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/show_creation_modes.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/show_creation_modes.tsx @@ -6,9 +6,9 @@ */ import React from 'react'; + import { useActions } from 'kea'; -import { i18n } from '@kbn/i18n'; import { EuiFlyoutHeader, EuiTitle, @@ -16,9 +16,10 @@ import { EuiFlyoutFooter, EuiButtonEmpty, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { FLYOUT_ARIA_LABEL_ID, FLYOUT_CANCEL_BUTTON } from '../constants'; -import { DocumentCreationLogic, DocumentCreationButtons } from '../'; +import { DocumentCreationLogic, DocumentCreationButtons } from '../index'; export const ShowCreationModes: React.FC = () => { const { closeDocumentCreation } = useActions(DocumentCreationLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/upload_json_file.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/upload_json_file.test.tsx index cee76ebe6857e..7dc8952a18688 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/upload_json_file.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/upload_json_file.test.tsx @@ -8,10 +8,13 @@ import { setMockValues, setMockActions, rerender } from '../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiFilePicker, EuiButtonEmpty, EuiButton } from '@elastic/eui'; import { Errors } from '../creation_response_components'; + import { UploadJsonFile, FlyoutHeader, FlyoutBody, FlyoutFooter } from './upload_json_file'; describe('UploadJsonFile', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/upload_json_file.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/upload_json_file.tsx index cab79a929f7b9..5d50ae55fcd10 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/upload_json_file.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/upload_json_file.tsx @@ -6,9 +6,9 @@ */ import React from 'react'; + import { useValues, useActions } from 'kea'; -import { i18n } from '@kbn/i18n'; import { EuiFlyoutHeader, EuiTitle, @@ -22,12 +22,13 @@ import { EuiSpacer, EuiText, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { AppLogic } from '../../../app_logic'; import { FLYOUT_ARIA_LABEL_ID, FLYOUT_CANCEL_BUTTON, FLYOUT_CONTINUE_BUTTON } from '../constants'; import { Errors } from '../creation_response_components'; -import { DocumentCreationLogic } from '../'; +import { DocumentCreationLogic } from '../index'; export const UploadJsonFile: React.FC = () => ( <> diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/errors.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/errors.test.tsx index 7ac97ae81b6ca..f03989aeaf5a3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/errors.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/errors.test.tsx @@ -8,7 +8,9 @@ import { setMockValues } from '../../../../__mocks__/kea.mock'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiCallOut } from '@elastic/eui'; import { Errors } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/errors.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/errors.tsx index 618828182e67d..3564d8ad088ee 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/errors.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/errors.tsx @@ -6,12 +6,13 @@ */ import React from 'react'; + import { useValues } from 'kea'; import { EuiCallOut } from '@elastic/eui'; import { DOCUMENT_CREATION_ERRORS, DOCUMENT_CREATION_WARNINGS } from '../constants'; -import { DocumentCreationLogic } from '../'; +import { DocumentCreationLogic } from '../index'; export const Errors: React.FC = () => { const { errors, warnings } = useValues(DocumentCreationLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary.test.tsx index 9558d23fa3a77..f53f94322879c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary.test.tsx @@ -8,15 +8,19 @@ import { setMockValues, setMockActions } from '../../../../__mocks__/kea.mock'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiFlyoutBody, EuiCallOut, EuiButton } from '@elastic/eui'; +import { FlyoutHeader, FlyoutBody, FlyoutFooter } from './summary'; import { InvalidDocumentsSummary, ValidDocumentsSummary, SchemaFieldsSummary, } from './summary_sections'; -import { Summary, FlyoutHeader, FlyoutBody, FlyoutFooter } from './summary'; + +import { Summary } from './'; describe('Summary', () => { const values = { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary.tsx index 673c6726afb5d..8361afe62e1ca 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary.tsx @@ -6,9 +6,9 @@ */ import React from 'react'; + import { useValues, useActions } from 'kea'; -import { i18n } from '@kbn/i18n'; import { EuiFlyoutHeader, EuiTitle, @@ -19,10 +19,11 @@ import { EuiFlexItem, EuiButton, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { DocumentCreationLogic } from '../'; import { FLYOUT_ARIA_LABEL_ID, FLYOUT_CLOSE_BUTTON, DOCUMENT_CREATION_ERRORS } from '../constants'; import { DocumentCreationStep } from '../types'; -import { DocumentCreationLogic } from '../'; import { InvalidDocumentsSummary, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_documents.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_documents.test.tsx index 0704d465bbac4..cd8209bafed3f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_documents.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_documents.test.tsx @@ -6,7 +6,9 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiCodeBlock, EuiCallOut } from '@elastic/eui'; import { ExampleDocumentJson, MoreDocumentsText } from './summary_documents'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_documents.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_documents.tsx index be19a7677a1ab..0dad75cb1f98f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_documents.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_documents.tsx @@ -7,8 +7,8 @@ import React, { Fragment } from 'react'; -import { i18n } from '@kbn/i18n'; import { EuiCodeBlock, EuiCallOut, EuiTitle, EuiText, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; interface ExampleDocumentJsonProps { document: object; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_section.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_section.test.tsx index 41028d61c55f2..24fa2766cb15d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_section.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_section.test.tsx @@ -6,7 +6,9 @@ */ import React, { ReactElement } from 'react'; + import { shallow } from 'enzyme'; + import { EuiAccordion, EuiIcon } from '@elastic/eui'; import { SummarySectionAccordion, SummarySectionEmpty } from './summary_section'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_sections.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_sections.test.tsx index 9ead42f33521f..7eb9f3f46036d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_sections.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_sections.test.tsx @@ -8,11 +8,13 @@ import { setMockValues } from '../../../../__mocks__/kea.mock'; import React from 'react'; + import { shallow } from 'enzyme'; import { EuiBadge } from '@elastic/eui'; -import { SummarySectionAccordion, SummarySectionEmpty } from './summary_section'; + import { ExampleDocumentJson, MoreDocumentsText } from './summary_documents'; +import { SummarySectionAccordion, SummarySectionEmpty } from './summary_section'; import { InvalidDocumentsSummary, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_sections.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_sections.tsx index 637188132d6bc..f2e863c2a9983 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_sections.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_response_components/summary_sections.tsx @@ -6,15 +6,16 @@ */ import React from 'react'; + import { useValues } from 'kea'; -import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem, EuiBadge } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { DocumentCreationLogic } from '../'; -import { SummarySectionAccordion, SummarySectionEmpty } from './summary_section'; import { ExampleDocumentJson, MoreDocumentsText } from './summary_documents'; +import { SummarySectionAccordion, SummarySectionEmpty } from './summary_section'; export const InvalidDocumentsSummary: React.FC = () => { const { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.test.tsx index 4b90acfbc37a8..7cbcc6b17e047 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.test.tsx @@ -9,8 +9,11 @@ import { setMockActions } from '../../../__mocks__/kea.mock'; import '../../__mocks__/engine_logic.mock'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiCard } from '@elastic/eui'; + import { EuiCardTo } from '../../../shared/react_router_helpers'; import { DocumentCreationButtons } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.tsx index ec9c6615f5b8c..6d3caca87dcc3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_buttons.tsx @@ -6,10 +6,9 @@ */ import React from 'react'; + import { useActions } from 'kea'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; import { EuiText, EuiCode, @@ -20,6 +19,8 @@ import { EuiCard, EuiIcon, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { EuiCardTo } from '../../../shared/react_router_helpers'; import { DOCS_PREFIX, ENGINE_CRAWLER_PATH } from '../../routes'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_flyout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_flyout.test.tsx index 4c5375d78f95f..66995b8d20dfe 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_flyout.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_flyout.test.tsx @@ -8,7 +8,9 @@ import { setMockValues, setMockActions } from '../../../__mocks__/kea.mock'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiFlyout } from '@elastic/eui'; import { @@ -18,9 +20,8 @@ import { UploadJsonFile, } from './creation_mode_components'; import { Summary } from './creation_response_components'; -import { DocumentCreationStep } from './types'; - import { DocumentCreationFlyout, FlyoutContent } from './document_creation_flyout'; +import { DocumentCreationStep } from './types'; describe('DocumentCreationFlyout', () => { const values = { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_flyout.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_flyout.tsx index 16f805d7e86fd..159f3403d3740 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_flyout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_flyout.tsx @@ -6,14 +6,12 @@ */ import React from 'react'; + import { useValues, useActions } from 'kea'; import { EuiPortal, EuiFlyout } from '@elastic/eui'; -import { DocumentCreationLogic } from './'; -import { DocumentCreationStep } from './types'; import { FLYOUT_ARIA_LABEL_ID } from './constants'; - import { ShowCreationModes, ApiCodeExample, @@ -21,6 +19,9 @@ import { UploadJsonFile, } from './creation_mode_components'; import { Summary } from './creation_response_components'; +import { DocumentCreationStep } from './types'; + +import { DocumentCreationLogic } from './'; export const DocumentCreationFlyout: React.FC = () => { const { closeDocumentCreation } = useActions(DocumentCreationLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.test.ts index 63c59343580d3..37d3d1577767f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.test.ts @@ -7,13 +7,9 @@ import { LogicMounter, mockHttpValues } from '../../../__mocks__'; -import { nextTick } from '@kbn/test/jest'; import dedent from 'dedent'; -jest.mock('./utils', () => ({ - readUploadedFileAsText: jest.fn(), -})); -import { readUploadedFileAsText } from './utils'; +import { nextTick } from '@kbn/test/jest'; jest.mock('../engine', () => ({ EngineLogic: { values: { engineName: 'test-engine' } }, @@ -21,6 +17,12 @@ jest.mock('../engine', () => ({ import { DOCUMENTS_API_JSON_EXAMPLE } from './constants'; import { DocumentCreationStep } from './types'; + +jest.mock('./utils', () => ({ + readUploadedFileAsText: jest.fn(), +})); +import { readUploadedFileAsText } from './utils'; + import { DocumentCreationLogic } from './'; describe('DocumentCreationLogic', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.ts index 13d2618bcd31f..a0ef73bbcea21 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { kea, MakeLogicType } from 'kea'; import dedent from 'dedent'; +import { kea, MakeLogicType } from 'kea'; import { isPlainObject, chunk, uniq } from 'lodash'; import { HttpLogic } from '../../../shared/http'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_creation_button.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_creation_button.test.tsx index ab1679c455c6e..82fa9d3c82ce9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_creation_button.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_creation_button.test.tsx @@ -8,10 +8,13 @@ import { setMockActions } from '../../../__mocks__/kea.mock'; import React from 'react'; + import { shallow, ShallowWrapper } from 'enzyme'; + import { EuiButton } from '@elastic/eui'; import { DocumentCreationFlyout } from '../document_creation'; + import { DocumentCreationButton } from './document_creation_button'; describe('DocumentCreationButton', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_creation_button.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_creation_button.tsx index a05005fefa082..687f589d37594 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_creation_button.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_creation_button.tsx @@ -6,10 +6,11 @@ */ import React from 'react'; + import { useActions } from 'kea'; -import { i18n } from '@kbn/i18n'; import { EuiButton } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { DocumentCreationLogic, DocumentCreationFlyout } from '../document_creation'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.test.tsx index 55613077efdba..ba060b7497270 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.test.tsx @@ -10,14 +10,17 @@ import { setMockValues, setMockActions } from '../../../__mocks__/kea.mock'; import { unmountHandler } from '../../../__mocks__/shallow_useeffect.mock'; import React from 'react'; -import { shallow } from 'enzyme'; import { useParams } from 'react-router-dom'; + +import { shallow } from 'enzyme'; + import { EuiPageContent, EuiBasicTable } from '@elastic/eui'; import { Loading } from '../../../shared/loading'; -import { DocumentDetail } from '.'; import { ResultFieldValue } from '../result'; +import { DocumentDetail } from '.'; + describe('DocumentDetail', () => { const values = { dataLoading: false, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.tsx index ca6af345de7ed..8f80978c29002 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail.tsx @@ -6,9 +6,10 @@ */ import React, { useEffect } from 'react'; -import { useActions, useValues } from 'kea'; import { useParams } from 'react-router-dom'; +import { useActions, useValues } from 'kea'; + import { EuiButton, EuiPageHeader, @@ -21,15 +22,15 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { Loading } from '../../../shared/loading'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { FlashMessages } from '../../../shared/flash_messages'; +import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { Loading } from '../../../shared/loading'; import { useDecodedParams } from '../../utils/encode_path_params'; import { ResultFieldValue } from '../result'; +import { DOCUMENTS_TITLE } from './constants'; import { DocumentDetailLogic } from './document_detail_logic'; import { FieldDetails } from './types'; -import { DOCUMENTS_TITLE } from './constants'; const DOCUMENT_DETAIL_TITLE = (documentId: string) => i18n.translate('xpack.enterpriseSearch.appSearch.documentDetail.title', { 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 ef5ebad3aea13..d2683fac649a0 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 @@ -15,9 +15,10 @@ import { mockEngineValues } from '../../__mocks__'; import { nextTick } from '@kbn/test/jest'; -import { DocumentDetailLogic } from './document_detail_logic'; import { InternalSchemaTypes } from '../../../shared/types'; +import { DocumentDetailLogic } from './document_detail_logic'; + describe('DocumentDetailLogic', () => { const { mount } = new LogicMounter(DocumentDetailLogic); const { http } = mockHttpValues; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.ts index 8b023fb585f86..17c2c788523d0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/document_detail_logic.ts @@ -6,11 +6,12 @@ */ import { kea, MakeLogicType } from 'kea'; + import { i18n } from '@kbn/i18n'; import { flashAPIErrors, setQueuedSuccessMessage } from '../../../shared/flash_messages'; -import { KibanaLogic } from '../../../shared/kibana'; import { HttpLogic } from '../../../shared/http'; +import { KibanaLogic } from '../../../shared/kibana'; import { ENGINE_DOCUMENTS_PATH } from '../../routes'; import { EngineLogic, generateEnginePath } from '../engine'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.test.tsx index 43bbc6cc67895..ace76ae55c046 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.test.tsx @@ -8,10 +8,12 @@ import { setMockValues } from '../../../__mocks__/kea.mock'; import React from 'react'; + import { shallow } from 'enzyme'; import { DocumentCreationButton } from './document_creation_button'; import { SearchExperience } from './search_experience'; + import { Documents } from '.'; describe('Documents', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.tsx index 7223900911512..8c3ae7fd24f6d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/documents.tsx @@ -7,16 +7,19 @@ import React from 'react'; -import { EuiPageHeader, EuiPageHeaderSection, EuiTitle, EuiCallOut, EuiSpacer } from '@elastic/eui'; import { useValues } from 'kea'; + +import { EuiPageHeader, EuiPageHeaderSection, EuiTitle, EuiCallOut, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { DocumentCreationButton } from './document_creation_button'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { FlashMessages } from '../../../shared/flash_messages'; -import { DOCUMENTS_TITLE } from './constants'; -import { EngineLogic } from '../engine'; +import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; + import { AppLogic } from '../../app_logic'; +import { EngineLogic } from '../engine'; + +import { DOCUMENTS_TITLE } from './constants'; +import { DocumentCreationButton } from './document_creation_button'; import { SearchExperience } from './search_experience'; interface Props { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_search_ui_config.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_search_ui_config.ts index b9577d9d0f07d..9fac068555db5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_search_ui_config.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_search_ui_config.ts @@ -6,6 +6,7 @@ */ import { Schema } from '../../../../shared/types'; + import { Fields } from './types'; export const buildSearchUIConfig = (apiConnector: object, schema: Schema, fields: Fields) => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_sort_options.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_sort_options.ts index ab3a943ef2f55..54cf2bdd4f257 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_sort_options.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/build_sort_options.ts @@ -7,8 +7,8 @@ import { flatten } from 'lodash'; -import { Fields, SortOption, SortDirection } from './types'; import { ASCENDING, DESCENDING } from './constants'; +import { Fields, SortOption, SortDirection } from './types'; const fieldNameToSortOptions = (fieldName: string): SortOption[] => ['asc', 'desc'].map((direction) => ({ diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_callout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_callout.test.tsx index e62e4521927dc..6ed2d7edc9639 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_callout.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_callout.test.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; + import { EuiButton } from '@elastic/eui'; import { CustomizationCallout } from './customization_callout'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_callout.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_callout.tsx index 8954549f74651..48a9fcdeaa878 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_callout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_callout.tsx @@ -6,9 +6,9 @@ */ import React from 'react'; -import { i18n } from '@kbn/i18n'; import { EuiButton, EuiFlexGroup, EuiIcon, EuiSpacer, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; interface Props { onClick(): void; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_modal.test.tsx index 11e13f4222abb..332c5b822eb6d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_modal.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_modal.test.tsx @@ -8,7 +8,9 @@ import { setMockValues, setMockActions } from '../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiButton, EuiButtonEmpty } from '@elastic/eui'; import { CustomizationModal } from './customization_modal'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_modal.tsx index 2d3604b2ba279..e05fc10053ff1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_modal.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/customization_modal.tsx @@ -7,6 +7,8 @@ import React, { useState, useMemo } from 'react'; +import { useValues } from 'kea'; + import { EuiButton, EuiButtonEmpty, @@ -21,7 +23,6 @@ import { EuiOverlayMask, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useValues } from 'kea'; import { EngineLogic } from '../../engine'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/hooks.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/hooks.test.tsx index aecb4cc154117..028a9af21311f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/hooks.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/hooks.test.tsx @@ -25,8 +25,9 @@ jest.mock('react', () => ({ })); import React from 'react'; -import { act } from 'react-dom/test-utils'; + import { mount, ReactWrapper } from 'enzyme'; +import { act } from 'react-dom/test-utils'; import { useSearchContextState, useSearchContextActions } from './hooks'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/pagination.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/pagination.test.tsx index 5fe47d5942ab8..b55163ca9843a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/pagination.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/pagination.test.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { shallow } from 'enzyme'; + // @ts-expect-error types are not available for this package yet import { Paging, ResultsPerPage } from '@elastic/react-search-ui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/pagination.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/pagination.tsx index 846671c62de82..d81b056842642 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/pagination.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/pagination.tsx @@ -10,6 +10,7 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; // @ts-expect-error types are not available for this package yet import { Paging, ResultsPerPage } from '@elastic/react-search-ui'; + import { PagingView, ResultsPerPageView } from './views'; export const Pagination: React.FC<{ 'aria-label': string }> = ({ 'aria-label': ariaLabel }) => ( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.test.tsx index b0dccf0583e2f..bfa5c8264fece 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.test.tsx @@ -8,21 +8,24 @@ import '../../../../__mocks__/enterprise_search_url.mock'; import { setMockValues } from '../../../../__mocks__'; +import React from 'react'; + +import { shallow, ShallowWrapper } from 'enzyme'; + +// @ts-expect-error types are not available for this package yet +import { SearchProvider, Facet } from '@elastic/react-search-ui'; + jest.mock('../../../../shared/use_local_storage', () => ({ useLocalStorage: jest.fn(), })); import { useLocalStorage } from '../../../../shared/use_local_storage'; -import React from 'react'; -// @ts-expect-error types are not available for this package yet -import { SearchProvider, Facet } from '@elastic/react-search-ui'; -import { shallow, ShallowWrapper } from 'enzyme'; - import { CustomizationCallout } from './customization_callout'; import { CustomizationModal } from './customization_modal'; + import { Fields } from './types'; -import { SearchExperience } from './search_experience'; +import { SearchExperience } from './'; describe('SearchExperience', () => { const values = { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.tsx index 6ae4f264d7c74..6fbc6305edb25 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience.tsx @@ -7,13 +7,14 @@ import React, { useState } from 'react'; -import { i18n } from '@kbn/i18n'; import { useValues } from 'kea'; + import { EuiButton, EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; // @ts-expect-error types are not available for this package yet; import { SearchProvider, SearchBox, Sorting, Facet } from '@elastic/react-search-ui'; // @ts-expect-error types are not available for this package yet import AppSearchAPIConnector from '@elastic/search-ui-app-search-connector'; +import { i18n } from '@kbn/i18n'; import './search_experience.scss'; @@ -21,14 +22,14 @@ import { externalUrl } from '../../../../shared/enterprise_search_url'; import { useLocalStorage } from '../../../../shared/use_local_storage'; import { EngineLogic } from '../../engine'; -import { Fields, SortOption } from './types'; -import { SearchBoxView, SortingView, MultiCheckboxFacetsView } from './views'; -import { SearchExperienceContent } from './search_experience_content'; import { buildSearchUIConfig } from './build_search_ui_config'; -import { CustomizationCallout } from './customization_callout'; -import { CustomizationModal } from './customization_modal'; import { buildSortOptions } from './build_sort_options'; import { ASCENDING, DESCENDING } from './constants'; +import { CustomizationCallout } from './customization_callout'; +import { CustomizationModal } from './customization_modal'; +import { SearchExperienceContent } from './search_experience_content'; +import { Fields, SortOption } from './types'; +import { SearchBoxView, SortingView, MultiCheckboxFacetsView } from './views'; const RECENTLY_UPLOADED = i18n.translate( 'xpack.enterpriseSearch.appSearch.documents.search.sortBy.option.recentlyUploaded', diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.test.tsx index 737e3ea1b2999..49f51c2010e3a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.test.tsx @@ -6,18 +6,20 @@ */ import { setMockValues } from '../../../../__mocks__/kea.mock'; -import { setMockSearchContextState } from './__mocks__/hooks.mock'; import React from 'react'; import { shallow, mount } from 'enzyme'; + // @ts-expect-error types are not available for this package yet import { Results } from '@elastic/react-search-ui'; -import { ResultView } from './views'; -import { Pagination } from './pagination'; import { SchemaTypes } from '../../../../shared/types'; + +import { setMockSearchContextState } from './__mocks__/hooks.mock'; +import { Pagination } from './pagination'; import { SearchExperienceContent } from './search_experience_content'; +import { ResultView } from './views'; describe('SearchExperienceContent', () => { const searchState = { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.tsx index 45c20d8ffce2c..91db26ac676c9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/search_experience_content.tsx @@ -7,20 +7,22 @@ import React from 'react'; -import { i18n } from '@kbn/i18n'; +import { useValues } from 'kea'; + import { EuiFlexGroup, EuiSpacer, EuiButton, EuiEmptyPrompt } from '@elastic/eui'; // @ts-expect-error types are not available for this package yet import { Results, Paging, ResultsPerPage } from '@elastic/react-search-ui'; -import { useValues } from 'kea'; +import { i18n } from '@kbn/i18n'; -import { ResultView } from './views'; -import { Pagination } from './pagination'; -import { useSearchContextState } from './hooks'; -import { DocumentCreationButton } from '../document_creation_button'; import { AppLogic } from '../../../app_logic'; -import { EngineLogic } from '../../engine'; import { DOCS_PREFIX } from '../../../routes'; +import { EngineLogic } from '../../engine'; import { Result } from '../../result/types'; +import { DocumentCreationButton } from '../document_creation_button'; + +import { useSearchContextState } from './hooks'; +import { Pagination } from './pagination'; +import { ResultView } from './views'; export const SearchExperienceContent: React.FC = () => { const { resultSearchTerm, totalResults, wasSearched } = useSearchContextState(); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/paging_view.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/paging_view.test.tsx index 03b9e33f89fef..28cd126e5c004 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/paging_view.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/paging_view.test.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { shallow } from 'enzyme'; + import { EuiPagination } from '@elastic/eui'; import { PagingView } from './paging_view'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.test.tsx index e06603894c288..24685aef71078 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.test.tsx @@ -9,10 +9,11 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { ResultView } from '.'; import { SchemaTypes } from '../../../../../shared/types'; import { Result } from '../../../result/result'; +import { ResultView } from '.'; + describe('ResultView', () => { const result = { id: { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.tsx index 9dd3fcea5f754..b133780310a4c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/result_view.tsx @@ -7,9 +7,9 @@ import React from 'react'; -import { Result as ResultType } from '../../../result/types'; import { Schema } from '../../../../../shared/types'; import { Result } from '../../../result/result'; +import { Result as ResultType } from '../../../result/types'; export interface Props { result: ResultType; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/results_per_page_view.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/results_per_page_view.test.tsx index 70e4d7e4e1878..24db762e26e32 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/results_per_page_view.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/results_per_page_view.test.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { shallow } from 'enzyme'; + import { EuiSelect } from '@elastic/eui'; import { ResultsPerPageView } from '.'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/results_per_page_view.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/results_per_page_view.tsx index b57944042e67f..5056d56d1f3d0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/results_per_page_view.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/results_per_page_view.tsx @@ -7,8 +7,8 @@ import React from 'react'; -import { i18n } from '@kbn/i18n'; import { EuiSelect, EuiSelectOption } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; const wrapResultsPerPageOptionForEuiSelect: (option: number) => EuiSelectOption = (option) => ({ text: option, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/search_box_view.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/search_box_view.test.tsx index 182e2ea222f90..a35fcefb30ac6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/search_box_view.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/search_box_view.test.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { shallow } from 'enzyme'; + import { EuiFieldSearch } from '@elastic/eui'; import { SearchBoxView } from './search_box_view'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/sorting_view.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/sorting_view.test.tsx index 4f7317a2bf5d0..a147f45feef14 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/sorting_view.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/sorting_view.test.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { shallow } from 'enzyme'; + import { EuiSelect } from '@elastic/eui'; import { SortingView } from '.'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/sorting_view.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/sorting_view.tsx index 047caf6ca1e3b..e3f21b67a6530 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/sorting_view.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/documents/search_experience/views/sorting_view.tsx @@ -7,8 +7,8 @@ import React from 'react'; -import { i18n } from '@kbn/i18n'; import { EuiSelect, EuiSelectOption } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; interface Option { label: string; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.ts index fbe08cbeb939f..664a3006cfa2c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_logic.ts @@ -10,6 +10,7 @@ import { kea, MakeLogicType } from 'kea'; import { HttpLogic } from '../../../shared/http'; import { IIndexingStatus } from '../../../shared/types'; + import { EngineDetails } from './types'; interface EngineValues { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.test.tsx index 8ed36ad5ab006..1781883aa6532 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.test.tsx @@ -9,7 +9,9 @@ import { setMockValues, rerender } from '../../../__mocks__'; import { mockEngineValues } from '../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiBadge, EuiIcon } from '@elastic/eui'; import { EngineNav } from './engine_nav'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx index b1b31c245eb99..447e4d678bcdb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx @@ -6,11 +6,13 @@ */ import React from 'react'; + import { useValues } from 'kea'; import { EuiText, EuiBadge, EuiIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { getAppSearchUrl } from '../../../shared/enterprise_search_url'; import { SideNavLink, SideNavItem } from '../../../shared/layout'; import { AppLogic } from '../../app_logic'; import { @@ -27,23 +29,23 @@ import { ENGINE_SEARCH_UI_PATH, ENGINE_API_LOGS_PATH, } from '../../routes'; -import { getAppSearchUrl } from '../../../shared/enterprise_search_url'; -import { ENGINES_TITLE } from '../engines'; -import { OVERVIEW_TITLE } from '../engine_overview'; import { ANALYTICS_TITLE } from '../analytics'; -import { DOCUMENTS_TITLE } from '../documents'; -import { SCHEMA_TITLE } from '../schema'; +import { API_LOGS_TITLE } from '../api_logs'; import { CRAWLER_TITLE } from '../crawler'; -import { RELEVANCE_TUNING_TITLE } from '../relevance_tuning'; -import { SYNONYMS_TITLE } from '../synonyms'; import { CURATIONS_TITLE } from '../curations'; +import { DOCUMENTS_TITLE } from '../documents'; +import { OVERVIEW_TITLE } from '../engine_overview'; +import { ENGINES_TITLE } from '../engines'; +import { RELEVANCE_TUNING_TITLE } from '../relevance_tuning'; import { RESULT_SETTINGS_TITLE } from '../result_settings'; +import { SCHEMA_TITLE } from '../schema'; import { SEARCH_UI_TITLE } from '../search_ui'; -import { API_LOGS_TITLE } from '../api_logs'; +import { SYNONYMS_TITLE } from '../synonyms'; -import { EngineLogic, generateEnginePath } from './'; import { EngineDetails } from './types'; +import { EngineLogic, generateEnginePath } from './'; + import './engine_nav.scss'; export const EngineNav: React.FC = () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx index cff05b296846b..3740882dee3db 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx @@ -6,17 +6,18 @@ */ import '../../../__mocks__/react_router_history.mock'; -import { unmountHandler } from '../../../__mocks__/shallow_useeffect.mock'; import { mockFlashMessageHelpers, setMockValues, setMockActions } from '../../../__mocks__'; +import { unmountHandler } from '../../../__mocks__/shallow_useeffect.mock'; import { mockEngineValues } from '../../__mocks__'; import React from 'react'; -import { shallow } from 'enzyme'; import { Switch, Redirect, useParams } from 'react-router-dom'; +import { shallow } from 'enzyme'; + import { Loading } from '../../../shared/loading'; -import { EngineOverview } from '../engine_overview'; import { AnalyticsRouter } from '../analytics'; +import { EngineOverview } from '../engine_overview'; import { RelevanceTuning } from '../relevance_tuning'; import { EngineRouter } from './engine_router'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx index 257bb1e69ad7f..2f1c3bc57d331 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx @@ -7,12 +7,14 @@ import React, { useEffect } from 'react'; import { Route, Switch, Redirect, useParams } from 'react-router-dom'; + import { useValues, useActions } from 'kea'; import { i18n } from '@kbn/i18n'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { setQueuedErrorMessage } from '../../../shared/flash_messages'; +import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { Loading } from '../../../shared/loading'; import { AppLogic } from '../../app_logic'; // TODO: Uncomment and add more routes as we migrate them @@ -31,13 +33,11 @@ import { // ENGINE_SEARCH_UI_PATH, // ENGINE_API_LOGS_PATH, } from '../../routes'; -import { ENGINES_TITLE } from '../engines'; -import { OVERVIEW_TITLE } from '../engine_overview'; - -import { Loading } from '../../../shared/loading'; -import { EngineOverview } from '../engine_overview'; import { AnalyticsRouter } from '../analytics'; import { DocumentDetail, Documents } from '../documents'; +import { OVERVIEW_TITLE } from '../engine_overview'; +import { EngineOverview } from '../engine_overview'; +import { ENGINES_TITLE } from '../engines'; import { RelevanceTuning } from '../relevance_tuning'; import { EngineLogic } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/types.ts index d20f9890cd4db..b50e8eb555dc9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/types.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { ApiToken } from '../credentials/types'; import { Schema, SchemaConflicts, IIndexingStatus } from '../../../shared/types'; +import { ApiToken } from '../credentials/types'; export interface Engine { name: string; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.test.tsx index 7b52a04d07958..42fa9777563db 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.test.tsx @@ -8,6 +8,7 @@ import '../../../__mocks__/engine_logic.mock'; import React from 'react'; + import { shallow, ShallowWrapper } from 'enzyme'; import { EuiButtonTo } from '../../../../shared/react_router_helpers'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.tsx index d7290533f4f7b..625ba2e905840 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/recent_api_logs.tsx @@ -17,9 +17,9 @@ import { import { EuiButtonTo } from '../../../../shared/react_router_helpers'; import { ENGINE_API_LOGS_PATH } from '../../../routes'; +import { RECENT_API_EVENTS } from '../../api_logs/constants'; import { generateEnginePath } from '../../engine'; -import { RECENT_API_EVENTS } from '../../api_logs/constants'; import { VIEW_API_LOGS } from '../constants'; export const RecentApiLogs: React.FC = () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.test.tsx index 867b78f859a22..a2f35b4709939 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.test.tsx @@ -9,6 +9,7 @@ import { setMockValues } from '../../../../__mocks__/kea.mock'; import '../../../__mocks__/engine_logic.mock'; import React from 'react'; + import { shallow, ShallowWrapper } from 'enzyme'; import { EuiButtonTo } from '../../../../shared/react_router_helpers'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.tsx index 4fa2246ee6170..6bd973ae142a8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_charts.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { useValues } from 'kea'; import { @@ -21,12 +22,12 @@ import { import { EuiButtonTo } from '../../../../shared/react_router_helpers'; import { ENGINE_ANALYTICS_PATH, ENGINE_API_LOGS_PATH } from '../../../routes'; +import { AnalyticsChart, convertToChartData } from '../../analytics'; +import { TOTAL_QUERIES, TOTAL_API_OPERATIONS } from '../../analytics/constants'; import { generateEnginePath } from '../../engine'; -import { TOTAL_QUERIES, TOTAL_API_OPERATIONS } from '../../analytics/constants'; import { VIEW_ANALYTICS, VIEW_API_LOGS, LAST_7_DAYS } from '../constants'; -import { AnalyticsChart, convertToChartData } from '../../analytics'; -import { EngineOverviewLogic } from '../'; +import { EngineOverviewLogic } from '../index'; export const TotalCharts: React.FC = () => { const { startDate, queriesPerDay, operationsPerDay } = useValues(EngineOverviewLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_stats.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_stats.test.tsx index a897c635eeadd..7fcda61073c5b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_stats.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_stats.test.tsx @@ -8,9 +8,11 @@ import { setMockValues } from '../../../../__mocks__/kea.mock'; import React from 'react'; + import { shallow } from 'enzyme'; import { AnalyticsCards } from '../../analytics'; + import { TotalStats } from './total_stats'; describe('TotalStats', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_stats.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_stats.tsx index 3eb208fa86504..35c6fa439a416 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_stats.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/total_stats.tsx @@ -6,12 +6,12 @@ */ import React from 'react'; + import { useValues } from 'kea'; -import { TOTAL_QUERIES, TOTAL_DOCUMENTS, TOTAL_CLICKS } from '../../analytics/constants'; import { AnalyticsCards } from '../../analytics'; - -import { EngineOverviewLogic } from '../'; +import { TOTAL_QUERIES, TOTAL_DOCUMENTS, TOTAL_CLICKS } from '../../analytics/constants'; +import { EngineOverviewLogic } from '../index'; export const TotalStats: React.FC = () => { const { totalQueries, documentCount, totalClicks } = useValues(EngineOverviewLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/unavailable_prompt.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/unavailable_prompt.test.tsx index 7cd042a646e73..4c61a713b3793 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/unavailable_prompt.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/unavailable_prompt.test.tsx @@ -6,7 +6,9 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiEmptyPrompt } from '@elastic/eui'; import { UnavailablePrompt } from './unavailable_prompt'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/unavailable_prompt.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/unavailable_prompt.tsx index 2916be92ead99..69e79ecfc580d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/unavailable_prompt.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/components/unavailable_prompt.tsx @@ -7,8 +7,8 @@ import React from 'react'; -import { i18n } from '@kbn/i18n'; import { EuiEmptyPrompt } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; export const UnavailablePrompt: React.FC = () => ( { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx index 9e673d48a7e5b..77552b36af239 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview.tsx @@ -6,16 +6,19 @@ */ import React, { useEffect } from 'react'; + import { useActions, useValues } from 'kea'; +import { Loading } from '../../../shared/loading'; import { AppLogic } from '../../app_logic'; import { EngineLogic } from '../engine'; -import { Loading } from '../../../shared/loading'; -import { EngineOverviewLogic } from './'; import { EmptyEngineOverview } from './engine_overview_empty'; + import { EngineOverviewMetrics } from './engine_overview_metrics'; +import { EngineOverviewLogic } from './'; + export const EngineOverview: React.FC = () => { const { myRole: { canManageEngineDocuments, canViewEngineCredentials }, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.test.tsx index 5947618e59c16..9066283229a04 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.test.tsx @@ -6,12 +6,15 @@ */ import React from 'react'; + import { shallow, ShallowWrapper } from 'enzyme'; + import { EuiButton } from '@elastic/eui'; import { docLinks } from '../../../shared/doc_links'; import { DocumentCreationButtons, DocumentCreationFlyout } from '../document_creation'; + import { EmptyEngineOverview } from './engine_overview_empty'; describe('EmptyEngineOverview', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.tsx index 6a0c46286907d..81bf3716edfb8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.tsx @@ -7,7 +7,6 @@ import React from 'react'; -import { i18n } from '@kbn/i18n'; import { EuiPageHeader, EuiPageHeaderSection, @@ -15,6 +14,7 @@ import { EuiTitle, EuiButton, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { DOCS_PREFIX } from '../../routes'; import { DocumentCreationButtons, DocumentCreationFlyout } from '../document_creation'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.test.tsx index ebcdbaf1f7f09..638c8b0da87ce 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.test.tsx @@ -8,6 +8,7 @@ import { setMockValues } from '../../../__mocks__/kea.mock'; import React from 'react'; + import { shallow } from 'enzyme'; import { UnavailablePrompt, TotalStats, TotalCharts, RecentApiLogs } from './components'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.tsx index ffb1a25d21cae..34a154ca83741 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.tsx @@ -6,15 +6,16 @@ */ import React from 'react'; + import { useValues } from 'kea'; -import { i18n } from '@kbn/i18n'; import { EuiPageHeader, EuiTitle, EuiSpacer } from '@elastic/eui'; - -import { EngineOverviewLogic } from './'; +import { i18n } from '@kbn/i18n'; import { UnavailablePrompt, TotalStats, TotalCharts, RecentApiLogs } from './components'; +import { EngineOverviewLogic } from './'; + export const EngineOverviewMetrics: React.FC = () => { const { apiLogsUnavailable } = useValues(EngineOverviewLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/assets/icons.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/assets/icons.test.tsx index 9c2818f9907a4..33ca5bd8248c9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/assets/icons.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/assets/icons.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { EngineIcon } from './engine_icon'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.test.tsx index b04226b6b1dfb..ac540eec3ff91 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.test.tsx @@ -9,7 +9,9 @@ import '../../../../__mocks__/kea.mock'; import { mockTelemetryActions } from '../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiEmptyPrompt, EuiButton } from '@elastic/eui'; import { EmptyState } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.tsx index 60a454a5707c9..5419a175c9eff 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/empty_state.tsx @@ -6,13 +6,15 @@ */ import React from 'react'; + import { useActions } from 'kea'; + import { EuiPageContent, EuiEmptyPrompt, EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { TelemetryLogic } from '../../../../shared/telemetry'; import { getAppSearchUrl } from '../../../../shared/enterprise_search_url'; import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome'; +import { TelemetryLogic } from '../../../../shared/telemetry'; import { CREATE_ENGINES_PATH } from '../../../routes'; import { EnginesOverviewHeader } from './header'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.test.tsx index 6dedb90690ace..5ccd2c552ef02 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.test.tsx @@ -10,6 +10,7 @@ import '../../../../__mocks__/enterprise_search_url.mock'; import { mockTelemetryActions } from '../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; import { EnginesOverviewHeader } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.tsx index 8a8227821b492..290270c08258c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/header.tsx @@ -6,7 +6,9 @@ */ import React from 'react'; + import { useActions } from 'kea'; + import { EuiPageHeader, EuiPageHeaderSection, @@ -17,8 +19,8 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { TelemetryLogic } from '../../../../shared/telemetry'; import { getAppSearchUrl } from '../../../../shared/enterprise_search_url'; +import { TelemetryLogic } from '../../../../shared/telemetry'; export const EnginesOverviewHeader: React.FC = () => { const { sendAppSearchTelemetry } = useActions(TelemetryLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/loading_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/loading_state.test.tsx index 4adc8c11fa0dd..f7ccfea4bb4d4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/loading_state.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/loading_state.test.tsx @@ -6,7 +6,9 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiLoadingContent } from '@elastic/eui'; import { LoadingState } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/loading_state.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/loading_state.tsx index 48160602106cd..155d8263c484d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/loading_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/components/loading_state.tsx @@ -6,9 +6,11 @@ */ import React from 'react'; + import { EuiPageContent, EuiSpacer, EuiLoadingContent } from '@elastic/eui'; import { SetAppSearchChrome as SetPageChrome } from '../../../../shared/kibana_chrome'; + import { EnginesOverviewHeader } from './header'; export const LoadingState: React.FC = () => { 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 c25f60e47598e..9e9bfc4973124 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 @@ -10,6 +10,7 @@ import { LogicMounter, mockHttpValues } from '../../../__mocks__'; import { nextTick } from '@kbn/test/jest'; import { EngineDetails } from '../engine/types'; + import { EnginesLogic } from './'; describe('EnginesLogic', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.test.tsx index f4aeb60a88250..cdc06dbbe3921 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.test.tsx @@ -9,6 +9,7 @@ import '../../../__mocks__/shallow_useeffect.mock'; import { setMockValues, setMockActions, rerender } from '../../../__mocks__'; import React from 'react'; + import { shallow, ShallowWrapper } from 'enzyme'; import { LoadingState, EmptyState } from './components'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx index c13db688fc2b6..2835c8b7cb3c4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx @@ -6,7 +6,9 @@ */ import React, { useEffect } from 'react'; + import { useValues, useActions } from 'kea'; + import { EuiPageContent, EuiPageContentHeader, @@ -15,17 +17,17 @@ import { EuiSpacer, } from '@elastic/eui'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; -import { SendAppSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; import { FlashMessages } from '../../../shared/flash_messages'; +import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { LicensingLogic } from '../../../shared/licensing'; +import { SendAppSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; import { EngineIcon } from './assets/engine_icon'; import { MetaEngineIcon } from './assets/meta_engine_icon'; -import { ENGINES_TITLE, META_ENGINES_TITLE } from './constants'; import { EnginesOverviewHeader, LoadingState, EmptyState } from './components'; -import { EnginesTable } from './engines_table'; +import { ENGINES_TITLE, META_ENGINES_TITLE } from './constants'; import { EnginesLogic } from './engines_logic'; +import { EnginesTable } from './engines_table'; import './engines_overview.scss'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.test.tsx index 65b96035eaaee..d6f0946164ea4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.test.tsx @@ -9,10 +9,13 @@ import '../../../__mocks__/enterprise_search_url.mock'; import { mockTelemetryActions, mountWithIntl } from '../../../__mocks__'; import React from 'react'; + import { EuiBasicTable, EuiPagination, EuiButtonEmpty } from '@elastic/eui'; + import { EuiLinkTo } from '../../../shared/react_router_helpers'; import { EngineDetails } from '../engine/types'; + import { EnginesTable } from './engines_table'; describe('EnginesTable', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.tsx index b439d7e6bdf33..d41c5c908c08f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_table.tsx @@ -6,18 +6,19 @@ */ import React from 'react'; + import { useActions } from 'kea'; + import { EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui'; -import { FormattedMessage, FormattedDate, FormattedNumber } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; - -import { TelemetryLogic } from '../../../shared/telemetry'; -import { EuiLinkTo } from '../../../shared/react_router_helpers'; -import { generateEncodedPath } from '../../utils/encode_path_params'; -import { ENGINE_PATH } from '../../routes'; +import { FormattedMessage, FormattedDate, FormattedNumber } from '@kbn/i18n/react'; import { ENGINES_PAGE_SIZE } from '../../../../../common/constants'; +import { EuiLinkTo } from '../../../shared/react_router_helpers'; +import { TelemetryLogic } from '../../../shared/telemetry'; import { UNIVERSAL_LANGUAGE } from '../../constants'; +import { ENGINE_PATH } from '../../routes'; +import { generateEncodedPath } from '../../utils/encode_path_params'; import { EngineDetails } from '../engine/types'; interface EnginesTablePagination { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/error_connecting/error_connecting.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/error_connecting/error_connecting.test.tsx index 6ff33385df9a5..9ec3fdda63656 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/error_connecting/error_connecting.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/error_connecting/error_connecting.test.tsx @@ -6,9 +6,11 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { ErrorStatePrompt } from '../../../shared/error_state'; + import { ErrorConnecting } from './'; describe('ErrorConnecting', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/error_connecting/error_connecting.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/error_connecting/error_connecting.tsx index ad5eff6c4dacf..d7fde0cd5dd25 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/error_connecting/error_connecting.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/error_connecting/error_connecting.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { EuiPageContent } from '@elastic/eui'; import { ErrorStatePrompt } from '../../../shared/error_state'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx index 2d39b5a9aa05c..f76ad78c847d1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/library/library.tsx @@ -5,6 +5,8 @@ * 2.0. */ +import React from 'react'; + import { EuiSpacer, EuiPageHeader, @@ -13,7 +15,6 @@ import { EuiPageContentBody, EuiPageContent, } from '@elastic/eui'; -import React from 'react'; import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { Schema } from '../../../shared/types'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_callout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_callout.test.tsx index d0bc1c9a88c5f..124edb6871453 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_callout.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_callout.test.tsx @@ -6,14 +6,16 @@ */ import '../../../../__mocks__/shallow_useeffect.mock'; -import { setMockValues, setMockActions } from '../../../../__mocks__/kea.mock'; -import { mountWithIntl } from '../../../../__mocks__'; +import { setMockValues, setMockActions, mountWithIntl } from '../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiCallOut, EuiLink } from '@elastic/eui'; import { LogRetentionOptions } from '../'; + import { LogRetentionCallout } from './'; describe('LogRetentionCallout', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_callout.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_callout.tsx index 0252a788f75ef..235d977793161 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_callout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_callout.tsx @@ -6,11 +6,12 @@ */ import React, { useEffect } from 'react'; + import { useValues, useActions } from 'kea'; +import { EuiCallOut, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiCallOut, EuiSpacer } from '@elastic/eui'; import { EuiLinkTo } from '../../../../shared/react_router_helpers'; @@ -19,7 +20,7 @@ import { SETTINGS_PATH } from '../../../routes'; import { ANALYTICS_TITLE } from '../../analytics'; import { API_LOGS_TITLE } from '../../api_logs'; -import { LogRetentionLogic, LogRetentionOptions, renderLogRetentionDate } from '../'; +import { LogRetentionLogic, LogRetentionOptions, renderLogRetentionDate } from '../index'; const TITLE_MAP = { [LogRetentionOptions.Analytics]: ANALYTICS_TITLE, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_tooltip.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_tooltip.test.tsx index 14615f6ac2dd9..854a9f1d8d162 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_tooltip.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_tooltip.test.tsx @@ -9,10 +9,13 @@ import '../../../../__mocks__/shallow_useeffect.mock'; import { setMockValues, setMockActions } from '../../../../__mocks__/kea.mock'; import React from 'react'; + import { shallow, mount } from 'enzyme'; + import { EuiIconTip } from '@elastic/eui'; import { LogRetentionOptions, LogRetentionMessage } from '../'; + import { LogRetentionTooltip } from './'; describe('LogRetentionTooltip', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_tooltip.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_tooltip.tsx index e3b428baa6d9a..bf074ba0272f2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_tooltip.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_tooltip.tsx @@ -6,10 +6,11 @@ */ import React, { useEffect } from 'react'; + import { useValues, useActions } from 'kea'; -import { i18n } from '@kbn/i18n'; import { EuiIconTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { LogRetentionLogic, LogRetentionMessage, LogRetentionOptions } from '../'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/log_retention_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/log_retention_logic.test.ts index 9615aba5fdef4..19bd2af50aad9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/log_retention_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/log_retention_logic.test.ts @@ -10,7 +10,8 @@ import { LogicMounter, mockHttpValues, mockFlashMessageHelpers } from '../../../ import { nextTick } from '@kbn/test/jest'; import { LogRetentionOptions } from './types'; -import { LogRetentionLogic } from './log_retention_logic'; + +import { LogRetentionLogic } from './'; describe('LogRetentionLogic', () => { const { mount } = new LogicMounter(LogRetentionLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/log_retention_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/log_retention_logic.ts index 77d4cf395196a..ec078842dab55 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/log_retention_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/log_retention_logic.ts @@ -7,8 +7,8 @@ import { kea, MakeLogicType } from 'kea'; -import { HttpLogic } from '../../../shared/http'; import { flashAPIErrors } from '../../../shared/flash_messages'; +import { HttpLogic } from '../../../shared/http'; import { LogRetentionOptions, LogRetention, LogRetentionServer } from './types'; import { convertLogRetentionFromServerToClient } from './utils/convert_log_retention'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/constants.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/constants.tsx index 0f231092a36e2..c7c4d90d91ce8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/constants.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/constants.tsx @@ -6,8 +6,9 @@ */ import React from 'react'; -import { FormattedDate, FormattedMessage } from '@kbn/i18n/react'; + import { i18n } from '@kbn/i18n'; +import { FormattedDate, FormattedMessage } from '@kbn/i18n/react'; import { LogRetentionOptions, LogRetentionSettings, LogRetentionPolicy } from '../types'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/log_retention_message.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/log_retention_message.test.tsx index be95261a35c25..cd71e37108927 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/log_retention_message.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/log_retention_message.test.tsx @@ -5,13 +5,14 @@ * 2.0. */ -import { setMockValues } from '../../../../__mocks__/kea.mock'; -import { mountWithIntl } from '../../../../__mocks__'; +import { setMockValues, mountWithIntl } from '../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; import { LogRetentionOptions } from '../types'; + import { LogRetentionMessage } from './'; describe('LogRetentionMessage', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/log_retention_message.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/log_retention_message.tsx index 62bac44b122af..7d34a2567ba14 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/log_retention_message.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/log_retention_message.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { useValues } from 'kea'; import { AppLogic } from '../../../app_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.tsx index 56e31ec6bf970..83e83c0f9ea43 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/relevance_tuning/relevance_tuning.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { EuiPageHeader, EuiPageHeaderSection, @@ -14,8 +15,8 @@ import { EuiPageContent, } from '@elastic/eui'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { FlashMessages } from '../../../shared/flash_messages'; +import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { RELEVANCE_TUNING_TITLE } from './constants'; 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 586a845ce382a..7f7bce1b7ba95 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 @@ -9,7 +9,7 @@ import { LogicMounter } from '../../../__mocks__'; import { BoostType } from './types'; -import { RelevanceTuningLogic } from './relevance_tuning_logic'; +import { RelevanceTuningLogic } from './'; describe('RelevanceTuningLogic', () => { const { mount } = new LogicMounter(RelevanceTuningLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.test.tsx index 0c3749d1ccb3d..41428999b1e40 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.test.tsx @@ -6,15 +6,17 @@ */ import React from 'react'; + import { shallow, ShallowWrapper } from 'enzyme'; + import { EuiPanel } from '@elastic/eui'; -import { ResultField } from './result_field'; -import { ResultHeader } from './result_header'; import { ReactRouterHelper } from '../../../shared/react_router_helpers/eui_components'; import { SchemaTypes } from '../../../shared/types'; import { Result } from './result'; +import { ResultField } from './result_field'; +import { ResultHeader } from './result_header'; describe('Result', () => { const props = { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.tsx index d84b079ea9d72..7288fdf39f3ff 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result.tsx @@ -6,6 +6,7 @@ */ import React, { useState, useMemo } from 'react'; + import classNames from 'classnames'; import './result.scss'; @@ -14,13 +15,14 @@ import { EuiPanel, EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { ReactRouterHelper } from '../../../shared/react_router_helpers/eui_components'; -import { generateEncodedPath } from '../../utils/encode_path_params'; -import { ENGINE_DOCUMENT_DETAIL_PATH } from '../../routes'; import { Schema } from '../../../shared/types'; -import { FieldValue, Result as ResultType } from './types'; +import { ENGINE_DOCUMENT_DETAIL_PATH } from '../../routes'; +import { generateEncodedPath } from '../../utils/encode_path_params'; + import { ResultField } from './result_field'; import { ResultHeader } from './result_header'; +import { FieldValue, Result as ResultType } from './types'; interface Props { result: ResultType; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field.test.tsx index 6869708627b8d..1e79266dd7e7d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { ResultField } from './result_field'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field.tsx index a1c3ccd93622a..003810ec40a8d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field.tsx @@ -6,9 +6,11 @@ */ import React from 'react'; -import { ResultFieldValue } from '.'; + import { FieldType, Raw, Snippet } from './types'; +import { ResultFieldValue } from '.'; + import './result_field.scss'; interface Props { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field_value.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field_value.test.tsx index e1cefa1d79469..c732c9c8216c0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field_value.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_field_value.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow, ShallowWrapper } from 'enzyme'; import { ResultFieldValue } from '.'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.test.tsx index 9d90b3ae35a8f..dcefd0f6bc0b0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { ResultHeader } from './result_header'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header_item.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header_item.test.tsx index e8cc8796440a9..52fa81943bb2e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header_item.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/result/result_header_item.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { mount } from 'enzyme'; import { ResultHeaderItem } from './result_header_item'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/generic_confirmation_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/generic_confirmation_modal.test.tsx index 3e450b7a7bb70..8477f0e8e2ce2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/generic_confirmation_modal.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/generic_confirmation_modal.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { GenericConfirmationModal } from './generic_confirmation_modal'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/generic_confirmation_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/generic_confirmation_modal.tsx index b792ace4dac3f..eb64fe6421d80 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/generic_confirmation_modal.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/generic_confirmation_modal.tsx @@ -6,7 +6,6 @@ */ import React, { ReactNode, useState } from 'react'; -import { i18n } from '@kbn/i18n'; import { EuiButton, @@ -21,6 +20,7 @@ import { EuiSpacer, EuiText, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; interface GenericConfirmationModalProps { description: ReactNode; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_confirmation_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_confirmation_modal.test.tsx index a6d0cab532729..494517a438372 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_confirmation_modal.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_confirmation_modal.test.tsx @@ -8,9 +8,11 @@ import { setMockActions, setMockValues } from '../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; import { LogRetentionOptions } from '../../log_retention'; + import { GenericConfirmationModal } from './generic_confirmation_modal'; import { LogRetentionConfirmationModal } from './log_retention_confirmation_modal'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_confirmation_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_confirmation_modal.tsx index 52a2478d7158e..ca1fa9a8d0737 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_confirmation_modal.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_confirmation_modal.tsx @@ -6,12 +6,14 @@ */ import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiTextColor, EuiOverlayMask } from '@elastic/eui'; import { useActions, useValues } from 'kea'; +import { EuiTextColor, EuiOverlayMask } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + import { LogRetentionLogic, LogRetentionOptions } from '../../log_retention'; + import { GenericConfirmationModal } from './generic_confirmation_modal'; export const LogRetentionConfirmationModal: React.FC = () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.test.tsx index 882b82979a511..aee23e61e76fe 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.test.tsx @@ -9,9 +9,11 @@ import '../../../../__mocks__/shallow_useeffect.mock'; import { setMockActions, setMockValues } from '../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; import { LogRetention } from '../../log_retention/types'; + import { LogRetentionPanel } from './log_retention_panel'; describe('', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx index 3a40be9efd5db..76fdcdac58ad4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx @@ -6,11 +6,12 @@ */ import React, { useEffect } from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiLink, EuiSpacer, EuiSwitch, EuiText, EuiTextColor, EuiTitle } from '@elastic/eui'; import { useActions, useValues } from 'kea'; +import { EuiLink, EuiSpacer, EuiSwitch, EuiText, EuiTextColor, EuiTitle } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + import { DOCS_PREFIX } from '../../../routes'; import { LogRetentionLogic, LogRetentionOptions, LogRetentionMessage } from '../../log_retention'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.test.tsx index fead8cda0c0e2..41d446b8e36fc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.test.tsx @@ -6,7 +6,9 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiPageContentBody } from '@elastic/eui'; import { Settings } from './settings'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.tsx index c029cf344f18b..510075eba4abf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/settings.tsx @@ -15,10 +15,11 @@ import { EuiTitle, } from '@elastic/eui'; -import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { FlashMessages } from '../../../shared/flash_messages'; +import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { LogRetentionPanel, LogRetentionConfirmationModal } from './log_retention'; + import { SETTINGS_TITLE } from './'; export const Settings: React.FC = () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.test.tsx index e8dcb6ff98358..0b4a86870a69d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.test.tsx @@ -6,10 +6,12 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { SetupGuideLayout } from '../../../shared/setup_guide'; + import { SetupGuide } from './'; describe('SetupGuide', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx index befb06c719a39..3d96b22859fad 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/setup_guide/setup_guide.tsx @@ -6,15 +6,17 @@ */ import React from 'react'; + import { EuiSpacer, EuiTitle, EuiText } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { APP_SEARCH_PLUGIN } from '../../../../../common/constants'; -import { SetupGuideLayout, SETUP_GUIDE_TITLE } from '../../../shared/setup_guide'; import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { SetupGuideLayout, SETUP_GUIDE_TITLE } from '../../../shared/setup_guide'; import { SendAppSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; import { DOCS_PREFIX } from '../../routes'; + import GettingStarted from './assets/getting_started.png'; export const SetupGuide: React.FC = () => ( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx index dc3c0b03148d9..0e8220266d613 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.test.tsx @@ -11,13 +11,16 @@ import { setMockValues, setMockActions } from '../__mocks__'; import React from 'react'; import { Redirect } from 'react-router-dom'; + import { shallow } from 'enzyme'; import { Layout, SideNav, SideNavLink } from '../shared/layout'; -import { SetupGuide } from './components/setup_guide'; -import { ErrorConnecting } from './components/error_connecting'; -import { EnginesOverview } from './components/engines'; + import { EngineRouter } from './components/engine'; +import { EnginesOverview } from './components/engines'; +import { ErrorConnecting } from './components/error_connecting'; +import { SetupGuide } from './components/setup_guide'; + import { AppSearch, AppSearchUnconfigured, AppSearchConfigured, AppSearchNav } from './'; describe('AppSearch', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx index 918697422af6b..36ac3fb4dbc5b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx @@ -7,18 +7,26 @@ import React, { useEffect } from 'react'; import { Route, Redirect, Switch } from 'react-router-dom'; + import { useActions, useValues } from 'kea'; +import { APP_SEARCH_PLUGIN } from '../../../common/constants'; +import { InitialAppData } from '../../../common/types'; import { getAppSearchUrl } from '../shared/enterprise_search_url'; -import { KibanaLogic } from '../shared/kibana'; import { HttpLogic } from '../shared/http'; -import { AppLogic } from './app_logic'; -import { InitialAppData } from '../../../common/types'; - -import { APP_SEARCH_PLUGIN } from '../../../common/constants'; +import { KibanaLogic } from '../shared/kibana'; import { Layout, SideNav, SideNavLink } from '../shared/layout'; -import { EngineNav, EngineRouter } from './components/engine'; +import { NotFound } from '../shared/not_found'; +import { AppLogic } from './app_logic'; +import { Credentials, CREDENTIALS_TITLE } from './components/credentials'; +import { EngineNav, EngineRouter } from './components/engine'; +import { EnginesOverview, ENGINES_TITLE } from './components/engines'; +import { ErrorConnecting } from './components/error_connecting'; +import { Library } from './components/library'; +import { ROLE_MAPPINGS_TITLE } from './components/role_mappings'; +import { Settings, SETTINGS_TITLE } from './components/settings'; +import { SetupGuide } from './components/setup_guide'; import { ROOT_PATH, SETUP_GUIDE_PATH, @@ -30,15 +38,6 @@ import { LIBRARY_PATH, } from './routes'; -import { SetupGuide } from './components/setup_guide'; -import { ErrorConnecting } from './components/error_connecting'; -import { NotFound } from '../shared/not_found'; -import { EnginesOverview, ENGINES_TITLE } from './components/engines'; -import { Settings, SETTINGS_TITLE } from './components/settings'; -import { Credentials, CREDENTIALS_TITLE } from './components/credentials'; -import { ROLE_MAPPINGS_TITLE } from './components/role_mappings'; -import { Library } from './components/library'; - export const AppSearch: React.FC = (props) => { const { config } = useValues(KibanaLogic); return !config.host ? : ; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.test.tsx index 6ff33385df9a5..9ec3fdda63656 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.test.tsx @@ -6,9 +6,11 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { ErrorStatePrompt } from '../../../shared/error_state'; + import { ErrorConnecting } from './'; describe('ErrorConnecting', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.tsx index cb1abc275d37f..afee20df106e8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/error_connecting/error_connecting.tsx @@ -6,10 +6,11 @@ */ import React from 'react'; + import { EuiPage, EuiPageContent } from '@elastic/eui'; -import { SendEnterpriseSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; import { ErrorStatePrompt } from '../../../shared/error_state'; +import { SendEnterpriseSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; export const ErrorConnecting: React.FC = () => ( diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx index a9098689b3d0e..8631e6e2a51d4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx @@ -8,11 +8,13 @@ import { setMockValues, mockTelemetryActions } from '../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; import { EuiCard } from '@elastic/eui'; -import { EuiButtonTo } from '../../../shared/react_router_helpers'; + import { APP_SEARCH_PLUGIN, WORKPLACE_SEARCH_PLUGIN } from '../../../../../common/constants'; +import { EuiButtonTo } from '../../../shared/react_router_helpers'; import { ProductCard } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx index d31daeef54de9..20727e37460f1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx @@ -6,14 +6,16 @@ */ import React from 'react'; + import { useValues, useActions } from 'kea'; import { snakeCase } from 'lodash'; -import { i18n } from '@kbn/i18n'; + import { EuiCard, EuiTextColor } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { KibanaLogic } from '../../../shared/kibana'; import { EuiButtonTo } from '../../../shared/react_router_helpers'; import { TelemetryLogic } from '../../../shared/telemetry'; -import { KibanaLogic } from '../../../shared/kibana'; import './product_card.scss'; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.test.tsx index 0d55e2ce21c74..9ee34634e3797 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.test.tsx @@ -8,11 +8,13 @@ import { setMockValues } from '../../../__mocks__/kea.mock'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiPage } from '@elastic/eui'; -import { SetupGuideCta } from '../setup_guide'; import { ProductCard } from '../product_card'; +import { SetupGuideCta } from '../setup_guide'; import { ProductSelector } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx index 910840f023bb2..f2476a5770c25 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_selector/product_selector.tsx @@ -6,7 +6,9 @@ */ import React from 'react'; + import { useValues } from 'kea'; + import { EuiPage, EuiPageBody, @@ -25,11 +27,10 @@ import { KibanaLogic } from '../../../shared/kibana'; import { SetEnterpriseSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { SendEnterpriseSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; -import { ProductCard } from '../product_card'; -import { SetupGuideCta } from '../setup_guide'; - import AppSearchImage from '../../assets/app_search.png'; import WorkplaceSearchImage from '../../assets/workplace_search.png'; +import { ProductCard } from '../product_card'; +import { SetupGuideCta } from '../setup_guide'; interface ProductSelectorProps { access: { diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide.test.tsx index e2f3595d26974..44f06de7ff137 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide.test.tsx @@ -6,10 +6,12 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { SetEnterpriseSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { SetupGuideLayout } from '../../../shared/setup_guide'; + import { SetupGuide } from './'; describe('SetupGuide', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide.tsx index 02327e01c5ede..c59742d7ccbea 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide.tsx @@ -6,14 +6,16 @@ */ import React from 'react'; + import { EuiSpacer, EuiTitle, EuiText } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { ENTERPRISE_SEARCH_PLUGIN } from '../../../../../common/constants'; -import { SetupGuideLayout, SETUP_GUIDE_TITLE } from '../../../shared/setup_guide'; import { SetEnterpriseSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { SetupGuideLayout, SETUP_GUIDE_TITLE } from '../../../shared/setup_guide'; import { SendEnterpriseSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; + import GettingStarted from './assets/getting_started.png'; export const SetupGuide: React.FC = () => ( diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.test.tsx index 140e779df55d2..659af6d23c6d0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { SetupGuideCta } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.tsx index 7d32b11ba7ae8..17260cc15793a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/setup_guide/setup_guide_cta.tsx @@ -6,8 +6,10 @@ */ import React from 'react'; -import { i18n } from '@kbn/i18n'; + import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + import { EuiPanelTo } from '../../../shared/react_router_helpers'; import CtaImage from './assets/getting_started.png'; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx index e4508f4e99276..2d8dbd55f4366 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx @@ -5,15 +5,17 @@ * 2.0. */ +import { setMockValues, rerender } from '../__mocks__'; + import React from 'react'; -import { shallow } from 'enzyme'; -import { setMockValues, rerender } from '../__mocks__'; +import { shallow } from 'enzyme'; -import { EnterpriseSearch } from './'; -import { SetupGuide } from './components/setup_guide'; import { ErrorConnecting } from './components/error_connecting'; import { ProductSelector } from './components/product_selector'; +import { SetupGuide } from './components/setup_guide'; + +import { EnterpriseSearch } from './'; describe('EnterpriseSearch', () => { it('renders the Setup Guide and Product Selector', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx index 50ed0bce75cf8..b21e46429672a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx @@ -7,18 +7,17 @@ import React from 'react'; import { Route, Switch } from 'react-router-dom'; + import { useValues } from 'kea'; -import { KibanaLogic } from '../shared/kibana'; import { InitialAppData } from '../../../common/types'; - import { HttpLogic } from '../shared/http'; - -import { ROOT_PATH, SETUP_GUIDE_PATH } from './routes'; +import { KibanaLogic } from '../shared/kibana'; import { ErrorConnecting } from './components/error_connecting'; import { ProductSelector } from './components/product_selector'; import { SetupGuide } from './components/setup_guide'; +import { ROOT_PATH, SETUP_GUIDE_PATH } from './routes'; import './index.scss'; 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 f36561787eb69..2e0940b9c4af2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.test.tsx @@ -6,17 +6,19 @@ */ import React from 'react'; + import { getContext } from 'kea'; -import { coreMock } from 'src/core/public/mocks'; -import { licensingMock } from '../../../licensing/public/mocks'; +import { coreMock } from '../../../../../src/core/public/mocks'; import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; +import { licensingMock } from '../../../licensing/public/mocks'; -import { renderApp, renderHeaderActions } from './'; -import { EnterpriseSearch } from './enterprise_search'; import { AppSearch } from './app_search'; -import { WorkplaceSearch } from './workplace_search'; +import { EnterpriseSearch } from './enterprise_search'; import { KibanaLogic } from './shared/kibana'; +import { WorkplaceSearch } from './workplace_search'; + +import { renderApp, renderHeaderActions } from './'; describe('renderApp', () => { const kibanaDeps = { diff --git a/x-pack/plugins/enterprise_search/public/applications/index.tsx b/x-pack/plugins/enterprise_search/public/applications/index.tsx index 97e43f758e5b8..155ff5b92ba27 100644 --- a/x-pack/plugins/enterprise_search/public/applications/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/index.tsx @@ -7,21 +7,23 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { Router } from 'react-router-dom'; import { Provider } from 'react-redux'; -import { Store } from 'redux'; +import { Router } from 'react-router-dom'; + import { getContext, resetContext } from 'kea'; +import { Store } from 'redux'; + import { I18nProvider } from '@kbn/i18n/react'; -import { AppMountParameters, CoreStart } from 'src/core/public'; -import { PluginsStart, ClientConfigType, ClientData } from '../plugin'; +import { AppMountParameters, CoreStart } from '../../../../../src/core/public'; import { InitialAppData } from '../../common/types'; +import { PluginsStart, ClientConfigType, ClientData } from '../plugin'; +import { externalUrl } from './shared/enterprise_search_url'; +import { mountFlashMessagesLogic } from './shared/flash_messages'; +import { mountHttpLogic } from './shared/http'; import { mountKibanaLogic } from './shared/kibana'; import { mountLicensingLogic } from './shared/licensing'; -import { mountHttpLogic } from './shared/http'; -import { mountFlashMessagesLogic } from './shared/flash_messages'; -import { externalUrl } from './shared/enterprise_search_url'; /** * This file serves as a reusable wrapper to share Kibana-level context and other helpers diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.test.tsx index 9870264eb1c2f..d9d31f5a45d4b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.test.tsx @@ -8,7 +8,9 @@ import '../../__mocks__/kea.mock'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiEmptyPrompt } from '@elastic/eui'; import { ErrorStatePrompt } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx index cf8b234442002..f855c7b67dc6e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/error_state/error_state_prompt.tsx @@ -6,12 +6,14 @@ */ import React from 'react'; + import { useValues } from 'kea'; + import { EuiEmptyPrompt, EuiCode } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiButtonTo } from '../react_router_helpers'; import { KibanaLogic } from '../../shared/kibana'; +import { EuiButtonTo } from '../react_router_helpers'; import './error_state_prompt.scss'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.test.tsx index 2a31c0ecd66a8..aa45ce58af86a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.test.tsx @@ -8,7 +8,9 @@ import { setMockValues } from '../../__mocks__/kea.mock'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiCallOut } from '@elastic/eui'; import { FlashMessages } from './flash_messages'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.tsx index 5f38961b8a341..60d80487a2593 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages.tsx @@ -6,7 +6,9 @@ */ import React, { Fragment } from 'react'; + import { useValues } from 'kea'; + import { EuiCallOut, EuiCallOutProps, EuiSpacer } from '@elastic/eui'; import { FlashMessagesLogic } from './flash_messages_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.test.ts index 61f667719e3e6..7fc78c99fb242 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.test.ts @@ -5,12 +5,14 @@ * 2.0. */ +import { mockKibanaValues } from '../../__mocks__/kibana_logic.mock'; + import { resetContext } from 'kea'; -import { mockKibanaValues } from '../../__mocks__/kibana_logic.mock'; const { history } = mockKibanaValues; -import { FlashMessagesLogic, mountFlashMessagesLogic, IFlashMessage } from './flash_messages_logic'; +import { FlashMessagesLogic, mountFlashMessagesLogic } from './flash_messages_logic'; +import { IFlashMessage } from './types'; describe('FlashMessagesLogic', () => { const mount = () => mountFlashMessagesLogic(); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts index 26af4103aada1..5993e67b28a39 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/flash_messages_logic.ts @@ -6,15 +6,10 @@ */ import { kea, MakeLogicType } from 'kea'; -import { ReactNode } from 'react'; import { KibanaLogic } from '../kibana'; -export interface IFlashMessage { - type: 'success' | 'info' | 'warning' | 'error'; - message: ReactNode; - description?: ReactNode; -} +import { IFlashMessage } from './types'; interface FlashMessagesValues { messages: IFlashMessage[]; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.test.ts index 1df1c6a7a680e..b6b0e23ce7d6a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.test.ts @@ -8,6 +8,7 @@ import '../../__mocks__/kibana_logic.mock'; import { FlashMessagesLogic } from './flash_messages_logic'; + import { flashAPIErrors } from './handle_api_errors'; describe('flashAPIErrors', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.ts index 5fb824ebde9a0..11003d0fcc171 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.ts @@ -7,7 +7,8 @@ import { HttpResponse } from 'src/core/public'; -import { FlashMessagesLogic, IFlashMessage } from './flash_messages_logic'; +import { FlashMessagesLogic } from './flash_messages_logic'; +import { IFlashMessage } from './types'; /** * The API errors we are handling can come from one of two ways: diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/index.ts index 8d3605a19c22c..40317eb390547 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/index.ts @@ -6,7 +6,8 @@ */ export { FlashMessages } from './flash_messages'; -export { FlashMessagesLogic, IFlashMessage, mountFlashMessagesLogic } from './flash_messages_logic'; +export { FlashMessagesLogic, mountFlashMessagesLogic } from './flash_messages_logic'; +export { IFlashMessage } from './types'; export { flashAPIErrors } from './handle_api_errors'; export { setSuccessMessage, diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/types.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/types.ts new file mode 100644 index 0000000000000..c1d2f8420198d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/types.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 { ReactNode } from 'react'; + +export interface IFlashMessage { + type: 'success' | 'info' | 'warning' | 'error'; + message: ReactNode; + description?: ReactNode; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/hidden_text/hidden_text.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/hidden_text/hidden_text.test.tsx index 1888edca53034..af63b9a801edd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/hidden_text/hidden_text.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/hidden_text/hidden_text.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { HiddenText } from '.'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/hidden_text/hidden_text.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/hidden_text/hidden_text.tsx index 5503baf0bdae4..35901496c5fbd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/hidden_text/hidden_text.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/hidden_text/hidden_text.tsx @@ -6,6 +6,7 @@ */ import React, { useState, ReactElement } from 'react'; + import { i18n } from '@kbn/i18n'; interface ChildrenProps { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.test.tsx index d8f02be60ef92..44bd8b78320d6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.test.tsx @@ -9,13 +9,14 @@ import '../../__mocks__/shallow_useeffect.mock'; import { setMockActions, setMockValues } from '../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; import { EuiPanel } from '@elastic/eui'; +import { IndexingStatus } from './indexing_status'; import { IndexingStatusContent } from './indexing_status_content'; import { IndexingStatusErrors } from './indexing_status_errors'; -import { IndexingStatus } from './indexing_status'; describe('IndexingStatus', () => { const getItemDetailPath = jest.fn(); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.tsx index 3898eda126415..ee0557e15396c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status.tsx @@ -11,12 +11,12 @@ import { useValues, useActions } from 'kea'; import { EuiPanel, EuiSpacer } from '@elastic/eui'; +import { IIndexingStatus } from '../types'; + import { IndexingStatusContent } from './indexing_status_content'; import { IndexingStatusErrors } from './indexing_status_errors'; import { IndexingStatusLogic } from './indexing_status_logic'; -import { IIndexingStatus } from '../types'; - export interface IIndexingStatusProps { viewLinkPath: string; itemId: string; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_content.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_content.test.tsx index a744ddf8b5290..8998e640d6c35 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_content.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_content.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { EuiProgress, EuiTitle } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_errors.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_errors.test.tsx index 3747fe020af20..eb5fa9d70f026 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_errors.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_errors.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { EuiCallOut } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_logic.ts index 11ba1304d0a22..a436b669bcbe5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/indexing_status/indexing_status_logic.ts @@ -7,9 +7,9 @@ import { kea, MakeLogicType } from 'kea'; +import { flashAPIErrors } from '../flash_messages'; import { HttpLogic } from '../http'; import { IIndexingStatus } from '../types'; -import { flashAPIErrors } from '../flash_messages'; interface IndexingStatusProps { statusPath: string; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.test.ts index 10b550fc93eb3..a5f54d16b2fad 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.test.ts @@ -5,10 +5,10 @@ * 2.0. */ -import { resetContext } from 'kea'; - import { mockKibanaValues } from '../../__mocks__'; +import { resetContext } from 'kea'; + import { KibanaLogic, mountKibanaLogic } from './kibana_logic'; describe('KibanaLogic', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts index 4bb1859df09ea..8015d22f7c44a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana/kibana_logic.ts @@ -5,12 +5,13 @@ * 2.0. */ -import { kea, MakeLogicType } from 'kea'; - import { FC } from 'react'; + import { History } from 'history'; -import { ApplicationStart, ChromeBreadcrumb } from 'src/core/public'; -import { ChartsPluginStart } from 'src/plugins/charts/public'; +import { kea, MakeLogicType } from 'kea'; + +import { ApplicationStart, ChromeBreadcrumb } from '../../../../../../../src/core/public'; +import { ChartsPluginStart } from '../../../../../../../src/plugins/charts/public'; import { CloudSetup } from '../../../../../cloud/public'; import { HttpLogic } from '../http'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts index 64f53c767b17c..908cc0601ab9c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts @@ -6,10 +6,8 @@ */ import { useValues } from 'kea'; -import { EuiBreadcrumb } from '@elastic/eui'; -import { KibanaLogic } from '../kibana'; -import { HttpLogic } from '../http'; +import { EuiBreadcrumb } from '@elastic/eui'; import { ENTERPRISE_SEARCH_PLUGIN, @@ -18,6 +16,8 @@ import { } from '../../../../common/constants'; import { stripLeadingSlash } from '../../../../common/strip_slashes'; +import { HttpLogic } from '../http'; +import { KibanaLogic } from '../kibana'; import { letBrowserHandleEvent, createHref } from '../react_router_helpers'; /** diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.test.tsx index 5b1aa64c42d64..c9743e6824018 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.test.tsx @@ -9,6 +9,7 @@ import '../../__mocks__/shallow_useeffect.mock'; import { setMockValues, mockKibanaValues, mockHistory } from '../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; jest.mock('./generate_breadcrumbs', () => ({ diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.tsx index fa127566b1b02..e639f9d22fb4b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.tsx @@ -6,6 +6,7 @@ */ import React, { useEffect } from 'react'; + import { useValues } from 'kea'; import { KibanaLogic } from '../kibana'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.test.tsx index c67518e977de2..28092f75cdede 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.test.tsx @@ -6,7 +6,9 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiPageSideBar, EuiButton, EuiPageBody, EuiCallOut } from '@elastic/eui'; import { Layout, INavContext } from './layout'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.tsx index 1af85905e6ccb..9cf5fccddbd5b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/layout.tsx @@ -6,6 +6,7 @@ */ import React, { useState } from 'react'; + import classNames from 'classnames'; import { EuiPage, EuiPageSideBar, EuiPageBody, EuiButton, EuiCallOut } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.test.tsx index ceb5f21ce056f..451b49738029d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.test.tsx @@ -9,11 +9,13 @@ import '../../__mocks__/react_router_history.mock'; import React from 'react'; import { useLocation } from 'react-router-dom'; + import { shallow } from 'enzyme'; import { EuiLink } from '@elastic/eui'; -import { EuiLinkTo } from '../react_router_helpers'; + import { ENTERPRISE_SEARCH_PLUGIN, APP_SEARCH_PLUGIN } from '../../../../common/constants'; +import { EuiLinkTo } from '../react_router_helpers'; import { SideNav, SideNavLink, SideNavItem } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.tsx index 605d3940a8cc7..58a5c7bbb229f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/layout/side_nav.tsx @@ -7,14 +7,15 @@ import React, { useContext } from 'react'; import { useLocation } from 'react-router-dom'; + import classNames from 'classnames'; -import { i18n } from '@kbn/i18n'; import { EuiIcon, EuiTitle, EuiText, EuiLink } from '@elastic/eui'; // TODO: Remove EuiLink after full Kibana transition -import { EuiLinkTo } from '../react_router_helpers'; +import { i18n } from '@kbn/i18n'; import { ENTERPRISE_SEARCH_PLUGIN } from '../../../../common/constants'; import { stripTrailingSlash } from '../../../../common/strip_slashes'; +import { EuiLinkTo } from '../react_router_helpers'; import { NavContext, INavContext } from './layout'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/loading/loading.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/loading/loading.test.tsx index c443467d5cb32..eab5694a27968 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/loading/loading.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/loading/loading.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { EuiLoadingSpinner } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/loading/loading.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/loading/loading.tsx index cfcbeaee72095..27a4dfdec0c07 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/loading/loading.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/loading/loading.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { EuiLoadingSpinner } from '@elastic/eui'; import './loading.scss'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.test.tsx index 0bda848bc8d6c..7e75b2b47bb7a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.test.tsx @@ -8,12 +8,14 @@ import { setMockValues } from '../../__mocks__/kea.mock'; import React from 'react'; + import { shallow } from 'enzyme'; import { EuiButton as EuiButtonExternal, EuiEmptyPrompt } from '@elastic/eui'; import { APP_SEARCH_PLUGIN, WORKPLACE_SEARCH_PLUGIN } from '../../../../common/constants'; import { SetAppSearchChrome } from '../kibana_chrome'; + import { AppSearchLogo } from './assets/app_search_logo'; import { WorkplaceSearchLogo } from './assets/workplace_search_logo'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.tsx index 6102987464f55..5699568c40558 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/not_found/not_found.tsx @@ -6,8 +6,9 @@ */ import React from 'react'; + import { useValues } from 'kea'; -import { i18n } from '@kbn/i18n'; + import { EuiPageContent, EuiEmptyPrompt, @@ -16,6 +17,7 @@ import { EuiFlexItem, EuiButton, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { APP_SEARCH_PLUGIN, @@ -23,11 +25,11 @@ import { LICENSED_SUPPORT_URL, } from '../../../../common/constants'; -import { EuiButtonTo } from '../react_router_helpers'; -import { BreadcrumbTrail } from '../kibana_chrome/generate_breadcrumbs'; import { SetAppSearchChrome, SetWorkplaceSearchChrome } from '../kibana_chrome'; -import { SendAppSearchTelemetry, SendWorkplaceSearchTelemetry } from '../telemetry'; +import { BreadcrumbTrail } from '../kibana_chrome/generate_breadcrumbs'; import { LicensingLogic } from '../licensing'; +import { EuiButtonTo } from '../react_router_helpers'; +import { SendAppSearchTelemetry, SendWorkplaceSearchTelemetry } from '../telemetry'; import { AppSearchLogo } from './assets/app_search_logo'; import { WorkplaceSearchLogo } from './assets/workplace_search_logo'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/create_href.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/create_href.test.ts index 0619dab19e2bd..fe2973cfdee32 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/create_href.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/create_href.test.ts @@ -5,9 +5,10 @@ * 2.0. */ -import { httpServiceMock } from 'src/core/public/mocks'; import { mockHistory } from '../../__mocks__'; +import { httpServiceMock } from 'src/core/public/mocks'; + import { createHref } from './'; describe('createHref', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/create_href.ts b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/create_href.ts index e36a65c2457db..ea28fc4d440c5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/create_href.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/create_href.ts @@ -6,6 +6,7 @@ */ import { History } from 'history'; + import { HttpSetup } from 'src/core/public'; /** diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_components.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_components.test.tsx index 4de43ce997b48..75639ffeb9d6b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_components.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_components.test.tsx @@ -7,11 +7,13 @@ import '../../__mocks__/kea.mock'; +import { mockKibanaValues, mockHistory } from '../../__mocks__'; + import React from 'react'; + import { shallow, mount } from 'enzyme'; -import { EuiLink, EuiButton, EuiButtonEmpty, EuiPanel, EuiCard } from '@elastic/eui'; -import { mockKibanaValues, mockHistory } from '../../__mocks__'; +import { EuiLink, EuiButton, EuiButtonEmpty, EuiPanel, EuiCard } from '@elastic/eui'; import { EuiLinkTo, EuiButtonTo, EuiButtonEmptyTo, EuiPanelTo, EuiCardTo } from './eui_components'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_components.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_components.tsx index 384eb79c993c1..b9fee9d16273b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_components.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_components.tsx @@ -6,7 +6,9 @@ */ import React from 'react'; + import { useValues } from 'kea'; + import { EuiLink, EuiButton, @@ -20,8 +22,9 @@ import { } from '@elastic/eui'; import { EuiPanelProps } from '@elastic/eui/src/components/panel/panel'; -import { KibanaLogic } from '../kibana'; import { HttpLogic } from '../http'; +import { KibanaLogic } from '../kibana'; + import { letBrowserHandleEvent, createHref } from './'; /** diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_add_field_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_add_field_modal.test.tsx index c0bd1f3671f15..88c170b059d9c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_add_field_modal.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_add_field_modal.test.tsx @@ -6,16 +6,17 @@ */ import React from 'react'; + import { shallow, mount } from 'enzyme'; +import { EuiFieldText, EuiModal, EuiSelect } from '@elastic/eui'; + import { NUMBER } from '../constants/field_types'; import { FIELD_NAME_CORRECTED_PREFIX } from './constants'; import { SchemaAddFieldModal } from './'; -import { EuiFieldText, EuiModal, EuiSelect } from '@elastic/eui'; - describe('SchemaAddFieldModal', () => { const addNewField = jest.fn(); const closeAddFieldModal = jest.fn(); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_errors_accordion.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_errors_accordion.test.tsx index b1fde05906d44..a82f9e9b6113b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_errors_accordion.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_errors_accordion.test.tsx @@ -6,11 +6,13 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { EuiAccordion, EuiTableRow } from '@elastic/eui'; import { EuiLinkTo } from '../react_router_helpers'; + import { SchemaErrorsAccordion } from './schema_errors_accordion'; describe('SchemaErrorsAccordion', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_existing_field.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_existing_field.test.tsx index 62f66bc95a5eb..5e89dce24bd4a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_existing_field.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/schema/schema_existing_field.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { EuiSelect } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/cloud/instructions.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/cloud/instructions.test.tsx index 5d69a8ea84acf..0136f9745c322 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/cloud/instructions.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/cloud/instructions.test.tsx @@ -5,11 +5,13 @@ * 2.0. */ +import { mountWithIntl } from '../../../__mocks__'; + import React from 'react'; + import { shallow } from 'enzyme'; -import { EuiSteps, EuiLink } from '@elastic/eui'; -import { mountWithIntl } from '../../../__mocks__'; +import { EuiSteps, EuiLink } from '@elastic/eui'; import { CloudSetupInstructions } from './instructions'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/cloud/instructions.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/cloud/instructions.tsx index 24ba5bd4e5d0a..b355c88943a54 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/cloud/instructions.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/cloud/instructions.tsx @@ -6,9 +6,10 @@ */ import React from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; + import { EuiPageContent, EuiSteps, EuiText, EuiLink, EuiCallOut } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { docLinks } from '../../doc_links'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/instructions.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/instructions.test.tsx index 74fb74ce8cf70..fd31ca720b82b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/instructions.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/instructions.test.tsx @@ -5,11 +5,13 @@ * 2.0. */ +import { mountWithIntl } from '../../__mocks__'; + import React from 'react'; + import { shallow } from 'enzyme'; -import { EuiSteps, EuiLink } from '@elastic/eui'; -import { mountWithIntl } from '../../__mocks__'; +import { EuiSteps, EuiLink } from '@elastic/eui'; import { SetupInstructions } from './instructions'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/instructions.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/instructions.tsx index 83c244ea24ff1..5e39d1acdf189 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/instructions.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/instructions.tsx @@ -6,8 +6,7 @@ */ import React from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { i18n } from '@kbn/i18n'; + import { EuiPageContent, EuiSpacer, @@ -18,6 +17,8 @@ import { EuiAccordion, EuiLink, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; interface Props { productName: string; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.test.tsx index 0b70bb70f8441..90ddddd7d20aa 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.test.tsx @@ -8,11 +8,13 @@ import { setMockValues, rerender } from '../../__mocks__'; import React from 'react'; + import { shallow, ShallowWrapper } from 'enzyme'; + import { EuiIcon } from '@elastic/eui'; -import { SetupInstructions } from './instructions'; import { CloudSetupInstructions } from './cloud/instructions'; +import { SetupInstructions } from './instructions'; import { SetupGuideLayout } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.tsx index a0e89bfd8e57d..2140b3392abae 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/setup_guide/setup_guide.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { useValues } from 'kea'; import { @@ -22,9 +23,9 @@ import { import { KibanaLogic } from '../kibana'; -import { SetupInstructions } from './instructions'; import { CloudSetupInstructions } from './cloud/instructions'; import { SETUP_GUIDE_TITLE } from './constants'; +import { SetupInstructions } from './instructions'; import './setup_guide.scss'; /** diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/table_header/table_header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/table_header/table_header.test.tsx index d2588ed8d4aca..a481f22095aa3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/table_header/table_header.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/table_header/table_header.test.tsx @@ -6,7 +6,9 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiTableHeader, EuiTableHeaderCell } from '@elastic/eui'; import { TableHeader } from './table_header'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx index 60f4d404a917a..5fc8074d0a4d7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx @@ -9,6 +9,7 @@ import '../../__mocks__/shallow_useeffect.mock'; import { mockTelemetryActions } from '../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; import { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx index e0f54a9e421bf..1759b4075deca 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx @@ -6,6 +6,7 @@ */ import React, { useEffect } from 'react'; + import { useActions } from 'kea'; import { TelemetryLogic, SendTelemetryHelper } from './telemetry_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/telemetry_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/telemetry_logic.test.ts index 52aec2c384adb..e516daedc1ba6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/telemetry_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/telemetry_logic.test.ts @@ -5,9 +5,10 @@ * 2.0. */ -import { JSON_HEADER as headers } from '../../../../common/constants'; import { LogicMounter, mockHttpValues } from '../../__mocks__'; +import { JSON_HEADER as headers } from '../../../../common/constants'; + import { TelemetryLogic } from './telemetry_logic'; describe('Telemetry logic', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncate.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncate.test.tsx index 71ed60cbd1c93..f9bf55b4fe800 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncate.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/truncate/truncate.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { TruncatedContent } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts index c249f5ee20588..ce92f62d3a017 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts @@ -5,10 +5,11 @@ * 2.0. */ -import { mergeServerAndStaticData } from '../views/content_sources/sources_logic'; -import { staticSourceData } from '../views/content_sources/source_data'; import { groups } from './groups.mock'; +import { staticSourceData } from '../views/content_sources/source_data'; +import { mergeServerAndStaticData } from '../views/content_sources/sources_logic'; + export const contentSources = [ { id: '123', diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.test.ts index f1e6ca237681f..8ba94e83d26cf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.test.ts @@ -5,9 +5,9 @@ * 2.0. */ +import { DEFAULT_INITIAL_APP_DATA } from '../../../common/__mocks__'; import { LogicMounter } from '../__mocks__'; -import { DEFAULT_INITIAL_APP_DATA } from '../../../common/__mocks__'; import { AppLogic } from './app_logic'; describe('AppLogic', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.test.tsx index c2c645ebe439a..a7a788b48789a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.test.tsx @@ -5,12 +5,14 @@ * 2.0. */ -import { externalUrl } from '../../../shared/enterprise_search_url'; - import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiButtonEmpty } from '@elastic/eui'; +import { externalUrl } from '../../../shared/enterprise_search_url'; + import { WorkplaceSearchHeaderActions } from './'; describe('WorkplaceSearchHeaderActions', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.tsx index c1912deb8d40a..c79865d25ecd7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/kibana_header_actions.tsx @@ -6,10 +6,10 @@ */ import React from 'react'; + import { EuiButtonEmpty, EuiText } from '@elastic/eui'; import { externalUrl, getWorkplaceSearchUrl } from '../../../shared/enterprise_search_url'; - import { NAV } from '../../constants'; export const WorkplaceSearchHeaderActions: React.FC = () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx index d2b2da1a48176..8f37f608f4e28 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx @@ -8,9 +8,11 @@ import '../../../__mocks__/enterprise_search_url.mock'; import React from 'react'; + import { shallow } from 'enzyme'; import { SideNav, SideNavLink } from '../../../shared/layout'; + import { WorkplaceSearchNav } from './'; describe('WorkplaceSearchNav', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx index 2696e5acf1c12..c184247b253d6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx @@ -12,9 +12,7 @@ import { EuiSpacer } from '@elastic/eui'; import { WORKPLACE_SEARCH_PLUGIN } from '../../../../../common/constants'; import { getWorkplaceSearchUrl } from '../../../shared/enterprise_search_url'; import { SideNav, SideNavLink } from '../../../shared/layout'; - import { NAV } from '../../constants'; - import { SOURCES_PATH, SECURITY_PATH, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/api_key/api_key.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/api_key/api_key.test.tsx index 66e9ac9ed7a8b..32f21c158736f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/api_key/api_key.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/api_key/api_key.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { EuiCodeBlock, EuiFormLabel } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/component_loader/component_loader.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/component_loader/component_loader.test.tsx index cb3fc32432999..991c7a061b4bb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/component_loader/component_loader.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/component_loader/component_loader.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { EuiLoadingSpinner, EuiTextColor } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/content_section/content_section.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/content_section/content_section.test.tsx index c21e8b8d3449f..21280926d7aae 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/content_section/content_section.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/content_section/content_section.test.tsx @@ -6,12 +6,15 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiSpacer } from '@elastic/eui'; -import { ContentSection } from './'; import { ViewContentHeader } from '../view_content_header'; +import { ContentSection } from './'; + const props = { children:
, testSubj: 'contentSection', diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/content_section/content_section.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/content_section/content_section.tsx index b0ab18bbfde95..e606263ac6f1c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/content_section/content_section.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/content_section/content_section.tsx @@ -10,7 +10,6 @@ import React from 'react'; import { EuiSpacer } from '@elastic/eui'; import { SpacerSizeTypes } from '../../../types'; - import { ViewContentHeader } from '../view_content_header'; import './content_section.scss'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/credential_item/credential_item.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/credential_item/credential_item.test.tsx index 79c4abdf2e223..13e2a229b3a76 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/credential_item/credential_item.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/credential_item/credential_item.test.tsx @@ -6,6 +6,7 @@ */ import * as React from 'react'; + import { shallow } from 'enzyme'; import { EuiCopy, EuiButtonIcon, EuiFieldText } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_badge/license_badge.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_badge/license_badge.test.tsx index 4e38894766d86..6deb37d850076 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_badge/license_badge.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_badge/license_badge.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { EuiBadge } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_callout/license_callout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_callout/license_callout.test.tsx index d8266127a0f42..6a69178ad07da 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_callout/license_callout.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/license_callout/license_callout.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { EuiLink, EuiText } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.test.tsx index 5ef191d0d0fe8..0bced6a7fc4e0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.test.tsx @@ -9,7 +9,9 @@ import '../../../../__mocks__/kea.mock'; import { mockTelemetryActions } from '../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.tsx index ded4278c35e14..3611bfb2a3f69 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/product_button/product_button.tsx @@ -6,13 +6,14 @@ */ import React from 'react'; + import { useActions } from 'kea'; import { EuiButton, EuiButtonProps, EuiLinkProps } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { TelemetryLogic } from '../../../../shared/telemetry'; import { getWorkplaceSearchUrl } from '../../../../shared/enterprise_search_url'; +import { TelemetryLogic } from '../../../../shared/telemetry'; export const ProductButton: React.FC = () => { const { sendWorkplaceSearchTelemetry } = useActions(TelemetryLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.test.tsx index e5cd2bb2e0461..9af91107d7304 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { ApiKey } from '../api_key'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.tsx index 8c6e2b0174eb0..236d475b8f687 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_config_fields/source_config_fields.tsx @@ -16,7 +16,6 @@ import { CLIENT_ID_LABEL, CLIENT_SECRET_LABEL, } from '../../../constants'; - import { ApiKey } from '../api_key'; import { CredentialItem } from '../credential_item'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.test.tsx index f8cf9d63915d6..3bea6f224dc2b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_icon/source_icon.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { EuiIcon } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.test.tsx index cfac7148bf88a..9661471bb1dd7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.test.tsx @@ -5,11 +5,13 @@ * 2.0. */ +import { contentSources } from '../../../__mocks__/content_sources.mock'; + import React from 'react'; + import { shallow } from 'enzyme'; import { EuiTableRow, EuiSwitch, EuiIcon } from '@elastic/eui'; -import { contentSources } from '../../../__mocks__/content_sources.mock'; import { SourceIcon } from '../source_icon'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.tsx index 11d71481751b0..6cfc68b45ee3c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/source_row/source_row.tsx @@ -26,14 +26,13 @@ import { import { EuiLinkTo } from '../../../../shared/react_router_helpers'; import { SOURCE_STATUSES as statuses } from '../../../constants'; -import { ContentSourceDetails } from '../../../types'; import { ADD_SOURCE_PATH, SOURCE_DETAILS_PATH, getContentSourcePath, getSourcesPath, } from '../../../routes'; - +import { ContentSourceDetails } from '../../../types'; import { SourceIcon } from '../source_icon'; import './source_row.scss'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/sources_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/sources_table.test.tsx index efe529bcfb289..f54f7ccdf24bd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/sources_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/sources_table.test.tsx @@ -5,13 +5,15 @@ * 2.0. */ +import { contentSources } from '../../../__mocks__/content_sources.mock'; + import React from 'react'; + import { shallow } from 'enzyme'; import { EuiTable } from '@elastic/eui'; -import { TableHeader } from '../../../../shared/table_header/table_header'; -import { contentSources } from '../../../__mocks__/content_sources.mock'; +import { TableHeader } from '../../../../shared/table_header/table_header'; import { SourceRow } from '../source_row'; import { SourcesTable } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/sources_table.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/sources_table.tsx index a0aba097d17f4..66e7e2e752a1e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/sources_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/sources_table/sources_table.tsx @@ -10,8 +10,8 @@ import React from 'react'; import { EuiTable, EuiTableBody } from '@elastic/eui'; import { TableHeader } from '../../../../shared/table_header/table_header'; -import { SourceRow, ISourceRow } from '../source_row'; import { ContentSourceDetails } from '../../../types'; +import { SourceRow, ISourceRow } from '../source_row'; interface SourcesTableProps extends ISourceRow { sources: ContentSourceDetails[]; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/table_pagination_bar/table_pagination_bar.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/table_pagination_bar/table_pagination_bar.test.tsx index 343c9b68bc834..d22ddcce49dc4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/table_pagination_bar/table_pagination_bar.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/table_pagination_bar/table_pagination_bar.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { EuiFlexGroup, EuiTablePagination } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_icon/user_icon.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_icon/user_icon.test.tsx index d8046bd88cf4a..5ce83b641cf8f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_icon/user_icon.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_icon/user_icon.test.tsx @@ -5,10 +5,11 @@ * 2.0. */ +import { users } from '../../../__mocks__/users.mock'; + import React from 'react'; -import { shallow } from 'enzyme'; -import { users } from '../../../__mocks__/users.mock'; +import { shallow } from 'enzyme'; import { UserIcon } from './user_icon'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_row/user_row.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_row/user_row.test.tsx index fe2bfd27db55a..f15c74ed1054b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_row/user_row.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/user_row/user_row.test.tsx @@ -5,13 +5,14 @@ * 2.0. */ +import { users } from '../../../__mocks__/users.mock'; + import React from 'react'; + import { shallow } from 'enzyme'; import { EuiTableRow } from '@elastic/eui'; -import { users } from '../../../__mocks__/users.mock'; - import { UserRow } from './'; describe('SourcesTable', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/view_content_header/view_content_header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/view_content_header/view_content_header.test.tsx index 01a05a5d94c75..fda1a27e103c2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/view_content_header/view_content_header.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/view_content_header/view_content_header.test.tsx @@ -6,7 +6,9 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiFlexGroup } from '@elastic/eui'; import { ViewContentHeader } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/view_content_header/view_content_header.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/view_content_header/view_content_header.tsx index ed2989de5ce3c..fa3a1d3ccb2e4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/view_content_header/view_content_header.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/view_content_header/view_content_header.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiText, EuiTitle, EuiSpacer } from '@elastic/eui'; - import { FlexGroupAlignItems } from '@elastic/eui/src/components/flex/flex_group'; interface ViewContentHeaderProps { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx index 73ee7662888bb..5678ad545d50d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.test.tsx @@ -10,13 +10,15 @@ import { setMockValues, setMockActions, mockKibanaValues } from '../__mocks__'; import React from 'react'; import { Redirect } from 'react-router-dom'; + import { shallow } from 'enzyme'; import { Layout } from '../shared/layout'; + import { WorkplaceSearchHeaderActions } from './components/layout'; -import { SetupGuide } from './views/setup_guide'; import { ErrorState } from './views/error_state'; import { Overview } from './views/overview'; +import { SetupGuide } from './views/setup_guide'; import { WorkplaceSearch, WorkplaceSearchUnconfigured, WorkplaceSearchConfigured } from './'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index a4c12f1d71d4e..d690dee4dc98c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx @@ -7,16 +7,18 @@ import React, { useEffect } from 'react'; import { Route, Redirect, Switch, useLocation } from 'react-router-dom'; + import { useActions, useValues } from 'kea'; import { WORKPLACE_SEARCH_PLUGIN } from '../../../common/constants'; import { InitialAppData } from '../../../common/types'; -import { KibanaLogic } from '../shared/kibana'; import { HttpLogic } from '../shared/http'; -import { AppLogic } from './app_logic'; +import { KibanaLogic } from '../shared/kibana'; import { Layout } from '../shared/layout'; -import { WorkplaceSearchNav, WorkplaceSearchHeaderActions } from './components/layout'; +import { NotFound } from '../shared/not_found'; +import { AppLogic } from './app_logic'; +import { WorkplaceSearchNav, WorkplaceSearchHeaderActions } from './components/layout'; import { GROUPS_PATH, SETUP_GUIDE_PATH, @@ -25,19 +27,16 @@ import { ORG_SETTINGS_PATH, SECURITY_PATH, } from './routes'; - -import { SetupGuide } from './views/setup_guide'; +import { SourcesRouter } from './views/content_sources'; +import { SourceSubNav } from './views/content_sources/components/source_sub_nav'; import { ErrorState } from './views/error_state'; -import { NotFound } from '../shared/not_found'; -import { Overview } from './views/overview'; import { GroupsRouter } from './views/groups'; +import { GroupSubNav } from './views/groups/components/group_sub_nav'; +import { Overview } from './views/overview'; import { Security } from './views/security'; -import { SourcesRouter } from './views/content_sources'; import { SettingsRouter } from './views/settings'; - -import { GroupSubNav } from './views/groups/components/group_sub_nav'; -import { SourceSubNav } from './views/content_sources/components/source_sub_nav'; import { SettingsSubNav } from './views/settings/components/settings_sub_nav'; +import { SetupGuide } from './views/setup_guide'; export const WorkplaceSearch: React.FC = (props) => { const { config } = useValues(KibanaLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.test.tsx index d9c1dbeefad92..68bec94270a01 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { EuiLink } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx index aa219a475406f..41f53523bca4e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.test.tsx @@ -7,10 +7,10 @@ import '../../../../../__mocks__/shallow_useeffect.mock'; import { mockKibanaValues, setMockActions, setMockValues } from '../../../../../__mocks__'; - import { sourceConfigData } from '../../../../__mocks__/content_sources.mock'; import React from 'react'; + import { shallow } from 'enzyme'; import { Loading } from '../../../../../shared/loading'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx index 0664c930775bc..b00f9807f0acd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source.tsx @@ -9,16 +9,16 @@ import React, { useEffect } from 'react'; import { useActions, useValues } from 'kea'; -import { AppLogic } from '../../../../app_logic'; import { KibanaLogic } from '../../../../../shared/kibana'; import { Loading } from '../../../../../shared/loading'; +import { AppLogic } from '../../../../app_logic'; import { CUSTOM_SERVICE_TYPE } from '../../../../constants'; -import { staticSourceData } from '../../source_data'; -import { AddSourceLogic, AddSourceProps, AddSourceSteps } from './add_source_logic'; -import { SourceDataItem } from '../../../../types'; import { SOURCE_ADDED_PATH, getSourcesPath } from '../../../../routes'; +import { SourceDataItem } from '../../../../types'; +import { staticSourceData } from '../../source_data'; import { AddSourceHeader } from './add_source_header'; +import { AddSourceLogic, AddSourceProps, AddSourceSteps } from './add_source_logic'; import { ConfigCompleted } from './config_completed'; import { ConfigurationIntro } from './configuration_intro'; import { ConfigureCustom } from './configure_custom'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_header.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_header.test.tsx index 7167fcf3bc252..879f7993f3dc1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_header.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_header.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { EuiText, EuiTextColor } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.test.tsx index fe24eeb5c7cb5..6da348c6e2755 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.test.tsx @@ -14,6 +14,7 @@ import { } from '../../../../__mocks__/content_sources.mock'; import React from 'react'; + import { shallow } from 'enzyme'; import { EuiEmptyPrompt, EuiFieldSearch } from '@elastic/eui'; @@ -21,16 +22,15 @@ import { EuiEmptyPrompt, EuiFieldSearch } from '@elastic/eui'; import { Loading } from '../../../../../../applications/shared/loading'; import { ViewContentHeader } from '../../../../components/shared/view_content_header'; +import { AddSourceList } from './add_source_list'; +import { AvailableSourcesList } from './available_sources_list'; +import { ConfiguredSourcesList } from './configured_sources_list'; import { ADD_SOURCE_NEW_SOURCE_DESCRIPTION, ADD_SOURCE_ORG_SOURCE_DESCRIPTION, ADD_SOURCE_PRIVATE_SOURCE_DESCRIPTION, } from './constants'; -import { AddSourceList } from './add_source_list'; -import { AvailableSourcesList } from './available_sources_list'; -import { ConfiguredSourcesList } from './configured_sources_list'; - describe('AddSourceList', () => { const initializeSources = jest.fn(); const resetSourcesState = jest.fn(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx index 3a0db0f44047d..d026782d12540 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_list.tsx @@ -18,15 +18,18 @@ import { EuiPanel, EuiEmptyPrompt, } from '@elastic/eui'; -import noSharedSourcesIcon from '../../../../assets/share_circle.svg'; +import { Loading } from '../../../../../../applications/shared/loading'; import { AppLogic } from '../../../../app_logic'; +import noSharedSourcesIcon from '../../../../assets/share_circle.svg'; import { ContentSection } from '../../../../components/shared/content_section'; import { ViewContentHeader } from '../../../../components/shared/view_content_header'; -import { Loading } from '../../../../../../applications/shared/loading'; import { CUSTOM_SERVICE_TYPE } from '../../../../constants'; import { SourceDataItem } from '../../../../types'; +import { SourcesLogic } from '../../sources_logic'; +import { AvailableSourcesList } from './available_sources_list'; +import { ConfiguredSourcesList } from './configured_sources_list'; import { ADD_SOURCE_NEW_SOURCE_DESCRIPTION, ADD_SOURCE_ORG_SOURCE_DESCRIPTION, @@ -39,10 +42,6 @@ import { ADD_SOURCE_EMPTY_BODY, } from './constants'; -import { SourcesLogic } from '../../sources_logic'; -import { AvailableSourcesList } from './available_sources_list'; -import { ConfiguredSourcesList } from './configured_sources_list'; - export const AddSourceList: React.FC = () => { const { contentSources, dataLoading, availableSources, configuredSources } = useValues( SourcesLogic 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 a3fd35503ea0d..ed67eb9994bc8 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 @@ -11,20 +11,18 @@ import { mockHttpValues, mockKibanaValues, } from '../../../../../__mocks__'; +import { sourceConfigData } from '../../../../__mocks__/content_sources.mock'; + +import { nextTick } from '@kbn/test/jest'; -import { AppLogic } from '../../../../app_logic'; jest.mock('../../../../app_logic', () => ({ AppLogic: { values: { isOrganization: true } }, })); +import { AppLogic } from '../../../../app_logic'; -import { SourcesLogic } from '../../sources_logic'; - -import { nextTick } from '@kbn/test/jest'; - -import { CustomSource } from '../../../../types'; import { SOURCES_PATH, getSourcesPath } from '../../../../routes'; - -import { sourceConfigData } from '../../../../__mocks__/content_sources.mock'; +import { CustomSource } from '../../../../types'; +import { SourcesLogic } from '../../sources_logic'; import { AddSourceLogic, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts index f10e81487567e..4e996aff6f5b0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts @@ -5,33 +5,27 @@ * 2.0. */ -import { keys, pickBy } from 'lodash'; - -import { kea, MakeLogicType } from 'kea'; - import { Search } from 'history'; +import { kea, MakeLogicType } from 'kea'; +import { keys, pickBy } from 'lodash'; import { i18n } from '@kbn/i18n'; - import { HttpFetchQuery } from 'src/core/public'; -import { HttpLogic } from '../../../../../shared/http'; -import { KibanaLogic } from '../../../../../shared/kibana'; -import { parseQueryParams } from '../../../../../shared/query_params'; - import { flashAPIErrors, setSuccessMessage, clearFlashMessages, } from '../../../../../shared/flash_messages'; - -import { staticSourceData } from '../../source_data'; -import { SOURCES_PATH, getSourcesPath } from '../../../../routes'; -import { CUSTOM_SERVICE_TYPE, WORKPLACE_SEARCH_URL_PREFIX } from '../../../../constants'; - +import { HttpLogic } from '../../../../../shared/http'; +import { KibanaLogic } from '../../../../../shared/kibana'; +import { parseQueryParams } from '../../../../../shared/query_params'; import { AppLogic } from '../../../../app_logic'; -import { SourcesLogic } from '../../sources_logic'; +import { CUSTOM_SERVICE_TYPE, WORKPLACE_SEARCH_URL_PREFIX } from '../../../../constants'; +import { SOURCES_PATH, getSourcesPath } from '../../../../routes'; import { CustomSource } from '../../../../types'; +import { staticSourceData } from '../../source_data'; +import { SourcesLogic } from '../../sources_logic'; export interface AddSourceProps { sourceIndex: number; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/available_sources_list.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/available_sources_list.test.tsx index 43f1486644c72..fcb55f24ddb03 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/available_sources_list.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/available_sources_list.test.tsx @@ -7,10 +7,10 @@ import '../../../../../__mocks__/shallow_useeffect.mock'; import { setMockValues } from '../../../../../__mocks__'; - import { mergedAvailableSources } from '../../../../__mocks__/content_sources.mock'; import React from 'react'; + import { shallow } from 'enzyme'; import { EuiCard, EuiToolTip, EuiTitle } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/available_sources_list.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/available_sources_list.tsx index 8060f765a91b0..fafc1ea54a6cf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/available_sources_list.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/available_sources_list.tsx @@ -7,7 +7,7 @@ import React from 'react'; -import { i18n } from '@kbn/i18n'; +import { useValues } from 'kea'; import { EuiCard, @@ -18,15 +18,13 @@ import { EuiText, EuiToolTip, } from '@elastic/eui'; - -import { useValues } from 'kea'; +import { i18n } from '@kbn/i18n'; import { LicensingLogic } from '../../../../../shared/licensing'; import { EuiLinkTo } from '../../../../../shared/react_router_helpers'; - import { SourceIcon } from '../../../../components/shared/source_icon'; -import { SourceDataItem } from '../../../../types'; import { ADD_CUSTOM_PATH, getSourcesPath } from '../../../../routes'; +import { SourceDataItem } from '../../../../types'; import { AVAILABLE_SOURCE_EMPTY_STATE, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.test.tsx index cd40da7f6b376..163da5297e370 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { ConfigCompleted } from './config_completed'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.tsx index bd85b3c7c2dd5..1d4f1f2fca980 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_completed.tsx @@ -7,9 +7,6 @@ import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - import { EuiButton, EuiFlexGroup, @@ -20,7 +17,10 @@ import { EuiText, EuiTextAlign, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiLinkTo, EuiButtonTo } from '../../../../../shared/react_router_helpers'; import { getSourcesPath, ADD_SOURCE_PATH, @@ -28,8 +28,6 @@ import { PRIVATE_SOURCES_DOCS_URL, } from '../../../../routes'; -import { EuiLinkTo, EuiButtonTo } from '../../../../../shared/react_router_helpers'; - import { CONFIG_COMPLETED_PRIVATE_SOURCES_DOCS_LINK, CONFIG_COMPLETED_CONFIGURE_NEW_BUTTON, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_docs_links.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_docs_links.test.tsx index b56f36df5486e..914eca94ad6f3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_docs_links.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_docs_links.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { EuiButtonEmpty } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_docs_links.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_docs_links.tsx index 259e911d6d54f..043d28e9dba03 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_docs_links.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/config_docs_links.tsx @@ -7,9 +7,8 @@ import React from 'react'; -import { i18n } from '@kbn/i18n'; - import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { DOCUMENTATION_LINK_TITLE } from '../../../../constants'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_intro.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_intro.test.tsx index 2d26982cbc2f5..2ebc021925abf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_intro.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_intro.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { EuiText, EuiTitle } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_intro.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_intro.tsx index ff1caafb91bdb..914eee74dfc4e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_intro.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configuration_intro.tsx @@ -7,9 +7,6 @@ import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - import { EuiBadge, EuiButton, @@ -20,6 +17,10 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import connectionIllustration from '../../../../assets/connection_illustration.svg'; import { CONFIG_INTRO_ALT_TEXT, @@ -31,8 +32,6 @@ import { CONFIG_INTRO_STEP2_TEXT, } from './constants'; -import connectionIllustration from '../../../../assets/connection_illustration.svg'; - interface ConfigurationIntroProps { header: React.ReactNode; name: string; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.test.tsx index a3b572737bdeb..099989255bf47 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.test.tsx @@ -9,6 +9,7 @@ import '../../../../../__mocks__/shallow_useeffect.mock'; import { setMockActions, setMockValues } from '../../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; import { EuiForm, EuiFieldText } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.tsx index 2d0113f1d0e7d..36242f5523e77 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_custom.tsx @@ -9,8 +9,6 @@ import React, { ChangeEvent, FormEvent } from 'react'; import { useActions, useValues } from 'kea'; -import { FormattedMessage } from '@kbn/i18n/react'; - import { EuiButton, EuiFieldText, @@ -20,8 +18,10 @@ import { EuiSpacer, EuiText, } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import { CUSTOM_SOURCE_DOCS_URL } from '../../../../routes'; + import { AddSourceLogic } from './add_source_logic'; import { CONFIG_CUSTOM_BUTTON } from './constants'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_oauth.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_oauth.test.tsx index a57ff390150ea..985488558c984 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_oauth.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_oauth.test.tsx @@ -9,6 +9,7 @@ import '../../../../../__mocks__/shallow_useeffect.mock'; import { setMockActions, setMockValues } from '../../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; import { EuiCheckboxGroup } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_oauth.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_oauth.tsx index 3eae438eb960c..eb7b61ef658db 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_oauth.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configure_oauth.tsx @@ -6,10 +6,10 @@ */ import React, { useEffect, useState, FormEvent } from 'react'; +import { useLocation } from 'react-router-dom'; import { Location } from 'history'; import { useActions, useValues } from 'kea'; -import { useLocation } from 'react-router-dom'; import { EuiButton, @@ -19,13 +19,12 @@ import { EuiFormRow, EuiSpacer, } from '@elastic/eui'; - import { EuiCheckboxGroupIdToSelectedMap } from '@elastic/eui/src/components/form/checkbox/checkbox_group'; -import { parseQueryParams } from '../../../../../../applications/shared/query_params'; import { Loading } from '../../../../../../applications/shared/loading'; -import { AddSourceLogic } from './add_source_logic'; +import { parseQueryParams } from '../../../../../../applications/shared/query_params'; +import { AddSourceLogic } from './add_source_logic'; import { CONFIG_OAUTH_LABEL, CONFIG_OAUTH_BUTTON } from './constants'; interface OauthQueryParams { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.test.tsx index 3bb7d42748f25..2e2e04556cdb7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.test.tsx @@ -5,13 +5,14 @@ * 2.0. */ +import { mergedConfiguredSources } from '../../../../__mocks__/content_sources.mock'; + import React from 'react'; + import { shallow } from 'enzyme'; import { EuiPanel } from '@elastic/eui'; -import { mergedConfiguredSources } from '../../../../__mocks__/content_sources.mock'; - import { ConfiguredSourcesList } from './configured_sources_list'; describe('ConfiguredSourcesList', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.tsx index cb5e96a4019a1..5f64913410d4c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/configured_sources_list.tsx @@ -21,8 +21,8 @@ import { import { EuiButtonEmptyTo } from '../../../../../shared/react_router_helpers'; import { SourceIcon } from '../../../../components/shared/source_icon'; -import { SourceDataItem } from '../../../../types'; import { getSourcesPath } from '../../../../routes'; +import { SourceDataItem } from '../../../../types'; import { CONFIGURED_SOURCES_LIST_UNCONNECTED_TOOLTIP, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.test.tsx index cff95136968db..b795b0af09944 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.test.tsx @@ -9,12 +9,14 @@ import '../../../../../__mocks__/shallow_useeffect.mock'; import { setMockActions, setMockValues } from '../../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; import { EuiBadge, EuiCallOut, EuiSwitch } from '@elastic/eui'; import { FeatureIds } from '../../../../types'; import { staticSourceData } from '../../source_data'; + import { ConnectInstance } from './connect_instance'; describe('ConnectInstance', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx index ea5d556350759..d85bd21c54e5a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/connect_instance.tsx @@ -8,8 +8,6 @@ import React, { useState, useEffect, FormEvent } from 'react'; import { useActions, useValues } from 'kea'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButton, @@ -27,16 +25,16 @@ import { EuiBadge, EuiBadgeGroup, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { LicensingLogic } from '../../../../../../applications/shared/licensing'; - import { AppLogic } from '../../../../app_logic'; -import { AddSourceLogic } from './add_source_logic'; -import { FeatureIds, Configuration, Features } from '../../../../types'; import { DOCUMENT_PERMISSIONS_DOCS_URL } from '../../../../routes'; -import { SourceFeatures } from './source_features'; - +import { FeatureIds, Configuration, Features } from '../../../../types'; import { LEARN_MORE_LINK } from '../../constants'; + +import { AddSourceLogic } from './add_source_logic'; import { CONNECT_REMOTE, CONNECT_PRIVATE, @@ -47,6 +45,7 @@ import { CONNECT_NOT_SYNCED_TITLE, CONNECT_NOT_SYNCED_TEXT, } from './constants'; +import { SourceFeatures } from './source_features'; interface ConnectInstanceProps { header: React.ReactNode; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/re_authenticate.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/re_authenticate.test.tsx index 94c2e734751ee..38b6925008181 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/re_authenticate.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/re_authenticate.test.tsx @@ -9,6 +9,7 @@ import '../../../../../__mocks__/shallow_useeffect.mock'; import { setMockActions, setMockValues } from '../../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; import { ReAuthenticate } from './re_authenticate'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/re_authenticate.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/re_authenticate.tsx index 0cdf461f2d64c..15082f6de85bf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/re_authenticate.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/re_authenticate.tsx @@ -6,13 +6,14 @@ */ import React, { useEffect, useState, FormEvent } from 'react'; +import { useLocation } from 'react-router-dom'; import { Location } from 'history'; import { useActions, useValues } from 'kea'; -import { useLocation } from 'react-router-dom'; -import { i18n } from '@kbn/i18n'; import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + import { parseQueryParams } from '../../../../../../applications/shared/query_params'; import { AddSourceLogic } from './add_source_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_config.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_config.test.tsx index 4d1955d7928a8..c0f7f1139cb73 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_config.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_config.test.tsx @@ -7,18 +7,18 @@ import '../../../../../__mocks__/shallow_useeffect.mock'; import { setMockActions, setMockValues } from '../../../../../__mocks__'; +import { sourceConfigData } from '../../../../__mocks__/content_sources.mock'; import React from 'react'; + import { shallow } from 'enzyme'; import { EuiSteps, EuiButton, EuiButtonEmpty } from '@elastic/eui'; -import { sourceConfigData } from '../../../../__mocks__/content_sources.mock'; +import { ApiKey } from '../../../../components/shared/api_key'; import { staticSourceData } from '../../source_data'; -import { ApiKey } from '../../../../components/shared/api_key'; import { ConfigDocsLinks } from './config_docs_links'; - import { SaveConfig } from './save_config'; describe('SaveConfig', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_config.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_config.tsx index f6d5d0f4066ab..06ee2b6eb40f1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_config.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_config.tsx @@ -8,7 +8,6 @@ import React, { FormEvent } from 'react'; import { useActions, useValues } from 'kea'; -import { i18n } from '@kbn/i18n'; import { EuiButton, @@ -21,7 +20,10 @@ import { EuiSpacer, EuiSteps, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { LicensingLogic } from '../../../../../../applications/shared/licensing'; +import { ApiKey } from '../../../../components/shared/api_key'; import { PUBLIC_KEY_LABEL, CONSUMER_KEY_LABEL, @@ -31,16 +33,11 @@ import { CLIENT_SECRET_LABEL, REMOVE_BUTTON, } from '../../../../constants'; - -import { OAUTH_SAVE_CONFIG_BUTTON, OAUTH_BACK_BUTTON, OAUTH_STEP_2 } from './constants'; - -import { LicensingLogic } from '../../../../../../applications/shared/licensing'; - -import { ApiKey } from '../../../../components/shared/api_key'; -import { AddSourceLogic } from './add_source_logic'; import { Configuration } from '../../../../types'; +import { AddSourceLogic } from './add_source_logic'; import { ConfigDocsLinks } from './config_docs_links'; +import { OAUTH_SAVE_CONFIG_BUTTON, OAUTH_BACK_BUTTON, OAUTH_STEP_2 } from './constants'; interface SaveConfigProps { header: React.ReactNode; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.test.tsx index 551bd7f1bb006..5ed777322cc08 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { EuiLink, EuiPanel, EuiTitle } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx index a61ad1aeb728a..b42bd674109fe 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx @@ -7,9 +7,6 @@ import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - import { EuiFlexGroup, EuiFlexItem, @@ -22,12 +19,12 @@ import { EuiLink, EuiPanel, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { EuiLinkTo } from '../../../../../shared/react_router_helpers'; import { CredentialItem } from '../../../../components/shared/credential_item'; import { LicenseBadge } from '../../../../components/shared/license_badge'; - -import { CustomSource } from '../../../../types'; import { SOURCES_PATH, SOURCE_DISPLAY_SETTINGS_PATH, @@ -36,7 +33,7 @@ import { getContentSourcePath, getSourcesPath, } from '../../../../routes'; - +import { CustomSource } from '../../../../types'; import { ACCESS_TOKEN_LABEL, ID_LABEL, LEARN_CUSTOM_FEATURES_BUTTON } from '../../constants'; import { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/source_features.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/source_features.test.tsx index ccc6d05df5f9a..cd8ba37695ac6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/source_features.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/source_features.test.tsx @@ -11,10 +11,10 @@ import React from 'react'; import { EuiPanel } from '@elastic/eui'; -import { SourceFeatures } from './source_features'; - import { staticSourceData } from '../../source_data'; +import { SourceFeatures } from './source_features'; + describe('SourceFeatures', () => { const { features, objTypes } = staticSourceData[0]; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/source_features.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/source_features.tsx index 186e8fcdc3790..f304a1a36d9dd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/source_features.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/source_features.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { useValues } from 'kea'; -import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFlexGroup, @@ -19,13 +18,13 @@ import { EuiText, EuiTitle, } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import { LicensingLogic } from '../../../../../../applications/shared/licensing'; - import { AppLogic } from '../../../../app_logic'; import { LicenseBadge } from '../../../../components/shared/license_badge'; -import { Features, FeatureIds } from '../../../../types'; import { ENT_SEARCH_LICENSE_MANAGEMENT } from '../../../../routes'; +import { Features, FeatureIds } from '../../../../types'; import { SOURCE_FEATURES_SEARCHABLE, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/custom_source_icon.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/custom_source_icon.test.tsx index 6567f74bd8790..fcce69d70ad50 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/custom_source_icon.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/custom_source_icon.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { CustomSourceIcon } from './custom_source_icon'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.test.tsx index 72744690baf30..feebc7f8d445e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.test.tsx @@ -8,24 +8,21 @@ import '../../../../../__mocks__/shallow_useeffect.mock'; import { mockKibanaValues } from '../../../../../__mocks__'; - import { setMockValues, setMockActions } from '../../../../../__mocks__'; import { unmountHandler } from '../../../../../__mocks__/shallow_useeffect.mock'; - -import { shallow } from 'enzyme'; +import { exampleResult } from '../../../../__mocks__/content_sources.mock'; import React from 'react'; -import { EuiButton, EuiTabbedContent } from '@elastic/eui'; +import { shallow } from 'enzyme'; -import { exampleResult } from '../../../../__mocks__/content_sources.mock'; +import { EuiButton, EuiTabbedContent } from '@elastic/eui'; import { Loading } from '../../../../../shared/loading'; import { ViewContentHeader } from '../../../../components/shared/view_content_header'; -import { FieldEditorModal } from './field_editor_modal'; - import { DisplaySettings } from './display_settings'; +import { FieldEditorModal } from './field_editor_modal'; describe('DisplaySettings', () => { const { navigateToUrl } = mockKibanaValues; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx index 62beb4e40793b..29266cdefe584 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings.tsx @@ -19,21 +19,18 @@ import { EuiTabbedContentTab, } from '@elastic/eui'; +import { clearFlashMessages } from '../../../../../shared/flash_messages'; +import { KibanaLogic } from '../../../../../shared/kibana'; +import { Loading } from '../../../../../shared/loading'; +import { AppLogic } from '../../../../app_logic'; +import { ViewContentHeader } from '../../../../components/shared/view_content_header'; +import { SAVE_BUTTON } from '../../../../constants'; import { DISPLAY_SETTINGS_RESULT_DETAIL_PATH, DISPLAY_SETTINGS_SEARCH_RESULT_PATH, getContentSourcePath, } from '../../../../routes'; -import { clearFlashMessages } from '../../../../../shared/flash_messages'; - -import { KibanaLogic } from '../../../../../shared/kibana'; -import { AppLogic } from '../../../../app_logic'; - -import { Loading } from '../../../../../shared/loading'; -import { ViewContentHeader } from '../../../../components/shared/view_content_header'; - -import { SAVE_BUTTON } from '../../../../constants'; import { UNSAVED_MESSAGE, DISPLAY_SETTINGS_TITLE, @@ -43,9 +40,7 @@ import { SEARCH_RESULTS_LABEL, RESULT_DETAIL_LABEL, } from './constants'; - import { DisplaySettingsLogic } from './display_settings_logic'; - import { FieldEditorModal } from './field_editor_modal'; import { ResultDetail } from './result_detail'; import { SearchResults } from './search_results'; 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 c51f3e97bf155..73df0298ecd19 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 @@ -5,25 +5,22 @@ * 2.0. */ -import { LogicMounter } from '../../../../../__mocks__/kea.mock'; +import { LogicMounter, mockFlashMessageHelpers, mockHttpValues } from '../../../../../__mocks__'; +import { exampleResult } from '../../../../__mocks__/content_sources.mock'; -import { mockFlashMessageHelpers, mockHttpValues } from '../../../../../__mocks__'; +import { nextTick } from '@kbn/test/jest'; const contentSource = { id: 'source123' }; jest.mock('../../source_logic', () => ({ SourceLogic: { values: { contentSource } }, })); -import { AppLogic } from '../../../../app_logic'; jest.mock('../../../../app_logic', () => ({ AppLogic: { values: { isOrganization: true } }, })); +import { AppLogic } from '../../../../app_logic'; -import { nextTick } from '@kbn/test/jest'; - -import { exampleResult } from '../../../../__mocks__/content_sources.mock'; import { LEAVE_UNASSIGNED_FIELD } from './constants'; - import { DisplaySettingsLogic, defaultSearchResultConfig } from './display_settings_logic'; describe('DisplaySettingsLogic', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts index 7c5946d08292c..62d959083af59 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_logic.ts @@ -5,24 +5,23 @@ * 2.0. */ -import { cloneDeep, isEqual, differenceBy } from 'lodash'; import { DropResult } from 'react-beautiful-dnd'; import { kea, MakeLogicType } from 'kea'; - -import { HttpLogic } from '../../../../../shared/http'; +import { cloneDeep, isEqual, differenceBy } from 'lodash'; import { setSuccessMessage, clearFlashMessages, flashAPIErrors, } from '../../../../../shared/flash_messages'; - +import { HttpLogic } from '../../../../../shared/http'; import { AppLogic } from '../../../../app_logic'; +import { DetailField, SearchResultConfig, OptionValue, Result } from '../../../../types'; import { SourceLogic } from '../../source_logic'; -import { DetailField, SearchResultConfig, OptionValue, Result } from '../../../../types'; import { LEAVE_UNASSIGNED_FIELD, SUCCESS_MESSAGE } from './constants'; + export interface DisplaySettingsResponseProps { sourceName: string; searchResultConfig: SearchResultConfig; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_router.test.tsx index f216cbf286b94..f04afe60aa49d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_router.test.tsx @@ -10,12 +10,11 @@ import '../../../../../__mocks__/shallow_useeffect.mock'; import { setMockValues } from '../../../../../__mocks__'; import React from 'react'; -import { shallow } from 'enzyme'; - import { Route, Switch } from 'react-router-dom'; -import { DisplaySettings } from './display_settings'; +import { shallow } from 'enzyme'; +import { DisplaySettings } from './display_settings'; import { DisplaySettingsRouter } from './display_settings_router'; describe('DisplaySettingsRouter', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_router.tsx index fa9817494ee09..bd753631ed48c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/display_settings_router.tsx @@ -6,12 +6,11 @@ */ import React from 'react'; +import { Route, Switch } from 'react-router-dom'; import { useValues } from 'kea'; -import { Route, Switch } from 'react-router-dom'; import { AppLogic } from '../../../../app_logic'; - import { DISPLAY_SETTINGS_RESULT_DETAIL_PATH, DISPLAY_SETTINGS_SEARCH_RESULT_PATH, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.test.tsx index 381e4fe4c0b25..15e1fe0ed417c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.test.tsx @@ -8,11 +8,11 @@ import '../../../../../__mocks__/shallow_useeffect.mock'; import { setMockValues } from '../../../../../__mocks__'; -import { shallow } from 'enzyme'; +import { exampleResult } from '../../../../__mocks__/content_sources.mock'; import React from 'react'; -import { exampleResult } from '../../../../__mocks__/content_sources.mock'; +import { shallow } from 'enzyme'; import { ExampleResultDetailCard } from './example_result_detail_card'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.tsx index 46c06f28af6d6..93a7d660215f0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_result_detail_card.tsx @@ -14,9 +14,8 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiText, EuiTitle } from '@elasti import { URL_LABEL } from '../../../../constants'; -import { DisplaySettingsLogic } from './display_settings_logic'; - import { CustomSourceIcon } from './custom_source_icon'; +import { DisplaySettingsLogic } from './display_settings_logic'; import { TitleField } from './title_field'; export const ExampleResultDetailCard: React.FC = () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.test.tsx index d08195f3e83bc..6f90c1045ae31 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.test.tsx @@ -8,14 +8,13 @@ import '../../../../../__mocks__/shallow_useeffect.mock'; import { setMockValues } from '../../../../../__mocks__'; -import { shallow } from 'enzyme'; +import { exampleResult } from '../../../../__mocks__/content_sources.mock'; import React from 'react'; -import { exampleResult } from '../../../../__mocks__/content_sources.mock'; +import { shallow } from 'enzyme'; import { CustomSourceIcon } from './custom_source_icon'; - import { ExampleSearchResultGroup } from './example_search_result_group'; describe('ExampleSearchResultGroup', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.tsx index 5d5f73467f82c..df89eed38ae92 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_search_result_group.tsx @@ -7,15 +7,15 @@ import React from 'react'; -import { isColorDark, hexToRgb } from '@elastic/eui'; import classNames from 'classnames'; import { useValues } from 'kea'; -import { DESCRIPTION_LABEL } from '../../../../constants'; +import { isColorDark, hexToRgb } from '@elastic/eui'; -import { DisplaySettingsLogic } from './display_settings_logic'; +import { DESCRIPTION_LABEL } from '../../../../constants'; import { CustomSourceIcon } from './custom_source_icon'; +import { DisplaySettingsLogic } from './display_settings_logic'; import { SubtitleField } from './subtitle_field'; import { TitleField } from './title_field'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.test.tsx index 6241bcf05fbff..49845e79d86aa 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.test.tsx @@ -8,14 +8,13 @@ import '../../../../../__mocks__/shallow_useeffect.mock'; import { setMockValues } from '../../../../../__mocks__'; -import { shallow } from 'enzyme'; +import { exampleResult } from '../../../../__mocks__/content_sources.mock'; import React from 'react'; -import { exampleResult } from '../../../../__mocks__/content_sources.mock'; +import { shallow } from 'enzyme'; import { CustomSourceIcon } from './custom_source_icon'; - import { ExampleStandoutResult } from './example_standout_result'; describe('ExampleStandoutResult', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.tsx index 3c139001d3ea2..48c3149e622bd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/example_standout_result.tsx @@ -14,9 +14,8 @@ import { isColorDark, hexToRgb } from '@elastic/eui'; import { DESCRIPTION_LABEL } from '../../../../constants'; -import { DisplaySettingsLogic } from './display_settings_logic'; - import { CustomSourceIcon } from './custom_source_icon'; +import { DisplaySettingsLogic } from './display_settings_logic'; import { SubtitleField } from './subtitle_field'; import { TitleField } from './title_field'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/field_editor_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/field_editor_modal.test.tsx index 82687165d0535..fe7bced843841 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/field_editor_modal.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/field_editor_modal.test.tsx @@ -8,13 +8,13 @@ import '../../../../../__mocks__/shallow_useeffect.mock'; import { setMockValues, setMockActions } from '../../../../../__mocks__'; -import { shallow } from 'enzyme'; +import { exampleResult } from '../../../../__mocks__/content_sources.mock'; import React from 'react'; -import { EuiModal, EuiSelect, EuiFieldText } from '@elastic/eui'; +import { shallow } from 'enzyme'; -import { exampleResult } from '../../../../__mocks__/content_sources.mock'; +import { EuiModal, EuiSelect, EuiFieldText } from '@elastic/eui'; import { FieldEditorModal } from './field_editor_modal'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/result_detail.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/result_detail.test.tsx index 217a8142af5d5..768573ce80fee 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/result_detail.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/result_detail.test.tsx @@ -6,9 +6,8 @@ */ import '../../../../../__mocks__/shallow_useeffect.mock'; - import { setMockValues, setMockActions } from '../../../../../__mocks__'; -import { shallow, mount } from 'enzyme'; +import { exampleResult } from '../../../../__mocks__/content_sources.mock'; /** * Mocking necessary due to console warnings from react d-n-d, which EUI uses @@ -40,12 +39,11 @@ jest.mock('react-beautiful-dnd', () => ({ import React from 'react'; -import { EuiTextColor } from '@elastic/eui'; +import { shallow, mount } from 'enzyme'; -import { exampleResult } from '../../../../__mocks__/content_sources.mock'; +import { EuiTextColor } from '@elastic/eui'; import { ExampleResultDetailCard } from './example_result_detail_card'; - import { ResultDetail } from './result_detail'; describe('ResultDetail', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/result_detail.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/result_detail.tsx index 8382ddc9e82b3..6832f075476e7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/result_detail.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/result_detail.tsx @@ -28,10 +28,9 @@ import { } from '@elastic/eui'; import { ADD_FIELD_LABEL, EDIT_FIELD_LABEL, REMOVE_FIELD_LABEL } from '../../../../constants'; -import { VISIBLE_FIELDS_TITLE, EMPTY_FIELDS_DESCRIPTION, PREVIEW_TITLE } from './constants'; +import { VISIBLE_FIELDS_TITLE, EMPTY_FIELDS_DESCRIPTION, PREVIEW_TITLE } from './constants'; import { DisplaySettingsLogic } from './display_settings_logic'; - import { ExampleResultDetailCard } from './example_result_detail_card'; export const ResultDetail: React.FC = () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.test.tsx index 26116a7e736bc..28de0006f162f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.test.tsx @@ -8,16 +8,15 @@ import '../../../../../__mocks__/shallow_useeffect.mock'; import { setMockValues, setMockActions } from '../../../../../__mocks__'; -import { shallow } from 'enzyme'; +import { exampleResult } from '../../../../__mocks__/content_sources.mock'; import React from 'react'; -import { exampleResult } from '../../../../__mocks__/content_sources.mock'; +import { shallow } from 'enzyme'; +import { LEAVE_UNASSIGNED_FIELD } from './constants'; import { ExampleSearchResultGroup } from './example_search_result_group'; import { ExampleStandoutResult } from './example_standout_result'; - -import { LEAVE_UNASSIGNED_FIELD } from './constants'; import { SearchResults } from './search_results'; describe('SearchResults', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.tsx index b2ba2b13e5ec3..859fb2d5d2a20 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/search_results.tsx @@ -21,9 +21,8 @@ import { EuiTitle, } from '@elastic/eui'; -import { DisplaySettingsLogic } from './display_settings_logic'; - import { DESCRIPTION_LABEL } from '../../../../constants'; + import { LEAVE_UNASSIGNED_FIELD, SEARCH_RESULTS_TITLE, @@ -34,7 +33,7 @@ import { STANDARD_RESULTS_TITLE, STANDARD_RESULTS_DESCRIPTION, } from './constants'; - +import { DisplaySettingsLogic } from './display_settings_logic'; import { ExampleSearchResultGroup } from './example_search_result_group'; import { ExampleStandoutResult } from './example_standout_result'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/subtitle_field.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/subtitle_field.test.tsx index 812af1b1fd26b..76c28ae3d4060 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/subtitle_field.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/subtitle_field.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { SubtitleField } from './subtitle_field'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/title_field.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/title_field.test.tsx index f2a82f058c0de..2ed4aa0b0fad1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/title_field.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/display_settings/title_field.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { TitleField } from './title_field'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.test.tsx index 0e91d2a3a4a28..a30f1bfbd596a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.test.tsx @@ -8,14 +8,14 @@ import '../../../../__mocks__/shallow_useeffect.mock'; import { setMockValues } from '../../../../__mocks__'; +import { fullContentSources } from '../../../__mocks__/content_sources.mock'; import React from 'react'; + import { shallow } from 'enzyme'; import { EuiEmptyPrompt, EuiPanel, EuiTable } from '@elastic/eui'; -import { fullContentSources } from '../../../__mocks__/content_sources.mock'; - import { Loading } from '../../../../shared/loading'; import { ComponentLoader } from '../../../components/shared/component_loader'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx index a67adfdd3802a..34d7edd99c376 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx @@ -9,8 +9,6 @@ import React from 'react'; import { useValues } from 'kea'; -import { FormattedMessage } from '@kbn/i18n/react'; - import { EuiEmptyPrompt, EuiFlexGroup, @@ -30,7 +28,21 @@ import { EuiTextColor, EuiTitle, } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { Loading } from '../../../../shared/loading'; +import { EuiPanelTo } from '../../../../shared/react_router_helpers'; +import { AppLogic } from '../../../app_logic'; +import aclImage from '../../../assets/supports_acl.svg'; +import { ComponentLoader } from '../../../components/shared/component_loader'; +import { CredentialItem } from '../../../components/shared/credential_item'; +import { LicenseBadge } from '../../../components/shared/license_badge'; +import { ViewContentHeader } from '../../../components/shared/view_content_header'; +import { + RECENT_ACTIVITY_TITLE, + CREDENTIALS_TITLE, + DOCUMENTATION_LINK_TITLE, +} from '../../../constants'; import { CUSTOM_SOURCE_DOCS_URL, DOCUMENT_PERMISSIONS_DOCS_URL, @@ -38,12 +50,6 @@ import { EXTERNAL_IDENTITIES_DOCS_URL, getGroupPath, } from '../../../routes'; - -import { - RECENT_ACTIVITY_TITLE, - CREDENTIALS_TITLE, - DOCUMENTATION_LINK_TITLE, -} from '../../../constants'; import { SOURCES_NO_CONTENT_TITLE, CONTENT_SUMMARY_TITLE, @@ -70,17 +76,6 @@ import { DOC_PERMISSIONS_DESCRIPTION, CUSTOM_CALLOUT_TITLE, } from '../constants'; - -import { AppLogic } from '../../../app_logic'; - -import { ComponentLoader } from '../../../components/shared/component_loader'; -import { CredentialItem } from '../../../components/shared/credential_item'; -import { ViewContentHeader } from '../../../components/shared/view_content_header'; -import { LicenseBadge } from '../../../components/shared/license_badge'; -import { Loading } from '../../../../shared/loading'; -import { EuiPanelTo } from '../../../../shared/react_router_helpers'; - -import aclImage from '../../../assets/supports_acl.svg'; import { SourceLogic } from '../source_logic'; export const Overview: React.FC = () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.test.tsx index b30c5d78fd42f..ccf3275ffd96f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.test.tsx @@ -8,21 +8,20 @@ import '../../../../../__mocks__/shallow_useeffect.mock'; import { setMockValues, setMockActions } from '../../../../../__mocks__'; +import { mostRecentIndexJob } from '../../../../__mocks__/content_sources.mock'; import React from 'react'; + import { shallow } from 'enzyme'; import { EuiEmptyPrompt, EuiFieldSearch } from '@elastic/eui'; -import { mostRecentIndexJob } from '../../../../__mocks__/content_sources.mock'; - import { IndexingStatus } from '../../../../../shared/indexing_status'; import { Loading } from '../../../../../shared/loading'; import { SchemaAddFieldModal } from '../../../../../shared/schema/schema_add_field_modal'; -import { SchemaFieldsTable } from './schema_fields_table'; - import { Schema } from './schema'; +import { SchemaFieldsTable } from './schema_fields_table'; describe('Schema', () => { const initializeSchema = jest.fn(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.tsx index fe48e1c14ff41..77d1002a9ad26 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema.tsx @@ -20,17 +20,12 @@ import { EuiPanel, } from '@elastic/eui'; -import { getReindexJobRoute } from '../../../../routes'; -import { AppLogic } from '../../../../app_logic'; - +import { IndexingStatus } from '../../../../../shared/indexing_status'; import { Loading } from '../../../../../shared/loading'; -import { ViewContentHeader } from '../../../../components/shared/view_content_header'; - import { SchemaAddFieldModal } from '../../../../../shared/schema/schema_add_field_modal'; -import { IndexingStatus } from '../../../../../shared/indexing_status'; - -import { SchemaFieldsTable } from './schema_fields_table'; -import { SchemaLogic } from './schema_logic'; +import { AppLogic } from '../../../../app_logic'; +import { ViewContentHeader } from '../../../../components/shared/view_content_header'; +import { getReindexJobRoute } from '../../../../routes'; import { SCHEMA_ADD_FIELD_BUTTON, @@ -42,6 +37,8 @@ import { SCHEMA_EMPTY_SCHEMA_TITLE, SCHEMA_EMPTY_SCHEMA_DESCRIPTION, } from './constants'; +import { SchemaFieldsTable } from './schema_fields_table'; +import { SchemaLogic } from './schema_logic'; export const Schema: React.FC = () => { const { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.test.tsx index 421aa04692bd7..e9276b8ec3878 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.test.tsx @@ -10,9 +10,10 @@ import '../../../../../__mocks__/shallow_useeffect.mock'; import { setMockValues, setMockActions } from '../../../../../__mocks__'; import React from 'react'; -import { shallow } from 'enzyme'; import { useParams } from 'react-router-dom'; +import { shallow } from 'enzyme'; + import { SchemaErrorsAccordion } from '../../../../../shared/schema/schema_errors_accordion'; import { SchemaChangeErrors } from './schema_change_errors'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.tsx index a212052e1beba..29cb2b7589220 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_change_errors.tsx @@ -14,8 +14,9 @@ import { EuiSpacer } from '@elastic/eui'; import { SchemaErrorsAccordion } from '../../../../../shared/schema/schema_errors_accordion'; import { ViewContentHeader } from '../../../../components/shared/view_content_header'; -import { SchemaLogic } from './schema_logic'; + import { SCHEMA_ERRORS_HEADING } from './constants'; +import { SchemaLogic } from './schema_logic'; export const SchemaChangeErrors: React.FC = () => { const { activeReindexJobId, sourceId } = useParams() as { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_fields_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_fields_table.test.tsx index a9d6494dcee00..bc0363d55da69 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_fields_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_fields_table.test.tsx @@ -10,6 +10,7 @@ import '../../../../../__mocks__/shallow_useeffect.mock'; import { setMockValues, setMockActions } from '../../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; import { SchemaExistingField } from '../../../../../shared/schema/schema_existing_field'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_fields_table.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_fields_table.tsx index a683d9384f636..3f56a2cfc745b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_fields_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_fields_table.tsx @@ -9,8 +9,6 @@ import React from 'react'; import { useActions, useValues } from 'kea'; -import { i18n } from '@kbn/i18n'; - import { EuiFlexGroup, EuiFlexItem, @@ -21,13 +19,15 @@ import { EuiTableRow, EuiTableRowCell, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { SchemaExistingField } from '../../../../../shared/schema/schema_existing_field'; -import { SchemaLogic } from './schema_logic'; + import { SCHEMA_ERRORS_TABLE_FIELD_NAME_HEADER, SCHEMA_ERRORS_TABLE_DATA_TYPE_HEADER, } from './constants'; +import { SchemaLogic } from './schema_logic'; export const SchemaFieldsTable: React.FC = () => { const { updateExistingFieldType } = useActions(SchemaLogic); 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 5957822eb8d49..af650d95efaf6 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 @@ -6,6 +6,7 @@ */ import { LogicMounter, mockFlashMessageHelpers, mockHttpValues } from '../../../../../__mocks__'; +import { mostRecentIndexJob } from '../../../../__mocks__/content_sources.mock'; import { nextTick } from '@kbn/test/jest'; @@ -14,7 +15,6 @@ jest.mock('../../source_logic', () => ({ SourceLogic: { values: { contentSource } }, })); -import { AppLogic } from '../../../../app_logic'; jest.mock('../../../../app_logic', () => ({ AppLogic: { values: { isOrganization: true } }, })); @@ -22,16 +22,15 @@ jest.mock('../../../../app_logic', () => ({ const spyScrollTo = jest.fn(); Object.defineProperty(global.window, 'scrollTo', { value: spyScrollTo }); -import { mostRecentIndexJob } from '../../../../__mocks__/content_sources.mock'; import { TEXT } from '../../../../../shared/constants/field_types'; import { ADD, UPDATE } from '../../../../../shared/constants/operations'; +import { AppLogic } from '../../../../app_logic'; import { SCHEMA_FIELD_ERRORS_ERROR_MESSAGE, SCHEMA_FIELD_ADDED_MESSAGE, SCHEMA_UPDATED_MESSAGE, } from './constants'; - import { SchemaLogic, dataTypeOptions } from './schema_logic'; describe('SchemaLogic', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.ts index 09b608af43536..9906efe707d85 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/schema/schema_logic.ts @@ -5,23 +5,20 @@ * 2.0. */ -import { cloneDeep, isEqual } from 'lodash'; import { kea, MakeLogicType } from 'kea'; - -import { HttpLogic } from '../../../../../shared/http'; +import { cloneDeep, isEqual } from 'lodash'; import { TEXT } from '../../../../../shared/constants/field_types'; import { ADD, UPDATE } from '../../../../../shared/constants/operations'; -import { IndexJob, TOperation, Schema, SchemaTypes } from '../../../../../shared/types'; -import { OptionValue } from '../../../../types'; - import { flashAPIErrors, setSuccessMessage, clearFlashMessages, } from '../../../../../shared/flash_messages'; - +import { HttpLogic } from '../../../../../shared/http'; +import { IndexJob, TOperation, Schema, SchemaTypes } from '../../../../../shared/types'; import { AppLogic } from '../../../../app_logic'; +import { OptionValue } from '../../../../types'; import { SourceLogic } from '../../source_logic'; import { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_added.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_added.test.tsx index d3256a86baebc..ddf89159b2675 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_added.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_added.test.tsx @@ -10,10 +10,10 @@ import '../../../../__mocks__/shallow_useeffect.mock'; import { setMockActions } from '../../../../__mocks__'; import React from 'react'; -import { shallow } from 'enzyme'; - import { useLocation } from 'react-router-dom'; +import { shallow } from 'enzyme'; + import { Loading } from '../../../../shared/loading'; import { SourceAdded } from './source_added'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_added.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_added.tsx index 5901c06b3f66c..5f1d2ed0c81c3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_added.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_added.tsx @@ -6,10 +6,10 @@ */ import React, { useEffect } from 'react'; +import { useLocation } from 'react-router-dom'; import { Location } from 'history'; import { useActions } from 'kea'; -import { useLocation } from 'react-router-dom'; import { Loading } from '../../../../shared/loading'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.test.tsx index 6a773b81909a3..e904efb73afc8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.test.tsx @@ -8,8 +8,11 @@ import '../../../../__mocks__/shallow_useeffect.mock'; import { setMockValues, setMockActions } from '../../../../__mocks__'; +import { fullContentSources, contentItems } from '../../../__mocks__/content_sources.mock'; +import { meta } from '../../../__mocks__/meta.mock'; import React from 'react'; + import { shallow } from 'enzyme'; import { @@ -21,12 +24,9 @@ import { EuiLink, } from '@elastic/eui'; -import { meta } from '../../../__mocks__/meta.mock'; -import { fullContentSources, contentItems } from '../../../__mocks__/content_sources.mock'; - +import { Loading } from '../../../../../applications/shared/loading'; import { DEFAULT_META } from '../../../../shared/constants'; import { ComponentLoader } from '../../../components/shared/component_loader'; -import { Loading } from '../../../../../applications/shared/loading'; import { TablePaginationBar } from '../../../components/shared/table_pagination_bar'; import { SourceContent } from './source_content'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx index 61676279ada03..bf18f88e47537 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_content.tsx @@ -11,9 +11,6 @@ import { useActions, useValues } from 'kea'; import { startCase } from 'lodash'; import moment from 'moment'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - import { EuiButton, EuiButtonEmpty, @@ -31,20 +28,17 @@ import { EuiTableRowCell, EuiLink, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; -import { CUSTOM_SOURCE_DOCS_URL } from '../../../routes'; -import { SourceContentItem } from '../../../types'; - +import { Loading } from '../../../../../applications/shared/loading'; import { TruncatedContent } from '../../../../shared/truncate'; - -const MAX_LENGTH = 28; - import { ComponentLoader } from '../../../components/shared/component_loader'; -import { Loading } from '../../../../../applications/shared/loading'; import { TablePaginationBar } from '../../../components/shared/table_pagination_bar'; import { ViewContentHeader } from '../../../components/shared/view_content_header'; - import { CUSTOM_SERVICE_TYPE } from '../../../constants'; +import { CUSTOM_SOURCE_DOCS_URL } from '../../../routes'; +import { SourceContentItem } from '../../../types'; import { NO_CONTENT_MESSAGE, CUSTOM_DOCUMENTATION_LINK, @@ -55,9 +49,10 @@ import { SOURCE_CONTENT_TITLE, CONTENT_LOADING_TEXT, } from '../constants'; - import { SourceLogic } from '../source_logic'; +const MAX_LENGTH = 28; + export const SourceContent: React.FC = () => { const [searchTerm, setSearchTerm] = useState(''); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.test.tsx index 7a8a932f391e1..7c4c02cdc9819 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { EuiBadge, EuiHealth, EuiText, EuiTitle } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx index 8334c34d6c615..765836191ff00 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_info_card.tsx @@ -18,7 +18,6 @@ import { } from '@elastic/eui'; import { SourceIcon } from '../../../components/shared/source_icon'; - import { REMOTE_SOURCE_LABEL, CREATED_LABEL, STATUS_LABEL, READY_TEXT } from '../constants'; interface SourceInfoCardProps { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.test.tsx index d73da79375ffe..f13189afe8252 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.test.tsx @@ -8,14 +8,14 @@ import '../../../../__mocks__/shallow_useeffect.mock'; import { setMockValues, setMockActions } from '../../../../__mocks__'; +import { fullContentSources, sourceConfigData } from '../../../__mocks__/content_sources.mock'; import React from 'react'; + import { shallow } from 'enzyme'; import { EuiConfirmModal } from '@elastic/eui'; -import { fullContentSources, sourceConfigData } from '../../../__mocks__/content_sources.mock'; - import { SourceConfigFields } from '../../../components/shared/source_config_fields'; import { SourceSettings } from './source_settings'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx index 2fa00c7f029f1..75a1779a1fda8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx @@ -6,12 +6,10 @@ */ import React, { useEffect, useState, ChangeEvent, FormEvent } from 'react'; +import { Link } from 'react-router-dom'; import { useActions, useValues } from 'kea'; import { isEmpty } from 'lodash'; -import { Link } from 'react-router-dom'; - -import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButton, @@ -23,7 +21,12 @@ import { EuiFlexItem, EuiFormRow, } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { AppLogic } from '../../../app_logic'; +import { ContentSection } from '../../../components/shared/content_section'; +import { SourceConfigFields } from '../../../components/shared/source_config_fields'; +import { ViewContentHeader } from '../../../components/shared/view_content_header'; import { CANCEL_BUTTON, OK_BUTTON, @@ -31,6 +34,8 @@ import { SAVE_CHANGES_BUTTON, REMOVE_BUTTON, } from '../../../constants'; +import { SourceDataItem } from '../../../types'; +import { AddSourceLogic } from '../components/add_source/add_source_logic'; import { SOURCE_SETTINGS_TITLE, SOURCE_SETTINGS_DESCRIPTION, @@ -41,16 +46,7 @@ import { SOURCE_REMOVE_TITLE, SOURCE_REMOVE_DESCRIPTION, } from '../constants'; - -import { ContentSection } from '../../../components/shared/content_section'; -import { SourceConfigFields } from '../../../components/shared/source_config_fields'; -import { ViewContentHeader } from '../../../components/shared/view_content_header'; - -import { SourceDataItem } from '../../../types'; -import { AppLogic } from '../../../app_logic'; -import { AddSourceLogic } from '../components/add_source/add_source_logic'; import { staticSourceData } from '../source_data'; - import { SourceLogic } from '../source_logic'; export const SourceSettings: React.FC = () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.test.tsx index fe5545668e4ce..59f3bfb0a5611 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.test.tsx @@ -8,12 +8,13 @@ import { setMockValues } from '../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; +import { SideNavLink } from '../../../../shared/layout'; import { CUSTOM_SERVICE_TYPE } from '../../../constants'; -import { SourceSubNav } from './source_sub_nav'; -import { SideNavLink } from '../../../../shared/layout'; +import { SourceSubNav } from './source_sub_nav'; describe('SourceSubNav', () => { it('renders empty when no group id present', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.tsx index 739b9ec138f29..99cebd5ded585 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_sub_nav.tsx @@ -6,15 +6,12 @@ */ import React from 'react'; + import { useValues } from 'kea'; +import { SideNavLink } from '../../../../shared/layout'; import { AppLogic } from '../../../app_logic'; import { NAV, CUSTOM_SERVICE_TYPE } from '../../../constants'; - -import { SourceLogic } from '../source_logic'; - -import { SideNavLink } from '../../../../shared/layout'; - import { getContentSourcePath, SOURCE_DETAILS_PATH, @@ -23,6 +20,7 @@ import { SOURCE_DISPLAY_SETTINGS_PATH, SOURCE_SETTINGS_PATH, } from '../../../routes'; +import { SourceLogic } from '../source_logic'; export const SourceSubNav: React.FC = () => { const { isOrganization } = useValues(AppLogic); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.test.tsx index 68addbacc5a23..b986658f19fb3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.test.tsx @@ -8,18 +8,16 @@ import '../../../__mocks__/shallow_useeffect.mock'; import { setMockValues, setMockActions } from '../../../__mocks__'; - -import { shallow } from 'enzyme'; +import { contentSources } from '../../__mocks__/content_sources.mock'; import React from 'react'; import { Redirect } from 'react-router-dom'; -import { contentSources } from '../../__mocks__/content_sources.mock'; +import { shallow } from 'enzyme'; import { Loading } from '../../../shared/loading'; import { SourcesTable } from '../../components/shared/sources_table'; import { ViewContentHeader } from '../../components/shared/view_content_header'; - import { ADD_SOURCE_PATH, getSourcesPath } from '../../routes'; import { OrganizationSources } from './organization_sources'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.tsx index 24c1f130da50d..4559003b4597f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/organization_sources.tsx @@ -6,11 +6,16 @@ */ import React, { useEffect } from 'react'; +import { Link, Redirect } from 'react-router-dom'; import { useActions, useValues } from 'kea'; -import { Link, Redirect } from 'react-router-dom'; import { EuiButton } from '@elastic/eui'; + +import { Loading } from '../../../shared/loading'; +import { ContentSection } from '../../components/shared/content_section'; +import { SourcesTable } from '../../components/shared/sources_table'; +import { ViewContentHeader } from '../../components/shared/view_content_header'; import { ADD_SOURCE_PATH, getSourcesPath } from '../../routes'; import { @@ -18,14 +23,7 @@ import { ORG_SOURCES_HEADER_TITLE, ORG_SOURCES_HEADER_DESCRIPTION, } from './constants'; - -import { Loading } from '../../../shared/loading'; -import { ContentSection } from '../../components/shared/content_section'; -import { SourcesTable } from '../../components/shared/sources_table'; -import { ViewContentHeader } from '../../components/shared/view_content_header'; - import { SourcesLogic } from './sources_logic'; - import { SourcesView } from './sources_view'; export const OrganizationSources: React.FC = () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.tsx index d68b451ffa6f5..61be7b6281e9e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/private_sources.tsx @@ -13,10 +13,14 @@ import { EuiCallOut, EuiEmptyPrompt, EuiSpacer, EuiPanel } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { LicensingLogic } from '../../../../applications/shared/licensing'; - -import { ADD_SOURCE_PATH, getSourcesPath } from '../../routes'; - +import { Loading } from '../../../shared/loading'; +import { EuiButtonTo } from '../../../shared/react_router_helpers'; +import { AppLogic } from '../../app_logic'; import noSharedSourcesIcon from '../../assets/share_circle.svg'; +import { ContentSection } from '../../components/shared/content_section'; +import { SourcesTable } from '../../components/shared/sources_table'; +import { ViewContentHeader } from '../../components/shared/view_content_header'; +import { ADD_SOURCE_PATH, getSourcesPath } from '../../routes'; import { AND, @@ -34,16 +38,8 @@ import { LICENSE_CALLOUT_TITLE, LICENSE_CALLOUT_DESCRIPTION, } from './constants'; - -import { Loading } from '../../../shared/loading'; -import { EuiButtonTo } from '../../../shared/react_router_helpers'; -import { ContentSection } from '../../components/shared/content_section'; -import { SourcesTable } from '../../components/shared/sources_table'; -import { ViewContentHeader } from '../../components/shared/view_content_header'; - -import { AppLogic } from '../../app_logic'; -import { SourcesView } from './sources_view'; import { SourcesLogic } from './sources_logic'; +import { SourcesView } from './sources_view'; // TODO: Remove this after links in Kibana sidenav interface SidebarLink { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx index 5b34603bca68f..cdad8e07a88be 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_data.tsx @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; +import { SOURCE_NAMES, SOURCE_OBJ_TYPES, GITHUB_LINK_TITLE } from '../../constants'; import { ADD_BOX_PATH, ADD_CONFLUENCE_PATH, @@ -62,11 +63,8 @@ import { ZENDESK_DOCS_URL, CUSTOM_SOURCE_DOCS_URL, } from '../../routes'; - import { FeatureIds, SourceDataItem } from '../../types'; -import { SOURCE_NAMES, SOURCE_OBJ_TYPES, GITHUB_LINK_TITLE } from '../../constants'; - const connectStepDescription = { attachments: i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.sources.connectStepDescription.attachments', 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 15df7ddc99395..d20d0576d11ce 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 @@ -12,16 +12,16 @@ import { mockKibanaValues, expectedAsyncError, } from '../../../__mocks__'; +import { fullContentSources, contentItems } from '../../__mocks__/content_sources.mock'; +import { meta } from '../../__mocks__/meta.mock'; + +import { DEFAULT_META } from '../../../shared/constants'; -import { AppLogic } from '../../app_logic'; jest.mock('../../app_logic', () => ({ AppLogic: { values: { isOrganization: true } }, })); +import { AppLogic } from '../../app_logic'; -import { fullContentSources, contentItems } from '../../__mocks__/content_sources.mock'; -import { meta } from '../../__mocks__/meta.mock'; - -import { DEFAULT_META } from '../../../shared/constants'; import { NOT_FOUND_PATH } from '../../routes'; import { SourceLogic } from './source_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts index c1f5d6033543f..72700ce42c75d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts @@ -9,17 +9,15 @@ import { kea, MakeLogicType } from 'kea'; import { i18n } from '@kbn/i18n'; -import { HttpLogic } from '../../../shared/http'; -import { KibanaLogic } from '../../../shared/kibana'; - +import { DEFAULT_META } from '../../../shared/constants'; import { flashAPIErrors, setSuccessMessage, setQueuedSuccessMessage, clearFlashMessages, } from '../../../shared/flash_messages'; - -import { DEFAULT_META } from '../../../shared/constants'; +import { HttpLogic } from '../../../shared/http'; +import { KibanaLogic } from '../../../shared/kibana'; import { AppLogic } from '../../app_logic'; import { NOT_FOUND_PATH, SOURCES_PATH, getSourcesPath } from '../../routes'; import { ContentSourceFullData, Meta, DocumentSummaryItem, SourceContentItem } from '../../types'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx index cf3c075d0c1e9..004f7e5e45bfa 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.test.tsx @@ -8,20 +8,17 @@ import '../../../__mocks__/shallow_useeffect.mock'; import { setMockValues, setMockActions } from '../../../__mocks__'; +import { contentSources } from '../../__mocks__/content_sources.mock'; import React from 'react'; -import { shallow } from 'enzyme'; import { useParams } from 'react-router-dom'; - import { Route, Switch } from 'react-router-dom'; -import { contentSources } from '../../__mocks__/content_sources.mock'; +import { shallow } from 'enzyme'; import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; - -import { NAV } from '../../constants'; - import { Loading } from '../../../shared/loading'; +import { NAV } from '../../constants'; import { DisplaySettingsRouter } from './components/display_settings'; import { Overview } from './components/overview'; @@ -29,7 +26,6 @@ import { Schema } from './components/schema'; import { SchemaChangeErrors } from './components/schema/schema_change_errors'; import { SourceContent } from './components/source_content'; import { SourceSettings } from './components/source_settings'; - import { SourceRouter } from './source_router'; describe('SourceRouter', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx index ac450441f8783..ef9788efbdaf2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_router.tsx @@ -6,24 +6,19 @@ */ import React, { useEffect } from 'react'; +import { Route, Switch, useParams } from 'react-router-dom'; import { useActions, useValues } from 'kea'; import moment from 'moment'; -import { Route, Switch, useParams } from 'react-router-dom'; import { EuiButton, EuiCallOut, EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { Loading } from '../../../shared/loading'; import { SendWorkplaceSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; - +import { AppLogic } from '../../app_logic'; import { NAV } from '../../constants'; - -import { - SOURCE_DISABLED_CALLOUT_TITLE, - SOURCE_DISABLED_CALLOUT_DESCRIPTION, - SOURCE_DISABLED_CALLOUT_BUTTON, -} from './constants'; - +import { CUSTOM_SERVICE_TYPE } from '../../constants'; import { ENT_SEARCH_LICENSE_MANAGEMENT, REINDEX_JOB_PATH, @@ -36,13 +31,6 @@ import { getSourcesPath, } from '../../routes'; -import { AppLogic } from '../../app_logic'; - -import { Loading } from '../../../shared/loading'; - -import { CUSTOM_SERVICE_TYPE } from '../../constants'; -import { SourceLogic } from './source_logic'; - import { DisplaySettingsRouter } from './components/display_settings'; import { Overview } from './components/overview'; import { Schema } from './components/schema'; @@ -50,6 +38,12 @@ import { SchemaChangeErrors } from './components/schema/schema_change_errors'; import { SourceContent } from './components/source_content'; import { SourceInfoCard } from './components/source_info_card'; import { SourceSettings } from './components/source_settings'; +import { + SOURCE_DISABLED_CALLOUT_TITLE, + SOURCE_DISABLED_CALLOUT_DESCRIPTION, + SOURCE_DISABLED_CALLOUT_BUTTON, +} from './constants'; +import { SourceLogic } from './source_logic'; export const SourceRouter: React.FC = () => { const { sourceId } = useParams() as { sourceId: string }; 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 b7db569eb704c..13844f51b2319 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 @@ -11,13 +11,12 @@ import { mockHttpValues, expectedAsyncError, } from '../../../__mocks__'; +import { configuredSources, contentSources } from '../../__mocks__/content_sources.mock'; -import { AppLogic } from '../../app_logic'; jest.mock('../../app_logic', () => ({ AppLogic: { values: { isOrganization: true } }, })); - -import { configuredSources, contentSources } from '../../__mocks__/content_sources.mock'; +import { AppLogic } from '../../app_logic'; import { SourcesLogic, fetchSourceStatuses, POLLING_INTERVAL } from './sources_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.ts index 5108ed45501f7..9de2b447619a6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_logic.ts @@ -5,22 +5,18 @@ * 2.0. */ -import { cloneDeep, findIndex } from 'lodash'; - import { kea, MakeLogicType } from 'kea'; +import { cloneDeep, findIndex } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { HttpLogic } from '../../../shared/http'; - import { flashAPIErrors, setQueuedSuccessMessage } from '../../../shared/flash_messages'; - +import { HttpLogic } from '../../../shared/http'; +import { AppLogic } from '../../app_logic'; import { Connector, ContentSourceDetails, ContentSourceStatus, SourceDataItem } from '../../types'; import { staticSourceData } from './source_data'; -import { AppLogic } from '../../app_logic'; - interface ServerStatuses { [key: string]: string; } diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.test.tsx index b1a6ea128ac8c..2438061c67759 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.test.tsx @@ -10,10 +10,10 @@ import '../../../__mocks__/shallow_useeffect.mock'; import { setMockValues, setMockActions } from '../../../__mocks__'; import React from 'react'; -import { shallow } from 'enzyme'; - import { Route, Switch, Redirect } from 'react-router-dom'; +import { shallow } from 'enzyme'; + import { ADD_SOURCE_PATH, PERSONAL_SOURCES_PATH, SOURCES_PATH, getSourcesPath } from '../../routes'; import { SourcesRouter } from './sources_router'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx index 28ad2fe3a3965..dcc15be4462c7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx @@ -6,16 +6,16 @@ */ import React, { useEffect } from 'react'; +import { Redirect, Route, Switch, useLocation } from 'react-router-dom'; import { Location } from 'history'; import { useActions, useValues } from 'kea'; -import { Redirect, Route, Switch, useLocation } from 'react-router-dom'; +import { LicensingLogic } from '../../../../applications/shared/licensing'; +import { FlashMessages } from '../../../shared/flash_messages'; import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { SendWorkplaceSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; - -import { LicensingLogic } from '../../../../applications/shared/licensing'; - +import { AppLogic } from '../../app_logic'; import { NAV } from '../../constants'; import { ADD_SOURCE_PATH, @@ -26,17 +26,13 @@ import { getSourcesPath, } from '../../routes'; -import { FlashMessages } from '../../../shared/flash_messages'; - -import { AppLogic } from '../../app_logic'; -import { staticSourceData } from './source_data'; -import { SourcesLogic } from './sources_logic'; - import { AddSource, AddSourceList } from './components/add_source'; import { SourceAdded } from './components/source_added'; import { OrganizationSources } from './organization_sources'; import { PrivateSources } from './private_sources'; +import { staticSourceData } from './source_data'; import { SourceRouter } from './source_router'; +import { SourcesLogic } from './sources_logic'; import './sources.scss'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_view.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_view.test.tsx index 742d19ebbd156..06d7ecff50299 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_view.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_view.test.tsx @@ -9,10 +9,10 @@ import '../../../__mocks__/shallow_useeffect.mock'; import { setMockValues, setMockActions } from '../../../__mocks__'; -import { shallow } from 'enzyme'; - import React from 'react'; +import { shallow } from 'enzyme'; + import { EuiModal } from '@elastic/eui'; import { Loading } from '../../../shared/loading'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_view.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_view.tsx index ac70d74cc3d78..c62f0b00258d6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_view.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_view.tsx @@ -9,9 +9,6 @@ import React from 'react'; import { useActions, useValues } from 'kea'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; - import { EuiButton, EuiLink, @@ -25,10 +22,11 @@ import { EuiOverlayMask, EuiText, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { Loading } from '../../../shared/loading'; import { SourceIcon } from '../../components/shared/source_icon'; - import { EXTERNAL_IDENTITIES_DOCS_URL, DOCUMENT_PERMISSIONS_DOCS_URL } from '../../routes'; import { @@ -36,7 +34,6 @@ import { DOCUMENT_PERMISSIONS_LINK, UNDERSTAND_BUTTON, } from './constants'; - import { SourcesLogic } from './sources_logic'; interface SourcesViewProps { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/error_state.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/error_state.test.tsx index 0408bbf3e7e84..a8fcdfd7cb257 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/error_state.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/error_state.test.tsx @@ -6,9 +6,11 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { ErrorStatePrompt } from '../../../shared/error_state'; + import { ErrorState } from './'; describe('ErrorState', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/error_state.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/error_state.tsx index 74e52912b551b..8116d55542820 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/error_state.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/error_state/error_state.tsx @@ -8,6 +8,7 @@ // TODO: Remove EuiPage & EuiPageBody before exposing full app import React from 'react'; + import { EuiPage, EuiPageBody, EuiPageContent } from '@elastic/eui'; import { WORKPLACE_SEARCH_PLUGIN } from '../../../../../common/constants'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/__mocks__/groups_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/__mocks__/groups_logic.mock.ts index 3df7fbb5a0596..0e072210d2489 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/__mocks__/groups_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/__mocks__/groups_logic.mock.ts @@ -5,9 +5,8 @@ * 2.0. */ -import { ContentSource, User, Group } from '../../../types'; - import { DEFAULT_META } from '../../../../shared/constants'; +import { ContentSource, User, Group } from '../../../types'; export const mockGroupsValues = { groups: [] as Group[], diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.test.tsx index 1065c2c2df4c3..26ac5e484f0d7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.test.tsx @@ -8,12 +8,13 @@ import { setMockValues, setMockActions } from '../../../../__mocks__'; import React from 'react'; -import { shallow } from 'enzyme'; -import { AddGroupModal } from './add_group_modal'; +import { shallow } from 'enzyme'; import { EuiModal, EuiOverlayMask } from '@elastic/eui'; +import { AddGroupModal } from './add_group_modal'; + describe('AddGroupModal', () => { const closeNewGroupModal = jest.fn(); const saveNewGroup = jest.fn(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.tsx index f49c978d06e90..fb82e9393f2a2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/add_group_modal.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { useActions, useValues } from 'kea'; -import { i18n } from '@kbn/i18n'; import { EuiButton, @@ -22,9 +21,9 @@ import { EuiModalHeaderTitle, EuiOverlayMask, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { CANCEL_BUTTON } from '../../../constants'; - import { GroupsLogic } from '../groups_logic'; const ADD_GROUP_HEADER = i18n.translate( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/clear_filters_link.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/clear_filters_link.test.tsx index 2dffe89f38569..9118bc5e7adf3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/clear_filters_link.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/clear_filters_link.test.tsx @@ -8,12 +8,13 @@ import { setMockActions } from '../../../../__mocks__'; import React from 'react'; -import { shallow } from 'enzyme'; -import { ClearFiltersLink } from './clear_filters_link'; +import { shallow } from 'enzyme'; import { EuiLink } from '@elastic/eui'; +import { ClearFiltersLink } from './clear_filters_link'; + describe('ClearFiltersLink', () => { const resetGroupsFilters = jest.fn(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/clear_filters_link.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/clear_filters_link.tsx index 3734148ea3afa..6aeb2241bca61 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/clear_filters_link.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/clear_filters_link.tsx @@ -8,9 +8,9 @@ import React from 'react'; import { useActions } from 'kea'; -import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { GroupsLogic } from '../groups_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_list.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_list.test.tsx index 965a4887f4359..a460070772d1f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_list.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_list.test.tsx @@ -8,14 +8,15 @@ import { users } from '../../../__mocks__/users.mock'; import React from 'react'; + import { shallow } from 'enzyme'; import { EuiFieldSearch, EuiFilterSelectItem, EuiCard, EuiPopoverTitle } from '@elastic/eui'; -import { FilterableUsersList } from './filterable_users_list'; - import { User } from '../../../types'; +import { FilterableUsersList } from './filterable_users_list'; + const mockSetState = jest.fn(); const useStateMock: any = (initState: any) => [initState, mockSetState]; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_list.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_list.tsx index ef222e934260b..8a7875b5e8310 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_list.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_list.tsx @@ -7,8 +7,6 @@ import React, { useState } from 'react'; -import { i18n } from '@kbn/i18n'; - import { EuiCard, EuiFieldSearch, @@ -17,6 +15,7 @@ import { EuiPopoverTitle, EuiSpacer, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { User } from '../../../types'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_popover.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_popover.test.tsx index 36a99425c9793..1813b766b9875 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_popover.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_popover.test.tsx @@ -9,13 +9,14 @@ import { setMockActions } from '../../../../__mocks__'; import { users } from '../../../__mocks__/users.mock'; import React from 'react'; -import { shallow } from 'enzyme'; -import { FilterableUsersPopover } from './filterable_users_popover'; -import { FilterableUsersList } from './filterable_users_list'; +import { shallow } from 'enzyme'; import { EuiFilterGroup, EuiPopover } from '@elastic/eui'; +import { FilterableUsersList } from './filterable_users_list'; +import { FilterableUsersPopover } from './filterable_users_popover'; + const closePopover = jest.fn(); const addFilteredUser = jest.fn(); const removeFilteredUser = jest.fn(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_popover.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_popover.tsx index b47232197c47f..3cf4d97c781d9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_popover.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/filterable_users_popover.tsx @@ -12,8 +12,8 @@ import { useActions } from 'kea'; import { EuiFilterGroup, EuiPopover } from '@elastic/eui'; import { User } from '../../../types'; - import { GroupsLogic } from '../groups_logic'; + import { FilterableUsersList } from './filterable_users_list'; interface FilterableUsersPopoverProps { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.test.tsx index 8ee14b7c82cc4..949ae9d502e73 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.test.tsx @@ -6,16 +6,17 @@ */ import { setMockValues } from '../../../../__mocks__'; -import { groups } from '../../../__mocks__/groups.mock'; import { contentSources } from '../../../__mocks__/content_sources.mock'; +import { groups } from '../../../__mocks__/groups.mock'; import React from 'react'; -import { shallow } from 'enzyme'; -import { GroupManagerModal } from './group_manager_modal'; +import { shallow } from 'enzyme'; import { EuiOverlayMask, EuiModal, EuiEmptyPrompt } from '@elastic/eui'; +import { GroupManagerModal } from './group_manager_modal'; + const hideModal = jest.fn(); const selectAll = jest.fn(); const saveItems = jest.fn(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.tsx index ae5c042fc27dc..b4317ed9bd417 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_manager_modal.tsx @@ -9,8 +9,6 @@ import React from 'react'; import { useValues } from 'kea'; -import { i18n } from '@kbn/i18n'; - import { EuiButton, EuiButtonEmpty, @@ -26,15 +24,13 @@ import { EuiOverlayMask, EuiSpacer, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { EuiButtonTo } from '../../../../shared/react_router_helpers'; - -import { Group } from '../../../types'; +import noSharedSourcesIcon from '../../../assets/share_circle.svg'; import { CANCEL_BUTTON } from '../../../constants'; import { SOURCES_PATH } from '../../../routes'; - -import noSharedSourcesIcon from '../../../assets/share_circle.svg'; - +import { Group } from '../../../types'; import { GroupLogic } from '../group_logic'; import { GroupsLogic } from '../groups_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.test.tsx index ea49ae09f3a25..e39d72a861b6f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.test.tsx @@ -9,21 +9,22 @@ import { setMockActions, setMockValues } from '../../../../__mocks__'; import { groups } from '../../../__mocks__/groups.mock'; import React from 'react'; + import { shallow } from 'enzyme'; +import { EuiFieldText } from '@elastic/eui'; + +import { Loading } from '../../../../shared/loading'; +import { ContentSection } from '../../../components/shared/content_section'; +import { SourcesTable } from '../../../components/shared/sources_table'; +import { ViewContentHeader } from '../../../components/shared/view_content_header'; + import { GroupOverview, EMPTY_SOURCES_DESCRIPTION, EMPTY_USERS_DESCRIPTION, } from './group_overview'; -import { ContentSection } from '../../../components/shared/content_section'; -import { ViewContentHeader } from '../../../components/shared/view_content_header'; -import { SourcesTable } from '../../../components/shared/sources_table'; -import { Loading } from '../../../../shared/loading'; - -import { EuiFieldText } from '@elastic/eui'; - const deleteGroup = jest.fn(); const showSharedSourcesModal = jest.fn(); const showManageUsersModal = jest.fn(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx index ca67c2aac98ad..df9c0b5db9b7d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_overview.tsx @@ -9,8 +9,6 @@ import React from 'react'; import { useActions, useValues } from 'kea'; -import { i18n } from '@kbn/i18n'; - import { EuiButton, EuiConfirmModal, @@ -22,20 +20,19 @@ import { EuiSpacer, EuiHorizontalRule, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; -import { CANCEL_BUTTON } from '../../../constants'; - -import { AppLogic } from '../../../app_logic'; +import { Loading } from '../../../../shared/loading'; import { TruncatedContent } from '../../../../shared/truncate'; +import { AppLogic } from '../../../app_logic'; import { ContentSection } from '../../../components/shared/content_section'; -import { ViewContentHeader } from '../../../components/shared/view_content_header'; -import { Loading } from '../../../../shared/loading'; import { SourcesTable } from '../../../components/shared/sources_table'; +import { ViewContentHeader } from '../../../components/shared/view_content_header'; +import { CANCEL_BUTTON } from '../../../constants'; +import { GroupLogic, MAX_NAME_LENGTH } from '../group_logic'; import { GroupUsersTable } from './group_users_table'; -import { GroupLogic, MAX_NAME_LENGTH } from '../group_logic'; - export const EMPTY_SOURCES_DESCRIPTION = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.groups.overview.emptySourcesDescription', { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.test.tsx index 19898172fb4e7..205eafd69cd10 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.test.tsx @@ -9,14 +9,15 @@ import { setMockValues } from '../../../../__mocks__'; import { groups } from '../../../__mocks__/groups.mock'; import React from 'react'; + import { shallow } from 'enzyme'; import moment from 'moment'; +import { EuiTableRow } from '@elastic/eui'; + import { GroupRow, NO_USERS_MESSAGE, NO_SOURCES_MESSAGE } from './group_row'; import { GroupUsers } from './group_users'; -import { EuiTableRow } from '@elastic/eui'; - describe('GroupRow', () => { beforeEach(() => { setMockValues({ isFederatedAuth: true }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.tsx index 1a085aea93cc6..5e89d4491d597 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row.tsx @@ -6,21 +6,20 @@ */ import React from 'react'; -import moment from 'moment'; -import { useValues } from 'kea'; -import { i18n } from '@kbn/i18n'; +import { useValues } from 'kea'; +import moment from 'moment'; import { EuiTableRow, EuiTableRowCell, EuiIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; -import { TruncatedContent } from '../../../../shared/truncate'; import { EuiLinkTo } from '../../../../shared/react_router_helpers'; - -import { Group } from '../../../types'; - +import { TruncatedContent } from '../../../../shared/truncate'; import { AppLogic } from '../../../app_logic'; import { getGroupPath } from '../../../routes'; +import { Group } from '../../../types'; import { MAX_NAME_LENGTH } from '../group_logic'; + import { GroupSources } from './group_sources'; import { GroupUsers } from './group_users'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row_sources_dropdown.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row_sources_dropdown.test.tsx index e4c626a28c1a6..23c00d0fa209e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row_sources_dropdown.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row_sources_dropdown.test.tsx @@ -8,13 +8,14 @@ import { contentSources } from '../../../__mocks__/content_sources.mock'; import React from 'react'; + import { shallow } from 'enzyme'; +import { EuiFilterGroup } from '@elastic/eui'; + import { GroupRowSourcesDropdown } from './group_row_sources_dropdown'; import { SourceOptionItem } from './source_option_item'; -import { EuiFilterGroup } from '@elastic/eui'; - const onButtonClick = jest.fn(); const closePopover = jest.fn(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row_sources_dropdown.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row_sources_dropdown.tsx index a8f8c18cc6f38..77d7de91caf7c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row_sources_dropdown.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row_sources_dropdown.tsx @@ -7,9 +7,8 @@ import React from 'react'; -import { i18n } from '@kbn/i18n'; - import { EuiFilterGroup, EuiPopover, EuiPopoverTitle, EuiButtonEmpty } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { ContentSource } from '../../../types'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row_users_dropdown.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row_users_dropdown.test.tsx index 7dae74155d0d6..e75b325a4eae9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row_users_dropdown.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row_users_dropdown.test.tsx @@ -9,12 +9,13 @@ import { setMockActions, setMockValues } from '../../../../__mocks__'; import { users } from '../../../__mocks__/users.mock'; import React from 'react'; + import { shallow, mount } from 'enzyme'; import { EuiLoadingContent, EuiButtonEmpty } from '@elastic/eui'; -import { GroupRowUsersDropdown } from './group_row_users_dropdown'; import { FilterableUsersPopover } from './filterable_users_popover'; +import { GroupRowUsersDropdown } from './group_row_users_dropdown'; const fetchGroupUsers = jest.fn(); const onButtonClick = jest.fn(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row_users_dropdown.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row_users_dropdown.tsx index 9ca9c8339ba6a..aaf715fc71615 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row_users_dropdown.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_row_users_dropdown.tsx @@ -12,6 +12,7 @@ import { useActions, useValues } from 'kea'; import { EuiLoadingContent, EuiButtonEmpty } from '@elastic/eui'; import { GroupsLogic } from '../groups_logic'; + import { FilterableUsersPopover } from './filterable_users_popover'; interface GroupRowUsersDropdownProps { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_source_prioritization.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_source_prioritization.test.tsx index 49305ec33d228..4a9244486bf30 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_source_prioritization.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_source_prioritization.test.tsx @@ -9,14 +9,15 @@ import { setMockActions, setMockValues } from '../../../../__mocks__'; import { groups } from '../../../__mocks__/groups.mock'; import React from 'react'; + import { shallow } from 'enzyme'; +import { EuiTable, EuiEmptyPrompt, EuiRange } from '@elastic/eui'; + import { Loading } from '../../../../shared/loading'; import { GroupSourcePrioritization } from './group_source_prioritization'; -import { EuiTable, EuiEmptyPrompt, EuiRange } from '@elastic/eui'; - const updatePriority = jest.fn(); const saveGroupSourcePrioritization = jest.fn(); const showSharedSourcesModal = jest.fn(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_source_prioritization.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_source_prioritization.tsx index 6907618e40b46..9b131e730b937 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_source_prioritization.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_source_prioritization.tsx @@ -9,8 +9,6 @@ import React, { ChangeEvent, MouseEvent } from 'react'; import { useActions, useValues } from 'kea'; -import { i18n } from '@kbn/i18n'; - import { EuiButton, EuiEmptyPrompt, @@ -26,14 +24,13 @@ import { EuiTableRow, EuiTableRowCell, } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { Loading } from '../../../../shared/loading'; -import { ViewContentHeader } from '../../../components/shared/view_content_header'; import { SourceIcon } from '../../../components/shared/source_icon'; - -import { GroupLogic } from '../group_logic'; - +import { ViewContentHeader } from '../../../components/shared/view_content_header'; import { ContentSource } from '../../../types'; +import { GroupLogic } from '../group_logic'; const HEADER_TITLE = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.groups.sourceProioritization.headerTitle', diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sources.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sources.test.tsx index fd2a5e2bc6d9a..a245f0a768b0e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sources.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sources.test.tsx @@ -8,15 +8,15 @@ import { contentSources } from '../../../__mocks__/content_sources.mock'; import React from 'react'; -import { shallow } from 'enzyme'; -import { GroupSources } from './group_sources'; -import { GroupRowSourcesDropdown } from './group_row_sources_dropdown'; +import { shallow } from 'enzyme'; import { SourceIcon } from '../../../components/shared/source_icon'; - import { ContentSourceDetails } from '../../../types'; +import { GroupRowSourcesDropdown } from './group_row_sources_dropdown'; +import { GroupSources } from './group_sources'; + describe('GroupSources', () => { it('renders', () => { const wrapper = shallow(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sources.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sources.tsx index ae3b5000941b1..97739e46caba4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sources.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sources.tsx @@ -9,7 +9,6 @@ import React, { useState } from 'react'; import { SourceIcon } from '../../../components/shared/source_icon'; import { MAX_TABLE_ROW_ICONS } from '../../../constants'; - import { ContentSource } from '../../../types'; import { GroupRowSourcesDropdown } from './group_row_sources_dropdown'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sub_nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sub_nav.test.tsx index ead4af451ee7a..e4dde81949bfa 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sub_nav.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sub_nav.test.tsx @@ -8,12 +8,13 @@ import { setMockValues } from '../../../../__mocks__'; import React from 'react'; -import { shallow } from 'enzyme'; -import { GroupSubNav } from './group_sub_nav'; +import { shallow } from 'enzyme'; import { SideNavLink } from '../../../../shared/layout'; +import { GroupSubNav } from './group_sub_nav'; + describe('GroupSubNav', () => { it('renders empty when no group id present', () => { setMockValues({ group: {} }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sub_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sub_nav.tsx index e2bd6e8ae91f2..c5fc0717d1105 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sub_nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_sub_nav.tsx @@ -6,14 +6,13 @@ */ import React from 'react'; -import { useValues } from 'kea'; -import { GroupLogic } from '../group_logic'; -import { NAV } from '../../../constants'; +import { useValues } from 'kea'; import { SideNavLink } from '../../../../shared/layout'; - +import { NAV } from '../../../constants'; import { getGroupPath, getGroupSourcePrioritizationPath } from '../../../routes'; +import { GroupLogic } from '../group_logic'; export const GroupSubNav: React.FC = () => { const { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users.test.tsx index f1bc063e1a223..eba79ea70177d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users.test.tsx @@ -8,14 +8,14 @@ import { users } from '../../../__mocks__/users.mock'; import React from 'react'; + import { shallow } from 'enzyme'; +import { UserIcon } from '../../../components/shared/user_icon'; import { User } from '../../../types'; -import { GroupUsers } from './group_users'; import { GroupRowUsersDropdown } from './group_row_users_dropdown'; - -import { UserIcon } from '../../../components/shared/user_icon'; +import { GroupUsers } from './group_users'; const props = { groupUsers: users, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users.tsx index 850910428c4b2..6e60df15ed30a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users.tsx @@ -9,7 +9,6 @@ import React, { useState } from 'react'; import { UserIcon } from '../../../components/shared/user_icon'; import { MAX_TABLE_ROW_ICONS } from '../../../constants'; - import { User } from '../../../types'; import { GroupRowUsersDropdown } from './group_row_users_dropdown'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.test.tsx index 83e945547438f..a6376d7653627 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.test.tsx @@ -9,14 +9,15 @@ import { setMockValues } from '../../../../__mocks__'; import { groups } from '../../../__mocks__/groups.mock'; import React from 'react'; + import { shallow } from 'enzyme'; -import { User } from '../../../types'; +import { EuiTable, EuiTablePagination } from '@elastic/eui'; -import { GroupUsersTable } from './group_users_table'; import { TableHeader } from '../../../../shared/table_header'; +import { User } from '../../../types'; -import { EuiTable, EuiTablePagination } from '@elastic/eui'; +import { GroupUsersTable } from './group_users_table'; const group = groups[0]; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.tsx index 4b337fda9143d..5d070b1a21b7d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/group_users_table.tsx @@ -9,17 +9,14 @@ import React, { useState } from 'react'; import { useValues } from 'kea'; -import { i18n } from '@kbn/i18n'; - import { EuiTable, EuiTableBody, EuiTablePagination } from '@elastic/eui'; import { Pager } from '@elastic/eui'; - -import { User } from '../../../types'; +import { i18n } from '@kbn/i18n'; import { TableHeader } from '../../../../shared/table_header'; -import { UserRow } from '../../../components/shared/user_row'; - import { AppLogic } from '../../../app_logic'; +import { UserRow } from '../../../components/shared/user_row'; +import { User } from '../../../types'; import { GroupLogic } from '../group_logic'; const USERS_PER_PAGE = 10; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.test.tsx index d6724499490cf..f60a13ec296d5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.test.tsx @@ -8,18 +8,18 @@ import { setMockActions, setMockValues } from '../../../../__mocks__'; import { groups } from '../../../__mocks__/groups.mock'; -import { DEFAULT_META } from '../../../../shared/constants'; - import React from 'react'; + import { shallow } from 'enzyme'; +import { EuiTable, EuiTableHeaderCell } from '@elastic/eui'; + +import { DEFAULT_META } from '../../../../shared/constants'; import { TablePaginationBar } from '../../../components/shared/table_pagination_bar'; -import { GroupsTable } from './groups_table'; -import { GroupRow } from './group_row'; import { ClearFiltersLink } from './clear_filters_link'; - -import { EuiTable, EuiTableHeaderCell } from '@elastic/eui'; +import { GroupRow } from './group_row'; +import { GroupsTable } from './groups_table'; const setActivePage = jest.fn(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.tsx index 31f549c3e2065..95292116cba05 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/groups_table.tsx @@ -9,8 +9,6 @@ import React from 'react'; import { useActions, useValues } from 'kea'; -import { i18n } from '@kbn/i18n'; - import { EuiSpacer, EuiTable, @@ -18,14 +16,14 @@ import { EuiTableHeader, EuiTableHeaderCell, } from '@elastic/eui'; - -import { TablePaginationBar } from '../../../components/shared/table_pagination_bar'; +import { i18n } from '@kbn/i18n'; import { AppLogic } from '../../../app_logic'; +import { TablePaginationBar } from '../../../components/shared/table_pagination_bar'; import { GroupsLogic } from '../groups_logic'; -import { GroupRow } from './group_row'; import { ClearFiltersLink } from './clear_filters_link'; +import { GroupRow } from './group_row'; const GROUP_TABLE_HEADER = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.groups.groupsTable.groupTableHeader', diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/manage_users_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/manage_users_modal.test.tsx index 059dff969aee3..49d51dfc7254c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/manage_users_modal.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/manage_users_modal.test.tsx @@ -9,11 +9,12 @@ import { setMockActions, setMockValues } from '../../../../__mocks__'; import { users } from '../../../__mocks__/users.mock'; import React from 'react'; + import { shallow } from 'enzyme'; -import { ManageUsersModal } from './manage_users_modal'; import { FilterableUsersList } from './filterable_users_list'; import { GroupManagerModal } from './group_manager_modal'; +import { ManageUsersModal } from './manage_users_modal'; const addGroupUser = jest.fn(); const removeGroupUser = jest.fn(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/shared_sources_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/shared_sources_modal.test.tsx index f937ded7d4918..dd72850a06ad9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/shared_sources_modal.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/shared_sources_modal.test.tsx @@ -9,10 +9,11 @@ import { setMockActions, setMockValues } from '../../../../__mocks__'; import { groups } from '../../../__mocks__/groups.mock'; import React from 'react'; + import { shallow } from 'enzyme'; -import { SharedSourcesModal } from './shared_sources_modal'; import { GroupManagerModal } from './group_manager_modal'; +import { SharedSourcesModal } from './shared_sources_modal'; import { SourcesList } from './sources_list'; const group = groups[0]; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/source_option_item.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/source_option_item.test.tsx index d037a49875a7e..bad60e15ed2d0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/source_option_item.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/source_option_item.test.tsx @@ -8,14 +8,14 @@ import { contentSources } from '../../../__mocks__/content_sources.mock'; import React from 'react'; -import { shallow } from 'enzyme'; -import { SourceOptionItem } from './source_option_item'; +import { shallow } from 'enzyme'; import { TruncatedContent } from '../../../../shared/truncate'; - import { SourceIcon } from '../../../components/shared/source_icon'; +import { SourceOptionItem } from './source_option_item'; + describe('SourceOptionItem', () => { it('renders', () => { const wrapper = shallow(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/source_option_item.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/source_option_item.tsx index a87980415bd1f..e2da597a83598 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/source_option_item.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/source_option_item.tsx @@ -10,7 +10,6 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { TruncatedContent } from '../../../../shared/truncate'; - import { SourceIcon } from '../../../components/shared/source_icon'; import { ContentSource } from '../../../types'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/sources_list.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/sources_list.test.tsx index 56e700c10e04c..05fe2c92f9f72 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/sources_list.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/sources_list.test.tsx @@ -8,12 +8,13 @@ import { contentSources } from '../../../__mocks__/content_sources.mock'; import React from 'react'; -import { shallow } from 'enzyme'; -import { SourcesList } from './sources_list'; +import { shallow } from 'enzyme'; import { EuiFilterSelectItem } from '@elastic/eui'; +import { SourcesList } from './sources_list'; + const addFilteredSource = jest.fn(); const removeFilteredSource = jest.fn(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_sources_dropdown.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_sources_dropdown.test.tsx index b7efe84df180c..1e2a57da9ad2e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_sources_dropdown.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_sources_dropdown.test.tsx @@ -9,11 +9,11 @@ import { setMockActions, setMockValues } from '../../../../__mocks__'; import { contentSources } from '../../../__mocks__/content_sources.mock'; import React from 'react'; -import { shallow } from 'enzyme'; -import { TableFilterSourcesDropdown } from './table_filter_sources_dropdown'; +import { shallow } from 'enzyme'; import { SourcesList } from './sources_list'; +import { TableFilterSourcesDropdown } from './table_filter_sources_dropdown'; const addFilteredSource = jest.fn(); const removeFilteredSource = jest.fn(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_sources_dropdown.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_sources_dropdown.tsx index b38d5fc55b6f8..5f75340d562ef 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_sources_dropdown.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_sources_dropdown.tsx @@ -9,11 +9,11 @@ import React from 'react'; import { useActions, useValues } from 'kea'; -import { i18n } from '@kbn/i18n'; - import { EuiFilterButton, EuiFilterGroup, EuiPopover } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { GroupsLogic } from '../groups_logic'; + import { SourcesList } from './sources_list'; const FILTER_SOURCES_BUTTON_TEXT = i18n.translate( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_users_dropdown.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_users_dropdown.test.tsx index 9eaaa64b1c4e4..e472563862015 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_users_dropdown.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_users_dropdown.test.tsx @@ -9,10 +9,11 @@ import { setMockActions, setMockValues } from '../../../../__mocks__'; import { users } from '../../../__mocks__/users.mock'; import React from 'react'; + import { shallow } from 'enzyme'; -import { TableFilterUsersDropdown } from './table_filter_users_dropdown'; import { FilterableUsersPopover } from './filterable_users_popover'; +import { TableFilterUsersDropdown } from './table_filter_users_dropdown'; const closeFilterUsersDropdown = jest.fn(); const toggleFilterUsersDropdown = jest.fn(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_users_dropdown.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_users_dropdown.tsx index 9ddb955767c14..c09e1e3cf87cc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_users_dropdown.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filter_users_dropdown.tsx @@ -9,11 +9,11 @@ import React from 'react'; import { useActions, useValues } from 'kea'; -import { i18n } from '@kbn/i18n'; - import { EuiFilterButton } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { GroupsLogic } from '../groups_logic'; + import { FilterableUsersPopover } from './filterable_users_popover'; const FILTER_USERS_BUTTON_TEXT = i18n.translate( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filters.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filters.test.tsx index 0fdaf74376494..bcc58c394b516 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filters.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filters.test.tsx @@ -8,13 +8,14 @@ import { setMockActions, setMockValues } from '../../../../__mocks__'; import React from 'react'; -import { shallow } from 'enzyme'; -import { TableFilters } from './table_filters'; +import { shallow } from 'enzyme'; import { EuiFieldSearch } from '@elastic/eui'; + import { TableFilterSourcesDropdown } from './table_filter_sources_dropdown'; import { TableFilterUsersDropdown } from './table_filter_users_dropdown'; +import { TableFilters } from './table_filters'; const setFilterValue = jest.fn(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filters.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filters.tsx index cfd40e1a0df4e..e9ea6a7c6b4aa 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filters.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/table_filters.tsx @@ -9,9 +9,8 @@ import React, { ChangeEvent } from 'react'; import { useActions, useValues } from 'kea'; -import { i18n } from '@kbn/i18n'; - import { EuiFieldSearch, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { AppLogic } from '../../../app_logic'; import { GroupsLogic } from '../groups_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/user_option_item.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/user_option_item.test.tsx index 01f67cc910afd..6c8dbbde2e69f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/user_option_item.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/components/user_option_item.test.tsx @@ -8,12 +8,14 @@ import { users } from '../../../__mocks__/users.mock'; import React from 'react'; + import { shallow } from 'enzyme'; -import { UserOptionItem } from './user_option_item'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + import { UserIcon } from '../../../components/shared/user_icon'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { UserOptionItem } from './user_option_item'; const user = users[0]; 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 d8d41b5e2888a..836efa82995fc 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 @@ -11,15 +11,15 @@ import { mockFlashMessageHelpers, mockHttpValues, } from '../../../__mocks__'; +import { groups } from '../../__mocks__/groups.mock'; import { nextTick } from '@kbn/test/jest'; -import { groups } from '../../__mocks__/groups.mock'; +import { GROUPS_PATH } from '../../routes'; + import { mockGroupValues } from './__mocks__/group_logic.mock'; import { GroupLogic } from './group_logic'; -import { GROUPS_PATH } from '../../routes'; - describe('GroupLogic', () => { const { mount } = new LogicMounter(GroupLogic); const { http } = mockHttpValues; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.ts index 7e7ce838434f5..f23b182b98649 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_logic.ts @@ -7,10 +7,9 @@ import { kea, MakeLogicType } from 'kea'; import { isEqual } from 'lodash'; + import { i18n } from '@kbn/i18n'; -import { HttpLogic } from '../../../shared/http'; -import { KibanaLogic } from '../../../shared/kibana'; import { clearFlashMessages, flashAPIErrors, @@ -18,9 +17,9 @@ import { setQueuedSuccessMessage, setQueuedErrorMessage, } from '../../../shared/flash_messages'; - +import { HttpLogic } from '../../../shared/http'; +import { KibanaLogic } from '../../../shared/kibana'; import { GROUPS_PATH } from '../../routes'; - import { ContentSourceDetails, GroupDetails, User, SourcePriority } from '../../types'; export const MAX_NAME_LENGTH = 40; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_router.test.tsx index a04fc4c744790..0b218f2496154 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_router.test.tsx @@ -7,25 +7,21 @@ import '../../../__mocks__/shallow_useeffect.mock'; import { setMockValues, setMockActions } from '../../../__mocks__'; +import { groups } from '../../__mocks__/groups.mock'; import React from 'react'; -import { shallow } from 'enzyme'; - import { Route, Switch } from 'react-router-dom'; -import { groups } from '../../__mocks__/groups.mock'; +import { shallow } from 'enzyme'; +import { FlashMessages } from '../../../shared/flash_messages'; import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { GroupOverview } from './components/group_overview'; import { GroupSourcePrioritization } from './components/group_source_prioritization'; - -import { GroupRouter } from './group_router'; - -import { FlashMessages } from '../../../shared/flash_messages'; - import { ManageUsersModal } from './components/manage_users_modal'; import { SharedSourcesModal } from './components/shared_sources_modal'; +import { GroupRouter } from './group_router'; describe('GroupRouter', () => { const initializeGroup = jest.fn(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_router.tsx index 82eb7931dfcdc..a5b8bd138d0c8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/group_router.tsx @@ -6,23 +6,21 @@ */ import React, { useEffect } from 'react'; +import { Route, Switch, useParams } from 'react-router-dom'; import { useActions, useValues } from 'kea'; -import { Route, Switch, useParams } from 'react-router-dom'; import { FlashMessages } from '../../../shared/flash_messages'; import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { SendWorkplaceSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; - -import { GROUP_SOURCE_PRIORITIZATION_PATH, GROUP_PATH } from '../../routes'; import { NAV } from '../../constants'; -import { GroupLogic } from './group_logic'; - -import { ManageUsersModal } from './components/manage_users_modal'; -import { SharedSourcesModal } from './components/shared_sources_modal'; +import { GROUP_SOURCE_PRIORITIZATION_PATH, GROUP_PATH } from '../../routes'; import { GroupOverview } from './components/group_overview'; import { GroupSourcePrioritization } from './components/group_source_prioritization'; +import { ManageUsersModal } from './components/manage_users_modal'; +import { SharedSourcesModal } from './components/shared_sources_modal'; +import { GroupLogic } from './group_logic'; export const GroupRouter: React.FC = () => { const { groupId } = useParams() as { groupId: string }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.test.tsx index d67dd5857561e..8470c5d3e0f66 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.test.tsx @@ -11,23 +11,22 @@ import { groups } from '../../__mocks__/groups.mock'; import { meta } from '../../__mocks__/meta.mock'; import React from 'react'; + import { shallow } from 'enzyme'; -import { Groups } from './groups'; +import { EuiFieldSearch, EuiLoadingSpinner } from '@elastic/eui'; -import { ViewContentHeader } from '../../components/shared/view_content_header'; -import { Loading } from '../../../shared/loading'; +import { DEFAULT_META } from '../../../shared/constants'; import { FlashMessages } from '../../../shared/flash_messages'; +import { Loading } from '../../../shared/loading'; +import { EuiButtonTo } from '../../../shared/react_router_helpers'; +import { ViewContentHeader } from '../../components/shared/view_content_header'; import { AddGroupModal } from './components/add_group_modal'; import { ClearFiltersLink } from './components/clear_filters_link'; import { GroupsTable } from './components/groups_table'; import { TableFilters } from './components/table_filters'; - -import { DEFAULT_META } from '../../../shared/constants'; - -import { EuiFieldSearch, EuiLoadingSpinner } from '@elastic/eui'; -import { EuiButtonTo } from '../../../shared/react_router_helpers'; +import { Groups } from './groups'; const getSearchResults = jest.fn(); const openNewGroupModal = jest.fn(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.tsx index 7a8b9343691f9..b2bf0364b2d1f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups.tsx @@ -8,26 +8,22 @@ import React, { useEffect } from 'react'; import { useActions, useValues } from 'kea'; -import { i18n } from '@kbn/i18n'; import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiSpacer } from '@elastic/eui'; -import { EuiButtonTo } from '../../../shared/react_router_helpers'; - -import { AppLogic } from '../../app_logic'; +import { i18n } from '@kbn/i18n'; +import { FlashMessages, FlashMessagesLogic } from '../../../shared/flash_messages'; import { Loading } from '../../../shared/loading'; +import { EuiButtonTo } from '../../../shared/react_router_helpers'; +import { AppLogic } from '../../app_logic'; import { ViewContentHeader } from '../../components/shared/view_content_header'; - import { getGroupPath, USERS_PATH } from '../../routes'; -import { FlashMessages, FlashMessagesLogic } from '../../../shared/flash_messages'; - -import { GroupsLogic } from './groups_logic'; - import { AddGroupModal } from './components/add_group_modal'; import { ClearFiltersLink } from './components/clear_filters_link'; import { GroupsTable } from './components/groups_table'; import { TableFilters } from './components/table_filters'; +import { GroupsLogic } from './groups_logic'; export const Groups: React.FC = () => { const { messages } = useValues(FlashMessagesLogic); 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 26d7f9784cc6e..806c6e1c69f84 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 @@ -6,15 +6,15 @@ */ import { LogicMounter, mockFlashMessageHelpers, mockHttpValues } from '../../../__mocks__'; +import { contentSources } from '../../__mocks__/content_sources.mock'; +import { groups } from '../../__mocks__/groups.mock'; +import { users } from '../../__mocks__/users.mock'; import { nextTick } from '@kbn/test/jest'; -import { DEFAULT_META } from '../../../shared/constants'; import { JSON_HEADER as headers } from '../../../../../common/constants'; +import { DEFAULT_META } from '../../../shared/constants'; -import { groups } from '../../__mocks__/groups.mock'; -import { contentSources } from '../../__mocks__/content_sources.mock'; -import { users } from '../../__mocks__/users.mock'; import { mockGroupsValues } from './__mocks__/groups_logic.mock'; import { GroupsLogic } from './groups_logic'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.ts index 68a6eb7bdf344..a036cdda3d68e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_logic.ts @@ -6,22 +6,20 @@ */ import { kea, MakeLogicType } from 'kea'; -import { i18n } from '@kbn/i18n'; -import { HttpLogic } from '../../../shared/http'; +import { i18n } from '@kbn/i18n'; +import { JSON_HEADER as headers } from '../../../../../common/constants'; +import { Meta } from '../../../../../common/types'; +import { DEFAULT_META } from '../../../shared/constants'; import { clearFlashMessages, flashAPIErrors, setSuccessMessage, } from '../../../shared/flash_messages'; - +import { HttpLogic } from '../../../shared/http'; import { ContentSource, Group, User } from '../../types'; -import { JSON_HEADER as headers } from '../../../../../common/constants'; -import { DEFAULT_META } from '../../../shared/constants'; -import { Meta } from '../../../../../common/types'; - export const MAX_NAME_LENGTH = 40; interface GroupsServerData { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_router.test.tsx index 43c31038a45c6..0295605eddd4d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_router.test.tsx @@ -9,14 +9,13 @@ import '../../../__mocks__/shallow_useeffect.mock'; import { setMockActions } from '../../../__mocks__'; import React from 'react'; -import { shallow } from 'enzyme'; - import { Route, Switch } from 'react-router-dom'; -import { GroupsRouter } from './groups_router'; +import { shallow } from 'enzyme'; import { GroupRouter } from './group_router'; import { Groups } from './groups'; +import { GroupsRouter } from './groups_router'; describe('GroupsRouter', () => { const initializeGroups = jest.fn(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_router.tsx index e835a2668f3d3..d8c4f4801ba24 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/groups/groups_router.tsx @@ -6,21 +6,18 @@ */ import React, { useEffect } from 'react'; +import { Route, Switch } from 'react-router-dom'; import { useActions } from 'kea'; -import { Route, Switch } from 'react-router-dom'; - import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { SendWorkplaceSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; - -import { GROUP_PATH, GROUPS_PATH } from '../../routes'; import { NAV } from '../../constants'; - -import { GroupsLogic } from './groups_logic'; +import { GROUP_PATH, GROUPS_PATH } from '../../routes'; import { GroupRouter } from './group_router'; import { Groups } from './groups'; +import { GroupsLogic } from './groups_logic'; import './groups.scss'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/overview_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/overview_logic.mock.ts index f03dcfe98ddd0..787354974cb31 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/overview_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/overview_logic.mock.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { setMockValues as setMockKeaValues, setMockActions } from '../../../../__mocks__/kea.mock'; import { DEFAULT_INITIAL_APP_DATA } from '../../../../../../common/__mocks__'; +import { setMockValues as setMockKeaValues, setMockActions } from '../../../../__mocks__/kea.mock'; const { workplaceSearch: mockAppValues } = DEFAULT_INITIAL_APP_DATA; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.test.tsx index 8f962ec4cf665..68dece976a09c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.test.tsx @@ -10,6 +10,7 @@ import '../../../__mocks__/enterprise_search_url.mock'; import { mockTelemetryActions } from '../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; import { EuiEmptyPrompt, EuiButton, EuiButtonEmpty } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.tsx index 68a4c4dc10f4f..2f8d06b71fc27 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_card.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { useActions } from 'kea'; import { @@ -20,8 +21,8 @@ import { EuiLinkProps, } from '@elastic/eui'; -import { TelemetryLogic } from '../../../shared/telemetry'; import { getWorkplaceSearchUrl } from '../../../shared/enterprise_search_url'; +import { TelemetryLogic } from '../../../shared/telemetry'; interface OnboardingCardProps { title: React.ReactNode; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.test.tsx index 7f676ce2faac2..7a368e7d384ea 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.test.tsx @@ -6,16 +6,18 @@ */ import { mockTelemetryActions } from '../../../__mocks__'; + import './__mocks__/overview_logic.mock'; -import { setMockValues } from './__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; import { SOURCES_PATH, USERS_PATH } from '../../routes'; -import { OnboardingSteps, OrgNameOnboarding } from './onboarding_steps'; +import { setMockValues } from './__mocks__'; import { OnboardingCard } from './onboarding_card'; +import { OnboardingSteps, OrgNameOnboarding } from './onboarding_steps'; const account = { id: '1', diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx index ae30a52c1541c..fc3998fcdfeec 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx @@ -6,8 +6,7 @@ */ import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; + import { useValues, useActions } from 'kea'; import { @@ -22,17 +21,18 @@ import { EuiButtonEmptyProps, EuiLinkProps, } from '@elastic/eui'; -import sharedSourcesIcon from '../../components/shared/assets/source_icons/share_circle.svg'; -import { TelemetryLogic } from '../../../shared/telemetry'; -import { getWorkplaceSearchUrl } from '../../../shared/enterprise_search_url'; -import { SOURCES_PATH, USERS_PATH, ORG_SETTINGS_PATH } from '../../routes'; - -import { ContentSection } from '../../components/shared/content_section'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { getWorkplaceSearchUrl } from '../../../shared/enterprise_search_url'; +import { TelemetryLogic } from '../../../shared/telemetry'; import { AppLogic } from '../../app_logic'; -import { OverviewLogic } from './overview_logic'; +import sharedSourcesIcon from '../../components/shared/assets/source_icons/share_circle.svg'; +import { ContentSection } from '../../components/shared/content_section'; +import { SOURCES_PATH, USERS_PATH, ORG_SETTINGS_PATH } from '../../routes'; import { OnboardingCard } from './onboarding_card'; +import { OverviewLogic } from './overview_logic'; const SOURCES_TITLE = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.overviewOnboardingSourcesCard.title', diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.test.tsx index cf4f96f6b788b..412977f18fadf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.test.tsx @@ -6,12 +6,14 @@ */ import './__mocks__/overview_logic.mock'; -import { setMockValues } from './__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiFlexGrid } from '@elastic/eui'; +import { setMockValues } from './__mocks__'; import { OrganizationStats } from './organization_stats'; import { StatisticCard } from './statistic_card'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.tsx index 52c370caac989..525035030b8cc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.tsx @@ -6,18 +6,18 @@ */ import React from 'react'; -import { EuiFlexGrid } from '@elastic/eui'; + import { useValues } from 'kea'; -import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFlexGrid } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { AppLogic } from '../../app_logic'; import { ContentSection } from '../../components/shared/content_section'; import { SOURCES_PATH, USERS_PATH } from '../../routes'; -import { AppLogic } from '../../app_logic'; import { OverviewLogic } from './overview_logic'; - import { StatisticCard } from './statistic_card'; export const OrganizationStats: React.FC = () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.test.tsx index fc70a07e339e4..2ec2d949ff491 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.test.tsx @@ -7,18 +7,19 @@ import '../../../__mocks__/react_router_history.mock'; import './__mocks__/overview_logic.mock'; -import { mockActions, setMockValues } from './__mocks__'; import React from 'react'; + import { shallow, mount } from 'enzyme'; import { Loading } from '../../../shared/loading'; import { ViewContentHeader } from '../../components/shared/view_content_header'; +import { mockActions, setMockValues } from './__mocks__'; import { OnboardingSteps } from './onboarding_steps'; import { OrganizationStats } from './organization_stats'; -import { RecentActivity } from './recent_activity'; import { Overview } from './overview'; +import { RecentActivity } from './recent_activity'; describe('Overview', () => { describe('non-happy-path states', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.tsx index 07bc999922661..6bf84b585da80 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview.tsx @@ -8,22 +8,22 @@ // TODO: Remove EuiPage & EuiPageBody before exposing full app import React, { useEffect } from 'react'; + +import { useActions, useValues } from 'kea'; + import { EuiPage, EuiPageBody, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useActions, useValues } from 'kea'; import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { Loading } from '../../../shared/loading'; import { SendWorkplaceSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; - import { AppLogic } from '../../app_logic'; -import { OverviewLogic } from './overview_logic'; - -import { Loading } from '../../../shared/loading'; import { ProductButton } from '../../components/shared/product_button'; import { ViewContentHeader } from '../../components/shared/view_content_header'; import { OnboardingSteps } from './onboarding_steps'; import { OrganizationStats } from './organization_stats'; +import { OverviewLogic } from './overview_logic'; import { RecentActivity } from './recent_activity'; const ONBOARDING_HEADER_TITLE = i18n.translate( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts index 6d0beb638cd52..75513cfba3a09 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts @@ -6,6 +6,7 @@ */ import { kea, MakeLogicType } from 'kea'; + import { HttpLogic } from '../../../shared/http'; import { FeedActivity } from './recent_activity'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.test.tsx index 9c571bd8bc169..0b62207afc520 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.test.tsx @@ -6,15 +6,17 @@ */ import { mockTelemetryActions } from '../../../__mocks__'; + import './__mocks__/overview_logic.mock'; -import { setMockValues } from './__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; import { EuiEmptyPrompt, EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import { setMockValues } from './__mocks__'; import { RecentActivity, RecentActivityItem } from './recent_activity'; const organization = { name: 'foo', defaultOrgName: 'bar' }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx index 1dcec989a94c7..43d3f880feef4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx @@ -7,19 +7,19 @@ import React from 'react'; -import moment from 'moment'; import { useValues, useActions } from 'kea'; +import moment from 'moment'; import { EuiEmptyPrompt, EuiLink, EuiPanel, EuiSpacer, EuiLinkProps } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { ContentSection } from '../../components/shared/content_section'; -import { TelemetryLogic } from '../../../shared/telemetry'; import { getWorkplaceSearchUrl } from '../../../shared/enterprise_search_url'; -import { SOURCE_DETAILS_PATH, getContentSourcePath } from '../../routes'; +import { TelemetryLogic } from '../../../shared/telemetry'; +import { AppLogic } from '../../app_logic'; +import { ContentSection } from '../../components/shared/content_section'; import { RECENT_ACTIVITY_TITLE } from '../../constants'; +import { SOURCE_DETAILS_PATH, getContentSourcePath } from '../../routes'; -import { AppLogic } from '../../app_logic'; import { OverviewLogic } from './overview_logic'; import './recent_activity.scss'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/statistic_card.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/statistic_card.test.tsx index 2893c3630393e..ff1d69e406830 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/statistic_card.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/statistic_card.test.tsx @@ -8,6 +8,7 @@ import '../../../__mocks__/enterprise_search_url.mock'; import React from 'react'; + import { shallow } from 'enzyme'; import { EuiCard } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/statistic_card.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/statistic_card.tsx index 83e6c2012a046..346debb1c5251 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/statistic_card.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/statistic_card.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { EuiCard, EuiFlexItem, EuiTitle, EuiTextColor } from '@elastic/eui'; import { getWorkplaceSearchUrl } from '../../../shared/enterprise_search_url'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/components/private_sources_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/components/private_sources_table.test.tsx index fb28fba9b3aea..4f7160ba631f1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/components/private_sources_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/components/private_sources_table.test.tsx @@ -8,7 +8,9 @@ import { setMockValues } from '../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiSwitch } from '@elastic/eui'; import { PrivateSourcesTable } from './private_sources_table'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/components/private_sources_table.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/components/private_sources_table.tsx index 8ba29e5986e04..559b2fe3edbd1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/components/private_sources_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/components/private_sources_table.tsx @@ -24,11 +24,10 @@ import { EuiTableRowCell, EuiSpacer, } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { LicensingLogic } from '../../../../shared/licensing'; -import { SecurityLogic, PrivateSourceSection } from '../security_logic'; import { REMOTE_SOURCES_TOGGLE_TEXT, REMOTE_SOURCES_TABLE_DESCRIPTION, @@ -38,6 +37,7 @@ import { STANDARD_SOURCES_EMPTY_TABLE_TITLE, SOURCE, } from '../../../constants'; +import { SecurityLogic, PrivateSourceSection } from '../security_logic'; interface PrivateSourcesTableProps { sourceType: 'remote' | 'standard'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security.test.tsx index 24e6e5808355a..4eed6a6fefe68 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security.test.tsx @@ -9,11 +9,14 @@ import { setMockValues, setMockActions } from '../../../__mocks__'; import { unmountHandler } from '../../../__mocks__/shallow_useeffect.mock'; import React from 'react'; + import { shallow } from 'enzyme'; + import { EuiSwitch, EuiConfirmModal } from '@elastic/eui'; -import { Loading } from '../../../shared/loading'; +import { Loading } from '../../../shared/loading'; import { ViewContentHeader } from '../../components/shared/view_content_header'; + import { Security } from './security'; describe('Security', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security.tsx index 818dd34447c73..ba1ffb66f4691 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security.tsx @@ -23,15 +23,11 @@ import { EuiOverlayMask, } from '@elastic/eui'; -import { LicensingLogic } from '../../../shared/licensing'; import { FlashMessages } from '../../../shared/flash_messages'; -import { LicenseCallout } from '../../components/shared/license_callout'; +import { LicensingLogic } from '../../../shared/licensing'; import { Loading } from '../../../shared/loading'; +import { LicenseCallout } from '../../components/shared/license_callout'; import { ViewContentHeader } from '../../components/shared/view_content_header'; -import { SecurityLogic } from './security_logic'; - -import { PrivateSourcesTable } from './components/private_sources_table'; - import { SECURITY_UNSAVED_CHANGES_MESSAGE, RESET_BUTTON, @@ -46,6 +42,9 @@ import { PRIVATE_SOURCES_UPDATE_CONFIRMATION_TEXT, } from '../../constants'; +import { PrivateSourcesTable } from './components/private_sources_table'; +import { SecurityLogic } from './security_logic'; + export const Security: React.FC = () => { const [confirmModalVisible, setConfirmModalVisibility] = useState(false); 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 c2bd1be390592..02d8fdd3c30e4 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,11 +5,13 @@ * 2.0. */ -import { LogicMounter } from '../../../__mocks__/kea.mock'; import { mockHttpValues, mockFlashMessageHelpers } from '../../../__mocks__'; -import { SecurityLogic } from './security_logic'; +import { LogicMounter } from '../../../__mocks__/kea.mock'; + import { nextTick } from '@kbn/test/jest'; +import { SecurityLogic } from './security_logic'; + describe('SecurityLogic', () => { const { http } = mockHttpValues; const { flashAPIErrors } = mockFlashMessageHelpers; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security_logic.ts index 8689cec037848..07ebec41366b2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/security/security_logic.ts @@ -5,11 +5,10 @@ * 2.0. */ +import { kea, MakeLogicType } from 'kea'; import { cloneDeep } from 'lodash'; import { isEqual } from 'lodash'; -import { kea, MakeLogicType } from 'kea'; - import { clearFlashMessages, setSuccessMessage, @@ -17,7 +16,6 @@ import { } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; import { AppLogic } from '../../app_logic'; - import { SOURCE_RESTRICTIONS_SUCCESS_MESSAGE } from '../../constants'; export interface PrivateSource { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/connectors.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/connectors.test.tsx index d1dd9e64c4d2d..13ef86a21a208 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/connectors.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/connectors.test.tsx @@ -8,10 +8,10 @@ import '../../../../__mocks__/shallow_useeffect.mock'; import { setMockValues, setMockActions } from '../../../../__mocks__'; - import { configuredSources } from '../../../__mocks__/content_sources.mock'; import React from 'react'; + import { shallow } from 'enzyme'; import { Loading } from '../../../../shared/loading'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/connectors.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/connectors.tsx index 5b74f6d1d2806..9387cd4602255 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/connectors.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/connectors.tsx @@ -19,12 +19,11 @@ import { EuiSpacer, } from '@elastic/eui'; -import { EuiButtonEmptyTo } from '../../../../shared/react_router_helpers'; import { Loading } from '../../../../shared/loading'; -import { SourceIcon } from '../../../components/shared/source_icon'; +import { EuiButtonEmptyTo } from '../../../../shared/react_router_helpers'; import { LicenseCallout } from '../../../components/shared/license_callout'; +import { SourceIcon } from '../../../components/shared/source_icon'; import { ViewContentHeader } from '../../../components/shared/view_content_header'; - import { CONFIGURE_BUTTON, CONNECTORS_HEADER_TITLE, @@ -36,9 +35,7 @@ import { } from '../../../constants'; import { getSourcesPath } from '../../../routes'; import { SourceDataItem } from '../../../types'; - import { staticSourceData } from '../../content_sources/source_data'; - import { SettingsLogic } from '../settings_logic'; export const Connectors: React.FC = () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/customize.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/customize.test.tsx index 8f77c229ad6f8..ed05829d9e082 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/customize.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/customize.test.tsx @@ -10,6 +10,7 @@ import '../../../../__mocks__/shallow_useeffect.mock'; import { setMockValues, setMockActions } from '../../../../__mocks__'; import React from 'react'; + import { shallow } from 'enzyme'; import { EuiFieldText } from '@elastic/eui'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/customize.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/customize.tsx index d57621bd397db..37f9e288f7f3d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/customize.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/customize.tsx @@ -11,16 +11,14 @@ import { useActions, useValues } from 'kea'; import { EuiButton, EuiFieldText, EuiFlexGroup, EuiFlexItem, EuiFormRow } from '@elastic/eui'; +import { ContentSection } from '../../../components/shared/content_section'; +import { ViewContentHeader } from '../../../components/shared/view_content_header'; import { CUSTOMIZE_HEADER_TITLE, CUSTOMIZE_HEADER_DESCRIPTION, CUSTOMIZE_NAME_LABEL, CUSTOMIZE_NAME_BUTTON, } from '../../../constants'; - -import { ContentSection } from '../../../components/shared/content_section'; -import { ViewContentHeader } from '../../../components/shared/view_content_header'; - import { SettingsLogic } from '../settings_logic'; export const Customize: React.FC = () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.test.tsx index 6fc9d51f42a86..55a58610e0ed6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.test.tsx @@ -8,17 +8,18 @@ import '../../../../__mocks__/shallow_useeffect.mock'; import { setMockValues, setMockActions } from '../../../../__mocks__'; +import { oauthApplication } from '../../../__mocks__/content_sources.mock'; import React from 'react'; + import { shallow } from 'enzyme'; import { EuiModal, EuiForm } from '@elastic/eui'; -import { oauthApplication } from '../../../__mocks__/content_sources.mock'; -import { OAUTH_DESCRIPTION, REDIRECT_INSECURE_ERROR_TEXT } from '../../../constants'; - import { CredentialItem } from '../../../components/shared/credential_item'; import { ViewContentHeader } from '../../../components/shared/view_content_header'; +import { OAUTH_DESCRIPTION, REDIRECT_INSECURE_ERROR_TEXT } from '../../../constants'; + import { OauthApplication } from './oauth_application'; describe('OauthApplication', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.tsx index 04759e4f5fdd0..28e7e2a33eaa1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/oauth_application.tsx @@ -26,7 +26,11 @@ import { EuiText, } from '@elastic/eui'; -import { ENT_SEARCH_LICENSE_MANAGEMENT } from '../../../routes'; +import { LicensingLogic } from '../../../../shared/licensing'; +import { ContentSection } from '../../../components/shared/content_section'; +import { CredentialItem } from '../../../components/shared/credential_item'; +import { LicenseBadge } from '../../../components/shared/license_badge'; +import { ViewContentHeader } from '../../../components/shared/view_content_header'; import { CLIENT_ID_LABEL, CLIENT_SECRET_LABEL, @@ -48,12 +52,7 @@ import { LICENSE_MODAL_DESCRIPTION, LICENSE_MODAL_LINK, } from '../../../constants'; - -import { LicensingLogic } from '../../../../shared/licensing'; -import { ContentSection } from '../../../components/shared/content_section'; -import { LicenseBadge } from '../../../components/shared/license_badge'; -import { ViewContentHeader } from '../../../components/shared/view_content_header'; -import { CredentialItem } from '../../../components/shared/credential_item'; +import { ENT_SEARCH_LICENSE_MANAGEMENT } from '../../../routes'; import { SettingsLogic } from '../settings_logic'; export const OauthApplication: React.FC = () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/settings_sub_nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/settings_sub_nav.test.tsx index f00bb7d897e25..5cd8a3fc1cf03 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/settings_sub_nav.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/settings_sub_nav.test.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { SideNavLink } from '../../../../shared/layout'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/settings_sub_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/settings_sub_nav.tsx index 20a6e349c1272..3f68997a17b8b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/settings_sub_nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/settings_sub_nav.tsx @@ -7,10 +7,8 @@ import React from 'react'; -import { NAV } from '../../../constants'; - import { SideNavLink } from '../../../../shared/layout'; - +import { NAV } from '../../../constants'; import { ORG_SETTINGS_CUSTOMIZE_PATH, ORG_SETTINGS_CONNECTORS_PATH, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.test.tsx index 73ea92117c6df..ed9f715fd6916 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.test.tsx @@ -8,16 +8,17 @@ import '../../../../__mocks__/shallow_useeffect.mock'; import { setMockValues, setMockActions } from '../../../../__mocks__'; +import { sourceConfigData } from '../../../__mocks__/content_sources.mock'; import React from 'react'; + import { shallow } from 'enzyme'; import { EuiConfirmModal } from '@elastic/eui'; -import { sourceConfigData } from '../../../__mocks__/content_sources.mock'; - import { Loading } from '../../../../shared/loading'; import { SaveConfig } from '../../content_sources/components/add_source/save_config'; + import { SourceConfig } from './source_config'; describe('SourceConfig', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx index 4b59e0f3401c5..4ed223931d6a4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/source_config.tsx @@ -8,18 +8,16 @@ import React, { useEffect, useState } from 'react'; import { useActions, useValues } from 'kea'; -import { i18n } from '@kbn/i18n'; import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { Loading } from '../../../../shared/loading'; import { SourceDataItem } from '../../../types'; -import { staticSourceData } from '../../content_sources/source_data'; -import { AddSourceLogic } from '../../content_sources/components/add_source/add_source_logic'; - import { AddSourceHeader } from '../../content_sources/components/add_source/add_source_header'; +import { AddSourceLogic } from '../../content_sources/components/add_source/add_source_logic'; import { SaveConfig } from '../../content_sources/components/add_source/save_config'; - +import { staticSourceData } from '../../content_sources/source_data'; import { SettingsLogic } from '../settings_logic'; interface SourceConfigProps { 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 b8b08b8658372..a57c2c1f9ad44 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 @@ -5,15 +5,14 @@ * 2.0. */ -import { LogicMounter } from '../../../__mocks__/kea.mock'; - import { mockFlashMessageHelpers, mockHttpValues, mockKibanaValues } from '../../../__mocks__'; +import { LogicMounter } from '../../../__mocks__/kea.mock'; +import { configuredSources, oauthApplication } from '../../__mocks__/content_sources.mock'; import { nextTick } from '@kbn/test/jest'; -import { configuredSources, oauthApplication } from '../../__mocks__/content_sources.mock'; - import { ORG_UPDATED_MESSAGE, OAUTH_APP_UPDATED_MESSAGE } from '../../constants'; + import { SettingsLogic } from './settings_logic'; describe('SettingsLogic', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.ts index 5a4f366c737d5..ad552ff8f5a41 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_logic.ts @@ -6,6 +6,7 @@ */ import { kea, MakeLogicType } from 'kea'; + import { i18n } from '@kbn/i18n'; import { @@ -14,13 +15,11 @@ import { setSuccessMessage, flashAPIErrors, } from '../../../shared/flash_messages'; -import { KibanaLogic } from '../../../shared/kibana'; import { HttpLogic } from '../../../shared/http'; - -import { Connector } from '../../types'; +import { KibanaLogic } from '../../../shared/kibana'; import { ORG_UPDATED_MESSAGE, OAUTH_APP_UPDATED_MESSAGE } from '../../constants'; - import { ORG_SETTINGS_CONNECTORS_PATH } from '../../routes'; +import { Connector } from '../../types'; interface IOauthApplication { name: string; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.test.tsx index 7f3ba0a8f34b3..411414fb33eaf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.test.tsx @@ -10,19 +10,17 @@ import '../../../__mocks__/shallow_useeffect.mock'; import { setMockActions } from '../../../__mocks__'; import React from 'react'; -import { shallow } from 'enzyme'; - import { Route, Redirect, Switch } from 'react-router-dom'; -import { staticSourceData } from '../content_sources/source_data'; +import { shallow } from 'enzyme'; import { FlashMessages } from '../../../shared/flash_messages'; +import { staticSourceData } from '../content_sources/source_data'; import { Connectors } from './components/connectors'; import { Customize } from './components/customize'; import { OauthApplication } from './components/oauth_application'; import { SourceConfig } from './components/source_config'; - import { SettingsRouter } from './settings_router'; describe('SettingsRouter', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.tsx index ee9122b015eff..34dcc48621a2e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/settings_router.tsx @@ -6,26 +6,23 @@ */ import React, { useEffect } from 'react'; +import { Redirect, Route, Switch } from 'react-router-dom'; import { useActions } from 'kea'; -import { Redirect, Route, Switch } from 'react-router-dom'; +import { FlashMessages } from '../../../shared/flash_messages'; import { ORG_SETTINGS_PATH, ORG_SETTINGS_CUSTOMIZE_PATH, ORG_SETTINGS_CONNECTORS_PATH, ORG_SETTINGS_OAUTH_APPLICATION_PATH, } from '../../routes'; - -import { FlashMessages } from '../../../shared/flash_messages'; +import { staticSourceData } from '../content_sources/source_data'; import { Connectors } from './components/connectors'; import { Customize } from './components/customize'; import { OauthApplication } from './components/oauth_application'; import { SourceConfig } from './components/source_config'; - -import { staticSourceData } from '../content_sources/source_data'; - import { SettingsLogic } from './settings_logic'; export const SettingsRouter: React.FC = () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.test.tsx index 8bec56603cd80..6b03e86080402 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.test.tsx @@ -6,10 +6,12 @@ */ import React from 'react'; + import { shallow } from 'enzyme'; import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; import { SetupGuideLayout } from '../../../shared/setup_guide'; + import { SetupGuide } from './'; describe('SetupGuide', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.tsx index 810125fc931a6..13191f42bc566 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/setup_guide/setup_guide.tsx @@ -6,18 +6,19 @@ */ import React from 'react'; + import { EuiSpacer, EuiTitle, EuiText, EuiButton, EuiLink } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { WORKPLACE_SEARCH_PLUGIN } from '../../../../../common/constants'; -import { SetupGuideLayout, SETUP_GUIDE_TITLE } from '../../../shared/setup_guide'; import { SetWorkplaceSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { SetupGuideLayout, SETUP_GUIDE_TITLE } from '../../../shared/setup_guide'; import { SendWorkplaceSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; +import { DOCS_PREFIX } from '../../routes'; import GettingStarted from './assets/getting_started.png'; -import { DOCS_PREFIX } from '../../routes'; const GETTING_STARTED_LINK_URL = `${DOCS_PREFIX}/workplace-search-getting-started.html`; export const SetupGuide: React.FC = () => { diff --git a/x-pack/plugins/enterprise_search/public/index.ts b/x-pack/plugins/enterprise_search/public/index.ts index da343728b7d41..b7131e70fec07 100644 --- a/x-pack/plugins/enterprise_search/public/index.ts +++ b/x-pack/plugins/enterprise_search/public/index.ts @@ -6,6 +6,7 @@ */ import { PluginInitializerContext } from 'src/core/public'; + import { EnterpriseSearchPlugin } from './plugin'; export const plugin = (initializerContext: PluginInitializerContext) => { diff --git a/x-pack/plugins/enterprise_search/public/plugin.ts b/x-pack/plugins/enterprise_search/public/plugin.ts index c10eb74f47720..f00e81a5accf7 100644 --- a/x-pack/plugins/enterprise_search/public/plugin.ts +++ b/x-pack/plugins/enterprise_search/public/plugin.ts @@ -12,15 +12,15 @@ import { HttpSetup, Plugin, PluginInitializerContext, -} from 'src/core/public'; -import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/public'; + DEFAULT_APP_CATEGORIES, +} from '../../../../src/core/public'; +import { ChartsPluginStart } from '../../../../src/plugins/charts/public'; import { FeatureCatalogueCategory, HomePublicPluginSetup, } from '../../../../src/plugins/home/public'; import { CloudSetup } from '../../cloud/public'; import { LicensingPluginStart } from '../../licensing/public'; -import { ChartsPluginStart } from '../../../../src/plugins/charts/public'; import { APP_SEARCH_PLUGIN, diff --git a/x-pack/plugins/enterprise_search/server/__mocks__/router.mock.ts b/x-pack/plugins/enterprise_search/server/__mocks__/router.mock.ts index 88cf30bb2a549..5c19ca7062b65 100644 --- a/x-pack/plugins/enterprise_search/server/__mocks__/router.mock.ts +++ b/x-pack/plugins/enterprise_search/server/__mocks__/router.mock.ts @@ -5,13 +5,13 @@ * 2.0. */ -import { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; import { IRouter, KibanaRequest, RequestHandlerContext, RouteValidatorConfig, } from 'src/core/server'; +import { httpServiceMock, httpServerMock } from 'src/core/server/mocks'; /** * Test helper that mocks Kibana's router and DRYs out various helper (callRoute, schema validation) diff --git a/x-pack/plugins/enterprise_search/server/__mocks__/routerDependencies.mock.ts b/x-pack/plugins/enterprise_search/server/__mocks__/routerDependencies.mock.ts index c84254660a728..50ff082858fc8 100644 --- a/x-pack/plugins/enterprise_search/server/__mocks__/routerDependencies.mock.ts +++ b/x-pack/plugins/enterprise_search/server/__mocks__/routerDependencies.mock.ts @@ -6,6 +6,7 @@ */ import { loggingSystemMock } from 'src/core/server/mocks'; + import { ConfigType } from '../'; export const mockLogger = loggingSystemMock.createLogger().get(); diff --git a/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.ts b/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.ts index 537e1b77f3e84..36ba2976f929a 100644 --- a/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.ts +++ b/x-pack/plugins/enterprise_search/server/collectors/app_search/telemetry.ts @@ -6,6 +6,7 @@ */ import { get } from 'lodash'; + import { SavedObjectsServiceStart, Logger } from 'src/core/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; diff --git a/x-pack/plugins/enterprise_search/server/collectors/enterprise_search/telemetry.ts b/x-pack/plugins/enterprise_search/server/collectors/enterprise_search/telemetry.ts index 732dfbd02c10b..f71c8a5444c9c 100644 --- a/x-pack/plugins/enterprise_search/server/collectors/enterprise_search/telemetry.ts +++ b/x-pack/plugins/enterprise_search/server/collectors/enterprise_search/telemetry.ts @@ -6,6 +6,7 @@ */ import { get } from 'lodash'; + import { SavedObjectsServiceStart, Logger } from 'src/core/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; diff --git a/x-pack/plugins/enterprise_search/server/collectors/workplace_search/telemetry.ts b/x-pack/plugins/enterprise_search/server/collectors/workplace_search/telemetry.ts index 01210eba95368..e36ce94066789 100644 --- a/x-pack/plugins/enterprise_search/server/collectors/workplace_search/telemetry.ts +++ b/x-pack/plugins/enterprise_search/server/collectors/workplace_search/telemetry.ts @@ -6,6 +6,7 @@ */ import { get } from 'lodash'; + import { SavedObjectsServiceStart, Logger } from 'src/core/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; diff --git a/x-pack/plugins/enterprise_search/server/index.ts b/x-pack/plugins/enterprise_search/server/index.ts index ac012077fdf84..c4552b9134eae 100644 --- a/x-pack/plugins/enterprise_search/server/index.ts +++ b/x-pack/plugins/enterprise_search/server/index.ts @@ -5,8 +5,9 @@ * 2.0. */ -import { PluginInitializerContext, PluginConfigDescriptor } from 'src/core/server'; import { schema, TypeOf } from '@kbn/config-schema'; +import { PluginInitializerContext, PluginConfigDescriptor } from 'src/core/server'; + import { EnterpriseSearchPlugin } from './plugin'; export const plugin = (initializerContext: PluginInitializerContext) => { diff --git a/x-pack/plugins/enterprise_search/server/lib/check_access.test.ts b/x-pack/plugins/enterprise_search/server/lib/check_access.test.ts index 4a978c66b16d6..3c5d33fa74d3b 100644 --- a/x-pack/plugins/enterprise_search/server/lib/check_access.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/check_access.test.ts @@ -5,14 +5,15 @@ * 2.0. */ +import { spacesMock } from '../../../spaces/server/mocks'; + +import { checkAccess } from './check_access'; + jest.mock('./enterprise_search_config_api', () => ({ callEnterpriseSearchConfigAPI: jest.fn(), })); import { callEnterpriseSearchConfigAPI } from './enterprise_search_config_api'; -import { checkAccess } from './check_access'; -import { spacesMock } from '../../../spaces/server/mocks'; - const enabledSpace = { id: 'space', name: 'space', diff --git a/x-pack/plugins/enterprise_search/server/lib/check_access.ts b/x-pack/plugins/enterprise_search/server/lib/check_access.ts index 25c92d62c1203..0a5e0c9e2b832 100644 --- a/x-pack/plugins/enterprise_search/server/lib/check_access.ts +++ b/x-pack/plugins/enterprise_search/server/lib/check_access.ts @@ -6,9 +6,10 @@ */ import { KibanaRequest, Logger } from 'src/core/server'; -import { SpacesPluginStart } from '../../../spaces/server'; + import { SecurityPluginSetup } from '../../../security/server'; -import { ConfigType } from '../'; +import { SpacesPluginStart } from '../../../spaces/server'; +import { ConfigType } from '../index'; import { callEnterpriseSearchConfigAPI } from './enterprise_search_config_api'; diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts index 61aeffd99db00..6c6744ef3e32b 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts @@ -5,14 +5,15 @@ * 2.0. */ +import { DEFAULT_INITIAL_APP_DATA } from '../../common/__mocks__'; + jest.mock('node-fetch'); -// eslint-disable-next-line @typescript-eslint/no-var-requires -const fetchMock = require('node-fetch') as jest.Mock; +import fetch from 'node-fetch'; + const { Response } = jest.requireActual('node-fetch'); import { loggingSystemMock } from 'src/core/server/mocks'; -import { DEFAULT_INITIAL_APP_DATA } from '../../common/__mocks__'; import { callEnterpriseSearchConfigAPI } from './enterprise_search_config_api'; describe('callEnterpriseSearchConfigAPI', () => { @@ -101,7 +102,7 @@ describe('callEnterpriseSearchConfigAPI', () => { }); it('calls the config API endpoint', async () => { - fetchMock.mockImplementationOnce((url: string) => { + ((fetch as unknown) as jest.Mock).mockImplementationOnce((url: string) => { expect(url).toEqual('http://localhost:3002/api/ent/v2/internal/client_config'); return Promise.resolve(new Response(JSON.stringify(mockResponse))); }); @@ -117,7 +118,7 @@ describe('callEnterpriseSearchConfigAPI', () => { }); it('falls back without error when data is unavailable', async () => { - fetchMock.mockImplementationOnce((url: string) => Promise.resolve(new Response('{}'))); + ((fetch as unknown) as jest.Mock).mockReturnValueOnce(Promise.resolve(new Response('{}'))); expect(await callEnterpriseSearchConfigAPI(mockDependencies)).toEqual({ access: { @@ -180,21 +181,17 @@ describe('callEnterpriseSearchConfigAPI', () => { const config = { host: '' }; expect(await callEnterpriseSearchConfigAPI({ ...mockDependencies, config })).toEqual({}); - expect(fetchMock).not.toHaveBeenCalled(); + expect(fetch).not.toHaveBeenCalled(); }); it('handles server errors', async () => { - fetchMock.mockImplementationOnce(() => { - return Promise.reject('500'); - }); + ((fetch as unknown) as jest.Mock).mockReturnValueOnce(Promise.reject('500')); expect(await callEnterpriseSearchConfigAPI(mockDependencies)).toEqual({}); expect(mockDependencies.log.error).toHaveBeenCalledWith( 'Could not perform access check to Enterprise Search: 500' ); - fetchMock.mockImplementationOnce(() => { - return Promise.resolve('Bad Data'); - }); + ((fetch as unknown) as jest.Mock).mockReturnValueOnce(Promise.resolve('Bad Data')); expect(await callEnterpriseSearchConfigAPI(mockDependencies)).toEqual({}); expect(mockDependencies.log.error).toHaveBeenCalledWith( 'Could not perform access check to Enterprise Search: TypeError: response.json is not a function' @@ -212,7 +209,7 @@ describe('callEnterpriseSearchConfigAPI', () => { ); // Timeout - fetchMock.mockImplementationOnce(async () => { + ((fetch as unknown) as jest.Mock).mockImplementationOnce(async () => { jest.advanceTimersByTime(250); return Promise.reject({ name: 'AbortError' }); }); diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts index 9f207361cef91..0ed4ad257f30b 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts @@ -9,11 +9,12 @@ import AbortController from 'abort-controller'; import fetch from 'node-fetch'; import { KibanaRequest, Logger } from 'src/core/server'; -import { ConfigType } from '../'; -import { Access } from './check_access'; -import { InitialAppData } from '../../common/types'; import { stripTrailingSlash } from '../../common/strip_slashes'; +import { InitialAppData } from '../../common/types'; +import { ConfigType } from '../index'; + +import { Access } from './check_access'; interface Params { request: KibanaRequest; diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts index 8d47ba0ec77ba..7199067a2c8f4 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts @@ -6,6 +6,7 @@ */ import { mockConfig, mockLogger } from '../__mocks__'; + import { JSON_HEADER, READ_ONLY_MODE_HEADER } from '../../common/constants'; import { EnterpriseSearchRequestHandler } from './enterprise_search_request_handler'; @@ -13,6 +14,7 @@ import { EnterpriseSearchRequestHandler } from './enterprise_search_request_hand jest.mock('node-fetch'); // eslint-disable-next-line @typescript-eslint/no-var-requires const fetchMock = require('node-fetch') as jest.Mock; + const { Response } = jest.requireActual('node-fetch'); const responseMock = { diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts index 39590b310fc26..f47df58c4eca1 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts @@ -7,6 +7,7 @@ import fetch, { Response } from 'node-fetch'; import querystring from 'querystring'; + import { RequestHandler, RequestHandlerContext, @@ -14,8 +15,9 @@ import { KibanaResponseFactory, Logger, } from 'src/core/server'; -import { ConfigType } from '../index'; + import { JSON_HEADER, READ_ONLY_MODE_HEADER } from '../../common/constants'; +import { ConfigType } from '../index'; interface ConstructorDependencies { config: ConfigType; diff --git a/x-pack/plugins/enterprise_search/server/plugin.ts b/x-pack/plugins/enterprise_search/server/plugin.ts index 569479f921cdd..1b9659899097d 100644 --- a/x-pack/plugins/enterprise_search/server/plugin.ts +++ b/x-pack/plugins/enterprise_search/server/plugin.ts @@ -13,37 +13,39 @@ import { SavedObjectsServiceStart, IRouter, KibanaRequest, -} from 'src/core/server'; -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { SpacesPluginStart } from '../../spaces/server'; -import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server'; -import { SecurityPluginSetup } from '../../security/server'; + DEFAULT_APP_CATEGORIES, +} from '../../../../src/core/server'; +import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; +import { SecurityPluginSetup } from '../../security/server'; +import { SpacesPluginStart } from '../../spaces/server'; import { ENTERPRISE_SEARCH_PLUGIN, APP_SEARCH_PLUGIN, WORKPLACE_SEARCH_PLUGIN, } from '../common/constants'; -import { ConfigType } from './'; + +import { registerTelemetryUsageCollector as registerASTelemetryUsageCollector } from './collectors/app_search/telemetry'; +import { registerTelemetryUsageCollector as registerESTelemetryUsageCollector } from './collectors/enterprise_search/telemetry'; +import { registerTelemetryUsageCollector as registerWSTelemetryUsageCollector } from './collectors/workplace_search/telemetry'; + import { checkAccess } from './lib/check_access'; import { EnterpriseSearchRequestHandler, IEnterpriseSearchRequestHandler, } from './lib/enterprise_search_request_handler'; -import { enterpriseSearchTelemetryType } from './saved_objects/enterprise_search/telemetry'; -import { registerTelemetryUsageCollector as registerESTelemetryUsageCollector } from './collectors/enterprise_search/telemetry'; -import { registerTelemetryRoute } from './routes/enterprise_search/telemetry'; +import { registerAppSearchRoutes } from './routes/app_search'; import { registerConfigDataRoute } from './routes/enterprise_search/config_data'; +import { registerTelemetryRoute } from './routes/enterprise_search/telemetry'; +import { registerWorkplaceSearchRoutes } from './routes/workplace_search'; import { appSearchTelemetryType } from './saved_objects/app_search/telemetry'; -import { registerTelemetryUsageCollector as registerASTelemetryUsageCollector } from './collectors/app_search/telemetry'; -import { registerAppSearchRoutes } from './routes/app_search'; - +import { enterpriseSearchTelemetryType } from './saved_objects/enterprise_search/telemetry'; import { workplaceSearchTelemetryType } from './saved_objects/workplace_search/telemetry'; -import { registerTelemetryUsageCollector as registerWSTelemetryUsageCollector } from './collectors/workplace_search/telemetry'; -import { registerWorkplaceSearchRoutes } from './routes/workplace_search'; + +import { ConfigType } from './'; interface PluginsSetup { usageCollection?: UsageCollectionSetup; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts index 49ff0353bef03..0070680985a34 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts @@ -7,8 +7,8 @@ import { schema } from '@kbn/config-schema'; -import { RouteDependencies } from '../../plugin'; import { ENGINES_PAGE_SIZE } from '../../../common/constants'; +import { RouteDependencies } from '../../plugin'; interface EnginesResponse { results: object[]; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts index 233e728a3010a..92fdcb689db1d 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts @@ -7,12 +7,12 @@ import { RouteDependencies } from '../../plugin'; -import { registerEnginesRoutes } from './engines'; -import { registerCredentialsRoutes } from './credentials'; -import { registerSettingsRoutes } from './settings'; import { registerAnalyticsRoutes } from './analytics'; +import { registerCredentialsRoutes } from './credentials'; import { registerDocumentsRoutes, registerDocumentRoutes } from './documents'; +import { registerEnginesRoutes } from './engines'; import { registerSearchSettingsRoutes } from './search_settings'; +import { registerSettingsRoutes } from './settings'; export const registerAppSearchRoutes = (dependencies: RouteDependencies) => { registerEnginesRoutes(dependencies); diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/config_data.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/config_data.ts index f9c65eedbb13a..e2cbd409bd396 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/config_data.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/config_data.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { RouteDependencies } from '../../plugin'; import { callEnterpriseSearchConfigAPI } from '../../lib/enterprise_search_config_api'; +import { RouteDependencies } from '../../plugin'; export function registerConfigDataRoute({ router, config, log }: RouteDependencies) { router.get( diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts index 08c398ba3eb0d..62f68748fcea1 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.test.ts @@ -5,9 +5,10 @@ * 2.0. */ -import { loggingSystemMock, savedObjectsServiceMock } from 'src/core/server/mocks'; import { MockRouter, mockLogger, mockDependencies } from '../../__mocks__'; +import { loggingSystemMock, savedObjectsServiceMock } from 'src/core/server/mocks'; + jest.mock('../../collectors/lib/telemetry', () => ({ incrementUICounter: jest.fn(), })); diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.ts index c8750bdff5d38..90afba414c044 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.ts @@ -7,12 +7,13 @@ import { schema } from '@kbn/config-schema'; -import { RouteDependencies } from '../../plugin'; -import { incrementUICounter } from '../../collectors/lib/telemetry'; - -import { ES_TELEMETRY_NAME } from '../../collectors/enterprise_search/telemetry'; import { AS_TELEMETRY_NAME } from '../../collectors/app_search/telemetry'; +import { ES_TELEMETRY_NAME } from '../../collectors/enterprise_search/telemetry'; +import { incrementUICounter } from '../../collectors/lib/telemetry'; import { WS_TELEMETRY_NAME } from '../../collectors/workplace_search/telemetry'; + +import { RouteDependencies } from '../../plugin'; + const productToTelemetryMap = { enterprise_search: ES_TELEMETRY_NAME, app_search: AS_TELEMETRY_NAME, diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/index.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/index.ts index c4819c3579adc..cc6226e340653 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/index.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/index.ts @@ -7,11 +7,11 @@ import { RouteDependencies } from '../../plugin'; -import { registerOverviewRoute } from './overview'; import { registerGroupsRoutes } from './groups'; -import { registerSourcesRoutes } from './sources'; -import { registerSettingsRoutes } from './settings'; +import { registerOverviewRoute } from './overview'; import { registerSecurityRoutes } from './security'; +import { registerSettingsRoutes } from './settings'; +import { registerSourcesRoutes } from './sources'; export const registerWorkplaceSearchRoutes = (dependencies: RouteDependencies) => { registerOverviewRoute(dependencies); diff --git a/x-pack/plugins/enterprise_search/server/saved_objects/app_search/telemetry.ts b/x-pack/plugins/enterprise_search/server/saved_objects/app_search/telemetry.ts index 29b1ce8182f52..ab873b6678885 100644 --- a/x-pack/plugins/enterprise_search/server/saved_objects/app_search/telemetry.ts +++ b/x-pack/plugins/enterprise_search/server/saved_objects/app_search/telemetry.ts @@ -8,6 +8,7 @@ /* istanbul ignore file */ import { SavedObjectsType } from 'src/core/server'; + import { AS_TELEMETRY_NAME } from '../../collectors/app_search/telemetry'; export const appSearchTelemetryType: SavedObjectsType = { diff --git a/x-pack/plugins/enterprise_search/server/saved_objects/enterprise_search/telemetry.ts b/x-pack/plugins/enterprise_search/server/saved_objects/enterprise_search/telemetry.ts index 07659299ef87f..e2edff1b6a213 100644 --- a/x-pack/plugins/enterprise_search/server/saved_objects/enterprise_search/telemetry.ts +++ b/x-pack/plugins/enterprise_search/server/saved_objects/enterprise_search/telemetry.ts @@ -8,6 +8,7 @@ /* istanbul ignore file */ import { SavedObjectsType } from 'src/core/server'; + import { ES_TELEMETRY_NAME } from '../../collectors/enterprise_search/telemetry'; export const enterpriseSearchTelemetryType: SavedObjectsType = { diff --git a/x-pack/plugins/enterprise_search/server/saved_objects/workplace_search/telemetry.ts b/x-pack/plugins/enterprise_search/server/saved_objects/workplace_search/telemetry.ts index a466d69cf8343..af4d5908dec67 100644 --- a/x-pack/plugins/enterprise_search/server/saved_objects/workplace_search/telemetry.ts +++ b/x-pack/plugins/enterprise_search/server/saved_objects/workplace_search/telemetry.ts @@ -8,6 +8,7 @@ /* istanbul ignore file */ import { SavedObjectsType } from 'src/core/server'; + import { WS_TELEMETRY_NAME } from '../../collectors/workplace_search/telemetry'; export const workplaceSearchTelemetryType: SavedObjectsType = { From 08c08e88d93e525d7c00016830e35fe2080ccd72 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 9 Feb 2021 10:08:46 -0800 Subject: [PATCH 26/28] [CI] Combines Jest unit tests (#89948) Signed-off-by: Tyler Smalley --- .ci/es-snapshots/Jenkinsfile_verify_es | 1 - .ci/jobs.yml | 1 - jest.config.integration.js | 5 +++- jest.config.js | 10 +++++++- jest.config.oss.js | 19 --------------- packages/kbn-test/jest-preset.js | 4 +++- .../shell_scripts/extract_archives.sh | 2 +- test/scripts/jenkins_unit.sh | 17 +++----------- test/scripts/jenkins_xpack.sh | 23 ------------------- test/scripts/test/jest_unit.sh | 4 +++- test/scripts/test/xpack_jest_unit.sh | 6 ----- vars/kibanaCoverage.groovy | 7 ------ vars/kibanaPipeline.groovy | 22 ++++++++---------- vars/workers.groovy | 4 ++-- x-pack/jest.config.js | 12 ---------- 15 files changed, 34 insertions(+), 103 deletions(-) delete mode 100644 jest.config.oss.js delete mode 100755 test/scripts/jenkins_xpack.sh delete mode 100755 test/scripts/test/xpack_jest_unit.sh delete mode 100644 x-pack/jest.config.js diff --git a/.ci/es-snapshots/Jenkinsfile_verify_es b/.ci/es-snapshots/Jenkinsfile_verify_es index 11a39faa9aed0..b40cd91a45c57 100644 --- a/.ci/es-snapshots/Jenkinsfile_verify_es +++ b/.ci/es-snapshots/Jenkinsfile_verify_es @@ -29,7 +29,6 @@ kibanaPipeline(timeoutMinutes: 150) { withEnv(["ES_SNAPSHOT_MANIFEST=${SNAPSHOT_MANIFEST}"]) { parallel([ 'kibana-intake-agent': workers.intake('kibana-intake', './test/scripts/jenkins_unit.sh'), - 'x-pack-intake-agent': workers.intake('x-pack-intake', './test/scripts/jenkins_xpack.sh'), 'kibana-oss-agent': workers.functional('kibana-oss-tests', { kibanaPipeline.buildOss() }, [ 'oss-ciGroup1': kibanaPipeline.ossCiGroupProcess(1), 'oss-ciGroup2': kibanaPipeline.ossCiGroupProcess(2), diff --git a/.ci/jobs.yml b/.ci/jobs.yml index b05e834f5a459..6aa93d4a1056a 100644 --- a/.ci/jobs.yml +++ b/.ci/jobs.yml @@ -2,7 +2,6 @@ JOB: - kibana-intake - - x-pack-intake - kibana-firefoxSmoke - kibana-ciGroup1 - kibana-ciGroup2 diff --git a/jest.config.integration.js b/jest.config.integration.js index df9fa9029aaa3..50767932a52d7 100644 --- a/jest.config.integration.js +++ b/jest.config.integration.js @@ -17,6 +17,7 @@ module.exports = { testPathIgnorePatterns: preset.testPathIgnorePatterns.filter( (pattern) => !pattern.includes('integration_tests') ), + setupFilesAfterEnv: ['/packages/kbn-test/target/jest/setup/after_env.integration.js'], reporters: [ 'default', [ @@ -24,5 +25,7 @@ module.exports = { { reportName: 'Jest Integration Tests' }, ], ], - setupFilesAfterEnv: ['/packages/kbn-test/target/jest/setup/after_env.integration.js'], + coverageReporters: !!process.env.CI + ? [['json', { file: 'jest-integration.json' }]] + : ['html', 'text'], }; diff --git a/jest.config.js b/jest.config.js index 89f66b5ee462f..03dc832ba170c 100644 --- a/jest.config.js +++ b/jest.config.js @@ -7,6 +7,14 @@ */ module.exports = { + preset: '@kbn/test', rootDir: '.', - projects: [...require('./jest.config.oss').projects, ...require('./x-pack/jest.config').projects], + projects: [ + '/packages/*/jest.config.js', + '/src/*/jest.config.js', + '/src/legacy/*/jest.config.js', + '/src/plugins/*/jest.config.js', + '/test/*/jest.config.js', + '/x-pack/plugins/*/jest.config.js', + ], }; diff --git a/jest.config.oss.js b/jest.config.oss.js deleted file mode 100644 index fcd704382f39d..0000000000000 --- a/jest.config.oss.js +++ /dev/null @@ -1,19 +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. - */ - -module.exports = { - preset: '@kbn/test', - rootDir: '.', - projects: [ - '/packages/*/jest.config.js', - '/src/*/jest.config.js', - '/src/legacy/*/jest.config.js', - '/src/plugins/*/jest.config.js', - '/test/*/jest.config.js', - ], -}; diff --git a/packages/kbn-test/jest-preset.js b/packages/kbn-test/jest-preset.js index 9ed11c4fe5fdd..717be8f413b48 100644 --- a/packages/kbn-test/jest-preset.js +++ b/packages/kbn-test/jest-preset.js @@ -19,7 +19,9 @@ module.exports = { coveragePathIgnorePatterns: ['/node_modules/', '.*\\.d\\.ts'], // A list of reporter names that Jest uses when writing coverage reports - coverageReporters: !!process.env.CODE_COVERAGE ? ['json'] : ['html', 'text'], + coverageReporters: !!process.env.CODE_COVERAGE + ? [['json', { file: 'jest.json' }]] + : ['html', 'text'], // An array of file extensions your modules use moduleFileExtensions: ['js', 'mjs', 'json', 'ts', 'tsx', 'node'], diff --git a/src/dev/code_coverage/shell_scripts/extract_archives.sh b/src/dev/code_coverage/shell_scripts/extract_archives.sh index 376467f9f2e55..14b35f8786d02 100644 --- a/src/dev/code_coverage/shell_scripts/extract_archives.sh +++ b/src/dev/code_coverage/shell_scripts/extract_archives.sh @@ -6,7 +6,7 @@ EXTRACT_DIR=/tmp/extracted_coverage mkdir -p $EXTRACT_DIR echo "### Extracting downloaded artifacts" -for x in kibana-intake x-pack-intake kibana-oss-tests kibana-xpack-tests; do +for x in kibana-intake kibana-oss-tests kibana-xpack-tests; do tar -xzf $DOWNLOAD_DIR/coverage/${x}/kibana-coverage.tar.gz -C $EXTRACT_DIR || echo "### Error 'tarring': ${x}" done diff --git a/test/scripts/jenkins_unit.sh b/test/scripts/jenkins_unit.sh index 6e28f9c3ef56a..9e387f97a016e 100755 --- a/test/scripts/jenkins_unit.sh +++ b/test/scripts/jenkins_unit.sh @@ -2,12 +2,6 @@ source test/scripts/jenkins_test_setup.sh -rename_coverage_file() { - test -f target/kibana-coverage/jest/coverage-final.json \ - && mv target/kibana-coverage/jest/coverage-final.json \ - target/kibana-coverage/jest/$1-coverage-final.json -} - if [[ -z "$CODE_COVERAGE" ]] ; then # Lint ./test/scripts/lint/eslint.sh @@ -34,13 +28,8 @@ if [[ -z "$CODE_COVERAGE" ]] ; then ./test/scripts/checks/test_hardening.sh else echo " -> Running jest tests with coverage" - node scripts/jest --ci --verbose --coverage --config jest.config.oss.js || true; - rename_coverage_file "oss" - echo "" - echo "" + node scripts/jest --ci --verbose --maxWorkers=6 --coverage || true; + echo " -> Running jest integration tests with coverage" - node --max-old-space-size=8192 scripts/jest_integration --ci --verbose --coverage || true; - rename_coverage_file "oss-integration" - echo "" - echo "" + node scripts/jest_integration --ci --verbose --coverage || true; fi diff --git a/test/scripts/jenkins_xpack.sh b/test/scripts/jenkins_xpack.sh deleted file mode 100755 index 66fb5ae5370bc..0000000000000 --- a/test/scripts/jenkins_xpack.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env bash - -source test/scripts/jenkins_test_setup.sh - -if [[ -z "$CODE_COVERAGE" ]] ; then - echo " -> Running jest tests" - - ./test/scripts/test/xpack_jest_unit.sh -else - echo " -> Build runtime for canvas" - # build runtime for canvas - echo "NODE_ENV=$NODE_ENV" - node ./x-pack/plugins/canvas/scripts/shareable_runtime - echo " -> Running jest tests with coverage" - cd x-pack - node --max-old-space-size=6144 scripts/jest --ci --verbose --maxWorkers=5 --coverage --config jest.config.js || true; - # rename file in order to be unique one - test -f ../target/kibana-coverage/jest/coverage-final.json \ - && mv ../target/kibana-coverage/jest/coverage-final.json \ - ../target/kibana-coverage/jest/xpack-coverage-final.json - echo "" - echo "" -fi diff --git a/test/scripts/test/jest_unit.sh b/test/scripts/test/jest_unit.sh index 88c0fe528b88c..1442a0f728727 100755 --- a/test/scripts/test/jest_unit.sh +++ b/test/scripts/test/jest_unit.sh @@ -2,5 +2,7 @@ source src/dev/ci_setup/setup_env.sh +export NODE_OPTIONS="--max-old-space-size=2048" + checks-reporter-with-killswitch "Jest Unit Tests" \ - node scripts/jest --config jest.config.oss.js --ci --verbose --maxWorkers=5 + node scripts/jest --ci --verbose --maxWorkers=8 diff --git a/test/scripts/test/xpack_jest_unit.sh b/test/scripts/test/xpack_jest_unit.sh deleted file mode 100755 index 33b1c8a2b5183..0000000000000 --- a/test/scripts/test/xpack_jest_unit.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash - -source src/dev/ci_setup/setup_env.sh - -checks-reporter-with-killswitch "X-Pack Jest" \ - node scripts/jest x-pack --ci --verbose --maxWorkers=5 diff --git a/vars/kibanaCoverage.groovy b/vars/kibanaCoverage.groovy index 609d8f78aeb96..e393f3a5d2150 100644 --- a/vars/kibanaCoverage.groovy +++ b/vars/kibanaCoverage.groovy @@ -197,13 +197,6 @@ def ingest(jobName, buildNumber, buildUrl, timestamp, previousSha, teamAssignmen def runTests() { parallel([ 'kibana-intake-agent': workers.intake('kibana-intake', './test/scripts/jenkins_unit.sh'), - 'x-pack-intake-agent': { - withEnv([ - 'NODE_ENV=test' // Needed for jest tests only - ]) { - workers.intake('x-pack-intake', './test/scripts/jenkins_xpack.sh')() - } - }, 'kibana-oss-agent' : workers.functional( 'kibana-oss-tests', { kibanaPipeline.buildOss() }, diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index 17349f6b566dc..5efcea3edb9bb 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -186,20 +186,21 @@ def uploadCoverageArtifacts(prefix, pattern) { def withGcsArtifactUpload(workerName, closure) { def uploadPrefix = "kibana-ci-artifacts/jobs/${env.JOB_NAME}/${BUILD_NUMBER}/${workerName}" def ARTIFACT_PATTERNS = [ + 'target/junit/**/*', 'target/kibana-*', - 'target/test-metrics/*', + 'target/kibana-coverage/**/*', 'target/kibana-security-solution/**/*.png', - 'target/junit/**/*', + 'target/test-metrics/*', 'target/test-suites-ci-plan.json', - 'test/**/screenshots/session/*.png', - 'test/**/screenshots/failure/*.png', 'test/**/screenshots/diff/*.png', + 'test/**/screenshots/failure/*.png', + 'test/**/screenshots/session/*.png', 'test/functional/failure_debug/html/*.html', - 'x-pack/test/**/screenshots/session/*.png', - 'x-pack/test/**/screenshots/failure/*.png', 'x-pack/test/**/screenshots/diff/*.png', - 'x-pack/test/functional/failure_debug/html/*.html', + 'x-pack/test/**/screenshots/failure/*.png', + 'x-pack/test/**/screenshots/session/*.png', 'x-pack/test/functional/apps/reporting/reports/session/*.pdf', + 'x-pack/test/functional/failure_debug/html/*.html', ] withEnv([ @@ -462,15 +463,10 @@ def allCiTasks() { } }, jest: { - workers.ci(name: 'jest', size: 'c2-8', ramDisk: true) { + workers.ci(name: 'jest', size: 'n2-standard-16', ramDisk: false) { scriptTask('Jest Unit Tests', 'test/scripts/test/jest_unit.sh')() } }, - xpackJest: { - workers.ci(name: 'xpack-jest', size: 'c2-8', ramDisk: true) { - scriptTask('X-Pack Jest Unit Tests', 'test/scripts/test/xpack_jest_unit.sh')() - } - }, ]) } diff --git a/vars/workers.groovy b/vars/workers.groovy index e1684f7aadb43..5d3328bc8a3c4 100644 --- a/vars/workers.groovy +++ b/vars/workers.groovy @@ -19,8 +19,8 @@ def label(size) { return 'docker && tests-xl-highmem' case 'xxl': return 'docker && tests-xxl && gobld/machineType:custom-64-270336' - case 'c2-8': - return 'docker && linux && immutable && gobld/machineType:c2-standard-8' + case 'n2-standard-16': + return 'docker && linux && immutable && gobld/machineType:n2-standard-16' } error "unknown size '${size}'" diff --git a/x-pack/jest.config.js b/x-pack/jest.config.js deleted file mode 100644 index 231004359632b..0000000000000 --- a/x-pack/jest.config.js +++ /dev/null @@ -1,12 +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. - */ - -module.exports = { - preset: '@kbn/test', - rootDir: '..', - projects: ['/x-pack/plugins/*/jest.config.js'], -}; From 1f5d52ea2e3dd1aeb0292fdbe3a2af378fba923d Mon Sep 17 00:00:00 2001 From: Sandra Gonzales Date: Tue, 9 Feb 2021 13:28:46 -0500 Subject: [PATCH 27/28] [Metrics UI] Add ability for user to set anomaly threshold (#90313) * add anomaly threshold configuration to settings * hide panel if not ml capable * send threshold value to query * update license * update api integration to expect anomalyThreshold to exist * add some tests to the anomaly queries * change type to number * api validate in source update and config for anamolyThreshold and tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../results/metrics_hosts_anomalies.ts | 1 + .../infra_ml/results/metrics_k8s_anomalies.ts | 1 + .../infra/common/http_api/source_api.ts | 4 + .../components/expression_chart.test.tsx | 1 + .../indices_configuration_form_state.ts | 21 ++++- .../source_configuration/input_fields.tsx | 44 +++++++++- .../ml_configuration_panel.tsx | 85 +++++++++++++++++++ .../source_configuration_form_state.tsx | 3 + .../source_configuration_settings.tsx | 17 +++- .../components/timeline/timeline.tsx | 3 +- .../hooks/use_metrics_hosts_anomalies.ts | 8 +- .../hooks/use_metrics_k8s_anomalies.ts | 17 +++- .../public/utils/fixtures/metrics_explorer.ts | 1 + .../lib/infra_ml/metrics_hosts_anomalies.ts | 12 ++- .../lib/infra_ml/metrics_k8s_anomalies.ts | 12 ++- .../queries/metrics_host_anomalies.test.ts | 63 ++++++++++++++ .../queries/metrics_hosts_anomalies.ts | 24 ++++-- .../queries/metrics_k8s_anomalies.test.ts | 62 ++++++++++++++ .../infra_ml/queries/metrics_k8s_anomalies.ts | 24 ++++-- .../infra/server/lib/sources/defaults.ts | 1 + .../infra/server/lib/sources/errors.ts | 9 +- ..._new_indexing_strategy_index_names.test.ts | 1 + .../infra/server/lib/sources/sources.ts | 10 ++- .../results/metrics_hosts_anomalies.ts | 2 + .../infra_ml/results/metrics_k8s_anomalies.ts | 2 + .../infra/server/routes/source/index.ts | 10 +++ .../log_entries_search_strategy.test.ts | 1 + .../log_entry_search_strategy.test.ts | 1 + .../apis/metrics_ui/sources.ts | 29 ++++++- 29 files changed, 441 insertions(+), 28 deletions(-) create mode 100644 x-pack/plugins/infra/public/components/source_configuration/ml_configuration_panel.tsx create mode 100644 x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_host_anomalies.test.ts create mode 100644 x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.test.ts diff --git a/x-pack/plugins/infra/common/http_api/infra_ml/results/metrics_hosts_anomalies.ts b/x-pack/plugins/infra/common/http_api/infra_ml/results/metrics_hosts_anomalies.ts index 27574d01be898..0b70b65b7069e 100644 --- a/x-pack/plugins/infra/common/http_api/infra_ml/results/metrics_hosts_anomalies.ts +++ b/x-pack/plugins/infra/common/http_api/infra_ml/results/metrics_hosts_anomalies.ts @@ -62,6 +62,7 @@ export const getMetricsHostsAnomaliesRequestPayloadRT = rt.type({ rt.type({ // the ID of the source configuration sourceId: rt.string, + anomalyThreshold: rt.number, // the time range to fetch the log entry anomalies from timeRange: timeRangeRT, }), diff --git a/x-pack/plugins/infra/common/http_api/infra_ml/results/metrics_k8s_anomalies.ts b/x-pack/plugins/infra/common/http_api/infra_ml/results/metrics_k8s_anomalies.ts index 3c2615a447b07..3ee6189dcbf9a 100644 --- a/x-pack/plugins/infra/common/http_api/infra_ml/results/metrics_k8s_anomalies.ts +++ b/x-pack/plugins/infra/common/http_api/infra_ml/results/metrics_k8s_anomalies.ts @@ -62,6 +62,7 @@ export const getMetricsK8sAnomaliesRequestPayloadRT = rt.type({ rt.type({ // the ID of the source configuration sourceId: rt.string, + anomalyThreshold: rt.number, // the time range to fetch the log entry anomalies from timeRange: timeRangeRT, }), diff --git a/x-pack/plugins/infra/common/http_api/source_api.ts b/x-pack/plugins/infra/common/http_api/source_api.ts index 257383be859aa..f14151531ba35 100644 --- a/x-pack/plugins/infra/common/http_api/source_api.ts +++ b/x-pack/plugins/infra/common/http_api/source_api.ts @@ -90,6 +90,7 @@ export const SavedSourceConfigurationRuntimeType = rt.partial({ metricsExplorerDefaultView: rt.string, fields: SavedSourceConfigurationFieldsRuntimeType, logColumns: rt.array(SavedSourceConfigurationColumnRuntimeType), + anomalyThreshold: rt.number, }); export interface InfraSavedSourceConfiguration @@ -107,6 +108,7 @@ export const pickSavedSourceConfiguration = ( inventoryDefaultView, metricsExplorerDefaultView, logColumns, + anomalyThreshold, } = value; const { container, host, pod, tiebreaker, timestamp } = fields; @@ -119,6 +121,7 @@ export const pickSavedSourceConfiguration = ( metricsExplorerDefaultView, fields: { container, host, pod, tiebreaker, timestamp }, logColumns, + anomalyThreshold, }; }; @@ -140,6 +143,7 @@ export const StaticSourceConfigurationRuntimeType = rt.partial({ metricsExplorerDefaultView: rt.string, fields: StaticSourceConfigurationFieldsRuntimeType, logColumns: rt.array(SavedSourceConfigurationColumnRuntimeType), + anomalyThreshold: rt.number, }); export interface InfraStaticSourceConfiguration diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx index 939834fa7c4a8..7e4209e4253d7 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx @@ -64,6 +64,7 @@ describe('ExpressionChart', () => { pod: 'kubernetes.pod.uid', tiebreaker: '_doc', }, + anomalyThreshold: 20, }, }; diff --git a/x-pack/plugins/infra/public/components/source_configuration/indices_configuration_form_state.ts b/x-pack/plugins/infra/public/components/source_configuration/indices_configuration_form_state.ts index 794048a8b3a3a..b4dede79d11f2 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/indices_configuration_form_state.ts +++ b/x-pack/plugins/infra/public/components/source_configuration/indices_configuration_form_state.ts @@ -7,7 +7,11 @@ import { ReactNode, useCallback, useMemo, useState } from 'react'; -import { createInputFieldProps, validateInputFieldNotEmpty } from './input_fields'; +import { + createInputFieldProps, + createInputRangeFieldProps, + validateInputFieldNotEmpty, +} from './input_fields'; interface FormState { name: string; @@ -20,6 +24,7 @@ interface FormState { podField: string; tiebreakerField: string; timestampField: string; + anomalyThreshold: number; } type FormStateChanges = Partial; @@ -124,6 +129,17 @@ export const useIndicesConfigurationFormState = ({ }), [formState.timestampField] ); + const anomalyThresholdFieldProps = useMemo( + () => + createInputRangeFieldProps({ + errors: validateInputFieldNotEmpty(formState.anomalyThreshold), + name: 'anomalyThreshold', + onChange: (anomalyThreshold) => + setFormStateChanges((changes) => ({ ...changes, anomalyThreshold })), + value: formState.anomalyThreshold, + }), + [formState.anomalyThreshold] + ); const fieldProps = useMemo( () => ({ @@ -135,6 +151,7 @@ export const useIndicesConfigurationFormState = ({ podField: podFieldFieldProps, tiebreakerField: tiebreakerFieldFieldProps, timestampField: timestampFieldFieldProps, + anomalyThreshold: anomalyThresholdFieldProps, }), [ nameFieldProps, @@ -145,6 +162,7 @@ export const useIndicesConfigurationFormState = ({ podFieldFieldProps, tiebreakerFieldFieldProps, timestampFieldFieldProps, + anomalyThresholdFieldProps, ] ); @@ -183,4 +201,5 @@ const defaultFormState: FormState = { podField: '', tiebreakerField: '', timestampField: '', + anomalyThreshold: 0, }; diff --git a/x-pack/plugins/infra/public/components/source_configuration/input_fields.tsx b/x-pack/plugins/infra/public/components/source_configuration/input_fields.tsx index b8832d27a0a4d..a7a842417ebc2 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/input_fields.tsx +++ b/x-pack/plugins/infra/public/components/source_configuration/input_fields.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { ReactText } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -43,7 +43,47 @@ export const createInputFieldProps = < value, }); -export const validateInputFieldNotEmpty = (value: string) => +export interface InputRangeFieldProps< + Value extends ReactText = ReactText, + FieldElement extends HTMLInputElement = HTMLInputElement, + ButtonElement extends HTMLButtonElement = HTMLButtonElement +> { + error: React.ReactNode[]; + isInvalid: boolean; + name: string; + onChange?: ( + evt: React.ChangeEvent | React.MouseEvent, + isValid: boolean + ) => void; + value: Value; +} + +export const createInputRangeFieldProps = < + Value extends ReactText = ReactText, + FieldElement extends HTMLInputElement = HTMLInputElement, + ButtonElement extends HTMLButtonElement = HTMLButtonElement +>({ + errors, + name, + onChange, + value, +}: { + errors: FieldErrorMessage[]; + name: string; + onChange: (newValue: number, isValid: boolean) => void; + value: Value; +}): InputRangeFieldProps => ({ + error: errors, + isInvalid: errors.length > 0, + name, + onChange: ( + evt: React.ChangeEvent | React.MouseEvent, + isValid: boolean + ) => onChange(+evt.currentTarget.value, isValid), + value, +}); + +export const validateInputFieldNotEmpty = (value: React.ReactText) => value === '' ? [ { + return ( + + +

+ +

+
+ + + + + } + description={ + + } + > + + } + > + + + +
+ ); +}; diff --git a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_form_state.tsx b/x-pack/plugins/infra/public/components/source_configuration/source_configuration_form_state.tsx index 3f947bdb40677..c80235137eea6 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_form_state.tsx +++ b/x-pack/plugins/infra/public/components/source_configuration/source_configuration_form_state.tsx @@ -27,6 +27,7 @@ export const useSourceConfigurationFormState = (configuration?: InfraSourceConfi podField: configuration.fields.pod, tiebreakerField: configuration.fields.tiebreaker, timestampField: configuration.fields.timestamp, + anomalyThreshold: configuration.anomalyThreshold, } : undefined, [configuration] @@ -79,6 +80,7 @@ export const useSourceConfigurationFormState = (configuration?: InfraSourceConfi timestamp: indicesConfigurationFormState.formState.timestampField, }, logColumns: logColumnsConfigurationFormState.formState.logColumns, + anomalyThreshold: indicesConfigurationFormState.formState.anomalyThreshold, }), [indicesConfigurationFormState.formState, logColumnsConfigurationFormState.formState] ); @@ -97,6 +99,7 @@ export const useSourceConfigurationFormState = (configuration?: InfraSourceConfi timestamp: indicesConfigurationFormState.formStateChanges.timestampField, }, logColumns: logColumnsConfigurationFormState.formStateChanges.logColumns, + anomalyThreshold: indicesConfigurationFormState.formStateChanges.anomalyThreshold, }), [ indicesConfigurationFormState.formStateChanges, diff --git a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx b/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx index bdf4584bc6287..4b609a881bd18 100644 --- a/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx +++ b/x-pack/plugins/infra/public/components/source_configuration/source_configuration_settings.tsx @@ -26,6 +26,8 @@ import { NameConfigurationPanel } from './name_configuration_panel'; import { useSourceConfigurationFormState } from './source_configuration_form_state'; import { SourceLoadingPage } from '../source_loading_page'; import { Prompt } from '../../utils/navigation_warning_prompt'; +import { MLConfigurationPanel } from './ml_configuration_panel'; +import { useInfraMLCapabilitiesContext } from '../../containers/ml/infra_ml_capabilities'; interface SourceConfigurationSettingsProps { shouldAllowEdit: boolean; @@ -52,7 +54,6 @@ export const SourceConfigurationSettings = ({ formState, formStateChanges, } = useSourceConfigurationFormState(source && source.configuration); - const persistUpdates = useCallback(async () => { if (sourceExists) { await updateSourceConfiguration(formStateChanges); @@ -74,6 +75,8 @@ export const SourceConfigurationSettings = ({ source, ]); + const { hasInfraMLCapabilites } = useInfraMLCapabilitiesContext(); + if ((isLoading || isUninitialized) && !source) { return ; } @@ -125,6 +128,18 @@ export const SourceConfigurationSettings = ({ /> + {hasInfraMLCapabilites && ( + <> + + + + + + )} {errors.length > 0 ? ( <> diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx index a6a296f7d5725..0248241d616dc 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/timeline/timeline.tsx @@ -51,7 +51,7 @@ interface Props { } export const Timeline: React.FC = ({ interval, yAxisFormatter, isVisible }) => { - const { sourceId } = useSourceContext(); + const { sourceId, source } = useSourceContext(); const { metric, nodeType, accountId, region } = useWaffleOptionsContext(); const { currentTime, jumpToTime, stopAutoReload } = useWaffleTimeContext(); const { filterQueryAsJson } = useWaffleFiltersContext(); @@ -70,6 +70,7 @@ export const Timeline: React.FC = ({ interval, yAxisFormatter, isVisible const anomalyParams = { sourceId: 'default', + anomalyThreshold: source?.configuration.anomalyThreshold || 0, startTime, endTime, defaultSortOptions: { diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_metrics_hosts_anomalies.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_metrics_hosts_anomalies.ts index c3732fb22cb63..25afd05633fa5 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_metrics_hosts_anomalies.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_metrics_hosts_anomalies.ts @@ -138,6 +138,7 @@ export const useMetricsHostsAnomaliesResults = ({ endTime, startTime, sourceId, + anomalyThreshold, defaultSortOptions, defaultPaginationOptions, onGetMetricsHostsAnomaliesDatasetsError, @@ -146,6 +147,7 @@ export const useMetricsHostsAnomaliesResults = ({ endTime: number; startTime: number; sourceId: string; + anomalyThreshold: number; defaultSortOptions: Sort; defaultPaginationOptions: Pick; onGetMetricsHostsAnomaliesDatasetsError?: (error: Error) => void; @@ -182,6 +184,7 @@ export const useMetricsHostsAnomaliesResults = ({ return await callGetMetricHostsAnomaliesAPI( { sourceId, + anomalyThreshold, startTime: queryStartTime, endTime: queryEndTime, metric, @@ -215,6 +218,7 @@ export const useMetricsHostsAnomaliesResults = ({ }, [ sourceId, + anomalyThreshold, dispatch, reducerState.timeRange, reducerState.sortOptions, @@ -296,6 +300,7 @@ export const useMetricsHostsAnomaliesResults = ({ interface RequestArgs { sourceId: string; + anomalyThreshold: number; startTime: number; endTime: number; metric: Metric; @@ -307,13 +312,14 @@ export const callGetMetricHostsAnomaliesAPI = async ( requestArgs: RequestArgs, fetch: HttpHandler ) => { - const { sourceId, startTime, endTime, metric, sort, pagination } = requestArgs; + const { sourceId, anomalyThreshold, startTime, endTime, metric, sort, pagination } = requestArgs; const response = await fetch(INFA_ML_GET_METRICS_HOSTS_ANOMALIES_PATH, { method: 'POST', body: JSON.stringify( getMetricsHostsAnomaliesRequestPayloadRT.encode({ data: { sourceId, + anomalyThreshold, timeRange: { startTime, endTime, diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_metrics_k8s_anomalies.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_metrics_k8s_anomalies.ts index 2a8beeaa814fc..c135a2c5e6661 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_metrics_k8s_anomalies.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_metrics_k8s_anomalies.ts @@ -138,6 +138,7 @@ export const useMetricsK8sAnomaliesResults = ({ endTime, startTime, sourceId, + anomalyThreshold, defaultSortOptions, defaultPaginationOptions, onGetMetricsHostsAnomaliesDatasetsError, @@ -146,6 +147,7 @@ export const useMetricsK8sAnomaliesResults = ({ endTime: number; startTime: number; sourceId: string; + anomalyThreshold: number; defaultSortOptions: Sort; defaultPaginationOptions: Pick; onGetMetricsHostsAnomaliesDatasetsError?: (error: Error) => void; @@ -183,6 +185,7 @@ export const useMetricsK8sAnomaliesResults = ({ return await callGetMetricsK8sAnomaliesAPI( { sourceId, + anomalyThreshold, startTime: queryStartTime, endTime: queryEndTime, metric, @@ -217,6 +220,7 @@ export const useMetricsK8sAnomaliesResults = ({ }, [ sourceId, + anomalyThreshold, dispatch, reducerState.timeRange, reducerState.sortOptions, @@ -298,6 +302,7 @@ export const useMetricsK8sAnomaliesResults = ({ interface RequestArgs { sourceId: string; + anomalyThreshold: number; startTime: number; endTime: number; metric: Metric; @@ -310,13 +315,23 @@ export const callGetMetricsK8sAnomaliesAPI = async ( requestArgs: RequestArgs, fetch: HttpHandler ) => { - const { sourceId, startTime, endTime, metric, sort, pagination, datasets } = requestArgs; + const { + sourceId, + anomalyThreshold, + startTime, + endTime, + metric, + sort, + pagination, + datasets, + } = requestArgs; const response = await fetch(INFA_ML_GET_METRICS_K8S_ANOMALIES_PATH, { method: 'POST', body: JSON.stringify( getMetricsK8sAnomaliesRequestPayloadRT.encode({ data: { sourceId, + anomalyThreshold, timeRange: { startTime, endTime, diff --git a/x-pack/plugins/infra/public/utils/fixtures/metrics_explorer.ts b/x-pack/plugins/infra/public/utils/fixtures/metrics_explorer.ts index 6d8f9ae476044..27648b6d7b193 100644 --- a/x-pack/plugins/infra/public/utils/fixtures/metrics_explorer.ts +++ b/x-pack/plugins/infra/public/utils/fixtures/metrics_explorer.ts @@ -40,6 +40,7 @@ export const source = { message: ['message'], tiebreaker: '@timestamp', }, + anomalyThreshold: 20, }; export const chartOptions: MetricsExplorerChartOptions = { diff --git a/x-pack/plugins/infra/server/lib/infra_ml/metrics_hosts_anomalies.ts b/x-pack/plugins/infra/server/lib/infra_ml/metrics_hosts_anomalies.ts index 072f07dfaffdb..7873fd8e43a7b 100644 --- a/x-pack/plugins/infra/server/lib/infra_ml/metrics_hosts_anomalies.ts +++ b/x-pack/plugins/infra/server/lib/infra_ml/metrics_hosts_anomalies.ts @@ -76,6 +76,7 @@ async function getCompatibleAnomaliesJobIds( export async function getMetricsHostsAnomalies( context: InfraPluginRequestHandlerContext & { infra: Required }, sourceId: string, + anomalyThreshold: number, startTime: number, endTime: number, metric: 'memory_usage' | 'network_in' | 'network_out' | undefined, @@ -108,6 +109,7 @@ export async function getMetricsHostsAnomalies( timing: { spans: fetchLogEntryAnomaliesSpans }, } = await fetchMetricsHostsAnomalies( context.infra.mlSystem, + anomalyThreshold, jobIds, startTime, endTime, @@ -162,6 +164,7 @@ const parseAnomalyResult = (anomaly: MappedAnomalyHit, jobId: string) => { async function fetchMetricsHostsAnomalies( mlSystem: MlSystem, + anomalyThreshold: number, jobIds: string[], startTime: number, endTime: number, @@ -178,7 +181,14 @@ async function fetchMetricsHostsAnomalies( const results = decodeOrThrow(metricsHostsAnomaliesResponseRT)( await mlSystem.mlAnomalySearch( - createMetricsHostsAnomaliesQuery(jobIds, startTime, endTime, sort, expandedPagination), + createMetricsHostsAnomaliesQuery({ + jobIds, + anomalyThreshold, + startTime, + endTime, + sort, + pagination: expandedPagination, + }), jobIds ) ); diff --git a/x-pack/plugins/infra/server/lib/infra_ml/metrics_k8s_anomalies.ts b/x-pack/plugins/infra/server/lib/infra_ml/metrics_k8s_anomalies.ts index 44837d88ddb43..0c87b2f0f8b53 100644 --- a/x-pack/plugins/infra/server/lib/infra_ml/metrics_k8s_anomalies.ts +++ b/x-pack/plugins/infra/server/lib/infra_ml/metrics_k8s_anomalies.ts @@ -76,6 +76,7 @@ async function getCompatibleAnomaliesJobIds( export async function getMetricK8sAnomalies( context: InfraPluginRequestHandlerContext & { infra: Required }, sourceId: string, + anomalyThreshold: number, startTime: number, endTime: number, metric: 'memory_usage' | 'network_in' | 'network_out' | undefined, @@ -107,6 +108,7 @@ export async function getMetricK8sAnomalies( timing: { spans: fetchLogEntryAnomaliesSpans }, } = await fetchMetricK8sAnomalies( context.infra.mlSystem, + anomalyThreshold, jobIds, startTime, endTime, @@ -158,6 +160,7 @@ const parseAnomalyResult = (anomaly: MappedAnomalyHit, jobId: string) => { async function fetchMetricK8sAnomalies( mlSystem: MlSystem, + anomalyThreshold: number, jobIds: string[], startTime: number, endTime: number, @@ -174,7 +177,14 @@ async function fetchMetricK8sAnomalies( const results = decodeOrThrow(metricsK8sAnomaliesResponseRT)( await mlSystem.mlAnomalySearch( - createMetricsK8sAnomaliesQuery(jobIds, startTime, endTime, sort, expandedPagination), + createMetricsK8sAnomaliesQuery({ + jobIds, + anomalyThreshold, + startTime, + endTime, + sort, + pagination: expandedPagination, + }), jobIds ) ); diff --git a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_host_anomalies.test.ts b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_host_anomalies.test.ts new file mode 100644 index 0000000000000..4c3e0ca8bc26f --- /dev/null +++ b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_host_anomalies.test.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createMetricsHostsAnomaliesQuery } from './metrics_hosts_anomalies'; +import { Sort, Pagination } from '../../../../common/http_api/infra_ml'; + +describe('createMetricsHostAnomaliesQuery', () => { + const jobIds = ['kibana-metrics-ui-default-default-hosts_memory_usage']; + const anomalyThreshold = 30; + const startTime = 1612454527112; + const endTime = 1612541227112; + const sort: Sort = { field: 'anomalyScore', direction: 'desc' }; + const pagination: Pagination = { pageSize: 101 }; + + test('returns the correct query', () => { + expect( + createMetricsHostsAnomaliesQuery({ + jobIds, + anomalyThreshold, + startTime, + endTime, + sort, + pagination, + }) + ).toMatchObject({ + allowNoIndices: true, + ignoreUnavailable: true, + trackScores: false, + trackTotalHits: false, + body: { + query: { + bool: { + filter: [ + { terms: { job_id: ['kibana-metrics-ui-default-default-hosts_memory_usage'] } }, + { range: { record_score: { gte: 30 } } }, + { range: { timestamp: { gte: 1612454527112, lte: 1612541227112 } } }, + { terms: { result_type: ['record'] } }, + ], + }, + }, + sort: [{ record_score: 'desc' }, { _doc: 'desc' }], + size: 101, + _source: [ + 'job_id', + 'record_score', + 'typical', + 'actual', + 'partition_field_value', + 'timestamp', + 'bucket_span', + 'by_field_value', + 'host.name', + 'influencers.influencer_field_name', + 'influencers.influencer_field_values', + ], + }, + }); + }); +}); diff --git a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_hosts_anomalies.ts b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_hosts_anomalies.ts index 07b25931d838e..45587cd258e5d 100644 --- a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_hosts_anomalies.ts +++ b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_hosts_anomalies.ts @@ -25,19 +25,27 @@ const sortToMlFieldMap = { startTime: 'timestamp', }; -export const createMetricsHostsAnomaliesQuery = ( - jobIds: string[], - startTime: number, - endTime: number, - sort: Sort, - pagination: Pagination -) => { +export const createMetricsHostsAnomaliesQuery = ({ + jobIds, + anomalyThreshold, + startTime, + endTime, + sort, + pagination, +}: { + jobIds: string[]; + anomalyThreshold: number; + startTime: number; + endTime: number; + sort: Sort; + pagination: Pagination; +}) => { const { field } = sort; const { pageSize } = pagination; const filters = [ ...createJobIdsFilters(jobIds), - ...createAnomalyScoreFilter(50), + ...createAnomalyScoreFilter(anomalyThreshold), ...createTimeRangeFilters(startTime, endTime), ...createResultTypeFilters(['record']), ]; diff --git a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.test.ts b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.test.ts new file mode 100644 index 0000000000000..81dcb390dff56 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.test.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createMetricsK8sAnomaliesQuery } from './metrics_k8s_anomalies'; +import { Sort, Pagination } from '../../../../common/http_api/infra_ml'; + +describe('createMetricsK8sAnomaliesQuery', () => { + const jobIds = ['kibana-metrics-ui-default-default-k8s_memory_usage']; + const anomalyThreshold = 30; + const startTime = 1612454527112; + const endTime = 1612541227112; + const sort: Sort = { field: 'anomalyScore', direction: 'desc' }; + const pagination: Pagination = { pageSize: 101 }; + + test('returns the correct query', () => { + expect( + createMetricsK8sAnomaliesQuery({ + jobIds, + anomalyThreshold, + startTime, + endTime, + sort, + pagination, + }) + ).toMatchObject({ + allowNoIndices: true, + ignoreUnavailable: true, + trackScores: false, + trackTotalHits: false, + body: { + query: { + bool: { + filter: [ + { terms: { job_id: ['kibana-metrics-ui-default-default-k8s_memory_usage'] } }, + { range: { record_score: { gte: 30 } } }, + { range: { timestamp: { gte: 1612454527112, lte: 1612541227112 } } }, + { terms: { result_type: ['record'] } }, + ], + }, + }, + sort: [{ record_score: 'desc' }, { _doc: 'desc' }], + size: 101, + _source: [ + 'job_id', + 'record_score', + 'typical', + 'actual', + 'partition_field_value', + 'timestamp', + 'bucket_span', + 'by_field_value', + 'influencers.influencer_field_name', + 'influencers.influencer_field_values', + ], + }, + }); + }); +}); diff --git a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.ts b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.ts index 8a6e9396fb098..56a4b99e7236c 100644 --- a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.ts +++ b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.ts @@ -25,19 +25,27 @@ const sortToMlFieldMap = { startTime: 'timestamp', }; -export const createMetricsK8sAnomaliesQuery = ( - jobIds: string[], - startTime: number, - endTime: number, - sort: Sort, - pagination: Pagination -) => { +export const createMetricsK8sAnomaliesQuery = ({ + jobIds, + anomalyThreshold, + startTime, + endTime, + sort, + pagination, +}: { + jobIds: string[]; + anomalyThreshold: number; + startTime: number; + endTime: number; + sort: Sort; + pagination: Pagination; +}) => { const { field } = sort; const { pageSize } = pagination; const filters = [ ...createJobIdsFilters(jobIds), - ...createAnomalyScoreFilter(50), + ...createAnomalyScoreFilter(anomalyThreshold), ...createTimeRangeFilters(startTime, endTime), ...createResultTypeFilters(['record']), ]; diff --git a/x-pack/plugins/infra/server/lib/sources/defaults.ts b/x-pack/plugins/infra/server/lib/sources/defaults.ts index ce7c4410baca9..1b924619a905c 100644 --- a/x-pack/plugins/infra/server/lib/sources/defaults.ts +++ b/x-pack/plugins/infra/server/lib/sources/defaults.ts @@ -45,4 +45,5 @@ export const defaultSourceConfiguration: InfraSourceConfiguration = { }, }, ], + anomalyThreshold: 50, }; diff --git a/x-pack/plugins/infra/server/lib/sources/errors.ts b/x-pack/plugins/infra/server/lib/sources/errors.ts index fb0dc3b031511..082dfc611cc5b 100644 --- a/x-pack/plugins/infra/server/lib/sources/errors.ts +++ b/x-pack/plugins/infra/server/lib/sources/errors.ts @@ -4,10 +4,17 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +/* eslint-disable max-classes-per-file */ export class NotFoundError extends Error { constructor(message?: string) { super(message); Object.setPrototypeOf(this, new.target.prototype); } } + +export class AnomalyThresholdRangeError extends Error { + constructor(message?: string) { + super(message); + Object.setPrototypeOf(this, new.target.prototype); + } +} diff --git a/x-pack/plugins/infra/server/lib/sources/migrations/7_9_0_add_new_indexing_strategy_index_names.test.ts b/x-pack/plugins/infra/server/lib/sources/migrations/7_9_0_add_new_indexing_strategy_index_names.test.ts index 4cea6cbe32cfb..21b7643ca6a7f 100644 --- a/x-pack/plugins/infra/server/lib/sources/migrations/7_9_0_add_new_indexing_strategy_index_names.test.ts +++ b/x-pack/plugins/infra/server/lib/sources/migrations/7_9_0_add_new_indexing_strategy_index_names.test.ts @@ -126,6 +126,7 @@ const createTestSourceConfiguration = (logAlias: string, metricAlias: string) => ], logAlias, metricAlias, + anomalyThreshold: 20, }, id: 'TEST_ID', type: infraSourceConfigurationSavedObjectName, diff --git a/x-pack/plugins/infra/server/lib/sources/sources.ts b/x-pack/plugins/infra/server/lib/sources/sources.ts index aad877a077acf..fe005b04978da 100644 --- a/x-pack/plugins/infra/server/lib/sources/sources.ts +++ b/x-pack/plugins/infra/server/lib/sources/sources.ts @@ -10,9 +10,10 @@ import { failure } from 'io-ts/lib/PathReporter'; import { identity, constant } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; import { map, fold } from 'fp-ts/lib/Either'; +import { inRange } from 'lodash'; import { SavedObjectsClientContract } from 'src/core/server'; import { defaultSourceConfiguration } from './defaults'; -import { NotFoundError } from './errors'; +import { AnomalyThresholdRangeError, NotFoundError } from './errors'; import { infraSourceConfigurationSavedObjectName } from './saved_object_type'; import { InfraSavedSourceConfiguration, @@ -104,6 +105,9 @@ export class InfraSources { source: InfraSavedSourceConfiguration ) { const staticDefaultSourceConfiguration = await this.getStaticDefaultSourceConfiguration(); + const { anomalyThreshold } = source; + if (anomalyThreshold && !inRange(anomalyThreshold, 0, 101)) + throw new AnomalyThresholdRangeError('anomalyThreshold must be 1-100'); const newSourceConfiguration = mergeSourceConfiguration( staticDefaultSourceConfiguration, @@ -140,6 +144,10 @@ export class InfraSources { sourceProperties: InfraSavedSourceConfiguration ) { const staticDefaultSourceConfiguration = await this.getStaticDefaultSourceConfiguration(); + const { anomalyThreshold } = sourceProperties; + + if (anomalyThreshold && !inRange(anomalyThreshold, 0, 101)) + throw new AnomalyThresholdRangeError('anomalyThreshold must be 1-100'); const { configuration, version } = await this.getSourceConfiguration( savedObjectsClient, diff --git a/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_hosts_anomalies.ts b/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_hosts_anomalies.ts index 215ebf3280c03..8ec0b83994e1a 100644 --- a/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_hosts_anomalies.ts +++ b/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_hosts_anomalies.ts @@ -34,6 +34,7 @@ export const initGetHostsAnomaliesRoute = ({ framework }: InfraBackendLibs) => { const { data: { sourceId, + anomalyThreshold, timeRange: { startTime, endTime }, sort: sortParam, pagination: paginationParam, @@ -54,6 +55,7 @@ export const initGetHostsAnomaliesRoute = ({ framework }: InfraBackendLibs) => { } = await getMetricsHostsAnomalies( requestContext, sourceId, + anomalyThreshold, startTime, endTime, metric, diff --git a/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_k8s_anomalies.ts b/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_k8s_anomalies.ts index 906278be657d3..d41fa0ffafecc 100644 --- a/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_k8s_anomalies.ts +++ b/x-pack/plugins/infra/server/routes/infra_ml/results/metrics_k8s_anomalies.ts @@ -33,6 +33,7 @@ export const initGetK8sAnomaliesRoute = ({ framework }: InfraBackendLibs) => { const { data: { sourceId, + anomalyThreshold, timeRange: { startTime, endTime }, sort: sortParam, pagination: paginationParam, @@ -53,6 +54,7 @@ export const initGetK8sAnomaliesRoute = ({ framework }: InfraBackendLibs) => { } = await getMetricK8sAnomalies( requestContext, sourceId, + anomalyThreshold, startTime, endTime, metric, diff --git a/x-pack/plugins/infra/server/routes/source/index.ts b/x-pack/plugins/infra/server/routes/source/index.ts index f1132049bd03c..5c3827e56ce79 100644 --- a/x-pack/plugins/infra/server/routes/source/index.ts +++ b/x-pack/plugins/infra/server/routes/source/index.ts @@ -16,6 +16,7 @@ import { import { InfraBackendLibs } from '../../lib/infra_types'; import { hasData } from '../../lib/sources/has_data'; import { createSearchClient } from '../../lib/create_search_client'; +import { AnomalyThresholdRangeError } from '../../lib/sources/errors'; const typeToInfraIndexType = (value: string | undefined) => { switch (value) { @@ -137,6 +138,15 @@ export const initSourceRoute = (libs: InfraBackendLibs) => { throw error; } + if (error instanceof AnomalyThresholdRangeError) { + return response.customError({ + statusCode: 400, + body: { + message: error.message, + }, + }); + } + return response.customError({ statusCode: error.statusCode ?? 500, body: { diff --git a/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.test.ts b/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.test.ts index 16a45dc6489ee..bc4976a068f4d 100644 --- a/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.test.ts +++ b/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.test.ts @@ -279,6 +279,7 @@ const createSourceConfigurationMock = (): InfraSource => ({ timestamp: 'TIMESTAMP_FIELD', tiebreaker: 'TIEBREAKER_FIELD', }, + anomalyThreshold: 20, }, }); diff --git a/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.test.ts b/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.test.ts index 6bcc61f2be4a6..7ac8b71c04b2a 100644 --- a/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.test.ts +++ b/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.test.ts @@ -216,6 +216,7 @@ const createSourceConfigurationMock = () => ({ timestamp: 'TIMESTAMP_FIELD', tiebreaker: 'TIEBREAKER_FIELD', }, + anomalyThreshold: 20, }, }); diff --git a/x-pack/test/api_integration/apis/metrics_ui/sources.ts b/x-pack/test/api_integration/apis/metrics_ui/sources.ts index 7fb631477cb76..a5bab8de92f38 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/sources.ts +++ b/x-pack/test/api_integration/apis/metrics_ui/sources.ts @@ -17,11 +17,12 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const supertest = getService('supertest'); + const SOURCE_API_URL = '/api/metrics/source/default'; const patchRequest = async ( body: InfraSavedSourceConfiguration ): Promise => { const response = await supertest - .patch('/api/metrics/source/default') + .patch(SOURCE_API_URL) .set('kbn-xsrf', 'xxx') .send(body) .expect(200); @@ -73,6 +74,7 @@ export default function ({ getService }: FtrProviderContext) { expect(configuration?.fields.timestamp).to.be('@timestamp'); expect(configuration?.fields.container).to.be('container.id'); expect(configuration?.logColumns).to.have.length(3); + expect(configuration?.anomalyThreshold).to.be(50); expect(status?.logIndicesExist).to.be(true); expect(status?.metricIndicesExist).to.be(true); }); @@ -173,6 +175,31 @@ export default function ({ getService }: FtrProviderContext) { expect(fieldColumn).to.have.property('id', 'ADDED_COLUMN_ID'); expect(fieldColumn).to.have.property('field', 'ADDED_COLUMN_FIELD'); }); + it('validates anomalyThreshold is between range 1-100', async () => { + // create config with bad request + await supertest + .patch(SOURCE_API_URL) + .set('kbn-xsrf', 'xxx') + .send({ name: 'NAME', anomalyThreshold: -20 }) + .expect(400); + // create config with good request + await supertest + .patch(SOURCE_API_URL) + .set('kbn-xsrf', 'xxx') + .send({ name: 'NAME', anomalyThreshold: 20 }) + .expect(200); + + await supertest + .patch(SOURCE_API_URL) + .set('kbn-xsrf', 'xxx') + .send({ anomalyThreshold: -2 }) + .expect(400); + await supertest + .patch(SOURCE_API_URL) + .set('kbn-xsrf', 'xxx') + .send({ anomalyThreshold: 101 }) + .expect(400); + }); }); }); } From 5f8de693b9183d056bd9666309e6817331f3f017 Mon Sep 17 00:00:00 2001 From: ymao1 Date: Tue, 9 Feb 2021 14:07:53 -0500 Subject: [PATCH 28/28] [Alerting] Configurable number of hits for ES query alert (#90089) * Adding size parameter to ES query alert * Can't use const inside validation * Updating docs * Fixing functional test * License Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/user/alerting/alert-types.asciidoc | 3 +- .../alert-types-es-query-conditions.png | Bin 97147 -> 107595 bytes .../alert_types/es_query/expression.test.tsx | 3 + .../alert_types/es_query/expression.tsx | 21 ++- .../public/alert_types/es_query/types.ts | 1 + .../alert_types/es_query/validation.test.ts | 37 +++++ .../public/alert_types/es_query/validation.ts | 26 +++- .../es_query/action_context.test.ts | 2 + .../alert_types/es_query/alert_type.test.ts | 6 + .../server/alert_types/es_query/alert_type.ts | 14 +- .../es_query/alert_type_params.test.ts | 29 +++- .../alert_types/es_query/alert_type_params.ts | 3 + .../public/common/expression_items/index.ts | 1 + .../common/expression_items/value.test.tsx | 136 ++++++++++++++++++ .../public/common/expression_items/value.tsx | 102 +++++++++++++ .../builtin_alert_types/es_query/alert.ts | 6 + 16 files changed, 382 insertions(+), 8 deletions(-) create mode 100644 x-pack/plugins/triggers_actions_ui/public/common/expression_items/value.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/common/expression_items/value.tsx diff --git a/docs/user/alerting/alert-types.asciidoc b/docs/user/alerting/alert-types.asciidoc index 279739e95b522..016ecc3167298 100644 --- a/docs/user/alerting/alert-types.asciidoc +++ b/docs/user/alerting/alert-types.asciidoc @@ -130,12 +130,13 @@ image::images/alert-types-es-query-select.png[Choosing an ES query alert type] [float] ==== Defining the conditions -The ES query alert has 4 clauses that define the condition to detect. +The ES query alert has 5 clauses that define the condition to detect. [role="screenshot"] image::images/alert-types-es-query-conditions.png[Four 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*. +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 evaulated against the threshold condition. Aggregations are not supported at this time. Threshold:: This clause defines a threshold value and a comparison operator (`is above`, `is above or equals`, `is below`, `is below or equals`, or `is between`). The number of documents that match the specified query is compared to this threshold. diff --git a/docs/user/alerting/images/alert-types-es-query-conditions.png b/docs/user/alerting/images/alert-types-es-query-conditions.png index ce2bd6a42a4b5c2256121bf513e751988c7cb3e3..3cbba5eb4950e20ce45bb19c1fc3ba5bb0704626 100644 GIT binary patch literal 107595 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%0A;SKFnF$eJnoDw*SC4vu*ARKg;B@sQx#R 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 H%jf?Ea*#}v literal 97147 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
  • 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 { index: ['test-index'], timeField: '@timestamp', esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, thresholdComparator: '>', threshold: [0], timeWindowSize: 15, @@ -137,6 +138,7 @@ describe('EsQueryAlertTypeExpression', () => { const errors = { index: [], esQuery: [], + size: [], timeField: [], timeWindowSize: [], }; @@ -169,6 +171,7 @@ describe('EsQueryAlertTypeExpression', () => { test('should render EsQueryAlertTypeExpression with expected components', async () => { const wrapper = await setup(getAlertParams()); expect(wrapper.find('[data-test-subj="indexSelectPopover"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="sizeValueExpression"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="queryJsonEditor"]').exists()).toBeTruthy(); expect(wrapper.find('[data-test-subj="testQuerySuccess"]').exists()).toBeFalsy(); expect(wrapper.find('[data-test-subj="testQueryError"]').exists()).toBeFalsy(); diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.tsx b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.tsx index 27f8071564c55..37c64688ec49a 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/expression.tsx @@ -30,6 +30,7 @@ import { COMPARATORS, ThresholdExpression, ForLastExpression, + ValueExpression, AlertTypeParamsExpressionProps, } from '../../../../triggers_actions_ui/public'; import { validateExpression } from './validation'; @@ -45,6 +46,7 @@ const DEFAULT_VALUES = { "match_all" : {} } }`, + SIZE: 100, TIME_WINDOW_SIZE: 5, TIME_WINDOW_UNIT: 'm', THRESHOLD: [1000], @@ -53,6 +55,7 @@ const DEFAULT_VALUES = { const expressionFieldsWithValidation = [ 'index', 'esQuery', + 'size', 'timeField', 'threshold0', 'threshold1', @@ -74,6 +77,7 @@ export const EsQueryAlertTypeExpression: React.FunctionComponent< index, timeField, esQuery, + size, thresholdComparator, threshold, timeWindowSize, @@ -83,6 +87,7 @@ export const EsQueryAlertTypeExpression: React.FunctionComponent< const getDefaultParams = () => ({ ...alertParams, esQuery: esQuery ?? DEFAULT_VALUES.QUERY, + size: size ?? DEFAULT_VALUES.SIZE, timeWindowSize: timeWindowSize ?? DEFAULT_VALUES.TIME_WINDOW_SIZE, timeWindowUnit: timeWindowUnit ?? DEFAULT_VALUES.TIME_WINDOW_UNIT, threshold: threshold ?? DEFAULT_VALUES.THRESHOLD, @@ -214,7 +219,7 @@ export const EsQueryAlertTypeExpression: React.FunctionComponent<
    @@ -234,6 +239,7 @@ export const EsQueryAlertTypeExpression: React.FunctionComponent< ...alertParams, index: indices, esQuery: DEFAULT_VALUES.QUERY, + size: DEFAULT_VALUES.SIZE, thresholdComparator: DEFAULT_VALUES.THRESHOLD_COMPARATOR, timeWindowSize: DEFAULT_VALUES.TIME_WINDOW_SIZE, timeWindowUnit: DEFAULT_VALUES.TIME_WINDOW_UNIT, @@ -246,6 +252,19 @@ export const EsQueryAlertTypeExpression: React.FunctionComponent< }} onTimeFieldChange={(updatedTimeField: string) => setParam('timeField', updatedTimeField)} /> + { + setParam('size', updatedValue); + }} + />
    diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/types.ts b/x-pack/plugins/stack_alerts/public/alert_types/es_query/types.ts index a22af7a7bc8a5..af34b88ba28c5 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/types.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/types.ts @@ -17,6 +17,7 @@ export interface EsQueryAlertParams extends AlertTypeParams { index: string[]; timeField?: string; esQuery: string; + size: number; thresholdComparator?: string; threshold: number[]; timeWindowSize: number; diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.test.ts b/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.test.ts index 7d604e964fb9d..52278b4576557 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.test.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.test.ts @@ -13,6 +13,7 @@ describe('expression params validation', () => { const initialParams: EsQueryAlertParams = { index: [], esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, timeWindowSize: 1, timeWindowUnit: 's', threshold: [0], @@ -25,6 +26,7 @@ describe('expression params validation', () => { const initialParams: EsQueryAlertParams = { index: ['test'], esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, timeWindowSize: 1, timeWindowUnit: 's', threshold: [0], @@ -37,6 +39,7 @@ describe('expression params validation', () => { const initialParams: EsQueryAlertParams = { index: ['test'], esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n`, + size: 100, timeWindowSize: 1, timeWindowUnit: 's', threshold: [0], @@ -49,6 +52,7 @@ describe('expression params validation', () => { const initialParams: EsQueryAlertParams = { index: ['test'], esQuery: `{\n \"aggs\":{\n \"match_all\" : {}\n }\n}`, + size: 100, timeWindowSize: 1, timeWindowUnit: 's', threshold: [0], @@ -61,6 +65,7 @@ describe('expression params validation', () => { const initialParams: EsQueryAlertParams = { index: ['test'], esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, threshold: [], timeWindowSize: 1, timeWindowUnit: 's', @@ -74,6 +79,7 @@ describe('expression params validation', () => { const initialParams: EsQueryAlertParams = { index: ['test'], esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, threshold: [1], timeWindowSize: 1, timeWindowUnit: 's', @@ -87,6 +93,7 @@ describe('expression params validation', () => { const initialParams: EsQueryAlertParams = { index: ['test'], esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, threshold: [10, 1], timeWindowSize: 1, timeWindowUnit: 's', @@ -97,4 +104,34 @@ describe('expression params validation', () => { 'Threshold 1 must be > Threshold 0.' ); }); + + test('if size property is < 0 should return proper error message', () => { + const initialParams: EsQueryAlertParams = { + index: ['test'], + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n`, + size: -1, + timeWindowSize: 1, + timeWindowUnit: 's', + threshold: [0], + }; + expect(validateExpression(initialParams).errors.size.length).toBeGreaterThan(0); + expect(validateExpression(initialParams).errors.size[0]).toBe( + 'Size must be between 0 and 10,000.' + ); + }); + + test('if size property is > 10000 should return proper error message', () => { + const initialParams: EsQueryAlertParams = { + index: ['test'], + esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n`, + size: 25000, + timeWindowSize: 1, + timeWindowUnit: 's', + threshold: [0], + }; + expect(validateExpression(initialParams).errors.size.length).toBeGreaterThan(0); + expect(validateExpression(initialParams).errors.size[0]).toBe( + 'Size must be between 0 and 10,000.' + ); + }); }); diff --git a/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.ts b/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.ts index 8b402d63ae565..e6449dd4a6089 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.ts +++ b/x-pack/plugins/stack_alerts/public/alert_types/es_query/validation.ts @@ -10,12 +10,21 @@ import { EsQueryAlertParams } from './types'; import { ValidationResult, builtInComparators } from '../../../../triggers_actions_ui/public'; export const validateExpression = (alertParams: EsQueryAlertParams): ValidationResult => { - const { index, timeField, esQuery, threshold, timeWindowSize, thresholdComparator } = alertParams; + const { + index, + timeField, + esQuery, + size, + threshold, + timeWindowSize, + thresholdComparator, + } = alertParams; const validationResult = { errors: {} }; const errors = { index: new Array(), timeField: new Array(), esQuery: new Array(), + size: new Array(), threshold0: new Array(), threshold1: new Array(), thresholdComparator: new Array(), @@ -94,5 +103,20 @@ export const validateExpression = (alertParams: EsQueryAlertParams): ValidationR }) ); } + if (!size) { + errors.size.push( + i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.requiredSizeText', { + defaultMessage: 'Size is required.', + }) + ); + } + if ((size && size < 0) || size > 10000) { + errors.size.push( + i18n.translate('xpack.stackAlerts.esQuery.ui.validation.error.invalidSizeRangeText', { + defaultMessage: 'Size must be between 0 and {max, number}.', + values: { max: 10000 }, + }) + ); + } return validationResult; }; diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.test.ts index d4b2029c11579..9d4edd83a3913 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/action_context.test.ts @@ -14,6 +14,7 @@ describe('ActionContext', () => { index: ['[index]'], timeField: '[timeField]', esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, timeWindowSize: 5, timeWindowUnit: 'm', thresholdComparator: '>', @@ -41,6 +42,7 @@ describe('ActionContext', () => { index: ['[index]'], timeField: '[timeField]', esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, timeWindowSize: 5, timeWindowUnit: 'm', thresholdComparator: 'between', diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts index 2049f9f1153dd..c38dad5134373 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.test.ts @@ -57,6 +57,10 @@ describe('alertType', () => { "description": "The string representation of the ES query.", "name": "esQuery", }, + Object { + "description": "The number of hits to retrieve for each query.", + "name": "size", + }, Object { "description": "An array of values to use as the threshold; 'between' and 'notBetween' require two values, the others require one.", "name": "threshold", @@ -75,6 +79,7 @@ describe('alertType', () => { index: ['index-name'], timeField: 'time-field', esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, timeWindowSize: 5, timeWindowUnit: 'm', thresholdComparator: '<', @@ -92,6 +97,7 @@ describe('alertType', () => { index: ['index-name'], timeField: 'time-field', esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, timeWindowSize: 5, timeWindowUnit: 'm', thresholdComparator: 'between', diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts index 51c1fc4073d60..8fe988d95d72f 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type.ts @@ -23,8 +23,6 @@ import { ESSearchHit } from '../../../../../typings/elasticsearch'; export const ES_QUERY_ID = '.es-query'; -const DEFAULT_MAX_HITS_PER_EXECUTION = 1000; - const ActionGroupId = 'query matched'; const ConditionMetAlertInstanceId = 'query matched'; @@ -88,6 +86,13 @@ export function getAlertType( } ); + const actionVariableContextSizeLabel = i18n.translate( + 'xpack.stackAlerts.esQuery.actionVariableContextSizeLabel', + { + defaultMessage: 'The number of hits to retrieve for each query.', + } + ); + const actionVariableContextThresholdLabel = i18n.translate( 'xpack.stackAlerts.esQuery.actionVariableContextThresholdLabel', { @@ -130,6 +135,7 @@ export function getAlertType( params: [ { name: 'index', description: actionVariableContextIndexLabel }, { name: 'esQuery', description: actionVariableContextQueryLabel }, + { name: 'size', description: actionVariableContextSizeLabel }, { name: 'threshold', description: actionVariableContextThresholdLabel }, { name: 'thresholdComparator', description: actionVariableContextThresholdComparatorLabel }, ], @@ -160,7 +166,7 @@ export function getAlertType( } // During each alert execution, we run the configured query, get a hit count - // (hits.total) and retrieve up to DEFAULT_MAX_HITS_PER_EXECUTION hits. We + // (hits.total) and retrieve up to params.size hits. We // evaluate the threshold condition using the value of hits.total. If the threshold // condition is met, the hits are counted toward the query match and we update // the alert state with the timestamp of the latest hit. In the next execution @@ -200,7 +206,7 @@ export function getAlertType( from: dateStart, to: dateEnd, filter, - size: DEFAULT_MAX_HITS_PER_EXECUTION, + size: params.size, sortOrder: 'desc', searchAfterSortId: undefined, timeField: params.timeField, diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.test.ts index a1a697446ff65..ab3ca6a2d4c31 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.test.ts @@ -7,12 +7,17 @@ import { TypeOf } from '@kbn/config-schema'; import type { Writable } from '@kbn/utility-types'; -import { EsQueryAlertParamsSchema, EsQueryAlertParams } from './alert_type_params'; +import { + EsQueryAlertParamsSchema, + EsQueryAlertParams, + ES_QUERY_MAX_HITS_PER_EXECUTION, +} from './alert_type_params'; const DefaultParams: Writable> = { index: ['index-name'], timeField: 'time-field', esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, timeWindowSize: 5, timeWindowUnit: 'm', thresholdComparator: '>', @@ -99,6 +104,28 @@ describe('alertType Params validate()', () => { ); }); + it('fails for invalid size', async () => { + delete params.size; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[size]: expected value of type [number] but got [undefined]"` + ); + + params.size = 'foo'; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[size]: expected value of type [number] but got [string]"` + ); + + params.size = -1; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[size]: Value must be equal to or greater than [0]."` + ); + + params.size = ES_QUERY_MAX_HITS_PER_EXECUTION + 1; + expect(onValidate()).toThrowErrorMatchingInlineSnapshot( + `"[size]: Value must be equal to or lower than [10000]."` + ); + }); + it('fails for invalid timeWindowSize', async () => { delete params.timeWindowSize; expect(onValidate()).toThrowErrorMatchingInlineSnapshot( diff --git a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts index 24fed92776b53..23f314b521511 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/es_query/alert_type_params.ts @@ -11,6 +11,8 @@ import { ComparatorFnNames } from '../lib'; import { validateTimeWindowUnits } from '../../../../triggers_actions_ui/server'; import { AlertTypeState } from '../../../../alerts/server'; +export const ES_QUERY_MAX_HITS_PER_EXECUTION = 10000; + // alert type parameters export type EsQueryAlertParams = TypeOf; export interface EsQueryAlertState extends AlertTypeState { @@ -21,6 +23,7 @@ export const EsQueryAlertParamsSchemaProperties = { index: schema.arrayOf(schema.string({ minLength: 1 }), { minSize: 1 }), timeField: schema.string({ minLength: 1 }), esQuery: schema.string({ minLength: 1 }), + size: schema.number({ min: 0, max: ES_QUERY_MAX_HITS_PER_EXECUTION }), timeWindowSize: schema.number({ min: 1 }), timeWindowUnit: schema.string({ validate: validateTimeWindowUnits }), threshold: schema.arrayOf(schema.number(), { minSize: 1, maxSize: 2 }), diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/index.ts b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/index.ts index bfcbba28b4bda..f975375adcb07 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/index.ts @@ -10,3 +10,4 @@ export { OfExpression } from './of'; export { GroupByExpression } from './group_by_over'; export { ThresholdExpression } from './threshold'; export { ForLastExpression } from './for_the_last'; +export { ValueExpression } from './value'; diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/value.test.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/value.test.tsx new file mode 100644 index 0000000000000..e9a3dce84e149 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/value.test.tsx @@ -0,0 +1,136 @@ +/* + * 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 * as React from 'react'; +import { shallow } from 'enzyme'; +import { act } from 'react-dom/test-utils'; +import { ValueExpression } from './value'; +import { mountWithIntl, nextTick } from '@kbn/test/jest'; + +describe('value expression', () => { + it('renders description and value', () => { + const wrapper = shallow( + + ); + expect(wrapper.find('[data-test-subj="valueFieldTitle"]')).toMatchInlineSnapshot(` + + test + + `); + expect(wrapper.find('[data-test-subj="valueFieldNumberForm"]')).toMatchInlineSnapshot(` + + + + `); + }); + + it('renders errors', () => { + const wrapper = shallow( + + ); + expect(wrapper.find('[data-test-subj="valueFieldNumberForm"]')).toMatchInlineSnapshot(` + + + + `); + }); + + it('renders closed popover initially and opens on click', async () => { + const wrapper = mountWithIntl( + + ); + + expect(wrapper.find('[data-test-subj="valueExpression"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="valueFieldTitle"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="valueFieldNumber"]').exists()).toBeFalsy(); + + wrapper.find('[data-test-subj="valueExpression"]').first().simulate('click'); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="valueFieldTitle"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="valueFieldNumber"]').exists()).toBeTruthy(); + }); + + it('emits onChangeSelectedValue action when value is updated', async () => { + const onChangeSelectedValue = jest.fn(); + const wrapper = mountWithIntl( + + ); + + wrapper.find('[data-test-subj="valueExpression"]').first().simulate('click'); + await act(async () => { + await nextTick(); + wrapper.update(); + }); + wrapper + .find('input[data-test-subj="valueFieldNumber"]') + .simulate('change', { target: { value: 3000 } }); + expect(onChangeSelectedValue).toHaveBeenCalledWith(3000); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/common/expression_items/value.tsx b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/value.tsx new file mode 100644 index 0000000000000..cdf57136fe4b2 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/common/expression_items/value.tsx @@ -0,0 +1,102 @@ +/* + * 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, { useState } from 'react'; +import { + EuiExpression, + EuiPopover, + EuiFieldNumber, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, +} from '@elastic/eui'; +import { ClosablePopoverTitle } from './components'; +import { IErrorObject } from '../../types'; + +interface ValueExpressionProps { + description: string; + value: number; + onChangeSelectedValue: (updatedValue: number) => void; + popupPosition?: + | 'upCenter' + | 'upLeft' + | 'upRight' + | 'downCenter' + | 'downLeft' + | 'downRight' + | 'leftCenter' + | 'leftUp' + | 'leftDown' + | 'rightCenter' + | 'rightUp' + | 'rightDown'; + display?: 'fullWidth' | 'inline'; + errors: string | string[] | IErrorObject; +} + +export const ValueExpression = ({ + description, + value, + onChangeSelectedValue, + display = 'inline', + popupPosition, + errors, +}: ValueExpressionProps) => { + const [valuePopoverOpen, setValuePopoverOpen] = useState(false); + return ( + { + setValuePopoverOpen(true); + }} + /> + } + isOpen={valuePopoverOpen} + closePopover={() => { + setValuePopoverOpen(false); + }} + ownFocus + display={display === 'fullWidth' ? 'block' : 'inlineBlock'} + anchorPosition={popupPosition ?? 'downLeft'} + repositionOnScroll + > +
    + setValuePopoverOpen(false)} + > + <>{description} + + + + 0 && value !== undefined} + error={errors} + > + 0 && value !== undefined} + onChange={(e: any) => { + onChangeSelectedValue(e.target.value as number); + }} + /> + + + +
    +
    + ); +}; diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts index 30fd3aea2b2dc..777caacd465d8 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/builtin_alert_types/es_query/alert.ts @@ -68,6 +68,7 @@ export default function alertTests({ getService }: FtrProviderContext) { await createAlert({ name: 'never fire', esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, thresholdComparator: '<', threshold: [0], }); @@ -75,6 +76,7 @@ export default function alertTests({ getService }: FtrProviderContext) { await createAlert({ name: 'always fire', esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`, + size: 100, thresholdComparator: '>', threshold: [-1], }); @@ -123,6 +125,7 @@ export default function alertTests({ getService }: FtrProviderContext) { await createAlert({ name: 'never fire', esQuery: JSON.stringify(rangeQuery(ES_GROUPS_TO_WRITE * ALERT_INTERVALS_TO_WRITE + 1)), + size: 100, thresholdComparator: '>=', threshold: [0], }); @@ -132,6 +135,7 @@ export default function alertTests({ getService }: FtrProviderContext) { esQuery: JSON.stringify( rangeQuery(Math.floor((ES_GROUPS_TO_WRITE * ALERT_INTERVALS_TO_WRITE) / 2)) ), + size: 100, thresholdComparator: '>=', threshold: [0], }); @@ -173,6 +177,7 @@ export default function alertTests({ getService }: FtrProviderContext) { name: string; timeField?: string; esQuery: string; + size: number; thresholdComparator: string; threshold: number[]; timeWindowSize?: number; @@ -215,6 +220,7 @@ export default function alertTests({ getService }: FtrProviderContext) { index: [ES_TEST_INDEX_NAME], timeField: params.timeField || 'date', esQuery: params.esQuery, + size: params.size, timeWindowSize: params.timeWindowSize || ALERT_INTERVAL_SECONDS * 5, timeWindowUnit: 's', thresholdComparator: params.thresholdComparator,